Cloud features: part 1

This commit is contained in:
utkabobr
2025-04-06 05:38:54 +03:00
parent abf53f1c43
commit a27a8c1d5d
45 changed files with 2211 additions and 139 deletions
+3
View File
@@ -1,3 +1,6 @@
[submodule "EventBus"]
path = EventBus
url = https://github.com/utkabobr/EventBus
[submodule "SAPIL"]
path = SAPIL
url = https://github.com/utkabobr/SAPIL
Submodule
+1
Submodule SAPIL added at d0c6422d79
+8 -6
View File
@@ -6,14 +6,14 @@ def commit = getGitCommitHash(file('.'))
android {
namespace 'ru.ytkab0bp.slicebeam'
compileSdk 34
compileSdk 35
defaultConfig {
applicationId "ru.ytkab0bp.slicebeam"
minSdk 21
targetSdk 34
versionCode 6
versionName "0.2.0"
targetSdk 35
versionCode 8
versionName "0.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -90,12 +90,14 @@ dependencies {
implementation project(":eventbus")
implementation project(":eventbus_api")
annotationProcessor project(":eventbus_processor")
implementation 'com.github.instacart:truetime-android:3.5'
implementation project(":sapil")
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'com.github.instacart:truetime-android:4.0.0.alpha'
implementation 'com.github.mrudultora:Colorpicker:1.2.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.loopj.android:android-async-http:1.4.11'
implementation 'androidx.activity:activity:1.9.1'
implementation 'androidx.activity:activity:1.10.1'
}
+9 -1
View File
@@ -5,7 +5,15 @@
<uses-feature android:glEsVersion="0x00030000" android:required="true"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="23" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
<!-- WebView fails sometime if not queried, idk why -->
<package android:name="com.google.android.webview"/>
</queries>
<application
android:allowBackup="true"
@@ -45,6 +45,10 @@ public class BeamServerData {
return !BuildConfig.IS_GOOGLE_PLAY || Prefs.isRussianIP();
}
public static boolean isCloudAvailable() {
return isBoostyAvailable();
}
public static void load() {
client.get(DATA_URL, new AsyncHttpResponseHandler() {
@Override
@@ -48,10 +48,14 @@ import java.util.Objects;
import java.util.UUID;
import java.util.zip.ZipFile;
import ru.ytkab0bp.sapil.APICallback;
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
import ru.ytkab0bp.slicebeam.cloud.CloudController;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.ChangeLogBottomSheet;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.NeedDismissAIGeneratorMenu;
import ru.ytkab0bp.slicebeam.events.NeedDismissSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
@@ -69,9 +73,11 @@ import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.SnackbarsLayout;
public class MainActivity extends AppCompatActivity {
// Activity result
public final static int REQUEST_CODE_OPEN_FILE = 1, REQUEST_CODE_EXPORT_GCODE = 2,
REQUEST_CODE_IMPORT_PROFILES = 3, REQUEST_CODE_EXPORT_PROFILES = 4,
REQUEST_CODE_EXPORT_3MF = 5,
REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO = 6, REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO = 7;
private static MainActivity activeInstance;
@@ -79,6 +85,8 @@ public class MainActivity extends AppCompatActivity {
public static List<ConfigObject> EXPORTING_FILAMENTS;
public static List<ConfigObject> EXPORTING_PRINTERS;
public static File aiTempFile;
private static SparseArray<NavigationDelegate> liveDelegate = new SparseArray<>();
private static int lastId;
@@ -317,6 +325,23 @@ public class MainActivity extends AppCompatActivity {
.setPositiveButton(android.R.string.ok, null)
.show();
}
} else if (requestCode == REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO) {
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissAIGeneratorMenu());
Bitmap bm = BitmapFactory.decodeFile(aiTempFile.getAbsolutePath());
generateAiModel(bm);
aiTempFile.delete();
aiTempFile = null;
} else if (requestCode == REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO) {
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissAIGeneratorMenu());
try {
InputStream in = getContentResolver().openInputStream(data.getData());
Bitmap bm = BitmapFactory.decodeStream(in);
generateAiModel(bm);
} catch (Exception e) {
Log.e("ai_generator", "Failed to write to downloads", e);
}
}
}
}
@@ -462,21 +487,106 @@ public class MainActivity extends AppCompatActivity {
}
});
}
.show();
return;
private void generateAiModel(Bitmap bm) {
String uploadTag = UUID.randomUUID().toString();
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorUploading).tag(uploadTag));
IOUtils.IO_POOL.submit(()->{
Bitmap scaled;
if (bm.getWidth() > 1024 || bm.getHeight() > 1024) {
if (bm.getWidth() > bm.getHeight()) {
int w = 1024;
int h = (int) ((float) w * bm.getHeight() / bm.getWidth());
scaled = Bitmap.createScaledBitmap(bm, w, h, true);
} else {
int h = 1024;
int w = (int) ((float) h * bm.getWidth() / bm.getHeight());
scaled = Bitmap.createScaledBitmap(bm, w, h, true);
}
bm.recycle();
} else {
scaled = bm;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
scaled.compress(Bitmap.CompressFormat.PNG, 100, out);
scaled.recycle();
String processTag = UUID.randomUUID().toString();
CloudAPI.INSTANCE.modelsGenerate(Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP), "image/png", new APICallback<InputStream>() {
@Override
public void onResponse(InputStream in) {
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(processTag));
String downloadTag = UUID.randomUUID().toString();
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorDownloading).tag(downloadTag));
String fileName = "generated_" + UUID.randomUUID() + ".stl";
File f = new File(SliceBeam.getModelCacheDir(), fileName);
try {
loadIniForImport(resolver.openInputStream(uri));
} catch (FileNotFoundException e) {
new BeamAlertDialogBuilder(this)
.setTitle(R.string.MenuFileImportProfilesFailed)
FileOutputStream fos = new FileOutputStream(f);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "application/vnd.ms-pki.stl");
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
if (uri != null) {
try {
OutputStream out = getContentResolver().openOutputStream(uri);
byte[] buf = new byte[10240];
int c;
while ((c = in.read(buf)) != -1) {
out.write(buf, 0, c);
fos.write(buf, 0, c);
}
out.close();
} catch (IOException e) {
Log.e("ai_generator", "Failed to write to downloads", e);
}
}
} else {
File downloadsDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File file = new File(downloadsDirectory, fileName);
try {
FileOutputStream out = new FileOutputStream(file);
byte[] buf = new byte[10240];
int c;
while ((c = in.read(buf)) != -1) {
out.write(buf, 0, c);
fos.write(buf, 0, c);
}
out.close();
} catch (IOException e) {
Log.e("ai_generator", "Failed to write to downloads", e);
}
}
fos.close();
} catch (Exception e) {
Log.e("ai_generator", "Failed to write to downloads", e);
}
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(downloadTag));
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileAIGeneratorSavedAs, fileName));
loadFile(f, true);
CloudController.checkGeneratorRemaining();
}
@Override
public void onException(Exception e) {
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(processTag));
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(MainActivity.this)
.setTitle(R.string.MenuFileAIGeneratorError)
.setMessage(e.toString())
.setPositiveButton(android.R.string.ok, null)
.show();
}
}
.show());
}
});
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(uploadTag));
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorProcessing).tag(processTag));
});
}
private void loadIniForImport(InputStream in) {
@@ -1,29 +1,34 @@
package ru.ytkab0bp.slicebeam;
import static android.opengl.GLES30.*;
import static android.opengl.GLES30.GL_COLOR_BUFFER_BIT;
import static android.opengl.GLES30.GL_DEPTH_BUFFER_BIT;
import static android.opengl.GLES30.GL_DEPTH_TEST;
import static android.opengl.GLES30.glClear;
import static android.opengl.GLES30.glClearColor;
import static android.opengl.GLES30.glDisable;
import static android.opengl.GLES30.glEnable;
import static android.opengl.GLES30.glViewport;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.text.style.ReplacementSpan;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
@@ -45,6 +50,7 @@ import androidx.core.view.ViewCompat;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
@@ -82,10 +88,15 @@ import javax.microedition.khronos.opengles.GL10;
import cz.msebera.android.httpclient.Header;
import ru.ytkab0bp.eventbus.EventHandler;
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
import ru.ytkab0bp.slicebeam.cloud.CloudController;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.CloudManageBottomSheet;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.BeamServerDataUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.CloudLoginStateUpdatedEvent;
import ru.ytkab0bp.slicebeam.recycler.BigHeaderItem;
import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerAdapter;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.recycler.TextHintRecyclerItem;
@@ -108,6 +119,7 @@ import ru.ytkab0bp.slicebeam.view.TextColorImageSpan;
public class SetupActivity extends AppCompatActivity {
public final static String EXTRA_ABOUT = "about";
public final static String EXTRA_BOOSTY_ONLY = "boosty_only";
public final static String EXTRA_CLOUD_PROFILE = "cloud_profile";
private final static String TAG = "SetupActivity";
@@ -140,6 +152,7 @@ public class SetupActivity extends AppCompatActivity {
private List<ProfilesRepo> repos = new ArrayList<>();
private ReposItem reposItem;
private ProfilesItem profilesItem;
private CloudProfileItem cloudItem;
private boolean isReposLoaded;
private boolean limitRepoFragmentCount = true;
private boolean limitProfileFragmentCount = true;
@@ -149,6 +162,7 @@ public class SetupActivity extends AppCompatActivity {
private boolean isProfilesLoaded;
private boolean about;
private boolean boostyOnly;
private boolean cloudProfile;
private List<ConfigObject> enabledPrinters = new ArrayList<>();
@@ -166,8 +180,9 @@ public class SetupActivity extends AppCompatActivity {
about = getIntent().getBooleanExtra(EXTRA_ABOUT, false);
boostyOnly = getIntent().getBooleanExtra(EXTRA_BOOSTY_ONLY, false);
cloudProfile = getIntent().getBooleanExtra(EXTRA_CLOUD_PROFILE, false);
if (!about && !boostyOnly) {
if (!about && !boostyOnly && !cloudProfile) {
new BeamAlertDialogBuilder(this)
.setTitle(R.string.IntroEarlyAccess)
.setMessage(R.string.IntroEarlyAccessMessage)
@@ -175,11 +190,15 @@ public class SetupActivity extends AppCompatActivity {
.show();
}
if (boostyOnly || cloudProfile) {
backgroundProgress = 1f;
}
pager = new ViewPager2(this);
adapter = new SimpleRecyclerAdapter() {
@Override
public int getItemCount() {
return about || boostyOnly ? 1 : limitRepoFragmentCount ? REPOS_INDEX + 1 : limitProfileFragmentCount ? PROFILES_INDEX + 1 : super.getItemCount();
return about || boostyOnly || cloudProfile ? 1 : limitRepoFragmentCount ? REPOS_INDEX + 1 : limitProfileFragmentCount ? PROFILES_INDEX + 1 : super.getItemCount();
}
};
setItems();
@@ -210,7 +229,7 @@ public class SetupActivity extends AppCompatActivity {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (position == 0 && !boostyOnly) {
if (position == 0 && !boostyOnly && !cloudProfile) {
backgroundProgress = positionOffset;
} else {
backgroundProgress = 1f;
@@ -348,14 +367,7 @@ public class SetupActivity extends AppCompatActivity {
}
}
title.setTranslationY(ViewUtils.lerp(titleY, (ViewUtils.dp(52) - title.getHeight() * title.getScaleY()) / 2f, backgroundProgress));
float sc = ViewUtils.lerp(1, 22 / 32f, backgroundProgress);
title.setPivotX(title.getWidth() / 2f);
title.setPivotY(0);
title.setScaleX(sc);
title.setScaleY(sc);
int color = ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.textColorOnAccent), ThemesRepo.getColor(android.R.attr.colorAccent), backgroundProgress - boostyProgress);
title.setTextColor(color);
invalidateTitleY();
backgroundView.requestRender();
}
});
@@ -368,7 +380,7 @@ public class SetupActivity extends AppCompatActivity {
super.onSizeChanged(w, h, oldw, oldh);
titleY = h / 4;
title.setTranslationY(ViewUtils.lerp(titleY, title.getPaddingTop(), backgroundProgress));
invalidateTitleY();
}
};
fl.setClipChildren(false);
@@ -417,10 +429,13 @@ public class SetupActivity extends AppCompatActivity {
topColor = ColorUtils.blendARGB(bottomColor, ThemesRepo.getColor(R.attr.boostyColorTop), boostyProgress);
bottomColor = ColorUtils.blendARGB(bottomColor, ThemesRepo.getColor(R.attr.boostyColorBottom), boostyProgress);
}
if (cloudProfile) {
bottomColor = ColorUtils.blendARGB(bottomColor, topColor, 0.5f);
}
shader.setUniformColor("top_color", topColor);
shader.setUniformColor("bottom_color", bottomColor);
shader.setUniform("progress", backgroundProgress - (boostyProgress != 0 ? 1.2f : 0));
shader.setUniform("progress", backgroundProgress - (cloudProfile ? 1.4f : 0) - (boostyProgress != 0 ? 1.2f : 0));
shader.setUniform("time", time);
backgroundModel.render();
shader.stopUsing();
@@ -434,7 +449,7 @@ public class SetupActivity extends AppCompatActivity {
title = new TextView(this);
title.setGravity(Gravity.CENTER);
title.setTypeface(Typeface.DEFAULT_BOLD);
title.setText(R.string.AppName);
title.setText(cloudProfile ? R.string.SettingsCloudManageTitle : R.string.AppName);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 32);
title.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
title.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL));
@@ -459,6 +474,17 @@ public class SetupActivity extends AppCompatActivity {
}
}
private void invalidateTitleY() {
float sc = ViewUtils.lerp(1, 22 / 32f, backgroundProgress);
title.setPivotX(title.getWidth() / 2f);
title.setPivotY(0);
title.setScaleX(sc);
title.setScaleY(sc);
int color = ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.textColorOnAccent), ThemesRepo.getColor(android.R.attr.colorAccent), cloudProfile ? 0f : backgroundProgress - boostyProgress);
title.setTextColor(color);
title.setTranslationY(ViewUtils.lerp(titleY, (ViewUtils.dp(52) - title.getHeight() * title.getScaleY()) / 2f, backgroundProgress));
}
@Override
protected void onDestroy() {
super.onDestroy();
@@ -468,7 +494,7 @@ public class SetupActivity extends AppCompatActivity {
@EventHandler(runOnMainThread = true)
public void onDataUpdated(BeamServerDataUpdatedEvent e) {
if (!about) {
if (!about && !boostyOnly && !cloudProfile) {
boolean wasBoosty = BOOSTY_INDEX != -1;
if (wasBoosty != BeamServerData.isBoostyAvailable()) {
setItems();
@@ -476,8 +502,18 @@ public class SetupActivity extends AppCompatActivity {
}
}
@EventHandler(runOnMainThread = true)
public void onCloudAuthStateUpdated(CloudLoginStateUpdatedEvent e) {
if (cloudProfile) {
cloudItem.bindLoginButton(true);
cloudItem.bindFeatures();
}
}
private void setItems() {
if (boostyOnly) {
if (cloudProfile){
adapter.setItems(Collections.singletonList(cloudItem = new CloudProfileItem()));
} else if (boostyOnly) {
adapter.setItems(Collections.singletonList(new BoostyItem()));
} else if (about) {
adapter.setItems(Collections.singletonList(new AboutItem()));
@@ -613,6 +649,323 @@ public class SetupActivity extends AppCompatActivity {
fakeScroller.start();
}
private final class CloudProfileItem extends SimpleRecyclerItem<View> {
private FrameLayout buttonView;
private TextView buttonText;
private ProgressBar buttonProgress;
private RecyclerView recyclerView;
@Override
public View onCreateView(Context ctx) {
LinearLayout ll = new LinearLayout(ctx);
ll.setOrientation(LinearLayout.VERTICAL);
ll.setPadding(0, ViewUtils.dp(42), 0, 0);
TextView title = new TextView(ctx);
title.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
title.setText(R.string.SettingsCloudManageDescription);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
title.setGravity(Gravity.CENTER);
title.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
ll.addView(title);
FrameLayout fl = new FrameLayout(ctx);
recyclerView = new RecyclerView(ctx);
recyclerView.setLayoutManager(new LinearLayoutManager(ctx));
recyclerView.setAdapter(adapter = new SimpleRecyclerAdapter());
recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
fl.addView(recyclerView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
bindFeatures();
ll.addView(fl, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
TextView tosButton = new TextView(ctx);
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(ctx.getString(R.string.SettingsCloudManageTermsOfService)).append(" ");
Drawable dr = ContextCompat.getDrawable(ctx, R.drawable.external_link_outline_24);
int size = ViewUtils.dp(16);
dr.setBounds(0, 0, size, size);
sb.append("d", new TextColorImageSpan(dr, ViewUtils.dp(2f)), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
tosButton.setText(sb);
tosButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
tosButton.setTextColor(Color.WHITE);
tosButton.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
tosButton.setGravity(Gravity.CENTER);
tosButton.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8));
tosButton.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
tosButton.setOnClickListener(v -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://beam3d.ru/slicebeam_cloud_tos.html"))));
ll.addView(tosButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
leftMargin = rightMargin = ViewUtils.dp(16);
bottomMargin = ViewUtils.dp(8);
}});
buttonView = new FrameLayout(ctx);
buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(android.R.attr.colorAccent), 16));
buttonText = new TextView(ctx);
buttonText.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
buttonText.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
buttonText.setGravity(Gravity.CENTER);
buttonText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
buttonView.addView(buttonText, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
buttonProgress = new ProgressBar(ctx);
buttonProgress.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.textColorOnAccent)));
buttonView.addView(buttonProgress, new FrameLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28), Gravity.CENTER));
bindLoginButton(false);
ll.addView(buttonView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
leftMargin = rightMargin = ViewUtils.dp(16);
bottomMargin = ViewUtils.dp(16);
}});
ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return ll;
}
private void bindFeatures() {
List<SimpleRecyclerItem> items = new ArrayList<>();
if (CloudController.getUserFeatures() != null) {
for (CloudAPI.SubscriptionLevel lvl : CloudController.getUserFeatures().levels) {
items.add(new CloudSubscriptionLevel(lvl));
}
}
adapter.setItems(items);
}
private void bindLoginButton(boolean animate) {
boolean loggedIn = Prefs.getCloudAPIToken() != null;
boolean loading = !loggedIn && CloudController.isLoggingIn();
boolean wasLoading = buttonProgress.getTag() != null;
if (animate) {
if (wasLoading != loading) {
buttonProgress.setTag(loading ? 1 : null);
buttonProgress.animate().cancel();
buttonProgress.animate().scaleX(loading ? 1f : 0.4f).scaleY(loading ? 1f : 0.4f).alpha(loading ? 1f : 0f).setDuration(150).setInterpolator(ViewUtils.CUBIC_INTERPOLATOR).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (loading) {
buttonProgress.setVisibility(View.VISIBLE);
buttonProgress.setAlpha(0f);
buttonProgress.setScaleX(0.4f);
buttonProgress.setScaleY(0.4f);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (!loading) {
buttonProgress.setVisibility(View.GONE);
}
}
}).start();
buttonText.animate().cancel();
buttonText.animate().scaleX(!loading ? 1f : 0.4f).scaleY(!loading ? 1f : 0.4f).alpha(!loading ? 1f : 0f).setDuration(150).setInterpolator(ViewUtils.CUBIC_INTERPOLATOR).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (!loading) {
buttonText.setVisibility(View.VISIBLE);
buttonText.setAlpha(0f);
buttonText.setScaleX(0.4f);
buttonText.setScaleY(0.4f);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (loading) {
buttonText.setVisibility(View.GONE);
}
}
}).start();
}
} else {
buttonProgress.setTag(loading ? 1 : null);
buttonProgress.setVisibility(loading ? View.VISIBLE : View.GONE);
buttonText.setVisibility(loading ? View.GONE : View.VISIBLE);
}
buttonText.setText(loggedIn ? R.string.SettingsCloudManageButtonManage : R.string.SettingsCloudManageButtonLogIn);
buttonView.setOnClickListener(v-> {
if (loading) {
new BeamAlertDialogBuilder(v.getContext())
.setTitle(R.string.SettingsCloudManageButtonLogInCancelTitle)
.setMessage(R.string.SettingsCloudManageButtonLogInCancel)
.setNegativeButton(R.string.No, null)
.setPositiveButton(R.string.Yes, (dialog, which) -> CloudController.cancelLogin())
.show();
} else if (Prefs.getCloudAPIToken() != null) {
new CloudManageBottomSheet(v.getContext()).show();
} else {
CloudController.beginLogin();
}
});
}
}
private final static class CloudSubscriptionLevel extends SimpleRecyclerItem<CloudSubscriptionLevel.LevelHolderView> {
private CloudAPI.SubscriptionLevel level;
private CloudSubscriptionLevel(CloudAPI.SubscriptionLevel level) {
this.level = level;
}
@Override
public LevelHolderView onCreateView(Context ctx) {
return new LevelHolderView(ctx);
}
@Override
public void onBindView(LevelHolderView view) {
view.bind(this);
}
public final static class LevelHolderView extends LinearLayout implements IThemeView {
private ImageView icon;
private TextView title;
private TextView price;
private RecyclerView featuresLayout;
private SimpleRecyclerAdapter featuresAdapter;
public LevelHolderView(@NonNull Context context) {
super(context);
setOrientation(VERTICAL);
setPadding(0, ViewUtils.dp(16), 0, ViewUtils.dp(8));
LinearLayout inner = new LinearLayout(context);
inner.setOrientation(HORIZONTAL);
inner.setGravity(Gravity.CENTER_VERTICAL);
inner.setPadding(ViewUtils.dp(28), 0, ViewUtils.dp(28), 0);
addView(inner, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
bottomMargin = ViewUtils.dp(8);
}});
icon = new ImageView(context);
inner.addView(icon, new LayoutParams(ViewUtils.dp(26), ViewUtils.dp(26)));
title = new TextView(context);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
inner.addView(title, new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
leftMargin = ViewUtils.dp(12);
}});
price = new TextView(context);
price.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
price.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
inner.addView(price);
featuresLayout = new RecyclerView(context) {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return false;
}
@Override
protected boolean dispatchHoverEvent(MotionEvent event) {
return false;
}
};
featuresLayout.setLayoutManager(new LinearLayoutManager(context));
featuresLayout.setAdapter(featuresAdapter = new SimpleRecyclerAdapter());
addView(featuresLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
topMargin = ViewUtils.dp(3);
leftMargin = rightMargin = ViewUtils.dp(16);
bottomMargin = ViewUtils.dp(8);
}});
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
leftMargin = rightMargin = ViewUtils.dp(12);
topMargin = ViewUtils.dp(12);
}});
onApplyTheme();
}
public void bind(CloudSubscriptionLevel item) {
CloudAPI.SubscriptionLevel lvl = item.level;
title.setText(lvl.title);
price.setText(lvl.price);
if (lvl.level <= 0) {
icon.setImageResource(R.drawable.zero_ruble_outline_28);
price.setText(R.string.SettingsCloudManageFree);
} else if (lvl.level == 1) {
icon.setImageResource(R.drawable.stars_outline_28);
} else {
icon.setImageResource(R.drawable.cloud_plus_outline_28);
}
List<SimpleRecyclerItem> items = new ArrayList<>();
CloudAPI.UserFeatures features = CloudController.getUserFeatures();
CloudAPI.UserInfo info = CloudController.getUserInfo();
Context ctx = getContext();
if (features.syncRequiredLevel != -1 && lvl.level >= features.syncRequiredLevel) {
items.add(new PreferenceItem()
.setForceDark(true)
.setPaddings(ViewUtils.dp(8))
.setIcon(R.drawable.sync_outline_28)
.setTitle(ctx.getString(R.string.SettingsCloudManageFeatureCloudSync))
.setSubtitle(ctx.getString(R.string.SettingsCloudManageFeatureCloudSyncDescription)));
}
if (features.aiGeneratorRequiredLevel != -1 && lvl.level >= features.aiGeneratorRequiredLevel) {
items.add(new PreferenceItem()
.setForceDark(true)
.setPaddings(ViewUtils.dp(8))
.setIcon(R.drawable.brain_outline_28)
.setTitle(ctx.getString(R.string.SettingsCloudManageFeatureAIGenerator))
.setSubtitle(ctx.getString(R.string.SettingsCloudManageFeatureAIGeneratorDescription, features.aiGeneratorModelsPerMonth)));
}
if (lvl.level > 0) {
items.add(new PreferenceItem()
.setForceDark(true)
.setPaddings(ViewUtils.dp(8))
.setIcon(R.drawable.box_heart_outline_28)
.setTitle(ctx.getString(R.string.SettingsCloudManageFeatureFreeForAll))
.setSubtitle(ctx.getString(R.string.SettingsCloudManageFeatureFreeForAllDescription)));
}
featuresAdapter.setItems(items);
featuresLayout.setVisibility(items.isEmpty() ? View.GONE : View.VISIBLE);
boolean subscribed = lvl.level > 0 && info != null && lvl.level == info.currentLevel;
boolean allowSubscribe = lvl.level > 0 && (info == null || lvl.level > info.currentLevel);
if (subscribed) {
price.setText(R.string.SettingsCloudManageSubscribed);
}
price.setVisibility(allowSubscribe || subscribed ? View.VISIBLE : View.GONE);
setOnClickListener(v -> {
if (subscribed) {
v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(lvl.manageUrl)));
} else {
new BeamAlertDialogBuilder(getContext())
.setTitle(lvl.title)
.setMessage(R.string.SettingsCloudManageLevelRedirectMessage)
.setPositiveButton(android.R.string.ok, (dialog, which) -> v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(lvl.subscribeOrUpgradeUrl))))
.setNegativeButton(R.string.SettingsCloudManageLevelRedirectAlreadySubscribed, (dialog, which) -> v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(features.alreadySubscribedInfoUrl))))
.show();
}
});
setClickable(allowSubscribe || subscribed);
onApplyTheme();
}
@Override
public void onApplyTheme() {
int accent = ThemesRepo.getColor(android.R.attr.colorAccent);
if (ColorUtils.calculateLuminance(accent) >= 0.6f) {
accent = ColorUtils.blendARGB(accent, Color.BLACK, 0.075f);
}
boolean tooLight = ColorUtils.calculateLuminance(accent) >= 0.6f;
title.setTextColor(0xffffffff);
price.setTextColor(0xffffffff);
icon.setImageTintList(ColorStateList.valueOf(0xffffffff));
featuresLayout.setBackground(ViewUtils.createRipple(0, tooLight ? 0x33ffffff : 0x21ffffff, 24));
setBackground(ViewUtils.createRipple(0x21000000, ColorUtils.blendARGB(0xffffffff, accent, tooLight ? 0.9f : 0.75f), 32));
}
}
}
private final class AboutItem extends SimpleRecyclerItem<View> {
@Override
@@ -5,6 +5,8 @@ import android.app.Application;
import android.content.Intent;
import android.util.Log;
import com.instacart.truetime.time.TrueTimeImpl;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -26,6 +28,7 @@ import ru.ytkab0bp.slicebeam.boot.PrefsTask;
import ru.ytkab0bp.slicebeam.boot.PrintConfigWarmupTask;
import ru.ytkab0bp.slicebeam.boot.TrueTimeTask;
import ru.ytkab0bp.slicebeam.boot.VibrationUtilsTask;
import ru.ytkab0bp.slicebeam.cloud.CloudController;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.slic3r.ConfigOptionDef;
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
@@ -35,6 +38,7 @@ import ru.ytkab0bp.slicebeam.utils.Prefs;
public class SliceBeam extends Application {
public static SliceBeam INSTANCE;
public static EventBus EVENT_BUS = EventBus.newBus("main");
public static TrueTimeImpl TRUE_TIME;
public static Slic3rConfigWrapper CONFIG;
public static int CONFIG_UID = 0;
public static BeamServerData SERVER_DATA;
@@ -82,6 +86,7 @@ public class SliceBeam extends Application {
} catch (Exception e) {
Log.e("Config", "Failed to save config", e);
}
CloudController.notifyDataChanged();
}
public static File getModelCacheDir() {
@@ -1,22 +1,72 @@
package ru.ytkab0bp.slicebeam.boot;
import com.instacart.library.truetime.TrueTime;
import androidx.annotation.NonNull;
import java.io.IOException;
import com.instacart.truetime.TrueTimeEventListener;
import com.instacart.truetime.time.TrueTimeImpl;
import com.instacart.truetime.time.TrueTimeParameters;
import java.net.InetAddress;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import kotlinx.coroutines.Dispatchers;
import ru.ytkab0bp.slicebeam.SliceBeam;
public class TrueTimeTask extends BootTask {
public TrueTimeTask() {
super(() -> {
for (int i = 0; i < 2; i++) {
CountDownLatch latch = new CountDownLatch(1);
SliceBeam.TRUE_TIME = new TrueTimeImpl(new TrueTimeParameters.Builder().buildParams(), Dispatchers.getIO(), new TrueTimeEventListener() {
@Override
public void initialize(@NonNull TrueTimeParameters trueTimeParameters) {}
@Override
public void initializeSuccess(@NonNull long[] longs) {
latch.countDown();
}
@Override
public void initializeFailed(@NonNull Exception e) {}
@Override
public void nextInitializeIn(long l) {}
@Override
public void resolvedNtpHostToIPs(@NonNull String s, @NonNull List<? extends InetAddress> list) {}
@Override
public void lastSntpRequestAttempt(@NonNull InetAddress inetAddress) {}
@Override
public void sntpRequestFailed(@NonNull Exception e) {}
@Override
public void syncDispatcherException(@NonNull Throwable throwable) {}
@Override
public void sntpRequest(@NonNull InetAddress inetAddress) {}
@Override
public void sntpRequestSuccessful(@NonNull InetAddress inetAddress) {}
@Override
public void sntpRequestFailed(@NonNull InetAddress inetAddress, @NonNull Exception e) {}
@Override
public void storingTrueTime(@NonNull long[] longs) {}
@Override
public void returningTrueTime(@NonNull Date date) {}
@Override
public void returningDeviceTime() {}
});
SliceBeam.TRUE_TIME.sync();
try {
TrueTime.build().withNtpHost("1.ru.pool.ntp.org").withConnectionTimeout(300).initialize();
break;
} catch (IOException ignore) {
try {
Thread.sleep(100);
latch.await();
} catch (InterruptedException ignored) {}
}
}
});
onWorker();
nonCritical = true;
@@ -0,0 +1,268 @@
package ru.ytkab0bp.slicebeam.cloud;
import androidx.annotation.Nullable;
import com.google.gson.Gson;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ru.ytkab0bp.sapil.APICallback;
import ru.ytkab0bp.sapil.APILibrary;
import ru.ytkab0bp.sapil.APIRequestHandle;
import ru.ytkab0bp.sapil.APIRunner;
import ru.ytkab0bp.sapil.Arg;
import ru.ytkab0bp.sapil.Header;
import ru.ytkab0bp.sapil.Method;
import ru.ytkab0bp.sapil.RequestType;
import ru.ytkab0bp.slicebeam.BuildConfig;
import ru.ytkab0bp.slicebeam.utils.Prefs;
public interface CloudAPI extends APIRunner {
CloudAPI INSTANCE = APILibrary.newRunner(CloudAPI.class, new RunnerConfig() {
private final Map<String, String> headers = new HashMap<>();
@Override
public String getBaseURL() {
return "https://api.beam3d.ru/v1/";
}
@Override
public String getDefaultUserAgent() {
return "SliceBeam v" + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_CODE;
}
@Override
public Map<String, String> getDefaultHeaders() {
headers.clear();
if (Prefs.getCloudAPIToken() != null) {
headers.put("Authorization", "Bearer " + Prefs.getCloudAPIToken());
}
return headers;
}
});
/**
* Begins login flow, returns auth link
*/
@Method("login/begin")
APIRequestHandle loginBegin(APICallback<LoginData> callback);
/**
* Checks new login state by session id
*/
@Method("login/check")
void loginCheck(@Arg("sessionId") String sessionId, APICallback<LoginState> callback);
/**
* Cancels login flow
*/
@Method("login/cancel")
void loginCancel(@Arg("sessionId") String sessionId, APICallback<Boolean> callback);
/**
* Gets current user info
* <p>
* Requires authorization
*/
@Method("user/getInfo")
void userGetInfo(APICallback<UserInfo> callback);
/**
* Gets user features
*/
@Method("user/getFeatures")
void userGetFeatures(APICallback<UserFeatures> callback);
/**
* Fetches sync state
* <p>
* Requires authorization
*/
@Method("sync/getState")
void syncGetState(APICallback<SyncState> callback);
/**
* Uploads new data to the server
* <p>
* @param data New base64 encoded data
* <p>
* Requires authorization
*/
@Method("sync/upload")
void syncUpload(@Arg("data") String data, APICallback<SyncState> callback);
/**
* Downloads base64 data
* <p>
* Requires authorization
*/
@Method("sync/get")
void syncGet(APICallback<String> callback);
/**
* Generates 3D model from image
* <p>
* @param image Base64 encoded image
* <p>
* Requires authorization
*/
@Method(requestType = RequestType.POST, value = "models/generate")
void modelsGenerate(@Arg("") String image, @Header("Content-Type") String type, APICallback<InputStream> callback);
/**
* Gets remaining model generations count
* <p>
* Requires authorization
*/
@Method("models/getRemainingCount")
void modelsGetRemainingCount(APICallback<ModelsRemainingCount> callback);
/**
* Destroys token
* <p>
* Requires authorization
*/
@Method("logout")
void logout(APICallback<Boolean> callback);
final class LoginData {
/**
* Url that should be clicked by the user to authorize
*/
public String url;
/**
* Session identifier
*/
public String sessionId;
/**
* Time at which session should be considered expired if not logged in
*/
public long expiresAt;
}
final class LoginState {
/**
* If user is now logged in
*/
public boolean loggedIn;
/**
* Bearer token if auth was successful
*/
public String bearer;
}
final class UserFeatures {
/**
* Which level is required for data sync
*/
public int syncRequiredLevel;
/**
* Which level is required for AI model generator
*/
public int aiGeneratorRequiredLevel;
/**
* Models per month max
*/
public int aiGeneratorModelsPerMonth;
/**
* Url at which user should be redirected for info about how to restore a subscription
*/
public String alreadySubscribedInfoUrl;
/**
* List of subscription levels
*/
public List<SubscriptionLevel> levels = new ArrayList<>();
}
final class SubscriptionLevel {
/**
* Int representation
*/
public int level;
/**
* Title of this level
*/
public String title;
/**
* Price of this level
*/
public String price;
/**
* Url at which user should be redirected for purchase
*/
public String subscribeOrUpgradeUrl;
/**
* Url at which user should be redirected for managing the subscription
*/
public String manageUrl;
}
final class UserInfo {
/**
* User's id
*/
public String id;
/**
* User's display name
*/
public String displayName;
/**
* User's avatar. Could be null
*/
@Nullable
public String avatarUrl;
/**
* Current subscription level
*/
public int currentLevel;
}
final class SyncState {
/**
* Cloud data last updated time
*/
public long lastUpdatedDate = 0;
/**
* Used size of cloud storage
*/
public long usedSize;
/**
* Max storage size
*/
public long maxSize;
}
final class ModelsRemainingCount {
/**
* Used generations
*/
public int used;
/**
* Max available generations
*/
public int max;
}
}
@@ -0,0 +1,299 @@
package ru.ytkab0bp.slicebeam.cloud;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import com.google.gson.Gson;
import ru.ytkab0bp.sapil.APICallback;
import ru.ytkab0bp.sapil.APIRequestHandle;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.events.CloudFeaturesUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.CloudLoginStateUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.CloudModelsRemainingCountUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.CloudUserInfoUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.NeedDismissSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.SnackbarsLayout;
public class CloudController {
public final static String USER_INFO_AI_GEN_TAG = "ai_gen_user_info";
private final static String CLOUD_SYNC_TAG = "cloud_sync";
private final static String TAG = "cloud";
private final static long MIN_SYNC_DELTA = 5 * 60 * 1000L; // Once in 5 minutes
private final static long MIN_SYNC_FEATURES_DELTA = 12 * 60 * 60 * 1000L; // Once in 12 hours
private static boolean isSyncInProgress;
private static CloudAPI.UserInfo userInfo;
private static CloudAPI.UserFeatures userFeatures;
private static int modelsUsed;
private static int modelsMaxGenerations;
private static boolean isLoggingIn;
private static APIRequestHandle beginLoginHandle;
private static String loginSessionId;
private static Runnable loginAutoCancel = () -> {
loginSessionId = null;
isLoggingIn = false;
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
};
private static Runnable loginCheck = new Runnable() {
@Override
public void run() {
CloudAPI.INSTANCE.loginCheck(loginSessionId, new APICallback<CloudAPI.LoginState>() {
@Override
public void onResponse(CloudAPI.LoginState response) {
if (response.loggedIn) {
Prefs.setCloudAPIToken(response.bearer);
loadUserInfo();
ViewUtils.removeCallbacks(loginAutoCancel);
} else if (isLoggingIn) {
ViewUtils.postOnMainThread(loginCheck, 5000);
}
}
@Override
public void onException(Exception e) {
Log.e(TAG, "Failed to check login state", e);
if (isLoggingIn) {
ViewUtils.postOnMainThread(loginCheck, 5000);
}
}
});
}
};
private static Gson gson = new Gson();
public static void init() {
if (Prefs.getCloudCachedUserFeatures() != null) {
userFeatures = gson.fromJson(Prefs.getCloudCachedUserFeatures(), CloudAPI.UserFeatures.class);
SliceBeam.EVENT_BUS.fireEvent(new CloudFeaturesUpdatedEvent());
}
long now = SliceBeam.TRUE_TIME.now().getTime();
boolean needSyncInfo = userFeatures == null || now - Prefs.getCloudLastFeaturesSync() > MIN_SYNC_FEATURES_DELTA;
if (needSyncInfo) {
checkUserFeatures();
}
if (Prefs.getCloudAPIToken() != null) {
if (Prefs.getCloudCachedUserInfo() != null) {
userInfo = gson.fromJson(Prefs.getCloudCachedUserInfo(), CloudAPI.UserInfo.class);
modelsUsed = Prefs.getCloudCachedUsedModels();
modelsMaxGenerations = Prefs.getCloudCachedMaxModels();
}
if (needSyncInfo || userInfo == null) {
loadUserInfo();
}
}
}
private static void loadUserInfo() {
CloudAPI.INSTANCE.userGetInfo(new APICallback<CloudAPI.UserInfo>() {
@Override
public void onResponse(CloudAPI.UserInfo response) {
userInfo = response;
if (userInfo.id.equals("null")) {
userInfo = null;
Prefs.setCloudAPIToken(null);
Prefs.setCloudCachedUserInfo(null);
SliceBeam.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
if (isLoggingIn) {
isLoggingIn = false;
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
}
} else {
Prefs.setCloudCachedUserInfo(gson.toJson(userInfo));
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(USER_INFO_AI_GEN_TAG));
SliceBeam.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
if (isLoggingIn) {
isLoggingIn = false;
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
}
if (isSyncAvailable() && Prefs.isCloudProfileSyncEnabled()) {
long now = SliceBeam.TRUE_TIME.now().getTime();
if (now != Prefs.getLocalLastModified()) {
sendData();
}
}
checkGeneratorRemaining();
}
Prefs.setCloudLastFeaturesSync(SliceBeam.TRUE_TIME.now().getTime());
}
@Override
public void onException(Exception e) {
Log.e(TAG, "Failed to get user info", e);
ViewUtils.postOnMainThread(CloudController::init, 15000);
}
});
}
public static boolean isLoggingIn() {
return isLoggingIn;
}
private static void beginLogin0() {
beginLoginHandle = CloudAPI.INSTANCE.loginBegin(new APICallback<CloudAPI.LoginData>() {
@Override
public void onResponse(CloudAPI.LoginData response) {
loginSessionId = response.sessionId;
ViewUtils.postOnMainThread(loginAutoCancel, response.expiresAt * 1000L - SliceBeam.TRUE_TIME.now().getTime());
ViewUtils.postOnMainThread(loginCheck, 5000);
ViewUtils.postOnMainThread(() -> SliceBeam.INSTANCE.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(response.url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)));
}
@Override
public void onException(Exception e) {
ViewUtils.postOnMainThread(CloudController::beginLogin0, 15000);
}
});
}
public static void beginLogin() {
isLoggingIn = true;
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
beginLogin0();
}
public static void cancelLogin() {
isLoggingIn = false;
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
if (loginSessionId != null) {
CloudAPI.INSTANCE.loginCancel(loginSessionId, response -> {});
}
if (beginLoginHandle != null && beginLoginHandle.isRunning()) {
beginLoginHandle.cancel();
beginLoginHandle = null;
}
ViewUtils.removeCallbacks(loginCheck);
ViewUtils.removeCallbacks(loginAutoCancel);
loginSessionId = null;
}
public static void logout() {
Prefs.setCloudAPIToken(null);
userInfo = null;
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
SliceBeam.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
CloudAPI.INSTANCE.logout(response -> {});
}
public static void checkGeneratorRemaining() {
CloudAPI.INSTANCE.modelsGetRemainingCount(new APICallback<CloudAPI.ModelsRemainingCount>() {
@Override
public void onResponse(CloudAPI.ModelsRemainingCount response) {
modelsUsed = response.used;
modelsMaxGenerations = response.max;
Prefs.setCloudCachedUsedMaxModels(modelsUsed, modelsMaxGenerations);
SliceBeam.EVENT_BUS.fireEvent(new CloudModelsRemainingCountUpdatedEvent());
}
@Override
public void onException(Exception e) {
Log.e(TAG, "Failed to check remaining models", e);
ViewUtils.postOnMainThread(CloudController::checkGeneratorRemaining, 15000);
}
});
}
public static void checkUserFeatures() {
CloudAPI.INSTANCE.userGetFeatures(new APICallback<CloudAPI.UserFeatures>() {
@Override
public void onResponse(CloudAPI.UserFeatures response) {
userFeatures = response;
Prefs.setCloudCachedUserFeatures(gson.toJson(userFeatures));
if (Prefs.getCloudAPIToken() == null) {
Prefs.setCloudLastFeaturesSync(SliceBeam.TRUE_TIME.now().getTime());
}
SliceBeam.EVENT_BUS.fireEvent(new CloudFeaturesUpdatedEvent());
}
@Override
public void onException(Exception e) {
Log.e(TAG, "Failed to get user features", e);
ViewUtils.postOnMainThread(CloudController::checkUserFeatures, 15000);
}
});
}
public static CloudAPI.UserInfo getUserInfo() {
return userInfo;
}
public static CloudAPI.UserFeatures getUserFeatures() {
return userFeatures;
}
public static boolean isSyncAvailable() {
return Prefs.getCloudAPIToken() != null && userInfo != null && userFeatures != null && userInfo.currentLevel >= userFeatures.syncRequiredLevel;
}
public static boolean needShowAIGenerator() {
return userFeatures != null && userFeatures.aiGeneratorRequiredLevel >= 0;
}
public static int getGeneratedModels() {
return modelsUsed;
}
public static int getMaxGeneratedModels() {
return modelsMaxGenerations;
}
private static void sendData() {
if (isSyncInProgress) {
return;
}
// TODO: IMPORTANT: Check getState first, then show conflict info
long modified = Prefs.getLocalLastModified();
isSyncInProgress = true;
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.CloudSyncInProgress).tag(CLOUD_SYNC_TAG));
CloudAPI.INSTANCE.syncUpload("", new APICallback<CloudAPI.SyncState>() {
@Override
public void onResponse(CloudAPI.SyncState response) {
isSyncInProgress = false;
if (Prefs.getLocalLastModified() != modified) { // Re-send otherwise
sendData();
return;
}
Prefs.setCloudLastSync(modified);
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.CloudSyncSuccess));
}
@Override
public void onException(Exception e) {
Log.e(TAG, "Failed to upload sync data", e);
isSyncInProgress = false;
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError));
}
});
}
public static void notifyDataChanged() {
long now = SliceBeam.TRUE_TIME.now().getTime();
Prefs.setLocalLastModified(now);
if (!isSyncAvailable() || !Prefs.isCloudProfileSyncEnabled()) {
return;
}
if (now - Prefs.getCloudLastSync() > MIN_SYNC_DELTA) {
sendData();
}
}
}
@@ -0,0 +1,154 @@
package ru.ytkab0bp.slicebeam.components;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.text.SpannableStringBuilder;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Space;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.util.ArrayList;
import java.util.List;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
import ru.ytkab0bp.slicebeam.cloud.CloudController;
import ru.ytkab0bp.slicebeam.recycler.PreferenceSwitchItem;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerAdapter;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.TextColorImageSpan;
public class CloudManageBottomSheet extends BottomSheetDialog {
public CloudManageBottomSheet(@NonNull Context context) {
super(context);
LinearLayout ll = new LinearLayout(context);
ll.setOrientation(LinearLayout.VERTICAL);
GradientDrawable gd = new GradientDrawable();
gd.setCornerRadii(new float[] {
ViewUtils.dp(28), ViewUtils.dp(28),
ViewUtils.dp(28), ViewUtils.dp(28),
0, 0,
0, 0
});
gd.setColor(ThemesRepo.getColor(R.attr.dialogBackground));
ll.setBackground(gd);
ll.setPadding(0, ViewUtils.dp(12), 0, ViewUtils.dp(12));
TextView title = new TextView(context);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
title.setText(R.string.SettingsCloudManageButtonManage);
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
title.setGravity(Gravity.CENTER);
title.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
leftMargin = rightMargin = ViewUtils.dp(21);
}});
ll.addView(title);
TextView description = new TextView(context);
description.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
description.setText(context.getString(R.string.SettingsCloudManageLoggedInAs, CloudController.getUserInfo().displayName));
description.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
description.setGravity(Gravity.CENTER);
description.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
leftMargin = rightMargin = ViewUtils.dp(21);
topMargin = ViewUtils.dp(8);
}});
ll.addView(description);
int currentLevel = CloudController.getUserInfo().currentLevel;
CloudAPI.SubscriptionLevel lvl = null;
CloudAPI.UserFeatures features = CloudController.getUserFeatures();
for (CloudAPI.SubscriptionLevel level : features.levels) {
if (level.level != -1 && level.level <= currentLevel && (lvl == null || level.level > lvl.level)) {
lvl = level;
}
}
if (lvl != null) {
List<SimpleRecyclerItem> items = new ArrayList<>();
if (currentLevel >= features.syncRequiredLevel) {
items.add(new PreferenceSwitchItem()
.setIcon(R.drawable.sync_outline_28)
.setTitle(context.getString(R.string.SettingsCloudManageFeatureCloudSync))
.setValueProvider(Prefs::isCloudProfileSyncEnabled)
.setChangeListener((buttonView, isChecked) -> {
Prefs.setCloudProfileSyncEnabled(isChecked);
if (isChecked) {
CloudController.notifyDataChanged();
}
}));
}
if (!items.isEmpty()) {
RecyclerView recyclerView = new RecyclerView(context);
recyclerView.setLayoutManager(new LinearLayoutManager(context));
recyclerView.setBackground(ViewUtils.createRipple(0, ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10), 16));
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
adapter.setItems(items);
recyclerView.setAdapter(adapter);
ll.addView(recyclerView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
topMargin = ViewUtils.dp(12);
leftMargin = rightMargin = ViewUtils.dp(12);
}});
}
TextView manageButton = new TextView(context);
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(context.getString(R.string.SettingsCloudManageSubscription)).append(" ");
Drawable dr = ContextCompat.getDrawable(context, R.drawable.external_link_outline_24);
int size = ViewUtils.dp(16);
dr.setBounds(0, 0, size, size);
sb.append("d", new TextColorImageSpan(dr, ViewUtils.dp(2f)), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
manageButton.setText(sb);
manageButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
manageButton.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
manageButton.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
manageButton.setGravity(Gravity.CENTER);
manageButton.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8));
manageButton.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
CloudAPI.SubscriptionLevel finalLvl = lvl;
manageButton.setOnClickListener(v -> v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(finalLvl.manageUrl))));
ll.addView(manageButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{
leftMargin = rightMargin = ViewUtils.dp(16);
topMargin = bottomMargin = ViewUtils.dp(6);
}});
} else {
ll.addView(new Space(context), new LinearLayout.LayoutParams(0, ViewUtils.dp(16)));
}
TextView buttonView = new TextView(context);
buttonView.setText(R.string.SettingsCloudManageButtonLogOut);
buttonView.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
buttonView.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
buttonView.setGravity(Gravity.CENTER);
buttonView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(R.attr.textColorNegative), 16));
buttonView.setOnClickListener(v-> {
CloudController.logout();
dismiss();
});
ll.addView(buttonView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
leftMargin = rightMargin = ViewUtils.dp(16);
bottomMargin = ViewUtils.dp(4);
}});
setContentView(ll);
}
}
@@ -5,6 +5,10 @@ import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
@@ -16,16 +20,23 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.RandomUtils;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolderView> {
@@ -36,6 +47,7 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
public boolean isEnabled = true;
public boolean isChecked = false;
public boolean isCheckable = false;
public boolean isShiny = false;
public View.OnClickListener clickListener;
public CompoundButton.OnCheckedChangeListener checkedChangeListener;
@@ -61,6 +73,11 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
return this;
}
public BedMenuItem setShiny(boolean shiny) {
isShiny = shiny;
return this;
}
public BedMenuItem setSingleLine(boolean singleLine) {
isSingleLine = singleLine;
return this;
@@ -77,6 +94,9 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
}
public final static class BedMenuItemHolderView extends LinearLayout implements IThemeView {
private final static float IN_BOUND = 0.05f;
private final static float OUT_BOUND = 0.1f;
private ImageView icon;
private TextView title;
@@ -84,8 +104,13 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
private Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Path path = new Path();
private Path path2 = new Path();
private float checkedProgress;
private boolean enabled;
private boolean shiny;
private List<Sparkle> sparkles;
private long lastDraw;
private Drawable sparkleDrawable;
public BedMenuItemHolderView(Context context) {
super(context);
@@ -109,12 +134,17 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT) {{
leftMargin = topMargin = bottomMargin = ViewUtils.dp(6);
}});
setClipToPadding(false);
setClipChildren(false);
setWillNotDraw(false);
onApplyTheme();
}
@Override
public void draw(@NonNull Canvas canvas) {
long dt = Math.min(System.currentTimeMillis() - lastDraw, 16);
lastDraw = System.currentTimeMillis();
int rad = ViewUtils.dp(16);
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), rad, rad, bgPaint);
@@ -133,6 +163,61 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
}
super.draw(canvas);
if (shiny) {
float side = Math.min(getWidth(), getHeight());
canvas.save();
if (sparkles == null) sparkles = new ArrayList<>();
if (sparkleDrawable == null) {
sparkleDrawable = ContextCompat.getDrawable(SliceBeam.INSTANCE, R.drawable.sparkle_28);
sparkleDrawable.setColorFilter(new PorterDuffColorFilter(ThemesRepo.getColor(android.R.attr.colorAccent), PorterDuff.Mode.SRC_IN));
}
float p = dt / 1000f;
for (Iterator<Sparkle> iterator = sparkles.iterator(); iterator.hasNext(); ) {
Sparkle sparkle = iterator.next();
sparkle.position.x += sparkle.velocity.x * p;
sparkle.position.y += sparkle.velocity.y * p;
sparkle.velocity.x *= 0.9999f;
sparkle.velocity.y *= 0.9999f;
sparkle.living += dt;
int size = (int) (side * sparkle.size);
float fadems = 200;
if ((sparkle.position.x - sparkle.size > 0 && sparkle.position.x + sparkle.size < 1f) &&
sparkle.lifetime - sparkle.living > fadems) {
sparkle.living = (long) (sparkle.lifetime - fadems);
}
if (sparkle.living >= sparkle.lifetime) {
iterator.remove();
} else {
float alpha = sparkle.living < fadems ? sparkle.living / fadems : sparkle.living > sparkle.lifetime - fadems ? (sparkle.lifetime - sparkle.living) / fadems : 1f;
canvas.saveLayerAlpha(-OUT_BOUND * side, -OUT_BOUND * side, getWidth() + OUT_BOUND * side, getHeight() + OUT_BOUND * side, (int) (alpha * sparkle.alpha * 0xFF));
canvas.translate(sparkle.position.x * side, sparkle.position.y * side);
sparkleDrawable.setBounds(-size / 2, -size / 2, size / 2, size / 2);
sparkleDrawable.draw(canvas);
canvas.restore();
}
}
if (sparkles.size() < 20) {
int s = 20 - sparkles.size();
for (int i = 0; i < s; i++) {
if (RandomUtils.RANDOM.nextFloat() < 0.01f) {
Sparkle sparkle = new Sparkle();
boolean leftSide = RandomUtils.RANDOM.nextBoolean();
sparkle.position = new PointF(leftSide ? RandomUtils.randomf(-OUT_BOUND, 0) : RandomUtils.randomf(1, 1 + OUT_BOUND), RandomUtils.randomf(-OUT_BOUND, 1 + OUT_BOUND));
sparkle.velocity = new PointF(RandomUtils.randomf(-0.05f, 0.05f), RandomUtils.randomf(-0.05f, 0.05f));
sparkle.size = RandomUtils.randomf(0.1f, 0.12f);
sparkle.alpha = RandomUtils.randomf(0.5f, 1f);
sparkle.lifetime = RandomUtils.randoml(4000, 10000);
sparkles.add(sparkle);
}
}
}
invalidate();
canvas.restore();
}
}
@Override
@@ -146,10 +231,12 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
public void bind(BedMenuItem item) {
enabled = item.isEnabled;
shiny = item.isShiny;
title.setMaxLines(item.isSingleLine ? 1 : 2);
title.setText(item.titleRes);
icon.setImageResource(item.iconRes);
checkedProgress = item.isCheckable && item.isChecked ? 1 : 0;
onApplyTheme();
title.setTextColor(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorPrimary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress));
icon.setImageTintList(ColorStateList.valueOf(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorSecondary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress)));
@@ -187,5 +274,14 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
bgPaint.setColor(ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10));
accentPaint.setColor(ThemesRepo.getColor(android.R.attr.colorAccent));
}
private final static class Sparkle {
private PointF position;
private PointF velocity;
private float size;
private float alpha;
private long lifetime;
private long living;
}
}
}
@@ -4,6 +4,7 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
@@ -14,12 +15,14 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import androidx.core.graphics.ColorUtils;
import androidx.recyclerview.widget.RecyclerView;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -30,14 +33,22 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ru.ytkab0bp.eventbus.EventHandler;
import ru.ytkab0bp.slicebeam.BeamServerData;
import ru.ytkab0bp.slicebeam.BuildConfig;
import ru.ytkab0bp.slicebeam.MainActivity;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SetupActivity;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.cloud.CloudController;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
import ru.ytkab0bp.slicebeam.components.WebViewMenu;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.CloudFeaturesUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.CloudModelsRemainingCountUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.NeedDismissAIGeneratorMenu;
import ru.ytkab0bp.slicebeam.events.NeedDismissCalibrationsMenu;
import ru.ytkab0bp.slicebeam.events.NeedDismissSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
import ru.ytkab0bp.slicebeam.events.SelectedObjectChangedEvent;
@@ -50,13 +61,18 @@ import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rRuntimeError;
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.Prefs;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
import ru.ytkab0bp.slicebeam.view.DividerView;
import ru.ytkab0bp.slicebeam.view.FadeRecyclerView;
import ru.ytkab0bp.slicebeam.view.SegmentsView;
import ru.ytkab0bp.slicebeam.view.SnackbarsLayout;
public class FileMenu extends ListBedMenu {
private final static List<String> K3D_SUPPORTED_LANGUAGES = Arrays.asList("en", "ru");
private boolean wasPortrait;
private String getK3DLanguage() {
String lang = Locale.getDefault().getLanguage();
return K3D_SUPPORTED_LANGUAGES.contains(lang) ? lang : "en";
@@ -74,13 +90,18 @@ public class FileMenu extends ListBedMenu {
.replace("\"", "\\\"");
}
private boolean hasModel() {
return fragment.getGlView().getRenderer().getModel() != null;
}
private boolean hasSelection() {
return fragment.getGlView().getRenderer().getModel() != null && fragment.getGlView().getRenderer().getSelectedObject() != -1;
return hasModel() && fragment.getGlView().getRenderer().getSelectedObject() != -1;
}
@Override
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
return Arrays.asList(
wasPortrait = portrait;
List<SimpleRecyclerItem> list = new ArrayList<>(Arrays.asList(
new BedMenuItem(R.string.MenuFileOpen, R.drawable.folder_simple_plus_outline_28).onClick(v -> {
if (!fragment.getGlView().getRenderer().getBed().isValid()) {
Toast.makeText(fragment.getContext(), R.string.BedConfigurationError, Toast.LENGTH_SHORT).show();
@@ -104,7 +125,31 @@ public class FileMenu extends ListBedMenu {
fragment.updateModel();
}
}),
new SpaceItem(portrait ? ViewUtils.dp(3) : 0, portrait ? 0 : ViewUtils.dp(3)),
new SpaceItem(portrait ? ViewUtils.dp(3) : 0, portrait ? 0 : ViewUtils.dp(3))));
if (BeamServerData.isBoostyAvailable() && CloudController.needShowAIGenerator()) {
list.add(new BedMenuItem(R.string.MenuFileAIGenerator, R.drawable.picture_stack_outline_28).setShiny(true).onClick(view -> {
if (Prefs.getCloudAPIToken() == null || CloudController.getUserInfo() != null && CloudController.getMaxGeneratedModels() == 0) {
Context ctx = view.getContext();
ctx.startActivity(new Intent(ctx, SetupActivity.class).putExtra(SetupActivity.EXTRA_CLOUD_PROFILE, true));
return;
}
if (CloudController.getUserInfo() == null) {
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorPleaseWaitSetup).tag(CloudController.USER_INFO_AI_GEN_TAG));
ViewUtils.postOnMainThread(() -> {
if (CloudController.getUserInfo() == null) {
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CloudController.USER_INFO_AI_GEN_TAG));
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorErrorNotLoadedUserAccount));
} else {
fragment.showUnfoldMenu(new AIGeneratorMenu(), view);
}
}, 2500);
return;
}
fragment.showUnfoldMenu(new AIGeneratorMenu(), view);
}));
}
list.addAll(Arrays.asList(
new BedMenuItem(R.string.MenuFileCalibrations, R.drawable.wrench_outline_28).setSingleLine(true).onClick(v -> {
if (!fragment.getGlView().getRenderer().getBed().isValid()) {
Toast.makeText(fragment.getContext(), R.string.BedConfigurationError, Toast.LENGTH_SHORT).show();
@@ -200,14 +245,28 @@ public class FileMenu extends ListBedMenu {
.show())
.setNegativeButton(android.R.string.cancel, null)
.show();
}),
new BedMenuItem(R.string.MenuFileExport3mf, R.drawable.arrow_down_to_square_outline_28).setEnabled(hasModel()).onClick(v -> {
if (fragment.getContext() instanceof Activity) {
Activity act = (Activity) fragment.getContext();
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
i.setType("application/3mf");
i.putExtra(Intent.EXTRA_TITLE, "SliceBeam_project.3mf");
act.startActivityForResult(i, MainActivity.REQUEST_CODE_EXPORT_3MF);
}
})
);
));
return list;
}
@EventHandler(runOnMainThread = true)
public void onObjectsChanged(ObjectsListChangedEvent e) {
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
adapter.notifyItemChanged(1);
int i = 8 - (BeamServerData.isBoostyAvailable() && CloudController.needShowAIGenerator() ? 0 : 1);
((BedMenuItem) adapter.getItems().get(i)).setEnabled(hasModel());
adapter.notifyItemChanged(i);
}
@EventHandler(runOnMainThread = true)
@@ -216,8 +275,144 @@ public class FileMenu extends ListBedMenu {
adapter.notifyItemChanged(1);
}
public final class CalibrationsMenu extends UnfoldMenu {
@EventHandler(runOnMainThread = true)
public void onFeaturedUpdated(CloudFeaturesUpdatedEvent e) {
adapter.setItems(onCreateItems(wasPortrait));
}
public final static class AIGeneratorMenu extends UnfoldMenu {
private TextView remainingView;
private SegmentsView segmentsView;
@Override
public int getRequestedSize(FrameLayout into, boolean portrait) {
return (int) (portrait ? ViewUtils.dp(52) + ViewUtils.dp(60) * 2 + ViewUtils.dp(28) + ViewUtils.dp(18) + ViewUtils.dp(2) : into.getWidth() * 0.6f);
}
@Override
protected View onCreateView(Context ctx, boolean portrait) {
LinearLayout ll = new LinearLayout(ctx);
ll.setOrientation(LinearLayout.VERTICAL);
RecyclerView rv = new FadeRecyclerView(ctx);
rv.setOverScrollMode(View.OVER_SCROLL_NEVER);
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
adapter.setItems(Arrays.asList(
new PreferenceItem().setIcon(R.drawable.camera_outline_28).setTitle(ctx.getString(R.string.MenuFileAIGeneratorFromCamera)).setOnClickListener(v -> {
if (CloudController.getGeneratedModels() >= CloudController.getMaxGeneratedModels()) {
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft));
return;
}
if (ctx instanceof MainActivity) {
try {
MainActivity.aiTempFile = File.createTempFile("ai_capture", ".jpg");
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
i.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(ctx, BuildConfig.APPLICATION_ID + ".provider", MainActivity.aiTempFile));
i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
((MainActivity) ctx).startActivityForResult(i, MainActivity.REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}),
new PreferenceItem().setIcon(R.drawable.picture_outline_28).setTitle(ctx.getString(R.string.MenuFileAIGeneratorFromGallery)).setOnClickListener(v -> {
if (CloudController.getGeneratedModels() >= CloudController.getMaxGeneratedModels()) {
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft));
return;
}
if (ctx instanceof MainActivity) {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
((MainActivity) ctx).startActivityForResult(Intent.createChooser(intent, ""), MainActivity.REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO);
}
})
));
rv.setAdapter(adapter);
ll.addView(rv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
remainingView = new TextView(ctx);
remainingView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13);
remainingView.setGravity(Gravity.CENTER);
remainingView.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
ll.addView(remainingView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(18)) {{
topMargin = ViewUtils.dp(8);
}});
segmentsView = new SegmentsView(ctx) {
@Override
protected int onGetColor(int i) {
return i == 1 ? ThemesRepo.getColor(android.R.attr.textColorSecondary) : ThemesRepo.getColor(android.R.attr.colorAccent);
}
};
ll.addView(segmentsView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(12)) {{
leftMargin = rightMargin = ViewUtils.dp(12);
topMargin = bottomMargin = ViewUtils.dp(8);
}});
updateRemaining();
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
LinearLayout toolbar = new LinearLayout(ctx);
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
toolbar.setOrientation(LinearLayout.HORIZONTAL);
toolbar.setGravity(Gravity.CENTER_VERTICAL);
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
toolbar.setOnClickListener(v -> dismiss());
ImageView icon = new ImageView(ctx);
icon.setImageResource(R.drawable.arrow_left_outline_28);
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
TextView title = new TextView(ctx);
title.setText(R.string.MenuOrientationPositionBack);
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
leftMargin = ViewUtils.dp(12);
}});
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
return ll;
}
@Override
protected void onCreate() {
super.onCreate();
SliceBeam.EVENT_BUS.registerListener(this);
ViewUtils.postOnMainThread(() -> segmentsView.startAnimation(), 50);
}
@EventHandler(runOnMainThread = true)
public void onDismiss(NeedDismissAIGeneratorMenu e) {
dismiss();
}
@EventHandler(runOnMainThread = true)
public void onRemainingUpdated(CloudModelsRemainingCountUpdatedEvent e) {
updateRemaining();
}
@Override
protected void onDestroy() {
super.onDestroy();
SliceBeam.EVENT_BUS.unregisterListener(this);
}
private void updateRemaining() {
int rev = CloudController.getMaxGeneratedModels() - CloudController.getGeneratedModels();
remainingView.setText(SliceBeam.INSTANCE.getString(R.string.MenuFileAIGeneratorRemaining, rev, CloudController.getMaxGeneratedModels()));
segmentsView.setValues(new float[]{0, rev / (float) CloudController.getMaxGeneratedModels(), 1});
}
}
public final class CalibrationsMenu extends UnfoldMenu {
@Override
public int getRequestedSize(FrameLayout into, boolean portrait) {
return (int) (portrait ? into.getHeight() * 0.35f : into.getWidth() * 0.6f);
}
@@ -32,6 +32,8 @@ public abstract class ListBedMenu extends BedMenu {
recyclerView = new RecyclerView(ctx);
recyclerView.setLayoutManager(new LinearLayoutManager(ctx, portrait ? RecyclerView.HORIZONTAL : RecyclerView.VERTICAL, false));
recyclerView.setItemAnimator(null);
recyclerView.setClipToPadding(false);
recyclerView.setClipChildren(false);
adapter = new SimpleRecyclerAdapter() {
@NonNull
@Override
@@ -0,0 +1,6 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class CloudFeaturesUpdatedEvent {}
@@ -0,0 +1,6 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class CloudLoginStateUpdatedEvent {}
@@ -0,0 +1,6 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class CloudModelsRemainingCountUpdatedEvent {}
@@ -0,0 +1,7 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class CloudUserInfoUpdatedEvent {
}
@@ -0,0 +1,6 @@
package ru.ytkab0bp.slicebeam.events;
import ru.ytkab0bp.eventbus.Event;
@Event
public class NeedDismissAIGeneratorMenu {}
@@ -15,9 +15,6 @@ import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
@@ -116,6 +113,7 @@ public class BedFragment extends Fragment {
private UnfoldMenu currentUnfoldMenu;
private BedSwipeDownLayout swipeDownLayout;
private boolean hasWebError;
private WebView panelWebView;
private LinearLayout panelWebViewError;
private ImageView webViewErrIcon;
@@ -229,7 +227,7 @@ public class BedFragment extends Fragment {
super.onResume();
glView.onResume();
ConfigObject cfg = SliceBeam.CONFIG.findPrinter(SliceBeam.CONFIG.presets.get("printer"));
boolean enable = cfg != null && cfg.get("host_type") != null && !TextUtils.isEmpty(cfg.get("print_host"));
boolean enable = cfg != null && cfg.get("host_type") != null && !TextUtils.isEmpty(cfg.get("print_host")) && panelWebView != null;
swipeDownLayout.setEnableTop(enable);
if (enable) {
String host = cfg.get("print_host");
@@ -246,6 +244,7 @@ public class BedFragment extends Fragment {
}
webViewProgressBar.animate().alpha(1).setDuration(150).start();
panelWebView.setAlpha(0f);
hasWebError = false;
panelWebView.loadUrl(host);
panelWebViewError.animate().alpha(0).setDuration(150).setListener(new AnimatorListenerAdapter() {
@Override
@@ -294,26 +293,35 @@ public class BedFragment extends Fragment {
}
swipeDownLayout = new BedSwipeDownLayout(ctx);
FrameLayout wfl = new FrameLayout(ctx);
try {
panelWebView = new WebView(ctx);
panelWebView.getSettings().setJavaScriptEnabled(true);
panelWebView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
hasWebError = true;
webViewErrDescription.setText(description);
panelWebViewError.setVisibility(View.VISIBLE);
panelWebViewError.setAlpha(0f);
panelWebViewError.animate().alpha(1).setDuration(150).setListener(null).start();
panelWebViewError.animate().alpha(1).setDuration(150).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
panelWebView.setVisibility(View.GONE);
}
}).start();
webViewProgressBar.animate().alpha(0).setDuration(150).start();
}
@Override
public void onPageFinished(WebView view, String url) {
if (!hasWebError) {
panelWebView.animate().alpha(1).setDuration(150).start();
webViewProgressBar.animate().alpha(0).setDuration(150).start();
}
}
});
FrameLayout wfl = new FrameLayout(ctx);
wfl.addView(panelWebView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
panelWebViewError = new LinearLayout(ctx);
panelWebViewError.setVisibility(View.GONE);
@@ -334,6 +342,9 @@ public class BedFragment extends Fragment {
webViewProgressBar = new ProgressBar(ctx);
webViewProgressBar.setAlpha(0f);
wfl.addView(webViewProgressBar, new FrameLayout.LayoutParams(ViewUtils.dp(36), ViewUtils.dp(36), Gravity.CENTER));
} catch (Exception e) {
Log.wtf("BedFragment", "Failed to initialize webview", e);
}
if (portrait) {
LinearLayout inner = new LinearLayout(ctx);
@@ -598,9 +609,11 @@ public class BedFragment extends Fragment {
public void onApplyTheme() {
super.onApplyTheme();
if (panelWebView != null) {
webViewErrIcon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
webViewErrDescription.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
webViewProgressBar.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
}
menuView.setBackgroundColor(ThemesRepo.getColor(android.R.attr.windowBackground));
for (int i = 0; i < MenuCategory.values().length; i++) {
if (i != currentMenuSlot) {
@@ -6,6 +6,8 @@ import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
@@ -34,6 +36,8 @@ import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.mrudultora.colorpicker.ColorPickerPopUp;
import java.util.ArrayList;
@@ -47,6 +51,8 @@ import java.util.concurrent.atomic.AtomicReference;
import ru.ytkab0bp.slicebeam.R;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
import ru.ytkab0bp.slicebeam.cloud.CloudController;
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.BeamColorPickerPopUp;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
@@ -56,6 +62,7 @@ import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
import ru.ytkab0bp.slicebeam.recycler.PreferenceSwitchItem;
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
import ru.ytkab0bp.slicebeam.slic3r.ConfigOptionDef;
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rConfigWrapper;
import ru.ytkab0bp.slicebeam.slic3r.Slic3rLocalization;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
@@ -68,6 +75,8 @@ import ru.ytkab0bp.slicebeam.view.FadeRecyclerView;
import ru.ytkab0bp.slicebeam.view.ProfileDropdownView;
public abstract class ProfileListFragment extends Fragment {
public final static int SPECIAL_TYPE_CLOUD_HEADER = 0;
private final static Object ROTATION_PAYLOAD = new Object();
protected ProfileDropdownView dropdownView;
@@ -146,8 +155,8 @@ public abstract class ProfileListFragment extends Fragment {
int pos = getChildViewHolder(ch).getAdapterPosition();
if (pos == -1 || ch.getAlpha() < 1) continue;
boolean top = currentList.get(pos).title != null;
boolean bottom = pos == getAdapter().getItemCount() - 1 || currentList.get(pos + 1).title != null;
boolean top = currentList.get(pos).title != null || currentList.get(pos).hasSpecialType();
boolean bottom = pos == getAdapter().getItemCount() - 1 || currentList.get(pos + 1).title != null || currentList.get(pos + 1).hasSpecialType();
if (top && startI != -1) {
c.drawRoundRect(0, getChildAt(startI).getTop() + getChildAt(startI).getTranslationY(), getWidth(), ch.getTop() + ch.getTranslationY() - ViewUtils.dp(8), ViewUtils.dp(32), ViewUtils.dp(32), bgPaint);
@@ -204,7 +213,7 @@ public abstract class ProfileListFragment extends Fragment {
};
recyclerView.setItemAnimator(new CubicBezierItemAnimator());
recyclerView.setAdapter(new RecyclerView.Adapter() {
private final static int TYPE_TITLE = 0, TYPE_SIMPLE = 1;
private final static int TYPE_TITLE = 0, TYPE_CLOUD_PROFILE = 1, TYPE_SIMPLE = 2;
private Map<Class<?>, Integer> viewType = new HashMap<>();
private Map<Integer, SimpleRecyclerItem> viewCreator = new HashMap<>();
@@ -219,6 +228,9 @@ public abstract class ProfileListFragment extends Fragment {
v = viewCreator.get(viewType).onCreateView(ctx);
break;
}
case TYPE_CLOUD_PROFILE:
v = new CloudProfileHeaderView(ctx);
break;
case TYPE_TITLE:
v = new CategoryHolderView(ctx);
break;
@@ -258,6 +270,43 @@ public abstract class ProfileListFragment extends Fragment {
el.simpleItem.onBindView(holder.itemView);
break;
}
case TYPE_CLOUD_PROFILE: {
OptionWrapper w = currentList.get(position);
CloudProfileHeaderView holderView = (CloudProfileHeaderView) holder.itemView;
holderView.setTag(w.color);
if (Prefs.getCloudAPIToken() != null) {
CloudAPI.UserInfo info = CloudController.getUserInfo();
if (info != null) {
if (!TextUtils.isEmpty(info.avatarUrl)) {
holderView.hasAvatar = true;
Glide.with(holderView.avatar)
.load(info.avatarUrl)
.circleCrop()
.transition(DrawableTransitionOptions.withCrossFade())
.into(holderView.avatar);
} else {
holderView.hasAvatar = false;
holderView.avatar.setImageResource(R.drawable.user_circle_outline_28);
}
holderView.title.setText(info.displayName);
} else {
holderView.hasAvatar = false;
holderView.avatar.setImageResource(R.drawable.user_circle_outline_28);
holderView.title.setText(R.string.SettingsCloudLoading);
}
holderView.subtitle.setText(R.string.SettingsCloudTapToManage);
} else {
holderView.hasAvatar = false;
holderView.avatar.setImageResource(R.drawable.user_circle_outline_28);
holderView.title.setText(R.string.SettingsCloudNotLoggedIn);
holderView.subtitle.setText(R.string.SettingsCloudTapToShowMore);
}
holderView.onApplyTheme();
holderView.setOnClickListener(view -> w.onClick.run());
break;
}
case TYPE_TITLE: {
OptionWrapper w = currentList.get(position);
CategoryHolderView holderView = (CategoryHolderView) holder.itemView;
@@ -301,6 +350,13 @@ public abstract class ProfileListFragment extends Fragment {
@Override
public int getItemViewType(int position) {
OptionWrapper w = currentList.get(position);
if (w.optionEl != null && w.optionEl.specialType != -1) {
switch (w.optionEl.specialType) {
default:
case SPECIAL_TYPE_CLOUD_HEADER:
return TYPE_CLOUD_PROFILE;
}
}
if (w.title != null) return TYPE_TITLE;
if (w.optionEl.simpleItem != null) {
@@ -469,7 +525,12 @@ public abstract class ProfileListFragment extends Fragment {
OptionElement el = items.get(i);
if (el == null) continue;
OptionWrapper w = el.title != null ? new OptionWrapper(el.icon, el.title, el.onClick, el.color, el.noTint) : new OptionWrapper(el);
if (el.title != null) {
if (el.specialType != -1) {
w.color = el.color;
w.noTint = el.noTint;
w.onClick = el.onClick;
}
if (el.title != null || el.specialType != -1) {
w.categoryIndex = j;
categoryElements.put(j, new ArrayList<>());
j++;
@@ -563,6 +624,8 @@ public abstract class ProfileListFragment extends Fragment {
}
public final class OptionElement {
public int specialType = -1;
public int icon;
public String title;
public int color;
@@ -916,6 +979,10 @@ public abstract class ProfileListFragment extends Fragment {
optionEl = el;
}
boolean hasSpecialType() {
return optionEl != null && optionEl.specialType != -1;
}
@Override
public View onCreateView(Context ctx) {
FrameLayout v = new FrameLayout(ctx);
@@ -926,6 +993,49 @@ public abstract class ProfileListFragment extends Fragment {
}
}
private final static class CloudProfileHeaderView extends LinearLayout implements IThemeView {
private ImageView avatar;
private TextView title;
private TextView subtitle;
private boolean hasAvatar;
public CloudProfileHeaderView(Context context) {
super(context);
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL);
setPadding(ViewUtils.dp(21), ViewUtils.dp(16), ViewUtils.dp(21), ViewUtils.dp(16));
avatar = new ImageView(context);
addView(avatar, new LayoutParams(ViewUtils.dp(26), ViewUtils.dp(26)) {{
setMarginEnd(ViewUtils.dp(12));
}});
LinearLayout ll = new LinearLayout(context);
ll.setOrientation(VERTICAL);
ll.setGravity(Gravity.CENTER);
title = new TextView(context);
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
ll.addView(title);
subtitle = new TextView(context);
subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
ll.addView(subtitle);
addView(ll, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
onApplyTheme();
}
@Override
public void onApplyTheme() {
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 32));
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
if (!hasAvatar) {
avatar.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
}
}
}
private final static class CategoryHolderView extends LinearLayout implements IThemeView {
private ImageView icon;
private TextView title;
@@ -28,6 +28,7 @@ import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
import ru.ytkab0bp.slicebeam.components.BeamColorPickerPopUp;
import ru.ytkab0bp.slicebeam.config.ConfigObject;
import ru.ytkab0bp.slicebeam.events.BeamServerDataUpdatedEvent;
import ru.ytkab0bp.slicebeam.events.CloudUserInfoUpdatedEvent;
import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
@@ -45,6 +46,10 @@ public class SettingsFragment extends ProfileListFragment {
@Override
protected List<OptionElement> getConfigItems() {
return Arrays.asList(
BeamServerData.isCloudAvailable() ? new OptionElement(SPECIAL_TYPE_CLOUD_HEADER).setOnClick(() -> {
Activity act = (Activity) getContext();
act.startActivity(new Intent(act, SetupActivity.class).putExtra(SetupActivity.EXTRA_CLOUD_PROFILE, true));
}) : null,
new OptionElement(R.drawable.paint_roller_outline_28, getContext().getString(R.string.SettingsInterface)),
new OptionElement(new PreferenceItem().setTitle(getContext().getString(R.string.SettingsInterfaceTheme)).setValueProvider(() -> getContext().getString(Prefs.getThemeMode().title)).setOnClickListener(v -> {
String[] items = new String[Prefs.ThemeMode.values().length];
@@ -107,7 +112,7 @@ public class SettingsFragment extends ProfileListFragment {
BeamTheme.LIGHT.colors.put(android.R.attr.colorAccent, Prefs.getAccentColor());
BeamTheme.DARK.colors.put(android.R.attr.colorAccent, Prefs.getAccentColor());
ThemesRepo.invalidate((Activity) getContext());
recyclerView.getAdapter().notifyItemChanged(1);
recyclerView.getAdapter().notifyItemChanged(2 - (BeamServerData.isCloudAvailable() ? 0 : 1));
}
})
.setNegativeButtonText(getContext().getString(R.string.SettingsInterfaceColorReset))
@@ -130,7 +135,7 @@ public class SettingsFragment extends ProfileListFragment {
Prefs.setRenderScale(variants[which]);
dialog.dismiss();
// I'm too lazy to calculate real position for now
recyclerView.getAdapter().notifyItemChanged(3);
recyclerView.getAdapter().notifyItemChanged(4 - (BeamServerData.isCloudAvailable() ? 0 : 1));
})
.show();
})),
@@ -177,6 +182,13 @@ public class SettingsFragment extends ProfileListFragment {
setConfigItems(getConfigItems());
}
@EventHandler(runOnMainThread = true)
public void onUserInfoUpdated(CloudUserInfoUpdatedEvent e) {
if (BeamServerData.isCloudAvailable()) {
recyclerView.getAdapter().notifyItemChanged(0);
}
}
@Override
protected void cloneCurrentProfile() {}
@@ -21,6 +21,7 @@ import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import ru.ytkab0bp.slicebeam.SliceBeam;
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
import ru.ytkab0bp.slicebeam.theme.IThemeView;
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
@@ -35,6 +36,8 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
private boolean noTint;
private ValueProvider valueProvider;
private float roundRadius;
private int mPaddings = ViewUtils.dp(12);
private boolean mForceDark;
public PreferenceItem setTitle(CharSequence title) {
mTitle = title;
@@ -46,6 +49,16 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
return this;
}
public PreferenceItem setPaddings(int paddings) {
this.mPaddings = paddings;
return this;
}
public PreferenceItem setForceDark(boolean mForceDark) {
this.mForceDark = mForceDark;
return this;
}
public PreferenceItem setSubtitleProvider(ValueProvider mSubtitle) {
this.mSubtitle = mSubtitle;
return this;
@@ -112,6 +125,8 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
private TextView value;
private float radius;
private PreferenceItem item;
public PreferenceHolderView(Context context) {
super(context);
@@ -165,14 +180,14 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
value.setVisibility(GONE);
addView(value, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
int pad = ViewUtils.dp(12);
setPadding(pad, pad, pad, pad);
setMinimumHeight(ViewUtils.dp(56));
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
onApplyTheme();
}
void bind(PreferenceItem item) {
this.item = item;
setPadding(item.mPaddings, item.mPaddings, item.mPaddings, item.mPaddings);
title.setText(item.mTitle);
title.setVisibility(TextUtils.isEmpty(item.mTitle) ? GONE : VISIBLE);
@@ -217,15 +232,19 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
ViewGroup.LayoutParams params = icon.getLayoutParams();
params.width = params.height = radius != 0 ? ViewUtils.dp(42) : ViewUtils.dp(28);
if (item.mForceDark) {
onApplyTheme();
}
}
@Override
public void onApplyTheme() {
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
value.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
BeamTheme theme = item != null && item.mForceDark ? BeamTheme.DARK : ThemesRepo.getCurrent();
title.setTextColor(theme.colors.get(android.R.attr.textColorPrimary));
subtitle.setTextColor(theme.colors.get(android.R.attr.textColorSecondary));
value.setTextColor(theme.colors.get(android.R.attr.textColorSecondary));
icon.setImageTintList(ColorStateList.valueOf(theme.colors.get(android.R.attr.textColorSecondary)));
setBackground(ViewUtils.createRipple(theme.colors.get(android.R.attr.colorControlHighlight), 16));
}
}
@@ -96,9 +96,10 @@ public class PreferenceSwitchItem extends SimpleRecyclerItem<PreferenceSwitchIte
icon = new ImageView(context);
icon.setLayoutParams(new LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
setMarginEnd(ViewUtils.dp(16));
gravity = Gravity.CENTER_VERTICAL;
setMarginStart(ViewUtils.dp(4));
setMarginEnd(ViewUtils.dp(8));
}});
addView(icon);
LinearLayout innerLayout = new LinearLayout(context);
innerLayout.setOrientation(VERTICAL);
@@ -168,7 +169,7 @@ public class PreferenceSwitchItem extends SimpleRecyclerItem<PreferenceSwitchIte
public void onApplyTheme() {
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorAccent)));
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
}
}
@@ -21,7 +21,7 @@ public class BeamTheme {
colors.put(R.attr.dialogBackground, 0xffffffff);
colors.put(R.attr.switchThumbUncheckedColor, 0xffeef2f3);
colors.put(R.attr.boostyColorTop, 0xfff06e2a);
colors.put(R.attr.boostyColorBottom, 0xfffce2d4);
colors.put(R.attr.boostyColorBottom, 0xff884725);
colors.put(R.attr.telegramColor, 0xff27a7e7);
colors.put(R.attr.k3dColor, 0xff039045);
colors.put(R.attr.modelHoverColor, 0xffffffff);
@@ -125,6 +125,100 @@ public class Prefs {
cachedThemeMode = null;
}
public static String getCloudAPIToken() {
return mPrefs.getString("cloud_api_token", null);
}
public static void setCloudAPIToken(String token) {
SharedPreferences.Editor e = mPrefs.edit();
if (token == null) {
e.remove("cloud_api_token");
} else {
e.putString("cloud_api_token", token);
}
e.apply();
}
public static boolean isCloudProfileSyncEnabled() {
return mPrefs.getBoolean("cloud_profile_sync", true);
}
public static void setCloudProfileSyncEnabled(boolean en) {
mPrefs.edit().putBoolean("cloud_profile_sync", en).apply();
}
public static String getCloudCachedUserInfo() {
return mPrefs.getString("cloud_cached_user_info", null);
}
public static void setCloudCachedUserInfo(String info) {
SharedPreferences.Editor e = mPrefs.edit();
if (info == null) {
e.remove("cloud_cached_user_info");
} else {
e.putString("cloud_cached_user_info", info);
}
e.apply();
}
public static int getCloudCachedUsedModels() {
return mPrefs.getInt("cloud_cached_models_used", 0);
}
public static int getCloudCachedMaxModels() {
return mPrefs.getInt("cloud_cached_models_max", 50);
}
public static void setCloudCachedUsedMaxModels(int used, int max) {
mPrefs.edit().putInt("cloud_cached_models_used", used).putInt("cloud_cached_models_max", max).apply();
}
public static String getCloudCachedUserFeatures() {
return mPrefs.getString("cloud_cached_user_features", null);
}
public static void setCloudCachedUserFeatures(String features) {
SharedPreferences.Editor e = mPrefs.edit();
if (features == null) {
e.remove("cloud_cached_user_features");
} else {
e.putString("cloud_cached_user_features", features);
}
e.apply();
}
public static long getCloudLastFeaturesSync() {
return mPrefs.getLong("cloud_last_features_sync", 0);
}
public static void setCloudLastFeaturesSync(long ls) {
mPrefs.edit().putLong("cloud_last_features_sync", ls).apply();
}
public static long getCloudLastSync() {
return mPrefs.getLong("cloud_last_sync", 0);
}
public static void setCloudLastSync(long ls) {
mPrefs.edit().putLong("cloud_last_sync", ls).apply();
}
public static long getLocalLastModified() {
return mPrefs.getLong("cloud_local_last_modified", 0);
}
public static void setLocalLastModified(long lm) {
mPrefs.edit().putLong("cloud_local_last_modified", lm).apply();
}
public static long getRemoteLastModified() {
return mPrefs.getLong("cloud_remote_last_modified", 0);
}
public static void setRemoteLastModified(long lm) {
mPrefs.edit().putLong("cloud_remote_last_modified", lm).apply();
}
public enum ThemeMode {
SYSTEM(R.string.SettingsInterfaceThemeSystem),
LIGHT(R.string.SettingsInterfaceThemeLight),
@@ -0,0 +1,16 @@
package ru.ytkab0bp.slicebeam.utils;
import java.util.Random;
public class RandomUtils {
public final static Random RANDOM = new Random();
public static float randomf(float min, float max) {
return min + RANDOM.nextFloat() * (max - min);
}
public static long randoml(long min, long max) {
return (long) (min + RANDOM.nextDouble() * (max - min));
}
}
@@ -23,6 +23,11 @@ public class ViewUtils {
private static Handler uiHandler = new Handler(Looper.getMainLooper());
private static Map<String, Typeface> typefaceCache = new HashMap<>();
public static Handler getUiHandler() {
return uiHandler;
}
public static void postOnMainThread(Runnable runnable) {
uiHandler.post(runnable);
}
@@ -110,6 +110,10 @@ public class SegmentsView extends View {
}
}
protected int onGetColor(int i) {
return mapColor(i);
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
@@ -125,7 +129,7 @@ public class SegmentsView extends View {
for (int i = 1; i < currentValues.length; i++) {
float prev = currentValues[i - 1];
float to = currentValues[i];
paint.setColor(mapColor(i - 1));
paint.setColor(onGetColor(i - 1));
canvas.drawRect(l + prev * dw, 0, l + to * dw, getHeight(), paint);
}
}
+8 -2
View File
@@ -1243,18 +1243,24 @@ extern "C" {
JNIEXPORT jint JNICALL Java_ru_ytkab0bp_slicebeam_slic3r_Native_shader_1get_1uniform_1location(JNIEnv* env, jclass, jlong ptr, jstring name) {
const char* chars = env->GetStringUTFChars(name, JNI_FALSE);
ShaderRef* shader = (ShaderRef*) (intptr_t) ptr;
if (shader) {
int location = shader->program.get_uniform_location(chars);
env->ReleaseStringUTFChars(name, chars);
return location;
}
return 0;
}
JNIEXPORT jint JNICALL Java_ru_ytkab0bp_slicebeam_slic3r_Native_shader_1get_1attrib_1location(JNIEnv* env, jclass, jlong ptr, jstring name) {
const char* chars = env->GetStringUTFChars(name, JNI_FALSE);
ShaderRef* shader = (ShaderRef*) (intptr_t) ptr;
const char *chars = env->GetStringUTFChars(name, JNI_FALSE);
ShaderRef *shader = (ShaderRef *) (intptr_t) ptr;
if (shader) {
int location = shader->program.get_attrib_location(chars);
env->ReleaseStringUTFChars(name, chars);
return location;
}
return 0;
}
JNIEXPORT void JNICALL Java_ru_ytkab0bp_slicebeam_slic3r_Native_shader_1start_1using(JNIEnv* env, jclass, jlong ptr) {
ShaderRef* shader = (ShaderRef*) (intptr_t) ptr;
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M12.073,2C12.824,2 13.464,2.233 14,2.593C14.536,2.233 15.176,2 15.927,2C17.932,2 19.58,3.618 19.58,5.642C19.58,6.04 19.539,6.493 19.377,6.977C19.215,7.465 18.964,7.884 18.662,8.262C18.115,8.946 17.279,9.63 16.238,10.435L15.523,10.988C14.626,11.681 13.374,11.681 12.477,10.988L11.762,10.435C10.72,9.63 9.885,8.946 9.338,8.262C9.036,7.884 8.785,7.465 8.623,6.977C8.461,6.493 8.42,6.04 8.42,5.642C8.42,3.618 10.068,2 12.073,2ZM12.073,4C11.16,4 10.42,4.735 10.42,5.642C10.42,6.671 10.845,7.199 12.985,8.853L13.7,9.405C13.877,9.541 14.123,9.541 14.3,9.405L15.015,8.853C17.155,7.199 17.58,6.671 17.58,5.642C17.58,4.735 16.84,4 15.927,4C15.375,4 14.873,4.312 14.401,4.99L14.268,5.182C14.165,5.329 13.963,5.366 13.815,5.264C13.783,5.242 13.755,5.214 13.732,5.182L13.599,4.99C13.127,4.312 12.625,4 12.073,4ZM6.885,10.383C7.397,10.175 7.642,9.591 7.434,9.079C7.225,8.568 6.642,8.323 6.13,8.531L4.481,9.203L4.361,9.252C3.811,9.476 3.312,9.678 2.929,10.03C2.594,10.337 2.337,10.719 2.179,11.145C1.998,11.633 1.999,12.171 2,12.765L2,12.895V18.527L2,18.648C1.999,19.185 1.998,19.686 2.163,20.144C2.307,20.543 2.542,20.904 2.849,21.198C3.201,21.534 3.659,21.736 4.151,21.953L4.151,21.953L4.261,22.002L12.083,25.466L12.172,25.505C12.642,25.713 13.03,25.886 13.446,25.956C13.813,26.017 14.187,26.017 14.553,25.956C14.969,25.886 15.358,25.713 15.828,25.505L15.828,25.505L15.916,25.466L23.739,22.002L23.849,21.953C24.341,21.736 24.799,21.534 25.151,21.198C25.458,20.904 25.693,20.543 25.837,20.144C26.002,19.686 26.001,19.185 26,18.648V18.648L26,18.527V12.896L26,12.766C26.001,12.172 26.002,11.634 25.821,11.146C25.663,10.719 25.405,10.337 25.07,10.03C24.687,9.678 24.188,9.476 23.637,9.253L23.516,9.204L21.869,8.533C21.358,8.325 20.774,8.571 20.566,9.083C20.358,9.594 20.604,10.178 21.115,10.386L22.718,11.038L15.108,14.414C14.504,14.682 14.358,14.739 14.222,14.762C14.075,14.787 13.925,14.787 13.778,14.762C13.642,14.739 13.496,14.682 12.891,14.414L9.156,12.757L5.281,11.037L6.885,10.383ZM15.92,16.242L24,12.657C24,12.73 24,12.809 24,12.896V18.527C24,19.263 23.986,19.381 23.955,19.465C23.916,19.574 23.852,19.673 23.768,19.753C23.704,19.814 23.602,19.875 22.929,20.173L15.106,23.637L15,23.684V16.621C15.263,16.535 15.53,16.416 15.831,16.282L15.92,16.242ZM12.169,16.282C12.469,16.415 12.737,16.534 13,16.621V23.684L12.893,23.637L5.071,20.173C4.398,19.875 4.296,19.814 4.232,19.753C4.148,19.673 4.084,19.574 4.045,19.465C4.014,19.381 4,19.263 4,18.527V12.895C4,12.808 4,12.729 4.001,12.658L8.344,14.585L12.08,16.242L12.169,16.282L12.169,16.282Z"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="m11.181,6.016c-0.543,-0.1 -1.064,0.26 -1.164,0.803 -0.071,0.387 -0.58,1.206 -1.998,1.18 -0.552,-0.01 -1.008,0.429 -1.018,0.981s0.429,1.008 0.981,1.018c2.349,0.043 3.745,-1.422 4.002,-2.818 0.1,-0.543 -0.26,-1.064 -0.803,-1.164z"
android:fillColor="#000"
android:fillType="evenOdd"/>
<path
android:pathData="m17.265,7.028c0.537,-0.13 1.077,0.201 1.207,0.738 0.158,0.655 0.392,1.266 0.761,1.679 0.318,0.355 0.789,0.63 1.661,0.538 0.549,-0.058 1.041,0.34 1.099,0.889 0.058,0.549 -0.34,1.041 -0.89,1.099 -1.5,0.159 -2.608,-0.35 -3.362,-1.194 -0.703,-0.786 -1.033,-1.789 -1.215,-2.543 -0.13,-0.537 0.201,-1.077 0.738,-1.207z"
android:fillColor="#000"
android:fillType="evenOdd"/>
<path
android:pathData="m10.569,21.498c-0.551,0.038 -1.028,-0.378 -1.067,-0.929 -0.047,-0.683 -0.171,-1.25 -0.479,-1.738 -0.299,-0.475 -0.836,-0.977 -1.896,-1.403 -0.512,-0.206 -0.761,-0.788 -0.555,-1.301 0.206,-0.512 0.788,-0.761 1.301,-0.555 1.364,0.548 2.275,1.29 2.843,2.193 0.56,0.89 0.724,1.836 0.781,2.666 0.038,0.551 -0.378,1.028 -0.929,1.067z"
android:fillColor="#000"
android:fillType="evenOdd"/>
<path
android:pathData="m10.017,6.819c0.1,-0.543 0.621,-0.903 1.164,-0.803 0.543,0.1 0.903,0.621 0.803,1.164 -0.257,1.397 -1.653,2.862 -4.002,2.818 -0.552,-0.01 -0.992,-0.466 -0.981,-1.018s0.466,-0.992 1.018,-0.981c1.418,0.026 1.927,-0.793 1.998,-1.18zM9.502,20.569c0.038,0.551 0.516,0.967 1.067,0.929s0.967,-0.516 0.929,-1.067c-0.057,-0.83 -0.221,-1.776 -0.781,-2.666 -0.569,-0.903 -1.479,-1.645 -2.843,-2.193 -0.512,-0.206 -1.095,0.043 -1.301,0.555 -0.206,0.512 0.043,1.095 0.555,1.301 1.06,0.426 1.597,0.928 1.896,1.403 0.307,0.488 0.431,1.055 0.479,1.738z"
android:fillColor="#000"
android:fillType="evenOdd"/>
<path
android:pathData="m11.624,2c-2.03,0 -3.766,1.209 -4.579,2.938 -2.37,0.718 -4.045,3.017 -4.045,5.67 0,1.206 0.344,2.333 0.939,3.273 -0.53,0.683 -0.842,1.551 -0.842,2.486 0,1.421 0.726,2.701 1.849,3.396 -0.049,0.233 -0.074,0.473 -0.074,0.72 0,1.804 1.46,3.411 3.307,3.352 0.709,1.285 2.062,2.166 3.632,2.166 0.807,0 1.56,-0.234 2.196,-0.637 0.645,0.403 1.408,0.637 2.224,0.637 1.613,0 3.008,-0.908 3.719,-2.237 1.767,-0.023 3.111,-1.521 3.111,-3.262 0,-0.263 -0.03,-0.519 -0.087,-0.765 1.22,-0.672 2.025,-2.004 2.025,-3.498 0,-0.984 -0.348,-1.893 -0.931,-2.591 0.308,-0.536 0.539,-1.125 0.678,-1.749 0.061,-0.276 0.105,-0.558 0.128,-0.847 0.014,-0.175 0.022,-0.352 0.022,-0.531 0,-2.632 -1.611,-4.911 -3.912,-5.749 -0.94,-1.652 -2.709,-2.772 -4.744,-2.772 -0.844,0 -1.645,0.193 -2.359,0.538 -0.68,-0.344 -1.447,-0.538 -2.258,-0.538zM11.624,4c-1.325,0 -2.473,0.868 -2.9,2.111 -0.116,0.339 -0.405,0.59 -0.757,0.657 -1.643,0.315 -2.967,1.876 -2.967,3.84 0,0.73 0.185,1.409 0.502,1.988 0.086,-0.048 0.173,-0.093 0.261,-0.136 1.29,-0.63 2.879,-0.807 4.208,-0.349 0.522,0.18 0.8,0.749 0.62,1.271 -0.18,0.522 -0.749,0.8 -1.271,0.62 -0.752,-0.259 -1.79,-0.18 -2.679,0.255 -0.815,0.398 -1.544,1.156 -1.544,2.11 0,0.96 0.62,1.702 1.362,1.891 0.305,0.078 0.556,0.295 0.676,0.586 0.121,0.291 0.097,0.622 -0.063,0.893 -0.125,0.211 -0.2,0.465 -0.2,0.745 0,0.809 0.6,1.354 1.209,1.354 0.129,0 0.253,-0.022 0.368,-0.064 0.259,-0.093 0.544,-0.075 0.789,0.049 0.245,0.124 0.429,0.343 0.507,0.607 0.276,0.925 1.108,1.573 2.066,1.573 0.438,0 0.846,-0.134 1.189,-0.368v-4.19,-0.007 -8.88,-0.007 -6.215c-0.414,-0.214 -0.881,-0.334 -1.376,-0.334zM16.241,4c1.383,0 2.583,0.82 3.139,2.018 0.124,0.267 0.358,0.465 0.642,0.543 1.626,0.448 2.876,2.026 2.876,3.958 0,0.119 -0.005,0.237 -0.014,0.354 -0.016,0.192 -0.043,0.381 -0.084,0.569 -0.175,0.754 -0.602,1.543 -1.213,2.189 -0.759,0.802 -1.693,1.274 -2.586,1.274 -0.552,0 -1,0.448 -1,1s0.448,1 1,1c1.455,0 2.772,-0.686 3.767,-1.628 0.147,0.281 0.233,0.608 0.233,0.96 0,1.012 -0.697,1.792 -1.527,1.934 -0.338,0.058 -0.623,0.285 -0.754,0.601 -0.132,0.317 -0.092,0.679 0.105,0.959 0.147,0.21 0.238,0.474 0.238,0.768 0,0.73 -0.542,1.262 -1.117,1.262 -0.575,0 -0.833,-0.212 -1.21,-0.547 -0.413,-0.367 -0.546,-0.725 -0.546,-0.882 0,-0.552 -0.448,-1 -1,-1 -0.552,0 -1,0.448 -1,1 0,0.948 0.575,1.805 1.217,2.376 0.174,0.154 0.366,0.3 0.572,0.431 -0.409,0.524 -1.042,0.858 -1.747,0.858 -0.455,0 -0.878,-0.138 -1.232,-0.377v-19.392c0.385,-0.149 0.803,-0.231 1.241,-0.231z"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M7,11.5a5.5,5.5 0,0 1,10.853 -1.268,1 1,0 0,0 1.002,0.77L19,11c1.652,0 3.117,0.8 4.03,2.04a1,1 0,0 0,1.61 -1.187,6.994 6.994,0 0,0 -5.059,-2.83A7.502,7.502 0,0 0,5.006 11.8,6 6,0 0,0 8,23h6.502a1,1 0,1 0,0 -2H8a4,4 0,0 1,-1.551 -7.688,1 1,0 0,0 0.602,-1.058A5.558,5.558 0,0 1,7 11.5ZM22,15a1,1 0,0 1,1 1v3h3a1,1 0,1 1,0 2h-3v3a1,1 0,1 1,-2 0v-3h-3a1,1 0,1 1,0 -2h3v-3a1,1 0,0 1,1 -1Z"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="m5.146,3.634c0.762,-0.408 1.512,-0.534 3.081,-0.534h1.872c0.497,0 0.9,0.403 0.9,0.9 0,0.497 -0.403,0.9 -0.9,0.9h-1.872c-1.487,0 -1.871,0.128 -2.233,0.322 -0.336,0.18 -0.594,0.438 -0.774,0.774 -0.194,0.362 -0.322,0.746 -0.322,2.233v7.544c0,1.487 0.128,1.871 0.322,2.233 0.18,0.336 0.438,0.594 0.774,0.774 0.362,0.194 0.746,0.322 2.233,0.322h7.544c1.487,0 1.871,-0.128 2.233,-0.322 0.336,-0.18 0.594,-0.438 0.774,-0.774 0.194,-0.362 0.322,-0.746 0.322,-2.233v-1.872c0,-0.497 0.403,-0.9 0.9,-0.9s0.9,0.403 0.9,0.9v1.872c0,1.57 -0.127,2.319 -0.534,3.082 -0.347,0.65 -0.863,1.165 -1.512,1.512 -0.762,0.408 -1.512,0.534 -3.082,0.534h-7.544c-1.57,0 -2.319,-0.127 -3.081,-0.534 -0.65,-0.347 -1.165,-0.863 -1.512,-1.512 -0.408,-0.762 -0.534,-1.512 -0.534,-3.082v-7.544c0,-1.57 0.127,-2.319 0.534,-3.081 0.347,-0.65 0.863,-1.165 1.512,-1.512z"
android:fillColor="#000"
android:fillType="evenOdd"/>
<path
android:pathData="m14,4c0,-0.497 0.403,-0.9 0.9,-0.9h5.1c0.497,0 0.9,0.403 0.9,0.9v5.1c0,0.497 -0.403,0.9 -0.9,0.9 -0.497,0 -0.9,-0.403 -0.9,-0.9v-2.927l-6.564,6.564c-0.351,0.352 -0.921,0.352 -1.273,0 -0.351,-0.352 -0.351,-0.921 0,-1.273l6.564,-6.564h-2.927c-0.497,0 -0.9,-0.403 -0.9,-0.9z"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M11.005,4.145c-0.412,-1.693 -2.25,-2.621 -3.852,-1.896a2.82,2.82 0,0 0,-1.57 3.22l1.704,7.504a2.66,2.66 0,0 0,-3.779 0.57,2.715 2.715,0 0,0 0.042,3.22l4.622,6.094 0.001,0.002 0.036,0.049a6.136,6.136 0,0 0,0.576 0.644c0.398,0.388 0.996,0.877 1.817,1.317 1.658,0.888 4.158,1.536 7.628,0.836 2.844,-0.574 4.844,-2.15 5.902,-4.29 1.041,-2.107 1.121,-4.655 0.37,-7.147l-1.113,-4.743c-0.399,-1.697 -2.197,-2.671 -3.831,-2.03 -0.403,0.159 -0.75,0.398 -1.03,0.693a2.744,2.744 0,0 0,-2.289 -0.52,2.57 2.57,0 0,0 -1.242,0.653 2.498,2.498 0,0 0,-0.216 -0.139c-0.619,-0.355 -1.357,-0.431 -2.095,-0.246a3.037,3.037 0,0 0,-0.636 0.28l-1.045,-4.071ZM14.521,12.088 L14.522,12.09a1,1 0,0 0,1.937 -0.5l-0.001,-0.002 -0.254,-0.991c-0.142,-0.552 0.147,-0.906 0.467,-0.977a0.765,0.765 0,0 1,0.889 0.521l0.432,1.318v0.003a1,1 0,0 0,1.902 -0.619v-0.003l-0.12,-0.367a0.9,0.9 0,0 1,0.515 -1.116,0.864 0.864,0 0,1 1.152,0.625l1.121,4.774a1,1 0,0 0,0.017 0.063c0.646,2.12 0.536,4.14 -0.24,5.709 -0.762,1.543 -2.218,2.755 -4.505,3.216 -3.016,0.61 -5.042,0.03 -6.288,-0.639a6.08,6.08 0,0 1,-1.367 -0.986,4.141 4.141,0 0,1 -0.377,-0.421 0.952,0.952 0,0 0,-0.025 -0.035l-4.634,-6.11a0.715,0.715 0,0 1,-0.01 -0.843,0.66 0.66,0 0,1 1.043,-0.052l2.128,2.061a1,1 0,0 0,1.671 -0.94L7.531,5.02l-0.002,-0.01a0.82,0.82 0,0 1,0.449 -0.94,0.785 0.785,0 0,1 1.088,0.565l2.023,7.878a1,1 0,0 0,1.938 -0.491c-0.29,-1.16 -0.203,-1.683 -0.12,-1.889 0.05,-0.122 0.118,-0.193 0.312,-0.268 0.278,-0.06 0.46,-0.01 0.566,0.052 0.113,0.064 0.222,0.186 0.278,0.4l0.458,1.77ZM9.805,21.703ZM9.805,21.703"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M12,10.5a1.5,1.5 0,1 1,-3 0,1.5 1.5,0 0,1 3,0ZM6.07,3.801C7.164,3.216 8.243,3 10.691,3h6.616c2.448,0 3.527,0.216 4.622,0.801A5.465,5.465 0,0 1,24.2 6.07c0.585,1.096 0.801,2.175 0.801,4.623v6.616c0,2.448 -0.216,3.527 -0.801,4.622a5.465,5.465 0,0 1,-2.27 2.269c-1.095,0.585 -2.174,0.801 -4.622,0.801h-6.616c-2.448,0 -3.527,-0.216 -4.623,-0.801a5.465,5.465 0,0 1,-2.268 -2.269C3.216,20.835 3,19.756 3,17.308v-6.616c0,-2.448 0.216,-3.527 0.801,-4.623A5.466,5.466 0,0 1,6.07 3.801ZM10.691,5c-2.335,0 -3.019,0.212 -3.68,0.565a3.466,3.466 0,0 0,-1.447 1.448C5.212,7.673 5,8.357 5,10.692v6.616c0,1.67 0.108,2.495 0.3,3.071L8,17.677c0.37,-0.37 0.69,-0.69 0.975,-0.932 0.301,-0.255 0.628,-0.482 1.03,-0.613a3,3 0,0 1,1.845 -0.007c0.403,0.129 0.731,0.353 1.034,0.607 0.27,0.226 0.57,0.52 0.916,0.861l2.93,-2.91c0.373,-0.37 0.695,-0.69 0.982,-0.932 0.302,-0.255 0.63,-0.481 1.034,-0.611a3,3 0,0 1,1.85 0.003c0.403,0.131 0.73,0.359 1.032,0.615 0.285,0.242 0.606,0.563 0.978,0.935l0.393,0.393v-4.394c0,-2.335 -0.212,-3.019 -0.565,-3.68a3.466,3.466 0,0 0,-1.448 -1.447C20.327,5.212 19.643,5 17.308,5h-6.616ZM22.994,17.909 L21.219,16.134c-0.407,-0.407 -0.67,-0.668 -0.886,-0.852 -0.207,-0.176 -0.304,-0.22 -0.357,-0.237a1,1 0,0 0,-0.617 -0.001c-0.053,0.017 -0.15,0.06 -0.358,0.236 -0.217,0.183 -0.48,0.444 -0.888,0.849l-3.605,3.58a1,1 0,0 1,-1.407 0.003l-0.613,-0.604a16.794,16.794 0,0 0,-0.888 -0.843c-0.208,-0.174 -0.306,-0.218 -0.358,-0.235a1,1 0,0 0,-0.616 0.002c-0.052,0.018 -0.15,0.062 -0.356,0.237 -0.215,0.183 -0.477,0.444 -0.883,0.85l-2.94,2.941c0.174,0.14 0.363,0.265 0.567,0.374 0.66,0.353 1.344,0.565 3.679,0.565h6.616c2.335,0 3.019,-0.212 3.68,-0.565a3.467,3.467 0,0 0,1.447 -1.448c0.321,-0.6 0.525,-1.219 0.56,-3.078Z"
android:fillColor="#000"/>
</vector>
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M19.872,7C21.655,7 22.302,7.186 22.954,7.534C23.606,7.883 24.117,8.394 24.466,9.046C24.814,9.698 25,10.345 25,12.128L25,18.872C25,20.655 24.814,21.302 24.466,21.954C24.117,22.606 23.606,23.117 22.954,23.466C22.302,23.814 21.655,24 19.872,24L8.128,24C6.345,24 5.698,23.814 5.046,23.466C4.394,23.117 3.883,22.606 3.534,21.954C3.204,21.336 3.02,20.723 3.002,19.144L3,12.128C3,10.345 3.186,9.698 3.534,9.046C3.883,8.394 4.394,7.883 5.046,7.534C5.664,7.204 6.277,7.02 7.856,7.002L19.872,7ZM19.999,14.414L15.061,19.354C14.511,19.903 13.642,19.937 13.053,19.457L12.939,19.354L11,17.414L6.517,21.897C6.817,21.963 7.223,21.994 7.89,21.999L19.872,22C21.196,22 21.599,21.922 22.01,21.702C22.314,21.54 22.54,21.314 22.702,21.01L22.778,20.855C22.929,20.51 22.991,20.095 22.999,19.11L22.999,17.414L19.999,14.414ZM20.11,9.001L7.89,9.001L7.474,9.009C6.653,9.034 6.324,9.119 5.99,9.298C5.686,9.46 5.46,9.686 5.298,9.99L5.222,10.145C5.071,10.49 5.009,10.905 5.001,11.89L5.001,19.11L5.009,19.526C5.022,19.957 5.052,20.252 5.103,20.484L9.939,15.646C10.489,15.097 11.358,15.063 11.947,15.543L12.061,15.646L14,17.586L18.939,12.646C19.489,12.097 20.358,12.063 20.947,12.543L21.061,12.646L23,14.585L23,12.128C23,10.804 22.922,10.401 22.702,9.99C22.54,9.686 22.314,9.46 22.01,9.298L21.855,9.222C21.51,9.071 21.095,9.009 20.11,9.001ZM8.5,11C9.328,11 10,11.672 10,12.5C10,13.328 9.328,14 8.5,14C7.672,14 7,13.328 7,12.5C7,11.672 7.672,11 8.5,11ZM17.436,3C18.328,3 18.651,3.093 18.977,3.267C19.303,3.441 19.559,3.697 19.733,4.023C19.864,4.269 19.949,4.513 19.983,4.999L8.017,4.999C8.051,4.513 8.136,4.269 8.267,4.023C8.441,3.697 8.697,3.441 9.023,3.267C9.349,3.093 9.672,3 10.564,3L17.436,3Z"
android:strokeWidth="1"
android:fillColor="#000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M10.119,10.118c1.258,-1.259 2.204,-3.562 2.847,-5.619l0.009,-0.03a33.18,33.18 0,0 0,0.208 -0.692c0.319,-1.09 0.484,-1.637 0.623,-1.716a0.38,0.38 0,0 1,0.403 0c0.14,0.078 0.306,0.625 0.629,1.714l0.033,0.114 0.005,0.018c0.055,0.184 0.112,0.37 0.172,0.56l0.009,0.029c0.65,2.057 1.602,4.363 2.86,5.622 1.26,1.258 3.553,2.202 5.599,2.844l0.027,0.009c0.19,0.06 0.379,0.116 0.564,0.17l0.122,0.036c1.09,0.32 1.638,0.486 1.717,0.625a0.38,0.38 0,0 1,0 0.403c-0.078,0.14 -0.625,0.307 -1.713,0.631l-0.123,0.037a34.148,34.148 0,0 0,-0.59 0.181c-2.047,0.651 -4.343,1.604 -5.602,2.863 -1.26,1.26 -2.212,3.555 -2.863,5.602l-0.009,0.028c-0.06,0.19 -0.118,0.378 -0.173,0.563l-0.036,0.122c-0.325,1.088 -0.492,1.635 -0.632,1.714a0.38,0.38 0,0 1,-0.402 -0.001c-0.14,-0.08 -0.305,-0.627 -0.625,-1.716l-0.002,-0.009 -0.034,-0.114a35.075,35.075 0,0 0,-0.17 -0.563l-0.009,-0.028c-0.642,-2.046 -1.586,-4.34 -2.844,-5.598 -1.26,-1.26 -3.565,-2.211 -5.621,-2.861l-0.03,-0.01a34.262,34.262 0,0 0,-0.56 -0.17l-0.131,-0.04c-1.089,-0.322 -1.636,-0.488 -1.715,-0.628a0.38,0.38 0,0 1,0 -0.403c0.08,-0.14 0.628,-0.304 1.717,-0.623l0.132,-0.039c0.184,-0.054 0.371,-0.11 0.56,-0.17l0.03,-0.008c2.056,-0.643 4.36,-1.588 5.618,-2.847Z"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="m16.405,11.793c-0.455,0.994 -1.047,2.02 -1.819,2.792s-1.798,1.364 -2.793,1.819c0.994,0.455 2.021,1.047 2.793,1.819 0.771,0.771 1.364,1.796 1.819,2.788 0.455,-0.993 1.048,-2.017 1.819,-2.788 0.771,-0.771 1.796,-1.364 2.789,-1.819 -0.993,-0.455 -2.018,-1.048 -2.789,-1.819 -0.772,-0.772 -1.365,-1.798 -1.819,-2.792zM15.357,8.959c-0.526,1.608 -1.253,3.281 -2.185,4.213 -0.932,0.932 -2.605,1.658 -4.214,2.185 -0.164,0.054 -0.327,0.105 -0.489,0.155 -0.361,0.111 -0.715,0.211 -1.052,0.301 -0.097,0.026 -0.192,0.051 -0.286,0.075 -0.447,0.115 -0.447,0.92 0,1.035 0.094,0.024 0.189,0.049 0.286,0.075 0.337,0.09 0.69,0.19 1.052,0.301 0.162,0.049 0.325,0.101 0.489,0.155 1.609,0.526 3.282,1.253 4.214,2.185 0.932,0.931 1.658,2.599 2.185,4.202 0.053,0.163 0.105,0.325 0.154,0.485 0.111,0.361 0.211,0.714 0.301,1.05 0.026,0.097 0.051,0.193 0.076,0.287 0.116,0.446 0.919,0.446 1.035,0 0.024,-0.094 0.05,-0.19 0.076,-0.287 0.09,-0.336 0.191,-0.689 0.301,-1.05 0.049,-0.16 0.101,-0.322 0.154,-0.485 0.526,-1.603 1.253,-3.271 2.185,-4.202 0.932,-0.931 2.6,-1.658 4.203,-2.184 0.163,-0.053 0.325,-0.105 0.485,-0.154 0.361,-0.111 0.714,-0.211 1.05,-0.301 0.097,-0.026 0.193,-0.051 0.287,-0.076 0.446,-0.115 0.446,-0.919 0,-1.035 -0.094,-0.024 -0.19,-0.05 -0.287,-0.076 -0.336,-0.09 -0.689,-0.191 -1.05,-0.301 -0.161,-0.049 -0.322,-0.101 -0.485,-0.154 -1.603,-0.526 -3.272,-1.253 -4.203,-2.184 -0.932,-0.932 -1.659,-2.604 -2.185,-4.213 -0.054,-0.164 -0.105,-0.327 -0.155,-0.489 -0.111,-0.361 -0.211,-0.715 -0.301,-1.051 -0.026,-0.097 -0.051,-0.192 -0.075,-0.286 -0.115,-0.447 -0.92,-0.447 -1.035,0 -0.024,0.094 -0.049,0.189 -0.075,0.286 -0.09,0.336 -0.19,0.69 -0.301,1.051 -0.049,0.162 -0.101,0.325 -0.155,0.489z"
android:fillColor="#000"
android:fillType="evenOdd"/>
<path
android:pathData="m7.297,2.363c-0.164,-0.49 -0.857,-0.49 -1.021,0l-0.651,1.953c-0.205,0.615 -0.688,1.098 -1.303,1.303l-1.954,0.651c-0.491,0.163 -0.491,0.857 0,1.021l1.949,0.649c0.618,0.206 1.102,0.692 1.306,1.311l0.653,1.984c0.162,0.493 0.86,0.493 1.022,0l0.651,-1.979c0.204,-0.621 0.692,-1.109 1.313,-1.313l1.98,-0.651c0.493,-0.162 0.493,-0.86 0,-1.022l-1.985,-0.653c-0.619,-0.204 -1.105,-0.688 -1.311,-1.305z"
android:fillColor="#000"/>
</vector>
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M14,2C20.627,2 26,7.373 26,14C26,20.627 20.627,26 14,26C7.373,26 2,20.627 2,14C2,7.373 7.373,2 14,2ZM14,20.5C11.914,20.5 9.92,21.082 8.203,22.149C9.838,23.315 11.839,24 14,24C16.161,24 18.161,23.315 19.796,22.15C18.079,21.082 16.086,20.5 14,20.5ZM14,4C8.477,4 4,8.477 4,14C4,16.616 5.004,18.997 6.648,20.779C8.786,19.308 11.331,18.5 14,18.5C16.669,18.5 19.215,19.308 21.353,20.777C22.996,18.996 24,16.615 24,14C24,8.477 19.523,4 14,4ZM14,7.5C16.624,7.5 18.75,9.626 18.75,12.25C18.75,14.874 16.624,17 14,17C11.376,17 9.25,14.874 9.25,12.25C9.25,9.626 11.376,7.5 14,7.5ZM14,9.5C12.48,9.5 11.25,10.73 11.25,12.25C11.25,13.77 12.48,15 14,15C15.52,15 16.75,13.77 16.75,12.25C16.75,10.73 15.52,9.5 14,9.5Z"
android:strokeWidth="1"
android:fillColor="#000"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="m5.231,9.995c-0.661,0.708 -1.231,1.941 -1.231,4.005s0.57,3.297 1.231,4.005c0.665,0.712 1.514,0.995 2.269,0.995s1.604,-0.282 2.269,-0.995c0.661,-0.708 1.231,-1.941 1.231,-4.005s-0.57,-3.297 -1.231,-4.005c-0.665,-0.712 -1.514,-0.995 -2.269,-0.995s-1.604,0.282 -2.269,0.995zM3.769,8.63c1.085,-1.163 2.486,-1.63 3.731,-1.63s2.646,0.468 3.731,1.63c1.089,1.167 1.769,2.934 1.769,5.37s-0.68,4.203 -1.769,5.37c-1.085,1.163 -2.486,1.63 -3.731,1.63s-2.646,-0.468 -3.731,-1.63c-1.089,-1.167 -1.769,-2.934 -1.769,-5.37s0.68,-4.203 1.769,-5.37zM17,8c0,-0.552 0.448,-1 1,-1h3.5c2.485,0 4.5,2.015 4.5,4.5 0,2.485 -2.015,4.5 -4.5,4.5h-2.5v1h2c0.552,0 1,0.448 1,1s-0.448,1 -1,1h-2v1c0,0.552 -0.448,1 -1,1s-1,-0.448 -1,-1v-1h-1c-0.552,0 -1,-0.448 -1,-1s0.448,-1 1,-1h1v-1h-1c-0.552,0 -1,-0.448 -1,-1s0.448,-1 1,-1h1zM19,14h2.5c1.381,0 2.5,-1.119 2.5,-2.5s-1.119,-2.5 -2.5,-2.5h-2.5z"
android:fillColor="#000"
android:fillType="evenOdd"/>
</vector>
+48 -1
View File
@@ -16,6 +16,18 @@
<string name="MenuFileOpenFileBigObject">Файл содержит более 500к треугольников. Нарезка может быть медленной.</string>
<string name="MenuFileOpenFileLoading">Загрузка файла…</string>
<string name="MenuFileDelete">Убрать модель</string>
<string name="MenuFileAIGenerator">Модель\nпо фото</string>
<string name="MenuFileAIGeneratorPleaseWaitSetup">Пожалуйста, подождите…</string>
<string name="MenuFileAIGeneratorErrorNotLoadedUserAccount">Ошибка: данные о пользователе пока не загружены.</string>
<string name="MenuFileAIGeneratorFromCamera">Сделать фото</string>
<string name="MenuFileAIGeneratorFromGallery">Выбрать из галереи</string>
<string name="MenuFileAIGeneratorRemaining">Осталось: %d / %d генераций</string>
<string name="MenuFileAIGeneratorUploading">Загрузка изображения…</string>
<string name="MenuFileAIGeneratorProcessing">Обработка изображения…</string>
<string name="MenuFileAIGeneratorDownloading">Скачивание модели…</string>
<string name="MenuFileAIGeneratorError">Не удалось сгенерировать модель</string>
<string name="MenuFileAIGeneratorSavedAs">Модель сохранена как %s.</string>
<string name="MenuFileAIGeneratorNoGenerationsLeft">Не осталось генераций.</string>
<string name="MenuFileCalibrations">Калибров.</string>
<string name="MenuFileCalibrationsLA">K3D Linear Advance</string>
<string name="MenuFileCalibrationsLADescription">Калибровка Linear/Pressure Advance</string>
@@ -155,6 +167,31 @@
<string name="SettingsProfileCopy">%s - Копия</string>
<string name="SettingsCloneProfile">Клон. текущий</string>
<string name="SettingsDeleteProfile">Удалить текущий</string>
<string name="SettingsCloudNotLoggedIn">Не авторизовано</string>
<string name="SettingsCloudLoading">Загрузка…</string>
<string name="SettingsCloudTapToManage">Нажмите для управления</string>
<string name="SettingsCloudTapToShowMore">Нажмите чтобы узнать больше</string>
<string name="SettingsCloudManageTitle">Аккаунт Beam 3D</string>
<string name="SettingsCloudManageDescription">Даёт следующие преимущества:</string>
<string name="SettingsCloudManageFeatureCloudSync">Облачная синхронизация профилей</string>
<string name="SettingsCloudManageFeatureCloudSyncDescription">Храните свои профили в облаке Beam</string>
<string name="SettingsCloudManageFeatureAIGenerator">ИИ генератор моделей</string>
<string name="SettingsCloudManageFeatureAIGeneratorDescription">%1$d моделей по фото в месяц</string>
<string name="SettingsCloudManageFeatureFreeForAll">Slice Beam может оставаться бесплатным для всех</string>
<string name="SettingsCloudManageFeatureFreeForAllDescription">Спасибо за вашу поддержку!</string>
<string name="SettingsCloudManageLevelRedirectMessage">При подписке на данный уровень вы соглашаетесь с условиями обслуживания.</string>
<string name="SettingsCloudManageLevelRedirectAlreadySubscribed">Уже подписаны?</string>
<string name="SettingsCloudManageFree">Бесплатно</string>
<string name="SettingsCloudManageSubscribed">Вы подписаны</string>
<string name="SettingsCloudManageWillBeLater">Будет позже</string>
<string name="SettingsCloudManageTermsOfService">Условия обслуживания</string>
<string name="SettingsCloudManageButtonManage">Настройки аккаунта</string>
<string name="SettingsCloudManageButtonLogIn">Войти</string>
<string name="SettingsCloudManageButtonLogInCancelTitle">Отмена</string>
<string name="SettingsCloudManageButtonLogInCancel">Отменить авторизацию?</string>
<string name="SettingsCloudManageButtonLogOut">Выйти</string>
<string name="SettingsCloudManageLoggedInAs">Вошли как «%1$s»</string>
<string name="SettingsCloudManageSubscription">Управление подпиской</string>
<string name="Changelog">Список изменений</string>
<string name="ChangelogBoostyDescription">Выход данного обновления поддержали:</string>
<string name="ChangelogNext">Далее</string>
@@ -162,9 +199,19 @@
<string name="OrcaConversionPleaseWait">Конвертация профилей, пожалуйста, подождите…</string>
<string name="OrcaConversionNotAConfigBundle">Это не пакет конфигураций</string>
<string name="AppCrashed">Что-то пошло не так</string>
<string name="AppCrashedDesc">Версия Android: %s\nУстройство: %s\nЛог: \n%s</string>
<string name="AppCrashedDesc">Версия Android: %1$s\nУстройство: %2$s\nЛог: \n%3$s</string>
<string name="AppCrashedShare">Поделиться</string>
<string name="AppCrashedRestart">Попытаться запустить приложение ещё раз</string>
<string name="BedConfigurationError">Ошибка конфигурации стола</string>
<string name="BedConfigurationErrorDesc">Вам необходимо исправить конфигурацию стола перед использованием.</string>
<string name="CloudSyncInProgress">Синхронизация с облаком…</string>
<string name="CloudSyncSuccess">Успешно синхронизировали профили.</string>
<string name="CloudSyncError">Не удалось синхронизировать профили, повторим попытку позже.</string>
<string name="CloudSyncConflict">Конфликт облачных профилей.</string>
<string name="CloudSyncConflictResolve">Разрешить</string>
<string name="CloudSyncConflictResolveMessage">Конфликт облачных профилей, пожалуйста, выберите какие профили вы хотите оставить.\n\nПоследнее изменение в облаке: %1$s\nПоследнее изменение на устройстве: %2$s</string>
<string name="CloudSyncConflictChooseRemote">Оставить облачные профили</string>
<string name="CloudSyncConflictChooseLocal">Оставить локальные профили</string>
<string name="Yes">Да</string>
<string name="No">Нет</string>
</resources>
+48 -1
View File
@@ -17,6 +17,18 @@
<string name="MenuFileOpenFileBigObject">File has more than 500k triangles. Processing could be slow.</string>
<string name="MenuFileOpenFileLoading">Loading file…</string>
<string name="MenuFileDelete">Remove model</string>
<string name="MenuFileAIGenerator">Model\nfrom photo</string>
<string name="MenuFileAIGeneratorPleaseWaitSetup">Please wait…</string>
<string name="MenuFileAIGeneratorErrorNotLoadedUserAccount">Error: user info not fetched yet.</string>
<string name="MenuFileAIGeneratorFromCamera">Take a photo</string>
<string name="MenuFileAIGeneratorFromGallery">Choose from gallery</string>
<string name="MenuFileAIGeneratorRemaining">Remaining: %d / %d generations</string>
<string name="MenuFileAIGeneratorUploading">Uploading image…</string>
<string name="MenuFileAIGeneratorProcessing">Processing image…</string>
<string name="MenuFileAIGeneratorDownloading">Downloading model…</string>
<string name="MenuFileAIGeneratorError">Failed to generate model</string>
<string name="MenuFileAIGeneratorSavedAs">Saved model as %s.</string>
<string name="MenuFileAIGeneratorNoGenerationsLeft">No generations left.</string>
<string name="MenuFileCalibrations">Calibrat.</string>
<string name="MenuFileCalibrationsLA">K3D Linear Advance</string>
<string name="MenuFileCalibrationsLADescription">Linear/Pressure Advance Calibration</string>
@@ -157,6 +169,31 @@
<string name="SettingsProfileCopy">%s - Copy</string>
<string name="SettingsCloneProfile">Clone current</string>
<string name="SettingsDeleteProfile">Delete current</string>
<string name="SettingsCloudNotLoggedIn">Not logged in</string>
<string name="SettingsCloudLoading">Loading…</string>
<string name="SettingsCloudTapToManage">Tap to manage</string>
<string name="SettingsCloudTapToShowMore">Tap to learn more</string>
<string name="SettingsCloudManageTitle">Beam 3D Account</string>
<string name="SettingsCloudManageDescription">Provides the following benefits:</string>
<string name="SettingsCloudManageFeatureCloudSync">Cloud profiles sync</string>
<string name="SettingsCloudManageFeatureCloudSyncDescription">Store your profiles in Beam Cloud</string>
<string name="SettingsCloudManageFeatureAIGenerator">AI model generator</string>
<string name="SettingsCloudManageFeatureAIGeneratorDescription">%1$d models from photo per month</string>
<string name="SettingsCloudManageFeatureFreeForAll">Slice Beam can remain free for all</string>
<string name="SettingsCloudManageFeatureFreeForAllDescription">Thanks for your support!</string>
<string name="SettingsCloudManageLevelRedirectMessage">By subscribing to this level you accept terms of service.</string>
<string name="SettingsCloudManageLevelRedirectAlreadySubscribed">Already subscribed?</string>
<string name="SettingsCloudManageFree">Free</string>
<string name="SettingsCloudManageSubscribed">You are subscribed</string>
<string name="SettingsCloudManageWillBeLater">Will be later</string>
<string name="SettingsCloudManageTermsOfService">Terms of service</string>
<string name="SettingsCloudManageButtonManage">Account settings</string>
<string name="SettingsCloudManageButtonLogIn">Login</string>
<string name="SettingsCloudManageButtonLogInCancelTitle">Cancel</string>
<string name="SettingsCloudManageButtonLogInCancel">Cancel login?</string>
<string name="SettingsCloudManageButtonLogOut">Log out</string>
<string name="SettingsCloudManageLoggedInAs">Logged in as «%1$s»</string>
<string name="SettingsCloudManageSubscription">Manage subscription</string>
<string name="Changelog">Changelog</string>
<string name="ChangelogBoosty" translatable="false">Boosty</string>
<string name="ChangelogBoostyDescription">The release of this update was supported by:</string>
@@ -165,9 +202,19 @@
<string name="OrcaConversionPleaseWait">Converting profiles, please wait…</string>
<string name="OrcaConversionNotAConfigBundle">Not a config bundle</string>
<string name="AppCrashed">Something went wrong</string>
<string name="AppCrashedDesc">Android version: %s\nDevice: %s\nLogs:\n%s</string>
<string name="AppCrashedDesc">Android version: %1$s\nDevice: %2$s\nLogs:\n%3$s</string>
<string name="AppCrashedShare">Share</string>
<string name="AppCrashedRestart">Try to start app again</string>
<string name="BedConfigurationError">Bed config error</string>
<string name="BedConfigurationErrorDesc">You should fix your bed configuration before usage.</string>
<string name="CloudSyncInProgress">Cloud synchronization in progress…</string>
<string name="CloudSyncSuccess">Successfully synchronized profiles.</string>
<string name="CloudSyncError">Failed to synchronize profiles, will retry later.</string>
<string name="CloudSyncConflict">Cloud profiles conflict.</string>
<string name="CloudSyncConflictResolve">Resolve</string>
<string name="CloudSyncConflictResolveMessage">Cloud profiles conflict, please select which profiles you want to keep.\n\nLast changed cloud: %1$s\nLast changed on device: %2$s</string>
<string name="CloudSyncConflictChooseRemote">Keep cloud profiles</string>
<string name="CloudSyncConflictChooseLocal">Keep local profiles</string>
<string name="Yes">Yes</string>
<string name="No">No</string>
</resources>
+2 -1
View File
@@ -1,6 +1,7 @@
rootProject.name = "Slice Beam"
include ':app', ':eventbus', ':eventbus_api', ':eventbus_processor'
include ':app', ':eventbus', ':eventbus_api', ':eventbus_processor', ':sapil'
project(':eventbus').projectDir = file('EventBus/eventbus')
project(':eventbus_api').projectDir = file('EventBus/eventbus_api')
project(':eventbus_processor').projectDir = file('EventBus/eventbus_processor')
project(':sapil').projectDir = file('SAPIL/sapil')