mirror of
https://github.com/Dark98/SliceBeam.git
synced 2026-07-03 16:49:03 +00:00
Cloud features: part 1
This commit is contained in:
@@ -45,6 +45,10 @@ public class BeamServerData {
|
||||
return !BuildConfig.IS_GOOGLE_PLAY || Prefs.isRussianIP();
|
||||
}
|
||||
|
||||
public static boolean isCloudAvailable() {
|
||||
return isBoostyAvailable();
|
||||
}
|
||||
|
||||
public static void load() {
|
||||
client.get(DATA_URL, new AsyncHttpResponseHandler() {
|
||||
@Override
|
||||
|
||||
@@ -48,10 +48,14 @@ import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import ru.ytkab0bp.sapil.APICallback;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudController;
|
||||
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
|
||||
import ru.ytkab0bp.slicebeam.components.ChangeLogBottomSheet;
|
||||
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
|
||||
import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedDismissAIGeneratorMenu;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedDismissSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
|
||||
@@ -69,9 +73,11 @@ import ru.ytkab0bp.slicebeam.utils.ViewUtils;
|
||||
import ru.ytkab0bp.slicebeam.view.SnackbarsLayout;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
// Activity result
|
||||
public final static int REQUEST_CODE_OPEN_FILE = 1, REQUEST_CODE_EXPORT_GCODE = 2,
|
||||
REQUEST_CODE_IMPORT_PROFILES = 3, REQUEST_CODE_EXPORT_PROFILES = 4,
|
||||
REQUEST_CODE_EXPORT_3MF = 5,
|
||||
REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO = 6, REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO = 7;
|
||||
|
||||
private static MainActivity activeInstance;
|
||||
|
||||
@@ -79,6 +85,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
public static List<ConfigObject> EXPORTING_FILAMENTS;
|
||||
public static List<ConfigObject> EXPORTING_PRINTERS;
|
||||
|
||||
public static File aiTempFile;
|
||||
|
||||
private static SparseArray<NavigationDelegate> liveDelegate = new SparseArray<>();
|
||||
private static int lastId;
|
||||
|
||||
@@ -317,6 +325,23 @@ public class MainActivity extends AppCompatActivity {
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
} else if (requestCode == REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissAIGeneratorMenu());
|
||||
|
||||
Bitmap bm = BitmapFactory.decodeFile(aiTempFile.getAbsolutePath());
|
||||
generateAiModel(bm);
|
||||
aiTempFile.delete();
|
||||
aiTempFile = null;
|
||||
} else if (requestCode == REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissAIGeneratorMenu());
|
||||
|
||||
try {
|
||||
InputStream in = getContentResolver().openInputStream(data.getData());
|
||||
Bitmap bm = BitmapFactory.decodeStream(in);
|
||||
generateAiModel(bm);
|
||||
} catch (Exception e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,21 +487,106 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
}
|
||||
.show();
|
||||
return;
|
||||
|
||||
private void generateAiModel(Bitmap bm) {
|
||||
String uploadTag = UUID.randomUUID().toString();
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorUploading).tag(uploadTag));
|
||||
IOUtils.IO_POOL.submit(()->{
|
||||
Bitmap scaled;
|
||||
if (bm.getWidth() > 1024 || bm.getHeight() > 1024) {
|
||||
if (bm.getWidth() > bm.getHeight()) {
|
||||
int w = 1024;
|
||||
int h = (int) ((float) w * bm.getHeight() / bm.getWidth());
|
||||
scaled = Bitmap.createScaledBitmap(bm, w, h, true);
|
||||
} else {
|
||||
int h = 1024;
|
||||
int w = (int) ((float) h * bm.getWidth() / bm.getHeight());
|
||||
scaled = Bitmap.createScaledBitmap(bm, w, h, true);
|
||||
}
|
||||
bm.recycle();
|
||||
} else {
|
||||
scaled = bm;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
scaled.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||
scaled.recycle();
|
||||
|
||||
String processTag = UUID.randomUUID().toString();
|
||||
CloudAPI.INSTANCE.modelsGenerate(Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP), "image/png", new APICallback<InputStream>() {
|
||||
@Override
|
||||
public void onResponse(InputStream in) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(processTag));
|
||||
|
||||
String downloadTag = UUID.randomUUID().toString();
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorDownloading).tag(downloadTag));
|
||||
String fileName = "generated_" + UUID.randomUUID() + ".stl";
|
||||
|
||||
File f = new File(SliceBeam.getModelCacheDir(), fileName);
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(f);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
|
||||
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "application/vnd.ms-pki.stl");
|
||||
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
|
||||
|
||||
Uri uri = getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues);
|
||||
|
||||
if (uri != null) {
|
||||
try {
|
||||
OutputStream out = getContentResolver().openOutputStream(uri);
|
||||
byte[] buf = new byte[10240];
|
||||
int c;
|
||||
while ((c = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, c);
|
||||
fos.write(buf, 0, c);
|
||||
}
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
File downloadsDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
File file = new File(downloadsDirectory, fileName);
|
||||
|
||||
try {
|
||||
FileOutputStream out = new FileOutputStream(file);
|
||||
byte[] buf = new byte[10240];
|
||||
int c;
|
||||
while ((c = in.read(buf)) != -1) {
|
||||
out.write(buf, 0, c);
|
||||
fos.write(buf, 0, c);
|
||||
}
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
}
|
||||
fos.close();
|
||||
} catch (Exception e) {
|
||||
Log.e("ai_generator", "Failed to write to downloads", e);
|
||||
}
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(downloadTag));
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.MenuFileAIGeneratorSavedAs, fileName));
|
||||
loadFile(f, true);
|
||||
CloudController.checkGeneratorRemaining();
|
||||
}
|
||||
|
||||
try {
|
||||
loadIniForImport(resolver.openInputStream(uri));
|
||||
} catch (FileNotFoundException e) {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.MenuFileImportProfilesFailed)
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(processTag));
|
||||
ViewUtils.postOnMainThread(() -> new BeamAlertDialogBuilder(MainActivity.this)
|
||||
.setTitle(R.string.MenuFileAIGeneratorError)
|
||||
.setMessage(e.toString())
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
.show());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(uploadTag));
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorProcessing).tag(processTag));
|
||||
});
|
||||
}
|
||||
|
||||
private void loadIniForImport(InputStream in) {
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
package ru.ytkab0bp.slicebeam;
|
||||
|
||||
import static android.opengl.GLES30.*;
|
||||
import static android.opengl.GLES30.GL_COLOR_BUFFER_BIT;
|
||||
import static android.opengl.GLES30.GL_DEPTH_BUFFER_BIT;
|
||||
import static android.opengl.GLES30.GL_DEPTH_TEST;
|
||||
import static android.opengl.GLES30.glClear;
|
||||
import static android.opengl.GLES30.glClearColor;
|
||||
import static android.opengl.GLES30.glDisable;
|
||||
import static android.opengl.GLES30.glEnable;
|
||||
import static android.opengl.GLES30.glViewport;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -45,6 +50,7 @@ import androidx.core.view.ViewCompat;
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
@@ -82,10 +88,15 @@ import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
import cz.msebera.android.httpclient.Header;
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudController;
|
||||
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
|
||||
import ru.ytkab0bp.slicebeam.components.CloudManageBottomSheet;
|
||||
import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
import ru.ytkab0bp.slicebeam.events.BeamServerDataUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudLoginStateUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.recycler.BigHeaderItem;
|
||||
import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerAdapter;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
|
||||
import ru.ytkab0bp.slicebeam.recycler.TextHintRecyclerItem;
|
||||
@@ -108,6 +119,7 @@ import ru.ytkab0bp.slicebeam.view.TextColorImageSpan;
|
||||
public class SetupActivity extends AppCompatActivity {
|
||||
public final static String EXTRA_ABOUT = "about";
|
||||
public final static String EXTRA_BOOSTY_ONLY = "boosty_only";
|
||||
public final static String EXTRA_CLOUD_PROFILE = "cloud_profile";
|
||||
|
||||
private final static String TAG = "SetupActivity";
|
||||
|
||||
@@ -140,6 +152,7 @@ public class SetupActivity extends AppCompatActivity {
|
||||
private List<ProfilesRepo> repos = new ArrayList<>();
|
||||
private ReposItem reposItem;
|
||||
private ProfilesItem profilesItem;
|
||||
private CloudProfileItem cloudItem;
|
||||
private boolean isReposLoaded;
|
||||
private boolean limitRepoFragmentCount = true;
|
||||
private boolean limitProfileFragmentCount = true;
|
||||
@@ -149,6 +162,7 @@ public class SetupActivity extends AppCompatActivity {
|
||||
private boolean isProfilesLoaded;
|
||||
private boolean about;
|
||||
private boolean boostyOnly;
|
||||
private boolean cloudProfile;
|
||||
|
||||
private List<ConfigObject> enabledPrinters = new ArrayList<>();
|
||||
|
||||
@@ -166,8 +180,9 @@ public class SetupActivity extends AppCompatActivity {
|
||||
|
||||
about = getIntent().getBooleanExtra(EXTRA_ABOUT, false);
|
||||
boostyOnly = getIntent().getBooleanExtra(EXTRA_BOOSTY_ONLY, false);
|
||||
cloudProfile = getIntent().getBooleanExtra(EXTRA_CLOUD_PROFILE, false);
|
||||
|
||||
if (!about && !boostyOnly) {
|
||||
if (!about && !boostyOnly && !cloudProfile) {
|
||||
new BeamAlertDialogBuilder(this)
|
||||
.setTitle(R.string.IntroEarlyAccess)
|
||||
.setMessage(R.string.IntroEarlyAccessMessage)
|
||||
@@ -175,11 +190,15 @@ public class SetupActivity extends AppCompatActivity {
|
||||
.show();
|
||||
}
|
||||
|
||||
if (boostyOnly || cloudProfile) {
|
||||
backgroundProgress = 1f;
|
||||
}
|
||||
|
||||
pager = new ViewPager2(this);
|
||||
adapter = new SimpleRecyclerAdapter() {
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return about || boostyOnly ? 1 : limitRepoFragmentCount ? REPOS_INDEX + 1 : limitProfileFragmentCount ? PROFILES_INDEX + 1 : super.getItemCount();
|
||||
return about || boostyOnly || cloudProfile ? 1 : limitRepoFragmentCount ? REPOS_INDEX + 1 : limitProfileFragmentCount ? PROFILES_INDEX + 1 : super.getItemCount();
|
||||
}
|
||||
};
|
||||
setItems();
|
||||
@@ -210,7 +229,7 @@ public class SetupActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
if (position == 0 && !boostyOnly) {
|
||||
if (position == 0 && !boostyOnly && !cloudProfile) {
|
||||
backgroundProgress = positionOffset;
|
||||
} else {
|
||||
backgroundProgress = 1f;
|
||||
@@ -348,14 +367,7 @@ public class SetupActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
title.setTranslationY(ViewUtils.lerp(titleY, (ViewUtils.dp(52) - title.getHeight() * title.getScaleY()) / 2f, backgroundProgress));
|
||||
float sc = ViewUtils.lerp(1, 22 / 32f, backgroundProgress);
|
||||
title.setPivotX(title.getWidth() / 2f);
|
||||
title.setPivotY(0);
|
||||
title.setScaleX(sc);
|
||||
title.setScaleY(sc);
|
||||
int color = ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.textColorOnAccent), ThemesRepo.getColor(android.R.attr.colorAccent), backgroundProgress - boostyProgress);
|
||||
title.setTextColor(color);
|
||||
invalidateTitleY();
|
||||
backgroundView.requestRender();
|
||||
}
|
||||
});
|
||||
@@ -368,7 +380,7 @@ public class SetupActivity extends AppCompatActivity {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
titleY = h / 4;
|
||||
title.setTranslationY(ViewUtils.lerp(titleY, title.getPaddingTop(), backgroundProgress));
|
||||
invalidateTitleY();
|
||||
}
|
||||
};
|
||||
fl.setClipChildren(false);
|
||||
@@ -417,10 +429,13 @@ public class SetupActivity extends AppCompatActivity {
|
||||
topColor = ColorUtils.blendARGB(bottomColor, ThemesRepo.getColor(R.attr.boostyColorTop), boostyProgress);
|
||||
bottomColor = ColorUtils.blendARGB(bottomColor, ThemesRepo.getColor(R.attr.boostyColorBottom), boostyProgress);
|
||||
}
|
||||
if (cloudProfile) {
|
||||
bottomColor = ColorUtils.blendARGB(bottomColor, topColor, 0.5f);
|
||||
}
|
||||
|
||||
shader.setUniformColor("top_color", topColor);
|
||||
shader.setUniformColor("bottom_color", bottomColor);
|
||||
shader.setUniform("progress", backgroundProgress - (boostyProgress != 0 ? 1.2f : 0));
|
||||
shader.setUniform("progress", backgroundProgress - (cloudProfile ? 1.4f : 0) - (boostyProgress != 0 ? 1.2f : 0));
|
||||
shader.setUniform("time", time);
|
||||
backgroundModel.render();
|
||||
shader.stopUsing();
|
||||
@@ -434,7 +449,7 @@ public class SetupActivity extends AppCompatActivity {
|
||||
title = new TextView(this);
|
||||
title.setGravity(Gravity.CENTER);
|
||||
title.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
title.setText(R.string.AppName);
|
||||
title.setText(cloudProfile ? R.string.SettingsCloudManageTitle : R.string.AppName);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 32);
|
||||
title.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
|
||||
title.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER_HORIZONTAL));
|
||||
@@ -459,6 +474,17 @@ public class SetupActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void invalidateTitleY() {
|
||||
float sc = ViewUtils.lerp(1, 22 / 32f, backgroundProgress);
|
||||
title.setPivotX(title.getWidth() / 2f);
|
||||
title.setPivotY(0);
|
||||
title.setScaleX(sc);
|
||||
title.setScaleY(sc);
|
||||
int color = ColorUtils.blendARGB(ThemesRepo.getColor(R.attr.textColorOnAccent), ThemesRepo.getColor(android.R.attr.colorAccent), cloudProfile ? 0f : backgroundProgress - boostyProgress);
|
||||
title.setTextColor(color);
|
||||
title.setTranslationY(ViewUtils.lerp(titleY, (ViewUtils.dp(52) - title.getHeight() * title.getScaleY()) / 2f, backgroundProgress));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -468,7 +494,7 @@ public class SetupActivity extends AppCompatActivity {
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onDataUpdated(BeamServerDataUpdatedEvent e) {
|
||||
if (!about) {
|
||||
if (!about && !boostyOnly && !cloudProfile) {
|
||||
boolean wasBoosty = BOOSTY_INDEX != -1;
|
||||
if (wasBoosty != BeamServerData.isBoostyAvailable()) {
|
||||
setItems();
|
||||
@@ -476,8 +502,18 @@ public class SetupActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onCloudAuthStateUpdated(CloudLoginStateUpdatedEvent e) {
|
||||
if (cloudProfile) {
|
||||
cloudItem.bindLoginButton(true);
|
||||
cloudItem.bindFeatures();
|
||||
}
|
||||
}
|
||||
|
||||
private void setItems() {
|
||||
if (boostyOnly) {
|
||||
if (cloudProfile){
|
||||
adapter.setItems(Collections.singletonList(cloudItem = new CloudProfileItem()));
|
||||
} else if (boostyOnly) {
|
||||
adapter.setItems(Collections.singletonList(new BoostyItem()));
|
||||
} else if (about) {
|
||||
adapter.setItems(Collections.singletonList(new AboutItem()));
|
||||
@@ -613,6 +649,323 @@ public class SetupActivity extends AppCompatActivity {
|
||||
fakeScroller.start();
|
||||
}
|
||||
|
||||
private final class CloudProfileItem extends SimpleRecyclerItem<View> {
|
||||
private FrameLayout buttonView;
|
||||
private TextView buttonText;
|
||||
private ProgressBar buttonProgress;
|
||||
private RecyclerView recyclerView;
|
||||
|
||||
@Override
|
||||
public View onCreateView(Context ctx) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
ll.setPadding(0, ViewUtils.dp(42), 0, 0);
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
|
||||
title.setText(R.string.SettingsCloudManageDescription);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
title.setGravity(Gravity.CENTER);
|
||||
title.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
ll.addView(title);
|
||||
|
||||
FrameLayout fl = new FrameLayout(ctx);
|
||||
recyclerView = new RecyclerView(ctx);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(ctx));
|
||||
recyclerView.setAdapter(adapter = new SimpleRecyclerAdapter());
|
||||
recyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
fl.addView(recyclerView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
bindFeatures();
|
||||
|
||||
ll.addView(fl, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
TextView tosButton = new TextView(ctx);
|
||||
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(ctx.getString(R.string.SettingsCloudManageTermsOfService)).append(" ");
|
||||
Drawable dr = ContextCompat.getDrawable(ctx, R.drawable.external_link_outline_24);
|
||||
int size = ViewUtils.dp(16);
|
||||
dr.setBounds(0, 0, size, size);
|
||||
sb.append("d", new TextColorImageSpan(dr, ViewUtils.dp(2f)), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
tosButton.setText(sb);
|
||||
tosButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||
tosButton.setTextColor(Color.WHITE);
|
||||
tosButton.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
tosButton.setGravity(Gravity.CENTER);
|
||||
tosButton.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8));
|
||||
tosButton.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
|
||||
tosButton.setOnClickListener(v -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://beam3d.ru/slicebeam_cloud_tos.html"))));
|
||||
ll.addView(tosButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
buttonView = new FrameLayout(ctx);
|
||||
buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(android.R.attr.colorAccent), 16));
|
||||
|
||||
buttonText = new TextView(ctx);
|
||||
buttonText.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
|
||||
buttonText.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
buttonText.setGravity(Gravity.CENTER);
|
||||
buttonText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
buttonView.addView(buttonText, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
|
||||
buttonProgress = new ProgressBar(ctx);
|
||||
buttonProgress.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(R.attr.textColorOnAccent)));
|
||||
buttonView.addView(buttonProgress, new FrameLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28), Gravity.CENTER));
|
||||
|
||||
bindLoginButton(false);
|
||||
|
||||
ll.addView(buttonView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
bottomMargin = ViewUtils.dp(16);
|
||||
}});
|
||||
|
||||
ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
return ll;
|
||||
}
|
||||
|
||||
private void bindFeatures() {
|
||||
List<SimpleRecyclerItem> items = new ArrayList<>();
|
||||
if (CloudController.getUserFeatures() != null) {
|
||||
for (CloudAPI.SubscriptionLevel lvl : CloudController.getUserFeatures().levels) {
|
||||
items.add(new CloudSubscriptionLevel(lvl));
|
||||
}
|
||||
}
|
||||
adapter.setItems(items);
|
||||
}
|
||||
|
||||
private void bindLoginButton(boolean animate) {
|
||||
boolean loggedIn = Prefs.getCloudAPIToken() != null;
|
||||
boolean loading = !loggedIn && CloudController.isLoggingIn();
|
||||
boolean wasLoading = buttonProgress.getTag() != null;
|
||||
if (animate) {
|
||||
if (wasLoading != loading) {
|
||||
buttonProgress.setTag(loading ? 1 : null);
|
||||
|
||||
buttonProgress.animate().cancel();
|
||||
buttonProgress.animate().scaleX(loading ? 1f : 0.4f).scaleY(loading ? 1f : 0.4f).alpha(loading ? 1f : 0f).setDuration(150).setInterpolator(ViewUtils.CUBIC_INTERPOLATOR).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
if (loading) {
|
||||
buttonProgress.setVisibility(View.VISIBLE);
|
||||
buttonProgress.setAlpha(0f);
|
||||
buttonProgress.setScaleX(0.4f);
|
||||
buttonProgress.setScaleY(0.4f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (!loading) {
|
||||
buttonProgress.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
buttonText.animate().cancel();
|
||||
buttonText.animate().scaleX(!loading ? 1f : 0.4f).scaleY(!loading ? 1f : 0.4f).alpha(!loading ? 1f : 0f).setDuration(150).setInterpolator(ViewUtils.CUBIC_INTERPOLATOR).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
if (!loading) {
|
||||
buttonText.setVisibility(View.VISIBLE);
|
||||
buttonText.setAlpha(0f);
|
||||
buttonText.setScaleX(0.4f);
|
||||
buttonText.setScaleY(0.4f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (loading) {
|
||||
buttonText.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
} else {
|
||||
buttonProgress.setTag(loading ? 1 : null);
|
||||
buttonProgress.setVisibility(loading ? View.VISIBLE : View.GONE);
|
||||
buttonText.setVisibility(loading ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
buttonText.setText(loggedIn ? R.string.SettingsCloudManageButtonManage : R.string.SettingsCloudManageButtonLogIn);
|
||||
buttonView.setOnClickListener(v-> {
|
||||
if (loading) {
|
||||
new BeamAlertDialogBuilder(v.getContext())
|
||||
.setTitle(R.string.SettingsCloudManageButtonLogInCancelTitle)
|
||||
.setMessage(R.string.SettingsCloudManageButtonLogInCancel)
|
||||
.setNegativeButton(R.string.No, null)
|
||||
.setPositiveButton(R.string.Yes, (dialog, which) -> CloudController.cancelLogin())
|
||||
.show();
|
||||
} else if (Prefs.getCloudAPIToken() != null) {
|
||||
new CloudManageBottomSheet(v.getContext()).show();
|
||||
} else {
|
||||
CloudController.beginLogin();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final static class CloudSubscriptionLevel extends SimpleRecyclerItem<CloudSubscriptionLevel.LevelHolderView> {
|
||||
private CloudAPI.SubscriptionLevel level;
|
||||
|
||||
private CloudSubscriptionLevel(CloudAPI.SubscriptionLevel level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LevelHolderView onCreateView(Context ctx) {
|
||||
return new LevelHolderView(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindView(LevelHolderView view) {
|
||||
view.bind(this);
|
||||
}
|
||||
|
||||
public final static class LevelHolderView extends LinearLayout implements IThemeView {
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
private TextView price;
|
||||
|
||||
private RecyclerView featuresLayout;
|
||||
private SimpleRecyclerAdapter featuresAdapter;
|
||||
|
||||
public LevelHolderView(@NonNull Context context) {
|
||||
super(context);
|
||||
|
||||
setOrientation(VERTICAL);
|
||||
setPadding(0, ViewUtils.dp(16), 0, ViewUtils.dp(8));
|
||||
|
||||
LinearLayout inner = new LinearLayout(context);
|
||||
inner.setOrientation(HORIZONTAL);
|
||||
inner.setGravity(Gravity.CENTER_VERTICAL);
|
||||
inner.setPadding(ViewUtils.dp(28), 0, ViewUtils.dp(28), 0);
|
||||
addView(inner, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
icon = new ImageView(context);
|
||||
inner.addView(icon, new LayoutParams(ViewUtils.dp(26), ViewUtils.dp(26)));
|
||||
|
||||
title = new TextView(context);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
inner.addView(title, new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
|
||||
price = new TextView(context);
|
||||
price.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
price.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
inner.addView(price);
|
||||
|
||||
featuresLayout = new RecyclerView(context) {
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean dispatchHoverEvent(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
featuresLayout.setLayoutManager(new LinearLayoutManager(context));
|
||||
featuresLayout.setAdapter(featuresAdapter = new SimpleRecyclerAdapter());
|
||||
addView(featuresLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
topMargin = ViewUtils.dp(3);
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
topMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
public void bind(CloudSubscriptionLevel item) {
|
||||
CloudAPI.SubscriptionLevel lvl = item.level;
|
||||
title.setText(lvl.title);
|
||||
price.setText(lvl.price);
|
||||
if (lvl.level <= 0) {
|
||||
icon.setImageResource(R.drawable.zero_ruble_outline_28);
|
||||
price.setText(R.string.SettingsCloudManageFree);
|
||||
} else if (lvl.level == 1) {
|
||||
icon.setImageResource(R.drawable.stars_outline_28);
|
||||
} else {
|
||||
icon.setImageResource(R.drawable.cloud_plus_outline_28);
|
||||
}
|
||||
|
||||
List<SimpleRecyclerItem> items = new ArrayList<>();
|
||||
CloudAPI.UserFeatures features = CloudController.getUserFeatures();
|
||||
CloudAPI.UserInfo info = CloudController.getUserInfo();
|
||||
Context ctx = getContext();
|
||||
if (features.syncRequiredLevel != -1 && lvl.level >= features.syncRequiredLevel) {
|
||||
items.add(new PreferenceItem()
|
||||
.setForceDark(true)
|
||||
.setPaddings(ViewUtils.dp(8))
|
||||
.setIcon(R.drawable.sync_outline_28)
|
||||
.setTitle(ctx.getString(R.string.SettingsCloudManageFeatureCloudSync))
|
||||
.setSubtitle(ctx.getString(R.string.SettingsCloudManageFeatureCloudSyncDescription)));
|
||||
}
|
||||
if (features.aiGeneratorRequiredLevel != -1 && lvl.level >= features.aiGeneratorRequiredLevel) {
|
||||
items.add(new PreferenceItem()
|
||||
.setForceDark(true)
|
||||
.setPaddings(ViewUtils.dp(8))
|
||||
.setIcon(R.drawable.brain_outline_28)
|
||||
.setTitle(ctx.getString(R.string.SettingsCloudManageFeatureAIGenerator))
|
||||
.setSubtitle(ctx.getString(R.string.SettingsCloudManageFeatureAIGeneratorDescription, features.aiGeneratorModelsPerMonth)));
|
||||
}
|
||||
if (lvl.level > 0) {
|
||||
items.add(new PreferenceItem()
|
||||
.setForceDark(true)
|
||||
.setPaddings(ViewUtils.dp(8))
|
||||
.setIcon(R.drawable.box_heart_outline_28)
|
||||
.setTitle(ctx.getString(R.string.SettingsCloudManageFeatureFreeForAll))
|
||||
.setSubtitle(ctx.getString(R.string.SettingsCloudManageFeatureFreeForAllDescription)));
|
||||
}
|
||||
featuresAdapter.setItems(items);
|
||||
featuresLayout.setVisibility(items.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
|
||||
boolean subscribed = lvl.level > 0 && info != null && lvl.level == info.currentLevel;
|
||||
boolean allowSubscribe = lvl.level > 0 && (info == null || lvl.level > info.currentLevel);
|
||||
if (subscribed) {
|
||||
price.setText(R.string.SettingsCloudManageSubscribed);
|
||||
}
|
||||
price.setVisibility(allowSubscribe || subscribed ? View.VISIBLE : View.GONE);
|
||||
setOnClickListener(v -> {
|
||||
if (subscribed) {
|
||||
v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(lvl.manageUrl)));
|
||||
} else {
|
||||
new BeamAlertDialogBuilder(getContext())
|
||||
.setTitle(lvl.title)
|
||||
.setMessage(R.string.SettingsCloudManageLevelRedirectMessage)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(lvl.subscribeOrUpgradeUrl))))
|
||||
.setNegativeButton(R.string.SettingsCloudManageLevelRedirectAlreadySubscribed, (dialog, which) -> v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(features.alreadySubscribedInfoUrl))))
|
||||
.show();
|
||||
}
|
||||
});
|
||||
setClickable(allowSubscribe || subscribed);
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
int accent = ThemesRepo.getColor(android.R.attr.colorAccent);
|
||||
if (ColorUtils.calculateLuminance(accent) >= 0.6f) {
|
||||
accent = ColorUtils.blendARGB(accent, Color.BLACK, 0.075f);
|
||||
}
|
||||
boolean tooLight = ColorUtils.calculateLuminance(accent) >= 0.6f;
|
||||
title.setTextColor(0xffffffff);
|
||||
price.setTextColor(0xffffffff);
|
||||
icon.setImageTintList(ColorStateList.valueOf(0xffffffff));
|
||||
featuresLayout.setBackground(ViewUtils.createRipple(0, tooLight ? 0x33ffffff : 0x21ffffff, 24));
|
||||
setBackground(ViewUtils.createRipple(0x21000000, ColorUtils.blendARGB(0xffffffff, accent, tooLight ? 0.9f : 0.75f), 32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class AboutItem extends SimpleRecyclerItem<View> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import com.instacart.truetime.time.TrueTimeImpl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -26,6 +28,7 @@ import ru.ytkab0bp.slicebeam.boot.PrefsTask;
|
||||
import ru.ytkab0bp.slicebeam.boot.PrintConfigWarmupTask;
|
||||
import ru.ytkab0bp.slicebeam.boot.TrueTimeTask;
|
||||
import ru.ytkab0bp.slicebeam.boot.VibrationUtilsTask;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudController;
|
||||
import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.ConfigOptionDef;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
|
||||
@@ -35,6 +38,7 @@ import ru.ytkab0bp.slicebeam.utils.Prefs;
|
||||
public class SliceBeam extends Application {
|
||||
public static SliceBeam INSTANCE;
|
||||
public static EventBus EVENT_BUS = EventBus.newBus("main");
|
||||
public static TrueTimeImpl TRUE_TIME;
|
||||
public static Slic3rConfigWrapper CONFIG;
|
||||
public static int CONFIG_UID = 0;
|
||||
public static BeamServerData SERVER_DATA;
|
||||
@@ -82,6 +86,7 @@ public class SliceBeam extends Application {
|
||||
} catch (Exception e) {
|
||||
Log.e("Config", "Failed to save config", e);
|
||||
}
|
||||
CloudController.notifyDataChanged();
|
||||
}
|
||||
|
||||
public static File getModelCacheDir() {
|
||||
|
||||
@@ -1,22 +1,72 @@
|
||||
package ru.ytkab0bp.slicebeam.boot;
|
||||
|
||||
import com.instacart.library.truetime.TrueTime;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import com.instacart.truetime.TrueTimeEventListener;
|
||||
import com.instacart.truetime.time.TrueTimeImpl;
|
||||
import com.instacart.truetime.time.TrueTimeParameters;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
import kotlinx.coroutines.Dispatchers;
|
||||
import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||
|
||||
public class TrueTimeTask extends BootTask {
|
||||
public TrueTimeTask() {
|
||||
super(() -> {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
try {
|
||||
TrueTime.build().withNtpHost("1.ru.pool.ntp.org").withConnectionTimeout(300).initialize();
|
||||
break;
|
||||
} catch (IOException ignore) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {}
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
SliceBeam.TRUE_TIME = new TrueTimeImpl(new TrueTimeParameters.Builder().buildParams(), Dispatchers.getIO(), new TrueTimeEventListener() {
|
||||
@Override
|
||||
public void initialize(@NonNull TrueTimeParameters trueTimeParameters) {}
|
||||
|
||||
@Override
|
||||
public void initializeSuccess(@NonNull long[] longs) {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initializeFailed(@NonNull Exception e) {}
|
||||
|
||||
@Override
|
||||
public void nextInitializeIn(long l) {}
|
||||
|
||||
@Override
|
||||
public void resolvedNtpHostToIPs(@NonNull String s, @NonNull List<? extends InetAddress> list) {}
|
||||
|
||||
@Override
|
||||
public void lastSntpRequestAttempt(@NonNull InetAddress inetAddress) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequestFailed(@NonNull Exception e) {}
|
||||
|
||||
@Override
|
||||
public void syncDispatcherException(@NonNull Throwable throwable) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequest(@NonNull InetAddress inetAddress) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequestSuccessful(@NonNull InetAddress inetAddress) {}
|
||||
|
||||
@Override
|
||||
public void sntpRequestFailed(@NonNull InetAddress inetAddress, @NonNull Exception e) {}
|
||||
|
||||
@Override
|
||||
public void storingTrueTime(@NonNull long[] longs) {}
|
||||
|
||||
@Override
|
||||
public void returningTrueTime(@NonNull Date date) {}
|
||||
|
||||
@Override
|
||||
public void returningDeviceTime() {}
|
||||
});
|
||||
SliceBeam.TRUE_TIME.sync();
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException ignored) {}
|
||||
});
|
||||
onWorker();
|
||||
nonCritical = true;
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
package ru.ytkab0bp.slicebeam.cloud;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.ytkab0bp.sapil.APICallback;
|
||||
import ru.ytkab0bp.sapil.APILibrary;
|
||||
import ru.ytkab0bp.sapil.APIRequestHandle;
|
||||
import ru.ytkab0bp.sapil.APIRunner;
|
||||
import ru.ytkab0bp.sapil.Arg;
|
||||
import ru.ytkab0bp.sapil.Header;
|
||||
import ru.ytkab0bp.sapil.Method;
|
||||
import ru.ytkab0bp.sapil.RequestType;
|
||||
import ru.ytkab0bp.slicebeam.BuildConfig;
|
||||
import ru.ytkab0bp.slicebeam.utils.Prefs;
|
||||
|
||||
public interface CloudAPI extends APIRunner {
|
||||
CloudAPI INSTANCE = APILibrary.newRunner(CloudAPI.class, new RunnerConfig() {
|
||||
private final Map<String, String> headers = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public String getBaseURL() {
|
||||
return "https://api.beam3d.ru/v1/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultUserAgent() {
|
||||
return "SliceBeam v" + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_CODE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDefaultHeaders() {
|
||||
headers.clear();
|
||||
if (Prefs.getCloudAPIToken() != null) {
|
||||
headers.put("Authorization", "Bearer " + Prefs.getCloudAPIToken());
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Begins login flow, returns auth link
|
||||
*/
|
||||
@Method("login/begin")
|
||||
APIRequestHandle loginBegin(APICallback<LoginData> callback);
|
||||
|
||||
/**
|
||||
* Checks new login state by session id
|
||||
*/
|
||||
@Method("login/check")
|
||||
void loginCheck(@Arg("sessionId") String sessionId, APICallback<LoginState> callback);
|
||||
|
||||
/**
|
||||
* Cancels login flow
|
||||
*/
|
||||
@Method("login/cancel")
|
||||
void loginCancel(@Arg("sessionId") String sessionId, APICallback<Boolean> callback);
|
||||
|
||||
/**
|
||||
* Gets current user info
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("user/getInfo")
|
||||
void userGetInfo(APICallback<UserInfo> callback);
|
||||
|
||||
/**
|
||||
* Gets user features
|
||||
*/
|
||||
@Method("user/getFeatures")
|
||||
void userGetFeatures(APICallback<UserFeatures> callback);
|
||||
|
||||
/**
|
||||
* Fetches sync state
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("sync/getState")
|
||||
void syncGetState(APICallback<SyncState> callback);
|
||||
|
||||
/**
|
||||
* Uploads new data to the server
|
||||
* <p>
|
||||
* @param data New base64 encoded data
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("sync/upload")
|
||||
void syncUpload(@Arg("data") String data, APICallback<SyncState> callback);
|
||||
|
||||
/**
|
||||
* Downloads base64 data
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("sync/get")
|
||||
void syncGet(APICallback<String> callback);
|
||||
|
||||
/**
|
||||
* Generates 3D model from image
|
||||
* <p>
|
||||
* @param image Base64 encoded image
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method(requestType = RequestType.POST, value = "models/generate")
|
||||
void modelsGenerate(@Arg("") String image, @Header("Content-Type") String type, APICallback<InputStream> callback);
|
||||
|
||||
/**
|
||||
* Gets remaining model generations count
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("models/getRemainingCount")
|
||||
void modelsGetRemainingCount(APICallback<ModelsRemainingCount> callback);
|
||||
|
||||
/**
|
||||
* Destroys token
|
||||
* <p>
|
||||
* Requires authorization
|
||||
*/
|
||||
@Method("logout")
|
||||
void logout(APICallback<Boolean> callback);
|
||||
|
||||
final class LoginData {
|
||||
/**
|
||||
* Url that should be clicked by the user to authorize
|
||||
*/
|
||||
public String url;
|
||||
|
||||
/**
|
||||
* Session identifier
|
||||
*/
|
||||
public String sessionId;
|
||||
|
||||
/**
|
||||
* Time at which session should be considered expired if not logged in
|
||||
*/
|
||||
public long expiresAt;
|
||||
}
|
||||
|
||||
final class LoginState {
|
||||
/**
|
||||
* If user is now logged in
|
||||
*/
|
||||
public boolean loggedIn;
|
||||
|
||||
/**
|
||||
* Bearer token if auth was successful
|
||||
*/
|
||||
public String bearer;
|
||||
}
|
||||
|
||||
final class UserFeatures {
|
||||
/**
|
||||
* Which level is required for data sync
|
||||
*/
|
||||
public int syncRequiredLevel;
|
||||
|
||||
/**
|
||||
* Which level is required for AI model generator
|
||||
*/
|
||||
public int aiGeneratorRequiredLevel;
|
||||
|
||||
/**
|
||||
* Models per month max
|
||||
*/
|
||||
public int aiGeneratorModelsPerMonth;
|
||||
|
||||
/**
|
||||
* Url at which user should be redirected for info about how to restore a subscription
|
||||
*/
|
||||
public String alreadySubscribedInfoUrl;
|
||||
|
||||
/**
|
||||
* List of subscription levels
|
||||
*/
|
||||
public List<SubscriptionLevel> levels = new ArrayList<>();
|
||||
}
|
||||
|
||||
final class SubscriptionLevel {
|
||||
/**
|
||||
* Int representation
|
||||
*/
|
||||
public int level;
|
||||
|
||||
/**
|
||||
* Title of this level
|
||||
*/
|
||||
public String title;
|
||||
|
||||
/**
|
||||
* Price of this level
|
||||
*/
|
||||
public String price;
|
||||
|
||||
/**
|
||||
* Url at which user should be redirected for purchase
|
||||
*/
|
||||
public String subscribeOrUpgradeUrl;
|
||||
|
||||
/**
|
||||
* Url at which user should be redirected for managing the subscription
|
||||
*/
|
||||
public String manageUrl;
|
||||
}
|
||||
|
||||
|
||||
final class UserInfo {
|
||||
/**
|
||||
* User's id
|
||||
*/
|
||||
public String id;
|
||||
|
||||
/**
|
||||
* User's display name
|
||||
*/
|
||||
public String displayName;
|
||||
|
||||
/**
|
||||
* User's avatar. Could be null
|
||||
*/
|
||||
@Nullable
|
||||
public String avatarUrl;
|
||||
|
||||
/**
|
||||
* Current subscription level
|
||||
*/
|
||||
public int currentLevel;
|
||||
}
|
||||
|
||||
|
||||
final class SyncState {
|
||||
/**
|
||||
* Cloud data last updated time
|
||||
*/
|
||||
public long lastUpdatedDate = 0;
|
||||
|
||||
/**
|
||||
* Used size of cloud storage
|
||||
*/
|
||||
public long usedSize;
|
||||
|
||||
/**
|
||||
* Max storage size
|
||||
*/
|
||||
public long maxSize;
|
||||
}
|
||||
|
||||
final class ModelsRemainingCount {
|
||||
/**
|
||||
* Used generations
|
||||
*/
|
||||
public int used;
|
||||
|
||||
/**
|
||||
* Max available generations
|
||||
*/
|
||||
public int max;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
package ru.ytkab0bp.slicebeam.cloud;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import ru.ytkab0bp.sapil.APICallback;
|
||||
import ru.ytkab0bp.sapil.APIRequestHandle;
|
||||
import ru.ytkab0bp.slicebeam.R;
|
||||
import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudFeaturesUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudLoginStateUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudModelsRemainingCountUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudUserInfoUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedDismissSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.utils.Prefs;
|
||||
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
|
||||
import ru.ytkab0bp.slicebeam.view.SnackbarsLayout;
|
||||
|
||||
public class CloudController {
|
||||
public final static String USER_INFO_AI_GEN_TAG = "ai_gen_user_info";
|
||||
private final static String CLOUD_SYNC_TAG = "cloud_sync";
|
||||
|
||||
private final static String TAG = "cloud";
|
||||
private final static long MIN_SYNC_DELTA = 5 * 60 * 1000L; // Once in 5 minutes
|
||||
private final static long MIN_SYNC_FEATURES_DELTA = 12 * 60 * 60 * 1000L; // Once in 12 hours
|
||||
|
||||
private static boolean isSyncInProgress;
|
||||
private static CloudAPI.UserInfo userInfo;
|
||||
private static CloudAPI.UserFeatures userFeatures;
|
||||
|
||||
private static int modelsUsed;
|
||||
private static int modelsMaxGenerations;
|
||||
private static boolean isLoggingIn;
|
||||
private static APIRequestHandle beginLoginHandle;
|
||||
private static String loginSessionId;
|
||||
private static Runnable loginAutoCancel = () -> {
|
||||
loginSessionId = null;
|
||||
isLoggingIn = false;
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
};
|
||||
private static Runnable loginCheck = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
CloudAPI.INSTANCE.loginCheck(loginSessionId, new APICallback<CloudAPI.LoginState>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.LoginState response) {
|
||||
if (response.loggedIn) {
|
||||
Prefs.setCloudAPIToken(response.bearer);
|
||||
loadUserInfo();
|
||||
ViewUtils.removeCallbacks(loginAutoCancel);
|
||||
} else if (isLoggingIn) {
|
||||
ViewUtils.postOnMainThread(loginCheck, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to check login state", e);
|
||||
|
||||
if (isLoggingIn) {
|
||||
ViewUtils.postOnMainThread(loginCheck, 5000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private static Gson gson = new Gson();
|
||||
|
||||
public static void init() {
|
||||
if (Prefs.getCloudCachedUserFeatures() != null) {
|
||||
userFeatures = gson.fromJson(Prefs.getCloudCachedUserFeatures(), CloudAPI.UserFeatures.class);
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudFeaturesUpdatedEvent());
|
||||
}
|
||||
long now = SliceBeam.TRUE_TIME.now().getTime();
|
||||
boolean needSyncInfo = userFeatures == null || now - Prefs.getCloudLastFeaturesSync() > MIN_SYNC_FEATURES_DELTA;
|
||||
if (needSyncInfo) {
|
||||
checkUserFeatures();
|
||||
}
|
||||
|
||||
if (Prefs.getCloudAPIToken() != null) {
|
||||
if (Prefs.getCloudCachedUserInfo() != null) {
|
||||
userInfo = gson.fromJson(Prefs.getCloudCachedUserInfo(), CloudAPI.UserInfo.class);
|
||||
modelsUsed = Prefs.getCloudCachedUsedModels();
|
||||
modelsMaxGenerations = Prefs.getCloudCachedMaxModels();
|
||||
}
|
||||
|
||||
if (needSyncInfo || userInfo == null) {
|
||||
loadUserInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadUserInfo() {
|
||||
CloudAPI.INSTANCE.userGetInfo(new APICallback<CloudAPI.UserInfo>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.UserInfo response) {
|
||||
userInfo = response;
|
||||
|
||||
if (userInfo.id.equals("null")) {
|
||||
userInfo = null;
|
||||
Prefs.setCloudAPIToken(null);
|
||||
Prefs.setCloudCachedUserInfo(null);
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
|
||||
|
||||
if (isLoggingIn) {
|
||||
isLoggingIn = false;
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
}
|
||||
} else {
|
||||
Prefs.setCloudCachedUserInfo(gson.toJson(userInfo));
|
||||
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(USER_INFO_AI_GEN_TAG));
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
|
||||
|
||||
if (isLoggingIn) {
|
||||
isLoggingIn = false;
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
}
|
||||
|
||||
if (isSyncAvailable() && Prefs.isCloudProfileSyncEnabled()) {
|
||||
long now = SliceBeam.TRUE_TIME.now().getTime();
|
||||
if (now != Prefs.getLocalLastModified()) {
|
||||
sendData();
|
||||
}
|
||||
}
|
||||
checkGeneratorRemaining();
|
||||
}
|
||||
Prefs.setCloudLastFeaturesSync(SliceBeam.TRUE_TIME.now().getTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to get user info", e);
|
||||
ViewUtils.postOnMainThread(CloudController::init, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isLoggingIn() {
|
||||
return isLoggingIn;
|
||||
}
|
||||
|
||||
private static void beginLogin0() {
|
||||
beginLoginHandle = CloudAPI.INSTANCE.loginBegin(new APICallback<CloudAPI.LoginData>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.LoginData response) {
|
||||
loginSessionId = response.sessionId;
|
||||
|
||||
ViewUtils.postOnMainThread(loginAutoCancel, response.expiresAt * 1000L - SliceBeam.TRUE_TIME.now().getTime());
|
||||
ViewUtils.postOnMainThread(loginCheck, 5000);
|
||||
ViewUtils.postOnMainThread(() -> SliceBeam.INSTANCE.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(response.url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
ViewUtils.postOnMainThread(CloudController::beginLogin0, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void beginLogin() {
|
||||
isLoggingIn = true;
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
beginLogin0();
|
||||
}
|
||||
|
||||
public static void cancelLogin() {
|
||||
isLoggingIn = false;
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
if (loginSessionId != null) {
|
||||
CloudAPI.INSTANCE.loginCancel(loginSessionId, response -> {});
|
||||
}
|
||||
if (beginLoginHandle != null && beginLoginHandle.isRunning()) {
|
||||
beginLoginHandle.cancel();
|
||||
beginLoginHandle = null;
|
||||
}
|
||||
ViewUtils.removeCallbacks(loginCheck);
|
||||
ViewUtils.removeCallbacks(loginAutoCancel);
|
||||
loginSessionId = null;
|
||||
}
|
||||
|
||||
public static void logout() {
|
||||
Prefs.setCloudAPIToken(null);
|
||||
userInfo = null;
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent());
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudUserInfoUpdatedEvent());
|
||||
CloudAPI.INSTANCE.logout(response -> {});
|
||||
}
|
||||
|
||||
public static void checkGeneratorRemaining() {
|
||||
CloudAPI.INSTANCE.modelsGetRemainingCount(new APICallback<CloudAPI.ModelsRemainingCount>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.ModelsRemainingCount response) {
|
||||
modelsUsed = response.used;
|
||||
modelsMaxGenerations = response.max;
|
||||
Prefs.setCloudCachedUsedMaxModels(modelsUsed, modelsMaxGenerations);
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudModelsRemainingCountUpdatedEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to check remaining models", e);
|
||||
ViewUtils.postOnMainThread(CloudController::checkGeneratorRemaining, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void checkUserFeatures() {
|
||||
CloudAPI.INSTANCE.userGetFeatures(new APICallback<CloudAPI.UserFeatures>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.UserFeatures response) {
|
||||
userFeatures = response;
|
||||
Prefs.setCloudCachedUserFeatures(gson.toJson(userFeatures));
|
||||
if (Prefs.getCloudAPIToken() == null) {
|
||||
Prefs.setCloudLastFeaturesSync(SliceBeam.TRUE_TIME.now().getTime());
|
||||
}
|
||||
SliceBeam.EVENT_BUS.fireEvent(new CloudFeaturesUpdatedEvent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to get user features", e);
|
||||
ViewUtils.postOnMainThread(CloudController::checkUserFeatures, 15000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static CloudAPI.UserInfo getUserInfo() {
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
public static CloudAPI.UserFeatures getUserFeatures() {
|
||||
return userFeatures;
|
||||
}
|
||||
|
||||
public static boolean isSyncAvailable() {
|
||||
return Prefs.getCloudAPIToken() != null && userInfo != null && userFeatures != null && userInfo.currentLevel >= userFeatures.syncRequiredLevel;
|
||||
}
|
||||
|
||||
public static boolean needShowAIGenerator() {
|
||||
return userFeatures != null && userFeatures.aiGeneratorRequiredLevel >= 0;
|
||||
}
|
||||
|
||||
public static int getGeneratedModels() {
|
||||
return modelsUsed;
|
||||
}
|
||||
|
||||
public static int getMaxGeneratedModels() {
|
||||
return modelsMaxGenerations;
|
||||
}
|
||||
|
||||
private static void sendData() {
|
||||
if (isSyncInProgress) {
|
||||
return;
|
||||
}
|
||||
// TODO: IMPORTANT: Check getState first, then show conflict info
|
||||
long modified = Prefs.getLocalLastModified();
|
||||
isSyncInProgress = true;
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.CloudSyncInProgress).tag(CLOUD_SYNC_TAG));
|
||||
CloudAPI.INSTANCE.syncUpload("", new APICallback<CloudAPI.SyncState>() {
|
||||
@Override
|
||||
public void onResponse(CloudAPI.SyncState response) {
|
||||
isSyncInProgress = false;
|
||||
if (Prefs.getLocalLastModified() != modified) { // Re-send otherwise
|
||||
sendData();
|
||||
return;
|
||||
}
|
||||
Prefs.setCloudLastSync(modified);
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(R.string.CloudSyncSuccess));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
Log.e(TAG, "Failed to upload sync data", e);
|
||||
isSyncInProgress = false;
|
||||
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CLOUD_SYNC_TAG));
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.CloudSyncError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void notifyDataChanged() {
|
||||
long now = SliceBeam.TRUE_TIME.now().getTime();
|
||||
Prefs.setLocalLastModified(now);
|
||||
if (!isSyncAvailable() || !Prefs.isCloudProfileSyncEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (now - Prefs.getCloudLastSync() > MIN_SYNC_DELTA) {
|
||||
sendData();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package ru.ytkab0bp.slicebeam.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.net.Uri;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.ytkab0bp.slicebeam.R;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudController;
|
||||
import ru.ytkab0bp.slicebeam.recycler.PreferenceSwitchItem;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerAdapter;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
|
||||
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
|
||||
import ru.ytkab0bp.slicebeam.utils.Prefs;
|
||||
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
|
||||
import ru.ytkab0bp.slicebeam.view.TextColorImageSpan;
|
||||
|
||||
public class CloudManageBottomSheet extends BottomSheetDialog {
|
||||
public CloudManageBottomSheet(@NonNull Context context) {
|
||||
super(context);
|
||||
|
||||
LinearLayout ll = new LinearLayout(context);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
GradientDrawable gd = new GradientDrawable();
|
||||
gd.setCornerRadii(new float[] {
|
||||
ViewUtils.dp(28), ViewUtils.dp(28),
|
||||
ViewUtils.dp(28), ViewUtils.dp(28),
|
||||
0, 0,
|
||||
0, 0
|
||||
});
|
||||
gd.setColor(ThemesRepo.getColor(R.attr.dialogBackground));
|
||||
ll.setBackground(gd);
|
||||
ll.setPadding(0, ViewUtils.dp(12), 0, ViewUtils.dp(12));
|
||||
|
||||
TextView title = new TextView(context);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setText(R.string.SettingsCloudManageButtonManage);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
title.setGravity(Gravity.CENTER);
|
||||
title.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
}});
|
||||
ll.addView(title);
|
||||
|
||||
TextView description = new TextView(context);
|
||||
description.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
description.setText(context.getString(R.string.SettingsCloudManageLoggedInAs, CloudController.getUserInfo().displayName));
|
||||
description.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
description.setGravity(Gravity.CENTER);
|
||||
description.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(21);
|
||||
topMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
ll.addView(description);
|
||||
|
||||
int currentLevel = CloudController.getUserInfo().currentLevel;
|
||||
CloudAPI.SubscriptionLevel lvl = null;
|
||||
CloudAPI.UserFeatures features = CloudController.getUserFeatures();
|
||||
for (CloudAPI.SubscriptionLevel level : features.levels) {
|
||||
if (level.level != -1 && level.level <= currentLevel && (lvl == null || level.level > lvl.level)) {
|
||||
lvl = level;
|
||||
}
|
||||
}
|
||||
|
||||
if (lvl != null) {
|
||||
List<SimpleRecyclerItem> items = new ArrayList<>();
|
||||
if (currentLevel >= features.syncRequiredLevel) {
|
||||
items.add(new PreferenceSwitchItem()
|
||||
.setIcon(R.drawable.sync_outline_28)
|
||||
.setTitle(context.getString(R.string.SettingsCloudManageFeatureCloudSync))
|
||||
.setValueProvider(Prefs::isCloudProfileSyncEnabled)
|
||||
.setChangeListener((buttonView, isChecked) -> {
|
||||
Prefs.setCloudProfileSyncEnabled(isChecked);
|
||||
if (isChecked) {
|
||||
CloudController.notifyDataChanged();
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (!items.isEmpty()) {
|
||||
RecyclerView recyclerView = new RecyclerView(context);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
recyclerView.setBackground(ViewUtils.createRipple(0, ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10), 16));
|
||||
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
|
||||
adapter.setItems(items);
|
||||
recyclerView.setAdapter(adapter);
|
||||
ll.addView(recyclerView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{
|
||||
topMargin = ViewUtils.dp(12);
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
}
|
||||
|
||||
TextView manageButton = new TextView(context);
|
||||
SpannableStringBuilder sb = SpannableStringBuilder.valueOf(context.getString(R.string.SettingsCloudManageSubscription)).append(" ");
|
||||
Drawable dr = ContextCompat.getDrawable(context, R.drawable.external_link_outline_24);
|
||||
int size = ViewUtils.dp(16);
|
||||
dr.setBounds(0, 0, size, size);
|
||||
sb.append("d", new TextColorImageSpan(dr, ViewUtils.dp(2f)), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
manageButton.setText(sb);
|
||||
manageButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15);
|
||||
manageButton.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
manageButton.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
manageButton.setGravity(Gravity.CENTER);
|
||||
manageButton.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8));
|
||||
manageButton.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
|
||||
CloudAPI.SubscriptionLevel finalLvl = lvl;
|
||||
manageButton.setOnClickListener(v -> v.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(finalLvl.manageUrl))));
|
||||
ll.addView(manageButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
topMargin = bottomMargin = ViewUtils.dp(6);
|
||||
}});
|
||||
} else {
|
||||
ll.addView(new Space(context), new LinearLayout.LayoutParams(0, ViewUtils.dp(16)));
|
||||
}
|
||||
|
||||
TextView buttonView = new TextView(context);
|
||||
buttonView.setText(R.string.SettingsCloudManageButtonLogOut);
|
||||
buttonView.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent));
|
||||
buttonView.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
buttonView.setGravity(Gravity.CENTER);
|
||||
buttonView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(R.attr.textColorNegative), 16));
|
||||
buttonView.setOnClickListener(v-> {
|
||||
CloudController.logout();
|
||||
dismiss();
|
||||
});
|
||||
ll.addView(buttonView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(16);
|
||||
bottomMargin = ViewUtils.dp(4);
|
||||
}});
|
||||
|
||||
setContentView(ll);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
@@ -16,16 +20,23 @@ import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.dynamicanimation.animation.FloatValueHolder;
|
||||
import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import ru.ytkab0bp.slicebeam.R;
|
||||
import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
|
||||
import ru.ytkab0bp.slicebeam.theme.IThemeView;
|
||||
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
|
||||
import ru.ytkab0bp.slicebeam.utils.RandomUtils;
|
||||
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
|
||||
|
||||
public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolderView> {
|
||||
@@ -36,6 +47,7 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
public boolean isEnabled = true;
|
||||
public boolean isChecked = false;
|
||||
public boolean isCheckable = false;
|
||||
public boolean isShiny = false;
|
||||
public View.OnClickListener clickListener;
|
||||
public CompoundButton.OnCheckedChangeListener checkedChangeListener;
|
||||
|
||||
@@ -61,6 +73,11 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
return this;
|
||||
}
|
||||
|
||||
public BedMenuItem setShiny(boolean shiny) {
|
||||
isShiny = shiny;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BedMenuItem setSingleLine(boolean singleLine) {
|
||||
isSingleLine = singleLine;
|
||||
return this;
|
||||
@@ -77,6 +94,9 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
}
|
||||
|
||||
public final static class BedMenuItemHolderView extends LinearLayout implements IThemeView {
|
||||
private final static float IN_BOUND = 0.05f;
|
||||
private final static float OUT_BOUND = 0.1f;
|
||||
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
|
||||
@@ -84,8 +104,13 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
private Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
private Path path = new Path();
|
||||
private Path path2 = new Path();
|
||||
private float checkedProgress;
|
||||
private boolean enabled;
|
||||
private boolean shiny;
|
||||
private List<Sparkle> sparkles;
|
||||
private long lastDraw;
|
||||
private Drawable sparkleDrawable;
|
||||
|
||||
public BedMenuItemHolderView(Context context) {
|
||||
super(context);
|
||||
@@ -109,12 +134,17 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT) {{
|
||||
leftMargin = topMargin = bottomMargin = ViewUtils.dp(6);
|
||||
}});
|
||||
setClipToPadding(false);
|
||||
setClipChildren(false);
|
||||
setWillNotDraw(false);
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
long dt = Math.min(System.currentTimeMillis() - lastDraw, 16);
|
||||
lastDraw = System.currentTimeMillis();
|
||||
|
||||
int rad = ViewUtils.dp(16);
|
||||
canvas.drawRoundRect(0, 0, getWidth(), getHeight(), rad, rad, bgPaint);
|
||||
|
||||
@@ -133,6 +163,61 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
}
|
||||
|
||||
super.draw(canvas);
|
||||
|
||||
if (shiny) {
|
||||
float side = Math.min(getWidth(), getHeight());
|
||||
canvas.save();
|
||||
if (sparkles == null) sparkles = new ArrayList<>();
|
||||
if (sparkleDrawable == null) {
|
||||
sparkleDrawable = ContextCompat.getDrawable(SliceBeam.INSTANCE, R.drawable.sparkle_28);
|
||||
sparkleDrawable.setColorFilter(new PorterDuffColorFilter(ThemesRepo.getColor(android.R.attr.colorAccent), PorterDuff.Mode.SRC_IN));
|
||||
}
|
||||
|
||||
float p = dt / 1000f;
|
||||
for (Iterator<Sparkle> iterator = sparkles.iterator(); iterator.hasNext(); ) {
|
||||
Sparkle sparkle = iterator.next();
|
||||
sparkle.position.x += sparkle.velocity.x * p;
|
||||
sparkle.position.y += sparkle.velocity.y * p;
|
||||
sparkle.velocity.x *= 0.9999f;
|
||||
sparkle.velocity.y *= 0.9999f;
|
||||
sparkle.living += dt;
|
||||
|
||||
int size = (int) (side * sparkle.size);
|
||||
|
||||
float fadems = 200;
|
||||
if ((sparkle.position.x - sparkle.size > 0 && sparkle.position.x + sparkle.size < 1f) &&
|
||||
sparkle.lifetime - sparkle.living > fadems) {
|
||||
sparkle.living = (long) (sparkle.lifetime - fadems);
|
||||
}
|
||||
if (sparkle.living >= sparkle.lifetime) {
|
||||
iterator.remove();
|
||||
} else {
|
||||
float alpha = sparkle.living < fadems ? sparkle.living / fadems : sparkle.living > sparkle.lifetime - fadems ? (sparkle.lifetime - sparkle.living) / fadems : 1f;
|
||||
canvas.saveLayerAlpha(-OUT_BOUND * side, -OUT_BOUND * side, getWidth() + OUT_BOUND * side, getHeight() + OUT_BOUND * side, (int) (alpha * sparkle.alpha * 0xFF));
|
||||
canvas.translate(sparkle.position.x * side, sparkle.position.y * side);
|
||||
sparkleDrawable.setBounds(-size / 2, -size / 2, size / 2, size / 2);
|
||||
sparkleDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
if (sparkles.size() < 20) {
|
||||
int s = 20 - sparkles.size();
|
||||
for (int i = 0; i < s; i++) {
|
||||
if (RandomUtils.RANDOM.nextFloat() < 0.01f) {
|
||||
Sparkle sparkle = new Sparkle();
|
||||
boolean leftSide = RandomUtils.RANDOM.nextBoolean();
|
||||
sparkle.position = new PointF(leftSide ? RandomUtils.randomf(-OUT_BOUND, 0) : RandomUtils.randomf(1, 1 + OUT_BOUND), RandomUtils.randomf(-OUT_BOUND, 1 + OUT_BOUND));
|
||||
sparkle.velocity = new PointF(RandomUtils.randomf(-0.05f, 0.05f), RandomUtils.randomf(-0.05f, 0.05f));
|
||||
sparkle.size = RandomUtils.randomf(0.1f, 0.12f);
|
||||
sparkle.alpha = RandomUtils.randomf(0.5f, 1f);
|
||||
sparkle.lifetime = RandomUtils.randoml(4000, 10000);
|
||||
sparkles.add(sparkle);
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,10 +231,12 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
|
||||
public void bind(BedMenuItem item) {
|
||||
enabled = item.isEnabled;
|
||||
shiny = item.isShiny;
|
||||
title.setMaxLines(item.isSingleLine ? 1 : 2);
|
||||
title.setText(item.titleRes);
|
||||
icon.setImageResource(item.iconRes);
|
||||
checkedProgress = item.isCheckable && item.isChecked ? 1 : 0;
|
||||
onApplyTheme();
|
||||
title.setTextColor(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorPrimary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ColorUtils.blendARGB(ThemesRepo.getColor(android.R.attr.textColorSecondary), ThemesRepo.getColor(R.attr.textColorOnAccent), checkedProgress)));
|
||||
|
||||
@@ -187,5 +274,14 @@ public class BedMenuItem extends SimpleRecyclerItem<BedMenuItem.BedMenuItemHolde
|
||||
bgPaint.setColor(ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10));
|
||||
accentPaint.setColor(ThemesRepo.getColor(android.R.attr.colorAccent));
|
||||
}
|
||||
|
||||
private final static class Sparkle {
|
||||
private PointF position;
|
||||
private PointF velocity;
|
||||
private float size;
|
||||
private float alpha;
|
||||
private long lifetime;
|
||||
private long living;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
@@ -14,12 +15,14 @@ import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
@@ -30,14 +33,22 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import ru.ytkab0bp.eventbus.EventHandler;
|
||||
import ru.ytkab0bp.slicebeam.BeamServerData;
|
||||
import ru.ytkab0bp.slicebeam.BuildConfig;
|
||||
import ru.ytkab0bp.slicebeam.MainActivity;
|
||||
import ru.ytkab0bp.slicebeam.R;
|
||||
import ru.ytkab0bp.slicebeam.SetupActivity;
|
||||
import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudController;
|
||||
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
|
||||
import ru.ytkab0bp.slicebeam.components.UnfoldMenu;
|
||||
import ru.ytkab0bp.slicebeam.components.WebViewMenu;
|
||||
import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudFeaturesUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudModelsRemainingCountUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedDismissAIGeneratorMenu;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedDismissCalibrationsMenu;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedDismissSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.ObjectsListChangedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.SelectedObjectChangedEvent;
|
||||
@@ -50,13 +61,18 @@ import ru.ytkab0bp.slicebeam.slic3r.Bed3D;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.Slic3rRuntimeError;
|
||||
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
|
||||
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
|
||||
import ru.ytkab0bp.slicebeam.utils.Prefs;
|
||||
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
|
||||
import ru.ytkab0bp.slicebeam.view.DividerView;
|
||||
import ru.ytkab0bp.slicebeam.view.FadeRecyclerView;
|
||||
import ru.ytkab0bp.slicebeam.view.SegmentsView;
|
||||
import ru.ytkab0bp.slicebeam.view.SnackbarsLayout;
|
||||
|
||||
public class FileMenu extends ListBedMenu {
|
||||
private final static List<String> K3D_SUPPORTED_LANGUAGES = Arrays.asList("en", "ru");
|
||||
|
||||
private boolean wasPortrait;
|
||||
|
||||
private String getK3DLanguage() {
|
||||
String lang = Locale.getDefault().getLanguage();
|
||||
return K3D_SUPPORTED_LANGUAGES.contains(lang) ? lang : "en";
|
||||
@@ -74,13 +90,18 @@ public class FileMenu extends ListBedMenu {
|
||||
.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private boolean hasModel() {
|
||||
return fragment.getGlView().getRenderer().getModel() != null;
|
||||
}
|
||||
|
||||
private boolean hasSelection() {
|
||||
return fragment.getGlView().getRenderer().getModel() != null && fragment.getGlView().getRenderer().getSelectedObject() != -1;
|
||||
return hasModel() && fragment.getGlView().getRenderer().getSelectedObject() != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SimpleRecyclerItem> onCreateItems(boolean portrait) {
|
||||
return Arrays.asList(
|
||||
wasPortrait = portrait;
|
||||
List<SimpleRecyclerItem> list = new ArrayList<>(Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuFileOpen, R.drawable.folder_simple_plus_outline_28).onClick(v -> {
|
||||
if (!fragment.getGlView().getRenderer().getBed().isValid()) {
|
||||
Toast.makeText(fragment.getContext(), R.string.BedConfigurationError, Toast.LENGTH_SHORT).show();
|
||||
@@ -104,7 +125,31 @@ public class FileMenu extends ListBedMenu {
|
||||
fragment.updateModel();
|
||||
}
|
||||
}),
|
||||
new SpaceItem(portrait ? ViewUtils.dp(3) : 0, portrait ? 0 : ViewUtils.dp(3)),
|
||||
new SpaceItem(portrait ? ViewUtils.dp(3) : 0, portrait ? 0 : ViewUtils.dp(3))));
|
||||
if (BeamServerData.isBoostyAvailable() && CloudController.needShowAIGenerator()) {
|
||||
list.add(new BedMenuItem(R.string.MenuFileAIGenerator, R.drawable.picture_stack_outline_28).setShiny(true).onClick(view -> {
|
||||
if (Prefs.getCloudAPIToken() == null || CloudController.getUserInfo() != null && CloudController.getMaxGeneratedModels() == 0) {
|
||||
Context ctx = view.getContext();
|
||||
ctx.startActivity(new Intent(ctx, SetupActivity.class).putExtra(SetupActivity.EXTRA_CLOUD_PROFILE, true));
|
||||
return;
|
||||
}
|
||||
if (CloudController.getUserInfo() == null) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuFileAIGeneratorPleaseWaitSetup).tag(CloudController.USER_INFO_AI_GEN_TAG));
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
if (CloudController.getUserInfo() == null) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(CloudController.USER_INFO_AI_GEN_TAG));
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorErrorNotLoadedUserAccount));
|
||||
} else {
|
||||
fragment.showUnfoldMenu(new AIGeneratorMenu(), view);
|
||||
}
|
||||
}, 2500);
|
||||
return;
|
||||
}
|
||||
|
||||
fragment.showUnfoldMenu(new AIGeneratorMenu(), view);
|
||||
}));
|
||||
}
|
||||
list.addAll(Arrays.asList(
|
||||
new BedMenuItem(R.string.MenuFileCalibrations, R.drawable.wrench_outline_28).setSingleLine(true).onClick(v -> {
|
||||
if (!fragment.getGlView().getRenderer().getBed().isValid()) {
|
||||
Toast.makeText(fragment.getContext(), R.string.BedConfigurationError, Toast.LENGTH_SHORT).show();
|
||||
@@ -200,14 +245,28 @@ public class FileMenu extends ListBedMenu {
|
||||
.show())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}),
|
||||
new BedMenuItem(R.string.MenuFileExport3mf, R.drawable.arrow_down_to_square_outline_28).setEnabled(hasModel()).onClick(v -> {
|
||||
if (fragment.getContext() instanceof Activity) {
|
||||
Activity act = (Activity) fragment.getContext();
|
||||
Intent i = new Intent(Intent.ACTION_CREATE_DOCUMENT);
|
||||
i.setType("application/3mf");
|
||||
i.putExtra(Intent.EXTRA_TITLE, "SliceBeam_project.3mf");
|
||||
act.startActivityForResult(i, MainActivity.REQUEST_CODE_EXPORT_3MF);
|
||||
}
|
||||
})
|
||||
);
|
||||
));
|
||||
return list;
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onObjectsChanged(ObjectsListChangedEvent e) {
|
||||
((BedMenuItem) adapter.getItems().get(1)).setEnabled(hasSelection());
|
||||
adapter.notifyItemChanged(1);
|
||||
|
||||
int i = 8 - (BeamServerData.isBoostyAvailable() && CloudController.needShowAIGenerator() ? 0 : 1);
|
||||
((BedMenuItem) adapter.getItems().get(i)).setEnabled(hasModel());
|
||||
adapter.notifyItemChanged(i);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
@@ -216,8 +275,144 @@ public class FileMenu extends ListBedMenu {
|
||||
adapter.notifyItemChanged(1);
|
||||
}
|
||||
|
||||
public final class CalibrationsMenu extends UnfoldMenu {
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onFeaturedUpdated(CloudFeaturesUpdatedEvent e) {
|
||||
adapter.setItems(onCreateItems(wasPortrait));
|
||||
}
|
||||
|
||||
public final static class AIGeneratorMenu extends UnfoldMenu {
|
||||
private TextView remainingView;
|
||||
private SegmentsView segmentsView;
|
||||
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return (int) (portrait ? ViewUtils.dp(52) + ViewUtils.dp(60) * 2 + ViewUtils.dp(28) + ViewUtils.dp(18) + ViewUtils.dp(2) : into.getWidth() * 0.6f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(Context ctx, boolean portrait) {
|
||||
LinearLayout ll = new LinearLayout(ctx);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
RecyclerView rv = new FadeRecyclerView(ctx);
|
||||
rv.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter();
|
||||
adapter.setItems(Arrays.asList(
|
||||
new PreferenceItem().setIcon(R.drawable.camera_outline_28).setTitle(ctx.getString(R.string.MenuFileAIGeneratorFromCamera)).setOnClickListener(v -> {
|
||||
if (CloudController.getGeneratedModels() >= CloudController.getMaxGeneratedModels()) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft));
|
||||
return;
|
||||
}
|
||||
if (ctx instanceof MainActivity) {
|
||||
try {
|
||||
MainActivity.aiTempFile = File.createTempFile("ai_capture", ".jpg");
|
||||
Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
i.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(ctx, BuildConfig.APPLICATION_ID + ".provider", MainActivity.aiTempFile));
|
||||
i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
((MainActivity) ctx).startActivityForResult(i, MainActivity.REQUEST_CODE_AI_GENERATOR_TAKE_PHOTO);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new PreferenceItem().setIcon(R.drawable.picture_outline_28).setTitle(ctx.getString(R.string.MenuFileAIGeneratorFromGallery)).setOnClickListener(v -> {
|
||||
if (CloudController.getGeneratedModels() >= CloudController.getMaxGeneratedModels()) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.ERROR, R.string.MenuFileAIGeneratorNoGenerationsLeft));
|
||||
return;
|
||||
}
|
||||
if (ctx instanceof MainActivity) {
|
||||
Intent intent = new Intent();
|
||||
intent.setType("image/*");
|
||||
intent.setAction(Intent.ACTION_GET_CONTENT);
|
||||
((MainActivity) ctx).startActivityForResult(Intent.createChooser(intent, ""), MainActivity.REQUEST_CODE_AI_GENERATOR_CHOOSE_PHOTO);
|
||||
}
|
||||
})
|
||||
));
|
||||
rv.setAdapter(adapter);
|
||||
ll.addView(rv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
remainingView = new TextView(ctx);
|
||||
remainingView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13);
|
||||
remainingView.setGravity(Gravity.CENTER);
|
||||
remainingView.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
ll.addView(remainingView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(18)) {{
|
||||
topMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
|
||||
segmentsView = new SegmentsView(ctx) {
|
||||
@Override
|
||||
protected int onGetColor(int i) {
|
||||
return i == 1 ? ThemesRepo.getColor(android.R.attr.textColorSecondary) : ThemesRepo.getColor(android.R.attr.colorAccent);
|
||||
}
|
||||
};
|
||||
ll.addView(segmentsView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(12)) {{
|
||||
leftMargin = rightMargin = ViewUtils.dp(12);
|
||||
topMargin = bottomMargin = ViewUtils.dp(8);
|
||||
}});
|
||||
updateRemaining();
|
||||
|
||||
ll.addView(new DividerView(ctx), new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(1f)));
|
||||
|
||||
LinearLayout toolbar = new LinearLayout(ctx);
|
||||
toolbar.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0);
|
||||
toolbar.setOrientation(LinearLayout.HORIZONTAL);
|
||||
toolbar.setGravity(Gravity.CENTER_VERTICAL);
|
||||
toolbar.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0));
|
||||
toolbar.setOnClickListener(v -> dismiss());
|
||||
|
||||
ImageView icon = new ImageView(ctx);
|
||||
icon.setImageResource(R.drawable.arrow_left_outline_28);
|
||||
icon.setColorFilter(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
toolbar.addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)));
|
||||
|
||||
TextView title = new TextView(ctx);
|
||||
title.setText(R.string.MenuOrientationPositionBack);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
toolbar.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f) {{
|
||||
leftMargin = ViewUtils.dp(12);
|
||||
}});
|
||||
ll.addView(toolbar, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)));
|
||||
return ll;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
SliceBeam.EVENT_BUS.registerListener(this);
|
||||
ViewUtils.postOnMainThread(() -> segmentsView.startAnimation(), 50);
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onDismiss(NeedDismissAIGeneratorMenu e) {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onRemainingUpdated(CloudModelsRemainingCountUpdatedEvent e) {
|
||||
updateRemaining();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
SliceBeam.EVENT_BUS.unregisterListener(this);
|
||||
}
|
||||
|
||||
private void updateRemaining() {
|
||||
int rev = CloudController.getMaxGeneratedModels() - CloudController.getGeneratedModels();
|
||||
remainingView.setText(SliceBeam.INSTANCE.getString(R.string.MenuFileAIGeneratorRemaining, rev, CloudController.getMaxGeneratedModels()));
|
||||
segmentsView.setValues(new float[]{0, rev / (float) CloudController.getMaxGeneratedModels(), 1});
|
||||
}
|
||||
}
|
||||
|
||||
public final class CalibrationsMenu extends UnfoldMenu {
|
||||
@Override
|
||||
public int getRequestedSize(FrameLayout into, boolean portrait) {
|
||||
return (int) (portrait ? into.getHeight() * 0.35f : into.getWidth() * 0.6f);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ public abstract class ListBedMenu extends BedMenu {
|
||||
recyclerView = new RecyclerView(ctx);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(ctx, portrait ? RecyclerView.HORIZONTAL : RecyclerView.VERTICAL, false));
|
||||
recyclerView.setItemAnimator(null);
|
||||
recyclerView.setClipToPadding(false);
|
||||
recyclerView.setClipChildren(false);
|
||||
adapter = new SimpleRecyclerAdapter() {
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.ytkab0bp.slicebeam.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudFeaturesUpdatedEvent {}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.ytkab0bp.slicebeam.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudLoginStateUpdatedEvent {}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package ru.ytkab0bp.slicebeam.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudModelsRemainingCountUpdatedEvent {}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.ytkab0bp.slicebeam.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class CloudUserInfoUpdatedEvent {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.ytkab0bp.slicebeam.events;
|
||||
|
||||
import ru.ytkab0bp.eventbus.Event;
|
||||
|
||||
@Event
|
||||
public class NeedDismissAIGeneratorMenu {}
|
||||
@@ -15,9 +15,6 @@ import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -116,6 +113,7 @@ public class BedFragment extends Fragment {
|
||||
private UnfoldMenu currentUnfoldMenu;
|
||||
|
||||
private BedSwipeDownLayout swipeDownLayout;
|
||||
private boolean hasWebError;
|
||||
private WebView panelWebView;
|
||||
private LinearLayout panelWebViewError;
|
||||
private ImageView webViewErrIcon;
|
||||
@@ -229,7 +227,7 @@ public class BedFragment extends Fragment {
|
||||
super.onResume();
|
||||
glView.onResume();
|
||||
ConfigObject cfg = SliceBeam.CONFIG.findPrinter(SliceBeam.CONFIG.presets.get("printer"));
|
||||
boolean enable = cfg != null && cfg.get("host_type") != null && !TextUtils.isEmpty(cfg.get("print_host"));
|
||||
boolean enable = cfg != null && cfg.get("host_type") != null && !TextUtils.isEmpty(cfg.get("print_host")) && panelWebView != null;
|
||||
swipeDownLayout.setEnableTop(enable);
|
||||
if (enable) {
|
||||
String host = cfg.get("print_host");
|
||||
@@ -246,6 +244,7 @@ public class BedFragment extends Fragment {
|
||||
}
|
||||
webViewProgressBar.animate().alpha(1).setDuration(150).start();
|
||||
panelWebView.setAlpha(0f);
|
||||
hasWebError = false;
|
||||
panelWebView.loadUrl(host);
|
||||
panelWebViewError.animate().alpha(0).setDuration(150).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
@@ -294,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) {
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
@@ -34,6 +36,8 @@ import androidx.dynamicanimation.animation.SpringAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import com.mrudultora.colorpicker.ColorPickerPopUp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -47,6 +51,8 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import ru.ytkab0bp.slicebeam.R;
|
||||
import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudAPI;
|
||||
import ru.ytkab0bp.slicebeam.cloud.CloudController;
|
||||
import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
|
||||
import ru.ytkab0bp.slicebeam.components.BeamColorPickerPopUp;
|
||||
import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
@@ -56,6 +62,7 @@ import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
|
||||
import ru.ytkab0bp.slicebeam.recycler.PreferenceSwitchItem;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.ConfigOptionDef;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.Slic3rConfigWrapper;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.Slic3rLocalization;
|
||||
import ru.ytkab0bp.slicebeam.theme.IThemeView;
|
||||
@@ -68,6 +75,8 @@ import ru.ytkab0bp.slicebeam.view.FadeRecyclerView;
|
||||
import ru.ytkab0bp.slicebeam.view.ProfileDropdownView;
|
||||
|
||||
public abstract class ProfileListFragment extends Fragment {
|
||||
public final static int SPECIAL_TYPE_CLOUD_HEADER = 0;
|
||||
|
||||
private final static Object ROTATION_PAYLOAD = new Object();
|
||||
|
||||
protected ProfileDropdownView dropdownView;
|
||||
@@ -146,8 +155,8 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
int pos = getChildViewHolder(ch).getAdapterPosition();
|
||||
if (pos == -1 || ch.getAlpha() < 1) continue;
|
||||
|
||||
boolean top = currentList.get(pos).title != null;
|
||||
boolean bottom = pos == getAdapter().getItemCount() - 1 || currentList.get(pos + 1).title != null;
|
||||
boolean top = currentList.get(pos).title != null || currentList.get(pos).hasSpecialType();
|
||||
boolean bottom = pos == getAdapter().getItemCount() - 1 || currentList.get(pos + 1).title != null || currentList.get(pos + 1).hasSpecialType();
|
||||
|
||||
if (top && startI != -1) {
|
||||
c.drawRoundRect(0, getChildAt(startI).getTop() + getChildAt(startI).getTranslationY(), getWidth(), ch.getTop() + ch.getTranslationY() - ViewUtils.dp(8), ViewUtils.dp(32), ViewUtils.dp(32), bgPaint);
|
||||
@@ -204,7 +213,7 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
};
|
||||
recyclerView.setItemAnimator(new CubicBezierItemAnimator());
|
||||
recyclerView.setAdapter(new RecyclerView.Adapter() {
|
||||
private final static int TYPE_TITLE = 0, TYPE_SIMPLE = 1;
|
||||
private final static int TYPE_TITLE = 0, TYPE_CLOUD_PROFILE = 1, TYPE_SIMPLE = 2;
|
||||
|
||||
private Map<Class<?>, Integer> viewType = new HashMap<>();
|
||||
private Map<Integer, SimpleRecyclerItem> viewCreator = new HashMap<>();
|
||||
@@ -219,6 +228,9 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
v = viewCreator.get(viewType).onCreateView(ctx);
|
||||
break;
|
||||
}
|
||||
case TYPE_CLOUD_PROFILE:
|
||||
v = new CloudProfileHeaderView(ctx);
|
||||
break;
|
||||
case TYPE_TITLE:
|
||||
v = new CategoryHolderView(ctx);
|
||||
break;
|
||||
@@ -258,6 +270,43 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
el.simpleItem.onBindView(holder.itemView);
|
||||
break;
|
||||
}
|
||||
case TYPE_CLOUD_PROFILE: {
|
||||
OptionWrapper w = currentList.get(position);
|
||||
CloudProfileHeaderView holderView = (CloudProfileHeaderView) holder.itemView;
|
||||
holderView.setTag(w.color);
|
||||
if (Prefs.getCloudAPIToken() != null) {
|
||||
CloudAPI.UserInfo info = CloudController.getUserInfo();
|
||||
if (info != null) {
|
||||
if (!TextUtils.isEmpty(info.avatarUrl)) {
|
||||
holderView.hasAvatar = true;
|
||||
Glide.with(holderView.avatar)
|
||||
.load(info.avatarUrl)
|
||||
.circleCrop()
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(holderView.avatar);
|
||||
} else {
|
||||
holderView.hasAvatar = false;
|
||||
holderView.avatar.setImageResource(R.drawable.user_circle_outline_28);
|
||||
}
|
||||
|
||||
holderView.title.setText(info.displayName);
|
||||
} else {
|
||||
holderView.hasAvatar = false;
|
||||
holderView.avatar.setImageResource(R.drawable.user_circle_outline_28);
|
||||
|
||||
holderView.title.setText(R.string.SettingsCloudLoading);
|
||||
}
|
||||
holderView.subtitle.setText(R.string.SettingsCloudTapToManage);
|
||||
} else {
|
||||
holderView.hasAvatar = false;
|
||||
holderView.avatar.setImageResource(R.drawable.user_circle_outline_28);
|
||||
holderView.title.setText(R.string.SettingsCloudNotLoggedIn);
|
||||
holderView.subtitle.setText(R.string.SettingsCloudTapToShowMore);
|
||||
}
|
||||
holderView.onApplyTheme();
|
||||
holderView.setOnClickListener(view -> w.onClick.run());
|
||||
break;
|
||||
}
|
||||
case TYPE_TITLE: {
|
||||
OptionWrapper w = currentList.get(position);
|
||||
CategoryHolderView holderView = (CategoryHolderView) holder.itemView;
|
||||
@@ -301,6 +350,13 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
OptionWrapper w = currentList.get(position);
|
||||
if (w.optionEl != null && w.optionEl.specialType != -1) {
|
||||
switch (w.optionEl.specialType) {
|
||||
default:
|
||||
case SPECIAL_TYPE_CLOUD_HEADER:
|
||||
return TYPE_CLOUD_PROFILE;
|
||||
}
|
||||
}
|
||||
if (w.title != null) return TYPE_TITLE;
|
||||
|
||||
if (w.optionEl.simpleItem != null) {
|
||||
@@ -469,7 +525,12 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
OptionElement el = items.get(i);
|
||||
if (el == null) continue;
|
||||
OptionWrapper w = el.title != null ? new OptionWrapper(el.icon, el.title, el.onClick, el.color, el.noTint) : new OptionWrapper(el);
|
||||
if (el.title != null) {
|
||||
if (el.specialType != -1) {
|
||||
w.color = el.color;
|
||||
w.noTint = el.noTint;
|
||||
w.onClick = el.onClick;
|
||||
}
|
||||
if (el.title != null || el.specialType != -1) {
|
||||
w.categoryIndex = j;
|
||||
categoryElements.put(j, new ArrayList<>());
|
||||
j++;
|
||||
@@ -563,6 +624,8 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
}
|
||||
|
||||
public final class OptionElement {
|
||||
public int specialType = -1;
|
||||
|
||||
public int icon;
|
||||
public String title;
|
||||
public int color;
|
||||
@@ -916,6 +979,10 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
optionEl = el;
|
||||
}
|
||||
|
||||
boolean hasSpecialType() {
|
||||
return optionEl != null && optionEl.specialType != -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(Context ctx) {
|
||||
FrameLayout v = new FrameLayout(ctx);
|
||||
@@ -926,6 +993,49 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private final static class CloudProfileHeaderView extends LinearLayout implements IThemeView {
|
||||
private ImageView avatar;
|
||||
private TextView title;
|
||||
private TextView subtitle;
|
||||
private boolean hasAvatar;
|
||||
|
||||
public CloudProfileHeaderView(Context context) {
|
||||
super(context);
|
||||
|
||||
setOrientation(HORIZONTAL);
|
||||
setGravity(Gravity.CENTER_VERTICAL);
|
||||
setPadding(ViewUtils.dp(21), ViewUtils.dp(16), ViewUtils.dp(21), ViewUtils.dp(16));
|
||||
|
||||
avatar = new ImageView(context);
|
||||
addView(avatar, new LayoutParams(ViewUtils.dp(26), ViewUtils.dp(26)) {{
|
||||
setMarginEnd(ViewUtils.dp(12));
|
||||
}});
|
||||
|
||||
LinearLayout ll = new LinearLayout(context);
|
||||
ll.setOrientation(VERTICAL);
|
||||
ll.setGravity(Gravity.CENTER);
|
||||
title = new TextView(context);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16);
|
||||
title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM));
|
||||
ll.addView(title);
|
||||
subtitle = new TextView(context);
|
||||
subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
|
||||
ll.addView(subtitle);
|
||||
addView(ll, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 32));
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
if (!hasAvatar) {
|
||||
avatar.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
|
||||
}
|
||||
}
|
||||
}
|
||||
private final static class CategoryHolderView extends LinearLayout implements IThemeView {
|
||||
private ImageView icon;
|
||||
private TextView title;
|
||||
|
||||
@@ -28,6 +28,7 @@ import ru.ytkab0bp.slicebeam.components.BeamAlertDialogBuilder;
|
||||
import ru.ytkab0bp.slicebeam.components.BeamColorPickerPopUp;
|
||||
import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
import ru.ytkab0bp.slicebeam.events.BeamServerDataUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.CloudUserInfoUpdatedEvent;
|
||||
import ru.ytkab0bp.slicebeam.recycler.PreferenceItem;
|
||||
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
|
||||
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
|
||||
@@ -45,6 +46,10 @@ public class SettingsFragment extends ProfileListFragment {
|
||||
@Override
|
||||
protected List<OptionElement> getConfigItems() {
|
||||
return Arrays.asList(
|
||||
BeamServerData.isCloudAvailable() ? new OptionElement(SPECIAL_TYPE_CLOUD_HEADER).setOnClick(() -> {
|
||||
Activity act = (Activity) getContext();
|
||||
act.startActivity(new Intent(act, SetupActivity.class).putExtra(SetupActivity.EXTRA_CLOUD_PROFILE, true));
|
||||
}) : null,
|
||||
new OptionElement(R.drawable.paint_roller_outline_28, getContext().getString(R.string.SettingsInterface)),
|
||||
new OptionElement(new PreferenceItem().setTitle(getContext().getString(R.string.SettingsInterfaceTheme)).setValueProvider(() -> getContext().getString(Prefs.getThemeMode().title)).setOnClickListener(v -> {
|
||||
String[] items = new String[Prefs.ThemeMode.values().length];
|
||||
@@ -107,7 +112,7 @@ public class SettingsFragment extends ProfileListFragment {
|
||||
BeamTheme.LIGHT.colors.put(android.R.attr.colorAccent, Prefs.getAccentColor());
|
||||
BeamTheme.DARK.colors.put(android.R.attr.colorAccent, Prefs.getAccentColor());
|
||||
ThemesRepo.invalidate((Activity) getContext());
|
||||
recyclerView.getAdapter().notifyItemChanged(1);
|
||||
recyclerView.getAdapter().notifyItemChanged(2 - (BeamServerData.isCloudAvailable() ? 0 : 1));
|
||||
}
|
||||
})
|
||||
.setNegativeButtonText(getContext().getString(R.string.SettingsInterfaceColorReset))
|
||||
@@ -130,7 +135,7 @@ public class SettingsFragment extends ProfileListFragment {
|
||||
Prefs.setRenderScale(variants[which]);
|
||||
dialog.dismiss();
|
||||
// I'm too lazy to calculate real position for now
|
||||
recyclerView.getAdapter().notifyItemChanged(3);
|
||||
recyclerView.getAdapter().notifyItemChanged(4 - (BeamServerData.isCloudAvailable() ? 0 : 1));
|
||||
})
|
||||
.show();
|
||||
})),
|
||||
@@ -177,6 +182,13 @@ public class SettingsFragment extends ProfileListFragment {
|
||||
setConfigItems(getConfigItems());
|
||||
}
|
||||
|
||||
@EventHandler(runOnMainThread = true)
|
||||
public void onUserInfoUpdated(CloudUserInfoUpdatedEvent e) {
|
||||
if (BeamServerData.isCloudAvailable()) {
|
||||
recyclerView.getAdapter().notifyItemChanged(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cloneCurrentProfile() {}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||
import ru.ytkab0bp.slicebeam.theme.BeamTheme;
|
||||
import ru.ytkab0bp.slicebeam.theme.IThemeView;
|
||||
import ru.ytkab0bp.slicebeam.theme.ThemesRepo;
|
||||
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
|
||||
@@ -35,6 +36,8 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
|
||||
private boolean noTint;
|
||||
private ValueProvider valueProvider;
|
||||
private float roundRadius;
|
||||
private int mPaddings = ViewUtils.dp(12);
|
||||
private boolean mForceDark;
|
||||
|
||||
public PreferenceItem setTitle(CharSequence title) {
|
||||
mTitle = title;
|
||||
@@ -46,6 +49,16 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setPaddings(int paddings) {
|
||||
this.mPaddings = paddings;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setForceDark(boolean mForceDark) {
|
||||
this.mForceDark = mForceDark;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PreferenceItem setSubtitleProvider(ValueProvider mSubtitle) {
|
||||
this.mSubtitle = mSubtitle;
|
||||
return this;
|
||||
@@ -112,6 +125,8 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
|
||||
private TextView value;
|
||||
private float radius;
|
||||
|
||||
private PreferenceItem item;
|
||||
|
||||
public PreferenceHolderView(Context context) {
|
||||
super(context);
|
||||
|
||||
@@ -165,14 +180,14 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
|
||||
value.setVisibility(GONE);
|
||||
addView(value, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
int pad = ViewUtils.dp(12);
|
||||
setPadding(pad, pad, pad, pad);
|
||||
setMinimumHeight(ViewUtils.dp(56));
|
||||
setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
onApplyTheme();
|
||||
}
|
||||
|
||||
void bind(PreferenceItem item) {
|
||||
this.item = item;
|
||||
setPadding(item.mPaddings, item.mPaddings, item.mPaddings, item.mPaddings);
|
||||
title.setText(item.mTitle);
|
||||
title.setVisibility(TextUtils.isEmpty(item.mTitle) ? GONE : VISIBLE);
|
||||
|
||||
@@ -217,15 +232,19 @@ public class PreferenceItem extends SimpleRecyclerItem<PreferenceItem.Preference
|
||||
|
||||
ViewGroup.LayoutParams params = icon.getLayoutParams();
|
||||
params.width = params.height = radius != 0 ? ViewUtils.dp(42) : ViewUtils.dp(28);
|
||||
if (item.mForceDark) {
|
||||
onApplyTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTheme() {
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
value.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
|
||||
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
|
||||
BeamTheme theme = item != null && item.mForceDark ? BeamTheme.DARK : ThemesRepo.getCurrent();
|
||||
title.setTextColor(theme.colors.get(android.R.attr.textColorPrimary));
|
||||
subtitle.setTextColor(theme.colors.get(android.R.attr.textColorSecondary));
|
||||
value.setTextColor(theme.colors.get(android.R.attr.textColorSecondary));
|
||||
icon.setImageTintList(ColorStateList.valueOf(theme.colors.get(android.R.attr.textColorSecondary)));
|
||||
setBackground(ViewUtils.createRipple(theme.colors.get(android.R.attr.colorControlHighlight), 16));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,9 +96,10 @@ public class PreferenceSwitchItem extends SimpleRecyclerItem<PreferenceSwitchIte
|
||||
|
||||
icon = new ImageView(context);
|
||||
icon.setLayoutParams(new LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{
|
||||
setMarginEnd(ViewUtils.dp(16));
|
||||
gravity = Gravity.CENTER_VERTICAL;
|
||||
setMarginStart(ViewUtils.dp(4));
|
||||
setMarginEnd(ViewUtils.dp(8));
|
||||
}});
|
||||
addView(icon);
|
||||
|
||||
LinearLayout innerLayout = new LinearLayout(context);
|
||||
innerLayout.setOrientation(VERTICAL);
|
||||
@@ -168,7 +169,7 @@ public class PreferenceSwitchItem extends SimpleRecyclerItem<PreferenceSwitchIte
|
||||
public void onApplyTheme() {
|
||||
title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary));
|
||||
subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorAccent)));
|
||||
icon.setImageTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.textColorSecondary)));
|
||||
setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public class BeamTheme {
|
||||
colors.put(R.attr.dialogBackground, 0xffffffff);
|
||||
colors.put(R.attr.switchThumbUncheckedColor, 0xffeef2f3);
|
||||
colors.put(R.attr.boostyColorTop, 0xfff06e2a);
|
||||
colors.put(R.attr.boostyColorBottom, 0xfffce2d4);
|
||||
colors.put(R.attr.boostyColorBottom, 0xff884725);
|
||||
colors.put(R.attr.telegramColor, 0xff27a7e7);
|
||||
colors.put(R.attr.k3dColor, 0xff039045);
|
||||
colors.put(R.attr.modelHoverColor, 0xffffffff);
|
||||
|
||||
@@ -125,6 +125,100 @@ public class Prefs {
|
||||
cachedThemeMode = null;
|
||||
}
|
||||
|
||||
public static String getCloudAPIToken() {
|
||||
return mPrefs.getString("cloud_api_token", null);
|
||||
}
|
||||
|
||||
public static void setCloudAPIToken(String token) {
|
||||
SharedPreferences.Editor e = mPrefs.edit();
|
||||
if (token == null) {
|
||||
e.remove("cloud_api_token");
|
||||
} else {
|
||||
e.putString("cloud_api_token", token);
|
||||
}
|
||||
e.apply();
|
||||
}
|
||||
|
||||
public static boolean isCloudProfileSyncEnabled() {
|
||||
return mPrefs.getBoolean("cloud_profile_sync", true);
|
||||
}
|
||||
|
||||
public static void setCloudProfileSyncEnabled(boolean en) {
|
||||
mPrefs.edit().putBoolean("cloud_profile_sync", en).apply();
|
||||
}
|
||||
|
||||
public static String getCloudCachedUserInfo() {
|
||||
return mPrefs.getString("cloud_cached_user_info", null);
|
||||
}
|
||||
|
||||
public static void setCloudCachedUserInfo(String info) {
|
||||
SharedPreferences.Editor e = mPrefs.edit();
|
||||
if (info == null) {
|
||||
e.remove("cloud_cached_user_info");
|
||||
} else {
|
||||
e.putString("cloud_cached_user_info", info);
|
||||
}
|
||||
e.apply();
|
||||
}
|
||||
|
||||
public static int getCloudCachedUsedModels() {
|
||||
return mPrefs.getInt("cloud_cached_models_used", 0);
|
||||
}
|
||||
|
||||
public static int getCloudCachedMaxModels() {
|
||||
return mPrefs.getInt("cloud_cached_models_max", 50);
|
||||
}
|
||||
|
||||
public static void setCloudCachedUsedMaxModels(int used, int max) {
|
||||
mPrefs.edit().putInt("cloud_cached_models_used", used).putInt("cloud_cached_models_max", max).apply();
|
||||
}
|
||||
|
||||
public static String getCloudCachedUserFeatures() {
|
||||
return mPrefs.getString("cloud_cached_user_features", null);
|
||||
}
|
||||
|
||||
public static void setCloudCachedUserFeatures(String features) {
|
||||
SharedPreferences.Editor e = mPrefs.edit();
|
||||
if (features == null) {
|
||||
e.remove("cloud_cached_user_features");
|
||||
} else {
|
||||
e.putString("cloud_cached_user_features", features);
|
||||
}
|
||||
e.apply();
|
||||
}
|
||||
|
||||
public static long getCloudLastFeaturesSync() {
|
||||
return mPrefs.getLong("cloud_last_features_sync", 0);
|
||||
}
|
||||
|
||||
public static void setCloudLastFeaturesSync(long ls) {
|
||||
mPrefs.edit().putLong("cloud_last_features_sync", ls).apply();
|
||||
}
|
||||
|
||||
public static long getCloudLastSync() {
|
||||
return mPrefs.getLong("cloud_last_sync", 0);
|
||||
}
|
||||
|
||||
public static void setCloudLastSync(long ls) {
|
||||
mPrefs.edit().putLong("cloud_last_sync", ls).apply();
|
||||
}
|
||||
|
||||
public static long getLocalLastModified() {
|
||||
return mPrefs.getLong("cloud_local_last_modified", 0);
|
||||
}
|
||||
|
||||
public static void setLocalLastModified(long lm) {
|
||||
mPrefs.edit().putLong("cloud_local_last_modified", lm).apply();
|
||||
}
|
||||
|
||||
public static long getRemoteLastModified() {
|
||||
return mPrefs.getLong("cloud_remote_last_modified", 0);
|
||||
}
|
||||
|
||||
public static void setRemoteLastModified(long lm) {
|
||||
mPrefs.edit().putLong("cloud_remote_last_modified", lm).apply();
|
||||
}
|
||||
|
||||
public enum ThemeMode {
|
||||
SYSTEM(R.string.SettingsInterfaceThemeSystem),
|
||||
LIGHT(R.string.SettingsInterfaceThemeLight),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package ru.ytkab0bp.slicebeam.utils;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class RandomUtils {
|
||||
|
||||
public final static Random RANDOM = new Random();
|
||||
|
||||
public static float randomf(float min, float max) {
|
||||
return min + RANDOM.nextFloat() * (max - min);
|
||||
}
|
||||
|
||||
public static long randoml(long min, long max) {
|
||||
return (long) (min + RANDOM.nextDouble() * (max - min));
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,11 @@ public class ViewUtils {
|
||||
private static Handler uiHandler = new Handler(Looper.getMainLooper());
|
||||
private static Map<String, Typeface> typefaceCache = new HashMap<>();
|
||||
|
||||
|
||||
public static Handler getUiHandler() {
|
||||
return uiHandler;
|
||||
}
|
||||
|
||||
public static void postOnMainThread(Runnable runnable) {
|
||||
uiHandler.post(runnable);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@ public class SegmentsView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
protected int onGetColor(int i) {
|
||||
return mapColor(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
@@ -125,7 +129,7 @@ public class SegmentsView extends View {
|
||||
for (int i = 1; i < currentValues.length; i++) {
|
||||
float prev = currentValues[i - 1];
|
||||
float to = currentValues[i];
|
||||
paint.setColor(mapColor(i - 1));
|
||||
paint.setColor(onGetColor(i - 1));
|
||||
canvas.drawRect(l + prev * dw, 0, l + to * dw, getHeight(), paint);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user