callback);
+
+ /**
+ * Uploads new data to the server
+ *
+ * @param data New base64 encoded data
+ *
+ * Requires authorization
+ */
+ @Method("sync/upload")
+ void syncUpload(@Arg("data") String data, APICallback callback);
+
+ /**
+ * Downloads base64 data
+ *
+ * Requires authorization
+ */
+ @Method("sync/get")
+ void syncGet(APICallback callback);
+
+ /**
+ * Generates 3D model from image
+ *
+ * @param image Base64 encoded image
+ *
+ * Requires authorization
+ */
+ @Method(requestType = RequestType.POST, value = "models/generate")
+ void modelsGenerate(@Arg("") String image, @Header("Content-Type") String type, APICallback callback);
+
+ /**
+ * Gets remaining model generations count
+ *
+ * Requires authorization
+ */
+ @Method("models/getRemainingCount")
+ void modelsGetRemainingCount(APICallback callback);
+
+ /**
+ * Destroys token
+ *
+ * Requires authorization
+ */
+ @Method("logout")
+ void logout(APICallback 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 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;
+ }
+}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudController.java b/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudController.java
new file mode 100644
index 0000000..2551811
--- /dev/null
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudController.java
@@ -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() {
+ @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() {
+ @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() {
+ @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() {
+ @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() {
+ @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() {
+ @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();
+ }
+ }
+}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/components/CloudManageBottomSheet.java b/app/src/main/java/ru/ytkab0bp/slicebeam/components/CloudManageBottomSheet.java
new file mode 100644
index 0000000..c535518
--- /dev/null
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/components/CloudManageBottomSheet.java
@@ -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 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);
+ }
+}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/BedMenuItem.java b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/BedMenuItem.java
index 9be4d9b..63ad83f 100644
--- a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/BedMenuItem.java
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/BedMenuItem.java
@@ -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 {
@@ -36,6 +47,7 @@ public class BedMenuItem extends SimpleRecyclerItem sparkles;
+ private long lastDraw;
+ private Drawable sparkleDrawable;
public BedMenuItemHolderView(Context context) {
super(context);
@@ -109,12 +134,17 @@ public class BedMenuItem extends SimpleRecyclerItem();
+ 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 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 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 onCreateItems(boolean portrait) {
- return Arrays.asList(
+ wasPortrait = portrait;
+ List 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);
}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/ListBedMenu.java b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/ListBedMenu.java
index a380342..7f46a5f 100644
--- a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/ListBedMenu.java
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/ListBedMenu.java
@@ -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
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudFeaturesUpdatedEvent.java b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudFeaturesUpdatedEvent.java
new file mode 100644
index 0000000..33ba49e
--- /dev/null
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudFeaturesUpdatedEvent.java
@@ -0,0 +1,6 @@
+package ru.ytkab0bp.slicebeam.events;
+
+import ru.ytkab0bp.eventbus.Event;
+
+@Event
+public class CloudFeaturesUpdatedEvent {}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudLoginStateUpdatedEvent.java b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudLoginStateUpdatedEvent.java
new file mode 100644
index 0000000..ce29678
--- /dev/null
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudLoginStateUpdatedEvent.java
@@ -0,0 +1,6 @@
+package ru.ytkab0bp.slicebeam.events;
+
+import ru.ytkab0bp.eventbus.Event;
+
+@Event
+public class CloudLoginStateUpdatedEvent {}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudModelsRemainingCountUpdatedEvent.java b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudModelsRemainingCountUpdatedEvent.java
new file mode 100644
index 0000000..e3d5b78
--- /dev/null
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudModelsRemainingCountUpdatedEvent.java
@@ -0,0 +1,6 @@
+package ru.ytkab0bp.slicebeam.events;
+
+import ru.ytkab0bp.eventbus.Event;
+
+@Event
+public class CloudModelsRemainingCountUpdatedEvent {}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudUserInfoUpdatedEvent.java b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudUserInfoUpdatedEvent.java
new file mode 100644
index 0000000..611f6a3
--- /dev/null
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/events/CloudUserInfoUpdatedEvent.java
@@ -0,0 +1,7 @@
+package ru.ytkab0bp.slicebeam.events;
+
+import ru.ytkab0bp.eventbus.Event;
+
+@Event
+public class CloudUserInfoUpdatedEvent {
+}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/events/NeedDismissAIGeneratorMenu.java b/app/src/main/java/ru/ytkab0bp/slicebeam/events/NeedDismissAIGeneratorMenu.java
new file mode 100644
index 0000000..eeaff86
--- /dev/null
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/events/NeedDismissAIGeneratorMenu.java
@@ -0,0 +1,6 @@
+package ru.ytkab0bp.slicebeam.events;
+
+import ru.ytkab0bp.eventbus.Event;
+
+@Event
+public class NeedDismissAIGeneratorMenu {}
\ No newline at end of file
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java
index bc8543a..a421846 100644
--- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java
@@ -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,46 +293,58 @@ public class BedFragment extends Fragment {
}
swipeDownLayout = new BedSwipeDownLayout(ctx);
- panelWebView = new WebView(ctx);
- panelWebView.getSettings().setJavaScriptEnabled(true);
- panelWebView.setWebViewClient(new WebViewClient() {
- @Override
- public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
- webViewErrDescription.setText(description);
- panelWebViewError.setVisibility(View.VISIBLE);
- panelWebViewError.setAlpha(0f);
- panelWebViewError.animate().alpha(1).setDuration(150).setListener(null).start();
- webViewProgressBar.animate().alpha(0).setDuration(150).start();
- }
-
- @Override
- public void onPageFinished(WebView view, String url) {
- 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);
- panelWebViewError.setOrientation(LinearLayout.VERTICAL);
- panelWebViewError.setGravity(Gravity.CENTER);
- panelWebViewError.setPadding(ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12));
- webViewErrIcon = new ImageView(ctx);
- webViewErrIcon.setImageResource(R.drawable.globe_cross_outline_28);
- panelWebViewError.addView(webViewErrIcon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
- bottomMargin = ViewUtils.dp(8);
- }});
- webViewErrDescription = new TextView(ctx);
- webViewErrDescription.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
- webViewErrDescription.setGravity(Gravity.CENTER);
- panelWebViewError.addView(webViewErrDescription);
- wfl.addView(panelWebViewError, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ 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(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ panelWebView.setVisibility(View.GONE);
+ }
+ }).start();
+ webViewProgressBar.animate().alpha(0).setDuration(150).start();
+ }
- webViewProgressBar = new ProgressBar(ctx);
- webViewProgressBar.setAlpha(0f);
- wfl.addView(webViewProgressBar, new FrameLayout.LayoutParams(ViewUtils.dp(36), ViewUtils.dp(36), Gravity.CENTER));
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ if (!hasWebError) {
+ panelWebView.animate().alpha(1).setDuration(150).start();
+ webViewProgressBar.animate().alpha(0).setDuration(150).start();
+ }
+ }
+ });
+
+ wfl.addView(panelWebView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+ panelWebViewError = new LinearLayout(ctx);
+ panelWebViewError.setVisibility(View.GONE);
+ panelWebViewError.setOrientation(LinearLayout.VERTICAL);
+ panelWebViewError.setGravity(Gravity.CENTER);
+ panelWebViewError.setPadding(ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12), ViewUtils.dp(12));
+ webViewErrIcon = new ImageView(ctx);
+ webViewErrIcon.setImageResource(R.drawable.globe_cross_outline_28);
+ panelWebViewError.addView(webViewErrIcon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
+ bottomMargin = ViewUtils.dp(8);
+ }});
+ webViewErrDescription = new TextView(ctx);
+ webViewErrDescription.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
+ webViewErrDescription.setGravity(Gravity.CENTER);
+ panelWebViewError.addView(webViewErrDescription);
+ wfl.addView(panelWebViewError, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+ 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();
- 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)));
+ 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) {
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java
index 7d7e769..d7a2620 100644
--- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java
@@ -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, Integer> viewType = new HashMap<>();
private Map 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;
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/SettingsFragment.java b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/SettingsFragment.java
index bfdbefb..23ada81 100644
--- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/SettingsFragment.java
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/SettingsFragment.java
@@ -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 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() {}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/recycler/PreferenceItem.java b/app/src/main/java/ru/ytkab0bp/slicebeam/recycler/PreferenceItem.java
index c3d2daf..da6b3aa 100644
--- a/app/src/main/java/ru/ytkab0bp/slicebeam/recycler/PreferenceItem.java
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/recycler/PreferenceItem.java
@@ -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 typefaceCache = new HashMap<>();
+
+ public static Handler getUiHandler() {
+ return uiHandler;
+ }
+
public static void postOnMainThread(Runnable runnable) {
uiHandler.post(runnable);
}
diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/view/SegmentsView.java b/app/src/main/java/ru/ytkab0bp/slicebeam/view/SegmentsView.java
index 49702e1..7515c93 100644
--- a/app/src/main/java/ru/ytkab0bp/slicebeam/view/SegmentsView.java
+++ b/app/src/main/java/ru/ytkab0bp/slicebeam/view/SegmentsView.java
@@ -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);
}
}
diff --git a/app/src/main/jni/slicebeam/beam_native.cpp b/app/src/main/jni/slicebeam/beam_native.cpp
index ede0af4..737b19d 100644
--- a/app/src/main/jni/slicebeam/beam_native.cpp
+++ b/app/src/main/jni/slicebeam/beam_native.cpp
@@ -1243,17 +1243,23 @@ 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;
- int location = shader->program.get_uniform_location(chars);
- env->ReleaseStringUTFChars(name, chars);
- return location;
+ 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;
- int location = shader->program.get_attrib_location(chars);
- env->ReleaseStringUTFChars(name, chars);
- return location;
+ 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) {
diff --git a/app/src/main/res/drawable/box_heart_outline_28.xml b/app/src/main/res/drawable/box_heart_outline_28.xml
new file mode 100644
index 0000000..1fffd7b
--- /dev/null
+++ b/app/src/main/res/drawable/box_heart_outline_28.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/brain_outline_28.xml b/app/src/main/res/drawable/brain_outline_28.xml
new file mode 100644
index 0000000..c4a1b2e
--- /dev/null
+++ b/app/src/main/res/drawable/brain_outline_28.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/cloud_plus_outline_28.xml b/app/src/main/res/drawable/cloud_plus_outline_28.xml
new file mode 100644
index 0000000..386d925
--- /dev/null
+++ b/app/src/main/res/drawable/cloud_plus_outline_28.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/external_link_outline_24.xml b/app/src/main/res/drawable/external_link_outline_24.xml
new file mode 100644
index 0000000..e1207b4
--- /dev/null
+++ b/app/src/main/res/drawable/external_link_outline_24.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/hand_point_up_outline_28.xml b/app/src/main/res/drawable/hand_point_up_outline_28.xml
deleted file mode 100644
index 1375ce6..0000000
--- a/app/src/main/res/drawable/hand_point_up_outline_28.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/picture_outline_28.xml b/app/src/main/res/drawable/picture_outline_28.xml
new file mode 100644
index 0000000..53c639c
--- /dev/null
+++ b/app/src/main/res/drawable/picture_outline_28.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/picture_stack_outline_28.xml b/app/src/main/res/drawable/picture_stack_outline_28.xml
new file mode 100644
index 0000000..4f4cd1b
--- /dev/null
+++ b/app/src/main/res/drawable/picture_stack_outline_28.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable/sparkle_28.xml b/app/src/main/res/drawable/sparkle_28.xml
new file mode 100644
index 0000000..c961007
--- /dev/null
+++ b/app/src/main/res/drawable/sparkle_28.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/stars_outline_28.xml b/app/src/main/res/drawable/stars_outline_28.xml
new file mode 100644
index 0000000..5ede2a1
--- /dev/null
+++ b/app/src/main/res/drawable/stars_outline_28.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/user_circle_outline_28.xml b/app/src/main/res/drawable/user_circle_outline_28.xml
new file mode 100644
index 0000000..d2735f4
--- /dev/null
+++ b/app/src/main/res/drawable/user_circle_outline_28.xml
@@ -0,0 +1,12 @@
+
+
+
diff --git a/app/src/main/res/drawable/zero_ruble_outline_28.xml b/app/src/main/res/drawable/zero_ruble_outline_28.xml
new file mode 100644
index 0000000..811a77f
--- /dev/null
+++ b/app/src/main/res/drawable/zero_ruble_outline_28.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 9d19927..08533c3 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -16,6 +16,18 @@
Файл содержит более 500к треугольников. Нарезка может быть медленной.
Загрузка файла…
Убрать модель
+ Модель\nпо фото
+ Пожалуйста, подождите…
+ Ошибка: данные о пользователе пока не загружены.
+ Сделать фото
+ Выбрать из галереи
+ Осталось: %d / %d генераций
+ Загрузка изображения…
+ Обработка изображения…
+ Скачивание модели…
+ Не удалось сгенерировать модель
+ Модель сохранена как %s.
+ Не осталось генераций.
Калибров.
K3D Linear Advance
Калибровка Linear/Pressure Advance
@@ -155,6 +167,31 @@
%s - Копия
Клон. текущий
Удалить текущий
+ Не авторизовано
+ Загрузка…
+ Нажмите для управления
+ Нажмите чтобы узнать больше
+ Аккаунт Beam 3D
+ Даёт следующие преимущества:
+ Облачная синхронизация профилей
+ Храните свои профили в облаке Beam
+ ИИ генератор моделей
+ %1$d моделей по фото в месяц
+ Slice Beam может оставаться бесплатным для всех
+ Спасибо за вашу поддержку!
+ При подписке на данный уровень вы соглашаетесь с условиями обслуживания.
+ Уже подписаны?
+ Бесплатно
+ Вы подписаны
+ Будет позже
+ Условия обслуживания
+ Настройки аккаунта
+ Войти
+ Отмена
+ Отменить авторизацию?
+ Выйти
+ Вошли как «%1$s»
+ Управление подпиской
Список изменений
Выход данного обновления поддержали:
Далее
@@ -162,9 +199,19 @@
Конвертация профилей, пожалуйста, подождите…
Это не пакет конфигураций
Что-то пошло не так
- Версия Android: %s\nУстройство: %s\nЛог: \n%s
+ Версия Android: %1$s\nУстройство: %2$s\nЛог: \n%3$s
Поделиться
Попытаться запустить приложение ещё раз
Ошибка конфигурации стола
Вам необходимо исправить конфигурацию стола перед использованием.
+ Синхронизация с облаком…
+ Успешно синхронизировали профили.
+ Не удалось синхронизировать профили, повторим попытку позже.
+ Конфликт облачных профилей.
+ Разрешить
+ Конфликт облачных профилей, пожалуйста, выберите какие профили вы хотите оставить.\n\nПоследнее изменение в облаке: %1$s\nПоследнее изменение на устройстве: %2$s
+ Оставить облачные профили
+ Оставить локальные профили
+ Да
+ Нет
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c2072ea..c2f312a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -17,6 +17,18 @@
File has more than 500k triangles. Processing could be slow.
Loading file…
Remove model
+ Model\nfrom photo
+ Please wait…
+ Error: user info not fetched yet.
+ Take a photo
+ Choose from gallery
+ Remaining: %d / %d generations
+ Uploading image…
+ Processing image…
+ Downloading model…
+ Failed to generate model
+ Saved model as %s.
+ No generations left.
Calibrat.
K3D Linear Advance
Linear/Pressure Advance Calibration
@@ -157,6 +169,31 @@
%s - Copy
Clone current
Delete current
+ Not logged in
+ Loading…
+ Tap to manage
+ Tap to learn more
+ Beam 3D Account
+ Provides the following benefits:
+ Cloud profiles sync
+ Store your profiles in Beam Cloud
+ AI model generator
+ %1$d models from photo per month
+ Slice Beam can remain free for all
+ Thanks for your support!
+ By subscribing to this level you accept terms of service.
+ Already subscribed?
+ Free
+ You are subscribed
+ Will be later
+ Terms of service
+ Account settings
+ Login
+ Cancel
+ Cancel login?
+ Log out
+ Logged in as «%1$s»
+ Manage subscription
Changelog
Boosty
The release of this update was supported by:
@@ -165,9 +202,19 @@
Converting profiles, please wait…
Not a config bundle
Something went wrong
- Android version: %s\nDevice: %s\nLogs:\n%s
+ Android version: %1$s\nDevice: %2$s\nLogs:\n%3$s
Share
Try to start app again
Bed config error
You should fix your bed configuration before usage.
+ Cloud synchronization in progress…
+ Successfully synchronized profiles.
+ Failed to synchronize profiles, will retry later.
+ Cloud profiles conflict.
+ Resolve
+ Cloud profiles conflict, please select which profiles you want to keep.\n\nLast changed cloud: %1$s\nLast changed on device: %2$s
+ Keep cloud profiles
+ Keep local profiles
+ Yes
+ No
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 6510bba..882e7ba 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -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')