diff --git a/controller/app/build.gradle b/controller/app/build.gradle index c3708baf..f9c57466 100644 --- a/controller/app/build.gradle +++ b/controller/app/build.gradle @@ -167,6 +167,7 @@ dependencies { // 10. Core dependencies implementation 'androidx.appcompat:appcompat:1.7.1' implementation 'com.google.android.material:material:1.13.0' + implementation 'com.airbnb.android:lottie:6.1.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.webkit:webkit:1.12.0' diff --git a/controller/app/src/main/AndroidManifest.xml b/controller/app/src/main/AndroidManifest.xml index 7917b9ae..63efa59f 100644 --- a/controller/app/src/main/AndroidManifest.xml +++ b/controller/app/src/main/AndroidManifest.xml @@ -20,7 +20,6 @@ - @@ -80,6 +79,25 @@ tools:replace="screenOrientation" /> + + + + + + + diff --git a/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java b/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java index cd883236..c481f807 100644 --- a/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java +++ b/controller/app/src/main/java/org/iiab/controller/AdbPairingReceiver.java @@ -19,6 +19,7 @@ public class AdbPairingReceiver extends BroadcastReceiver { public static final String KEY_PIN_REPLY = "key_pin_reply"; public static final int NOTIFICATION_ID = 9401; + public static final int RETURN_NOTIFICATION_ID = 9402; private static final String TAG = "AdbPairingNative"; @@ -73,10 +74,25 @@ private void performNativePairing(Context context, String hostIp, int pairingPor .putBoolean("pairing_just_succeeded", true) .apply(); - // CRITICAL: Bring our app back to the screen automatically! - Intent bringToFront = new Intent(context, MainActivity.class); - bringToFront.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); - context.startActivity(bringToFront); + // ADFA-4725 (overlay decision, Option B): no SAW / no background + // startActivity. Post an OS-sanctioned "tap to return" notification that + // opens the new UI (LibraryActivity). Reliable on restrictive phones. + Intent open = new Intent(context, org.iiab.controller.redesign.LibraryActivity.class); + open.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + int piFlags = android.app.PendingIntent.FLAG_UPDATE_CURRENT + | (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M + ? android.app.PendingIntent.FLAG_IMMUTABLE : 0); + android.app.PendingIntent pi = android.app.PendingIntent.getActivity(context, 0, open, piFlags); + NotificationManager nmReturn = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (nmReturn != null) { + NotificationCompat.Builder rb = new NotificationCompat.Builder(context, "adb_pairing_channel") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle("Paired") + .setContentText("Tap to return to K2Go") + .setAutoCancel(true) + .setContentIntent(pi); + nmReturn.notify(RETURN_NOTIFICATION_ID, rb.build()); + } } else { Log.e(TAG, "Native pairing failed."); diff --git a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java index 41979c53..867b696a 100644 --- a/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java +++ b/controller/app/src/main/java/org/iiab/controller/InstallationPlanner.java @@ -80,6 +80,54 @@ public static void wipeCache(Context context) { if (cacheFile.exists()) cacheFile.delete(); } + /** Canonical Wikipedia variants, ordered coverage-desc then detail-desc. */ + public static final String[] CANONICAL_VARIANTS = { + "all_maxi", "all_nopic", "all_mini", + "top1m_maxi", "top1m_nopic", "top1m_mini", + "top_maxi", "top_nopic", "top_mini" + }; + + /** Variants actually present for a language, in canonical order (for the UI to render). */ + public static java.util.List availableVariants(JSONObject langData) { + java.util.List out = new java.util.ArrayList<>(); + if (langData == null) return out; + for (String key : CANONICAL_VARIANTS) if (langData.has(key)) out.add(key); + return out; + } + + private static String coverageOf(String variant) { + if (variant.startsWith("all_")) return "all"; + if (variant.startsWith("top1m_")) return "top1m"; + return "top"; + } + private static String detailOf(String variant) { + if (variant.endsWith("_maxi")) return "maxi"; + if (variant.endsWith("_mini")) return "mini"; + return "nopic"; + } + + /** + * Resolve an explicit variant against what the language actually offers, degrading only + * DOWNWARD within the coverage family (top1m -> top) and across detail, never escalating. + * Returns a present key or null. + */ + private static String resolveInFamily(JSONObject langData, String requested) { + if (langData == null) return null; + if (langData.has(requested)) return requested; + String cov = coverageOf(requested); + String det = detailOf(requested); + String[] covLadder = cov.equals("all") ? new String[]{"all"} + : cov.equals("top1m") ? new String[]{"top1m", "top"} + : new String[]{"top"}; + String[] detLadder = det.equals("maxi") ? new String[]{"maxi", "nopic", "mini"} + : det.equals("nopic") ? new String[]{"nopic", "mini", "maxi"} + : new String[]{"mini", "nopic", "maxi"}; + for (String cc : covLadder) + for (String dd : detLadder) + if (langData.has(cc + "_" + dd)) return cc + "_" + dd; + return null; + } + public static void getOrFetchCatalog(Context context, CacheListener listener) { new Thread(() -> { File cacheFile = new File(context.getCacheDir(), CACHE_FILE_NAME); @@ -134,12 +182,19 @@ public static void getOrFetchCatalog(Context context, CacheListener listener) { if (!database.has(lang)) database.put(lang, new JSONObject()); - // NEW: Store size AND exact filename - JSONObject variantData = new JSONObject(); - variantData.put("size", sizeGb); - variantData.put("file", "wikipedia_" + lang + "_" + variant + "_" + date + ".zim"); - - database.getJSONObject(lang).put(variant, variantData); + // Store size, exact filename AND date. The listing can hold more than + // one date for the same variant; keep ONLY the newest by declared + // intent ("YYYY-MM" compares lexicographically = chronologically), + // not by whichever row happens to come last in the HTML. + JSONObject langObj = database.getJSONObject(lang); + JSONObject existing = langObj.optJSONObject(variant); + if (existing == null || date.compareTo(existing.optString("date", "")) > 0) { + JSONObject variantData = new JSONObject(); + variantData.put("size", sizeGb); + variantData.put("date", date); + variantData.put("file", "wikipedia_" + lang + "_" + variant + "_" + date + ".zim"); + langObj.put(variant, variantData); + } } } @@ -223,10 +278,10 @@ private static void computeProjection(Context context, Tier tier, boolean pullCo @Override public void onReady(JSONObject catalog) { double kiwixSize = 0.0; - String resolvedLang = langCode; + String resolvedLang = org.iiab.controller.applang.data.ContentLanguage.normalize(langCode); String resolvedFile = null; - JSONObject langData = catalog.optJSONObject(langCode); + JSONObject langData = catalog.optJSONObject(resolvedLang); if (langData == null) { langData = catalog.optJSONObject("en"); @@ -234,19 +289,29 @@ public void onReady(JSONObject catalog) { } if (langData != null) { - if (overrideVariant != null && langData.has(overrideVariant)) { - JSONObject vData = langData.optJSONObject(overrideVariant); - // VDATA NULL PROTECTION - if (vData != null) { - kiwixSize = vData.optDouble("size", 0.0); - resolvedFile = vData.optString("file", null); + if (overrideVariant != null && overrideVariant.isEmpty()) { + // Explicit "no Wikipedia" (skip): leave kiwixSize 0 / resolvedFile + // null. Skipping must mean ZERO — never a tier-coupled auto-pick. + } else if (overrideVariant != null) { + // Explicit pick from the UI. Honor it exactly, or degrade WITHIN + // the same coverage family (e.g. top1m_maxi -> top_maxi), NEVER + // escalating — a "Popular" pick must not silently become the full + // "Everything" ZIM. If nothing in-family exists, install no + // Wikipedia rather than substituting a different corpus. + String resolved = resolveInFamily(langData, overrideVariant); + if (resolved != null) { + JSONObject vData = langData.optJSONObject(resolved); + if (vData != null) { + kiwixSize = vData.optDouble("size", 0.0); + resolvedFile = vData.optString("file", null); + } } } else { + // Legacy callers with no explicit pick: priority auto-select. String[] priorities = {"all_maxi", "top1m_maxi", "all_nopic", "top1m_nopic", "top_maxi", "all_mini", "top_nopic"}; for (String p : priorities) { if (langData.has(p)) { JSONObject vData = langData.optJSONObject(p); - // VDATA NULL PROTECTION if (vData != null) { double size = vData.optDouble("size", 0.0); if (size <= finalLimit) { @@ -280,6 +345,12 @@ public void onError(String error) { *

Performs a (cached) network call; must run off the main thread — it does, * because every caller is inside {@link #calculateProjectedSize}'s worker thread. */ + /** No-network last-known OS size (RootfsCatalog fallback) — instant, for placeholders. */ + public static double fallbackOsSizeGb(Tier tier) { + RootfsCatalog catalog = new RootfsCatalog(); + return ByteFormatter.toGiB(catalog.fallbackBytes(toDomainTier(tier), catalog.detectAbi())); + } + private static double resolveOsSizeGb(Context context, Tier tier) { RootfsCatalog catalog = new RootfsCatalog(context); RootfsRepository repository = diff --git a/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/controller/app/src/main/java/org/iiab/controller/MainActivity.java index 6db46524..c57cb6c8 100644 --- a/controller/app/src/main/java/org/iiab/controller/MainActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/MainActivity.java @@ -111,6 +111,11 @@ public boolean isTermuxInstalled() { /** Notification-tap flag: open the full terminal directly (ADFA-4696). */ public static final String EXTRA_OPEN_TERMINAL = "org.iiab.controller.OPEN_TERMINAL"; + /** Launched only to show the terminal (from the new UI): finish on close instead of + * revealing the old dashboard. */ + public static final String EXTRA_TERMINAL_ONLY = "org.iiab.controller.TERMINAL_ONLY"; + private boolean terminalOnlyMode = false; + private TerminalController terminalController; public void invalidateModuleStateTrust() { @@ -581,10 +586,35 @@ protected void onNewIntent(Intent intent) { private void maybeOpenTerminalFromIntent(Intent intent) { if (intent == null || terminalController == null) return; if (!intent.getBooleanExtra(EXTRA_OPEN_TERMINAL, false)) return; + terminalOnlyMode = intent.getBooleanExtra(EXTRA_TERMINAL_ONLY, false); intent.removeExtra(EXTRA_OPEN_TERMINAL); // consume so it fires once + intent.removeExtra(EXTRA_TERMINAL_ONLY); View root = findViewById(android.R.id.content); - if (root != null) root.post(() -> terminalController.openFullTerminal()); - else terminalController.openFullTerminal(); + Runnable open = () -> { + terminalController.openFullTerminal(); + if (terminalOnlyMode) attachTerminalOnlyFinish(); + }; + if (root != null) root.post(open); + else open.run(); + } + + /** In terminal-only mode, hiding the terminal sheet returns to the caller (the new UI) + * rather than exposing the old dashboard behind it. */ + private void attachTerminalOnlyFinish() { + View sheet = findViewById(R.id.terminal_bottom_sheet); + if (sheet == null) return; + com.google.android.material.bottomsheet.BottomSheetBehavior b = + com.google.android.material.bottomsheet.BottomSheetBehavior.from(sheet); + b.addBottomSheetCallback(new com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@androidx.annotation.NonNull View bottomSheet, int newState) { + if (newState == com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN) { + finish(); + } + } + @Override + public void onSlide(@androidx.annotation.NonNull View bottomSheet, float slideOffset) { } + }); } private void toggleTheme() { diff --git a/controller/app/src/main/java/org/iiab/controller/PortalActivity.java b/controller/app/src/main/java/org/iiab/controller/PortalActivity.java index 12ca4cd2..6f513453 100644 --- a/controller/app/src/main/java/org/iiab/controller/PortalActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/PortalActivity.java @@ -99,11 +99,14 @@ protected void onCreate(Bundle savedInstanceState) { Button btnExit = findViewById(R.id.btnExit); Button btnForward = findViewById(R.id.btnForward); - // --- PREPARE HIDDEN BAR --- + // --- PERSISTENT BAR --- + // Keep the nav bar (with Exit) visible so a non-technical user always has an + // obvious way out. The handle is only used after a manual hide (btnHideNav). bottomNav.post(() -> { - bottomNav.setTranslationY(bottomNav.getHeight()); // Move outside the screen + bottomNav.setTranslationY(0); bottomNav.setVisibility(View.VISIBLE); }); + btnHandle.setVisibility(View.GONE); // --- AUTO-HIDE TIMER --- Handler hideHandler = new Handler(Looper.getMainLooper()); @@ -114,10 +117,8 @@ protected void onCreate(Bundle savedInstanceState) { btnHandle.animate().alpha(1f).setDuration(150); }; - Runnable resetTimer = () -> { - hideHandler.removeCallbacks(hideRunnable); - hideHandler.postDelayed(hideRunnable, 5000); - }; + // Persistent bar: never auto-hide. Cancel any pending hide; only btnHideNav hides. + Runnable resetTimer = () -> hideHandler.removeCallbacks(hideRunnable); // --- HANDLE LOGIC (Show Bar) --- btnHandle.setOnClickListener(v -> { diff --git a/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java b/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java index b202ec54..4e473a10 100644 --- a/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/SetupSectionFragment.java @@ -243,7 +243,7 @@ private void completeSetup() { SharedPreferences prefs = requireContext().getSharedPreferences( getString(R.string.pref_file_internal), Context.MODE_PRIVATE); prefs.edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply(); - startActivity(new Intent(requireContext(), MainActivity.class)); + startActivity(new Intent(requireContext(), org.iiab.controller.redesign.LibraryActivity.class)); requireActivity().finish(); } diff --git a/controller/app/src/main/java/org/iiab/controller/SplashActivity.java b/controller/app/src/main/java/org/iiab/controller/SplashActivity.java index 86a7458f..f643d2eb 100644 --- a/controller/app/src/main/java/org/iiab/controller/SplashActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/SplashActivity.java @@ -64,7 +64,7 @@ protected void onCreate(Bundle savedInstanceState) { fade.setAlpha(0f); new Handler(Looper.getMainLooper()).postDelayed(() -> fade.animate().alpha(1f).setDuration(EXIT_FADE_MS).withEndAction(() -> { - startActivity(new Intent(SplashActivity.this, MainActivity.class)); + startActivity(new Intent(SplashActivity.this, org.iiab.controller.redesign.LibraryActivity.class)); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); finish(); }).start(), EXIT_AT_MS); diff --git a/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java b/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java index d81b3691..8a2cd98b 100644 --- a/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java +++ b/controller/app/src/main/java/org/iiab/controller/SystemStateEvaluator.java @@ -26,6 +26,14 @@ private SystemStateEvaluator() { private static volatile String cachedDebianArch; private static volatile boolean archCalculated; + /** True when a system (rootfs) is actually installed on disk — the reliable signal for + * whether "Get more" should skip the destructive system step and go straight to content. */ + public static boolean isSystemInstalled(Context ctx) { + File rootfsDir = new File(ctx.getFilesDir(), "rootfs/installed-rootfs/iiab"); + return new File(rootfsDir, "bin/bash").exists() + || new File(rootfsDir, "usr/local/pdsm/flag_install_ready").exists(); + } + /** Server responding → ONLINE; else derive from the rootfs on disk. */ public static DashboardFragment.SystemState evaluate(Context ctx, boolean serverAlive) { File rootfsDir = new File(ctx.getFilesDir(), "rootfs/installed-rootfs/iiab"); diff --git a/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java b/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java index 3797b4d9..e7a795e3 100644 --- a/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java +++ b/controller/app/src/main/java/org/iiab/controller/TerminalSessionService.java @@ -97,6 +97,7 @@ private Notification buildNotification() { // version-footer gesture (ADFA-4696). SINGLE_TOP reuses the running Activity. Intent open = new Intent(this, MainActivity.class) .putExtra(MainActivity.EXTRA_OPEN_TERMINAL, true) + .putExtra(MainActivity.EXTRA_TERMINAL_ONLY, true) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent pending = PendingIntent.getActivity( this, 0, open, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); diff --git a/controller/app/src/main/java/org/iiab/controller/applang/data/ContentLanguage.java b/controller/app/src/main/java/org/iiab/controller/applang/data/ContentLanguage.java new file mode 100644 index 00000000..1cd0a63b --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/applang/data/ContentLanguage.java @@ -0,0 +1,41 @@ +package org.iiab.controller.applang.data; + +import android.content.res.Resources; +import android.os.LocaleList; + +import java.util.Locale; + +/** + * Content-language helper (ADFA-4725). Two jobs: + * - normalize Java's legacy ISO-639 codes to the modern ones the Kiwix catalog uses + * (iw->he, in->id, ji->yi), and + * - derive the device/system UI language as the default content language. + * Fixes the "auto-detect isn't working" report: install-time consumers used to fall back + * to a hardcoded "en" and never re-derived from the system locale, and legacy codes never + * matched the catalog. + */ +public final class ContentLanguage { + + private ContentLanguage() { + } + + /** Normalize a language code to the modern ISO-639 form; null/empty passes through. */ + public static String normalize(String code) { + if (code == null || code.isEmpty()) return code; + String c = code.toLowerCase(Locale.ROOT); + switch (c) { + case "iw": return "he"; + case "in": return "id"; + case "ji": return "yi"; + default: return c; + } + } + + /** The system UI language as a normalized content-language code (e.g. "es"), or "en". */ + public static String systemDefault() { + LocaleList locales = Resources.getSystem().getConfiguration().getLocales(); + if (locales.isEmpty()) return "en"; + String lang = locales.get(0).getLanguage(); + return (lang == null || lang.isEmpty()) ? "en" : normalize(lang); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java index 0904f50b..41984370 100644 --- a/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java +++ b/controller/app/src/main/java/org/iiab/controller/install/presentation/InstallService.java @@ -83,6 +83,7 @@ public final class InstallService extends Service { // Which pipeline to run (ADFA-4476). Absent/"install" -> normal install; "reset" -> scratch reset. public static final String EXTRA_MODE = "mode"; + public static final String EXTRA_SKIP_MAPS = "skipMaps"; // content flow: maps ship in the base image public static final String MODE_INSTALL = "install"; public static final String MODE_RESET = "reset"; @@ -103,6 +104,7 @@ public final class InstallService extends Service { private String overrideKiwixLang; private String overrideKiwixVariant; private boolean reinstall; + private boolean skipMaps; private boolean resetMode; // ADFA-4476 slice 3: per-module install queue state (module mode only). @@ -169,6 +171,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { overrideKiwixLang = intent.getStringExtra(EXTRA_KIWIX_LANG); overrideKiwixVariant = intent.getStringExtra(EXTRA_KIWIX_VARIANT); reinstall = intent.getBooleanExtra(EXTRA_REINSTALL, false); + skipMaps = intent.getBooleanExtra(EXTRA_SKIP_MAPS, false); resetMode = MODE_RESET.equals(intent.getStringExtra(EXTRA_MODE)); startForeground(NOTIFICATION_ID, buildNotification(getString(R.string.install_busy_provisioning))); @@ -194,8 +197,25 @@ public int onStartCommand(Intent intent, int flags, int startId) { // ---------------------------------------------------------------- pipeline + /** Record the tier being installed so a later content-only "Get more" can size correctly. */ + private void persistInstalledTier() { + try { + getSharedPreferences(getString(R.string.pref_file_internal), android.content.Context.MODE_PRIVATE) + .edit().putString("installed_tier", tier.name()).apply(); + } catch (Exception ignore) { /* best-effort */ } + } + private void runPipeline() { try { + // Non-destructive guard (ADFA-4725): an already-installed system is NEVER + // re-extracted unless an explicit reinstall was requested. "Get more" then only + // runs the additive companion-data steps (Kiwix zims + Maps) inside the existing + // rootfs, so the OS blocks and any customized content are left untouched. + if (!reinstall && debianRootfs.exists() && debianRootfs.isDirectory()) { + if (companionData) startCompanionData(); + else finishSuccess(); + return; + } if (reinstall && debianRootfs.exists() && debianRootfs.isDirectory()) { postProvisioning(getString(R.string.install_status_wiping_old)); try { @@ -208,6 +228,7 @@ private void runPipeline() { } } if (cancelled) return; + persistInstalledTier(); startRootfsDownload(); } catch (Exception e) { Log.e(TAG, "Install pipeline crashed", e); @@ -307,7 +328,7 @@ public void onError(String error) { private void startCompanionData() { editLocalVarsForMaps(debianRootfs, tier); SharedPreferences prefs = getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE); - String targetLang = (overrideKiwixLang != null) ? overrideKiwixLang : prefs.getString("selected_lang_minimal", "en"); + String targetLang = (overrideKiwixLang != null) ? overrideKiwixLang : prefs.getString("selected_lang_minimal", org.iiab.controller.applang.data.ContentLanguage.systemDefault()); InstallationPlanner.calculateProjectedSize(this, tier, true, targetLang, overrideKiwixVariant, new InstallationPlanner.PlanResultListener() { @@ -368,8 +389,9 @@ public void onError(String error) { private void runMapsAnsible() { if (cancelled) return; - if (tier == InstallationPlanner.Tier.BASIC) { - // BASIC bypass: maps already provisioned in the base image. + if (skipMaps || tier == InstallationPlanner.Tier.BASIC) { + // Maps ship in the software block (base image) and BASIC already has them; the + // content flow disables the maps reinstall. Skip straight to success. postProvisioning(getString(R.string.install_status_maps_provisioned)); new Handler(Looper.getMainLooper()).postDelayed(this::finishSuccess, 1500); return; diff --git a/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java b/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java index 972ff621..82b501d2 100644 --- a/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java +++ b/controller/app/src/main/java/org/iiab/controller/install/presentation/PlannerController.java @@ -309,7 +309,7 @@ private void onRootfsSizeResolved(RootfsUiState rootfsState) { } android.content.SharedPreferences prefs = fragment.requireContext().getSharedPreferences(fragment.getString(R.string.pref_file_internal), Context.MODE_PRIVATE); - String targetLang = (host.getOverrideKiwixLang() != null) ? host.getOverrideKiwixLang() : prefs.getString("selected_lang_minimal", "en"); + String targetLang = (host.getOverrideKiwixLang() != null) ? host.getOverrideKiwixLang() : prefs.getString("selected_lang_minimal", org.iiab.controller.applang.data.ContentLanguage.systemDefault()); InstallationPlanner.Tier evalTier = (host.getSelectedTier() != null) ? host.getSelectedTier() : InstallationPlanner.Tier.BASIC; InstallationPlanner.calculateProjectedSize(fragment.requireContext(), evalTier, chkCompanionData.isChecked(), targetLang, host.getOverrideKiwixVariant(), osGiB, new InstallationPlanner.PlanResultListener() { @@ -474,7 +474,7 @@ public void onReady(JSONObject catalog) { List displayNames = new ArrayList<>(); int selectedIndex = 0; android.content.SharedPreferences prefs = fragment.requireContext().getSharedPreferences(fragment.getString(R.string.pref_file_internal), Context.MODE_PRIVATE); - String currentTarget = (host.getOverrideKiwixLang() != null) ? host.getOverrideKiwixLang() : prefs.getString("selected_lang_minimal", "en"); + String currentTarget = (host.getOverrideKiwixLang() != null) ? host.getOverrideKiwixLang() : prefs.getString("selected_lang_minimal", org.iiab.controller.applang.data.ContentLanguage.systemDefault()); for (int i = 0; i < langKeys.size(); i++) { String code = langKeys.get(i); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/AbFlip.java b/controller/app/src/main/java/org/iiab/controller/redesign/AbFlip.java new file mode 100644 index 00000000..5ea4fdfd --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/AbFlip.java @@ -0,0 +1,19 @@ +package org.iiab.controller.redesign; + +import android.os.SystemClock; +import android.view.View; + +/** Hidden A/B-test gesture: 5 taps on the title within 1.5s flips the Step-2 layout. */ +final class AbFlip { + private AbFlip() { } + static void attach(View title, Runnable onFlip) { + final int[] count = {0}; + final long[] last = {0}; + title.setOnClickListener(v -> { + long now = SystemClock.uptimeMillis(); + if (now - last[0] > 1500) count[0] = 0; + last[0] = now; + if (++count[0] >= 5) { count[0] = 0; onFlip.run(); } + }); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java new file mode 100644 index 00000000..9bd3d7be --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/CloneFragment.java @@ -0,0 +1,19 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; + +/** Clone tab — designed shell (ADFA-4725, Phase 6). Live transfer lands later. */ +public class CloneFragment extends Fragment { + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + return inflater.inflate(R.layout.fragment_k2go_clone, c, false); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/ConnectFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/ConnectFragment.java new file mode 100644 index 00000000..c96d6b5c --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/ConnectFragment.java @@ -0,0 +1,19 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; + +/** Connect tab — designed shell (ADFA-4725, Phase 6). Live hotspot/QR lands later. */ +public class ConnectFragment extends Fragment { + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + return inflater.inflate(R.layout.fragment_k2go_connect, c, false); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java new file mode 100644 index 00000000..c0ecf5f9 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryActivity.java @@ -0,0 +1,310 @@ +package org.iiab.controller.redesign; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import androidx.appcompat.app.AppCompatActivity; +import com.airbnb.lottie.LottieAnimationView; +import com.airbnb.lottie.LottieDrawable; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import org.iiab.controller.R; +import org.iiab.controller.ServerController; +import org.iiab.controller.ServerStateRepository; +import org.iiab.controller.WatchdogService; +import org.iiab.controller.install.presentation.InstallProgressRepository; +import org.iiab.controller.install.presentation.InstallState; + +/** + * New content-first UI shell (ADFA-4725). Owns the server lifecycle for the new UI: + * starts the status poll, auto-starts the stack if it is down, and shows a boot gate + * (Lottie) that flips to OPEN once the server is reachable. + * Phase 2 = runtime gate. Content cards, wizard and Step-2 land in later phases. + */ +public class LibraryActivity extends AppCompatActivity implements ServerController.Host { + + private static final String TAG = "K2Go-Library"; + private static final long AUTOSTART_DELAY_MS = 3500L; + private static final long GATE_SAFETY_MS = 25000L; + /** Nothing installed → nothing to boot: dismiss the gate promptly instead of waiting. */ + private static final long NO_SYSTEM_GATE_MS = 900L; + /** Set by the Setup "Download" so the gate waits for the install to finish, not a timeout. */ + public static final String EXTRA_INSTALLING = "installing"; + private boolean installing = false; + + private ServerController serverController; + private boolean isNegotiating = false; + private Boolean targetServerState = null; + + private LottieAnimationView bootGate; + private View installProgress; + private android.widget.TextView installStatus, installDetail; + private android.widget.ProgressBar installBar; + private boolean gateDismissed = false; + private boolean closing = false; + private boolean closedDone = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Not set up yet? Run the first-run wizard, then it routes back here. + SharedPreferences prefs0 = getSharedPreferences( + getString(R.string.pref_file_internal), MODE_PRIVATE); + if (!prefs0.getBoolean(getString(R.string.pref_key_setup_complete), false)) { + startActivity(new Intent(this, WizardActivity.class)); + finish(); + return; + } + + setContentView(R.layout.activity_library); + + BottomNavigationView nav = findViewById(R.id.k2go_bottom_nav); + nav.setOnItemSelectedListener(item -> { + showTab(item.getItemId()); + return true; + }); + if (savedInstanceState == null) { + nav.setSelectedItemId(R.id.nav_library); + } + + bootGate = findViewById(R.id.k2go_boot_gate); + installProgress = findViewById(R.id.k2go_install_progress); + installStatus = findViewById(R.id.k2go_install_status); + installBar = findViewById(R.id.k2go_install_bar); + installDetail = findViewById(R.id.k2go_install_detail); + installing = getIntent().getBooleanExtra(EXTRA_INSTALLING, false); + // The Lottie has a text layer (OPEN/CLOSED sign) referencing "Atkinson Hyperlegible". + // Without this delegate Lottie looks for assets/fonts/Atkinson Hyperlegible.ttf and + // crashes (Font asset not found). Feed it the app's Atkinson font resource instead. + bootGate.setFontAssetDelegate(new com.airbnb.lottie.FontAssetDelegate() { + @Override + public android.graphics.Typeface fetchFont(String fontFamily) { + android.graphics.Typeface tf = androidx.core.content.res.ResourcesCompat.getFont( + LibraryActivity.this, R.font.atkinson_hyperlegible); + return tf != null ? tf : android.graphics.Typeface.DEFAULT; + } + }); + if (!reduceMotion()) { + bootGate.setAnimation(R.raw.library_animation); + bootGate.setMinAndMaxFrame("A_ENTRY_LOOP"); + bootGate.setRepeatCount(LottieDrawable.INFINITE); + bootGate.playAnimation(); + } + + // If the user skipped install there is no rootfs/server to wait for; the gate would + // otherwise burn the full safety timeout. Detect it and dismiss quickly. + final boolean systemInstalled = org.iiab.controller.SystemStateEvaluator.isSystemInstalled(this); + + serverController = new ServerController(this, this); + serverController.start(); + + ServerStateRepository.get().state().observe(this, s -> { + if (s == null) return; + if (closing) { + if (!s.alive) onClosedReady(); + } else if (s.alive) { + onServerReady(); + } + }); + + // Keep the gate up while an install runs, showing real progress, and dismiss only + // when it actually finishes (or fails) — a 2-3 GB download won't beat a timeout. + InstallProgressRepository.get().state().observe(this, st -> { + if (st == null || gateDismissed) return; + if (st.isRunning()) { installing = true; showInstallProgress(st); } + else if (st.isTerminal()) { hideInstallProgress(); onServerReady(); } + }); + + Handler main = new Handler(Looper.getMainLooper()); + if (installing) { + // A download is in progress: keep the gate and show live progress; dismissal + // comes from the install reaching SUCCESS/FAILED, not a timeout. + showInstallProgress(InstallProgressRepository.get().current()); + } else { + // If the stack isn't up after one poll cycle, start it. + if (systemInstalled) { + main.postDelayed(() -> { + if (!isFinishing() + && !ServerStateRepository.get().current().alive + && targetServerState == null) { + serverController.handleServerLaunchClick(findViewById(android.R.id.content)); + } + }, AUTOSTART_DELAY_MS); + } + // Safety: never trap the user behind the gate. + main.postDelayed(() -> { + if (!gateDismissed) { + onServerReady(); + } + }, systemInstalled ? GATE_SAFETY_MS : NO_SYSTEM_GATE_MS); + } + } + + private void showInstallProgress(InstallState st) { + if (installProgress == null || st == null || !st.isRunning()) return; + installProgress.setVisibility(View.VISIBLE); + if (st.phase == InstallState.Phase.DOWNLOADING) { + installStatus.setText("Downloading your library…"); + installBar.setIndeterminate(false); + installBar.setProgress(st.percent); + installDetail.setText(st.percent + "%" + (st.speed.isEmpty() ? "" : " · " + st.speed)); + } else { + installStatus.setText(st.message.isEmpty() ? "Setting up your library…" : st.message); + installBar.setIndeterminate(true); + installDetail.setText(""); + } + } + + private void hideInstallProgress() { + if (installProgress != null) installProgress.setVisibility(View.GONE); + } + + private void onServerReady() { + if (gateDismissed || bootGate == null) { + return; + } + gateDismissed = true; + hideInstallProgress(); + if (reduceMotion()) { bootGate.setVisibility(View.GONE); return; } + bootGate.removeAllAnimatorListeners(); + bootGate.setRepeatCount(0); + bootGate.setMinAndMaxFrame("B_OPEN_FLIP"); + bootGate.addAnimatorListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (bootGate != null) { + bootGate.setVisibility(View.GONE); + } + } + }); + bootGate.playAnimation(); + } + + private void showTab(int itemId) { + final String title; + if (itemId == R.id.nav_connect) { + title = "Connect"; + } else if (itemId == R.id.nav_clone) { + title = "Clone"; + } else if (itemId == R.id.nav_settings) { + title = "Settings"; + } else { + title = "Library"; + } + androidx.fragment.app.Fragment f; + if (itemId == R.id.nav_library) { + f = new LibraryHomeFragment(); + } else if (itemId == R.id.nav_connect) { + f = new ConnectFragment(); + } else if (itemId == R.id.nav_clone) { + f = new CloneFragment(); + } else if (itemId == R.id.nav_settings) { + f = new SettingsFragment(); + } else { + f = PlaceholderFragment.newInstance(title); + } + // Switching tabs clears any Settings sub-screen on the back stack. + getSupportFragmentManager().popBackStack(null, androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_nav_host, f) + .commit(); + } + + /** Push a Settings sub-screen (Language/About/Advanced/Feedback) keeping the bottom nav. */ + public void openSettingsSub(androidx.fragment.app.Fragment f) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_nav_host, f) + .addToBackStack("settings_sub") + .commit(); + } + + @Override + protected void onResume() { + super.onResume(); + if (serverController != null) serverController.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + if (serverController != null) serverController.onPause(); + } + + private boolean reduceMotion() { + try { + return android.provider.Settings.Global.getFloat(getContentResolver(), + android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f; + } catch (Exception e) { + return false; + } + } + + /** Settings "Turn off K2Go": full-screen closing scene + graceful teardown, then leave. */ + public void turnOffK2Go() { + if (closing) return; + closing = true; + if (bootGate != null && !reduceMotion()) { + bootGate.setVisibility(View.VISIBLE); + bootGate.removeAllAnimatorListeners(); + bootGate.setRepeatCount(LottieDrawable.INFINITE); + bootGate.setMinAndMaxFrame("C_EXIT_LOOP"); + bootGate.playAnimation(); + } + if (ServerStateRepository.get().current().alive && targetServerState == null) { + serverController.handleServerLaunchClick(findViewById(android.R.id.content)); + } else if (!ServerStateRepository.get().current().alive) { + onClosedReady(); + } + new Handler(Looper.getMainLooper()).postDelayed(this::onClosedReady, 15000L); + } + + private void onClosedReady() { + if (closedDone) return; + closedDone = true; + if (bootGate == null) { finishAndRemoveTask(); return; } + if (reduceMotion()) { finishAndRemoveTask(); return; } + bootGate.removeAllAnimatorListeners(); + bootGate.setRepeatCount(0); + bootGate.setMinAndMaxFrame("D_CLOSED_FLIP"); + bootGate.addAnimatorListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { finishAndRemoveTask(); } + }); + bootGate.playAnimation(); + } + + // --- ServerController.Host (shell: pulses / LEDs are no-ops for now) -------- + @Override public void addToLog(String message) { Log.d(TAG, message); } + @Override public void startFusionPulse() { } + @Override public void startExitPulse() { } + @Override public void stopBtnProgress() { } + @Override public void updateConnectivityLeds(boolean wifiOn, boolean hotspotOn) { } + @Override public void refreshServerUi() { } + @Override public Boolean getTargetServerState() { return targetServerState; } + @Override public void setTargetServerState(Boolean target) { targetServerState = target; } + @Override public boolean isNegotiating() { return isNegotiating; } + + @Override + public void enableSystemProtection() { + Intent i = new Intent(this, WatchdogService.class); + i.setAction(WatchdogService.ACTION_START); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + startForegroundService(i); + } else { + startService(i); + } + } + + @Override + public void disableSystemProtection() { + Intent i = new Intent(this, WatchdogService.class); + i.setAction(WatchdogService.ACTION_STOP); + startService(i); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java new file mode 100644 index 00000000..d4698f92 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/LibraryHomeFragment.java @@ -0,0 +1,194 @@ +package org.iiab.controller.redesign; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import org.iiab.controller.PortalActivity; +import org.iiab.controller.R; +import org.iiab.controller.ServerStateRepository; +import org.iiab.controller.config.BoxEndpoints; +import org.iiab.controller.util.AppExecutors; + +/** + * Content-first Library home (ADFA-4725, Phase 3): action cards with live 3-state + * status dots (gray = not installed / amber = starting / green = ready). Tapping a + * ready card opens its content in the portal WebView (Explore Wikipedia -> Kiwix). + */ +public class LibraryHomeFragment extends Fragment { + + private static final long POLL_MS = 3000L; + private static final int GRAY = 0, AMBER = 1, GREEN = 2, RED = 3; + + private static final class Card { + final String endpoint; final String title; final boolean requires64; final int iconRes; + View dot; TextView status; int state = GRAY; int amberStreak = 0; + Card(String e, String t, boolean r, int i) { endpoint = e; title = t; requires64 = r; iconRes = i; } + } + + private final List cards = new ArrayList<>(); + private final Handler main = new Handler(Looper.getMainLooper()); + private TextView homeStatus; + private View homeStatusDot; + + private final Runnable poll = new Runnable() { + @Override public void run() { + refreshStatuses(); + main.postDelayed(this, POLL_MS); + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_library, container, false); + homeStatus = root.findViewById(R.id.k2go_home_status); + homeStatusDot = root.findViewById(R.id.k2go_home_status_dot); + + cards.clear(); + cards.add(new Card("books", "Read a book", false, R.drawable.ic_card_book)); + cards.add(new Card("code", "Code on the Go", false, R.drawable.ic_card_code)); + cards.add(new Card("kiwix", "Explore Wikipedia", true, R.drawable.ic_card_wikipedia)); + cards.add(new Card("kolibri", "Take courses", false, R.drawable.ic_card_courses)); + cards.add(new Card("maps", "Navigate maps", false, R.drawable.ic_card_maps)); + + buildCards(inflater, root.findViewById(R.id.k2go_cards)); + + root.findViewById(R.id.k2go_get_more).setOnClickListener(v -> { + // If a system is already installed, skip the destructive system step and go + // straight to content (Step 2). Otherwise run the full setup from Step 1. + android.content.Intent i = new android.content.Intent(requireContext(), SetupLibraryActivity.class); + if (org.iiab.controller.SystemStateEvaluator.isSystemInstalled(requireContext())) { + i.putExtra(SetupLibraryActivity.EXTRA_CONTENT_ONLY, true); + } + startActivity(i); + }); + return root; + } + + private void buildCards(LayoutInflater inflater, LinearLayout host) { + LinearLayout row = null; + for (int i = 0; i < cards.size(); i++) { + if (i % 2 == 0) { + row = new LinearLayout(requireContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + host.addView(row, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + } + final Card c = cards.get(i); + View card = inflater.inflate(R.layout.view_k2go_card, row, false); + ((ImageView) card.findViewById(R.id.k2go_card_icon)).setImageResource(c.iconRes); + ((TextView) card.findViewById(R.id.k2go_card_title)).setText(c.title); + c.dot = card.findViewById(R.id.k2go_card_dot); + c.status = card.findViewById(R.id.k2go_card_status); + card.setOnClickListener(v -> onCardClick(c)); + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) card.getLayoutParams(); + lp.width = 0; lp.weight = 1f; + row.addView(card, lp); + applyState(c, GRAY); + } + if (cards.size() % 2 == 1 && row != null) { + View spacer = new View(requireContext()); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, 1); + lp.weight = 1f; + row.addView(spacer, lp); + } + } + + private void onCardClick(Card c) { + if (c.state == GREEN) { + Intent i = new Intent(requireContext(), PortalActivity.class); + i.putExtra("TARGET_URL", BoxEndpoints.BASE + "/" + c.endpoint + "/"); + startActivity(i); + } else { + c.amberStreak = 0; + applyState(c, AMBER); + AppExecutors.get().io().execute(() -> { + final int st = probe(c.endpoint); + main.post(() -> { if (isAdded()) applyState(c, (st == GREEN || st == GRAY) ? st : AMBER); }); + }); + Toast.makeText(requireContext(), "Retrying…", Toast.LENGTH_SHORT).show(); + } + } + + @Override public void onResume() { super.onResume(); main.post(poll); } + @Override public void onPause() { super.onPause(); main.removeCallbacks(poll); } + + private void refreshStatuses() { + boolean alive = ServerStateRepository.get().current().alive; + if (homeStatus != null) homeStatus.setText(alive ? "Ready to explore" : "Starting your library…"); + if (homeStatusDot != null) tint(homeStatusDot, alive ? R.color.k2go_leaf : R.color.k2go_amber); + for (final Card c : cards) { + if (c.requires64 && android.os.Build.SUPPORTED_64_BIT_ABIS.length == 0) { + applyState(c, GRAY); + if (c.status != null) c.status.setText("Not supported"); + continue; + } + if (!alive) { applyState(c, GRAY); continue; } + AppExecutors.get().io().execute(() -> { + final int st = probe(c.endpoint); + main.post(() -> { + if (!isAdded()) return; + if (st == GREEN || st == GRAY) { c.amberStreak = 0; applyState(c, st); } + else { c.amberStreak++; applyState(c, c.amberStreak >= 4 ? RED : AMBER); } + }); + }); + } + } + + private void applyState(Card c, int st) { + c.state = st; + if (c.dot == null || c.status == null) return; + int dotColor, textColor; + String label; + switch (st) { + case GREEN: dotColor = R.color.k2go_leaf; textColor = R.color.k2go_leaf; label = "Ready"; break; + case AMBER: dotColor = R.color.k2go_amber; textColor = R.color.k2go_amber_text; label = "Connecting"; break; + case RED: dotColor = R.color.k2go_clay; textColor = R.color.k2go_clay; label = "Unavailable · tap to retry"; break; + default: dotColor = R.color.k2go_muted; textColor = R.color.k2go_muted; label = "Not installed"; break; + } + tint(c.dot, dotColor); + c.status.setText(label); + c.status.setTextColor(ContextCompat.getColor(requireContext(), textColor)); + } + + private void tint(View v, int colorRes) { + v.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(requireContext(), colorRes))); + } + + private static int probe(String endpoint) { + HttpURLConnection c = null; + try { + URL u = new URL(BoxEndpoints.BASE + "/" + endpoint + "/"); + c = (HttpURLConnection) u.openConnection(); + c.setUseCaches(false); + c.setConnectTimeout(1500); + c.setReadTimeout(1500); + c.setRequestMethod("GET"); + int code = c.getResponseCode(); + if (code >= 200 && code < 400) return GREEN; + if (code == 404) return GRAY; + return AMBER; + } catch (Exception e) { + return AMBER; + } finally { + if (c != null) c.disconnect(); + } + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/PlaceholderFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/PlaceholderFragment.java new file mode 100644 index 00000000..89ea2347 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/PlaceholderFragment.java @@ -0,0 +1,37 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; + +/** Temporary tab placeholder for the Phase-1 shell; replaced by real fragments in later phases. */ +public class PlaceholderFragment extends Fragment { + + private static final String ARG_TITLE = "title"; + + public static PlaceholderFragment newInstance(String title) { + PlaceholderFragment f = new PlaceholderFragment(); + Bundle b = new Bundle(); + b.putString(ARG_TITLE, title); + f.setArguments(b); + return f; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.fragment_k2go_placeholder, container, false); + TextView title = v.findViewById(R.id.k2go_placeholder_title); + if (getArguments() != null) { + title.setText(getArguments().getString(ARG_TITLE, "")); + } + return v; + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java new file mode 100644 index 00000000..a0b8ec5b --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsFragment.java @@ -0,0 +1,127 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import org.iiab.controller.R; +import org.iiab.controller.feedback.presentation.FeedbackFragment; + +/** Settings v3 top level: Language · Theme · Help · Send feedback · About · Advanced, with a + * pinned "Turn off K2Go" footer that stays visible outside the scroll. Advanced/About/Language + * open as sub-screens (bottom nav stays); Theme + Language + Send feedback are functional. */ +public class SettingsFragment extends Fragment { + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_settings, c, false); + Context ctx = requireContext(); + LinearLayout list = root.findViewById(R.id.k2go_settings_list); + LinearLayout footer = root.findViewById(R.id.k2go_settings_footer); + + SettingsUi.row(ctx, list, "Language", null, null, v -> openSub("language")); + SettingsUi.row(ctx, list, "Theme", null, themeLabel(), v -> chooseTheme()); + SettingsUi.preview(ctx, list, "Help", null); + SettingsUi.row(ctx, list, "Send feedback", null, null, v -> openFeedback()); + SettingsUi.row(ctx, list, "About", null, versionName(), v -> openSub("about")); + SettingsUi.row(ctx, list, "Advanced", "System, backups, developer tools", null, v -> openSub("advanced")); + + buildFooter(ctx, footer); + return root; + } + + private void openSub(String screen) { + if (getActivity() instanceof LibraryActivity) { + ((LibraryActivity) getActivity()).openSettingsSub(SettingsSubFragment.newInstance(screen)); + } + } + + private void openFeedback() { + if (getActivity() instanceof LibraryActivity) { + ((LibraryActivity) getActivity()).openSettingsSub(new FeedbackFragment()); + } + } + + private void chooseTheme() { + final String[] labels = {"Light", "Follow system", "Dark"}; + final int[] modes = { + AppCompatDelegate.MODE_NIGHT_NO, + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, + AppCompatDelegate.MODE_NIGHT_YES}; + new AlertDialog.Builder(requireContext()) + .setTitle("Theme") + .setItems(labels, (d, w) -> { + prefs().edit().putInt("k2go_theme", modes[w]).apply(); + AppCompatDelegate.setDefaultNightMode(modes[w]); + }) + .show(); + } + + private String themeLabel() { + switch (prefs().getInt("k2go_theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)) { + case AppCompatDelegate.MODE_NIGHT_NO: return "Light"; + case AppCompatDelegate.MODE_NIGHT_YES: return "Dark"; + default: return "Follow system"; + } + } + + private void buildFooter(Context ctx, LinearLayout footer) { + TextView note = new TextView(ctx); + note.setText("K2Go keeps running in the background until you turn it off."); + note.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + note.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + LinearLayout.LayoutParams np = new LinearLayout.LayoutParams(-1, -2); + np.bottomMargin = SettingsUi.dp(ctx, 8); + footer.addView(note, np); + + TextView off = new TextView(ctx); + off.setText("Turn off K2Go"); + off.setGravity(Gravity.CENTER); + off.setPadding(SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16), SettingsUi.dp(ctx, 16)); + off.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_TitleLarge); + off.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_clay)); + off.setBackgroundResource(R.drawable.k2go_turnoff_bg); + off.setClickable(true); + off.setOnClickListener(v -> confirmTurnOff()); + footer.addView(off, new LinearLayout.LayoutParams(-1, -2)); + } + + private void confirmTurnOff() { + new AlertDialog.Builder(requireContext()) + .setTitle("Turn off K2Go?") + .setMessage("This stops the library and closes the app. (Home or back only minimize it — it keeps running in the background.)") + .setNegativeButton("Cancel", null) + .setPositiveButton("Turn off", (d, w) -> { + if (getActivity() instanceof LibraryActivity) { + ((LibraryActivity) getActivity()).turnOffK2Go(); + } + }) + .show(); + } + + private SharedPreferences prefs() { + return requireContext().getSharedPreferences( + getString(R.string.pref_file_internal), Context.MODE_PRIVATE); + } + + private String versionName() { + try { + return requireContext().getPackageManager() + .getPackageInfo(requireContext().getPackageName(), 0).versionName; + } catch (Exception e) { + return ""; + } + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java new file mode 100644 index 00000000..74798efd --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsSubFragment.java @@ -0,0 +1,129 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import java.util.List; +import org.iiab.controller.R; +import org.iiab.controller.applang.data.AppLocaleController; +import org.iiab.controller.applang.data.ContentLanguage; +import org.iiab.controller.applang.domain.AppLanguage; +import org.iiab.controller.applang.domain.SupportedAppLanguages; +import org.iiab.controller.delivery.data.AnalyticsConsent; + +/** Settings sub-screen host: Language (functional — UI + content), About (version, permissions, + * usage-stats consent), Advanced (power-user features, preview for now). Keeps the bottom nav; + * the ‹ back returns to the Settings top level. */ +public class SettingsSubFragment extends Fragment { + + private static final String ARG = "screen"; + + static SettingsSubFragment newInstance(String screen) { + SettingsSubFragment f = new SettingsSubFragment(); + Bundle b = new Bundle(); + b.putString(ARG, screen); + f.setArguments(b); + return f; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_settings_sub, c, false); + Context ctx = requireContext(); + LinearLayout list = root.findViewById(R.id.k2go_sub_list); + TextView title = root.findViewById(R.id.k2go_sub_title); + root.findViewById(R.id.k2go_sub_back).setOnClickListener(v -> + requireActivity().getSupportFragmentManager().popBackStack()); + + String screen = getArguments() != null ? getArguments().getString(ARG, "") : ""; + switch (screen) { + case "language": title.setText("Language"); buildLanguage(ctx, list); break; + case "about": title.setText("About"); buildAbout(ctx, list); break; + case "advanced": title.setText("Advanced"); buildAdvanced(ctx, list); break; + default: title.setText("Settings"); + } + return root; + } + + // ---- Language: one choice sets BOTH the app UI locale and the content language ---- + private void buildLanguage(Context ctx, LinearLayout list) { + SettingsUi.caption(ctx, list, "Sets the app language and the default content language."); + List langs = SupportedAppLanguages.all(getString(R.string.setup_app_lang_system)); + String current = AppLocaleController.currentTag(); + for (AppLanguage lang : langs) { + boolean isCurrent = lang.tag().equals(current); + SettingsUi.row(ctx, list, lang.toString(), null, isCurrent ? "✓" : null, + v -> applyLanguage(ctx, lang.tag())); + } + } + + private void applyLanguage(Context ctx, String tag) { + // Content language = the base subtag (ru-RU -> ru), normalized to the Kiwix form; + // empty tag (follow system) -> derive from the system locale. + String content = (tag == null || tag.isEmpty()) + ? ContentLanguage.systemDefault() + : ContentLanguage.normalize(tag.split("-")[0]); + ctx.getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE) + .edit().putString("selected_lang_minimal", content).apply(); + // Applying the UI locale recreates the activities (AppCompat persists the choice). + AppLocaleController.apply(tag); + } + + // ---- About ---- + private void buildAbout(Context ctx, LinearLayout list) { + SettingsUi.infoRow(ctx, list, "App version", versionName(ctx)); + SettingsUi.row(ctx, list, "Permissions", null, null, v -> openAppSettings(ctx)); + SettingsUi.toggle(ctx, list, "Share usage statistics", AnalyticsConsent.isEnabled(ctx), checked -> { + AnalyticsConsent.setEnabled(ctx, checked); + org.iiab.controller.analytics.AnalyticsClient.with(ctx).applyConsent(); + }); + SettingsUi.preview(ctx, list, "Open-source licenses", null); + SettingsUi.preview(ctx, list, "Privacy", null); + } + + private void openAppSettings(Context ctx) { + try { + ctx.startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:" + ctx.getPackageName()))); + } catch (Exception ignore) { /* no-op */ } + } + + /** The full Debian terminal lives in MainActivity; EXTRA_OPEN_TERMINAL opens it directly. */ + private void openTerminal(Context ctx) { + Intent i = new Intent(ctx, org.iiab.controller.MainActivity.class); + i.putExtra(org.iiab.controller.MainActivity.EXTRA_OPEN_TERMINAL, true); + i.putExtra(org.iiab.controller.MainActivity.EXTRA_TERMINAL_ONLY, true); + ctx.startActivity(i); + } + + // ---- Advanced (power-user features — preview for now) ---- + private void buildAdvanced(Context ctx, LinearLayout list) { + SettingsUi.caption(ctx, list, "For power users."); + SettingsUi.sectionHeader(ctx, list, "SYSTEM"); + SettingsUi.preview(ctx, list, "Module management", "Tier, add modules, hide"); + SettingsUi.preview(ctx, list, "Backups & recovery", null); + SettingsUi.sectionHeader(ctx, list, "DEVELOPER"); + SettingsUi.preview(ctx, list, "ADB", null); + SettingsUi.preview(ctx, list, "Network & DNS", null); + SettingsUi.row(ctx, list, "Terminal (Debian)", null, null, v -> openTerminal(ctx)); + } + + private String versionName(Context ctx) { + try { + return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionName; + } catch (Exception e) { + return ""; + } + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java new file mode 100644 index 00000000..402909ed --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SettingsUi.java @@ -0,0 +1,125 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.content.ContextCompat; +import org.iiab.controller.R; + +/** Programmatic row builders shared by the Settings top level and its sub-screens. */ +final class SettingsUi { + private SettingsUi() {} + + interface OnToggle { void changed(boolean checked); } + + static int dp(Context c, int v) { return Math.round(v * c.getResources().getDisplayMetrics().density); } + + static void sectionHeader(Context c, LinearLayout list, String text) { + TextView t = new TextView(c); + t.setText(text); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_teal)); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-2, -2); + lp.topMargin = dp(c, 18); lp.bottomMargin = dp(c, 4); + list.addView(t, lp); + } + + static void caption(Context c, LinearLayout list, String text) { + TextView t = new TextView(c); + t.setText(text); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_muted)); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-1, -2); + lp.bottomMargin = dp(c, 2); + list.addView(t, lp); + } + + static View row(Context c, LinearLayout list, String title, String subtitle, String value, View.OnClickListener onClick) { + LinearLayout row = baseRow(c, list); + if (onClick != null) { row.setClickable(true); row.setOnClickListener(onClick); } + LinearLayout col = new LinearLayout(c); + col.setOrientation(LinearLayout.VERTICAL); + col.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + col.addView(title(c, title)); + if (subtitle != null && !subtitle.isEmpty()) col.addView(sub(c, subtitle)); + row.addView(col); + if (value != null && !value.isEmpty()) { + TextView v = sub(c, value); + v.setPadding(0, 0, dp(c, 8), 0); + row.addView(v); + } + if (onClick != null) { + TextView chev = new TextView(c); + chev.setText("›"); + chev.setTextSize(18); + chev.setTextColor(ContextCompat.getColor(c, R.color.k2go_muted)); + row.addView(chev); + } + return row; + } + + static void preview(Context c, LinearLayout list, String title, String subtitle) { + LinearLayout row = baseRow(c, list); + row.setAlpha(0.5f); + LinearLayout col = new LinearLayout(c); + col.setOrientation(LinearLayout.VERTICAL); + col.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + col.addView(title(c, title)); + if (subtitle != null && !subtitle.isEmpty()) col.addView(sub(c, subtitle)); + row.addView(col); + row.addView(sub(c, "Soon")); + } + + static void toggle(Context c, LinearLayout list, String titleText, boolean checked, OnToggle cb) { + LinearLayout row = baseRow(c, list); + TextView t = title(c, titleText); + t.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + row.addView(t); + SwitchCompat sw = new SwitchCompat(c); + sw.setChecked(checked); + // The default switch enforces a 48dp touch-target min-height that inflates the row. + // Drop it and cap the switch so the toggle row matches the value rows. + sw.setMinimumHeight(0); + sw.setPadding(0, 0, 0, 0); + sw.setOnCheckedChangeListener((b, isChk) -> cb.changed(isChk)); + row.addView(sw, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, dp(c, 28))); + } + + static void infoRow(Context c, LinearLayout list, String titleText, String value) { + LinearLayout row = baseRow(c, list); + TextView t = title(c, titleText); + t.setLayoutParams(new LinearLayout.LayoutParams(0, -2, 1f)); + row.addView(t); + row.addView(sub(c, value)); + } + + private static TextView title(Context c, String s) { + TextView t = new TextView(c); + t.setText(s); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_ink)); + return t; + } + private static TextView sub(Context c, String s) { + TextView t = new TextView(c); + t.setText(s); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(c, R.color.k2go_muted)); + return t; + } + private static LinearLayout baseRow(Context c, LinearLayout list) { + LinearLayout row = new LinearLayout(c); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setBackgroundResource(R.drawable.k2go_card_bg); + row.setPadding(dp(c, 14), dp(c, 14), dp(c, 14), dp(c, 14)); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(-1, -2); + lp.topMargin = dp(c, 8); + list.addView(row, lp); + return row; + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java new file mode 100644 index 00000000..19bf52f4 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java @@ -0,0 +1,93 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.util.Log; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; + +/** + * "Set up your library" host (ADFA-4725): Step 1 System -> Step 2 Content (A/B). Holds the + * shared tier + content picks so the two Step-2 layouts (A expandable+bar, B 5-step+gauge) + * carry selections across the hidden tap-5x flip. + */ +public class SetupLibraryActivity extends AppCompatActivity { + + private InstallationPlanner.Tier selectedTier = InstallationPlanner.Tier.STANDARD; + + /** Launch extra: skip Step 1 (system) and open Step 2 (content) directly, for when a + * system is already installed so adding content never overwrites it. */ + public static final String EXTRA_CONTENT_ONLY = "contentOnly"; + private boolean contentEverything = false; // legacy (kept for compat; unused by the picker) + private boolean contentPictures = true; // legacy + private boolean optionB = false; // A is the default + // Shared Wikipedia selection so picks survive the A/B flip. + private final java.util.LinkedHashSet wikiVariants = new java.util.LinkedHashSet<>(); + private boolean wikiIncluded = true; + private String wikiView = "list"; // "list" | "grouped" + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_k2go_setup); + if (savedInstanceState == null) { + boolean contentOnly = getIntent().getBooleanExtra(EXTRA_CONTENT_ONLY, false); + androidx.fragment.app.Fragment first; + if (contentOnly) { + selectedTier = readInstalledTier(); // size content against the installed tier + first = step2Fragment(); + } else { + first = new Step1SystemFragment(); + } + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_setup_host, first) + .commit(); + } + } + + public void setSelectedTier(InstallationPlanner.Tier tier) { this.selectedTier = tier; } + public InstallationPlanner.Tier getSelectedTier() { return selectedTier; } + + public boolean isEverything() { return contentEverything; } + public void setEverything(boolean b) { contentEverything = b; } + public boolean isPictures() { return contentPictures; } + public void setPictures(boolean b) { contentPictures = b; } + + public java.util.LinkedHashSet getWikiVariants() { return wikiVariants; } + public boolean isWikiIncluded() { return wikiIncluded; } + public void setWikiIncluded(boolean b) { wikiIncluded = b; } + public String getWikiView() { return wikiView; } + public void setWikiView(String v) { wikiView = v; } + + private InstallationPlanner.Tier readInstalledTier() { + String t = getSharedPreferences(getString(R.string.pref_file_internal), MODE_PRIVATE) + .getString("installed_tier", InstallationPlanner.Tier.STANDARD.name()); + try { + return InstallationPlanner.Tier.valueOf(t); + } catch (Exception e) { + return InstallationPlanner.Tier.STANDARD; + } + } + + private Fragment step2Fragment() { + return optionB ? new Step2OptionBFragment() : new Step2OptionAFragment(); + } + + /** Called by Step 1 "Next". */ + public void goToStep2() { + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_setup_host, step2Fragment()) + .addToBackStack("step2") + .commit(); + } + + /** Hidden A/B-test switch: flip the Step-2 layout in place; picks carry over. */ + public void flipAbTest() { + optionB = !optionB; + Log.d("K2Go-ABtest", "Set-up-content layout -> " + (optionB ? "B (5-step + gauge)" : "A (expandable + bar)")); + getSupportFragmentManager().beginTransaction() + .replace(R.id.k2go_setup_host, step2Fragment()) + .commit(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java new file mode 100644 index 00000000..b81dd6c7 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step1SystemFragment.java @@ -0,0 +1,165 @@ +package org.iiab.controller.redesign; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.iiab.controller.applang.data.ContentLanguage; + +/** Step 1 of "Set up your library": choose the system edition (apps) with a live storage bar. + * Edition sizes are resolved from the rootfs .meta4 (live when online); when offline they show + * the last-known value from RootfsCatalog — never hardcoded in the UI. */ +public class Step1SystemFragment extends Fragment { + + private static final class Edition { + final InstallationPlanner.Tier tier; final String name; + final String desc; final boolean recommended; + ImageView radio; TextView sizeView; + Edition(InstallationPlanner.Tier t, String n, String d, boolean r) { + tier = t; name = n; desc = d; recommended = r; + } + } + + private final List editions = new ArrayList<>(); + private InstallationPlanner.Tier selected = InstallationPlanner.Tier.STANDARD; + private View barUsed, barSystem, barFree; + private LinearLayout bar; + private TextView legend; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle s) { + View root = inflater.inflate(R.layout.fragment_k2go_setup_step1, container, false); + bar = root.findViewById(R.id.k2go_storage_bar); + barUsed = root.findViewById(R.id.k2go_bar_used); + barSystem = root.findViewById(R.id.k2go_bar_system); + barFree = root.findViewById(R.id.k2go_bar_free); + legend = root.findViewById(R.id.k2go_storage_legend); + tint(barUsed, R.color.k2go_muted); + tint(barSystem, R.color.k2go_teal); + tint(barFree, R.color.k2go_hairline); + + editions.clear(); + editions.add(new Edition(InstallationPlanner.Tier.BASIC, "Basic", + "Wikipedia reader app + Books app", false)); + editions.add(new Edition(InstallationPlanner.Tier.STANDARD, "Standard", + "Basic plus Courses app", true)); + editions.add(new Edition(InstallationPlanner.Tier.FULL, "Full", + "Standard plus Maps app (all apps)", false)); + + LinearLayout host = root.findViewById(R.id.k2go_editions); + for (Edition e : editions) { + View row = inflater.inflate(R.layout.view_k2go_edition, host, false); + ((TextView) row.findViewById(R.id.k2go_edition_name)).setText(e.name); + ((TextView) row.findViewById(R.id.k2go_edition_desc)).setText(e.desc); + e.sizeView = row.findViewById(R.id.k2go_edition_size); + e.sizeView.setText(sizeText(InstallationPlanner.fallbackOsSizeGb(e.tier))); // instant last-known + row.findViewById(R.id.k2go_edition_reco) + .setVisibility(e.recommended ? View.VISIBLE : View.GONE); + e.radio = row.findViewById(R.id.k2go_edition_radio); + row.setOnClickListener(v -> select(e.tier)); + host.addView(row); + resolveEditionSize(e); // refine to the live .meta4 size when online + } + + root.findViewById(R.id.k2go_step1_next).setOnClickListener(v -> { + if (getActivity() instanceof SetupLibraryActivity) { + ((SetupLibraryActivity) getActivity()).setSelectedTier(selected); + ((SetupLibraryActivity) getActivity()).goToStep2(); + } + }); + + root.findViewById(R.id.k2go_step1_back).setOnClickListener(v -> { + // Setup was already marked complete when the wizard launched this; route to the + // library (reusing it if it is under us) instead of a bare finish() that would + // drop the user to the Android home screen. + android.content.Intent i = new android.content.Intent(requireContext(), LibraryActivity.class); + i.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP | android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(i); + requireActivity().finish(); + }); + root.findViewById(R.id.k2go_step1_skip_now).setOnClickListener(v -> { + requireContext().getSharedPreferences(getString(R.string.pref_file_internal), android.content.Context.MODE_PRIVATE) + .edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply(); + startActivity(new android.content.Intent(requireContext(), LibraryActivity.class)); + requireActivity().finish(); + }); + select(selected); + return root; + } + + private static String sizeText(double gb) { return String.format(Locale.US, "~ %.1f GB", gb); } + + private void resolveEditionSize(Edition e) { + InstallationPlanner.calculateProjectedSize(requireContext(), e.tier, false, + ContentLanguage.systemDefault(), null, + new InstallationPlanner.PlanResultListener() { + @Override + public void onCalculated(InstallationPlanner.StorageProjection p) { + if (isAdded() && e.sizeView != null) e.sizeView.setText(sizeText(p.osSize)); + } + @Override public void onError(String error) { /* keep last-known */ } + }); + } + + private void select(InstallationPlanner.Tier tier) { + selected = tier; + if (getActivity() instanceof SetupLibraryActivity) { + ((SetupLibraryActivity) getActivity()).setSelectedTier(tier); + } + for (Edition e : editions) { + if (e.radio != null) { + e.radio.setImageResource(e.tier == tier ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + } + } + recomputeBar(tier); + } + + private void recomputeBar(InstallationPlanner.Tier tier) { + InstallationPlanner.calculateProjectedSize(requireContext(), tier, false, + ContentLanguage.systemDefault(), null, + new InstallationPlanner.PlanResultListener() { + @Override + public void onCalculated(InstallationPlanner.StorageProjection projection) { + if (isAdded()) applyBar(projection.osSize); + } + @Override + public void onError(String error) { /* keep the last bar on error */ } + }); + } + + private void applyBar(double systemGb) { + double total = StorageInfo.totalGb(); + double used = StorageInfo.usedGb(); + double freeAfter = Math.max(0, StorageInfo.freeGb() - systemGb); + if (total <= 0) total = used + systemGb + freeAfter + 0.01; + bar.setWeightSum((float) total); + setWeight(barUsed, (float) used); + setWeight(barSystem, (float) systemGb); + setWeight(barFree, (float) freeAfter); + legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Free %.1f", + used, systemGb, freeAfter)); + } + + private void setWeight(View v, float w) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); + lp.weight = w; + v.setLayoutParams(lp); + } + + private void tint(View v, int colorRes) { + v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java new file mode 100644 index 00000000..0a6f8bd4 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionAFragment.java @@ -0,0 +1,212 @@ +package org.iiab.controller.redesign; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.util.Locale; +import java.util.Set; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.iiab.controller.SystemStateEvaluator; +import org.iiab.controller.applang.data.ContentLanguage; +import org.iiab.controller.install.presentation.InstallService; +import org.json.JSONObject; + +/** + * Step 2 Content — Option A (Expandable + Bar). Wikipedia is one collapsible module among Books + * (Coming soon), Maps (fixed 0.2 GB) and Courses (disabled). Its versions are chosen with the + * shared {@link WikiVersionPicker} (List/Grouped, multi-select, min one). Bar + total reflect the + * picks; Download drives InstallService. v1 installs the primary of the selection. + */ +public class Step2OptionAFragment extends Fragment { + + private static final double BOOKS_GB = 0.0, MAPS_GB = 0.2; + + private boolean wikiInc = true, booksInc = true; + private final boolean mapsInc = true; // Maps content is fixed for now + private String lang; + private JSONObject langData; + private double osGb = 1.4; + + private Set sel; + private WikiVersionPicker picker; + + private CheckBox wikiCheck, booksCheck, mapsCheck; + private View wikiBody, booksBody, mapsBody; + private TextView wikiChevron, booksChevron, mapsChevron, wikiSkip, booksSkip; + private View wikiCard, booksCard; + private TextView wikiSize, booksSize, legend, download, wikiHint; + private ViewGroup versions; + private LinearLayout bar; + private View bU, bS, bP, bF; + + private SetupLibraryActivity act() { + return (getActivity() instanceof SetupLibraryActivity) ? (SetupLibraryActivity) getActivity() : null; + } + private InstallationPlanner.Tier tier() { return act() != null ? act().getSelectedTier() : InstallationPlanner.Tier.STANDARD; } + + private double wikiSizeGb() { + String p = WikiVariants.primary(sel); + double s = (p == null) ? -1 : WikiVariants.sizeGb(langData, p); + return s >= 0 ? s : 0; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View v = inflater.inflate(R.layout.fragment_k2go_setup_step2a, c, false); + lang = ContentLanguage.systemDefault(); + sel = (act() != null) ? act().getWikiVariants() : new java.util.LinkedHashSet<>(); + if (act() != null) wikiInc = act().isWikiIncluded(); + + bar = v.findViewById(R.id.k2go_bar); + bU = v.findViewById(R.id.k2go_bar_used); bS = v.findViewById(R.id.k2go_bar_system); + bP = v.findViewById(R.id.k2go_bar_picks); bF = v.findViewById(R.id.k2go_bar_free); + legend = v.findViewById(R.id.k2go_legend); + download = v.findViewById(R.id.k2go_download); + wikiCheck = v.findViewById(R.id.k2go_wiki_check); booksCheck = v.findViewById(R.id.k2go_books_check); mapsCheck = v.findViewById(R.id.k2go_maps_check); + wikiBody = v.findViewById(R.id.k2go_wiki_body); booksBody = v.findViewById(R.id.k2go_books_body); mapsBody = v.findViewById(R.id.k2go_maps_body); + wikiChevron = v.findViewById(R.id.k2go_wiki_chevron); booksChevron = v.findViewById(R.id.k2go_books_chevron); mapsChevron = v.findViewById(R.id.k2go_maps_chevron); + wikiSkip = v.findViewById(R.id.k2go_wiki_skip); booksSkip = v.findViewById(R.id.k2go_books_skip); + wikiCard = v.findViewById(R.id.k2go_wiki_card); booksCard = v.findViewById(R.id.k2go_books_card); + wikiSize = v.findViewById(R.id.k2go_wiki_size); booksSize = v.findViewById(R.id.k2go_books_size); + versions = v.findViewById(R.id.k2go_wiki_versions); + wikiHint = v.findViewById(R.id.k2go_wiki_hint); + colorSeg(bU, R.color.k2go_muted); colorSeg(bS, R.color.k2go_teal); colorSeg(bP, R.color.k2go_leaf); colorSeg(bF, R.color.k2go_hairline); + + picker = new WikiVersionPicker(requireContext(), versions, sel, + act() != null ? act().getWikiView() : "list", this::refresh); + + AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { if (act() != null) act().flipAbTest(); }); + + v.findViewById(R.id.k2go_wiki_header).setOnClickListener(x -> toggle(wikiBody, wikiChevron)); + v.findViewById(R.id.k2go_books_header).setOnClickListener(x -> toggle(booksBody, booksChevron)); + v.findViewById(R.id.k2go_maps_header).setOnClickListener(x -> toggle(mapsBody, mapsChevron)); + + wikiCheck.setOnClickListener(x -> setWiki(wikiCheck.isChecked())); + wikiSkip.setOnClickListener(x -> setWiki(!wikiInc)); + booksCheck.setOnClickListener(x -> setBooks(booksCheck.isChecked())); + booksSkip.setOnClickListener(x -> setBooks(!booksInc)); + // Maps ships in the software block — shown for reference, not installed here. + mapsCheck.setChecked(true); mapsCheck.setEnabled(false); + ((TextView) v.findViewById(R.id.k2go_maps_size)).setText("Included"); + + download.setOnClickListener(x -> startDownload()); + v.findViewById(R.id.k2go_step2a_back).setOnClickListener(x -> backOrExit()); + + InstallationPlanner.calculateProjectedSize(requireContext(), tier(), false, lang, null, + new InstallationPlanner.PlanResultListener() { + @Override public void onCalculated(InstallationPlanner.StorageProjection p) { if (isAdded()) { osGb = p.osSize; refresh(); } } + @Override public void onError(String e) { } + }); + InstallationPlanner.getOrFetchCatalog(requireContext(), new InstallationPlanner.CacheListener() { + @Override public void onReady(JSONObject catalog) { + if (!isAdded()) return; + langData = catalog.optJSONObject(lang); + if (langData == null) langData = catalog.optJSONObject("en"); + picker.setLangData(langData); + if (wikiInc) picker.selectDefaultIfEmpty(); + picker.render(); + refresh(); + } + @Override public void onError(String e) { } + }); + + setWiki(wikiInc); + return v; + } + + private void toggle(View body, TextView chevron) { + boolean show = body.getVisibility() != View.VISIBLE; + body.setVisibility(show ? View.VISIBLE : View.GONE); + chevron.setText(show ? "▾" : "▸"); + } + + private void setWiki(boolean inc) { + wikiInc = inc; + wikiCheck.setChecked(inc); + wikiCard.setAlpha(inc ? 1f : 0.55f); + wikiSkip.setText(inc ? R.string.k2go_skip : R.string.k2go_add); + // Inclusion no longer forces the card open — Wikipedia starts collapsed like the + // other sources so the "tap to expand" pattern is visible. Skipping still collapses. + if (!inc) { wikiBody.setVisibility(View.GONE); wikiChevron.setText("▸"); } + if (act() != null) act().setWikiIncluded(inc); + refresh(); + } + private void setBooks(boolean inc) { + booksInc = inc; + booksCheck.setChecked(inc); + booksCard.setAlpha(inc ? 1f : 0.55f); + booksSkip.setText(inc ? R.string.k2go_skip : R.string.k2go_add); + if (!inc) { booksBody.setVisibility(View.GONE); booksChevron.setText("▸"); } + refresh(); + } + + private boolean wikiInvalid() { return wikiInc && sel.isEmpty(); } + + private void refresh() { + if (act() != null) { act().setWikiView(picker.getMode()); act().setWikiIncluded(wikiInc); } + wikiHint.setVisibility(wikiInvalid() ? View.VISIBLE : View.GONE); + wikiSize.setText(!wikiInc ? getString(R.string.k2go_skipped) + : sel.isEmpty() ? "—" : WikiVariants.gb(wikiSizeGb())); + booksSize.setText("0 GB"); + + double picks = (wikiInc && !sel.isEmpty() ? wikiSizeGb() : 0) + (booksInc ? BOOKS_GB : 0); // Maps ships with the system + applyBar(osGb, picks); + download.setText(String.format(Locale.US, "Download library · %.1f GB", osGb + picks)); + boolean ok = !wikiInvalid(); + download.setEnabled(ok); + download.setAlpha(ok ? 1f : 0.5f); + } + + private void applyBar(double systemGb, double picksGb) { + double total = StorageInfo.totalGb(); + double used = StorageInfo.usedGb(); + double freeAfter = Math.max(0, StorageInfo.freeGb() - systemGb - picksGb); + if (total <= 0) total = used + systemGb + picksGb + freeAfter + 0.01; + bar.setWeightSum((float) total); + setW(bU, (float) used); setW(bS, (float) systemGb); setW(bP, (float) picksGb); setW(bF, (float) freeAfter); + legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Your picks %.1f · Free %.1f", used, systemGb, picksGb, freeAfter)); + } + + /** Back to Step 1 when it is on the stack; otherwise (content-only entry) return to the library. */ + private void backOrExit() { + if (getActivity() == null) return; + androidx.fragment.app.FragmentManager fm = getActivity().getSupportFragmentManager(); + if (fm.getBackStackEntryCount() > 0) fm.popBackStack(); + else getActivity().finish(); + } + + private void startDownload() { + if (wikiInvalid()) { Toast.makeText(requireContext(), R.string.k2go_wiki_pick_one, Toast.LENGTH_SHORT).show(); return; } + String variant = (wikiInc && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : ""; + boolean companion = variant != null || booksInc; + Intent i = new Intent(requireContext(), InstallService.class); + i.setAction(InstallService.ACTION_START); + i.putExtra(InstallService.EXTRA_TIER, tier().name()); + i.putExtra(InstallService.EXTRA_COMPANION, companion); + i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); + i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variant); + i.putExtra(InstallService.EXTRA_REINSTALL, false); + i.putExtra(InstallService.EXTRA_SKIP_MAPS, true); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); + else requireContext().startService(i); + startActivity(new Intent(requireContext(), LibraryActivity.class) + .putExtra(LibraryActivity.EXTRA_INSTALLING, true)); + requireActivity().finish(); + } + + private void setW(View v, float w) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); lp.weight = w; v.setLayoutParams(lp); } + private void colorSeg(View v, int colorRes) { v.setBackgroundColor(ContextCompat.getColor(requireContext(), colorRes)); } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java new file mode 100644 index 00000000..b15a7f01 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/Step2OptionBFragment.java @@ -0,0 +1,220 @@ +package org.iiab.controller.redesign; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.MultiResourceGaugeView; +import org.iiab.controller.R; +import org.iiab.controller.SystemStateEvaluator; +import org.iiab.controller.applang.data.ContentLanguage; +import org.iiab.controller.install.presentation.InstallService; +import org.json.JSONObject; + +/** + * Step 2 Content — Option B (5-step map + arc Gauge). Wikipedia has a full screen of its own + * (step 1 of 5) hosting the shared {@link WikiVersionPicker} (List/Grouped, multi-select, min + * one). Maps is fixed; Books/Courses WIP. v1 installs the primary of the selection. + */ +public class Step2OptionBFragment extends Fragment { + + private static final String[] NAMES = {"Wikipedia", "Books", "Maps", "Courses", "Review"}; + private static final String[] INFO = { + "", "Books — coming soon (no content manager yet · 0 GB).", "Maps — included with your edition.", + "Courses — set up on the device after install.", "Review your library, then download."}; + + private int step = 0; + private String lang; + private JSONObject langData; + private boolean wikiIncluded = true; + private double lastTotal = 0, pOs = 1.4, pMaps = 0.2, pKiwix = 4.6; + + private Set sel; + private WikiVersionPicker picker; + + private final TextView[] dots = new TextView[5]; + private TextView caption, legend, btnBack, btnNext, btnSkip, info, wikiHint; + private CheckBox wikiCheck; + private MultiResourceGaugeView gauge; + private View wikiBlock; + private ViewGroup versions; + + private SetupLibraryActivity act() { + return (getActivity() instanceof SetupLibraryActivity) ? (SetupLibraryActivity) getActivity() : null; + } + private InstallationPlanner.Tier tier() { return act() != null ? act().getSelectedTier() : InstallationPlanner.Tier.STANDARD; } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c, @Nullable Bundle s) { + View v = inflater.inflate(R.layout.fragment_k2go_setup_step2b, c, false); + lang = ContentLanguage.systemDefault(); + sel = (act() != null) ? act().getWikiVariants() : new java.util.LinkedHashSet<>(); + if (act() != null) wikiIncluded = act().isWikiIncluded(); + + int[] ids = {R.id.k2go_sd0, R.id.k2go_sd1, R.id.k2go_sd2, R.id.k2go_sd3, R.id.k2go_sd4}; + for (int i = 0; i < 5; i++) dots[i] = v.findViewById(ids[i]); + caption = v.findViewById(R.id.k2go_step_caption); + gauge = v.findViewById(R.id.k2go_gauge); + legend = v.findViewById(R.id.k2go_legend); + info = v.findViewById(R.id.k2go_info_text); + btnBack = v.findViewById(R.id.k2go_btn_back); + btnNext = v.findViewById(R.id.k2go_btn_next); + btnSkip = v.findViewById(R.id.k2go_btn_skip); + wikiCheck = v.findViewById(R.id.k2go_wiki_check); + wikiBlock = v.findViewById(R.id.k2go_wiki_block); + versions = v.findViewById(R.id.k2go_wiki_versions); + wikiHint = v.findViewById(R.id.k2go_wiki_hint); + + picker = new WikiVersionPicker(requireContext(), versions, sel, + act() != null ? act().getWikiView() : "list", () -> { updateHint(); refreshProjection(); }); + + wikiCheck.setChecked(wikiIncluded); + wikiCheck.setOnClickListener(x -> { + wikiIncluded = wikiCheck.isChecked(); + persist(); + versions.setVisibility(wikiIncluded ? View.VISIBLE : View.GONE); + updateHint(); + refreshProjection(); + }); + btnBack.setOnClickListener(x -> { + if (step > 0) { step--; render(); return; } + if (getActivity() == null) return; + androidx.fragment.app.FragmentManager fm = getActivity().getSupportFragmentManager(); + if (fm.getBackStackEntryCount() > 0) fm.popBackStack(); + else getActivity().finish(); + }); + btnSkip.setOnClickListener(x -> { + if (step == 0) { + wikiIncluded = false; + wikiCheck.setChecked(false); + versions.setVisibility(View.GONE); + persist(); + updateHint(); + refreshProjection(); + } + if (step < 4) { step++; render(); } + }); + btnNext.setOnClickListener(x -> { + if (step == 0 && wikiIncluded && sel.isEmpty()) { + updateHint(); + Toast.makeText(requireContext(), R.string.k2go_wiki_pick_one, Toast.LENGTH_SHORT).show(); + return; + } + if (step < 4) { step++; render(); } else startDownload(); + }); + AbFlip.attach(v.findViewById(R.id.k2go_step2_title), () -> { if (act() != null) act().flipAbTest(); }); + + InstallationPlanner.getOrFetchCatalog(requireContext(), new InstallationPlanner.CacheListener() { + @Override public void onReady(JSONObject catalog) { + if (!isAdded()) return; + langData = catalog.optJSONObject(lang); + if (langData == null) langData = catalog.optJSONObject("en"); + picker.setLangData(langData); + if (wikiIncluded) picker.selectDefaultIfEmpty(); + picker.render(); + updateHint(); + refreshProjection(); + } + @Override public void onError(String e) { } + }); + + render(); + refreshProjection(); + return v; + } + + private void persist() { + if (act() != null) { act().setWikiIncluded(wikiIncluded); act().setWikiView(picker.getMode()); } + } + + private void render() { + for (int i = 0; i < 5; i++) { + boolean on = i == step; + dots[i].setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); + dots[i].setTextColor(ContextCompat.getColor(requireContext(), on ? R.color.k2go_on_teal : R.color.k2go_muted)); + } + caption.setText(NAMES[step]); + wikiBlock.setVisibility(step == 0 ? View.VISIBLE : View.GONE); + versions.setVisibility(step == 0 && wikiIncluded ? View.VISIBLE : View.GONE); + info.setVisibility(step == 0 ? View.GONE : View.VISIBLE); + info.setText(INFO[step]); + btnBack.setVisibility(step == 0 ? View.INVISIBLE : View.VISIBLE); + btnSkip.setVisibility(step < 4 ? View.VISIBLE : View.GONE); + updateHint(); + updateNextLabel(); + } + + private void updateHint() { + wikiHint.setVisibility(step == 0 && wikiIncluded && sel.isEmpty() ? View.VISIBLE : View.GONE); + } + + private void updateNextLabel() { + btnNext.setText(step == 4 ? String.format(Locale.US, "Download library · %.1f GB", lastTotal) : "Next"); + } + + private void refreshProjection() { + String variant = (wikiIncluded && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : ""; + InstallationPlanner.calculateProjectedSize(requireContext(), tier(), true, lang, variant, + new InstallationPlanner.PlanResultListener() { + @Override + public void onCalculated(InstallationPlanner.StorageProjection p) { + if (!isAdded()) return; + pOs = p.osSize; pMaps = p.mapsSize; pKiwix = p.kiwixSize; + double picks = ((wikiIncluded && variant != null) ? pKiwix : 0); // Maps ships with the system + lastTotal = pOs + picks; + updateGauge(pOs, picks); + updateNextLabel(); + } + @Override public void onError(String e) { } + }); + } + + private void updateGauge(double systemGb, double picksGb) { + double total = StorageInfo.totalGb(); + double used = StorageInfo.usedGb(); + if (total <= 0) total = used + systemGb + picksGb + 0.01; + double freeAfter = Math.max(0, total - used - systemGb - picksGb); + List segs = new ArrayList<>(); + segs.add(new MultiResourceGaugeView.Segment((float) (used / total * 100f), ContextCompat.getColor(requireContext(), R.color.k2go_muted))); + segs.add(new MultiResourceGaugeView.Segment((float) (systemGb / total * 100f), ContextCompat.getColor(requireContext(), R.color.k2go_teal))); + segs.add(new MultiResourceGaugeView.Segment((float) (picksGb / total * 100f), ContextCompat.getColor(requireContext(), R.color.k2go_leaf))); + if (gauge != null) { + gauge.updateData(segs, String.format(Locale.US, "%.1fG", used + systemGb + picksGb), + ContextCompat.getColor(requireContext(), R.color.k2go_ink), "projected use", ""); + } + legend.setText(String.format(Locale.US, "Used %.1f · System %.1f · Picks %.1f · Free %.1f", + used, systemGb, picksGb, freeAfter)); + } + + private void startDownload() { + String variant = (wikiIncluded && WikiVariants.primary(sel) != null) ? WikiVariants.primary(sel) : ""; + Intent i = new Intent(requireContext(), InstallService.class); + i.setAction(InstallService.ACTION_START); + i.putExtra(InstallService.EXTRA_TIER, tier().name()); + i.putExtra(InstallService.EXTRA_COMPANION, true); + i.putExtra(InstallService.EXTRA_ARCH, SystemStateEvaluator.termuxArch(requireContext())); + i.putExtra(InstallService.EXTRA_KIWIX_LANG, lang); + i.putExtra(InstallService.EXTRA_KIWIX_VARIANT, variant); + i.putExtra(InstallService.EXTRA_REINSTALL, false); + i.putExtra(InstallService.EXTRA_SKIP_MAPS, true); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) requireContext().startForegroundService(i); + else requireContext().startService(i); + startActivity(new Intent(requireContext(), LibraryActivity.class) + .putExtra(LibraryActivity.EXTRA_INSTALLING, true)); + requireActivity().finish(); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/StorageInfo.java b/controller/app/src/main/java/org/iiab/controller/redesign/StorageInfo.java new file mode 100644 index 00000000..59114130 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/StorageInfo.java @@ -0,0 +1,13 @@ +package org.iiab.controller.redesign; + +import android.os.Environment; + +/** Device storage in GB, from the app data partition (matches the old PlannerController source). */ +public final class StorageInfo { + private StorageInfo() { } + private static final double GB = 1024d * 1024d * 1024d; + + public static double totalGb() { return Environment.getDataDirectory().getTotalSpace() / GB; } + public static double freeGb() { return Environment.getDataDirectory().getFreeSpace() / GB; } + public static double usedGb() { return Math.max(0, totalGb() - freeGb()); } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WikiVariants.java b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVariants.java new file mode 100644 index 00000000..340ee4ef --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVariants.java @@ -0,0 +1,71 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import java.util.Locale; +import java.util.Set; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.json.JSONObject; + +/** + * Colloquial, i18n-ready labels for the Kiwix Wikipedia variants (the "dictionary"). Coverage is + * data-driven (all / top1m / top) — never a fixed dichotomy. All labels come from string + * resources so a later l10n pass can translate them without touching this code. + */ +public final class WikiVariants { + private WikiVariants() {} + + public static String coverageOf(String key) { + if (key.startsWith("all_")) return "all"; + if (key.startsWith("top1m_")) return "top1m"; + return "top"; + } + public static String detailOf(String key) { + if (key.endsWith("_maxi")) return "maxi"; + if (key.endsWith("_mini")) return "mini"; + return "nopic"; + } + + public static String coverageName(Context c, String cov) { + switch (cov) { + case "all": return c.getString(R.string.k2go_wiki_cov_all); + case "top1m": return c.getString(R.string.k2go_wiki_cov_top1m); + default: return c.getString(R.string.k2go_wiki_cov_top); + } + } + public static String coverageDesc(Context c, String cov) { + switch (cov) { + case "all": return c.getString(R.string.k2go_wiki_cov_all_desc); + case "top1m": return c.getString(R.string.k2go_wiki_cov_top1m_desc); + default: return c.getString(R.string.k2go_wiki_cov_top_desc); + } + } + public static String detailName(Context c, String det) { + switch (det) { + case "maxi": return c.getString(R.string.k2go_wiki_det_maxi); + case "mini": return c.getString(R.string.k2go_wiki_det_mini); + default: return c.getString(R.string.k2go_wiki_det_nopic); + } + } + public static String label(Context c, String key) { + return coverageName(c, coverageOf(key)) + " · " + detailName(c, detailOf(key)); + } + + public static double sizeGb(JSONObject langData, String key) { + if (langData == null) return -1; + JSONObject o = langData.optJSONObject(key); + return o == null ? -1 : o.optDouble("size", -1); + } + + /** The single ZIM v1 installs from a multi-selection: first in canonical order. */ + public static String primary(Set selected) { + for (String k : InstallationPlanner.CANONICAL_VARIANTS) if (selected.contains(k)) return k; + return null; + } + + public static String gb(double s) { + if (s < 0) return "—"; + if (s >= 1) return String.format(Locale.US, "%.1f GB", s); + return Math.round(s * 1000) + " MB"; + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WikiVersionPicker.java b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVersionPicker.java new file mode 100644 index 00000000..2367fbbb --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WikiVersionPicker.java @@ -0,0 +1,227 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.core.content.ContextCompat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.iiab.controller.InstallationPlanner; +import org.iiab.controller.R; +import org.json.JSONObject; + +/** + * Reusable Wikipedia-version picker. Renders a List (sortable by name/size) OR a Grouped-by- + * coverage view of ONLY the variants a language actually offers, multi-select, into a + * caller-provided container. The selection Set is shared and mutated in place so picks survive + * the A/B flip. v1 downloads a single ZIM (WikiVariants.primary of the selection); the UI is + * multi-capable for a later iteration. + */ +public class WikiVersionPicker { + public interface OnChange { void changed(); } + + private final Context ctx; + private final ViewGroup container; + private final Set selected; + private final OnChange onChange; + private JSONObject langData; + + private String mode; // "list" | "grouped" + private String sortKey = "size"; // "size" | "name" + private int sortDir = 1; // 1 asc, -1 desc + + public WikiVersionPicker(Context ctx, ViewGroup container, Set selected, String mode, OnChange onChange) { + this.ctx = ctx; + this.container = container; + this.selected = selected; + this.mode = (mode == null) ? "list" : mode; + this.onChange = onChange; + } + + public void setLangData(JSONObject ld) { this.langData = ld; } + public String getMode() { return mode; } + public void setMode(String m) { this.mode = m; render(); } + + /** Pre-select the smallest available variant so the min-one rule is satisfied by default. */ + public void selectDefaultIfEmpty() { + if (!selected.isEmpty() || langData == null) return; + String best = null; + double bs = Double.MAX_VALUE; + for (String k : InstallationPlanner.availableVariants(langData)) { + double s = WikiVariants.sizeGb(langData, k); + if (s >= 0 && s < bs) { bs = s; best = k; } + } + if (best != null) selected.add(best); + } + + private int dp(int d) { return Math.round(d * ctx.getResources().getDisplayMetrics().density); } + + public void render() { + container.removeAllViews(); + if (container instanceof LinearLayout) ((LinearLayout) container).setOrientation(LinearLayout.VERTICAL); + + container.addView(toggleRow()); + + List a = InstallationPlanner.availableVariants(langData); + if (a.isEmpty()) { container.addView(muted(ctx.getString(R.string.k2go_wiki_none))); return; } + + if ("grouped".equals(mode)) renderGrouped(a); else renderList(a); + } + + private View toggleRow() { + LinearLayout row = new LinearLayout(ctx); + row.setOrientation(LinearLayout.HORIZONTAL); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + lp.bottomMargin = dp(10); + row.setLayoutParams(lp); + row.addView(chip(ctx.getString(R.string.k2go_wiki_view_list), "list".equals(mode), new View.OnClickListener() { + @Override public void onClick(View v) { setMode("list"); } + })); + View gap = new View(ctx); + gap.setLayoutParams(new LinearLayout.LayoutParams(dp(8), 1)); + row.addView(gap); + row.addView(chip(ctx.getString(R.string.k2go_wiki_view_grouped), "grouped".equals(mode), new View.OnClickListener() { + @Override public void onClick(View v) { setMode("grouped"); } + })); + return row; + } + + private TextView chip(String text, boolean on, View.OnClickListener cl) { + TextView t = new TextView(ctx); + t.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + t.setText(text); + t.setGravity(Gravity.CENTER); + t.setBackgroundResource(on ? R.drawable.k2go_primary_bg : R.drawable.k2go_card_bg); + t.setPadding(dp(10), dp(8), dp(10), dp(8)); + t.setTextColor(ContextCompat.getColor(ctx, on ? R.color.k2go_on_teal : R.color.k2go_ink)); + t.setClickable(true); + t.setFocusable(true); + t.setOnClickListener(cl); + return t; + } + + private void renderList(List a) { + LinearLayout head = new LinearLayout(ctx); + head.setOrientation(LinearLayout.HORIZONTAL); + head.setPadding(dp(10), 0, dp(10), dp(8)); + TextView hName = header(ctx.getString(R.string.k2go_wiki_col_version), "name"); + hName.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + head.addView(hName); + head.addView(header(ctx.getString(R.string.k2go_wiki_col_size), "size")); + container.addView(head); + + List list = new ArrayList<>(a); + Collections.sort(list, (x, y) -> { + int c = "size".equals(sortKey) + ? Double.compare(WikiVariants.sizeGb(langData, x), WikiVariants.sizeGb(langData, y)) + : WikiVariants.label(ctx, x).compareToIgnoreCase(WikiVariants.label(ctx, y)); + return c * sortDir; + }); + for (String k : list) container.addView(variantRow(k, WikiVariants.label(ctx, k), 0)); + } + + private TextView header(String text, String key) { + TextView t = new TextView(ctx); + String arrow = sortKey.equals(key) ? (sortDir > 0 ? " ▲" : " ▼") : ""; + t.setText(text + arrow); + t.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + t.setTextSize(12); + t.setClickable(true); + t.setFocusable(true); + t.setOnClickListener(v -> { + if (sortKey.equals(key)) sortDir = -sortDir; else { sortKey = key; sortDir = 1; } + render(); + }); + return t; + } + + private void renderGrouped(List a) { + String[] covs = {"all", "top1m", "top"}; + String[] dets = {"maxi", "nopic", "mini"}; + for (String cov : covs) { + boolean any = false; + for (String det : dets) if (a.contains(cov + "_" + det)) { any = true; break; } + if (!any) continue; + + LinearLayout hd = new LinearLayout(ctx); + hd.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams hlp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + hlp.topMargin = dp(10); + hlp.bottomMargin = dp(6); + hd.setLayoutParams(hlp); + TextView title = new TextView(ctx); + title.setText(WikiVariants.coverageName(ctx, cov)); + title.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_ink)); + title.setTextSize(14); + TextView desc = new TextView(ctx); + desc.setText(WikiVariants.coverageDesc(ctx, cov)); + desc.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + desc.setTextSize(12); + hd.addView(title); + hd.addView(desc); + container.addView(hd); + + for (String det : dets) { + String k = cov + "_" + det; + if (a.contains(k)) container.addView(variantRow(k, WikiVariants.detailName(ctx, det), dp(10))); + } + } + } + + private View variantRow(final String key, String labelText, int indentPx) { + LinearLayout row = new LinearLayout(ctx); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + lp.leftMargin = indentPx; + lp.bottomMargin = dp(6); + row.setLayoutParams(lp); + row.setBackgroundResource(R.drawable.k2go_card_bg); + row.setPadding(dp(10), dp(8), dp(10), dp(8)); + row.setClickable(true); + row.setFocusable(true); + + final CheckBox cb = new CheckBox(ctx); + cb.setChecked(selected.contains(key)); + cb.setClickable(false); + cb.setFocusable(false); + row.addView(cb); + + TextView name = new TextView(ctx); + LinearLayout.LayoutParams nlp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f); + nlp.leftMargin = dp(8); + name.setLayoutParams(nlp); + name.setText(labelText); + name.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_ink)); + name.setTextSize(14); + row.addView(name); + + TextView size = new TextView(ctx); + size.setText(WikiVariants.gb(WikiVariants.sizeGb(langData, key))); + size.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + size.setTextSize(13); + row.addView(size); + + row.setOnClickListener(v -> { + if (selected.contains(key)) selected.remove(key); else selected.add(key); + cb.setChecked(selected.contains(key)); + if (onChange != null) onChange.changed(); + }); + return row; + } + + private TextView muted(String text) { + TextView t = new TextView(ctx); + t.setText(text); + t.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + t.setTextSize(13); + t.setPadding(dp(2), dp(6), dp(2), dp(6)); + return t; + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java new file mode 100644 index 00000000..c449b614 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/WizardActivity.java @@ -0,0 +1,235 @@ +package org.iiab.controller.redesign; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import org.iiab.controller.R; +import org.iiab.controller.applang.data.AppLocaleController; +import org.iiab.controller.applang.data.ContentLanguage; + +/** + * New first-run onboarding (ADFA-4725): Welcome -> Language -> Permissions -> Set up your library. + * Replaces the legacy SetupActivity in the redesign flow (M3 theme). Notifications / Storage / + * Keep-running only (overlay dropped). Set up your library -> SetupLibraryActivity (Step 1/2). + */ +public class WizardActivity extends AppCompatActivity { + + private int step = 0; + private String langTag = ""; // "" = system + + private TextView title, subtitle, primary; + private View welcome, language, perms, setup, back; + private TextView notifStatus, storageStatus, batteryStatus; + + private final ActivityResultLauncher permResult = + registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), r -> render()); + + @Override + protected void onCreate(Bundle b) { + super.onCreate(b); + setContentView(R.layout.activity_k2go_wizard); + title = findViewById(R.id.wiz_title); + subtitle = findViewById(R.id.wiz_subtitle); + primary = findViewById(R.id.wiz_primary); + welcome = findViewById(R.id.wiz_welcome); + language = findViewById(R.id.wiz_language); + perms = findViewById(R.id.wiz_perms); + setup = findViewById(R.id.wiz_setup); + back = findViewById(R.id.wiz_back); + notifStatus = findViewById(R.id.perm_notif_status); + storageStatus = findViewById(R.id.perm_storage_status); + batteryStatus = findViewById(R.id.perm_battery_status); + + // language selection + findViewById(R.id.lang_system).setOnClickListener(v -> pickLang("")); + findViewById(R.id.lang_en).setOnClickListener(v -> pickLang("en")); + findViewById(R.id.lang_es).setOnClickListener(v -> pickLang("es")); + + // permission requests + findViewById(R.id.perm_notif).setOnClickListener(v -> requestNotif()); + findViewById(R.id.perm_storage).setOnClickListener(v -> requestStorage()); + findViewById(R.id.perm_battery).setOnClickListener(v -> requestBattery()); + + // set-up-library choices + findViewById(R.id.setup_download).setOnClickListener(v -> { + markComplete(); + startActivity(new Intent(this, SetupLibraryActivity.class)); + finish(); + }); + findViewById(R.id.setup_copy).setOnClickListener(v -> { + markComplete(); + startActivity(new Intent(this, LibraryActivity.class)); + finish(); + }); + findViewById(R.id.wiz_skip_for_now).setOnClickListener(v -> { + markComplete(); + startActivity(new Intent(this, LibraryActivity.class)); + finish(); + }); + + primary.setOnClickListener(v -> onPrimary()); + back.setOnClickListener(v -> goBack()); + render(); + } + + private void pickLang(String tag) { + langTag = tag; + ((ImageView) findViewById(R.id.lang_system_radio)).setImageResource(tag.isEmpty() ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + ((ImageView) findViewById(R.id.lang_en_radio)).setImageResource("en".equals(tag) ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + ((ImageView) findViewById(R.id.lang_es_radio)).setImageResource("es".equals(tag) ? R.drawable.ic_radio_on : R.drawable.ic_radio_off); + } + + private void onPrimary() { + if (step == 0) { + step = 1; + } else if (step == 1) { + applyLanguage(); + step = 2; + } else if (step == 2) { + if (allPermsGranted()) step = 3; + } + render(); + } + + private void goBack() { + if (step > 0) { step--; render(); } + } + + @Override + public void onBackPressed() { + if (step > 0) { step--; render(); } + else super.onBackPressed(); + } + + private void render() { + welcome.setVisibility(step == 0 ? View.VISIBLE : View.GONE); + language.setVisibility(step == 1 ? View.VISIBLE : View.GONE); + perms.setVisibility(step == 2 ? View.VISIBLE : View.GONE); + setup.setVisibility(step == 3 ? View.VISIBLE : View.GONE); + title.setVisibility(step == 0 ? View.GONE : View.VISIBLE); + subtitle.setVisibility(step == 0 ? View.GONE : View.VISIBLE); + back.setVisibility(step >= 1 ? View.VISIBLE : View.GONE); + + switch (step) { + case 0: + primary.setVisibility(View.VISIBLE); + primary.setText("Get started"); + enable(primary, true); + break; + case 1: + title.setText("Choose language"); + subtitle.setText("Sets app + content language."); + primary.setVisibility(View.VISIBLE); + primary.setText("Next"); + enable(primary, true); + break; + case 2: + title.setText("Allow a few things"); + subtitle.setText(""); + refreshPermStatuses(); + primary.setVisibility(View.VISIBLE); + primary.setText("Continue"); + enable(primary, allPermsGranted()); + break; + default: + title.setText("Set up your library"); + subtitle.setText("How do you want it?"); + primary.setVisibility(View.GONE); + break; + } + } + + private void enable(TextView b, boolean on) { + b.setEnabled(on); + b.setAlpha(on ? 1f : 0.5f); + } + + // --- language --- + private void applyLanguage() { + AppLocaleController.apply(langTag); + String content = langTag.isEmpty() ? ContentLanguage.systemDefault() : langTag; + prefs().edit().putString("selected_lang_minimal", content).apply(); + } + + // --- permissions --- + private void refreshPermStatuses() { + setStatus(notifStatus, hasNotif()); + setStatus(storageStatus, hasStorage()); + setStatus(batteryStatus, hasBattery()); + } + private void setStatus(TextView t, boolean granted) { + t.setText(granted ? "Granted" : "Allow"); + t.setTextColor(ContextCompat.getColor(this, granted ? R.color.k2go_leaf : R.color.k2go_teal)); + } + private boolean allPermsGranted() { + boolean ok = hasStorage() && hasBattery(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) ok = ok && hasNotif(); + return ok; + } + private boolean hasNotif() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + return NotificationManagerCompat.from(this).areNotificationsEnabled(); + return true; + } + private boolean hasStorage() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) return android.os.Environment.isExternalStorageManager(); + return ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + == android.content.pm.PackageManager.PERMISSION_GRANTED; + } + private boolean hasBattery() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + return pm != null && pm.isIgnoringBatteryOptimizations(getPackageName()); + } + return true; + } + private void requestNotif() { + if (hasNotif()) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Intent i = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + i.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + permResult.launch(i); + } + } + private void requestStorage() { + if (hasStorage()) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + Intent i = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + i.addCategory("android.intent.category.DEFAULT"); + i.setData(Uri.parse("package:" + getPackageName())); + permResult.launch(i); + } catch (Exception e) { + permResult.launch(new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)); + } + } + } + private void requestBattery() { + if (hasBattery()) return; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Intent i = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + i.setData(Uri.parse("package:" + getPackageName())); + permResult.launch(i); + } + } + + private SharedPreferences prefs() { + return getSharedPreferences(getString(R.string.pref_file_internal), MODE_PRIVATE); + } + private void markComplete() { + prefs().edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply(); + } +} diff --git a/controller/app/src/main/res/color/k2go_nav_item.xml b/controller/app/src/main/res/color/k2go_nav_item.xml new file mode 100644 index 00000000..d686dcbe --- /dev/null +++ b/controller/app/src/main/res/color/k2go_nav_item.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/ic_card_book.xml b/controller/app/src/main/res/drawable/ic_card_book.xml new file mode 100644 index 00000000..f4b7c1b2 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_book.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_card_code.xml b/controller/app/src/main/res/drawable/ic_card_code.xml new file mode 100644 index 00000000..11092566 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_code.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_card_courses.xml b/controller/app/src/main/res/drawable/ic_card_courses.xml new file mode 100644 index 00000000..a6301fc0 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_courses.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_card_maps.xml b/controller/app/src/main/res/drawable/ic_card_maps.xml new file mode 100644 index 00000000..df3bbb09 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_maps.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_card_wikipedia.xml b/controller/app/src/main/res/drawable/ic_card_wikipedia.xml new file mode 100644 index 00000000..ea9306a0 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_card_wikipedia.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_clone.xml b/controller/app/src/main/res/drawable/ic_nav_clone.xml new file mode 100644 index 00000000..6b2a1f38 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_clone.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_connect.xml b/controller/app/src/main/res/drawable/ic_nav_connect.xml new file mode 100644 index 00000000..9f02b657 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_connect.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_library.xml b/controller/app/src/main/res/drawable/ic_nav_library.xml new file mode 100644 index 00000000..870da117 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_library.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_nav_settings.xml b/controller/app/src/main/res/drawable/ic_nav_settings.xml new file mode 100644 index 00000000..7c4ba1e0 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_nav_settings.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/ic_radio_off.xml b/controller/app/src/main/res/drawable/ic_radio_off.xml new file mode 100644 index 00000000..df1cc80f --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_radio_off.xml @@ -0,0 +1,3 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_radio_on.xml b/controller/app/src/main/res/drawable/ic_radio_on.xml new file mode 100644 index 00000000..318dd7dd --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_radio_on.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/k2go_card_bg.xml b/controller/app/src/main/res/drawable/k2go_card_bg.xml new file mode 100644 index 00000000..2bf70a1b --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_card_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_chip_bg.xml b/controller/app/src/main/res/drawable/k2go_chip_bg.xml new file mode 100644 index 00000000..71a3cf30 --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_chip_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_dot.xml b/controller/app/src/main/res/drawable/k2go_dot.xml new file mode 100644 index 00000000..cea8990b --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_dot.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_getmore_bg.xml b/controller/app/src/main/res/drawable/k2go_getmore_bg.xml new file mode 100644 index 00000000..765156e0 --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_getmore_bg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_info_circle.xml b/controller/app/src/main/res/drawable/k2go_info_circle.xml new file mode 100644 index 00000000..27f7bf0a --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_info_circle.xml @@ -0,0 +1,4 @@ + + + + diff --git a/controller/app/src/main/res/drawable/k2go_primary_bg.xml b/controller/app/src/main/res/drawable/k2go_primary_bg.xml new file mode 100644 index 00000000..3670bbca --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_primary_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/controller/app/src/main/res/drawable/k2go_turnoff_bg.xml b/controller/app/src/main/res/drawable/k2go_turnoff_bg.xml new file mode 100644 index 00000000..6871427e --- /dev/null +++ b/controller/app/src/main/res/drawable/k2go_turnoff_bg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/controller/app/src/main/res/layout/activity_k2go_setup.xml b/controller/app/src/main/res/layout/activity_k2go_setup.xml new file mode 100644 index 00000000..00d8c69f --- /dev/null +++ b/controller/app/src/main/res/layout/activity_k2go_setup.xml @@ -0,0 +1,6 @@ + + diff --git a/controller/app/src/main/res/layout/activity_k2go_wizard.xml b/controller/app/src/main/res/layout/activity_k2go_wizard.xml new file mode 100644 index 00000000..91d4cc5c --- /dev/null +++ b/controller/app/src/main/res/layout/activity_k2go_wizard.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/activity_library.xml b/controller/app/src/main/res/layout/activity_library.xml new file mode 100644 index 00000000..bbe1086d --- /dev/null +++ b/controller/app/src/main/res/layout/activity_library.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_clone.xml b/controller/app/src/main/res/layout/fragment_k2go_clone.xml new file mode 100644 index 00000000..597d9ae7 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_clone.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_connect.xml b/controller/app/src/main/res/layout/fragment_k2go_connect.xml new file mode 100644 index 00000000..4311a12b --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_connect.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_library.xml b/controller/app/src/main/res/layout/fragment_k2go_library.xml new file mode 100644 index 00000000..e1a73c4c --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_library.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_placeholder.xml b/controller/app/src/main/res/layout/fragment_k2go_placeholder.xml new file mode 100644 index 00000000..7dfcd7d3 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_placeholder.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_settings.xml b/controller/app/src/main/res/layout/fragment_k2go_settings.xml new file mode 100644 index 00000000..ad548b3e --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_settings.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_settings_sub.xml b/controller/app/src/main/res/layout/fragment_k2go_settings_sub.xml new file mode 100644 index 00000000..18d01f70 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_settings_sub.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml new file mode 100644 index 00000000..e29f0ad1 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step1.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml new file mode 100644 index 00000000..93cf3aa1 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2a.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml new file mode 100644 index 00000000..abc28d43 --- /dev/null +++ b/controller/app/src/main/res/layout/fragment_k2go_setup_step2b.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/view_k2go_card.xml b/controller/app/src/main/res/layout/view_k2go_card.xml new file mode 100644 index 00000000..76d9f4c8 --- /dev/null +++ b/controller/app/src/main/res/layout/view_k2go_card.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/layout/view_k2go_edition.xml b/controller/app/src/main/res/layout/view_k2go_edition.xml new file mode 100644 index 00000000..7bd48ad1 --- /dev/null +++ b/controller/app/src/main/res/layout/view_k2go_edition.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/controller/app/src/main/res/menu/bottom_nav_k2go.xml b/controller/app/src/main/res/menu/bottom_nav_k2go.xml new file mode 100644 index 00000000..f1699745 --- /dev/null +++ b/controller/app/src/main/res/menu/bottom_nav_k2go.xml @@ -0,0 +1,7 @@ + +

+ + + + + diff --git a/controller/app/src/main/res/raw/library_animation.json b/controller/app/src/main/res/raw/library_animation.json new file mode 100755 index 00000000..015125c4 --- /dev/null +++ b/controller/app/src/main/res/raw/library_animation.json @@ -0,0 +1 @@ +{"v":"5.12.2","fr":30,"ip":0,"op":210,"w":1080,"h":1920,"nm":"Neighborhood Library","ddd":0,"assets":[],"fonts":{"list":[{"fName":"AtkinsonHyperlegible","fFamily":"Atkinson Hyperlegible","fStyle":"Bold","fWeight":"700","ascent":73,"origin":1,"fPath":"","fClass":""}]},"markers":[{"tm":0,"cm":"A_ENTRY_LOOP","dr":75},{"tm":75,"cm":"B_OPEN_FLIP","dr":30},{"tm":105,"cm":"C_EXIT_LOOP","dr":75},{"tm":180,"cm":"D_CLOSED_FLIP","dr":30}],"layers":[{"ddd":0,"ind":1,"ty":5,"nm":"SIGN_LABEL","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[540,742,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":90,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":195,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[100,100,100]}]}},"ao":0,"t":{"d":{"k":[{"t":0,"s":{"s":44,"f":"AtkinsonHyperlegible","t":"CLOSED","j":2,"tr":12,"lh":53,"ls":0,"fc":[0.078,0.129,0.122]}},{"t":90,"s":{"s":44,"f":"AtkinsonHyperlegible","t":"OPEN","j":2,"tr":12,"lh":53,"ls":0,"fc":[0.078,0.129,0.122]}},{"t":195,"s":{"s":44,"f":"AtkinsonHyperlegible","t":"CLOSED","j":2,"tr":12,"lh":53,"ls":0,"fc":[0.078,0.129,0.122]}}]},"p":{},"m":{"g":1,"a":{"a":0,"k":[0,0]}},"a":[]},"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"SIGN_BOARD","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[540,740,0]},"a":{"a":0,"k":[540,740,0]},"s":{"a":1,"k":[{"t":0,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":90,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[100,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":195,"s":[6,100,100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[246,92]},"p":{"a":0,"k":[540,740]},"r":{"a":0,"k":14},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[216,64]},"p":{"a":0,"k":[540,740]},"r":{"a":0,"k":9},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":2},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"FLY_1","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":111,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":1,"k":[{"t":105,"s":[-20],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":148,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[-20]}]},"p":{"a":1,"k":[{"t":105,"s":[420,1450,0],"i":{"x":[0.3],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":130,"s":[460,1267.5,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[420,1225,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[420,1225,0],"h":1},{"t":180,"s":[420,1450,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"FLY_2","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":111,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[100],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":1,"k":[{"t":105,"s":[16],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":148,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[16]}]},"p":{"a":1,"k":[{"t":105,"s":[648,1452,0],"i":{"x":[0.3],"y":[1]},"o":{"x":[0.6],"y":[0]}},{"t":130,"s":[664,1191,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[600,1070,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":160,"s":[600,1070,0],"h":1},{"t":180,"s":[648,1452,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"BOOK_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]},"p":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":37,"s":[-12,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[7,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0,0,0]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[416,910]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"BOOK_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]},"p":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":37,"s":[10,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[-8,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0,0,0]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[452,1065]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"BOOK_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]},"p":{"a":1,"k":[{"t":0,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":37,"s":[-9,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[10,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[0,0,0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0,0,0]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[720,1220]},"r":{"a":0,"k":5},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"SHELF","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[560,620]},"p":{"a":0,"k":[540,1110]},"r":{"a":0,"k":18},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":9},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[280,965],[800,965]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[280,1120],[800,1120]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[280,1275],[800,1275]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[320,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[352,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[384,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[616,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[648,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[692,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[724,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[756,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[788,910]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[320,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[352,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[384,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[500,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[532,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[680,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[712,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[744,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[776,1065]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[320,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[352,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[384,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[484,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[516,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[26,104]},"p":{"a":0,"k":[548,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[32,104]},"p":{"a":0,"k":[580,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,104]},"p":{"a":0,"k":[656,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[28,104]},"p":{"a":0,"k":[688,1220]},"r":{"a":0,"k":4},"nm":"rc"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"FLOOR_BOOKS","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":112,"s":[95],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":150,"s":[95],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":168,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[682,1452],[720,1434],[758,1452]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[720,1434],[720,1452]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[682,1452],[690,1446]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[758,1452],[750,1446]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[267.7,1456],[300,1440.7],[332.3,1456]],"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[300,1440.7],[300,1456]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[267.7,1456],[274.5,1450.9]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[332.3,1456],[325.5,1450.9]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"LAMP","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[500,450],[580,450]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[540,450],[540,540]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":true,"v":[[504,596],[576,596],[562,540],[518,540]],"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]]}},"nm":"pl"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.984,0.973,0.953]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[540,646],[540,662]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.078,0.129,0.122]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[500,662],[580,662]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[512,662],[516,696]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"},{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[568,662],[564,696]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":4},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"FLOOR","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sh","ind":0,"ks":{"a":0,"k":{"c":false,"v":[[190,1472],[890,1472]],"i":[[0,0],[0,0]],"o":[[0,0],[0,0]]}},"nm":"ln"},{"ty":"st","c":{"a":0,"k":[0.055,0.361,0.388]},"o":{"a":0,"k":100},"w":{"a":0,"k":7},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"GLOW_CORE","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":22,"s":[6],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":50,"s":[58],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[80],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[85],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[66],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[85],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[360,300]},"p":{"a":0,"k":[540,660]},"nm":"el"},{"ty":"st","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"GLOW_ROOM","sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":22,"s":[3],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":50,"s":[30],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":75,"s":[42],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":105,"s":[44],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":142,"s":[30],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":180,"s":[44],"i":{"x":[0.4],"y":[1]},"o":{"x":[0.55],"y":[0]}},{"t":210,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"el","d":1,"s":{"a":0,"k":[780,940]},"p":{"a":0,"k":[540,1000]},"nm":"el"},{"ty":"st","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":2,"lj":2,"ml":4,"bm":0,"nm":"s"},{"ty":"fl","c":{"a":0,"k":[0.914,0.769,0.416]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"f"},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"g"}],"ip":0,"op":210,"st":0,"bm":0}]} \ No newline at end of file diff --git a/controller/app/src/main/res/values-night/colors_k2go.xml b/controller/app/src/main/res/values-night/colors_k2go.xml new file mode 100644 index 00000000..d778cd06 --- /dev/null +++ b/controller/app/src/main/res/values-night/colors_k2go.xml @@ -0,0 +1,14 @@ + + + #FBF8F3 + #0F1A19 + #17302E + #4FB3BA + #2E9E5B + #F2A23C + #F2A23C + #C4462F + #9BA6A3 + #2A3D3B + #0F1A19 + diff --git a/controller/app/src/main/res/values/colors_k2go.xml b/controller/app/src/main/res/values/colors_k2go.xml new file mode 100644 index 00000000..8d03494d --- /dev/null +++ b/controller/app/src/main/res/values/colors_k2go.xml @@ -0,0 +1,15 @@ + + + + #14211F + #FBF8F3 + #EFEBE2 + #0E5C63 + #2E9E5B + #F2A23C + #B0740F + #C4462F + #5C6360 + #D9D4C8 + #FBF8F3 + diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml new file mode 100644 index 00000000..72509441 --- /dev/null +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -0,0 +1,101 @@ + + + + Library + Connect + Clone + Settings + + Get more + 1 System → 2 Content + Allow + App & programs + Available while the app is open. + Back + Books and Maps are included with your edition; Courses is set up after install. + CHOOSE AN EDITION + Content — books, Wikipedia, maps + Copy from a phone + Copy the K2Go app and its library onto another phone + Coverage + Detail + Download library + English + Español + Everything + Get started + Hotspot + Internet download + Just scan — no need to type! + Keep running + Knowledge To Go + Let a nearby device browse your library. + Nearby devices is asked later, only when sharing. + Next + Next: choose content + Next: join the same network, then start. + Notifications + Popular + Recommended + Scan didn\'t work? Use this: + Scan to join my hotspot + Send to a nearby phone + Send + Set up your library + Starting your library… + Storage access + Storage + System default + 1 ✓ System → 2 Content + 1 ✓ System → 2 Content + Text only + The K2Go library on the other phone (if any) is replaced with this one. + These are apps — the programs that open content. You\'ll choose the content itself in Step 2. + They\'re connected — show next › + Total copy + WHAT THEY\'LL RECEIVE + Wi-Fi + Wikipedia (via Kiwix) + With pictures + download progress + projected use + skip battery limits + store & serve content + system + content · 3 GB+ + Receive + A library in your pocket + ‹ Back + Wikipedia + Books + Maps + Courses + Skip + Add + skipped + Coming soon — no content to choose yet. + Offline world base map. Included with the map (fixed for now). + Kiwix presets exist; Kolibri can\'t be preset yet — set up on the device after install. + added after install + Tap a source to Add or Skip it. Skip all = just the system. + Skip for now › + + + List + Grouped + Version + Size + Choose versions + Skip Wikipedia + Pick at least one version to continue. + No versions available for this language. + Everything + Every article + Top 1M + ~1M most-read articles + Popular + Most consulted articles + With pictures + Text only + Summaries + Preview — not functional yet + diff --git a/controller/app/src/main/res/values/themes_k2go.xml b/controller/app/src/main/res/values/themes_k2go.xml new file mode 100644 index 00000000..968b2864 --- /dev/null +++ b/controller/app/src/main/res/values/themes_k2go.xml @@ -0,0 +1,48 @@ + + + + + + + + + + +