diff --git a/README.md b/README.md index 0dda828..ec00e72 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ android/ Capacitor-generated Gradle project (appId md.zennotes) app/src/main/java/md/zennotes/ MainActivity.java registers native plugins, stashes ACTION_SEND shares DirectUploadPlugin.java streams signed object PUTs on Android 7+ + EdgeSwipePlugin.java Android-only: claims a mid-screen band of the left edge from + the system Back gesture so the edge swipe can open Browse ShareInboxPlugin.java Android ShareInbox (same jsName/contract as iOS) WidgetBridgePlugin.java ZenWidgets (same jsName/contract as iOS): writes the snapshot widgets/ New Note, Recent Notes, Today's Tasks: AppWidgetProviders + diff --git a/android/app/build.gradle b/android/app/build.gradle index 1e2ab0d..9806296 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "md.zennotes" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 25 - versionName "1.1.22" + versionCode 26 + versionName "1.1.23" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/src/main/java/md/zennotes/EdgeSwipePlugin.java b/android/app/src/main/java/md/zennotes/EdgeSwipePlugin.java new file mode 100644 index 0000000..540a803 --- /dev/null +++ b/android/app/src/main/java/md/zennotes/EdgeSwipePlugin.java @@ -0,0 +1,75 @@ +package md.zennotes; + +import android.graphics.Rect; +import android.os.Build; +import android.view.View; + +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +import java.util.Collections; + +/** + * Keeps the shell's left-edge swipe (open Browse) alive under gesture + * navigation. Android claims every swipe that starts at a screen edge as + * Back: the WebView saw touchstart then touchcancel, Browse never opened, and + * the swipe navigated back instead, or left the app from Home (Play review, + * 1.1.21). Excluding a band of the left edge from the Back gesture hands those + * touches to the WebView, as androidx DrawerLayout does for its drawer edge. + * + * The system honours at most 200dp per edge, so the band sits mid-screen, + * where a thumb swipes. Back keeps working above and below it and along the + * whole right edge. The JS shell decides when the band is wanted (phone + * layout, drawer closed), because only it knows the layout override and the + * drawer state. Android-only; iOS has no edge Back gesture to contend with. + */ +@CapacitorPlugin(name = "EdgeSwipe") +public class EdgeSwipePlugin extends Plugin { + + private static final int BAND_WIDTH_DP = 32; + private static final int BAND_HEIGHT_DP = 200; + + private boolean claimed = false; + private View.OnLayoutChangeListener relayout; + + @PluginMethod + public void setLeftEdgeClaimed(PluginCall call) { + boolean claim = Boolean.TRUE.equals(call.getBoolean("claimed", false)); + getActivity().runOnUiThread(() -> { + claimed = claim; + apply(); + call.resolve(); + }); + } + + private void apply() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return; // no gesture navigation, nothing to exclude + } + View webView = getBridge().getWebView(); + if (relayout == null) { + // Rects are in view coordinates: follow rotation and the keyboard. + relayout = (v, l, t, r, b, ol, ot, or, ob) -> exclude(v); + webView.addOnLayoutChangeListener(relayout); + } + exclude(webView); + } + + private void exclude(View view) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return; + } + if (!claimed || view.getHeight() == 0) { + view.setSystemGestureExclusionRects(Collections.emptyList()); + return; + } + float density = view.getResources().getDisplayMetrics().density; + int width = Math.round(BAND_WIDTH_DP * density); + int height = Math.min(Math.round(BAND_HEIGHT_DP * density), view.getHeight()); + int top = (view.getHeight() - height) / 2; + view.setSystemGestureExclusionRects( + Collections.singletonList(new Rect(0, top, width, top + height))); + } +} diff --git a/android/app/src/main/java/md/zennotes/MainActivity.java b/android/app/src/main/java/md/zennotes/MainActivity.java index 1f805e1..ee357d3 100644 --- a/android/app/src/main/java/md/zennotes/MainActivity.java +++ b/android/app/src/main/java/md/zennotes/MainActivity.java @@ -38,6 +38,7 @@ public void onCreate(Bundle savedInstanceState) { registerPlugin(DirectUploadPlugin.class); registerPlugin(WidgetBridgePlugin.class); registerPlugin(ImagePastePlugin.class); + registerPlugin(EdgeSwipePlugin.class); super.onCreate(savedInstanceState); // Cold-start share: the launch intent IS the share. Stash it now; the // WebView drains the inbox after the vault opens (importPendingShares). diff --git a/package-lock.json b/package-lock.json index 3565902..e164e89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zennotes-android", - "version": "1.1.22", + "version": "1.1.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zennotes-android", - "version": "1.1.22", + "version": "1.1.23", "dependencies": { "@aparajita/capacitor-secure-storage": "^8.0.0", "@capacitor/android": "^8.5.2", diff --git a/package.json b/package.json index 0bd6792..d4ae5e5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zennotes-android", "private": true, - "version": "1.1.22", + "version": "1.1.23", "type": "module", "description": "ZenNotes for Android — Capacitor shell over the ZenNotes app core", "homepage": "https://zennotes.org", diff --git a/src/bridge/edge-swipe.ts b/src/bridge/edge-swipe.ts new file mode 100644 index 0000000..82dfee5 --- /dev/null +++ b/src/bridge/edge-swipe.ts @@ -0,0 +1,19 @@ +/** + * Android gesture navigation claims every swipe that starts at a screen edge + * as Back, so the shell's left-edge swipe never reached the WebView. The + * native side excludes a mid-screen band of the left edge from that gesture + * while the shell asks for it (EdgeSwipePlugin.java has the full story). + * Android-only: there is no iOS counterpart and no web implementation, so + * every call is best-effort. + */ +import { registerPlugin } from '@capacitor/core' + +interface EdgeSwipePlugin { + setLeftEdgeClaimed(options: { claimed: boolean }): Promise +} + +const EdgeSwipe = registerPlugin('EdgeSwipe') + +export function setLeftEdgeClaimed(claimed: boolean): void { + void EdgeSwipe.setLeftEdgeClaimed({ claimed }).catch(() => {}) +} diff --git a/src/bridge/mobile-bridge.ts b/src/bridge/mobile-bridge.ts index c834f44..260a664 100644 --- a/src/bridge/mobile-bridge.ts +++ b/src/bridge/mobile-bridge.ts @@ -132,7 +132,7 @@ import { import { folderForRelativePath, posixNormalize, sanitizeNoteTitle } from './vault-core' import { isPhoneViewport } from '../viewport' -let appVersion = '1.1.22' +let appVersion = '1.1.23' export async function loadNativeAppVersion(): Promise { try { diff --git a/src/ui-mobile/MobileDrawer.tsx b/src/ui-mobile/MobileDrawer.tsx index d366a23..23c542d 100644 --- a/src/ui-mobile/MobileDrawer.tsx +++ b/src/ui-mobile/MobileDrawer.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import ReactDOM from 'react-dom/client' import { getShellSnapshot, useShellSnapshot, setNoteSortOrder, type NoteSortOrder } from '@zennotes/app-core/shell' import { getBrowseSnapshot, useBrowseSnapshot, getBrowseDirectory, requestCreateBrowseFolder, - requestRenameBrowseFolder, requestDeleteBrowseDirectory } from '@zennotes/app-core/browse' + requestRenameBrowseFolder, requestRenameBrowseDatabase, requestDeleteBrowseDirectory } from '@zennotes/app-core/browse' import { useWorkspaceSnapshot, openLocalVault, pickLocalVault, refreshRemoteProfiles, connectRemoteWorkspace, connectRemoteProfile, changeRemoteVaultPath, deleteRemoteProfile } from '@zennotes/app-core/workspace' import { openNote, openAppPage } from '@zennotes/app-core/navigation' @@ -893,7 +893,7 @@ function MobileDrawerBody(props: { // Rename/Delete here. Prompts overlay the open drawer (Modal layers above // z-49), so the drawer stays put and its list refreshes in place via the // vault change events. - const [folderMenu, setFolderMenu] = useState<{ subpath: string; name: string; host: ReturnType } | null>(null) + const [folderMenu, setFolderMenu] = useState<{ kind: 'folder' | 'database'; subpath: string; name: string; host: ReturnType } | null>(null) const pinNote = (notePath: string): void => { if (!vaultRoot) return @@ -973,15 +973,13 @@ function MobileDrawerBody(props: { const renameFolderFromDrawer = (subpath: string, _name: string): void => { const host = folderMenu?.host ?? captureMobileWorkspace() + const rename = folderMenu?.kind === 'database' ? requestRenameBrowseDatabase : requestRenameBrowseFolder setFolderMenu(null) - void requestRenameBrowseFolder(host, subpath).catch(reportActionError) + void rename(host, subpath).catch(reportActionError) } const newFolderHere = (): void => { void requestCreateBrowseFolder(captureMobileWorkspace(), path).catch(reportActionError) } - const deleteDatabase = (subpath: string, _title: string): void => { - void requestDeleteBrowseDirectory(captureMobileWorkspace(), subpath).catch(reportActionError) - } const deleteFolder = (subpath: string, _name: string): void => { const host = folderMenu?.host ?? captureMobileWorkspace() void requestDeleteBrowseDirectory(host, subpath).catch(reportActionError) @@ -1134,7 +1132,7 @@ function MobileDrawerBody(props: { + {folderMenu.kind === 'folder' && ( + + )}