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
6 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 −0
android/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"

view file

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+}

view file

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     }

view file

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'

view file

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);

view file

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+}

view file