diff --git a/app/src/main/assets/app.css b/app/src/main/assets/app.css
index 3f98668..a3f5753 100644
--- a/app/src/main/assets/app.css
+++ b/app/src/main/assets/app.css
@@ -21,6 +21,10 @@
* outline / outlineVariant
*/
:root {
+ /* Prevent Android WebView algorithmic dark mode from recoloring
+ pseudo-elements. This page manages its own dark theme. */
+ color-scheme: dark;
+
/* ── M3 Semantic Color Roles (dark scheme, hue 0° defaults) ── */
--md-primary: hsl(0, 70%, 80%); /* tone 80 */
--md-on-primary: hsl(0, 70%, 15%); /* tone 20 */
diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html
index e5d7cbb..590858f 100644
--- a/app/src/main/assets/index.html
+++ b/app/src/main/assets/index.html
@@ -4,6 +4,7 @@
+
LastWave
diff --git a/app/src/main/assets/settings/settings.css b/app/src/main/assets/settings/settings.css
index e579409..bb4e4e6 100644
--- a/app/src/main/assets/settings/settings.css
+++ b/app/src/main/assets/settings/settings.css
@@ -134,6 +134,8 @@
/*
* TOGGLE WRAPPER — fixed 52×52 touch target, centres switch inside.
+ * Track is 52×32; wrapper provides M3 minimum touch area with the
+ * height difference absorbed as vertical padding.
*/
.toggle-wrap {
width: 52px;
@@ -144,44 +146,75 @@
flex-shrink: 0;
}
-/* ── Material You Toggle ──────────────────────────────────── */
+/* ── Material You Toggle Switch ─────────────────────────────── */
-/* OFF track */
+/*
+ * Track 52 × 32 dp (border-box, 2 dp border)
+ * Thumb 20 × 20 dp — fixed size both states, only position changes
+ * Inner content area: 48 × 28 dp
+ *
+ * ON track uses --md-primary-container (deep muted tonal surface from
+ * Android dynamic color) — not raw --md-primary, which is too harsh.
+ * ON thumb uses --md-on-primary-container so it reads cleanly on top.
+ */
+
+/* ── Track ───────────────────────────────────────────────────── */
.toggle {
- width: 44px;
- height: 24px;
+ width: 52px;
+ height: 32px;
border-radius: 999px;
- background: rgba(120, 120, 120, 0.35);
- border: none;
+ background: rgba(255, 255, 255, 0.07);
+ border: 2px solid rgba(255, 255, 255, 0.28);
position: relative;
- transition: background 0.25s ease;
flex-shrink: 0;
cursor: pointer;
box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+ transition:
+ background 0.25s cubic-bezier(0.2, 0, 0, 1),
+ border-color 0.25s cubic-bezier(0.2, 0, 0, 1);
}
-/* ON track — direct var(), NO color-mix() */
+/* ON track — soft tonal container, never harsh primary */
.toggle.active {
- background: var(--md-primary, var(--accent, #4CAF50));
+ background: var(--md-primary-container, rgba(76, 175, 80, 0.30));
+ border-color: transparent;
}
-/* OFF thumb */
+/* ── Thumb ───────────────────────────────────────────────────── */
+/*
+ * Fixed 20 × 20 dp in both states — no size change, no scaling illusion.
+ * top = (28 − 20) / 2 = 4 dp (same both states)
+ * OFF left = 4 dp
+ * ON left = 48 − 20 − 4 = 24 dp
+ * Only `left` and `background` animate — nothing else shifts.
+ */
.toggle::after {
content: '';
position: absolute;
- top: 3px;
- left: 3px;
- width: 18px;
- height: 18px;
border-radius: 50%;
- background: #ffffff;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.30);
- transition: transform 0.25s cubic-bezier(0.34, 1.3, 0.64, 1);
-}
-
-/* ON thumb — slide right */
+ width: 20px;
+ height: 20px;
+ top: 4px;
+ left: 4px;
+ background: #ffffff !important;
+ opacity: 1 !important;
+ filter: none !important;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
+ transition:
+ left 0.25s cubic-bezier(0.2, 0, 0, 1),
+ box-shadow 0.25s ease;
+}
+
+/* ON thumb — slides right only */
.toggle.active::after {
- transform: translateX(20px);
+ left: 24px;
+ background: #ffffff !important;
+ opacity: 1 !important;
+ filter: none !important;
+ box-shadow:
+ 0 2px 6px rgba(0, 0, 0, 0.28),
+ 0 0 0 1px rgba(0, 0, 0, 0.05);
}
/* ── Material You Palette Grid ────────────────────────────── */
diff --git a/app/src/main/java/com/lastwave/app/MainActivity.java b/app/src/main/java/com/lastwave/app/MainActivity.java
index b805b2b..714019d 100644
--- a/app/src/main/java/com/lastwave/app/MainActivity.java
+++ b/app/src/main/java/com/lastwave/app/MainActivity.java
@@ -27,6 +27,8 @@
import androidx.core.view.WindowInsetsCompat;
import androidx.webkit.WebViewAssetLoader;
import androidx.webkit.WebViewClientCompat;
+import androidx.webkit.WebSettingsCompat;
+import androidx.webkit.WebViewFeature;
import java.io.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -119,6 +121,20 @@ protected void onCreate(Bundle savedInstanceState) {
// https://appassets.androidplatform.net. Enabling them would allow
// file:// URLs to make cross-origin requests, creating XSS risk.
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
+
+ // ── Disable WebView algorithmic dark mode ─────────────────────────────
+ // Without this, Android 10+ WebView applies a system-level color
+ // transformation pass to pseudo-elements (::after, ::before) that runs
+ // BELOW the CSS cascade and ignores !important — causing the toggle
+ // thumb and other white pseudo-elements to appear dark/grey in ON state.
+ // API 29-32: setForceDark(OFF)
+ // API 33+ : setAlgorithmicDarkeningAllowed(false)
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
+ WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF);
+ }
+ if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) {
+ WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false);
+ }
// FIX: Removed MIXED_CONTENT_ALWAYS_ALLOW — redundant because
// android:usesCleartextTraffic="false" already blocks HTTP at the OS
// level, and the setting contradicts the manifest's intent.
@@ -174,12 +190,17 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
+ // evaluateJavascript JSON-encodes its return value.
+ // Returning a JS boolean (not a string) means the callback
+ // receives exactly "true" or "false" — no extra quotes.
+ // Using String() would wrap it in a JS string, giving
+ // "\"true\"" — breaking the equality check below.
webView.evaluateJavascript(
- "window._lwHandleBack ? String(window._lwHandleBack()) : 'false'",
+ "typeof window._lwHandleBack === 'function' ? !!window._lwHandleBack() : false",
result -> {
- // result is a JSON string — "true" or "false"
+ // result is the JSON-encoded JS boolean: "true" or "false"
if (!"true".equals(result)) {
- // JS has nothing to go back to — exit normally
+ // JS stack is empty — let Android exit normally
setEnabled(false);
getOnBackPressedDispatcher().onBackPressed();
}
@@ -374,15 +395,33 @@ public void clearSession() {
// ── Wallpaper Colors ──────────────────────────────────────────────────
@JavascriptInterface
public String getWallpaperColors() {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) return "";
try {
- WallpaperManager wm = WallpaperManager.getInstance(MainActivity.this);
- WallpaperColors colors = wm.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
- if (colors == null) return "";
- String primary = colorToHex(colors.getPrimaryColor().toArgb());
- String secondary = colors.getSecondaryColor() != null ? colorToHex(colors.getSecondaryColor().toArgb()) : primary;
- String tertiary = colors.getTertiaryColor() != null ? colorToHex(colors.getTertiaryColor().toArgb()) : secondary;
- return "{\"primary\":\"" + primary + "\",\"secondary\":\"" + secondary + "\",\"tertiary\":\"" + tertiary + "\"}";
+ // Android 12+ (API 31): read the real Monet / Dynamic Color tonal
+ // palette directly from system resource slots. These are exactly what
+ // DynamicColors / dynamicDarkColorScheme() uses — NOT the raw dominant
+ // wallpaper hues from WallpaperManager which are pre-Monet colors.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ int primary = ContextCompat.getColor(MainActivity.this, android.R.color.system_accent1_400);
+ int secondary = ContextCompat.getColor(MainActivity.this, android.R.color.system_accent2_400);
+ int tertiary = ContextCompat.getColor(MainActivity.this, android.R.color.system_accent3_400);
+ return "{\"primary\":\"" + colorToHex(primary)
+ + "\",\"secondary\":\"" + colorToHex(secondary)
+ + "\",\"tertiary\":\"" + colorToHex(tertiary) + "\"}";
+ }
+ // Android 8.1–11 (API 27–30): best available fallback via WallpaperColors.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ WallpaperManager wm = WallpaperManager.getInstance(MainActivity.this);
+ WallpaperColors colors = wm.getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
+ if (colors == null) return "";
+ String primary = colorToHex(colors.getPrimaryColor().toArgb());
+ String secondary = colors.getSecondaryColor() != null
+ ? colorToHex(colors.getSecondaryColor().toArgb()) : primary;
+ String tertiary = colors.getTertiaryColor() != null
+ ? colorToHex(colors.getTertiaryColor().toArgb()) : secondary;
+ return "{\"primary\":\"" + primary + "\",\"secondary\":\"" + secondary
+ + "\",\"tertiary\":\"" + tertiary + "\"}";
+ }
+ return "";
} catch (Exception e) { return ""; }
}