cf2c58c238b2
fold: FoldPlugin.kt + WindowInfoTracker posture, src/shared/fold.ts wrapper, App.tsx wires data-posture + --peach-hinge-height; androidx.window 1.3.0 dep
- time
- 2026-05-01 04:11 UTC · 5 months ago
- machine
- koh2
- parent
- 1a849508944a
↓ peach-browser-cf2c58c2.tar.gzatlas at this save
files changed6 files
android/app/build.gradle+1 −0android/app/src/main/java/com/peachpasswords/app/FoldPlugin.kt+87 −0android/app/src/main/java/com/peachpasswords/app/MainActivity.kt+1 −0android/variables.gradle+1 −0src/popup/App.tsx+12 −0src/shared/fold.ts+51 −0android/app/build.gradle+1 −0
@@ -36,6 +36,7 @@3636 implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"3737 implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"3838 implementation "androidx.biometric:biometric:$androidxBiometricVersion"39+ implementation "androidx.window:window:$androidxWindowVersion"3940 implementation "androidx.security:security-crypto:1.1.0-alpha06"4041 implementation project(':capacitor-android')4142 testImplementation "junit:junit:$junitVersion"android/app/src/main/java/com/peachpasswords/app/FoldPlugin.kt+87 −0
@@ -0,0 +1,87 @@1+package com.peachpasswords.app2+3+import androidx.lifecycle.Lifecycle4+import androidx.lifecycle.LifecycleOwner5+import androidx.lifecycle.lifecycleScope6+import androidx.lifecycle.repeatOnLifecycle7+import androidx.window.layout.FoldingFeature8+import androidx.window.layout.WindowInfoTracker9+import com.getcapacitor.JSObject10+import com.getcapacitor.Plugin11+import com.getcapacitor.PluginCall12+import com.getcapacitor.PluginMethod13+import com.getcapacitor.annotation.CapacitorPlugin14+import kotlinx.coroutines.Job15+import kotlinx.coroutines.launch16+17+/**18+ * Bridges androidx.window's WindowInfoTracker to JS so the WebView can19+ * react to foldable posture (tabletop / book / flat). One collector per20+ * plugin instance — startMonitoring is idempotent.21+ */22+@CapacitorPlugin(name = "Fold")23+class FoldPlugin : Plugin() {24+25+ private var collectorJob: Job? = null26+27+ @PluginMethod28+ fun startMonitoring(call: PluginCall) {29+ val owner = activity as? LifecycleOwner30+ if (owner == null) {31+ call.reject("activity is not a LifecycleOwner")32+ return33+ }34+35+ if (collectorJob?.isActive == true) {36+ call.resolve(JSObject().put("posture", lastPosture))37+ return38+ }39+40+ collectorJob = owner.lifecycleScope.launch {41+ owner.repeatOnLifecycle(Lifecycle.State.STARTED) {42+ WindowInfoTracker.getOrCreate(activity)43+ .windowLayoutInfo(activity)44+ .collect { layoutInfo ->45+ val fold = layoutInfo.displayFeatures46+ .filterIsInstance<FoldingFeature>()47+ .firstOrNull()48+ emit(fold)49+ }50+ }51+ }52+53+ call.resolve(JSObject().put("posture", lastPosture))54+ }55+56+ @PluginMethod57+ fun stopMonitoring(call: PluginCall) {58+ collectorJob?.cancel()59+ collectorJob = null60+ call.resolve()61+ }62+63+ private var lastPosture: String = "flat"64+65+ private fun emit(fold: FoldingFeature?) {66+ val posture = when {67+ fold == null -> "flat"68+ fold.state == FoldingFeature.State.HALF_OPENED &&69+ fold.orientation == FoldingFeature.Orientation.HORIZONTAL -> "tabletop"70+ fold.state == FoldingFeature.State.HALF_OPENED &&71+ fold.orientation == FoldingFeature.Orientation.VERTICAL -> "book"72+ fold.state == FoldingFeature.State.FLAT -> "flat"73+ else -> "unknown"74+ }75+ lastPosture = posture76+77+ val data = JSObject().apply {78+ put("posture", posture)79+ put("hingeTop", fold?.bounds?.top ?: 0)80+ put("hingeBottom", fold?.bounds?.bottom ?: 0)81+ put("hingeLeft", fold?.bounds?.left ?: 0)82+ put("hingeRight", fold?.bounds?.right ?: 0)83+ put("hingeHeight", fold?.bounds?.let { it.bottom - it.top } ?: 0)84+ }85+ notifyListeners("postureChanged", data)86+ }87+}android/app/src/main/java/com/peachpasswords/app/MainActivity.kt+1 −0
@@ -10,6 +10,7 @@1010 registerPlugin(InMemoryVaultPlugin::class.java)1111 registerPlugin(SyncPlugin::class.java)1212 registerPlugin(SyncKeyPlugin::class.java)13+ registerPlugin(FoldPlugin::class.java)1314 super.onCreate(savedInstanceState)1415 VaultBridge.init(this)1516 }android/variables.gradle+1 −0
@@ -10,6 +10,7 @@1010 coreSplashScreenVersion = '1.2.0'1111 androidxBiometricVersion = '1.1.0'1212 androidxWebkitVersion = '1.14.0'13+ androidxWindowVersion = '1.3.0'1314 junitVersion = '4.13.2'1415 androidxJunitVersion = '1.3.0'1516 androidxEspressoCoreVersion = '3.7.0'src/popup/App.tsx+12 −0
@@ -6,6 +6,7 @@66 import { storageGet, storageSet } from "../shared/storage";77 import { sendMessage } from "../shared/bridge";88 import { clearInMemoryVault, publishInMemoryVault } from "../shared/in-memory-vault";9+import { startFoldMonitoring, type PostureChangedEvent } from "../shared/fold";910 import { ACCENT_COLORS } from "../shared/types";1011 import type { AccentColorName, PendingPasskeyRequest, ProFeature, VaultEntry } from "../shared/types";1112 import type { IncomingPairingRequest } from "../shared/sync-types";@@ -383,6 +384,16 @@383384 updateLayoutClass();384385 window.addEventListener("resize", updateLayoutClass);385386 387+ // Foldable posture (Z Fold tabletop / book) — set data-posture and a388+ // CSS variable for hinge height so layouts can pad away from the crease.389+ const stopFold = await startFoldMonitoring((e: PostureChangedEvent) => {390+ document.body.setAttribute("data-posture", e.posture);391+ document.documentElement.style.setProperty(392+ "--peach-hinge-height",393+ `${e.hingeHeight}px`,394+ );395+ });396+386397 const keyboardShow = await Keyboard.addListener("keyboardWillShow", (info) => {387398 document.body.style.paddingBottom = `${info.keyboardHeight}px`;388399 });@@ -417,6 +428,7 @@417428 void keyboardShow.remove();418429 void keyboardHide.remove();419430 void backButton.remove();431+ stopFold();420432 window.removeEventListener("resize", updateLayoutClass);421433 document.removeEventListener("touchstart", handleTouchStart);422434 document.removeEventListener("touchend", handleTouchEnd);src/shared/fold.ts+51 −0
@@ -0,0 +1,51 @@1+import { Capacitor, registerPlugin, type PluginListenerHandle } from "@capacitor/core";2+3+export type FoldPosture = "flat" | "tabletop" | "book" | "unknown";4+5+export interface PostureChangedEvent {6+ posture: FoldPosture;7+ hingeTop: number;8+ hingeBottom: number;9+ hingeLeft: number;10+ hingeRight: number;11+ hingeHeight: number;12+}13+14+interface FoldPlugin {15+ startMonitoring(): Promise<{ posture: FoldPosture }>;16+ stopMonitoring(): Promise<void>;17+ addListener(18+ event: "postureChanged",19+ fn: (e: PostureChangedEvent) => void,20+ ): Promise<PluginListenerHandle> | PluginListenerHandle;21+}22+23+const Fold = registerPlugin<FoldPlugin>("Fold");24+25+export async function startFoldMonitoring(26+ onChange: (e: PostureChangedEvent) => void,27+): Promise<() => void> {28+ if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "android") {29+ return () => {};30+ }31+ let initial: FoldPosture = "flat";32+ try {33+ const r = await Fold.startMonitoring();34+ initial = r.posture;35+ } catch {36+ return () => {};37+ }38+ onChange({39+ posture: initial,40+ hingeTop: 0,41+ hingeBottom: 0,42+ hingeLeft: 0,43+ hingeRight: 0,44+ hingeHeight: 0,45+ });46+ const handle = await Fold.addListener("postureChanged", onChange);47+ return () => {48+ void handle.remove();49+ void Fold.stopMonitoring().catch(() => {});50+ };51+}