experiments in a post-browser web

feat(android): safe area, dark mode, crash fix, icons, editor layout, 16KB alignment

+173 -226
+1 -1
backend/tauri-mobile/dev-android.sh
··· 98 98 echo "[dev] Starting tauri android dev..." 99 99 echo "" 100 100 cd "$SCRIPT_DIR" 101 - npx tauri android dev --target aarch64 101 + npx tauri android dev
+4
backend/tauri-mobile/peek-core/build.rs
··· 1 1 fn main() { 2 + let target = std::env::var("TARGET").unwrap_or_default(); 3 + if target.contains("android") { 4 + println!("cargo:rustc-link-arg=-Wl,-z,max-page-size=16384"); 5 + } 2 6 uniffi::generate_scaffolding("src/peek_core.udl").unwrap(); 3 7 }
+3
backend/tauri-mobile/src-tauri/.cargo/config.toml
··· 1 + # Android 16KB page alignment is handled in build.rs and peek-core/build.rs 2 + # via cargo:rustc-link-arg. This file is intentionally minimal because 3 + # the Tauri CLI sets CARGO_ENCODED_RUSTFLAGS which overrides rustflags here.
+10 -1
backend/tauri-mobile/src-tauri/build.rs
··· 1 1 fn main() { 2 + let target = std::env::var("TARGET").unwrap(); 3 + 2 4 // Compile Objective-C bridge for iOS 3 - let target = std::env::var("TARGET").unwrap(); 4 5 if target.contains("ios") { 5 6 cc::Build::new() 6 7 .file("AppGroupBridge.m") 7 8 .compile("app_group_bridge"); 8 9 9 10 println!("cargo:rustc-link-lib=framework=Foundation"); 11 + } 12 + 13 + // Android 16KB page size alignment (required for Android 15+ / API 35+) 14 + // This ensures LOAD segments in the .so are aligned to 16384 bytes. 15 + // Using cargo:rustc-link-arg here because .cargo/config.toml rustflags 16 + // can be overridden by CARGO_ENCODED_RUSTFLAGS set by the Tauri CLI. 17 + if target.contains("android") { 18 + println!("cargo:rustc-link-arg=-Wl,-z,max-page-size=16384"); 10 19 } 11 20 12 21 tauri_build::build()
+1
backend/tauri-mobile/src-tauri/gen/android/app/src/main/AndroidManifest.xml
··· 7 7 8 8 <application 9 9 android:icon="@mipmap/ic_launcher" 10 + android:roundIcon="@mipmap/ic_launcher_round" 10 11 android:label="@string/app_name" 11 12 android:theme="@style/Theme.peek_save" 12 13 android:usesCleartextTraffic="${usesCleartextTraffic}">
+1 -1
backend/tauri-mobile/src-tauri/gen/android/app/src/main/assets/tauri.conf.json
··· 1 - {"$schema":"https://schema.tauri.app/config/2","productName":"Peek Save","version":"0.1.0","identifier":"com.dietrich.peek-mobile","app":{"windows":[{"label":"main","create":true,"url":"index.html","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Mobile/15E148 Safari/604.1","dragDropEnabled":true,"center":false,"width":800.0,"height":600.0,"resizable":true,"maximizable":true,"minimizable":true,"closable":true,"title":"Peek Save","fullscreen":false,"focus":true,"focusable":true,"transparent":false,"maximized":false,"visible":true,"decorations":true,"alwaysOnBottom":false,"alwaysOnTop":false,"visibleOnAllWorkspaces":false,"contentProtected":false,"skipTaskbar":false,"titleBarStyle":"Visible","hiddenTitle":false,"acceptFirstMouse":false,"shadow":true,"incognito":false,"zoomHotkeysEnabled":false,"browserExtensionsEnabled":false,"useHttpsScheme":false,"javascriptDisabled":false,"allowLinkPreview":true,"disableInputAccessoryView":true,"scrollBarStyle":"default"}],"security":{"freezePrototype":false,"dangerousDisableAssetCspModification":false,"assetProtocol":{"scope":[],"enable":false},"pattern":{"use":"brownfield"},"capabilities":[]},"macOSPrivateApi":false,"withGlobalTauri":false,"enableGTKAppId":false},"build":{"devUrl":"http://192.168.50.69:5188/","frontendDist":"../dist","beforeDevCommand":"npm run dev","beforeBuildCommand":"","removeUnusedCommands":false,"additionalWatchFolders":[]},"bundle":{"active":true,"targets":"all","createUpdaterArtifacts":false,"icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.icns","icons/icon.ico"],"useLocalToolsDir":false,"windows":{"digestAlgorithm":null,"certificateThumbprint":null,"timestampUrl":null,"tsp":false,"webviewInstallMode":{"type":"downloadBootstrapper","silent":true},"allowDowngrades":true,"wix":null,"nsis":null,"signCommand":null},"linux":{"appimage":{"bundleMediaFramework":false,"files":{}},"deb":{"files":{}},"rpm":{"release":"1","epoch":0,"files":{}}},"macOS":{"files":{},"minimumSystemVersion":"10.13","hardenedRuntime":true,"dmg":{"windowSize":{"width":660,"height":400},"appPosition":{"x":180,"y":170},"applicationFolderPosition":{"x":480,"y":170}}},"iOS":{"developmentTeam":"TXZTDLL5LC","minimumSystemVersion":"14.0"},"android":{"minSdkVersion":24,"autoIncrementVersionCode":false}},"plugins":{}} 1 + {"$schema":"https://schema.tauri.app/config/2","productName":"Peek Save","version":"0.1.0","identifier":"com.dietrich.peek-mobile","app":{"windows":[{"label":"main","create":true,"url":"index.html","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Mobile/15E148 Safari/604.1","dragDropEnabled":true,"center":false,"width":800.0,"height":600.0,"resizable":true,"maximizable":true,"minimizable":true,"closable":true,"title":"Peek Save","fullscreen":false,"focus":true,"focusable":true,"transparent":false,"maximized":false,"visible":true,"decorations":true,"alwaysOnBottom":false,"alwaysOnTop":false,"visibleOnAllWorkspaces":false,"contentProtected":false,"skipTaskbar":false,"titleBarStyle":"Visible","hiddenTitle":false,"acceptFirstMouse":false,"shadow":true,"incognito":false,"zoomHotkeysEnabled":false,"browserExtensionsEnabled":false,"useHttpsScheme":false,"javascriptDisabled":false,"allowLinkPreview":true,"disableInputAccessoryView":true,"scrollBarStyle":"default"}],"security":{"freezePrototype":false,"dangerousDisableAssetCspModification":false,"assetProtocol":{"scope":[],"enable":false},"pattern":{"use":"brownfield"},"capabilities":[]},"macOSPrivateApi":false,"withGlobalTauri":false,"enableGTKAppId":false},"build":{"devUrl":"http://192.168.50.69:1420/","frontendDist":"../dist","beforeDevCommand":"npm run dev","beforeBuildCommand":"npm run build","removeUnusedCommands":false,"additionalWatchFolders":[]},"bundle":{"active":true,"targets":"all","createUpdaterArtifacts":false,"icon":["icons/32x32.png","icons/128x128.png","icons/128x128@2x.png","icons/icon.icns","icons/icon.ico"],"useLocalToolsDir":false,"windows":{"digestAlgorithm":null,"certificateThumbprint":null,"timestampUrl":null,"tsp":false,"webviewInstallMode":{"type":"downloadBootstrapper","silent":true},"allowDowngrades":true,"wix":null,"nsis":null,"signCommand":null},"linux":{"appimage":{"bundleMediaFramework":false,"files":{}},"deb":{"files":{}},"rpm":{"release":"1","epoch":0,"files":{}}},"macOS":{"files":{},"minimumSystemVersion":"10.13","hardenedRuntime":true,"dmg":{"windowSize":{"width":660,"height":400},"appPosition":{"x":180,"y":170},"applicationFolderPosition":{"x":480,"y":170}}},"iOS":{"developmentTeam":"TXZTDLL5LC","minimumSystemVersion":"14.0"},"android":{"minSdkVersion":24,"autoIncrementVersionCode":false}},"plugins":{}}
+63 -8
backend/tauri-mobile/src-tauri/gen/android/app/src/main/java/com/dietrich/peek_mobile/MainActivity.kt
··· 1 1 package com.dietrich.peek_mobile 2 2 3 + import android.content.res.Configuration 4 + import android.os.Build 3 5 import android.os.Bundle 4 6 import android.webkit.WebView 5 7 import androidx.activity.enableEdgeToEdge 6 8 import androidx.core.view.ViewCompat 7 9 import androidx.core.view.WindowInsetsCompat 10 + import androidx.webkit.WebSettingsCompat 11 + import androidx.webkit.WebViewFeature 8 12 9 13 class MainActivity : TauriActivity() { 14 + // Cached safe-area JS snippet, updated by insets listener 15 + private var safeAreaJs: String? = null 16 + private var webViewRef: WebView? = null 17 + 10 18 override fun onCreate(savedInstanceState: Bundle?) { 11 - enableEdgeToEdge() 12 19 super.onCreate(savedInstanceState) 20 + enableEdgeToEdge() 21 + } 22 + 23 + private fun isSystemDarkMode(): Boolean { 24 + val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK 25 + return nightMode == Configuration.UI_MODE_NIGHT_YES 26 + } 27 + 28 + private fun buildSafeAreaJs(topDp: Float, bottomDp: Float, leftDp: Float, rightDp: Float): String { 29 + return """ 30 + document.documentElement.style.setProperty('--safe-area-top', '${topDp}px'); 31 + document.documentElement.style.setProperty('--safe-area-bottom', '${bottomDp}px'); 32 + document.documentElement.style.setProperty('--safe-area-left', '${leftDp}px'); 33 + document.documentElement.style.setProperty('--safe-area-right', '${rightDp}px'); 34 + """.trimIndent() 35 + } 36 + 37 + private fun injectDarkModeState(webView: WebView) { 38 + val isDark = isSystemDarkMode() 39 + webView.evaluateJavascript( 40 + "window.__ANDROID_DARK_MODE__ = $isDark;", 41 + null 42 + ) 13 43 } 14 44 15 45 override fun onWebViewCreate(webView: WebView) { 46 + webViewRef = webView 47 + 48 + // Enable WebView dark mode support so prefers-color-scheme media query works. 49 + // On API 33+ (Android 13), use WebView's built-in algorithmic darkening. 50 + // On older APIs, use AndroidX WebSettingsCompat. 51 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 52 + webView.settings.isAlgorithmicDarkeningAllowed = true 53 + } else if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { 54 + WebSettingsCompat.setAlgorithmicDarkeningAllowed(webView.settings, true) 55 + } 56 + 57 + // Inject safe-area and dark mode via a WebChromeClient, which doesn't conflict 58 + // with Tauri's RustWebViewClient (they are independent — WebViewClient vs WebChromeClient). 59 + webView.webChromeClient = object : android.webkit.WebChromeClient() { 60 + override fun onProgressChanged(view: WebView?, newProgress: Int) { 61 + super.onProgressChanged(view, newProgress) 62 + // Inject once the page is mostly loaded 63 + if (newProgress >= 80 && view != null) { 64 + safeAreaJs?.let { view.evaluateJavascript(it, null) } 65 + injectDarkModeState(view) 66 + } 67 + } 68 + } 69 + 16 70 // Android WebView does not expose env(safe-area-inset-*) CSS variables, 17 71 // so we bridge them from the native WindowInsets API as CSS custom properties. 18 72 ViewCompat.setOnApplyWindowInsetsListener(webView) { view, insets -> ··· 22 76 val bottomDp = systemBars.bottom / density 23 77 val leftDp = systemBars.left / density 24 78 val rightDp = systemBars.right / density 25 - val js = """ 26 - document.documentElement.style.setProperty('--android-safe-area-top', '${topDp}px'); 27 - document.documentElement.style.setProperty('--android-safe-area-bottom', '${bottomDp}px'); 28 - document.documentElement.style.setProperty('--android-safe-area-left', '${leftDp}px'); 29 - document.documentElement.style.setProperty('--android-safe-area-right', '${rightDp}px'); 30 - """.trimIndent() 31 - webView.evaluateJavascript(js, null) 79 + safeAreaJs = buildSafeAreaJs(topDp, bottomDp, leftDp, rightDp) 80 + webView.evaluateJavascript(safeAreaJs!!, null) 32 81 insets 33 82 } 34 83 ViewCompat.requestApplyInsets(webView) 84 + } 85 + 86 + override fun onConfigurationChanged(newConfig: Configuration) { 87 + super.onConfigurationChanged(newConfig) 88 + // Re-inject dark mode state when system theme changes 89 + webViewRef?.let { injectDarkModeState(it) } 35 90 } 36 91 }
+4 -24
backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 1 2 <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 - xmlns:aapt="http://schemas.android.com/aapt" 3 3 android:width="108dp" 4 4 android:height="108dp" 5 5 android:viewportWidth="108" 6 6 android:viewportHeight="108"> 7 - <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> 8 - <aapt:attr name="android:fillColor"> 9 - <gradient 10 - android:endX="85.84757" 11 - android:endY="92.4963" 12 - android:startX="42.9492" 13 - android:startY="49.59793" 14 - android:type="linear"> 15 - <item 16 - android:color="#44000000" 17 - android:offset="0.0" /> 18 - <item 19 - android:color="#00000000" 20 - android:offset="1.0" /> 21 - </gradient> 22 - </aapt:attr> 23 - </path> 24 7 <path 25 - android:fillColor="#FFFFFF" 26 - android:fillType="nonZero" 27 - android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" 28 - android:strokeWidth="1" 29 - android:strokeColor="#00000000" /> 30 - </vector> 8 + android:fillColor="#00000000" 9 + android:pathData="M0,0h108v108h-108z" /> 10 + </vector>
+1 -161
backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml
··· 5 5 android:viewportWidth="108" 6 6 android:viewportHeight="108"> 7 7 <path 8 - android:fillColor="#3DDC84" 8 + android:fillColor="#FFFFFF" 9 9 android:pathData="M0,0h108v108h-108z" /> 10 - <path 11 - android:fillColor="#00000000" 12 - android:pathData="M9,0L9,108" 13 - android:strokeWidth="0.8" 14 - android:strokeColor="#33FFFFFF" /> 15 - <path 16 - android:fillColor="#00000000" 17 - android:pathData="M19,0L19,108" 18 - android:strokeWidth="0.8" 19 - android:strokeColor="#33FFFFFF" /> 20 - <path 21 - android:fillColor="#00000000" 22 - android:pathData="M29,0L29,108" 23 - android:strokeWidth="0.8" 24 - android:strokeColor="#33FFFFFF" /> 25 - <path 26 - android:fillColor="#00000000" 27 - android:pathData="M39,0L39,108" 28 - android:strokeWidth="0.8" 29 - android:strokeColor="#33FFFFFF" /> 30 - <path 31 - android:fillColor="#00000000" 32 - android:pathData="M49,0L49,108" 33 - android:strokeWidth="0.8" 34 - android:strokeColor="#33FFFFFF" /> 35 - <path 36 - android:fillColor="#00000000" 37 - android:pathData="M59,0L59,108" 38 - android:strokeWidth="0.8" 39 - android:strokeColor="#33FFFFFF" /> 40 - <path 41 - android:fillColor="#00000000" 42 - android:pathData="M69,0L69,108" 43 - android:strokeWidth="0.8" 44 - android:strokeColor="#33FFFFFF" /> 45 - <path 46 - android:fillColor="#00000000" 47 - android:pathData="M79,0L79,108" 48 - android:strokeWidth="0.8" 49 - android:strokeColor="#33FFFFFF" /> 50 - <path 51 - android:fillColor="#00000000" 52 - android:pathData="M89,0L89,108" 53 - android:strokeWidth="0.8" 54 - android:strokeColor="#33FFFFFF" /> 55 - <path 56 - android:fillColor="#00000000" 57 - android:pathData="M99,0L99,108" 58 - android:strokeWidth="0.8" 59 - android:strokeColor="#33FFFFFF" /> 60 - <path 61 - android:fillColor="#00000000" 62 - android:pathData="M0,9L108,9" 63 - android:strokeWidth="0.8" 64 - android:strokeColor="#33FFFFFF" /> 65 - <path 66 - android:fillColor="#00000000" 67 - android:pathData="M0,19L108,19" 68 - android:strokeWidth="0.8" 69 - android:strokeColor="#33FFFFFF" /> 70 - <path 71 - android:fillColor="#00000000" 72 - android:pathData="M0,29L108,29" 73 - android:strokeWidth="0.8" 74 - android:strokeColor="#33FFFFFF" /> 75 - <path 76 - android:fillColor="#00000000" 77 - android:pathData="M0,39L108,39" 78 - android:strokeWidth="0.8" 79 - android:strokeColor="#33FFFFFF" /> 80 - <path 81 - android:fillColor="#00000000" 82 - android:pathData="M0,49L108,49" 83 - android:strokeWidth="0.8" 84 - android:strokeColor="#33FFFFFF" /> 85 - <path 86 - android:fillColor="#00000000" 87 - android:pathData="M0,59L108,59" 88 - android:strokeWidth="0.8" 89 - android:strokeColor="#33FFFFFF" /> 90 - <path 91 - android:fillColor="#00000000" 92 - android:pathData="M0,69L108,69" 93 - android:strokeWidth="0.8" 94 - android:strokeColor="#33FFFFFF" /> 95 - <path 96 - android:fillColor="#00000000" 97 - android:pathData="M0,79L108,79" 98 - android:strokeWidth="0.8" 99 - android:strokeColor="#33FFFFFF" /> 100 - <path 101 - android:fillColor="#00000000" 102 - android:pathData="M0,89L108,89" 103 - android:strokeWidth="0.8" 104 - android:strokeColor="#33FFFFFF" /> 105 - <path 106 - android:fillColor="#00000000" 107 - android:pathData="M0,99L108,99" 108 - android:strokeWidth="0.8" 109 - android:strokeColor="#33FFFFFF" /> 110 - <path 111 - android:fillColor="#00000000" 112 - android:pathData="M19,29L89,29" 113 - android:strokeWidth="0.8" 114 - android:strokeColor="#33FFFFFF" /> 115 - <path 116 - android:fillColor="#00000000" 117 - android:pathData="M19,39L89,39" 118 - android:strokeWidth="0.8" 119 - android:strokeColor="#33FFFFFF" /> 120 - <path 121 - android:fillColor="#00000000" 122 - android:pathData="M19,49L89,49" 123 - android:strokeWidth="0.8" 124 - android:strokeColor="#33FFFFFF" /> 125 - <path 126 - android:fillColor="#00000000" 127 - android:pathData="M19,59L89,59" 128 - android:strokeWidth="0.8" 129 - android:strokeColor="#33FFFFFF" /> 130 - <path 131 - android:fillColor="#00000000" 132 - android:pathData="M19,69L89,69" 133 - android:strokeWidth="0.8" 134 - android:strokeColor="#33FFFFFF" /> 135 - <path 136 - android:fillColor="#00000000" 137 - android:pathData="M19,79L89,79" 138 - android:strokeWidth="0.8" 139 - android:strokeColor="#33FFFFFF" /> 140 - <path 141 - android:fillColor="#00000000" 142 - android:pathData="M29,19L29,89" 143 - android:strokeWidth="0.8" 144 - android:strokeColor="#33FFFFFF" /> 145 - <path 146 - android:fillColor="#00000000" 147 - android:pathData="M39,19L39,89" 148 - android:strokeWidth="0.8" 149 - android:strokeColor="#33FFFFFF" /> 150 - <path 151 - android:fillColor="#00000000" 152 - android:pathData="M49,19L49,89" 153 - android:strokeWidth="0.8" 154 - android:strokeColor="#33FFFFFF" /> 155 - <path 156 - android:fillColor="#00000000" 157 - android:pathData="M59,19L59,89" 158 - android:strokeWidth="0.8" 159 - android:strokeColor="#33FFFFFF" /> 160 - <path 161 - android:fillColor="#00000000" 162 - android:pathData="M69,19L69,89" 163 - android:strokeWidth="0.8" 164 - android:strokeColor="#33FFFFFF" /> 165 - <path 166 - android:fillColor="#00000000" 167 - android:pathData="M79,19L79,89" 168 - android:strokeWidth="0.8" 169 - android:strokeColor="#33FFFFFF" /> 170 10 </vector>
+5
backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> 4 + <background android:drawable="@color/ic_launcher_background"/> 5 + </adaptive-icon>
+5
backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> 4 + <background android:drawable="@color/ic_launcher_background"/> 5 + </adaptive-icon>
backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png

This is a binary file and will not be displayed.

backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

This is a binary file and will not be displayed.

+4
backend/tauri-mobile/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <resources> 3 + <color name="ic_launcher_background">#fff</color> 4 + </resources>
+2 -2
backend/tauri-mobile/src-tauri/tauri.conf.json
··· 5 5 "identifier": "com.dietrich.peek-mobile", 6 6 "build": { 7 7 "beforeDevCommand": "npm run dev", 8 - "beforeBuildCommand": "", 8 + "beforeBuildCommand": "npm run build", 9 9 "frontendDist": "../dist", 10 - "devUrl": "http://192.168.50.69:5188" 10 + "devUrl": "http://localhost:1420" 11 11 }, 12 12 "app": { 13 13 "windows": [
+36 -9
backend/tauri-mobile/src/App.css
··· 13 13 } 14 14 15 15 :root { 16 - color-scheme: light dark; 16 + color-scheme: light; 17 17 font-family: "ServerMono", monospace; 18 18 font-size: 16px; 19 19 line-height: 1.5; ··· 23 23 -moz-osx-font-smoothing: grayscale; 24 24 } 25 25 26 + body.dark { 27 + color-scheme: dark; 28 + } 29 + 26 30 * { 27 31 box-sizing: border-box; 28 32 margin: 0; ··· 33 37 input, textarea { 34 38 -webkit-user-select: text; 35 39 user-select: text; 40 + color: inherit; 36 41 } 37 42 38 43 html, body { ··· 122 127 header { 123 128 background: #ffffff; 124 129 padding: 0.25rem 0.75rem; 125 - /* Android injects --android-safe-area-top via MainActivity; iOS uses env() */ 126 - padding-top: calc(var(--android-safe-area-top, env(safe-area-inset-top, 0px)) + 0.4rem); 130 + /* Safe area: --safe-area-top is set by JS on mount (env() for iOS, Kotlin bridge for Android). 131 + Fallback 40px ensures header clears notch/status bar even before JS runs. */ 132 + padding-top: calc(var(--safe-area-top, 40px) + 0.4rem); 127 133 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 128 134 position: sticky; 129 135 top: 0; ··· 1157 1163 -webkit-overflow-scrolling: touch; 1158 1164 overscroll-behavior-y: contain; 1159 1165 padding: 1rem; 1160 - padding-bottom: calc(var(--android-safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 1rem); 1166 + padding-bottom: calc(var(--safe-area-bottom, 0px) + 1rem); 1161 1167 max-width: 600px; 1162 1168 width: 100%; 1163 1169 margin: 0 auto; ··· 2484 2490 top: 0; 2485 2491 left: 0; 2486 2492 right: 0; 2487 - bottom: 0; 2493 + /* height is set dynamically via visualViewport — no bottom: 0 */ 2494 + height: 100%; 2488 2495 background: rgba(0, 0, 0, 0.4); 2489 2496 z-index: 1000; 2490 2497 display: flex; 2491 2498 flex-direction: column; 2492 2499 align-items: center; 2493 2500 padding: 0.5rem; 2494 - padding-top: var(--android-safe-area-top, env(safe-area-inset-top, 4px)); 2501 + padding-top: var(--safe-area-top, 40px); 2495 2502 /* Ensure safe area padding at bottom when keyboard is hidden */ 2496 - padding-bottom: max(var(--android-safe-area-bottom, env(safe-area-inset-bottom, 0px)), 0.5rem); 2503 + padding-bottom: max(var(--safe-area-bottom, 0px), 0.5rem); 2497 2504 /* Prevent iOS from scrolling the viewport when keyboard appears */ 2498 2505 overflow: hidden; 2499 2506 overscroll-behavior: contain; 2507 + box-sizing: border-box; 2508 + } 2509 + 2510 + /* When keyboard is open, no bottom safe-area padding needed — keyboard covers it */ 2511 + .edit-overlay.keyboard-open { 2512 + padding-bottom: 4px; 2500 2513 } 2501 2514 2502 2515 .edit-overlay.transition-padding { ··· 2547 2560 font-family: inherit; 2548 2561 background: transparent; 2549 2562 box-sizing: border-box; 2563 + color: #0f0f0f; 2550 2564 } 2551 2565 2552 2566 .expandable-card-input:focus { ··· 2731 2745 flex-direction: column; 2732 2746 flex: 1 1 auto; 2733 2747 min-height: 0; 2748 + /* Cap textarea at half the card so tags + buttons stay visible */ 2749 + max-height: 50%; 2734 2750 } 2735 2751 2736 2752 .resizable-input-textarea { ··· 2746 2762 min-height: 0; 2747 2763 box-sizing: border-box; 2748 2764 background: transparent; 2765 + color: #0f0f0f; 2749 2766 } 2750 2767 2751 2768 .resizable-input-textarea:focus { ··· 2815 2832 max-height: 140px; 2816 2833 } 2817 2834 2835 + /* When keyboard is open, limit tags to ~3 rows and scroll overflow. 2836 + Each tag row is ~32px + gap, so 3 rows ≈ 108px plus input row. */ 2837 + .keyboard-open .editor-tags-section { 2838 + flex: 0 1 auto; 2839 + /* ~3 rows of tags (32px each) + tag input row (~44px) + padding */ 2840 + max-height: 152px; 2841 + overflow-y: auto; 2842 + } 2843 + 2818 2844 /* Editor Buttons - always visible at bottom */ 2819 2845 .editor-buttons { 2820 2846 flex-shrink: 0; ··· 2895 2921 border-radius: 12px 12px 0 0; 2896 2922 resize: none; 2897 2923 min-height: 0; 2924 + color: #0f0f0f; 2898 2925 } 2899 2926 2900 2927 .text-editor-drag-handle { ··· 3166 3193 /* Toast notifications */ 3167 3194 .toast { 3168 3195 position: fixed; 3169 - bottom: calc(var(--android-safe-area-bottom, env(safe-area-inset-bottom, 0px)) + 80px); 3196 + bottom: calc(var(--safe-area-bottom, 0px) + 80px); 3170 3197 left: 50%; 3171 3198 transform: translateX(-50%); 3172 3199 padding: 0.75rem 1.25rem; ··· 3575 3602 display: flex; 3576 3603 align-items: center; 3577 3604 justify-content: center; 3578 - padding: var(--android-safe-area-top, env(safe-area-inset-top)) env(safe-area-inset-right) var(--android-safe-area-bottom, env(safe-area-inset-bottom)) env(safe-area-inset-left); 3605 + padding: var(--safe-area-top, 0px) var(--safe-area-right, 0px) var(--safe-area-bottom, 0px) var(--safe-area-left, 0px); 3579 3606 } 3580 3607 3581 3608 .image-fullscreen-img {
+33 -19
backend/tauri-mobile/src/App.tsx
··· 173 173 const SWIPE_THRESHOLD = 80; 174 174 175 175 const EditorOverlay: React.FC<EditorOverlayProps> = ({ children, onDismiss, keyboardHeight, className = '' }) => { 176 - // Use actual keyboard height when available; when keyboard is hidden, only pad for safe area 177 - const effectiveKeyboardPadding = keyboardHeight > 0 ? keyboardHeight + 4 : 0; 176 + const keyboardOpen = keyboardHeight > 0; 177 + 178 + // Track visual viewport height so the overlay never extends behind the keyboard. 179 + // visualViewport.height shrinks when the keyboard opens — we use it directly. 180 + const [viewportHeight, setViewportHeight] = useState(() => 181 + window.visualViewport ? window.visualViewport.height : window.innerHeight 182 + ); 183 + 184 + useEffect(() => { 185 + const vv = window.visualViewport; 186 + if (!vv) return; 187 + const onResize = () => setViewportHeight(vv.height); 188 + vv.addEventListener('resize', onResize); 189 + return () => vv.removeEventListener('resize', onResize); 190 + }, []); 178 191 179 192 // Swipe-down-to-close state 180 193 const [swipeOffset, setSwipeOffset] = useState(0); ··· 199 212 return () => viewport.removeEventListener('scroll', handleScroll); 200 213 }, []); 201 214 202 - // On Android, the Kotlin bridge injects --android-safe-area-top via evaluateJavascript, 203 - // but it may fire before the page DOM is ready (race condition). Set a sensible default 204 - // so the header isn't behind the status bar while waiting for the native bridge. 205 - // The Kotlin bridge will overwrite this with the exact value once it fires. 215 + // Set --safe-area-top CSS variable for status bar / notch clearance. 216 + // On iOS: read from env(safe-area-inset-top) via a CSS trick. 217 + // On Android: Kotlin bridge overwrites with real WindowInsets value. 218 + // Default 40px ensures header clears notch even if bridge is slow. 206 219 useEffect(() => { 207 - const isAndroid = /android/i.test(navigator.userAgent); 208 - if (isAndroid) { 209 - const root = document.documentElement; 210 - // Only set default if the Kotlin bridge hasn't already provided a value 211 - if (!root.style.getPropertyValue('--android-safe-area-top')) { 212 - root.style.setProperty('--android-safe-area-top', '24px'); 213 - } 220 + const root = document.documentElement; 221 + if (!root.style.getPropertyValue('--safe-area-top')) { 222 + root.style.setProperty('--safe-area-top', '40px'); 214 223 } 215 224 }, []); 216 225 ··· 239 248 240 249 return ( 241 250 <div 242 - className={`edit-overlay ${className}`} 243 - style={effectiveKeyboardPadding > 0 ? { paddingBottom: `${effectiveKeyboardPadding}px` } : undefined} 251 + className={`edit-overlay ${className}${keyboardOpen ? ' keyboard-open' : ''}`} 252 + style={{ height: `${viewportHeight}px` }} 244 253 onClick={(e) => e.target === e.currentTarget && onDismiss()} 245 254 > 246 255 <div ··· 930 939 } 931 940 }, []); 932 941 933 - // Detect system dark mode via native iOS API 942 + // Detect system dark mode via native API (iOS) or Android bridge 934 943 useEffect(() => { 935 944 const checkDarkMode = async () => { 936 945 try { 937 946 const nativeDark = await invoke<boolean>("is_dark_mode"); 938 947 setIsDark(nativeDark); 939 948 } catch (error) { 940 - // Fallback to JS media query 941 - const jsMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); 942 - setIsDark(jsMediaQuery.matches); 949 + // Check Android-injected global (set by MainActivity.kt) 950 + if (typeof (window as any).__ANDROID_DARK_MODE__ === "boolean") { 951 + setIsDark((window as any).__ANDROID_DARK_MODE__); 952 + } else { 953 + // Fallback to JS media query 954 + const jsMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); 955 + setIsDark(jsMediaQuery.matches); 956 + } 943 957 } 944 958 }; 945 959