mirror of
https://github.com/Dark98/SliceBeam.git
synced 2026-07-02 16:49:02 +00:00
Cloud sync made right
This commit is contained in:
@@ -85,6 +85,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
public static List<ConfigObject> EXPORTING_FILAMENTS;
|
||||
public static List<ConfigObject> EXPORTING_PRINTERS;
|
||||
|
||||
public static boolean IS_GENERATING_AI_MODEL;
|
||||
|
||||
public static File aiTempFile;
|
||||
|
||||
private static SparseArray<NavigationDelegate> 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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -92,8 +92,8 @@ public interface CloudAPI extends APIRunner {
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("sync/upload")
|
||||
void syncUpload(@Arg("data") String data, APICallback<SyncState> callback);
|
||||
@Method(requestType = RequestType.POST, value = "sync/upload")
|
||||
void syncUpload(@Arg("") String data, @Header("Content-Type") String type, APICallback<SyncState> 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
|
||||
*/
|
||||
|
||||
@@ -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<CloudAPI.SyncState>() {
|
||||
private static void downloadData(long lastModified) {
|
||||
CloudAPI.INSTANCE.syncGet(new APICallback<String>() {
|
||||
@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<CloudAPI.SyncState>() {
|
||||
@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<CloudAPI.SyncState>() {
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}});
|
||||
}
|
||||
|
||||
|
||||
@@ -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/*");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<string name="MenuFileAIGeneratorError">Не удалось сгенерировать модель</string>
|
||||
<string name="MenuFileAIGeneratorSavedAs">Модель сохранена как %s.</string>
|
||||
<string name="MenuFileAIGeneratorNoGenerationsLeft">Не осталось генераций.</string>
|
||||
<string name="MenuFileAIGeneratorAlreadyGenerating">Уже генерируется другая модель.</string>
|
||||
<string name="MenuFileCalibrations">Калибров.</string>
|
||||
<string name="MenuFileCalibrationsLA">K3D Linear Advance</string>
|
||||
<string name="MenuFileCalibrationsLADescription">Калибровка Linear/Pressure Advance</string>
|
||||
@@ -173,12 +174,14 @@
|
||||
<string name="SettingsCloudTapToShowMore">Нажмите чтобы узнать больше</string>
|
||||
<string name="SettingsCloudManageTitle">Аккаунт Beam 3D</string>
|
||||
<string name="SettingsCloudManageDescription">Даёт следующие преимущества:</string>
|
||||
<string name="SettingsCloudManageFeatureEarlyAccess">Ранний доступ</string>
|
||||
<string name="SettingsCloudManageFeatureEarlyAccessDescription">Вы можете скачать ранние сборки из чата Telegram для подписчиков</string>
|
||||
<string name="SettingsCloudManageFeatureCloudSync">Облачная синхронизация профилей</string>
|
||||
<string name="SettingsCloudManageFeatureCloudSyncDescription">Храните свои профили в облаке Beam</string>
|
||||
<string name="SettingsCloudManageFeatureAIGenerator">ИИ генератор моделей</string>
|
||||
<string name="SettingsCloudManageFeatureAIGeneratorDescription">%1$d моделей по фото в месяц</string>
|
||||
<string name="SettingsCloudManageFeatureFreeForAll">Slice Beam может оставаться бесплатным для всех</string>
|
||||
<string name="SettingsCloudManageFeatureFreeForAllDescription">Спасибо за вашу поддержку!</string>
|
||||
<string name="SettingsCloudManageFeatureFreeForAllDescription">Ваш ник будет написан в списке поддержавших.\nСпасибо за вашу поддержку!</string>
|
||||
<string name="SettingsCloudManageLevelRedirectMessage">При подписке на данный уровень вы соглашаетесь с условиями обслуживания.</string>
|
||||
<string name="SettingsCloudManageLevelRedirectAlreadySubscribed">Уже подписаны?</string>
|
||||
<string name="SettingsCloudManageFree">Бесплатно</string>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<string name="MenuFileAIGeneratorError">Failed to generate model</string>
|
||||
<string name="MenuFileAIGeneratorSavedAs">Saved model as %s.</string>
|
||||
<string name="MenuFileAIGeneratorNoGenerationsLeft">No generations left.</string>
|
||||
<string name="MenuFileAIGeneratorAlreadyGenerating">Already generating another model.</string>
|
||||
<string name="MenuFileCalibrations">Calibrat.</string>
|
||||
<string name="MenuFileCalibrationsLA">K3D Linear Advance</string>
|
||||
<string name="MenuFileCalibrationsLADescription">Linear/Pressure Advance Calibration</string>
|
||||
@@ -175,12 +176,14 @@
|
||||
<string name="SettingsCloudTapToShowMore">Tap to learn more</string>
|
||||
<string name="SettingsCloudManageTitle">Beam 3D Account</string>
|
||||
<string name="SettingsCloudManageDescription">Provides the following benefits:</string>
|
||||
<string name="SettingsCloudManageFeatureEarlyAccess">Early access</string>
|
||||
<string name="SettingsCloudManageFeatureEarlyAccessDescription">You can download early builds from Telegram chat for subscribers</string>
|
||||
<string name="SettingsCloudManageFeatureCloudSync">Cloud profiles sync</string>
|
||||
<string name="SettingsCloudManageFeatureCloudSyncDescription">Store your profiles in Beam Cloud</string>
|
||||
<string name="SettingsCloudManageFeatureAIGenerator">AI model generator</string>
|
||||
<string name="SettingsCloudManageFeatureAIGeneratorDescription">%1$d models from photo per month</string>
|
||||
<string name="SettingsCloudManageFeatureFreeForAll">Slice Beam can remain free for all</string>
|
||||
<string name="SettingsCloudManageFeatureFreeForAllDescription">Thanks for your support!</string>
|
||||
<string name="SettingsCloudManageFeatureFreeForAllDescription">Your nickname will be written in the list of supporters.\nThanks for your support!</string>
|
||||
<string name="SettingsCloudManageLevelRedirectMessage">By subscribing to this level you accept terms of service.</string>
|
||||
<string name="SettingsCloudManageLevelRedirectAlreadySubscribed">Already subscribed?</string>
|
||||
<string name="SettingsCloudManageFree">Free</string>
|
||||
|
||||
Reference in New Issue
Block a user