From 7907482910608a97398b1304cf857e10369761dc Mon Sep 17 00:00:00 2001 From: careck Date: Tue, 2 Jun 2026 17:40:52 +1000 Subject: [PATCH 1/5] feat: mobile note move actions via context menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Move Up, Move Down, Indent (move into sibling above), and Outdent (move out of parent) to the long-press context menu on mobile/tablet. Replaces drag-and-drop which isn't available on touch devices. Also fixes: Android build identifier (com.2pisoftware → com.twopisoftware), #[cfg(desktop)] gate on manage_scripts_menu_item, and android-dev.sh --device/--emulator flags. --- krillnotes-desktop/android-dev.sh | 27 ++++++++++- krillnotes-desktop/src-tauri/src/lib.rs | 2 + krillnotes-desktop/src-tauri/tauri.conf.json | 2 +- .../src/components/ContextMenu.tsx | 27 +++++++++++ .../src/components/WorkspaceView.tsx | 47 +++++++++++++++++++ krillnotes-desktop/src/i18n/locales/de.json | 6 ++- krillnotes-desktop/src/i18n/locales/en.json | 6 ++- krillnotes-desktop/src/i18n/locales/es.json | 6 ++- krillnotes-desktop/src/i18n/locales/fr.json | 6 ++- krillnotes-desktop/src/i18n/locales/ja.json | 6 ++- krillnotes-desktop/src/i18n/locales/ko.json | 6 ++- krillnotes-desktop/src/i18n/locales/zh.json | 6 ++- 12 files changed, 137 insertions(+), 10 deletions(-) diff --git a/krillnotes-desktop/android-dev.sh b/krillnotes-desktop/android-dev.sh index 337893fe..2d81dd40 100755 --- a/krillnotes-desktop/android-dev.sh +++ b/krillnotes-desktop/android-dev.sh @@ -1,6 +1,8 @@ #!/bin/bash -# Launch Android emulator dev build -# Usage: ./android-dev.sh +# Launch Android dev build on emulator or physical device +# Usage: ./android-dev.sh # auto-detect (prefers running emulator) +# ./android-dev.sh --device # target USB-connected physical device +# ./android-dev.sh --emulator # target emulator only # Kill any lingering Vite dev server lsof -ti:1420 | xargs kill -9 2>/dev/null @@ -14,4 +16,25 @@ export NDK_HOME="$ANDROID_HOME/ndk/26.3.11579264" export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" export PATH="$JAVA_HOME/bin:$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin:$ANDROID_HOME/platform-tools:$PATH" +case "${1:-}" in + --device) + if ! adb devices | grep -qw "device$"; then + echo "❌ No physical device found. Check USB connection and USB debugging." >&2 + exit 1 + fi + SERIAL=$(adb devices | awk '/\tdevice$/ {print $1; exit}') + echo "📱 Targeting physical device: $SERIAL" + ;; + --emulator) + if ! adb devices | grep -q "^emulator-"; then + echo "❌ No emulator found. Start one from Android Studio first." >&2 + exit 1 + fi + echo "📱 Targeting emulator" + ;; + *) + echo "📱 Auto-detecting target (use --device or --emulator to override)" + ;; +esac + npm run tauri android dev diff --git a/krillnotes-desktop/src-tauri/src/lib.rs b/krillnotes-desktop/src-tauri/src/lib.rs index f5dc2bd0..7a7b87f9 100644 --- a/krillnotes-desktop/src-tauri/src/lib.rs +++ b/krillnotes-desktop/src-tauri/src/lib.rs @@ -93,6 +93,7 @@ pub struct AppState { #[cfg(desktop)] pub export_menu_item: Arc>>>, /// Handle to the "Manage Scripts" menu item, toggled based on ownership. + #[cfg(desktop)] pub manage_scripts_menu_item: Arc>>>, /// Window labels that have been approved for closing by the frontend. /// When a label is in this set, the next `CloseRequested` event for @@ -222,6 +223,7 @@ pub fn run() { workspace_menu_items: Arc::new(Mutex::new(HashMap::new())), #[cfg(desktop)] export_menu_item: Arc::new(Mutex::new(None)), + #[cfg(desktop)] manage_scripts_menu_item: Arc::new(Mutex::new(None)), closing_windows: Arc::new(Mutex::new(HashSet::new())), }) diff --git a/krillnotes-desktop/src-tauri/tauri.conf.json b/krillnotes-desktop/src-tauri/tauri.conf.json index 9da6a76c..4ae2c57c 100644 --- a/krillnotes-desktop/src-tauri/tauri.conf.json +++ b/krillnotes-desktop/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Krillnotes", "version": "1.1.1", - "identifier": "com.2pisoftware.krillnotes", + "identifier": "com.twopisoftware.krillnotes", "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://localhost:1420", diff --git a/krillnotes-desktop/src/components/ContextMenu.tsx b/krillnotes-desktop/src/components/ContextMenu.tsx index 7cee352d..5fe5ce0d 100644 --- a/krillnotes-desktop/src/components/ContextMenu.tsx +++ b/krillnotes-desktop/src/components/ContextMenu.tsx @@ -8,6 +8,7 @@ import { useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { useLayout } from '../hooks/useLayout'; +import type { Note, SchemaInfo } from '../types'; interface ContextMenuProps { x: number; @@ -31,6 +32,12 @@ interface ContextMenuProps { onShareSubtree?: (noteId: string) => void; onDelete: () => void; onClose: () => void; + notes?: Note[]; + schemas?: Record; + onMoveUp?: (noteId: string) => void; + onMoveDown?: (noteId: string) => void; + onIndent?: (noteId: string) => void; + onOutdent?: (noteId: string) => void; } function ContextMenu({ @@ -39,6 +46,7 @@ function ContextMenu({ onAddChild, onAddSibling, onAddRoot, onEdit, onCopy, onPasteAsChild, onPasteAsSibling, onTreeAction, onInviteToSubtree, onShareSubtree, onDelete, onClose, + notes, schemas, onMoveUp, onMoveDown, onIndent, onOutdent, }: ContextMenuProps) { const { t } = useTranslation(); const layout = useLayout(); @@ -72,6 +80,16 @@ function ContextMenu({ const canAddSibling = canWrite && !(isRootNote && isRootOwner === false); const itemClass = isPhone ? 'py-3 px-4 text-base' : 'py-1.5 px-3 text-sm'; + const showMoveActions = layout !== 'desktop' && canWrite && !!noteId && !!notes && !!onMoveUp; + const moveNote = showMoveActions ? notes!.find(n => n.id === noteId) : null; + const moveSiblings = moveNote ? notes!.filter(n => n.parentId === moveNote.parentId).sort((a, b) => a.position - b.position) : []; + const moveIdx = moveNote ? moveSiblings.findIndex(n => n.id === noteId) : -1; + const canMoveUp = moveIdx > 0; + const canMoveDown = moveIdx >= 0 && moveIdx < moveSiblings.length - 1; + const siblingAbove = moveIdx > 0 ? moveSiblings[moveIdx - 1] : null; + const canIndentNote = siblingAbove != null && !(schemas?.[siblingAbove.schema ?? '']?.isLeaf); + const canOutdentNote = !!moveNote?.parentId; + return createPortal( <> {isPhone &&
} @@ -145,6 +163,15 @@ function ContextMenu({ > {t('notes.pasteAsSibling')} + {showMoveActions && noteId && onMoveUp && onMoveDown && onIndent && onOutdent && ( + <> +
+ + + + + + )} {treeActions.length > 0 && ( <>
diff --git a/krillnotes-desktop/src/components/WorkspaceView.tsx b/krillnotes-desktop/src/components/WorkspaceView.tsx index f94d839a..7c2b1499 100644 --- a/krillnotes-desktop/src/components/WorkspaceView.tsx +++ b/krillnotes-desktop/src/components/WorkspaceView.tsx @@ -484,6 +484,47 @@ function WorkspaceView({ workspaceInfo, onOpenWorkspacePeers, sharingIndicatorMo } }; + const handleMoveUp = async (noteId: string) => { + const note = notes.find(n => n.id === noteId); + if (!note) return; + const siblings = notes.filter(n => n.parentId === note.parentId).sort((a, b) => a.position - b.position); + const idx = siblings.findIndex(n => n.id === noteId); + if (idx <= 0) return; + await handleMoveNote(noteId, note.parentId, siblings[idx - 1].position); + }; + + const handleMoveDown = async (noteId: string) => { + const note = notes.find(n => n.id === noteId); + if (!note) return; + const siblings = notes.filter(n => n.parentId === note.parentId).sort((a, b) => a.position - b.position); + const idx = siblings.findIndex(n => n.id === noteId); + if (idx < 0 || idx >= siblings.length - 1) return; + await handleMoveNote(noteId, note.parentId, siblings[idx + 1].position); + }; + + const handleIndent = async (noteId: string) => { + const note = notes.find(n => n.id === noteId); + if (!note) return; + const siblings = notes.filter(n => n.parentId === note.parentId).sort((a, b) => a.position - b.position); + const idx = siblings.findIndex(n => n.id === noteId); + if (idx <= 0) return; + const newParent = siblings[idx - 1]; + const newParentSchema = schemas[newParent.schema ?? '']; + if (newParentSchema?.isLeaf) return; + const children = notes.filter(n => n.parentId === newParent.id); + await handleMoveNote(noteId, newParent.id, children.length); + }; + + const handleOutdent = async (noteId: string) => { + const note = notes.find(n => n.id === noteId); + if (!note || !note.parentId) return; + const parent = notes.find(n => n.id === note.parentId); + if (!parent) return; + const parentSiblings = notes.filter(n => n.parentId === parent.parentId).sort((a, b) => a.position - b.position); + const parentIdx = parentSiblings.findIndex(n => n.id === parent.id); + await handleMoveNote(noteId, parent.parentId, parentIdx + 1); + }; + const handleNoteCreated = async (noteId: string) => { const fetchedNotes = await loadNotes(); await loadPermissionState(); @@ -788,6 +829,12 @@ function WorkspaceView({ workspaceInfo, onOpenWorkspacePeers, sharingIndicatorMo onShareSubtree={handleShareSubtree} onDelete={() => contextMenu.noteId && handleContextDelete(contextMenu.noteId)} onClose={() => setContextMenu(null)} + notes={notes} + schemas={schemas} + onMoveUp={(id) => { handleMoveUp(id); setContextMenu(null); }} + onMoveDown={(id) => { handleMoveDown(id); setContextMenu(null); }} + onIndent={(id) => { handleIndent(id); setContextMenu(null); }} + onOutdent={(id) => { handleOutdent(id); setContextMenu(null); }} /> )} diff --git a/krillnotes-desktop/src/i18n/locales/de.json b/krillnotes-desktop/src/i18n/locales/de.json index 0f118711..90be2505 100644 --- a/krillnotes-desktop/src/i18n/locales/de.json +++ b/krillnotes-desktop/src/i18n/locales/de.json @@ -602,7 +602,11 @@ "contextMenu": { "inviteToSubtree": "Zu diesem Teilbaum einladen…", "shareSubtree": "Teilbaum teilen…", - "noAccess": "Kein Zugang" + "noAccess": "Kein Zugang", + "moveUp": "Nach oben", + "moveDown": "Nach unten", + "indent": "Einrücken", + "outdent": "Ausrücken" }, "invite": { "scope": "Teilbaum-Bereich", diff --git a/krillnotes-desktop/src/i18n/locales/en.json b/krillnotes-desktop/src/i18n/locales/en.json index 656f233c..d1d1c259 100644 --- a/krillnotes-desktop/src/i18n/locales/en.json +++ b/krillnotes-desktop/src/i18n/locales/en.json @@ -602,7 +602,11 @@ "contextMenu": { "inviteToSubtree": "Invite to this subtree…", "shareSubtree": "Share subtree...", - "noAccess": "No access" + "noAccess": "No access", + "moveUp": "Move up", + "moveDown": "Move down", + "indent": "Move into above", + "outdent": "Move out of parent" }, "invite": { "scope": "Subtree scope", diff --git a/krillnotes-desktop/src/i18n/locales/es.json b/krillnotes-desktop/src/i18n/locales/es.json index 4af33894..12139ff3 100644 --- a/krillnotes-desktop/src/i18n/locales/es.json +++ b/krillnotes-desktop/src/i18n/locales/es.json @@ -602,7 +602,11 @@ "contextMenu": { "inviteToSubtree": "Invitar a este subárbol…", "shareSubtree": "Compartir subárbol…", - "noAccess": "Sin acceso" + "noAccess": "Sin acceso", + "moveUp": "Mover arriba", + "moveDown": "Mover abajo", + "indent": "Mover dentro", + "outdent": "Mover fuera" }, "invite": { "scope": "Alcance del subárbol", diff --git a/krillnotes-desktop/src/i18n/locales/fr.json b/krillnotes-desktop/src/i18n/locales/fr.json index da83d83c..cab36392 100644 --- a/krillnotes-desktop/src/i18n/locales/fr.json +++ b/krillnotes-desktop/src/i18n/locales/fr.json @@ -602,7 +602,11 @@ "contextMenu": { "inviteToSubtree": "Inviter à ce sous-arbre…", "shareSubtree": "Partager le sous-arbre…", - "noAccess": "Pas d'accès" + "noAccess": "Pas d'accès", + "moveUp": "Déplacer vers le haut", + "moveDown": "Déplacer vers le bas", + "indent": "Déplacer à l'intérieur", + "outdent": "Déplacer à l'extérieur" }, "invite": { "scope": "Portée du sous-arbre", diff --git a/krillnotes-desktop/src/i18n/locales/ja.json b/krillnotes-desktop/src/i18n/locales/ja.json index 1fcddf74..12b8a183 100644 --- a/krillnotes-desktop/src/i18n/locales/ja.json +++ b/krillnotes-desktop/src/i18n/locales/ja.json @@ -602,7 +602,11 @@ "contextMenu": { "inviteToSubtree": "このサブツリーに招待…", "shareSubtree": "サブツリーを共有…", - "noAccess": "アクセス権なし" + "noAccess": "アクセス権なし", + "moveUp": "上に移動", + "moveDown": "下に移動", + "indent": "内側に移動", + "outdent": "外側に移動" }, "invite": { "scope": "サブツリーの範囲", diff --git a/krillnotes-desktop/src/i18n/locales/ko.json b/krillnotes-desktop/src/i18n/locales/ko.json index 740ade0b..5a7c3020 100644 --- a/krillnotes-desktop/src/i18n/locales/ko.json +++ b/krillnotes-desktop/src/i18n/locales/ko.json @@ -602,7 +602,11 @@ "contextMenu": { "inviteToSubtree": "이 하위 트리에 초대…", "shareSubtree": "하위 트리 공유...", - "noAccess": "접근 불가" + "noAccess": "접근 불가", + "moveUp": "위로 이동", + "moveDown": "아래로 이동", + "indent": "안으로 이동", + "outdent": "밖으로 이동" }, "invite": { "scope": "하위 트리 범위", diff --git a/krillnotes-desktop/src/i18n/locales/zh.json b/krillnotes-desktop/src/i18n/locales/zh.json index 778391c1..f8d0002c 100644 --- a/krillnotes-desktop/src/i18n/locales/zh.json +++ b/krillnotes-desktop/src/i18n/locales/zh.json @@ -602,7 +602,11 @@ "contextMenu": { "inviteToSubtree": "邀请到此子树…", "shareSubtree": "共享子树…", - "noAccess": "无权访问" + "noAccess": "无权访问", + "moveUp": "上移", + "moveDown": "下移", + "indent": "移入", + "outdent": "移出" }, "invite": { "scope": "子树范围", From 4da3c3134f58b2128107cce15190616685c69312 Mon Sep 17 00:00:00 2001 From: careck Date: Tue, 2 Jun 2026 17:46:07 +1000 Subject: [PATCH 2/5] fix: increase initial window height to fit launch screen --- krillnotes-desktop/src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/krillnotes-desktop/src-tauri/tauri.conf.json b/krillnotes-desktop/src-tauri/tauri.conf.json index 4ae2c57c..45861b68 100644 --- a/krillnotes-desktop/src-tauri/tauri.conf.json +++ b/krillnotes-desktop/src-tauri/tauri.conf.json @@ -14,7 +14,7 @@ { "title": "Krillnotes", "width": 800, - "height": 600, + "height": 700, "dragDropEnabled": false } ], From 2fa1a27262b9b7d7484f8b5f6ab656ce22215325 Mon Sep 17 00:00:00 2001 From: careck Date: Tue, 2 Jun 2026 17:48:36 +1000 Subject: [PATCH 3/5] fix: use template literals for context menu item classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four buttons (Copy Note, Tree Actions, Invite, Share) used regular quotes instead of backticks, so itemClass wasn't interpolated — causing inconsistent padding and font sizes. --- krillnotes-desktop/src/components/ContextMenu.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/krillnotes-desktop/src/components/ContextMenu.tsx b/krillnotes-desktop/src/components/ContextMenu.tsx index 5fe5ce0d..b965d77f 100644 --- a/krillnotes-desktop/src/components/ContextMenu.tsx +++ b/krillnotes-desktop/src/components/ContextMenu.tsx @@ -146,7 +146,7 @@ function ContextMenu({ {t('common.edit')} From bc8e62d3f2ca9f95618c9829e860507fc5465c28 Mon Sep 17 00:00:00 2001 From: careck Date: Tue, 2 Jun 2026 17:51:30 +1000 Subject: [PATCH 4/5] fix: remove default focus outline from tree container Add outline-none to suppress the browser's default black border on the tabIndex={0} tree div. The focus-visible ring still shows on keyboard navigation. --- krillnotes-desktop/src/components/TreeView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/krillnotes-desktop/src/components/TreeView.tsx b/krillnotes-desktop/src/components/TreeView.tsx index 50b9ac4e..a9867d05 100644 --- a/krillnotes-desktop/src/components/TreeView.tsx +++ b/krillnotes-desktop/src/components/TreeView.tsx @@ -77,7 +77,7 @@ function TreeView({ if (tree.length === 0) { return (
Date: Tue, 2 Jun 2026 17:55:08 +1000 Subject: [PATCH 5/5] fix: remove focus ring from tree container entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tree div is just a keyboard event target — the selected node already highlights, so a container-level focus ring is redundant and visually distracting. --- krillnotes-desktop/src/components/TreeView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/krillnotes-desktop/src/components/TreeView.tsx b/krillnotes-desktop/src/components/TreeView.tsx index a9867d05..b7b82a89 100644 --- a/krillnotes-desktop/src/components/TreeView.tsx +++ b/krillnotes-desktop/src/components/TreeView.tsx @@ -77,7 +77,7 @@ function TreeView({ if (tree.length === 0) { return (