package com.dark98.santoku; 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.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.opengl.GLSurfaceView; import android.os.Build; import android.os.Bundle; import android.text.InputType; import android.text.SpannableStringBuilder; import android.text.TextUtils; 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; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.EditText; import android.widget.CheckBox; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import androidx.activity.EdgeToEdge; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.core.graphics.Insets; 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; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.AsyncHttpResponseHandler; import ru.ytkab0bp.sapil.APICallback; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import cz.msebera.android.httpclient.Header; import ru.ytkab0bp.eventbus.EventHandler; import com.dark98.santoku.cloud.CloudAPI; import com.dark98.santoku.cloud.CloudController; import com.dark98.santoku.components.BeamAlertDialogBuilder; import com.dark98.santoku.components.CloudManageBottomSheet; import com.dark98.santoku.config.ConfigObject; import com.dark98.santoku.events.CloudLoginStateUpdatedEvent; import com.dark98.santoku.events.CloudSyncFinishedEvent; import com.dark98.santoku.recycler.BigHeaderItem; import com.dark98.santoku.recycler.PreferenceItem; import com.dark98.santoku.recycler.SimpleRecyclerAdapter; import com.dark98.santoku.recycler.SimpleRecyclerItem; import com.dark98.santoku.recycler.TextHintRecyclerItem; import com.dark98.santoku.slic3r.GLModel; import com.dark98.santoku.slic3r.GLShaderProgram; import com.dark98.santoku.slic3r.GLShadersManager; import com.dark98.santoku.slic3r.Slic3rConfigWrapper; import com.dark98.santoku.slic3r.Slic3rUtils; import com.dark98.santoku.theme.BeamTheme; import com.dark98.santoku.theme.IThemeView; import com.dark98.santoku.theme.ThemesRepo; import com.dark98.santoku.utils.Prefs; import com.dark98.santoku.utils.ViewUtils; import com.dark98.santoku.view.BeamSwitch; import com.dark98.santoku.view.FadeRecyclerView; import com.dark98.santoku.view.MiniColorView; import com.dark98.santoku.view.TextColorImageSpan; public class SetupActivity extends AppCompatActivity { public final static String EXTRA_ABOUT = "about"; public final static String EXTRA_CLOUD_PROFILE = "cloud_profile"; public final static String EXTRA_CLOUD_IMPORT_FROM_SETUP = "cloud_import_from_setup"; private final static String TAG = "SetupActivity"; private final static List REPOS_URLS = Arrays.asList( "https://preset-repo-api.prusa3d.com/v1/repos", "https://raw.githubusercontent.com/Dark98/SliceBeam/refs/heads/master/.profiledumpsrepo/manifest.json" ); private final static int REPOS_INDEX = 1; private final static int PROFILES_INDEX = 2; private final static int TYPE_PRINTER = 0, TYPE_PRINT_CONFIG = 1, TYPE_FILAMENT = 2; private ViewPager2 pager; private SimpleRecyclerAdapter adapter; private TextView title; private GLSurfaceView backgroundView; private GLModel backgroundModel; private GLShadersManager shadersManager; private int titleY; private float backgroundProgress; private SpringAnimation fakeScroller; private AsyncHttpClient client = new AsyncHttpClient(); private List repos = new ArrayList<>(); private ReposItem reposItem; private ProfilesItem profilesItem; private CloudProfileItem cloudItem; private boolean isReposLoaded; private boolean limitRepoFragmentCount = true; private boolean limitProfileFragmentCount = true; private boolean isLoading; private Map> profilesMap = new HashMap<>(); private boolean isProfilesLoaded; private boolean about; private boolean cloudProfile; private boolean cloudImport; private List enabledPrinters = new ArrayList<>(); { client.setUserAgent(String.format(Locale.ROOT, "Santoku/%s-%d", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); client.setEnableRedirects(true); client.setLoggingEnabled(false); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); Santoku.EVENT_BUS.registerListener(this); about = getIntent().getBooleanExtra(EXTRA_ABOUT, false); cloudProfile = getIntent().getBooleanExtra(EXTRA_CLOUD_PROFILE, false); cloudImport = getIntent().getBooleanExtra(EXTRA_CLOUD_IMPORT_FROM_SETUP, false); if (!about && !cloudProfile) { new BeamAlertDialogBuilder(this) .setTitle(R.string.IntroEarlyAccess) .setMessage(R.string.IntroEarlyAccessMessage) .setPositiveButton(android.R.string.ok, null) .show(); } if (cloudProfile) { backgroundProgress = 1f; } pager = new ViewPager2(this); adapter = new SimpleRecyclerAdapter() { @Override public int getItemCount() { return about || cloudProfile ? 1 : limitRepoFragmentCount ? REPOS_INDEX + 1 : limitProfileFragmentCount ? PROFILES_INDEX + 1 : super.getItemCount(); } }; setItems(); pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageScrollStateChanged(int state) { if (state == ViewPager2.SCROLL_STATE_IDLE) { if (pager.getCurrentItem() <= REPOS_INDEX && !limitRepoFragmentCount) { ViewUtils.postOnMainThread(() -> { isProfilesLoaded = false; profilesMap.clear(); adapter.notifyItemChanged(PROFILES_INDEX); int realCount = adapter.getItemCount(); limitRepoFragmentCount = true; adapter.notifyItemRangeRemoved(REPOS_INDEX + 1, realCount - REPOS_INDEX - 1); }); } if (pager.getCurrentItem() <= PROFILES_INDEX && !limitProfileFragmentCount) { ViewUtils.postOnMainThread(() -> { int realCount = adapter.getItemCount(); limitProfileFragmentCount = true; adapter.notifyItemRangeRemoved(PROFILES_INDEX + 1, realCount - PROFILES_INDEX - 1); }); } } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (position == 0 && !cloudProfile) { backgroundProgress = positionOffset; } else { backgroundProgress = 1f; } if (position == REPOS_INDEX) { if (!isReposLoaded && !isLoading) { loadRepos(true); pager.setUserInputEnabled(false); } if (isLoading) { pager.setUserInputEnabled(false); } } else if (position == PROFILES_INDEX) { if (!isProfilesLoaded && !isLoading && !profilesItem.useCustomProfile) { AtomicInteger loadedCount = new AtomicInteger(); AtomicInteger totalNeeded = new AtomicInteger(); Runnable onLoadedAll = () -> { isProfilesLoaded = true; isLoading = false; pager.setUserInputEnabled(true); profilesItem.onProfilesLoaded(); }; for (ProfilesRepo repo : repos) { if (repo.checked) { totalNeeded.incrementAndGet(); client.get(repo.indexUrl, new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { List vendorProfiles = new ArrayList<>(); Runnable onVendorsLoaded = () -> { profilesMap.put(repo, vendorProfiles); loadedCount.incrementAndGet(); if (loadedCount.get() == totalNeeded.get()) { ViewUtils.postOnMainThread(onLoadedAll); } }; AtomicInteger loadedVendorsCount = new AtomicInteger(); AtomicInteger totalNeededVendors = new AtomicInteger(); List loadRunners = new ArrayList<>(); try { ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(responseBody)); ZipEntry en; while ((en = zis.getNextEntry()) != null) { String version = parseVendorVersion(zis); String baseUrl = repo.url + "/" + en.getName().substring(0, en.getName().length() - 4); String iniUrl = baseUrl + "/" + version + ".ini"; totalNeededVendors.incrementAndGet(); loadRunners.add(()-> client.get(iniUrl, new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { loadedVendorsCount.incrementAndGet(); try { Slic3rConfigWrapper cfg = new Slic3rConfigWrapper(new ByteArrayInputStream(responseBody)); for (ConfigObject obj : cfg.printerModels) { if (obj.get("thumbnail") != null) { obj.thumbnailUrl = baseUrl + "/" + obj.get("thumbnail"); } } vendorProfiles.add(cfg); } catch (IOException e) { onFailure(statusCode, headers, responseBody, e); return; } if (loadedVendorsCount.get() < totalNeededVendors.get()) { loadRunners.get(loadedVendorsCount.get()).run(); } else { onVendorsLoaded.run(); } } @Override public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { Log.e(TAG, "Failed to load vendor file " + iniUrl, error); isLoading = false; ViewUtils.postOnMainThread(() -> { Toast.makeText(Santoku.INSTANCE, R.string.IntroFailedToLoadRepos, Toast.LENGTH_SHORT).show(); fakeScroll(-1); pager.setUserInputEnabled(true); }); } })); zis.closeEntry(); } zis.close(); if (loadRunners.isEmpty()) { onVendorsLoaded.run(); } else { loadRunners.get(0).run(); } } catch (IOException e) { Log.e(TAG, "Failed to parse vendor indices", e); onFailure(statusCode, headers, responseBody, e); } } @Override public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { isLoading = false; Log.e(TAG, "Failed to load repo", error); ViewUtils.postOnMainThread(() -> { Toast.makeText(Santoku.INSTANCE, R.string.IntroFailedToLoadRepos, Toast.LENGTH_SHORT).show(); fakeScroll(-1); pager.setUserInputEnabled(true); }); } }); } } pager.setUserInputEnabled(false); } } invalidateTitleY(); backgroundView.requestRender(); } }); pager.setAdapter(adapter); pager.getChildAt(0).setOverScrollMode(View.OVER_SCROLL_NEVER); FrameLayout fl = new FrameLayout(this) { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); titleY = h / 4; invalidateTitleY(); } }; fl.setClipChildren(false); fl.setClipToPadding(false); backgroundView = new GLSurfaceView(this) { @Override public void surfaceDestroyed(SurfaceHolder holder) { super.surfaceDestroyed(holder); backgroundModel.release(); backgroundModel = null; shadersManager.clearShaders(); shadersManager = null; } }; backgroundView.setEGLContextClientVersion(3); backgroundView.setRenderer(new GLSurfaceView.Renderer() { @Override public void onSurfaceCreated(GL10 gl, EGLConfig config) {} @Override public void onSurfaceChanged(GL10 gl, int width, int height) { glViewport(0, 0, width, height); if (backgroundModel == null) { backgroundModel = new GLModel(); backgroundModel.initBackgroundTriangles(); shadersManager = new GLShadersManager(); } } private float time; private long lastUpdate; @Override public void onDrawFrame(GL10 gl) { long dt = Math.min(System.currentTimeMillis() - lastUpdate, 16); lastUpdate = System.currentTimeMillis(); time += dt / 1000f; glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClearColor(0, 0, 0, 0); if (backgroundModel != null) { glDisable(GL_DEPTH_TEST); GLShaderProgram shader = shadersManager.get(GLShadersManager.SHADER_BEAM_INTRO); shader.startUsing(); int topColor = ThemesRepo.getColor(android.R.attr.colorAccent); int bottomColor = ThemesRepo.getColor(android.R.attr.windowBackground); if (cloudProfile) { bottomColor = ColorUtils.blendARGB(bottomColor, topColor, 0.5f); } shader.setUniformColor("top_color", topColor); shader.setUniformColor("bottom_color", bottomColor); shader.setUniform("progress", backgroundProgress - (cloudProfile ? 1.4f : 0)); shader.setUniform("time", time); backgroundModel.render(); shader.stopUsing(); glEnable(GL_DEPTH_TEST); } } }); backgroundView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); fl.addView(backgroundView); title = new TextView(this); title.setGravity(Gravity.CENTER); title.setTypeface(Typeface.DEFAULT_BOLD); 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)); fl.addView(title); fl.addView(pager, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); ViewCompat.setOnApplyWindowInsetsListener(fl, (v2, insets) -> { Insets systemBars = insets.getSystemWindowInsets(); pager.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) title.getLayoutParams(); params.leftMargin = systemBars.left; params.topMargin = systemBars.top; params.rightMargin = systemBars.right; params.bottomMargin = systemBars.bottom; return insets.consumeSystemWindowInsets(); }); setContentView(fl); if (!isLoading && !isReposLoaded) { // Pre-load repos silently loadRepos(false); } } 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); title.setTextColor(color); title.setTranslationY(ViewUtils.lerp(titleY, (ViewUtils.dp(52) - title.getHeight() * title.getScaleY()) / 2f, backgroundProgress)); } @Override protected void onDestroy() { super.onDestroy(); Santoku.EVENT_BUS.unregisterListener(this); } @SuppressLint("NotifyDataSetChanged") @EventHandler(runOnMainThread = true) public void onCloudSyncFinished(CloudSyncFinishedEvent e) { if (cloudProfile && Prefs.getCloudAPIToken() != null && cloudImport) { finish(); } if (!about && !cloudProfile) { if (Prefs.getCloudAPIToken() != null) { limitRepoFragmentCount = false; limitProfileFragmentCount = false; pager.getAdapter().notifyDataSetChanged(); pager.setCurrentItem(pager.getAdapter().getItemCount() - 1); } } } @EventHandler(runOnMainThread = true) public void onCloudAuthStateUpdated(CloudLoginStateUpdatedEvent e) { if (cloudProfile) { cloudItem.bindLoginButton(true); } if (!about && !cloudProfile && reposItem != null) { reposItem.onCloudInfoUpdated(); } } private void setItems() { if (cloudProfile){ adapter.setItems(Collections.singletonList(cloudItem = new CloudProfileItem())); } else if (about) { adapter.setItems(Collections.singletonList(new AboutItem())); } else { List items = new ArrayList<>(Arrays.asList( new IntroItem(), reposItem = new ReposItem(), profilesItem = new ProfilesItem())); items.add(new FinishItem()); adapter.setItems(items); } } @Override public void onBackPressed() { if (pager.getCurrentItem() > 0) { pager.setCurrentItem(pager.getCurrentItem() - 1, true); } else { super.onBackPressed(); } } private void loadRepos(boolean fromPage) { isLoading = true; repos.clear(); List finishedIndexes = new ArrayList<>(); Map> reposMap = new HashMap>() { @Nullable @Override public List get(@Nullable Object key) { List list = super.get(key); if (list == null) put((String) key, list = new ArrayList<>()); return list; } }; for (String repo : REPOS_URLS) { client.get(repo, new AsyncHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, byte[] responseBody) { finishedIndexes.add(repo); try { JSONArray arr = new JSONArray(new String(responseBody)); for (int i = 0; i < arr.length(); i++) { JSONObject obj = arr.getJSONObject(i); if (obj.getString("id").endsWith("-fff")) { ProfilesRepo r = new ProfilesRepo(); r.url = obj.getString("url"); r.name = obj.getString("name"); r.description = obj.getString("description"); r.indexUrl = obj.getString("index_url"); reposMap.get(repo).add(r); } } if (finishedIndexes.size() == REPOS_URLS.size()) { // Filter in the right way for (String repo : REPOS_URLS) { repos.addAll(reposMap.get(repo)); } ViewUtils.postOnMainThread(() -> { isLoading = false; if (fromPage) { reposItem.onReposLoaded(); } pager.setUserInputEnabled(true); isReposLoaded = true; }); } } catch (JSONException e) { throw new RuntimeException(e); } } @Override public void onFailure(int statusCode, Header[] headers, byte[] responseBody, Throwable error) { isLoading = false; Log.e(TAG, "Failed to load repos", error); if (fromPage) { ViewUtils.postOnMainThread(() -> { Toast.makeText(Santoku.INSTANCE, R.string.IntroFailedToLoadRepos, Toast.LENGTH_SHORT).show(); fakeScroll(-1); pager.setUserInputEnabled(true); }); } } }); } } @Override protected void onPause() { super.onPause(); backgroundView.onPause(); } @Override protected void onResume() { super.onResume(); backgroundView.onResume(); } private void scrollToNext() { fakeScroll(1); } private void fakeScroll(float to) { if (fakeScroller != null) return; AtomicReference lastValue = new AtomicReference<>(0f); fakeScroller = new SpringAnimation(new FloatValueHolder(0)) .setMinimumVisibleChange(1 / 256f) .setSpring(new SpringForce(1f) .setStiffness(600f) .setDampingRatio(1f)) .addUpdateListener((animation, value, velocity) -> { float delta = value - lastValue.getAndSet(value); pager.fakeDragBy(delta * pager.getWidth() * -to); }) .addEndListener((animation, canceled, value, velocity) -> { pager.endFakeDrag(); fakeScroller = null; }); pager.beginFakeDrag(); fakeScroller.start(); } private final class CloudProfileItem extends SimpleRecyclerItem { private FrameLayout buttonView; private TextView buttonText; private ProgressBar buttonProgress; private TextView titleView; private TextView signUpButton; private boolean signUpInProgress; @Override public View onCreateView(Context ctx) { LinearLayout ll = new LinearLayout(ctx); ll.setOrientation(LinearLayout.VERTICAL); ll.setPadding(0, ViewUtils.dp(42), 0, 0); titleView = new TextView(ctx); titleView.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent)); titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); titleView.setGravity(Gravity.CENTER); titleView.setPadding(ViewUtils.dp(12), 0, ViewUtils.dp(12), 0); ll.addView(titleView); bindHeader(); View spacer = new View(ctx); ll.addView(spacer, 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(BuildConfig.BEAM_BASE_URL_PROD + "/tos")))); 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)); ll.addView(buttonView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{ leftMargin = rightMargin = ViewUtils.dp(16); }}); signUpButton = new TextView(ctx); signUpButton.setText(R.string.SettingsCloudManageButtonSignUp); signUpButton.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent)); signUpButton.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM)); signUpButton.setGravity(Gravity.CENTER); signUpButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); signUpButton.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16)); signUpButton.setOnClickListener(v -> showSignUpDialog(v.getContext())); ll.addView(signUpButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{ leftMargin = rightMargin = ViewUtils.dp(16); topMargin = ViewUtils.dp(8); bottomMargin = ViewUtils.dp(16); }}); bindLoginButton(false); ll.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return ll; } 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); bindHeader(); 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(); } }); if (signUpButton != null) { boolean showSignUp = !loggedIn && !loading; signUpButton.setVisibility(showSignUp ? View.VISIBLE : View.GONE); signUpButton.setEnabled(showSignUp && !signUpInProgress); signUpButton.setAlpha(signUpButton.isEnabled() ? 1f : 0.6f); } } private void bindHeader() { if (titleView == null) return; CloudAPI.UserInfo info = CloudController.getUserInfo(); if (Prefs.getCloudAPIToken() != null && info != null && info.displayName != null) { titleView.setText(titleView.getContext().getString(R.string.SettingsCloudManageLoggedInAs, info.displayName)); } else { titleView.setText(R.string.SettingsCloudManageDescription); } } private void showSignUpDialog(Context ctx) { if (signUpInProgress) { return; } LinearLayout ll = new LinearLayout(ctx); ll.setOrientation(LinearLayout.VERTICAL); ll.setPadding(ViewUtils.dp(16), ViewUtils.dp(8), ViewUtils.dp(16), 0); TextView emailLabel = new TextView(ctx); emailLabel.setText(R.string.SettingsCloudManageSignUpEmail); emailLabel.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); emailLabel.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(emailLabel, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); EditText emailInput = new EditText(ctx); emailInput.setHint(R.string.SettingsCloudManageSignUpEmail); emailInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); emailInput.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); emailInput.setHintTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(emailInput, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); TextView displayNameLabel = new TextView(ctx); displayNameLabel.setText(R.string.SettingsCloudManageSignUpDisplayName); displayNameLabel.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); displayNameLabel.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(displayNameLabel, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ topMargin = ViewUtils.dp(12); }}); EditText displayNameInput = new EditText(ctx); displayNameInput.setHint(R.string.SettingsCloudManageSignUpDisplayName); displayNameInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME); displayNameInput.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); displayNameInput.setHintTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(displayNameInput, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ topMargin = ViewUtils.dp(8); }}); TextView passwordLabel = new TextView(ctx); passwordLabel.setText(R.string.SettingsCloudManageSignUpPassword); passwordLabel.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13); passwordLabel.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(passwordLabel, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ topMargin = ViewUtils.dp(12); }}); EditText passwordInput = new EditText(ctx); passwordInput.setHint(R.string.SettingsCloudManageSignUpPassword); passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); passwordInput.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); passwordInput.setHintTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(passwordInput, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ topMargin = ViewUtils.dp(8); }}); CheckBox showPassword = new CheckBox(ctx); showPassword.setText(R.string.SettingsCloudManageSignUpShowPassword); showPassword.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); showPassword.setOnCheckedChangeListener((buttonView, isChecked) -> { int selection = passwordInput.getSelectionEnd(); if (isChecked) { passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } else { passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } passwordInput.setSelection(Math.max(selection, 0)); }); ll.addView(showPassword, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ topMargin = ViewUtils.dp(4); }}); TextView dialogTitle = new TextView(ctx); dialogTitle.setText(R.string.SettingsCloudManageButtonSignUp); dialogTitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); dialogTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 28); dialogTitle.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM)); dialogTitle.setGravity(Gravity.CENTER); dialogTitle.setPadding(0, ViewUtils.dp(4), 0, ViewUtils.dp(8)); ll.addView(dialogTitle, 0, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); new BeamAlertDialogBuilder(ctx) .setView(ll) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.SettingsCloudManageButtonSignUp, (dialog, which) -> { String email = emailInput.getText().toString().trim(); String displayName = displayNameInput.getText().toString().trim(); String password = passwordInput.getText().toString(); if (TextUtils.isEmpty(email) || TextUtils.isEmpty(displayName) || TextUtils.isEmpty(password)) { Toast.makeText(ctx, R.string.SettingsCloudManageSignUpMissingFields, Toast.LENGTH_SHORT).show(); return; } signUpInProgress = true; bindLoginButton(true); CloudAPI.INSTANCE.signup(email, password, displayName, new APICallback() { @Override public void onResponse(CloudAPI.AuthToken response) { Prefs.setCloudAPIToken(response.bearer); signUpInProgress = false; CloudController.init(); Santoku.EVENT_BUS.fireEvent(new CloudLoginStateUpdatedEvent()); } @Override public void onException(Exception e) { signUpInProgress = false; ViewUtils.postOnMainThread(() -> { new BeamAlertDialogBuilder(ctx) .setTitle(R.string.SettingsCloudManageSignUpFailed) .setMessage(e.toString()) .setPositiveButton(android.R.string.ok, null) .show(); bindLoginButton(true); }); } }); }) .show(); } } private final class AboutItem extends SimpleRecyclerItem { @Override public View onCreateView(Context ctx) { LinearLayout ll = new LinearLayout(ctx); ll.setOrientation(LinearLayout.VERTICAL); ll.setGravity(Gravity.BOTTOM); String versionStr = null; PackageManager pm = ctx.getPackageManager(); try { PackageInfo info = pm.getPackageInfo(ctx.getPackageName(), 0); versionStr = info.versionName; } catch (PackageManager.NameNotFoundException ignored) {} TextView subtitle = new TextView(ctx); subtitle.setText(ctx.getString(R.string.SettingsAboutVersion, versionStr)); subtitle.setGravity(Gravity.CENTER); subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(subtitle, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ bottomMargin = ViewUtils.dp(12); }}); TextView buttonView = new TextView(ctx); buttonView.setText(android.R.string.ok); 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(android.R.attr.colorAccent), 16)); buttonView.setOnClickListener(v-> finish()); 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 final class IntroItem extends SimpleRecyclerItem { private TextView buttonView; @Override public View onCreateView(Context ctx) { LinearLayout ll = new LinearLayout(ctx); ll.setOrientation(LinearLayout.VERTICAL); ll.setGravity(Gravity.BOTTOM); TextView favoriteHint = new TextView(ctx); favoriteHint.setText(R.string.IntroLetStartWithColor); favoriteHint.setGravity(Gravity.CENTER); favoriteHint.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); favoriteHint.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); ll.addView(favoriteHint, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ bottomMargin = ViewUtils.dp(12); }}); LinearLayout colors = new LinearLayout(ctx); colors.setOrientation(LinearLayout.HORIZONTAL); colors.setGravity(Gravity.CENTER); for (AccentColors color : AccentColors.values()) { MiniColorView view = new MiniColorView(ctx); view.setColor(color.color); int margin = colors.getChildCount() > 0 ? 10 : 0; view.setOnClickListener(v -> { int from = ThemesRepo.getColor(android.R.attr.colorAccent); if (from == color.color) return; Prefs.setAccentColor(color.color); int wasIndex = -1; for (int i = 0; i < colors.getChildCount(); i++) { AccentColors _c = AccentColors.values()[i]; if (_c.color == from) { wasIndex = i; break; } } int finalWasIndex = wasIndex; new SpringAnimation(new FloatValueHolder(0)) .setMinimumVisibleChange(1 / 256f) .setSpring(new SpringForce(1f) .setStiffness(1000f) .setDampingRatio(1f)) .addUpdateListener((animation, value, velocity) -> { BeamTheme.LIGHT.colors.put(android.R.attr.colorAccent, ColorUtils.blendARGB(from, color.color, value)); BeamTheme.DARK.colors.put(android.R.attr.colorAccent, ColorUtils.blendARGB(from, color.color, value)); buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(android.R.attr.colorAccent), 16)); backgroundView.requestRender(); if (finalWasIndex != -1) { ((MiniColorView) colors.getChildAt(finalWasIndex)).setSelectionProgress(1f - value); } ((MiniColorView) v).setSelectionProgress(value); }) .addEndListener((animation, canceled, value, velocity) -> adapter.notifyItemChanged(1)) .start(); }); colors.addView(view, new LinearLayout.LayoutParams(ViewUtils.dp(28), ViewUtils.dp(28)) {{ leftMargin = ViewUtils.dp(margin); }}); } ll.addView(colors, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ bottomMargin = ViewUtils.dp(16); }}); buttonView = new TextView(ctx); buttonView.setText(R.string.IntroStart); 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(android.R.attr.colorAccent), 16)); buttonView.setOnClickListener(v-> scrollToNext()); 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 final class ReposItem extends SimpleRecyclerItem { private ProgressBar progressBar; private FrameLayout loadedLayout; private SimpleRecyclerAdapter adapter; private TextView cloudImportView; private TextView cloudOrView; private TextView customProfileView; private TextView buttonView; @Override public View onCreateView(Context ctx) { FrameLayout fl = new FrameLayout(ctx); progressBar = new ProgressBar(ctx); fl.addView(progressBar, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); loadedLayout = new FrameLayout(ctx); RecyclerView recyclerView = new FadeRecyclerView(ctx); recyclerView.setAdapter(adapter = new SimpleRecyclerAdapter()); loadedLayout.addView(recyclerView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); LinearLayout ll = new LinearLayout(ctx); ll.setOrientation(LinearLayout.VERTICAL); cloudImportView = new TextView(ctx); cloudImportView.setVisibility(View.GONE); cloudImportView.setText(R.string.IntroImportFromCloud); cloudImportView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); cloudImportView.setGravity(Gravity.CENTER); cloudImportView.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8)); cloudImportView.setOnClickListener(v -> startActivity(new Intent(v.getContext(), SetupActivity.class).putExtra(SetupActivity.EXTRA_CLOUD_PROFILE, true).putExtra(SetupActivity.EXTRA_CLOUD_IMPORT_FROM_SETUP, true))); ll.addView(cloudImportView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{ leftMargin = rightMargin = ViewUtils.dp(16); }}); cloudOrView = new TextView(ctx); cloudOrView.setVisibility(View.GONE); cloudOrView.setText(R.string.IntroImportOr); cloudOrView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); cloudOrView.setGravity(Gravity.CENTER); ll.addView(cloudOrView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ leftMargin = rightMargin = ViewUtils.dp(16); }}); customProfileView = new TextView(ctx); customProfileView.setText(R.string.IntroCustomProfile); customProfileView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); customProfileView.setGravity(Gravity.CENTER); customProfileView.setPadding(ViewUtils.dp(12), ViewUtils.dp(8), ViewUtils.dp(12), ViewUtils.dp(8)); customProfileView.setOnClickListener(v -> { profilesItem.useCustomProfile = true; limitRepoFragmentCount = false; SetupActivity.this.adapter.notifyItemRangeInserted(REPOS_INDEX + 1, SetupActivity.this.adapter.getItemCount() - REPOS_INDEX - 1); scrollToNext(); }); ll.addView(customProfileView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(48)) {{ leftMargin = rightMargin = ViewUtils.dp(16); bottomMargin = ViewUtils.dp(6); }}); buttonView = new TextView(ctx); buttonView.setText(R.string.IntroNext); 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.setOnClickListener(v-> { boolean noChecked = true; for (ProfilesRepo repo : repos) { if (repo.checked) { noChecked = false; break; } } if (noChecked) { new BeamAlertDialogBuilder(SetupActivity.this) .setTitle(R.string.IntroNoRepos) .setMessage(R.string.IntroNoReposDescription) .setPositiveButton(android.R.string.ok, null) .show(); } else { profilesItem.useCustomProfile = false; limitRepoFragmentCount = false; SetupActivity.this.adapter.notifyItemRangeInserted(REPOS_INDEX + 1, SetupActivity.this.adapter.getItemCount() - REPOS_INDEX - 1); scrollToNext(); } }); ll.addView(buttonView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52)) {{ leftMargin = rightMargin = ViewUtils.dp(16); bottomMargin = ViewUtils.dp(16); }}); loadedLayout.addView(ll, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM)); loadedLayout.setAlpha(0f); fl.addView(loadedLayout, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); fl.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return fl; } @SuppressLint("NotifyDataSetChanged") @Override public void onBindView(View view) { progressBar.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorAccent))); cloudImportView.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent)); cloudImportView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16)); cloudImportView.setVisibility(Prefs.getCloudAPIToken() != null ? View.VISIBLE : View.GONE); cloudOrView.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); cloudOrView.setVisibility(Prefs.getCloudAPIToken() != null ? View.VISIBLE : View.GONE); customProfileView.setTextColor(ThemesRepo.getColor(android.R.attr.colorAccent)); customProfileView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 16)); buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(android.R.attr.colorAccent), 16)); if (adapter.getItemCount() == 0 && isReposLoaded) { List items = new ArrayList<>(repos); items.add(new TextHintRecyclerItem(Santoku.INSTANCE.getString(R.string.IntroSelectRepos))); adapter.setItems(items); } else { adapter.notifyDataSetChanged(); } if (isReposLoaded) { progressBar.setVisibility(View.GONE); loadedLayout.setAlpha(1f); } else { progressBar.setVisibility(View.VISIBLE); progressBar.setAlpha(1f); progressBar.setScaleX(1f); progressBar.setScaleY(1f); loadedLayout.setAlpha(0f); } } public void onCloudInfoUpdated() { if (cloudImportView != null) { cloudImportView.setVisibility(Prefs.getCloudAPIToken() != null ? View.VISIBLE : View.GONE); cloudOrView.setVisibility(Prefs.getCloudAPIToken() != null ? View.VISIBLE : View.GONE); } } public void onReposLoaded() { List items = new ArrayList<>(repos); items.add(new TextHintRecyclerItem(Santoku.INSTANCE.getString(R.string.IntroSelectRepos))); adapter.setItems(items); new SpringAnimation(new FloatValueHolder(0)) .setMinimumVisibleChange(1 / 256f) .setSpring(new SpringForce(1f) .setStiffness(1000f) .setDampingRatio(1f)) .addUpdateListener((animation, value, velocity) -> { progressBar.setAlpha(1f - value); progressBar.setScaleX(1f - value * 0.5f); progressBar.setScaleY(1f - value * 0.5f); loadedLayout.setAlpha(value); loadedLayout.setScaleX(0.5f + value * 0.5f); loadedLayout.setScaleY(0.5f + value * 0.5f); }) .addEndListener((animation, canceled, value, velocity) -> progressBar.setVisibility(View.GONE)) .start(); } } private final class ProfilesItem extends SimpleRecyclerItem { private ProgressBar progressBar; private FrameLayout loadedLayout; private FadeRecyclerView recyclerView; private SimpleRecyclerAdapter adapter; private TextView buttonView; private boolean useCustomProfile; @Override public View onCreateView(Context ctx) { FrameLayout fl = new FrameLayout(ctx); progressBar = new ProgressBar(ctx); fl.addView(progressBar, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); loadedLayout = new FrameLayout(ctx); recyclerView = new FadeRecyclerView(ctx); recyclerView.setAdapter(adapter = new SimpleRecyclerAdapter()); loadedLayout.addView(recyclerView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ topMargin = ViewUtils.dp(52); bottomMargin = ViewUtils.dp(72); }}); buttonView = new TextView(ctx); buttonView.setText(R.string.IntroNext); 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.setOnClickListener(v-> { boolean noChecked = enabledPrinters.isEmpty(); if (noChecked && !useCustomProfile) { new BeamAlertDialogBuilder(SetupActivity.this) .setTitle(R.string.IntroNoProfiles) .setMessage(R.string.IntroNoProfilesDescription) .setPositiveButton(android.R.string.ok, null) .show(); } else { limitProfileFragmentCount = false; SetupActivity.this.adapter.notifyItemRangeInserted(PROFILES_INDEX + 1, SetupActivity.this.adapter.getItemCount() - PROFILES_INDEX - 1); scrollToNext(); } }); loadedLayout.addView(buttonView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52), Gravity.BOTTOM) {{ leftMargin = rightMargin = ViewUtils.dp(16); bottomMargin = ViewUtils.dp(16); }}); loadedLayout.setAlpha(0f); fl.addView(loadedLayout, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); fl.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return fl; } @SuppressLint("NotifyDataSetChanged") @Override public void onBindView(View view) { progressBar.setIndeterminateTintList(ColorStateList.valueOf(ThemesRepo.getColor(android.R.attr.colorAccent))); buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(android.R.attr.colorAccent), 16)); if (adapter.getItemCount() == 0 && isReposLoaded) { adapter.setItems(getItems()); } else if (useCustomProfile) { adapter.setItems(getItems()); } else { adapter.notifyDataSetChanged(); } if (useCustomProfile) { progressBar.setAlpha(0f); loadedLayout.setAlpha(1f); } else { if (isProfilesLoaded) { progressBar.setVisibility(View.GONE); loadedLayout.setAlpha(1f); } else { progressBar.setVisibility(View.VISIBLE); progressBar.setAlpha(1f); progressBar.setScaleX(1f); progressBar.setScaleY(1f); loadedLayout.setAlpha(0f); } } } private List getItems() { if (useCustomProfile) { List items = new ArrayList<>(); items.add(new BigHeaderItem(getString(R.string.IntroCustomProfileHeader))); items.add(new ProfileItem()); return items; } List vendors = new ArrayList<>(); for (List w : profilesMap.values()) { vendors.addAll(w); } Collections.sort(vendors, (o1, o2) -> o1.vendor.values.get("name").compareToIgnoreCase(o2.vendor.values.get("name"))); List items = new ArrayList<>(); for (Slic3rConfigWrapper w : vendors) { items.add(new BigHeaderItem(w.vendor.values.get("name"))); for (ConfigObject printer : w.printerModels) { if (printer.getTitle().startsWith("*") && printer.getTitle().endsWith("*")) continue; items.add(new ProfileItem(printer, TYPE_PRINTER)); } } return items; } public void onProfilesLoaded() { adapter.setItems(getItems()); new SpringAnimation(new FloatValueHolder(0)) .setMinimumVisibleChange(1 / 256f) .setSpring(new SpringForce(1f) .setStiffness(1000f) .setDampingRatio(1f)) .addUpdateListener((animation, value, velocity) -> { progressBar.setAlpha(1f - value); progressBar.setScaleX(1f - value * 0.5f); progressBar.setScaleY(1f - value * 0.5f); loadedLayout.setAlpha(value); loadedLayout.setScaleX(0.5f + value * 0.5f); loadedLayout.setScaleY(0.5f + value * 0.5f); }) .addEndListener((animation, canceled, value, velocity) -> progressBar.setVisibility(View.GONE)) .start(); } } private final class FinishItem extends SimpleRecyclerItem { private TextView buttonView; @Override public View onCreateView(Context ctx) { FrameLayout fl = new FrameLayout(ctx); TextView title = new TextView(ctx); title.setText(R.string.IntroConfigured); title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); title.setGravity(Gravity.CENTER); fl.addView(title, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); buttonView = new TextView(ctx); buttonView.setText(R.string.IntroFinish); buttonView.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM)); buttonView.setGravity(Gravity.CENTER); buttonView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); buttonView.setOnClickListener(v-> { Slic3rConfigWrapper cfg = new Slic3rConfigWrapper(); if (profilesItem.useCustomProfile) { ConfigObject custom = ConfigObject.createCustomPrinterProfile(); custom.profileListType = ConfigObject.PROFILE_LIST_PRINTER; cfg.printerConfigs.add(custom); ConfigObject genericFilament = ConfigObject.createCustomFilamentProfile(); cfg.filamentConfigs.add(genericFilament); ConfigObject genericPrint = new ConfigObject(getString(R.string.IntroCustomProfileName)); genericPrint.profileListType = ConfigObject.PROFILE_LIST_PRINT; cfg.printConfigs.add(genericPrint); cfg.presets = new ConfigObject(); cfg.presets.put("printer", custom.getTitle()); cfg.presets.put("print", genericPrint.getTitle()); cfg.presets.put("filament", genericFilament.getTitle()); } else { for (ConfigObject printerModel : enabledPrinters) { String model = printerModel.getTitle(); String[] variants = !TextUtils.isEmpty(printerModel.get("variants")) ? printerModel.get("variants").split(";") : new String[]{}; String[] materials = !TextUtils.isEmpty(printerModel.get("default_materials")) ? printerModel.get("default_materials").split(";") : new String[]{}; for (String variant : variants) { variant = variant.trim(); for (List wrappers : profilesMap.values()) { for (Slic3rConfigWrapper w : wrappers) { ConfigObject obj = w.findPrinterVariant(model, variant); if (obj != null) { cfg.printerConfigs.add(obj); Slic3rUtils.ConfigChecker checker = new Slic3rUtils.ConfigChecker(obj.serialize()); for (ConfigObject printConfig : w.printConfigs) { if (!(printConfig.getTitle().startsWith("*") && printConfig.getTitle().endsWith("*")) && checker.checkCompatibility(printConfig.get("compatible_printers_condition"))) { cfg.printConfigs.add(printConfig); } } checker.release(); } } } } for (String mat : materials) { mat = mat.trim(); for (List wrappers : profilesMap.values()) { for (Slic3rConfigWrapper w : wrappers) { ConfigObject obj = w.findFilament(mat); if (obj != null) cfg.filamentConfigs.add(obj); } } } } cfg.presets = new ConfigObject(); if (!cfg.printerConfigs.isEmpty()) { boolean foundDefault = false; for (ConfigObject obj : cfg.printerConfigs) { if (obj.getTitle().contains("0.4")) { foundDefault = true; cfg.presets.put("printer", obj.getTitle()); break; } } if (!foundDefault && !cfg.printerConfigs.isEmpty()) { cfg.presets.put("printer", cfg.printerConfigs.get(0).getTitle()); } } ConfigObject defPrinter = cfg.printerConfigs.isEmpty() ? null : cfg.findPrinter(cfg.presets.get("printer")); if (defPrinter != null) { Slic3rUtils.ConfigChecker checker = new Slic3rUtils.ConfigChecker(defPrinter.serialize()); if (defPrinter.get("default_print_profile") != null && cfg.findPrint(defPrinter.get("default_print_profile")) != null) { cfg.presets.put("print", defPrinter.get("default_print_profile")); } else { if (!cfg.printConfigs.isEmpty()) { boolean foundDefault = false; for (ConfigObject obj : cfg.printConfigs) { if (obj.get("layer_height") != null && checker.checkCompatibility(obj.get("compatible_printers_condition")) && Float.parseFloat(obj.get("layer_height")) == 0.2f) { foundDefault = true; cfg.presets.put("print", obj.getTitle()); break; } } if (!foundDefault && !cfg.printConfigs.isEmpty()) { cfg.presets.put("print", cfg.printConfigs.get(0).getTitle()); } } } if (defPrinter.get("default_filament_profile") != null && cfg.findFilament(defPrinter.get("default_filament_profile")) != null) { cfg.presets.put("filament", defPrinter.get("default_filament_profile")); } else { if (!cfg.filamentConfigs.isEmpty()) { boolean foundDefault = false; for (ConfigObject obj : cfg.filamentConfigs) { if (obj.getTitle().contains("Generic PLA") && checker.checkCompatibility(obj.get("compatible_printers_condition"))) { // TODO: Slic3rUtils.checkCompatibility(obj.get("compatible_prints_condition"), serialized) foundDefault = true; cfg.presets.put("filament", obj.getTitle()); break; } } if (!foundDefault && !cfg.filamentConfigs.isEmpty()) { cfg.presets.put("filament", cfg.filamentConfigs.get(0).getTitle()); } } } checker.release(); } } try { if (Prefs.getCloudAPIToken() == null || Santoku.CONFIG == null) { Santoku.getCurrentConfigFile().delete(); Santoku.CONFIG = cfg; FileOutputStream fos = new FileOutputStream(Santoku.getConfigFile()); fos.write(cfg.serialize().getBytes(StandardCharsets.UTF_8)); fos.close(); } startActivity(new Intent(SetupActivity.this, MainActivity.class)); finish(); } catch (Exception e) { Log.e(TAG, "Failed to save config", e); } }); fl.addView(buttonView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtils.dp(52), Gravity.BOTTOM) {{ leftMargin = rightMargin = ViewUtils.dp(16); bottomMargin = ViewUtils.dp(16); }}); fl.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); return fl; } @Override public void onBindView(View view) { buttonView.setTextColor(ThemesRepo.getColor(R.attr.textColorOnAccent)); buttonView.setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ThemesRepo.getColor(android.R.attr.colorAccent), 16)); } } public enum AccentColors { DEFAULT(0xff38ef7d), BLUE(0xff5492f5), LIGHT_BLUE(0xff6dd5fa), RED(0xffe94056), ORANGE(0xffff4b2c), YELLOW(0xfffdc831), PINK(0xfff2709b), PURPLE(0xff6e74e1); public final int color; AccentColors(int color) { this.color = color; } } private static String parseVendorVersion(InputStream in) throws IOException { BufferedReader r = new BufferedReader(new InputStreamReader(in)); String line; while ((line = r.readLine()) != null) { if (line.contains(" = ")) continue; return line.substring(0, line.indexOf(' ')); } return null; } private final static class ProfilesRepo extends SimpleRecyclerItem { private String url; private String name; private String description; private String indexUrl; private boolean checked; @Override public RepoHolderView onCreateView(Context ctx) { return new RepoHolderView(ctx); } @Override public void onBindView(RepoHolderView view) { view.bind(this); } public final static class RepoHolderView extends LinearLayout implements IThemeView { private TextView title; private TextView subtitle; private BeamSwitch mSwitch; public RepoHolderView(@NonNull Context context) { super(context); setOrientation(HORIZONTAL); setGravity(Gravity.CENTER_VERTICAL); LinearLayout inner = new LinearLayout(context); inner.setOrientation(VERTICAL); title = new TextView(context); title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM)); inner.addView(title); subtitle = new TextView(context); subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); inner.addView(subtitle); addView(inner, new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); mSwitch = new BeamSwitch(context); addView(mSwitch); setPadding(ViewUtils.dp(21), ViewUtils.dp(16), ViewUtils.dp(21), ViewUtils.dp(16)); 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(ProfilesRepo item) { title.setText(item.name); subtitle.setText(item.description); mSwitch.setChecked(item.checked); setOnClickListener(v -> { item.checked = !item.checked; mSwitch.setChecked(item.checked); }); mSwitch.onApplyTheme(); onApplyTheme(); } @Override public void onApplyTheme() { title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); subtitle.setTextColor(ThemesRepo.getColor(android.R.attr.textColorSecondary)); setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10), 32)); } } } private final class ProfileItem extends SimpleRecyclerItem { private ConfigObject object; private int type = TYPE_PRINTER; private ProfileItem(ConfigObject obj, int type) { this.object = obj; this.type = type; } private ProfileItem() {} @Override public ProfileHolderView onCreateView(Context ctx) { return new ProfileHolderView(ctx); } @Override public void onBindView(ProfileHolderView view) { view.bind(this); } public final class ProfileHolderView extends LinearLayout implements IThemeView { private ImageView icon; private TextView title; private BeamSwitch mSwitch; public ProfileHolderView(@NonNull Context context) { super(context); setOrientation(HORIZONTAL); setGravity(Gravity.CENTER_VERTICAL); icon = new ImageView(context); addView(icon, new LinearLayout.LayoutParams(ViewUtils.dp(36), ViewUtils.dp(36)) {{ rightMargin = ViewUtils.dp(16); }}); LinearLayout inner = new LinearLayout(context); inner.setOrientation(VERTICAL); title = new TextView(context); title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); title.setTypeface(ViewUtils.getTypeface(ViewUtils.ROBOTO_MEDIUM)); inner.addView(title); addView(inner, new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); mSwitch = new BeamSwitch(context); addView(mSwitch, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ leftMargin = ViewUtils.dp(12); }}); setPadding(ViewUtils.dp(16), ViewUtils.dp(12), ViewUtils.dp(21), ViewUtils.dp(12)); setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {{ leftMargin = rightMargin = ViewUtils.dp(12); bottomMargin = ViewUtils.dp(8); }}); onApplyTheme(); } private List getList() { return enabledPrinters; } public void bind(ProfileItem item) { LayoutParams params = (LayoutParams) icon.getLayoutParams(); if (item.object == null || item.object.thumbnailUrl == null) { params.width = params.height = ViewUtils.dp(36); icon.setColorFilter(ThemesRepo.getColor(android.R.attr.colorAccent)); switch (type) { case TYPE_PRINTER: icon.setImageResource(R.drawable.printer_outline_28); break; case TYPE_PRINT_CONFIG: icon.setImageResource(R.drawable.wrench_outline_28); break; case TYPE_FILAMENT: icon.setImageResource(R.drawable.slot_filament_28); break; } } else { params.width = params.height = ViewUtils.dp(52); icon.setColorFilter(null); Glide.with(icon) .load(item.object.thumbnailUrl) .transform(new RoundedCorners(ViewUtils.dp(12))) .transition(DrawableTransitionOptions.withCrossFade()) .into(icon); } icon.requestLayout(); mSwitch.onApplyTheme(); if (item.object == null) { title.setText(R.string.IntroCustomProfileName); mSwitch.setChecked(true); setOnClickListener(null); setClickable(false); return; } title.setText(item.object.get("name") != null ? item.object.get("name") : item.object.getTitle()); boolean checked = getList().contains(item.object); mSwitch.setChecked(checked); setOnClickListener(v -> { boolean _checked = getList().contains(item.object); _checked = !_checked; mSwitch.setChecked(_checked); if (_checked) { getList().add(item.object); } else { getList().remove(item.object); } }); onApplyTheme(); } @Override public void onApplyTheme() { title.setTextColor(ThemesRepo.getColor(android.R.attr.textColorPrimary)); setBackground(ViewUtils.createRipple(ThemesRepo.getColor(android.R.attr.colorControlHighlight), ColorUtils.setAlphaComponent(ThemesRepo.getColor(android.R.attr.colorControlHighlight), 0x10), 32)); } } } }