Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/src/main/assets/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
1 change: 1 addition & 0 deletions app/src/main/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#0B0B0B" />
<meta name="color-scheme" content="dark" />
<title>LastWave</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
Expand Down
75 changes: 54 additions & 21 deletions app/src/main/assets/settings/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 ────────────────────────────── */
Expand Down
61 changes: 50 additions & 11 deletions app/src/main/java/com/lastwave/app/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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 ""; }
}

Expand Down
Loading