Add mobile format toolbar, image picker and move-to dialog

- Markdown format commands shared by Ctrl+B/I/K and a toolbar above the
  soft keyboard on touch devices; bold and italic stack correctly
- Fit the app to the visual viewport so the keyboard does not cover the
  caret or the toolbar
- Insert images from the photo library or a file picker
- Move notes and folders with a folder picker from the file tree menu or
  the command palette
- Open the file tree menu with a long press on touch devices
This commit is contained in:
eezd committed 2026-10-17 17:12:43 +08:00
1 parent eb63b14c8e
commit 91da6e116e
14 files changed
+565 -68

No files matched your search

+36 -1
View File
@@ -15,7 +15,36 @@ const dropTarget = ref(false)
const input = ref<HTMLInputElement>()
const draft = ref('')
// 长按打开菜单:Android 会触发 contextmenu,iOS Safari 不会,统一用触摸计时
const LONG_PRESS_MS = 500
let pressTimer: ReturnType<typeof setTimeout> | undefined
let pressStart: { x: number; y: number } | null = null
let longPressed = false
function onTouchStart(e: TouchEvent) {
longPressed = false
pressStart = { x: e.touches[0].clientX, y: e.touches[0].clientY }
pressTimer = setTimeout(() => {
longPressed = true
menuOpen.value = true
}, LONG_PRESS_MS)
}
function onTouchMove(e: TouchEvent) {
if (pressStart && Math.hypot(e.touches[0].clientX - pressStart.x, e.touches[0].clientY - pressStart.y) > 10) cancelPress()
}
function cancelPress() {
clearTimeout(pressTimer)
pressStart = null
}
function activate() {
// 长按后松手产生的点击不打开笔记
if (longPressed) {
longPressed = false
return
}
if (props.node.dir) {
if (isOpen.value) expanded.delete(props.node.path)
else expanded.add(props.node.path)
@@ -41,7 +70,7 @@ function commitRename() {
if (target && target !== props.node.path) void move(props.node.path, target)
}
function runMenu(action: 'note' | 'folder' | 'rename' | 'share' | 'delete') {
function runMenu(action: 'note' | 'folder' | 'rename' | 'move' | 'share' | 'delete') {
menuOpen.value = false
const path = props.node.path
if (action === 'note' || action === 'folder') {
@@ -49,6 +78,7 @@ function runMenu(action: 'note' | 'folder' | 'rename' | 'share' | 'delete') {
void (action === 'note' ? createNote(path) : createFolder(path))
} else if (action === 'rename') ws.renaming = path
else if (action === 'share') ws.sharing = path
else if (action === 'move') ws.moving = path
else void remove(path)
}
@@ -89,6 +119,10 @@ function onDrop(e: DragEvent) {
:draggable="!isRenaming"
@click="activate"
@contextmenu.prevent="menuOpen = true"
@touchstart.passive="onTouchStart"
@touchmove.passive="onTouchMove"
@touchend="cancelPress"
@touchcancel="cancelPress"
@dragstart="onDragStart"
@dragover="onDragOver"
@dragleave="dropTarget = false"
@@ -115,6 +149,7 @@ function onDrop(e: DragEvent) {
<button @click="runMenu('folder')"><Icon name="folderAdd" />新建文件夹</button>
</template>
<button @click="runMenu('rename')"><Icon name="edit" />重命名</button>
<button @click="runMenu('move')"><Icon name="folder" />移动到…</button>
<button v-if="!node.dir" @click="runMenu('share')"><Icon name="link" />分享</button>
<button class="danger" @click="runMenu('delete')"><Icon name="trash" />删除</button>
</div>