dsh-obsidian-memo 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +270 -0
- package/README.md +272 -0
- package/cordis.patch.yml +10 -0
- package/docs/ARCHITECTURE.md +88 -0
- package/docs/AUDIT-0.16.md +74 -0
- package/docs/CHANGELOG.md +120 -0
- package/docs/CONTRACT.md +544 -0
- package/docs/GUIDE.md +177 -0
- package/docs/KNOWN_ISSUES.md +113 -0
- package/icon.svg +7 -0
- package/lib/ai.js +161 -0
- package/lib/capture.js +175 -0
- package/lib/client.js +6994 -0
- package/lib/config-picker.js +29 -0
- package/lib/filename.js +33 -0
- package/lib/files.js +88 -0
- package/lib/host/ai-jobs.js +118 -0
- package/lib/host/configuration.js +151 -0
- package/lib/host/dispatch.js +32 -0
- package/lib/host/environment.js +24 -0
- package/lib/host/folders.js +77 -0
- package/lib/host/images.js +116 -0
- package/lib/host/mutations.js +123 -0
- package/lib/host/note-content.js +44 -0
- package/lib/host/notes.js +148 -0
- package/lib/host/rpc.js +82 -0
- package/lib/host/scanning.js +67 -0
- package/lib/host/session.js +67 -0
- package/lib/host/state.js +68 -0
- package/lib/host/vault.js +38 -0
- package/lib/host/writes.js +21 -0
- package/lib/index.js +88 -0
- package/lib/notes.js +454 -0
- package/lib/obsidian.js +93 -0
- package/lib/scan.js +269 -0
- package/lib/settings.js +228 -0
- package/lib/shared.js +309 -0
- package/lib/trash.js +205 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -0
- package/scripts/benchmark.mjs +58 -0
- package/scripts/build-client.mjs +39 -0
- package/src/client/ai/use-comments.js +75 -0
- package/src/client/ai/use-job-notifications.js +53 -0
- package/src/client/ai/use-project-dialog.js +58 -0
- package/src/client/capture/composer.js +266 -0
- package/src/client/capture/images.js +64 -0
- package/src/client/capture/keyboard.js +34 -0
- package/src/client/components/dialogs.js +67 -0
- package/src/client/components/feedback.js +50 -0
- package/src/client/components/icons.js +160 -0
- package/src/client/components/use-toasts.js +58 -0
- package/src/client/constants.js +127 -0
- package/src/client/data/state.js +116 -0
- package/src/client/data/use-notes-data.js +137 -0
- package/src/client/data/visibility.js +14 -0
- package/src/client/drag/drop-actions.js +47 -0
- package/src/client/drag/motion.js +84 -0
- package/src/client/drag/overlay.js +33 -0
- package/src/client/drag/use-card-drag.js +149 -0
- package/src/client/i18n/en.js +219 -0
- package/src/client/i18n/index.js +11 -0
- package/src/client/i18n/zh.js +219 -0
- package/src/client/images/cache.js +55 -0
- package/src/client/images/components.js +121 -0
- package/src/client/images/queue.js +106 -0
- package/src/client/index.js +40 -0
- package/src/client/panel/header.js +146 -0
- package/src/client/panel/use-shortcuts.js +79 -0
- package/src/client/panel.js +424 -0
- package/src/client/preferences.js +198 -0
- package/src/client/react.js +8 -0
- package/src/client/reader/blocks.js +51 -0
- package/src/client/reader/note-drawer.js +270 -0
- package/src/client/reader/use-dock-offset.js +23 -0
- package/src/client/reader/use-drawer-layout.js +96 -0
- package/src/client/reader/use-note-editor.js +230 -0
- package/src/client/reader/use-pinned-note.js +41 -0
- package/src/client/runtime.js +30 -0
- package/src/client/settings/ai-budget-row.js +65 -0
- package/src/client/settings/ai-budget.js +42 -0
- package/src/client/settings/callouts.js +21 -0
- package/src/client/settings/config-file.js +74 -0
- package/src/client/settings/image-processing.js +31 -0
- package/src/client/settings/official-page.js +32 -0
- package/src/client/settings/paths.js +131 -0
- package/src/client/settings/settings-drawer.js +244 -0
- package/src/client/settings/style-select.js +58 -0
- package/src/client/settings/trash.js +30 -0
- package/src/client/settings/use-settings-draft.js +207 -0
- package/src/client/styles/base.js +63 -0
- package/src/client/styles/capture.js +94 -0
- package/src/client/styles/index.js +26 -0
- package/src/client/styles/overrides.js +116 -0
- package/src/client/styles/reader.js +66 -0
- package/src/client/styles/settings.js +118 -0
- package/src/client/styles/tokens.js +29 -0
- package/src/client/styles/wall.js +83 -0
- package/src/client/testing.js +139 -0
- package/src/client/utils.js +66 -0
- package/src/client/wall/model.js +158 -0
- package/src/client/wall/note-card.js +201 -0
- package/src/client/wall/use-note-actions.js +107 -0
- package/src/client/wall/use-subfolders.js +42 -0
- package/src/client/wall/use-wall-content.js +135 -0
- package/src/client/wall/use-wall-filters.js +92 -0
- package/src/client/wall/use-wall-paging.js +55 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** 底部归档/项目/删除操作栏,转发拖放和归档目录选择。 */
|
|
2
|
+
import { ActionIcon } from '../components/icons.js';
|
|
3
|
+
import { h } from '../react.js';
|
|
4
|
+
|
|
5
|
+
function DropActions({ t, active, activeTargetId, targets, parked, onHover, onArchive, onArchiveChoose, onProject, onDelete }) {
|
|
6
|
+
function enter(event, kind, targetId = null) {
|
|
7
|
+
event.preventDefault(); event.stopPropagation();
|
|
8
|
+
event.dataTransfer.dropEffect = kind === 'archive' && targets.length === 0 ? 'none' : 'move';
|
|
9
|
+
onHover(kind, targetId);
|
|
10
|
+
}
|
|
11
|
+
function drop(event, kind, targetId = null) {
|
|
12
|
+
event.preventDefault(); event.stopPropagation();
|
|
13
|
+
if (kind === 'archive') onArchive(event, targetId);
|
|
14
|
+
else if (kind === 'project') onProject(event);
|
|
15
|
+
else onDelete(event);
|
|
16
|
+
}
|
|
17
|
+
return h('div', { className: 'SN_dropActions', role: 'group', 'aria-label': t('drag.actions') },
|
|
18
|
+
h('p', { className: 'SN_dropHint', 'aria-live': 'polite' }, t('drag.hint')),
|
|
19
|
+
h('div', { className: 'SN_dropRow' }, ['archive', 'project', 'delete'].map((kind) => h('div', {
|
|
20
|
+
key: kind, className: 'SN_dropItem', 'data-action': kind,
|
|
21
|
+
'data-parked': kind === 'archive' && parked ? 'true' : undefined,
|
|
22
|
+
'data-active': active === kind ? 'true' : 'false',
|
|
23
|
+
'aria-disabled': kind === 'archive' && targets.length === 0 ? 'true' : undefined,
|
|
24
|
+
onDragEnter: (event) => enter(event, kind),
|
|
25
|
+
onDragOver: (event) => enter(event, kind),
|
|
26
|
+
onDragLeave: (event) => { if (!event.currentTarget.contains(event.relatedTarget)) onHover(null, null); },
|
|
27
|
+
onDrop: (event) => drop(event, kind),
|
|
28
|
+
},
|
|
29
|
+
active === 'archive' && kind === 'archive' && targets.length > 0
|
|
30
|
+
? h('div', { className: 'SN_dropTargets', role: 'menu', 'aria-label': t('archive.aria') }, targets.map((target) => h('button', {
|
|
31
|
+
key: target.id,
|
|
32
|
+
type: 'button', role: 'menuitem',
|
|
33
|
+
className: 'SN_dropTarget',
|
|
34
|
+
'data-target-id': target.id,
|
|
35
|
+
'data-active': activeTargetId === target.id ? 'true' : 'false',
|
|
36
|
+
onDragEnter: (event) => enter(event, 'archive', target.id),
|
|
37
|
+
onDragOver: (event) => enter(event, 'archive', target.id),
|
|
38
|
+
onDragLeave: (event) => { if (!event.currentTarget.contains(event.relatedTarget)) onHover('archive', null); },
|
|
39
|
+
onDrop: (event) => drop(event, 'archive', target.id),
|
|
40
|
+
onClick: (event) => { event.stopPropagation(); if (parked) onArchiveChoose(target.id); },
|
|
41
|
+
}, target.label || target.id)))
|
|
42
|
+
: null,
|
|
43
|
+
h(ActionIcon, { kind }), h('span', { className: 'SN_dropLabel' }, t(kind === 'delete' ? 'drag.delete' : 'card.' + kind))))),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { DropActions };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** 拖拽浮层的帧合并、位置计算、停靠和收尾动画。 */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function cardDragLayout(point, viewport) {
|
|
5
|
+
const width = point?.width > 0 ? point.width : 320;
|
|
6
|
+
const height = point?.height > 0 ? point.height : 208;
|
|
7
|
+
const scale = 1 / 3;
|
|
8
|
+
const previewWidth = width * scale, previewHeight = height * scale;
|
|
9
|
+
const left = Math.max(8, Math.min((point?.x || 0) - Math.max(0, Math.min(point?.offsetX || 0, width)) * scale, viewport.width - previewWidth - 8));
|
|
10
|
+
const top = Math.max(48, Math.min((point?.y || 0) - Math.max(0, Math.min(point?.offsetY || 0, height)) * scale, viewport.height - previewHeight - 8));
|
|
11
|
+
const center = left + previewWidth / 2;
|
|
12
|
+
const hintWidth = Math.min(270, 2 * Math.max(1, Math.min(center - 8, viewport.width - 8 - center)));
|
|
13
|
+
return { width, height, scale, previewWidth, previewHeight, left, top, hintWidth };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createCardDragMotion(layer, initialPoint) {
|
|
17
|
+
const win = layer.ownerDocument.defaultView;
|
|
18
|
+
const point = Object.assign({}, initialPoint);
|
|
19
|
+
let viewport = { width: win.innerWidth, height: win.innerHeight };
|
|
20
|
+
let layout = cardDragLayout(point, viewport);
|
|
21
|
+
let frame = null, disposed = false, settling = false, animation = null, anchor = null;
|
|
22
|
+
const transform = (position) => 'translate3d(' + position.left + 'px,' + position.top + 'px,0)';
|
|
23
|
+
const position = (next) => {
|
|
24
|
+
if (next.left !== layout.left || next.top !== layout.top) layer.style.transform = transform(next);
|
|
25
|
+
if (next.hintWidth !== layout.hintWidth) layer.style.setProperty('--sn-drag-hint-width', next.hintWidth + 'px');
|
|
26
|
+
layout = next;
|
|
27
|
+
};
|
|
28
|
+
const paint = () => {
|
|
29
|
+
frame = null;
|
|
30
|
+
if (disposed || settling) return;
|
|
31
|
+
position(cardDragLayout(point, viewport));
|
|
32
|
+
};
|
|
33
|
+
const schedule = () => { if (frame === null) frame = win.requestAnimationFrame(paint); };
|
|
34
|
+
const move = (event) => {
|
|
35
|
+
if (disposed || settling || !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)
|
|
36
|
+
|| (event.type === 'drag' && event.clientX === 0 && event.clientY === 0)
|
|
37
|
+
|| (point.x === event.clientX && point.y === event.clientY)) return;
|
|
38
|
+
point.x = event.clientX; point.y = event.clientY;
|
|
39
|
+
schedule();
|
|
40
|
+
};
|
|
41
|
+
const placeParked = () => {
|
|
42
|
+
const rect = anchor.getBoundingClientRect();
|
|
43
|
+
viewport = { width: win.innerWidth, height: win.innerHeight };
|
|
44
|
+
Object.assign(point, { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, offsetX: layout.width / 2, offsetY: layout.height / 2 });
|
|
45
|
+
position(cardDragLayout(point, viewport));
|
|
46
|
+
};
|
|
47
|
+
const resize = () => { if (anchor) placeParked(); else { viewport = { width: win.innerWidth, height: win.innerHeight }; schedule(); } };
|
|
48
|
+
const detach = () => {
|
|
49
|
+
if (frame !== null) { win.cancelAnimationFrame(frame); frame = null; }
|
|
50
|
+
layer.ownerDocument.removeEventListener('dragover', move, true);
|
|
51
|
+
layer.ownerDocument.removeEventListener('drag', move, true);
|
|
52
|
+
win.removeEventListener('resize', resize);
|
|
53
|
+
};
|
|
54
|
+
layer.ownerDocument.addEventListener('dragover', move, true);
|
|
55
|
+
layer.ownerDocument.addEventListener('drag', move, true);
|
|
56
|
+
win.addEventListener('resize', resize);
|
|
57
|
+
return {
|
|
58
|
+
park(element) {
|
|
59
|
+
if (disposed || settling || !element) return;
|
|
60
|
+
detach(); anchor = element;
|
|
61
|
+
layer.dataset.parked = 'true';
|
|
62
|
+
placeParked();
|
|
63
|
+
win.addEventListener('resize', resize);
|
|
64
|
+
},
|
|
65
|
+
finish(mode, source) {
|
|
66
|
+
if (settling || disposed) return Promise.resolve();
|
|
67
|
+
settling = true; detach();
|
|
68
|
+
// 只在松手时读一次原卡片位置;回落结束前保留虚线占位,避免原卡片闪回。
|
|
69
|
+
const rect = mode === 'return' && source?.isConnected ? source.getBoundingClientRect() : null;
|
|
70
|
+
layer.dataset.settling = 'true';
|
|
71
|
+
if (!layer.animate || win.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches) return Promise.resolve();
|
|
72
|
+
const end = rect
|
|
73
|
+
? { transform: 'translate3d(' + rect.left + 'px,' + rect.top + 'px,0) scale(' + rect.width / layout.previewWidth + ')', opacity: 1 }
|
|
74
|
+
: { transform: transform(layout) + ' scale(.9)', opacity: 0 };
|
|
75
|
+
animation = layer.animate([{ transform: transform(layout), opacity: 1 }, end], {
|
|
76
|
+
duration: rect ? 150 : 110, easing: 'cubic-bezier(.2,.8,.2,1)', fill: 'forwards',
|
|
77
|
+
});
|
|
78
|
+
return animation.finished.catch(() => {});
|
|
79
|
+
},
|
|
80
|
+
dispose() { disposed = true; detach(); animation?.cancel(); },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { cardDragLayout, createCardDragMotion };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 渲染缩略拖拽浮层并绑定动画控制器。 */
|
|
2
|
+
import { ActionIcon } from '../components/icons.js';
|
|
3
|
+
import { cardDragLayout, createCardDragMotion } from './motion.js';
|
|
4
|
+
import { React, ReactDOM, h } from '../react.js';
|
|
5
|
+
import { NoteCard } from '../wall/note-card.js';
|
|
6
|
+
|
|
7
|
+
function CardDragOverlay({ note, point, action, targetId, targets, t, motionRef }) {
|
|
8
|
+
const layerRef = React.useRef(null);
|
|
9
|
+
React.useLayoutEffect(() => {
|
|
10
|
+
if (!layerRef.current || !motionRef) return undefined;
|
|
11
|
+
const motion = createCardDragMotion(layerRef.current, point);
|
|
12
|
+
motionRef.current = motion;
|
|
13
|
+
return () => { motion.dispose(); if (motionRef.current === motion) motionRef.current = null; };
|
|
14
|
+
}, [point, motionRef]);
|
|
15
|
+
const target = (targets || []).find((item) => item.id === targetId);
|
|
16
|
+
const text = action === 'archive'
|
|
17
|
+
? (target ? t('drag.releaseArchiveTarget', { label: target.label || target.id }) : t('drag.releaseArchive'))
|
|
18
|
+
: action === 'project' ? t('drag.releaseProject') : action === 'delete' ? t('drag.release') : '';
|
|
19
|
+
// 初始定位只交给 React 一次;后续移动/停靠/窗口缩放由同一个 motion 持有。
|
|
20
|
+
const layout = React.useMemo(() => cardDragLayout(point, { width: window.innerWidth || 1024, height: window.innerHeight || 768 }), [point]);
|
|
21
|
+
const layer = h('div', {
|
|
22
|
+
className: 'SN_dragLayer', ref: layerRef, 'aria-hidden': 'true',
|
|
23
|
+
style: { transform: 'translate3d(' + layout.left + 'px,' + layout.top + 'px,0)', width: layout.previewWidth + 'px', height: layout.previewHeight + 'px', '--sn-drag-hint-width': layout.hintWidth + 'px' },
|
|
24
|
+
},
|
|
25
|
+
h('div', { className: 'SN_dragPreview', style: { width: layout.width + 'px', height: layout.height + 'px', transform: 'scale(' + layout.scale + ')' } },
|
|
26
|
+
React.useMemo(() => h(NoteCard, { note, emojis: [], drawerOpen: true, preview: true, actions: { current: { t } } }), [note, t]),
|
|
27
|
+
),
|
|
28
|
+
text ? h('div', { className: 'SN_dragFollow', 'data-action': action }, h(ActionIcon, { kind: action, size: 15 }), h('span', null, text)) : null,
|
|
29
|
+
);
|
|
30
|
+
return typeof document !== 'undefined' && document.body ? ReactDOM.createPortal(layer, document.body) : layer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { CardDragOverlay };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** 持有一次拖拽会话;统一 drop/dragend、归档停靠和取消。 */
|
|
2
|
+
import { focusCaptureOnTab } from '../capture/keyboard.js';
|
|
3
|
+
import { React } from '../react.js';
|
|
4
|
+
|
|
5
|
+
function useCardDrag({
|
|
6
|
+
aliveRef, archiveTargets, findTarget, openProjectDialog, panelRef, pushToast, runArchive, runDelete,
|
|
7
|
+
setDateMenu, setDrawerArchive, setHoverPath, setSortMenu, tRef,
|
|
8
|
+
}) {
|
|
9
|
+
const [dragSession, setDragSession] = React.useState(null);
|
|
10
|
+
const dragSessionRef = React.useRef(null), dragMotionRef = React.useRef(null), captureFocusRef = React.useRef(false);
|
|
11
|
+
const dragNote = dragSession?.note || null;
|
|
12
|
+
|
|
13
|
+
React.useLayoutEffect(() => {
|
|
14
|
+
if (!dragNote && captureFocusRef.current) {
|
|
15
|
+
captureFocusRef.current = false;
|
|
16
|
+
panelRef.current?.querySelector('.SN_captureTitle')?.focus();
|
|
17
|
+
}
|
|
18
|
+
}, [dragNote]);
|
|
19
|
+
const [dropTarget, setDropTarget] = React.useState(null);
|
|
20
|
+
const dropAction = dropTarget?.kind || null, dropTargetId = dropTarget?.id || null;
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (!dragNote) return undefined;
|
|
24
|
+
// 空白墙面也接收本次内部拖放:否则浏览器先执行拒绝拖放的原生回弹,
|
|
25
|
+
// 延迟 dragend 后才轮到自定义回落。操作栏仍先处理 drop,再冒泡到此处。
|
|
26
|
+
const accept = (event) => {
|
|
27
|
+
const session = dragSessionRef.current;
|
|
28
|
+
if (!session || session.ending || session.parked || !Array.from(event.dataTransfer?.types || []).includes('application/x-dsh-memo')) return;
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
event.dataTransfer.dropEffect = 'move';
|
|
31
|
+
};
|
|
32
|
+
const end = (event) => {
|
|
33
|
+
if (event.type === 'drop') {
|
|
34
|
+
if (dragSessionRef.current?.parked || !Array.from(event.dataTransfer?.types || []).includes('application/x-dsh-memo')) return;
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
}
|
|
37
|
+
return endCardDrag();
|
|
38
|
+
};
|
|
39
|
+
const key = (event) => {
|
|
40
|
+
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); endCardDrag(); }
|
|
41
|
+
else if (event.key === 'Enter' && dragSessionRef.current?.parked) {
|
|
42
|
+
const panel = panelRef.current;
|
|
43
|
+
focusCaptureOnTab(event, {
|
|
44
|
+
panel, form: panel?.querySelector('.SN_capture'), busy: false,
|
|
45
|
+
// 等 React 提交输入岛可见状态后再聚焦,避免聚焦仍隐藏的 textarea。
|
|
46
|
+
title: { focus() { captureFocusRef.current = true; endCardDrag(); } },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const dismissParked = (event) => {
|
|
51
|
+
if (!dragSessionRef.current?.parked || event.target?.closest?.('.SN_dropItem[data-action="archive"]')) return;
|
|
52
|
+
endCardDrag();
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener('dragenter', accept, true);
|
|
55
|
+
document.addEventListener('dragover', accept, true);
|
|
56
|
+
document.addEventListener('drop', end);
|
|
57
|
+
document.addEventListener('keydown', key, true);
|
|
58
|
+
document.addEventListener('pointerdown', dismissParked, true);
|
|
59
|
+
document.addEventListener('focusin', dismissParked, true);
|
|
60
|
+
window.addEventListener('blur', end);
|
|
61
|
+
return () => {
|
|
62
|
+
document.removeEventListener('dragenter', accept, true);
|
|
63
|
+
document.removeEventListener('dragover', accept, true);
|
|
64
|
+
document.removeEventListener('drop', end);
|
|
65
|
+
document.removeEventListener('keydown', key, true); window.removeEventListener('blur', end);
|
|
66
|
+
document.removeEventListener('pointerdown', dismissParked, true);
|
|
67
|
+
document.removeEventListener('focusin', dismissParked, true);
|
|
68
|
+
};
|
|
69
|
+
}, [Boolean(dragNote)]);
|
|
70
|
+
|
|
71
|
+
function endCardDrag(mode = 'return', operation) {
|
|
72
|
+
const session = dragSessionRef.current;
|
|
73
|
+
if (!session || session.ending) return session?.finished || Promise.resolve();
|
|
74
|
+
session.ending = true;
|
|
75
|
+
if (session.parked) setDropTarget(null);
|
|
76
|
+
// drop 与 dragend 共用一次收尾;文件操作并行进行,不先闪回原卡片再移除。
|
|
77
|
+
const animation = dragMotionRef.current?.finish(mode, session.source);
|
|
78
|
+
session.finished = Promise.all([animation, operation]).then(() => {
|
|
79
|
+
if (dragSessionRef.current !== session || !aliveRef.current) return;
|
|
80
|
+
dragSessionRef.current = null;
|
|
81
|
+
setDragSession(null); setDropTarget(null);
|
|
82
|
+
});
|
|
83
|
+
return session.finished;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function startCardDrag(note, point, source) {
|
|
87
|
+
if (dragSessionRef.current) return false;
|
|
88
|
+
const session = { note, point, source, ending: false };
|
|
89
|
+
dragSessionRef.current = session;
|
|
90
|
+
setDragSession(session); setDropTarget(null); setHoverPath(null);
|
|
91
|
+
setDrawerArchive(false); setSortMenu(false); setDateMenu(false);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hoverDragAction(kind, targetId) {
|
|
95
|
+
if (!dragSessionRef.current || dragSessionRef.current.ending || dragSessionRef.current.parked) return;
|
|
96
|
+
setDropTarget((current) => {
|
|
97
|
+
if (!kind) return current ? null : current;
|
|
98
|
+
return current?.kind === kind && current.id === (targetId || null) ? current : { kind, id: targetId || null };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function deleteDrop(event) {
|
|
103
|
+
const session = dragSessionRef.current;
|
|
104
|
+
if (!session || session.ending) return;
|
|
105
|
+
const note = session.note;
|
|
106
|
+
const path = event.dataTransfer.getData('application/x-dsh-memo');
|
|
107
|
+
if (path !== note.path) { endCardDrag(); return; }
|
|
108
|
+
endCardDrag('drop', runDelete(note));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function archiveDrop(event, targetId) {
|
|
112
|
+
const session = dragSessionRef.current;
|
|
113
|
+
if (!session || session.ending) return;
|
|
114
|
+
const note = session.note;
|
|
115
|
+
const path = event.dataTransfer.getData('application/x-dsh-memo');
|
|
116
|
+
if (path !== note.path) { endCardDrag(); return; }
|
|
117
|
+
if (targetId) { endCardDrag('drop', runArchive(note, targetId)); return; }
|
|
118
|
+
if (!archiveTargets.length) {
|
|
119
|
+
endCardDrag();
|
|
120
|
+
pushToast({ text: tRef.current('archive.emptyHint'), kind: 'info', timeout: 6000 });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const anchor = event.currentTarget?.closest?.('.SN_dropItem[data-action="archive"]');
|
|
124
|
+
if (!anchor) { endCardDrag(); return; }
|
|
125
|
+
// 放到归档区但未选目录:停止跟随鼠标,预览停在按钮正中,保留底部目录菜单。
|
|
126
|
+
session.parked = true;
|
|
127
|
+
dragMotionRef.current?.park(anchor);
|
|
128
|
+
setDragSession(Object.assign({}, session));
|
|
129
|
+
setDropTarget({ kind: 'archive', id: null });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function chooseArchiveTarget(targetId) {
|
|
133
|
+
const session = dragSessionRef.current;
|
|
134
|
+
if (!session?.parked || session.ending || !findTarget(targetId)) return;
|
|
135
|
+
endCardDrag('drop', runArchive(session.note, targetId));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function projectDrop(event) {
|
|
139
|
+
const session = dragSessionRef.current;
|
|
140
|
+
if (!session || session.ending) return;
|
|
141
|
+
const note = session.note;
|
|
142
|
+
const path = event.dataTransfer.getData('application/x-dsh-memo');
|
|
143
|
+
endCardDrag().then(() => { if (aliveRef.current && path === note.path) openProjectDialog(note); });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { dragSession, dragSessionRef, dragMotionRef, dragNote, dropAction, dropTargetId, endCardDrag, startCardDrag, hoverDragAction, deleteDrop, archiveDrop, chooseArchiveTarget, projectDrop };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { useCardDrag };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/** 英文界面文案。 */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const en = {
|
|
5
|
+
'nav.label': 'Obsidian MEMO',
|
|
6
|
+
|
|
7
|
+
'panel.title': 'Obsidian MEMO',
|
|
8
|
+
'panel.count': '{count} notes',
|
|
9
|
+
'panel.refresh': 'Refresh notes',
|
|
10
|
+
'panel.settings': 'Settings',
|
|
11
|
+
'panel.loading': 'Loading…',
|
|
12
|
+
'panel.retry': 'Retry',
|
|
13
|
+
|
|
14
|
+
'sort.label': 'Sort',
|
|
15
|
+
'sort.createdAsc': 'Created · oldest first',
|
|
16
|
+
'sort.createdDesc': 'Created · newest first (default)',
|
|
17
|
+
'sort.mtimeDesc': 'Last modified · newest first',
|
|
18
|
+
'sort.nameAsc': 'File name · A→Z',
|
|
19
|
+
|
|
20
|
+
'date.label': 'Date filter',
|
|
21
|
+
'date.today': 'Today',
|
|
22
|
+
'date.3d': '3 days',
|
|
23
|
+
'date.week': 'This week',
|
|
24
|
+
'date.month': 'This month',
|
|
25
|
+
'date.realtime': 'Date cutoff by real time',
|
|
26
|
+
'date.realtimeHint': 'Checked: cut at 00:00 of each day; unchecked: cut relative to the last note',
|
|
27
|
+
'date.toggleOn': 'Date filter on: {range}; click to show all',
|
|
28
|
+
'date.toggleOff': 'Date filter off; showing all. Click to filter by “{range}”',
|
|
29
|
+
|
|
30
|
+
'state.unconfiguredTitle': 'Not configured',
|
|
31
|
+
'state.unconfiguredText': 'Pick your Obsidian vault and your in-progress folder in Settings; the note wall will appear here.',
|
|
32
|
+
'state.unconfiguredAction': 'Open settings',
|
|
33
|
+
'state.emptyTitle': 'No notes yet',
|
|
34
|
+
'state.emptyText': 'Put markdown notes in this folder and they will show up here as pastel sticky notes.',
|
|
35
|
+
'state.emptyAction': 'Refresh',
|
|
36
|
+
'state.dateEmptyTitle': 'No notes in this date range',
|
|
37
|
+
'state.dateEmptyText': 'Filtering by “{range}”. Pick another date range, or refresh to rescan the folder.',
|
|
38
|
+
'state.dateEmptyAction': 'Change date range',
|
|
39
|
+
'state.errorTitle': 'Could not read the notes',
|
|
40
|
+
'state.errorAction': 'Retry',
|
|
41
|
+
'state.loadErrorTitle': 'Cannot reach the sticky-notes service',
|
|
42
|
+
'state.loadErrorAction': 'Retry',
|
|
43
|
+
|
|
44
|
+
'card.pending': '✍️ Writing a comment…',
|
|
45
|
+
'card.archive': 'Archive',
|
|
46
|
+
'card.project': 'New project',
|
|
47
|
+
'card.obsidian': 'Open in Obsidian',
|
|
48
|
+
'card.moreComments': '+{count}',
|
|
49
|
+
'card.openHint': 'Click to open',
|
|
50
|
+
'card.aiComment': 'AI comment',
|
|
51
|
+
'card.noPresets': 'Add comment emojis in Settings first',
|
|
52
|
+
'card.moreImages': '+{count}',
|
|
53
|
+
|
|
54
|
+
'panel.subfolders': 'Showing subfolders',
|
|
55
|
+
'panel.topOnly': 'Top level only',
|
|
56
|
+
'panel.loadingMore': 'Loading…',
|
|
57
|
+
|
|
58
|
+
'archive.menuTitle': 'Archive to',
|
|
59
|
+
'archive.emptyHint': 'Add archive folders in Settings first',
|
|
60
|
+
'archive.aria': 'Archive menu',
|
|
61
|
+
|
|
62
|
+
'drawer.aria': 'Note drawer',
|
|
63
|
+
'drawer.close': 'Close',
|
|
64
|
+
'drawer.editHint': 'Double-click the body to edit',
|
|
65
|
+
'drawer.copy': 'Copy full note',
|
|
66
|
+
'drawer.loading': 'Loading…',
|
|
67
|
+
'drawer.retry': 'Retry',
|
|
68
|
+
'drawer.projects': 'Projects',
|
|
69
|
+
'drawer.comments': 'Comments ({count})',
|
|
70
|
+
'drawer.noComments': 'No comments yet',
|
|
71
|
+
'drawer.noBody': '(This note has no body text)',
|
|
72
|
+
'drawer.updated': 'Updated {time}',
|
|
73
|
+
'drawer.save': 'Save',
|
|
74
|
+
'drawer.saving': 'Saving…',
|
|
75
|
+
'drawer.cancel': 'Cancel',
|
|
76
|
+
'drawer.editDiscarded': 'Editing cancelled',
|
|
77
|
+
'drawer.saved': 'Saved',
|
|
78
|
+
'drawer.bodyAria': 'Markdown body editor',
|
|
79
|
+
'drawer.shortcutHint': 'Cmd/Ctrl+S to save · Esc to cancel',
|
|
80
|
+
'drawer.resize': 'Drag to resize (double-click to reset)',
|
|
81
|
+
'drawer.lock': 'Lock to the side',
|
|
82
|
+
'drawer.unlock': 'Unlock and float',
|
|
83
|
+
|
|
84
|
+
'comment.confirmTitle': 'A similar comment exists',
|
|
85
|
+
'comment.confirmText': 'This note already has a “{label}” comment. Add another one?',
|
|
86
|
+
'comment.confirmAgain': 'Add another',
|
|
87
|
+
'comment.cancel': 'Cancel',
|
|
88
|
+
|
|
89
|
+
'toast.archived': 'Archived to “{label}”',
|
|
90
|
+
'toast.undo': 'Undo',
|
|
91
|
+
'toast.restored': 'Restored',
|
|
92
|
+
'toast.commentStarted': 'Writing a “{label}” comment…',
|
|
93
|
+
'toast.commentDone': 'AI comment written',
|
|
94
|
+
'toast.projectStarted': 'Creating project document “{name}”…',
|
|
95
|
+
'toast.projectDone': 'Project document created',
|
|
96
|
+
'toast.jobFailed': 'Background job failed',
|
|
97
|
+
'toast.settingsSaved': 'Settings saved',
|
|
98
|
+
'toast.settingsUnconfigured': 'Not configured yet; the panel is unavailable',
|
|
99
|
+
'toast.budgetFailed': 'Thinking used up the whole token budget, so no text was written. Raise the AI budget in Settings (or pick a lower thinking preset) and try again.',
|
|
100
|
+
'toast.budgetCommentTruncated': 'The comment was saved but reached an output limit and may be incomplete. Adjust the budget or request a shorter response in Settings.',
|
|
101
|
+
'toast.budgetProjectTruncated': 'The project document was saved but reached an output limit and may be incomplete. Adjust the budget or request a shorter response in Settings.',
|
|
102
|
+
'toast.budgetAction': 'Open settings',
|
|
103
|
+
'toast.budgetAck': 'Got it',
|
|
104
|
+
|
|
105
|
+
'project.title': 'New project',
|
|
106
|
+
'project.desc': 'Create a project document from “{name}” and link it from the note.',
|
|
107
|
+
'project.nameLabel': 'Project name',
|
|
108
|
+
'project.namePlaceholder': 'e.g. Website redesign',
|
|
109
|
+
'project.confirm': 'Create',
|
|
110
|
+
'project.cancel': 'Cancel',
|
|
111
|
+
'project.busy': 'Creating…',
|
|
112
|
+
'project.needName': 'Enter a project name first',
|
|
113
|
+
|
|
114
|
+
'settings.title': 'Settings',
|
|
115
|
+
'settings.close': 'Close settings',
|
|
116
|
+
'settings.vault': 'Vault root',
|
|
117
|
+
'settings.vaultHint': 'Absolute path of the Obsidian vault.',
|
|
118
|
+
'settings.notes': 'Notes folder',
|
|
119
|
+
'settings.notesHint': 'Create notes here and display markdown from this folder (including subfolders).',
|
|
120
|
+
'settings.project': 'Project folder',
|
|
121
|
+
'settings.projectHint': 'Project folders are created here.',
|
|
122
|
+
'settings.archive': 'Archive folders',
|
|
123
|
+
'settings.archiveHint': 'Notes can be moved into these folders when archived.',
|
|
124
|
+
'settings.addArchive': 'Add archive folder',
|
|
125
|
+
'settings.emoji': 'Comment emojis',
|
|
126
|
+
'settings.emojiHint': 'Each emoji maps to one AI comment prompt.',
|
|
127
|
+
'settings.addEmoji': 'Add emoji',
|
|
128
|
+
'settings.emojiPlaceholder': '🔍',
|
|
129
|
+
'settings.labelPlaceholder': 'Insight',
|
|
130
|
+
'settings.promptPlaceholder': 'Prompt for the model…',
|
|
131
|
+
'settings.callout': 'Callout style',
|
|
132
|
+
'settings.calloutValue': 'callout-{style}',
|
|
133
|
+
'settings.aiBudget': 'AI budget',
|
|
134
|
+
'settings.aiBudgetHint': 'Thinking spends tokens too; a tight budget can end with no text.',
|
|
135
|
+
'settings.aiPreset.low': 'Low',
|
|
136
|
+
'settings.aiPreset.medium': 'Medium',
|
|
137
|
+
'settings.aiPreset.high': 'High',
|
|
138
|
+
'settings.aiPreset.custom': 'Custom',
|
|
139
|
+
'settings.aiPresetDetail.low': 'No thinking · 2048 tokens',
|
|
140
|
+
'settings.aiPresetDetail.medium': 'Low thinking · 4096 tokens',
|
|
141
|
+
'settings.aiPresetDetail.high': 'High thinking · 8192 tokens',
|
|
142
|
+
'settings.aiPresetDetail.custom': 'Custom effort and output cap',
|
|
143
|
+
'settings.aiEffort': 'Thinking',
|
|
144
|
+
'settings.aiEffort.off': 'Off',
|
|
145
|
+
'settings.aiEffort.low': 'Low',
|
|
146
|
+
'settings.aiEffort.high': 'High',
|
|
147
|
+
'settings.aiEffort.max': 'Max',
|
|
148
|
+
'settings.aiMaxTokens': 'Output cap',
|
|
149
|
+
'settings.aiTokensUnit': 'tokens',
|
|
150
|
+
'settings.aiProjectHint': 'Token budget for the generated project document.',
|
|
151
|
+
'settings.remove': 'Remove',
|
|
152
|
+
'settings.projectPrompt': 'Project prompt',
|
|
153
|
+
'settings.projectPromptHint': 'Used to generate the structured project document.',
|
|
154
|
+
'settings.save': 'Save',
|
|
155
|
+
'settings.saving': 'Saving…',
|
|
156
|
+
'settings.cancel': 'Cancel',
|
|
157
|
+
'settings.needEmoji': 'Keep at least one emoji',
|
|
158
|
+
'settings.notReady': 'Settings are not loaded yet',
|
|
159
|
+
'settings.resolved': 'Resolves to {path}',
|
|
160
|
+
'settings.pick': 'Choose…',
|
|
161
|
+
'settings.picking': 'Choosing…',
|
|
162
|
+
|
|
163
|
+
'error.offline': 'Connection is not ready',
|
|
164
|
+
'error.failed': 'Request failed',
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
Object.assign(en, {
|
|
168
|
+
'card.copy': 'Copy', 'card.delete': 'Delete note', 'card.project': 'To project',
|
|
169
|
+
'toast.copied': 'Title and body copied', 'toast.copyFailed': 'Could not copy. Try again.',
|
|
170
|
+
'toast.deleted': 'Moved to vault trash · Undo within 5 seconds', 'toast.deleteUndo': 'Undo',
|
|
171
|
+
'toast.sharedImages': 'Shared images remain in the vault for other notes',
|
|
172
|
+
'drag.hint': 'Drag a card to an action below', 'drag.release': 'Release to delete · Undo within 5 seconds',
|
|
173
|
+
'drag.releaseArchive': 'Release to choose an archive', 'drag.releaseArchiveTarget': 'Release to archive to "{label}"', 'drag.releaseProject': 'Release to create a project',
|
|
174
|
+
'drag.delete': 'Delete', 'drag.actions': 'Card drag actions', 'panel.backToTop': 'Back to top',
|
|
175
|
+
'capture.label': 'Quick capture', 'capture.title': 'Idea (used as the filename)',
|
|
176
|
+
'capture.placeholder': 'Capture a thought…', 'capture.tab': 'Add details',
|
|
177
|
+
'capture.body': 'Body', 'capture.bodyPlaceholder': 'A little more…',
|
|
178
|
+
'capture.add': 'Add images', 'capture.remove': 'Remove image {name}',
|
|
179
|
+
'capture.send': 'Create note', 'capture.sending': 'Creating…',
|
|
180
|
+
'capture.shortcut': 'Enter to create from the title; ⌘ / Ctrl + Enter from the body',
|
|
181
|
+
'capture.submitHint': '⌘ Enter to submit',
|
|
182
|
+
'capture.images': '{count} images attached', 'capture.created': 'Captured',
|
|
183
|
+
'capture.imageType': 'PNG, JPEG, GIF and WebP images are supported',
|
|
184
|
+
'capture.imageLimit': 'Up to {images} images, {imageLimit} each and {totalLimit} in total',
|
|
185
|
+
'capture.readFailed': 'Could not read the image; please choose it again',
|
|
186
|
+
'capture.compressFailed': 'Could not convert to WebP. Choose the image again or disable compression in Image processing.',
|
|
187
|
+
'capture.sourceLimit': 'Up to {images} images, {sourceImageLimit} each and {sourceTotalLimit} in total before compression',
|
|
188
|
+
'settings.images': 'Image processing', 'settings.compressWebp': 'Automatically compress to WebP',
|
|
189
|
+
'settings.imagesHint': 'Process images when creating notes. Converts to static WebP, keeps proportions and never upscales. Uncheck to preserve original files and animation.',
|
|
190
|
+
'settings.imageQuality': 'Image quality', 'settings.imageResolution': 'Maximum edge (px)',
|
|
191
|
+
'settings.attachmentDir': 'Image folder',
|
|
192
|
+
'settings.attachmentDirHint': 'Stores images for new notes. Leave blank to use _attachments in the notes folder. Enter a path within the vault or relative to it; missing folders are created automatically. Hidden folders starting with . are not indexed by Obsidian and are rejected.',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
Object.assign(en, {
|
|
196
|
+
'settings.tab.folders': 'Folders', 'settings.tab.ai': 'AI', 'settings.tab.images': 'Images', 'settings.tab.config': 'Manage & about',
|
|
197
|
+
'settings.about': 'About this project', 'settings.author': 'by cavenzhong',
|
|
198
|
+
'settings.vault': 'Obsidian vault', 'settings.notes': 'Notes folder', 'settings.project': 'Projects folder', 'settings.archive': 'Archive folders',
|
|
199
|
+
'settings.vaultHint': 'Choose the vault once. All other folders are relative to it.', 'settings.notesHint': 'Read Markdown notes from this folder.',
|
|
200
|
+
'settings.projectHint': 'New project documents go here.', 'settings.archiveHint': 'Destinations available when archiving notes.',
|
|
201
|
+
'settings.attachmentDirHint': 'Leave empty to use _attachments inside the notes folder.', 'settings.includeSubfolders': 'Include note subfolders',
|
|
202
|
+
'settings.chooseFolder': 'Choose a folder…', 'settings.newFolder': 'New folder in vault', 'settings.folderName': 'Folder name (nested paths supported)',
|
|
203
|
+
'settings.create': 'Create', 'settings.browseFolder': 'Browse other folders…', 'settings.clearFolder': 'Clear selection', 'settings.searchFolder': 'Search vault folders',
|
|
204
|
+
'settings.vaultRoot': 'Vault root', 'settings.recent': 'Recent',
|
|
205
|
+
'settings.addArchive': '+ Add archive folder', 'settings.newFolderHint': 'Creates a folder in this vault immediately; nested paths are supported. Canceling settings keeps the created folder.',
|
|
206
|
+
'settings.creatingFolder': 'Creating…', 'settings.folderCreated': 'Created and selected “{path}”. Save settings to use this folder.', 'settings.createAt': 'Create in',
|
|
207
|
+
'settings.config': 'Config JSON', 'settings.openConfig': 'Open config', 'settings.openConfigFile': 'Open JSON file', 'settings.revealConfig': 'Show enclosing folder', 'settings.reloadConfig': 'Reload',
|
|
208
|
+
'settings.configHint': 'The active configuration file. External edits load automatically.',
|
|
209
|
+
'settings.configActions': 'More…', 'settings.configLoadHint': 'Choose another JSON configuration and switch to it. The unsaved settings draft is replaced; the original file is kept.',
|
|
210
|
+
'settings.configCopyHint': 'Save committed settings to a new file and switch to it. The target must not exist; the original is kept.',
|
|
211
|
+
'settings.configPinned': 'DSH_STICKY_NOTES_SETTINGS fixes this location.', 'settings.configPath': 'Absolute config file path (.json)',
|
|
212
|
+
'settings.selectConfig': 'Select JSON file', 'settings.loadConfig': 'Load config', 'settings.relocateConfig': 'Save copy and use', 'settings.configDone': 'Configuration updated',
|
|
213
|
+
'settings.trash': 'Vault trash', 'settings.trashHint': 'Deleting a note still uses vault .trash with a 5-second undo. Transfer those files to the system trash separately here.',
|
|
214
|
+
'settings.revealTrash': 'Show in Finder / file manager', 'settings.emptyTrash': 'Move to system trash',
|
|
215
|
+
'settings.trashConfirm': 'Move the entire vault .trash, including files trashed by Obsidian, to the system trash. Restore through the system trash afterward. Existing system trash is kept.',
|
|
216
|
+
'settings.trashMoved': 'Moved {count} items to system trash',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
export { en };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** 组合中英文文案并补充共用 callout 名称。 */
|
|
2
|
+
import { en } from './en.js';
|
|
3
|
+
import { zh } from './zh.js';
|
|
4
|
+
import { CALLOUT_EN, CALLOUT_IDS, CALLOUT_ZH } from '../settings/callouts.js';
|
|
5
|
+
|
|
6
|
+
for (const id of CALLOUT_IDS) {
|
|
7
|
+
zh['callout.' + id] = CALLOUT_ZH[id];
|
|
8
|
+
en['callout.' + id] = CALLOUT_EN[id];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { zh, en };
|