diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/MainActivity.java b/app/src/main/java/ru/ytkab0bp/slicebeam/MainActivity.java index 9a9ca3e..d1f90bb 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/MainActivity.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/MainActivity.java @@ -85,6 +85,8 @@ public class MainActivity extends AppCompatActivity { public static List EXPORTING_FILAMENTS; public static List EXPORTING_PRINTERS; + public static boolean IS_GENERATING_AI_MODEL; + public static File aiTempFile; private static SparseArray liveDelegate = new SparseArray<>(); @@ -489,6 +491,7 @@ public class MainActivity extends AppCompatActivity { } private void generateAiModel(Bitmap bm) { + IS_GENERATING_AI_MODEL = true; String uploadTag = UUID.randomUUID().toString(); SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorUploading).tag(uploadTag)); IOUtils.IO_POOL.submit(()->{ @@ -572,6 +575,7 @@ public class MainActivity extends AppCompatActivity { SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileAIGeneratorSavedAs, fileName)); loadFile(f, true); CloudController.checkGeneratorRemaining(); + IS_GENERATING_AI_MODEL = false; } @Override @@ -582,6 +586,7 @@ public class MainActivity extends AppCompatActivity { .setMessage(e.toString()) .setPositiveButton(android.R.string.ok, null) .show()); + IS_GENERATING_AI_MODEL = false; } }); SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(uploadTag)); diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/SetupActivity.java b/app/src/main/java/ru/ytkab0bp/slicebeam/SetupActivity.java index a6201de..6f82cbf 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/SetupActivity.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/SetupActivity.java @@ -22,6 +22,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.opengl.GLSurfaceView; +import android.os.Build; import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -653,7 +654,7 @@ public class SetupActivity extends AppCompatActivity { private FrameLayout buttonView; private TextView buttonText; private ProgressBar buttonProgress; - private RecyclerView recyclerView; + private FadeRecyclerView recyclerView; @Override public View onCreateView(Context ctx) { @@ -670,8 +671,8 @@ public class SetupActivity extends AppCompatActivity { ll.addView(title); FrameLayout fl = new FrameLayout(ctx); - recyclerView = new RecyclerView(ctx); - recyclerView.setLayoutManager(new LinearLayoutManager(ctx)); + recyclerView = new FadeRecyclerView(ctx); + recyclerView.setBitmapMode(); 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)); @@ -901,6 +902,14 @@ public class SetupActivity extends AppCompatActivity { CloudAPI.UserFeatures features = CloudController.getUserFeatures(); CloudAPI.UserInfo info = CloudController.getUserInfo(); Context ctx = getContext(); + if (!BuildConfig.IS_GOOGLE_PLAY && features.earlyAccessLevel != -1 && lvl.level >= features.earlyAccessLevel) { + items.add(new PreferenceItem() + .setForceDark(true) + .setPaddings(ViewUtils.dp(8)) + .setIcon(R.drawable.clock_circle_dashed_outline_24) + .setTitle(ctx.getString(R.string.SettingsCloudManageFeatureEarlyAccess)) + .setSubtitle(ctx.getString(R.string.SettingsCloudManageFeatureEarlyAccessDescription))); + } if (features.syncRequiredLevel != -1 && lvl.level >= features.syncRequiredLevel) { items.add(new PreferenceItem() .setForceDark(true) diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/boot/CloudCachedInitTask.java b/app/src/main/java/ru/ytkab0bp/slicebeam/boot/CloudCachedInitTask.java new file mode 100644 index 0000000..a5c6e52 --- /dev/null +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/boot/CloudCachedInitTask.java @@ -0,0 +1,12 @@ +package ru.ytkab0bp.slicebeam.boot; + +import java.util.Collections; + +import ru.ytkab0bp.slicebeam.cloud.CloudController; + +public class CloudCachedInitTask extends BootTask { + public CloudCachedInitTask() { + super(Collections.singletonList(PrefsTask.class), CloudController::initCached); + onWorker(); + } +} diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/boot/CloudInitTask.java b/app/src/main/java/ru/ytkab0bp/slicebeam/boot/CloudInitTask.java index 1f5bcd1..66e8417 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/boot/CloudInitTask.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/boot/CloudInitTask.java @@ -6,7 +6,7 @@ import ru.ytkab0bp.slicebeam.cloud.CloudController; public class CloudInitTask extends BootTask { public CloudInitTask() { - super(Arrays.asList(PrefsTask.class, TrueTimeTask.class, LoadSlic3rConfigTask.class), CloudController::init); + super(Arrays.asList(PrefsTask.class, TrueTimeTask.class, LoadSlic3rConfigTask.class, CloudCachedInitTask.class), CloudController::init); onWorker(); nonCritical = true; } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/boot/TrueTimeTask.java b/app/src/main/java/ru/ytkab0bp/slicebeam/boot/TrueTimeTask.java index c6fe9ba..e9ec701 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/boot/TrueTimeTask.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/boot/TrueTimeTask.java @@ -10,6 +10,7 @@ import java.net.InetAddress; import java.util.Date; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import kotlinx.coroutines.Dispatchers; import ru.ytkab0bp.slicebeam.SliceBeam; @@ -65,7 +66,7 @@ public class TrueTimeTask extends BootTask { }); SliceBeam.TRUE_TIME.sync(); try { - latch.await(); + latch.await(300, TimeUnit.MILLISECONDS); } catch (InterruptedException ignored) {} }); onWorker(); diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudAPI.java b/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudAPI.java index 2c6cff3..2ae1f1d 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudAPI.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudAPI.java @@ -92,8 +92,8 @@ public interface CloudAPI extends APIRunner { *

* Requires authorization */ - @Method("sync/upload") - void syncUpload(@Arg("data") String data, APICallback callback); + @Method(requestType = RequestType.POST, value = "sync/upload") + void syncUpload(@Arg("") String data, @Header("Content-Type") String type, APICallback callback); /** * Downloads base64 data @@ -159,6 +159,11 @@ public interface CloudAPI extends APIRunner { } final class UserFeatures { + /** + * Which level is required for early access + */ + public int earlyAccessLevel; + /** * Which level is required for data sync */ diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudController.java b/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudController.java index 2551811..a0671f1 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudController.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/cloud/CloudController.java @@ -2,27 +2,40 @@ package ru.ytkab0bp.slicebeam.cloud; import android.content.Intent; import android.net.Uri; +import android.util.Base64; import android.util.Log; import com.google.gson.Gson; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + import ru.ytkab0bp.sapil.APICallback; import ru.ytkab0bp.sapil.APIRequestHandle; import ru.ytkab0bp.slicebeam.R; import ru.ytkab0bp.slicebeam.SliceBeam; +import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder; 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.slic3r.Slic3rConfigWrapper; +import ru.ytkab0bp.slicebeam.utils.IOUtils; 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"; + public 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 @@ -71,11 +84,20 @@ public class CloudController { private static Gson gson = new Gson(); - public static void init() { + public static void initCached() { if (Prefs.getCloudCachedUserFeatures() != null) { userFeatures = gson.fromJson(Prefs.getCloudCachedUserFeatures(), CloudAPI.UserFeatures.class); - SliceBeam.EVENT_BUS.fireEvent(new CloudFeaturesUpdatedEvent()); } + if (Prefs.getCloudAPIToken() != null) { + if (Prefs.getCloudCachedUserInfo() != null) { + userInfo = gson.fromJson(Prefs.getCloudCachedUserInfo(), CloudAPI.UserInfo.class); + modelsUsed = Prefs.getCloudCachedUsedModels(); + modelsMaxGenerations = Prefs.getCloudCachedMaxModels(); + } + } + } + + public static void init() { long now = SliceBeam.TRUE_TIME.now().getTime(); boolean needSyncInfo = userFeatures == null || now - Prefs.getCloudLastFeaturesSync() > MIN_SYNC_FEATURES_DELTA; if (needSyncInfo) { @@ -83,15 +105,15 @@ public class CloudController { } 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(); } + + if (!needSyncInfo && userInfo != null && isSyncAvailable() && Prefs.isCloudProfileSyncEnabled()) { + if (now - Prefs.getCloudLastSync() > MIN_SYNC_DELTA) { + syncData(); + } + } } } @@ -123,10 +145,7 @@ public class CloudController { } if (isSyncAvailable() && Prefs.isCloudProfileSyncEnabled()) { - long now = SliceBeam.TRUE_TIME.now().getTime(); - if (now != Prefs.getLocalLastModified()) { - sendData(); - } + syncData(); } checkGeneratorRemaining(); } @@ -254,30 +273,140 @@ public class CloudController { 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() { + private static void downloadData(long lastModified) { + CloudAPI.INSTANCE.syncGet(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)); + public void onResponse(String response) { + IOUtils.IO_POOL.submit(() -> { + try { + File f = SliceBeam.getConfigFile(); + byte[] data = Base64.decode(response, 0); + FileOutputStream fos = new FileOutputStream(f); + fos.write(data); + fos.close(); + + SliceBeam.CONFIG = new Slic3rConfigWrapper(f); + + Prefs.setCloudLocalLastModified(lastModified); + Prefs.setCloudLocalLastSentModified(lastModified); + Prefs.setCloudRemoteLastModified(lastModified); + Prefs.setCloudLastSync(SliceBeam.TRUE_TIME.now().getTime()); + SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG)); + SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.CloudSyncSuccess)); + } catch (IOException e) { + Log.e(TAG, "Failed to write 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)); + } + }); } @Override public void onException(Exception e) { - Log.e(TAG, "Failed to upload sync data", e); + Log.e(TAG, "Failed to download 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)); + } + }); + } + + private static void syncData() { + if (isSyncInProgress) { + return; + } + long modified = Prefs.getCloudLocalLastModified(); + isSyncInProgress = true; + SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.CloudSyncInProgress).tag(CLOUD_SYNC_TAG)); + + CloudAPI.INSTANCE.syncGetState(new APICallback() { + @Override + public void onResponse(CloudAPI.SyncState response) { + if (response.usedSize == 0) { + // No data on server yet, send anyway + uploadData(modified); + } else if (response.lastUpdatedDate != Prefs.getCloudRemoteLastModified()) { + if (Prefs.getCloudLocalLastSentModified() == modified) { + // Modified only on server + downloadData(response.lastUpdatedDate); + } else { + // Modified on client and on server + SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG)); + SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.WARNING, R.string.CloudSyncConflict).button(R.string.CloudSyncConflictResolve, v -> { + SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.getDefault()); + new BeamAlertDialogBuilder(v.getContext()) + .setTitle(R.string.CloudSyncConflict) + .setMessage(v.getContext().getString(R.string.CloudSyncConflictResolveMessage, format.format(new Date(response.lastUpdatedDate)), format.format(new Date(Prefs.getCloudLocalLastModified())))) + .setPositiveButton(R.string.CloudSyncConflictChooseRemote, (dialog, which) -> downloadData(response.lastUpdatedDate)) + .setNegativeButton(R.string.CloudSyncConflictChooseLocal, (dialog, which) -> uploadData(modified)) + .show(); + }).tag(CLOUD_SYNC_TAG)); + } + } else { + if (Prefs.getCloudLocalLastSentModified() != modified) { + // Modified only on client + uploadData(modified); + } else { + // Not modified on server and on client + SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG)); + } + } + } + + @Override + public void onException(Exception e) { + Log.e(TAG, "Failed to get sync state", e); + isSyncInProgress = false; + + SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG)); + SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError)); + } + }); + } + + private static void uploadData(long modified) { + IOUtils.IO_POOL.submit(() -> { + try { + File f = SliceBeam.getConfigFile(); + FileInputStream fis = new FileInputStream(f); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buffer = new byte[10240]; + int c; + while ((c = fis.read(buffer)) != -1) { + bos.write(buffer, 0, c); + } + bos.close(); + fis.close(); + + CloudAPI.INSTANCE.syncUpload(Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP), "application/ini", new APICallback() { + @Override + public void onResponse(CloudAPI.SyncState response) { + isSyncInProgress = false; + if (Prefs.getCloudLocalLastModified() != modified) { // Re-send otherwise + syncData(); + return; + } + Prefs.setCloudRemoteLastModified(response.lastUpdatedDate); + Prefs.setCloudLocalLastSentModified(modified); + Prefs.setCloudLastSync(SliceBeam.TRUE_TIME.now().getTime()); + 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)); + } + }); + } catch (IOException e) { + Log.e(TAG, "Failed to read sync data", e); isSyncInProgress = false; SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG)); @@ -288,12 +417,12 @@ public class CloudController { public static void notifyDataChanged() { long now = SliceBeam.TRUE_TIME.now().getTime(); - Prefs.setLocalLastModified(now); + Prefs.setCloudLocalLastModified(now); if (!isSyncAvailable() || !Prefs.isCloudProfileSyncEnabled()) { return; } if (now - Prefs.getCloudLastSync() > MIN_SYNC_DELTA) { - sendData(); + syncData(); } } } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/components/CloudManageBottomSheet.java b/app/src/main/java/ru/ytkab0bp/slicebeam/components/CloudManageBottomSheet.java index c535518..7c540b1 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/components/CloudManageBottomSheet.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/components/CloudManageBottomSheet.java @@ -25,8 +25,10 @@ import java.util.ArrayList; import java.util.List; 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.events.NeedDismissSnackbarEvent; import ru.ytkab0bp.slicebeam.recycler.PreferenceSwitchItem; import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerAdapter; import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem; @@ -94,6 +96,8 @@ public class CloudManageBottomSheet extends BottomSheetDialog { Prefs.setCloudProfileSyncEnabled(isChecked); if (isChecked) { CloudController.notifyDataChanged(); + } else { + SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CloudController.CLOUD_SYNC_TAG)); } })); } @@ -105,8 +109,8 @@ public class CloudManageBottomSheet extends BottomSheetDialog { 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); + topMargin = ViewUtils.dp(16); + leftMargin = rightMargin = ViewUtils.dp(16); }}); } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/FileMenu.java b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/FileMenu.java index 8d5f2eb..d651819 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/FileMenu.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/FileMenu.java @@ -303,6 +303,10 @@ public class FileMenu extends ListBedMenu { SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft)); return; } + if (MainActivity.IS_GENERATING_AI_MODEL) { + SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.WARNING, R.string.MenuFileAIGeneratorAlreadyGenerating)); + return; + } if (ctx instanceof MainActivity) { try { MainActivity.aiTempFile = File.createTempFile("ai_capture", ".jpg"); @@ -320,6 +324,10 @@ public class FileMenu extends ListBedMenu { SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft)); return; } + if (MainActivity.IS_GENERATING_AI_MODEL) { + SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.WARNING, R.string.MenuFileAIGeneratorAlreadyGenerating)); + return; + } if (ctx instanceof MainActivity) { Intent intent = new Intent(); intent.setType("image/*"); diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/events/NeedSnackbarEvent.java b/app/src/main/java/ru/ytkab0bp/slicebeam/events/NeedSnackbarEvent.java index b10115b..dbc997b 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/events/NeedSnackbarEvent.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/events/NeedSnackbarEvent.java @@ -1,5 +1,7 @@ package ru.ytkab0bp.slicebeam.events; +import android.view.View; + import ru.ytkab0bp.eventbus.Event; import ru.ytkab0bp.slicebeam.SliceBeam; import ru.ytkab0bp.slicebeam.view.SnackbarsLayout; @@ -10,6 +12,9 @@ public class NeedSnackbarEvent { public SnackbarsLayout.Type type = SnackbarsLayout.Type.DONE; public String tag; + public CharSequence buttonTitle; + public View.OnClickListener buttonClick; + public NeedSnackbarEvent(SnackbarsLayout.Type type, CharSequence title) { this.type = type; this.title = title; @@ -32,4 +37,10 @@ public class NeedSnackbarEvent { this.tag = tag; return this; } + + public NeedSnackbarEvent button(int title, View.OnClickListener click) { + this.buttonTitle = SliceBeam.INSTANCE.getString(title); + this.buttonClick = click; + return this; + } } 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 a421846..d202d73 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/BedFragment.java @@ -144,6 +144,11 @@ public class BedFragment extends Fragment { if (e.tag != null) { s.tag(e.tag); } + if (e.buttonTitle != null) { + s.lifetime = 0; + s.buttonTitle = e.buttonTitle; + s.buttonClick = e.buttonClick; + } snackbarsLayout.show(s); } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/utils/Prefs.java b/app/src/main/java/ru/ytkab0bp/slicebeam/utils/Prefs.java index 9d02b78..77b0624 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/utils/Prefs.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/utils/Prefs.java @@ -203,19 +203,27 @@ public class Prefs { mPrefs.edit().putLong("cloud_last_sync", ls).apply(); } - public static long getLocalLastModified() { + public static long getCloudLocalLastSentModified() { + return mPrefs.getLong("cloud_local_last_sent_modified", 0); + } + + public static void setCloudLocalLastSentModified(long lm) { + mPrefs.edit().putLong("cloud_local_last_sent_modified", lm).apply(); + } + + public static long getCloudLocalLastModified() { return mPrefs.getLong("cloud_local_last_modified", 0); } - public static void setLocalLastModified(long lm) { + public static void setCloudLocalLastModified(long lm) { mPrefs.edit().putLong("cloud_local_last_modified", lm).apply(); } - public static long getRemoteLastModified() { + public static long getCloudRemoteLastModified() { return mPrefs.getLong("cloud_remote_last_modified", 0); } - public static void setRemoteLastModified(long lm) { + public static void setCloudRemoteLastModified(long lm) { mPrefs.edit().putLong("cloud_remote_last_modified", lm).apply(); } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/view/FadeRecyclerView.java b/app/src/main/java/ru/ytkab0bp/slicebeam/view/FadeRecyclerView.java index 9ccc61d..6e36b9b 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/view/FadeRecyclerView.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/view/FadeRecyclerView.java @@ -1,9 +1,13 @@ package ru.ytkab0bp.slicebeam.view; import android.content.Context; +import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.Shader; import android.view.View; @@ -25,6 +29,10 @@ public class FadeRecyclerView extends RecyclerView implements IThemeView { private float topProgress, bottomProgress; private float overlayAlpha = 1f; + private Bitmap bitmap; + private Canvas bitmapCanvas; + private boolean bitmapMode; + public FadeRecyclerView(@NonNull Context context) { super(context); @@ -52,6 +60,16 @@ public class FadeRecyclerView extends RecyclerView implements IThemeView { onApplyTheme(); } + /** + * Very heavy, should be used only if transparent background is really needed + */ + public void setBitmapMode() { + this.bitmapMode = true; + topPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + bottomPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + invalidateShaders(); + } + public void setOverlayAlpha(float overlayAlpha) { this.overlayAlpha = overlayAlpha; invalidate(); @@ -59,15 +77,56 @@ public class FadeRecyclerView extends RecyclerView implements IThemeView { @Override public void draw(Canvas c) { - super.draw(c); + Canvas cv; + if (bitmapMode) { + if (bitmap == null || bitmap.getWidth() != getWidth() || bitmap.getHeight() != getHeight()) { + if (bitmap != null) { + bitmap.recycle(); + } + bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + bitmapCanvas = new Canvas(bitmap); + } + bitmap.eraseColor(Color.TRANSPARENT); + cv = bitmapCanvas; + super.draw(cv); + } else { + super.draw(cv = c); + } if (topProgress > 0) { - topPaint.setAlpha((int) (topProgress * overlayAlpha * 0xFF)); - c.drawRect(0, 0, getWidth(), ViewUtils.dp(HEIGHT_DP), topPaint); + cv.save(); + if (bitmapMode) { + cv.translate(0, -ViewUtils.dp(HEIGHT_DP) * (1f - topProgress * overlayAlpha)); + } else { + topPaint.setAlpha((int) (topProgress * overlayAlpha * 0xFF)); + } + cv.drawRect(0, 0, getWidth(), ViewUtils.dp(HEIGHT_DP), topPaint); + cv.restore(); } if (bottomProgress > 0) { - bottomPaint.setAlpha((int) (bottomProgress * overlayAlpha * 0xFF)); - c.drawRect(0, getHeight() - ViewUtils.dp(HEIGHT_DP), getWidth(), getHeight(), bottomPaint); + cv.save(); + if (bitmapMode) { + cv.translate(0, ViewUtils.dp(HEIGHT_DP) * (1f - bottomProgress * overlayAlpha)); + } else { + bottomPaint.setAlpha((int) (bottomProgress * overlayAlpha * 0xFF)); + } + cv.drawRect(0, getHeight() - ViewUtils.dp(HEIGHT_DP), getWidth(), getHeight(), bottomPaint); + cv.restore(); + } + + if (bitmapMode) { + c.drawBitmap(bitmap, 0, 0, null); + invalidate(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (bitmap != null) { + bitmap.recycle(); + bitmap = null; + bitmapCanvas = null; } } @@ -80,8 +139,9 @@ public class FadeRecyclerView extends RecyclerView implements IThemeView { private void invalidateShaders() { if (getWidth() == 0 || getHeight() == 0) return; - topPaint.setShader(new LinearGradient(getWidth() / 2f, 0, getWidth() / 2f, ViewUtils.dp(HEIGHT_DP), ThemesRepo.getColor(android.R.attr.windowBackground), 0, Shader.TileMode.CLAMP)); - bottomPaint.setShader(new LinearGradient(getWidth() / 2f, getHeight() - ViewUtils.dp(HEIGHT_DP), getWidth() / 2f, getHeight(), 0, ThemesRepo.getColor(android.R.attr.windowBackground), Shader.TileMode.CLAMP)); + int clr = bitmapMode ? Color.BLACK : ThemesRepo.getColor(android.R.attr.windowBackground); + topPaint.setShader(new LinearGradient(getWidth() / 2f, 0, getWidth() / 2f, ViewUtils.dp(HEIGHT_DP), bitmapMode ? 0 : clr, bitmapMode ? clr : 0, Shader.TileMode.CLAMP)); + bottomPaint.setShader(new LinearGradient(getWidth() / 2f, getHeight() - ViewUtils.dp(HEIGHT_DP), getWidth() / 2f, getHeight(), bitmapMode ? clr : 0, bitmapMode ? 0 : clr, Shader.TileMode.CLAMP)); invalidate(); } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/view/SnackbarsLayout.java b/app/src/main/java/ru/ytkab0bp/slicebeam/view/SnackbarsLayout.java index e93366c..6bd5a53 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/view/SnackbarsLayout.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/view/SnackbarsLayout.java @@ -16,6 +16,7 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; +import android.widget.Space; import android.widget.TextView; import androidx.annotation.NonNull; @@ -104,6 +105,8 @@ public class SnackbarsLayout extends FrameLayout { private TextView title; private Snackbar snackbar; + private TextView button; + private float progress; private GestureDetector gestureDetector; @@ -143,6 +146,16 @@ public class SnackbarsLayout extends FrameLayout { title.setEllipsize(TextUtils.TruncateAt.END); addView(title); + addView(new Space(context), new LinearLayout.LayoutParams(0, 0, 1f)); + + button = new TextView(context); + button.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + button.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM)); + button.setMaxLines(1); + button.setEllipsize(TextUtils.TruncateAt.END); + button.setPadding(ViewUtils.dp(8), ViewUtils.dp(8), ViewUtils.dp(8),ViewUtils.dp(8)); + addView(button); + setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ leftMargin = topMargin = rightMargin = bottomMargin = ViewUtils.dp(MARGIN_DP); }}); @@ -164,7 +177,7 @@ public class SnackbarsLayout extends FrameLayout { @Override public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { - if (snackbar.type == Type.LOADING) { + if (snackbar.lifetime == 0) { return false; } @@ -247,34 +260,48 @@ public class SnackbarsLayout extends FrameLayout { icon.setVisibility(snackbar.type == Type.LOADING ? GONE : VISIBLE); title.setText(snackbar.title); + button.setText(snackbar.buttonTitle); + button.setOnClickListener(snackbar.buttonClick); + button.setVisibility(snackbar.buttonTitle != null ? View.VISIBLE : View.GONE); + switch (snackbar.type) { case DONE: icon.setImageResource(R.drawable.done_outline_28); icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarDone))); title.setTextColor(ThemesRepo.getColor(R.attr.snackbarDone)); + button.setTextColor(ThemesRepo.getColor(R.attr.snackbarDone)); + button.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ColorUtils.setAlphaComponent(ThemesRepo.getColor(R.attr.snackbarDone), 0x21), 8)); setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarDone), 0.15f)); break; case WARNING: icon.setImageResource(R.drawable.warning_triangle_outline_28); icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarWarning))); title.setTextColor(ThemesRepo.getColor(R.attr.snackbarWarning)); + button.setTextColor(ThemesRepo.getColor(R.attr.snackbarWarning)); + button.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ColorUtils.setAlphaComponent(ThemesRepo.getColor(R.attr.snackbarWarning), 0x21), 8)); setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarWarning), 0.15f)); break; case LOADING: progressBar.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarInfo))); title.setTextColor(ThemesRepo.getColor(R.attr.snackbarInfo)); + button.setTextColor(ThemesRepo.getColor(R.attr.snackbarInfo)); + button.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ColorUtils.setAlphaComponent(ThemesRepo.getColor(R.attr.snackbarInfo), 0x21), 8)); setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarInfo), 0.15f)); break; case INFO: icon.setImageResource(R.drawable.info_outline_28); icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarInfo))); title.setTextColor(ThemesRepo.getColor(R.attr.snackbarInfo)); + button.setTextColor(ThemesRepo.getColor(R.attr.snackbarInfo)); + button.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ColorUtils.setAlphaComponent(ThemesRepo.getColor(R.attr.snackbarInfo), 0x21), 8)); setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarInfo), 0.15f)); break; case ERROR: icon.setImageResource(R.drawable.error_outline_28); icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.snackbarError))); title.setTextColor(ThemesRepo.getColor(R.attr.snackbarError)); + button.setTextColor(ThemesRepo.getColor(R.attr.snackbarError)); + button.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ColorUtils.setAlphaComponent(ThemesRepo.getColor(R.attr.snackbarError), 0x10), 8)); setBackgroundColor(ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.snackbarBase), ThemesRepo.getColor(R.attr.snackbarError), 0.15f)); break; } @@ -288,6 +315,9 @@ public class SnackbarsLayout extends FrameLayout { public int lifetime = 2500; public String tag; + public CharSequence buttonTitle; + public View.OnClickListener buttonClick; + public Snackbar(Type type, CharSequence title) { this.type = type; this.title = title; diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 08533c3..9a557f6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -28,6 +28,7 @@ Не удалось сгенерировать модель Модель сохранена как %s. Не осталось генераций. + Уже генерируется другая модель. Калибров. K3D Linear Advance Калибровка Linear/Pressure Advance @@ -173,12 +174,14 @@ Нажмите чтобы узнать больше Аккаунт Beam 3D Даёт следующие преимущества: + Ранний доступ + Вы можете скачать ранние сборки из чата Telegram для подписчиков Облачная синхронизация профилей Храните свои профили в облаке Beam ИИ генератор моделей %1$d моделей по фото в месяц Slice Beam может оставаться бесплатным для всех - Спасибо за вашу поддержку! + Ваш ник будет написан в списке поддержавших.\nСпасибо за вашу поддержку! При подписке на данный уровень вы соглашаетесь с условиями обслуживания. Уже подписаны? Бесплатно diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2f312a..1851a5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Failed to generate model Saved model as %s. No generations left. + Already generating another model. Calibrat. K3D Linear Advance Linear/Pressure Advance Calibration @@ -175,12 +176,14 @@ Tap to learn more Beam 3D Account Provides the following benefits: + Early access + You can download early builds from Telegram chat for subscribers 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! + Your nickname will be written in the list of supporters.\nThanks for your support! By subscribing to this level you accept terms of service. Already subscribed? Free