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,198 @@
|
|
|
1
|
+
/** 持久化筛选、排序和钉住偏好,计算抽屉尺寸边界;保留已有存储键。 */
|
|
2
|
+
import {
|
|
3
|
+
DATE_FILTER_ENABLED_KEY, DATE_FILTER_KEY, DATE_REALTIME_KEY, DEFAULT_DATE_FILTER,
|
|
4
|
+
DEFAULT_DATE_FILTER_ENABLED, DEFAULT_SORT, DRAWER_MAX_WIDTH, DRAWER_MIN_WIDTH, DRAWER_WIDTH_KEY,
|
|
5
|
+
PINNED_KEY, PINNED_NOTE_KEY, PINNED_WALL_MIN_WIDTH, POLL_BASE, POLL_MAX, SORT_KEY, SORT_OPTIONS,
|
|
6
|
+
} from './constants.js';
|
|
7
|
+
import { isDateFilterId } from './wall/model.js';
|
|
8
|
+
|
|
9
|
+
function readStoredSort() {
|
|
10
|
+
try {
|
|
11
|
+
if (typeof window === 'undefined' || !window.localStorage) return DEFAULT_SORT;
|
|
12
|
+
const stored = window.localStorage.getItem(SORT_KEY);
|
|
13
|
+
if (typeof stored === 'string' && SORT_OPTIONS.some((option) => option.id === stored)) return stored;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// 隐私模式 / 存储被禁用:用默认排序即可。
|
|
16
|
+
}
|
|
17
|
+
return DEFAULT_SORT;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function storeSort(id) {
|
|
21
|
+
try {
|
|
22
|
+
if (typeof window !== 'undefined' && window.localStorage) window.localStorage.setItem(SORT_KEY, id);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// 存不下也不影响本次排序。
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readStoredDateFilter() {
|
|
29
|
+
try {
|
|
30
|
+
if (typeof window === 'undefined' || !window.localStorage) return DEFAULT_DATE_FILTER;
|
|
31
|
+
const stored = window.localStorage.getItem(DATE_FILTER_KEY);
|
|
32
|
+
if (typeof stored === 'string' && isDateFilterId(stored)) return stored;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// 隐私模式 / 存储被禁用:用默认今天即可。
|
|
35
|
+
}
|
|
36
|
+
return DEFAULT_DATE_FILTER;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function storeDateFilter(id) {
|
|
40
|
+
try {
|
|
41
|
+
if (typeof window !== 'undefined' && window.localStorage) window.localStorage.setItem(DATE_FILTER_KEY, id);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// 存不下也不影响本次筛选。
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readStoredDateEnabled() {
|
|
48
|
+
try {
|
|
49
|
+
if (typeof window === 'undefined' || !window.localStorage) return DEFAULT_DATE_FILTER_ENABLED;
|
|
50
|
+
const stored = window.localStorage.getItem(DATE_FILTER_ENABLED_KEY);
|
|
51
|
+
if (stored === '1') return true;
|
|
52
|
+
if (stored === '0') return false;
|
|
53
|
+
// 旧版没有独立的开关键:`all` = 不筛选 = 关闭;其它历史值 = 开启。
|
|
54
|
+
const legacyFilter = window.localStorage.getItem(DATE_FILTER_KEY);
|
|
55
|
+
if (legacyFilter === 'all') return false;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return DEFAULT_DATE_FILTER_ENABLED;
|
|
58
|
+
}
|
|
59
|
+
return DEFAULT_DATE_FILTER_ENABLED;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function storeDateEnabled(enabled) {
|
|
63
|
+
try {
|
|
64
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
65
|
+
window.localStorage.setItem(DATE_FILTER_ENABLED_KEY, enabled === true ? '1' : '0');
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readStoredDateRealtime() {
|
|
73
|
+
try {
|
|
74
|
+
if (typeof window === 'undefined' || !window.localStorage) return false;
|
|
75
|
+
return window.localStorage.getItem(DATE_REALTIME_KEY) === '1';
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function storeDateRealtime(realtime) {
|
|
82
|
+
try {
|
|
83
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
84
|
+
window.localStorage.setItem(DATE_REALTIME_KEY, realtime === true ? '1' : '0');
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// ignore
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readStoredDrawerWidth() {
|
|
92
|
+
try {
|
|
93
|
+
if (typeof window === 'undefined' || !window.localStorage) return null;
|
|
94
|
+
const raw = window.localStorage.getItem(DRAWER_WIDTH_KEY);
|
|
95
|
+
if (typeof raw !== 'string' || raw === '') return null;
|
|
96
|
+
const width = Number(raw);
|
|
97
|
+
if (!Number.isFinite(width) || width <= 0) return null;
|
|
98
|
+
return Math.round(Math.min(DRAWER_MAX_WIDTH, Math.max(DRAWER_MIN_WIDTH, width)));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function storeDrawerWidth(width) {
|
|
105
|
+
try {
|
|
106
|
+
if (typeof window === 'undefined' || !window.localStorage) return;
|
|
107
|
+
if (typeof width !== 'number' || !Number.isFinite(width)) return;
|
|
108
|
+
window.localStorage.setItem(DRAWER_WIDTH_KEY, String(Math.round(width)));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// 存不下也不影响本次拖拽。
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clearStoredDrawerWidth() {
|
|
115
|
+
try {
|
|
116
|
+
if (typeof window !== 'undefined' && window.localStorage) window.localStorage.removeItem(DRAWER_WIDTH_KEY);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function drawerWidthBounds(panelWidth, pinned) {
|
|
123
|
+
const width = typeof panelWidth === 'number' && Number.isFinite(panelWidth) && panelWidth > 0
|
|
124
|
+
? panelWidth
|
|
125
|
+
: 0;
|
|
126
|
+
const lower = width === 0 ? DRAWER_MIN_WIDTH : Math.min(DRAWER_MIN_WIDTH, width);
|
|
127
|
+
const upper = pinned === true
|
|
128
|
+
? Math.max(DRAWER_MIN_WIDTH, Math.min(DRAWER_MAX_WIDTH, width - PINNED_WALL_MIN_WIDTH))
|
|
129
|
+
: Math.max(lower, Math.min(DRAWER_MAX_WIDTH, width === 0 ? DRAWER_MAX_WIDTH : width * 0.8));
|
|
130
|
+
return { lower: lower, upper: upper };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function clampDrawerWidth(width, panelWidth, pinned) {
|
|
134
|
+
const bounds = drawerWidthBounds(panelWidth, pinned);
|
|
135
|
+
if (typeof width !== 'number' || !Number.isFinite(width)) return Math.round(bounds.lower);
|
|
136
|
+
return Math.round(Math.max(bounds.lower, Math.min(bounds.upper, width)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function nextPollDelay(prevDelay, unchanged) {
|
|
140
|
+
if (unchanged !== true) return POLL_BASE;
|
|
141
|
+
const base = typeof prevDelay === 'number' && Number.isFinite(prevDelay) && prevDelay > 0
|
|
142
|
+
? prevDelay
|
|
143
|
+
: POLL_BASE;
|
|
144
|
+
return Math.min(POLL_MAX, Math.round(base * 1.5));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readStoredPinned() {
|
|
148
|
+
try {
|
|
149
|
+
if (typeof window === 'undefined' || !window.localStorage) return false;
|
|
150
|
+
return window.localStorage.getItem(PINNED_KEY) === '1';
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function storePinned(pinned) {
|
|
157
|
+
try {
|
|
158
|
+
if (typeof window === 'undefined' || !window.localStorage) return;
|
|
159
|
+
window.localStorage.setItem(PINNED_KEY, pinned ? '1' : '0');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// 隐私模式 / 存储被禁用:只影响记忆,不影响本次交互。
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readStoredPinnedNote() {
|
|
166
|
+
try {
|
|
167
|
+
if (typeof window === 'undefined' || !window.localStorage) return '';
|
|
168
|
+
const stored = window.localStorage.getItem(PINNED_NOTE_KEY);
|
|
169
|
+
return typeof stored === 'string' ? stored : '';
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return '';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function storePinnedNote(path) {
|
|
176
|
+
try {
|
|
177
|
+
if (typeof window === 'undefined' || !window.localStorage) return;
|
|
178
|
+
if (typeof path !== 'string' || path === '') return;
|
|
179
|
+
window.localStorage.setItem(PINNED_NOTE_KEY, path);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// ignore
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function clearStoredPinnedNote() {
|
|
186
|
+
try {
|
|
187
|
+
if (typeof window !== 'undefined' && window.localStorage) window.localStorage.removeItem(PINNED_NOTE_KEY);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// ignore
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export {
|
|
194
|
+
readStoredSort, storeSort, readStoredDateFilter, storeDateFilter, readStoredDateEnabled,
|
|
195
|
+
storeDateEnabled, readStoredDateRealtime, storeDateRealtime, readStoredDrawerWidth, storeDrawerWidth,
|
|
196
|
+
clearStoredDrawerWidth, drawerWidthBounds, clampDrawerWidth, nextPollDelay, readStoredPinned,
|
|
197
|
+
storePinned, readStoredPinnedNote, storePinnedNote, clearStoredPinnedNote,
|
|
198
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** 阅读模式的 Markdown 块、任务与评论渲染。 */
|
|
2
|
+
import { NoteImage } from '../images/components.js';
|
|
3
|
+
import { h } from '../react.js';
|
|
4
|
+
import { commentHead } from '../wall/model.js';
|
|
5
|
+
|
|
6
|
+
function NoteBlock(props) {
|
|
7
|
+
const block = props.block || {};
|
|
8
|
+
const text = typeof block.text === 'string' ? block.text : '';
|
|
9
|
+
if (block.kind === 'h') return h('h4', { className: 'SN_blockH' }, text);
|
|
10
|
+
if (block.kind === 'hr') return h('hr', { className: 'SN_blockHr' });
|
|
11
|
+
if (block.kind === 'image') return h(NoteImage, { path: props.path, source: block.source, alt: block.alt, wiki: block.wiki });
|
|
12
|
+
if (block.kind === 'li') {
|
|
13
|
+
if (typeof block.checked === 'boolean') {
|
|
14
|
+
const ordered = typeof block.marker === 'string';
|
|
15
|
+
return h('div', { className: 'SN_blockLi SN_blockTask' + (ordered ? ' SN_blockOrdered' : '') },
|
|
16
|
+
ordered ? h('span', { className: 'SN_listNumber' }, block.marker) : null,
|
|
17
|
+
h('button', {
|
|
18
|
+
type: 'button',
|
|
19
|
+
className: 'SN_taskCheck',
|
|
20
|
+
role: 'checkbox',
|
|
21
|
+
'aria-checked': block.checked,
|
|
22
|
+
disabled: props.taskBusy,
|
|
23
|
+
'data-checked': block.checked ? 'true' : 'false',
|
|
24
|
+
'aria-label': block.checked ? '取消勾选' : '标记完成',
|
|
25
|
+
onClick: () => props.onToggleTask(block.sourceLine, !block.checked),
|
|
26
|
+
}, block.checked ? '✓' : ''),
|
|
27
|
+
h('span', { className: 'SN_taskText' }, text),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (typeof block.marker === 'string') return h('div', { className: 'SN_blockLi SN_blockOrdered' },
|
|
31
|
+
h('span', { className: 'SN_listNumber' }, block.marker), h('span', { className: 'SN_listText' }, text));
|
|
32
|
+
return h('div', { className: 'SN_blockLi' }, text);
|
|
33
|
+
}
|
|
34
|
+
if (block.kind === 'quote') return h('blockquote', { className: 'SN_blockQuote' }, text);
|
|
35
|
+
if (block.kind === 'code') return h('pre', { className: 'SN_blockCode' }, text);
|
|
36
|
+
return h('p', { className: 'SN_blockP' }, text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function NoteComment(props) {
|
|
40
|
+
const comment = props.comment || {};
|
|
41
|
+
const head = commentHead(comment);
|
|
42
|
+
return h('div', { className: 'SN_commentCard' },
|
|
43
|
+
h('div', { className: 'SN_commentCardHead' },
|
|
44
|
+
h('span', { className: 'SN_commentCardLabel' }, head),
|
|
45
|
+
comment.time ? h('span', { className: 'SN_commentCardTime' }, comment.time) : null,
|
|
46
|
+
),
|
|
47
|
+
h('div', { className: 'SN_commentCardBody' }, comment.text || ''),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { NoteBlock, NoteComment };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/** 阅读/编辑抽屉视图,接收草稿、布局和操作回调。 */
|
|
2
|
+
import { ActionIcon, CloseIcon, ObsidianIcon, PinIcon } from '../components/icons.js';
|
|
3
|
+
import { Fragment, React, h } from '../react.js';
|
|
4
|
+
import { NoteBlock, NoteComment } from './blocks.js';
|
|
5
|
+
import { DEFAULT_NOTE_COLOR } from '../styles/tokens.js';
|
|
6
|
+
import { formatTime } from '../utils.js';
|
|
7
|
+
import { emojiLabel } from '../wall/model.js';
|
|
8
|
+
|
|
9
|
+
function NoteDrawer(props) {
|
|
10
|
+
const {
|
|
11
|
+
t, drawer, editing, bodyDraft, saving, drawerWidth, resizing, titleDraft, onTitleChange, pinned,
|
|
12
|
+
inline, onTogglePinned, drawerActions, onClose, onRetry, onStartEdit, onBodyChange, onSave,
|
|
13
|
+
onCancelEdit, onResizeStart, onResizeReset, onToggleTask, taskBusy,
|
|
14
|
+
} = props;
|
|
15
|
+
// 第 4 轮:标题编辑用多行 textarea 完整显示,高度随内容自适应(不支持 field-sizing 的浏览器靠这里)。
|
|
16
|
+
const titleEditorRef = React.useRef(null);
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const el = titleEditorRef.current;
|
|
19
|
+
if (!el || !editing) return;
|
|
20
|
+
el.style.height = 'auto';
|
|
21
|
+
const next = Math.min(132, Math.max(24, el.scrollHeight || 24));
|
|
22
|
+
el.style.height = next + 'px';
|
|
23
|
+
}, [editing, titleDraft]);
|
|
24
|
+
const drawerStyle = typeof drawerWidth === 'number' && Number.isFinite(drawerWidth)
|
|
25
|
+
? {
|
|
26
|
+
width: drawerWidth + 'px',
|
|
27
|
+
minWidth: 'min(320px,100%)',
|
|
28
|
+
maxWidth: pinned ? 'min(960px,100%)' : 'min(960px,80%)',
|
|
29
|
+
}
|
|
30
|
+
: pinned
|
|
31
|
+
// 锁定态没拖过宽:默认 34%,但不超过「面板宽 - 260」(且至少 320)。
|
|
32
|
+
? {
|
|
33
|
+
width: '34%',
|
|
34
|
+
minWidth: 'min(320px,100%)',
|
|
35
|
+
maxWidth: 'max(320px, min(960px, calc(100% - 260px)))',
|
|
36
|
+
}
|
|
37
|
+
: undefined;
|
|
38
|
+
const summary = drawer.summary || {};
|
|
39
|
+
const full = drawer.full || null;
|
|
40
|
+
const source = full || summary;
|
|
41
|
+
const title = source.title || source.name || '';
|
|
42
|
+
const tag = source.tag || null;
|
|
43
|
+
const updated = source.mtime ? t('drawer.updated', { time: formatTime(source.mtime) }) : '';
|
|
44
|
+
const blocks = full && Array.isArray(full.blocks) ? full.blocks : [];
|
|
45
|
+
const comments = full && Array.isArray(full.comments) ? full.comments : [];
|
|
46
|
+
const projects = source.projects;
|
|
47
|
+
const canEdit = Boolean(full);
|
|
48
|
+
|
|
49
|
+
function handleBodyDoubleClick(event) {
|
|
50
|
+
if (!canEdit) return;
|
|
51
|
+
const target = event.target;
|
|
52
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
53
|
+
// 双击按钮 / 空白边缘不算(§7.3)。
|
|
54
|
+
if (target.closest('button')) return;
|
|
55
|
+
const inText = target.closest('.SN_blockP, .SN_blockH, .SN_blockLi, .SN_blockQuote, .SN_blockCode, .SN_commentCard, .SN_projects');
|
|
56
|
+
if (!inText && blocks.length > 0) return;
|
|
57
|
+
onStartEdit();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const bodyNode = h('div', {
|
|
61
|
+
className: 'SN_noteDrawerBody',
|
|
62
|
+
onDoubleClick: handleBodyDoubleClick,
|
|
63
|
+
},
|
|
64
|
+
drawer.loading && !full ? h('div', { className: 'SN_noteViewHint' }, t('drawer.loading')) : null,
|
|
65
|
+
drawer.error
|
|
66
|
+
? h('div', { className: 'SN_noteViewError' },
|
|
67
|
+
h('span', null, drawer.error),
|
|
68
|
+
h('button', { type: 'button', className: 'SN_textBtn', onClick: onRetry }, t('drawer.retry')),
|
|
69
|
+
)
|
|
70
|
+
: null,
|
|
71
|
+
!drawer.loading && !drawer.error && blocks.length === 0
|
|
72
|
+
? h('p', { className: 'SN_noteViewEmpty' }, t('drawer.editHint'))
|
|
73
|
+
: null,
|
|
74
|
+
blocks.map((block, index) => h(NoteBlock, { key: index, block: block, path: drawer.path, onToggleTask, taskBusy })),
|
|
75
|
+
projects ? h('div', { className: 'SN_projects' },
|
|
76
|
+
h('span', { className: 'SN_projectsLabel' }, t('drawer.projects')),
|
|
77
|
+
h('span', { className: 'SN_projectsValue' }, String(projects)),
|
|
78
|
+
) : null,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// 0.13 评论固定区:flex:none、max-height:42%、min-height:96px、自身滚动、上边框;编辑态隐藏。
|
|
82
|
+
const commentsNode = h('section', { className: 'SN_noteDrawerComments', 'aria-label': t('drawer.comments', { count: comments.length }) },
|
|
83
|
+
h('div', { className: 'SN_noteDrawerCommentsHead' },
|
|
84
|
+
h('h3', { className: 'SN_noteViewSection' }, t('drawer.comments', { count: comments.length })),
|
|
85
|
+
),
|
|
86
|
+
comments.length === 0
|
|
87
|
+
? h('p', { className: 'SN_noteViewHint SN_commentEmpty' }, t('drawer.noComments'))
|
|
88
|
+
: h('div', { className: 'SN_noteDrawerCommentsBody' },
|
|
89
|
+
comments.map((comment, index) => h(NoteComment, { key: index, comment: comment })),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// 抽屉操作条(§7.3.2):header 分割线下方;锁定/未锁定都显示,编辑态隐藏。
|
|
94
|
+
// 左 = AI emoji;右上 = 归档 / 到项目 / 删除 / 复制全文;右下 = 紫色 Obsidian 图文入口。
|
|
95
|
+
const actionsNode = drawerActions
|
|
96
|
+
? h('div', { className: 'SN_drawerActions' },
|
|
97
|
+
h('div', { className: 'SN_drawerActionLeft' },
|
|
98
|
+
drawerActions.pending
|
|
99
|
+
? h('span', { className: 'SN_pending' },
|
|
100
|
+
h('span', { className: 'SN_pendingDot', 'aria-hidden': 'true' }),
|
|
101
|
+
t('card.pending'),
|
|
102
|
+
)
|
|
103
|
+
: (Array.isArray(drawerActions.emojis) ? drawerActions.emojis : []).length === 0
|
|
104
|
+
? h('span', { className: 'SN_aiEmpty' }, t('card.noPresets'))
|
|
105
|
+
: (drawerActions.emojis || []).map((emoji) => h('button', {
|
|
106
|
+
key: emoji.id || emoji.emoji,
|
|
107
|
+
type: 'button',
|
|
108
|
+
className: 'SN_aiBtn',
|
|
109
|
+
'aria-label': emojiLabel(emoji) || emoji.emoji || t('card.aiComment'),
|
|
110
|
+
onClick: (event) => {
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
drawerActions.onComment(emoji);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
h('span', { className: 'SN_aiEmoji', 'aria-hidden': 'true' }, emoji.emoji || '✨'),
|
|
116
|
+
h('span', { className: 'SN_aiLabel', 'aria-hidden': 'true' }, emoji.label || emoji.emoji || ''),
|
|
117
|
+
)),
|
|
118
|
+
),
|
|
119
|
+
h('div', { className: 'SN_drawerActionRight' },
|
|
120
|
+
h('div', { className: 'SN_drawerActionTop' }, [
|
|
121
|
+
{ kind: 'archive', label: t('card.archive'), onClick: drawerActions.onToggleArchive,
|
|
122
|
+
disabled: Boolean(drawerActions.pending), 'data-sn-drawer-menu-trigger': '',
|
|
123
|
+
'aria-haspopup': 'menu', 'aria-expanded': drawerActions.archiveOpen ? 'true' : 'false' },
|
|
124
|
+
{ kind: 'project', label: t('card.project'), onClick: drawerActions.onProject, disabled: Boolean(drawerActions.pending) },
|
|
125
|
+
{ kind: 'delete', label: t('card.delete'), onClick: drawerActions.onDelete, disabled: Boolean(drawerActions.pending) },
|
|
126
|
+
{ kind: 'copy', label: t('drawer.copy'), onClick: drawerActions.onCopy },
|
|
127
|
+
].map(({ kind, label, ...button }) => h('button', {
|
|
128
|
+
key: kind, type: 'button', ...button,
|
|
129
|
+
className: 'SN_drawerActionBtn' + (kind === 'delete' ? ' SN_drawerDeleteBtn' : ''),
|
|
130
|
+
'aria-label': label,
|
|
131
|
+
}, h(ActionIcon, { kind, size: 15 }), h('span', { className: 'SN_aiLabel', 'aria-hidden': 'true' }, label)))),
|
|
132
|
+
h('button', {
|
|
133
|
+
type: 'button', className: 'SN_mini SN_obsidianBtn',
|
|
134
|
+
'aria-label': t('card.obsidian'), onClick: drawerActions.onOpenObsidian,
|
|
135
|
+
}, h(ObsidianIcon, null), h('span', { className: 'SN_obsidianText' }, t('card.obsidian'))),
|
|
136
|
+
drawerActions.archiveOpen
|
|
137
|
+
? h('div', {
|
|
138
|
+
className: 'SN_drawerMenu',
|
|
139
|
+
role: 'menu',
|
|
140
|
+
ref: drawerActions.menuRef,
|
|
141
|
+
'aria-label': t('archive.aria'),
|
|
142
|
+
onClick: (event) => event.stopPropagation(),
|
|
143
|
+
},
|
|
144
|
+
h('div', { className: 'SN_menuTitle' }, t('archive.menuTitle')),
|
|
145
|
+
(Array.isArray(drawerActions.targets) ? drawerActions.targets : []).length === 0
|
|
146
|
+
? h('div', { className: 'SN_menuEmpty' }, t('archive.emptyHint'))
|
|
147
|
+
: (drawerActions.targets || []).map((target) => h('button', {
|
|
148
|
+
key: target.id,
|
|
149
|
+
type: 'button',
|
|
150
|
+
className: 'SN_menuItem',
|
|
151
|
+
role: 'menuitem',
|
|
152
|
+
onClick: (event) => {
|
|
153
|
+
event.stopPropagation();
|
|
154
|
+
drawerActions.onArchive(target.id);
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
h('span', { className: 'SN_menuLabel' }, target.label || target.id),
|
|
158
|
+
h('span', { className: 'SN_menuPath', title: target.path }, target.path),
|
|
159
|
+
)),
|
|
160
|
+
)
|
|
161
|
+
: null,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
: null;
|
|
165
|
+
const drawerNode = h('aside', {
|
|
166
|
+
className: 'SN_noteDrawer',
|
|
167
|
+
'data-pinned': pinned ? 'true' : 'false',
|
|
168
|
+
role: 'dialog',
|
|
169
|
+
'aria-label': t('drawer.aria'),
|
|
170
|
+
style: { ...drawerStyle, '--sn-card-bg': source.color || DEFAULT_NOTE_COLOR, '--sn-card-bg-dark': source.darkColor || source.color || DEFAULT_NOTE_COLOR },
|
|
171
|
+
},
|
|
172
|
+
h('div', {
|
|
173
|
+
className: 'SN_noteDrawerHandle',
|
|
174
|
+
role: 'separator',
|
|
175
|
+
'aria-orientation': 'vertical',
|
|
176
|
+
'aria-label': t('drawer.resize'),
|
|
177
|
+
title: t('drawer.resize'),
|
|
178
|
+
'data-resizing': resizing ? 'true' : 'false',
|
|
179
|
+
onPointerDown: onResizeStart,
|
|
180
|
+
onDoubleClick: onResizeReset,
|
|
181
|
+
}),
|
|
182
|
+
h('div', { className: 'SN_noteDrawerHead' },
|
|
183
|
+
h('div', { className: 'SN_noteDrawerHeadMain' },
|
|
184
|
+
editing
|
|
185
|
+
// 第 4 轮:标题编辑改成多行 textarea,完整换行显示,不再需要横向滚动到文件名尾部。
|
|
186
|
+
? h('textarea', {
|
|
187
|
+
className: 'SN_titleEditor',
|
|
188
|
+
ref: titleEditorRef,
|
|
189
|
+
value: titleDraft,
|
|
190
|
+
rows: 1,
|
|
191
|
+
maxLength: 60,
|
|
192
|
+
wrap: 'soft',
|
|
193
|
+
spellCheck: false,
|
|
194
|
+
'aria-label': '便签文件名',
|
|
195
|
+
onChange: (event) => onTitleChange(event.target.value.replace(/[\r\n]+/g, ' ')),
|
|
196
|
+
onKeyDown: (event) => {
|
|
197
|
+
// 文件名不能换行:Enter 不输入换行(保存仍用 Cmd/Ctrl+S 或按钮)。
|
|
198
|
+
if (event.key === 'Enter') event.preventDefault();
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
: h('h2', {
|
|
202
|
+
className: 'SN_noteDrawerTitle',
|
|
203
|
+
title: title,
|
|
204
|
+
// 0.13:双击标题进入编辑态(编辑态标题就是 input);标题不再带 byline。
|
|
205
|
+
onDoubleClick: canEdit ? onStartEdit : undefined,
|
|
206
|
+
}, title),
|
|
207
|
+
h('div', { className: 'SN_noteDrawerMeta' },
|
|
208
|
+
tag ? h('span', { className: 'SN_noteDrawerTag', title: tag }, tag) : null,
|
|
209
|
+
updated ? h('span', null, updated) : null,
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
h('div', { className: 'SN_noteDrawerHeadActions' },
|
|
213
|
+
h('button', {
|
|
214
|
+
type: 'button',
|
|
215
|
+
className: 'SN_iconBtn',
|
|
216
|
+
'data-active': pinned ? 'true' : 'false',
|
|
217
|
+
'aria-pressed': pinned ? 'true' : 'false',
|
|
218
|
+
'aria-label': pinned ? t('drawer.unlock') : t('drawer.lock'),
|
|
219
|
+
title: pinned ? t('drawer.unlock') : t('drawer.lock'),
|
|
220
|
+
onClick: onTogglePinned,
|
|
221
|
+
}, h(PinIcon, { pinned: pinned })),
|
|
222
|
+
// 第 3 轮:锁定态不渲染关闭按钮(只留锁定/解锁);Esc 仍可关。
|
|
223
|
+
pinned
|
|
224
|
+
? null
|
|
225
|
+
: h('button', {
|
|
226
|
+
type: 'button',
|
|
227
|
+
className: 'SN_iconBtn',
|
|
228
|
+
onClick: onClose,
|
|
229
|
+
'aria-label': t('drawer.close'),
|
|
230
|
+
title: t('drawer.close'),
|
|
231
|
+
}, h(CloseIcon, null)),
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
editing
|
|
235
|
+
? h('div', { className: 'SN_noteEdit' },
|
|
236
|
+
h('textarea', {
|
|
237
|
+
className: 'SN_noteEditor',
|
|
238
|
+
value: bodyDraft,
|
|
239
|
+
spellCheck: false,
|
|
240
|
+
autoFocus: true,
|
|
241
|
+
disabled: saving,
|
|
242
|
+
'aria-label': t('drawer.bodyAria'),
|
|
243
|
+
onChange: (event) => onBodyChange(event.target.value),
|
|
244
|
+
}),
|
|
245
|
+
h('div', { className: 'SN_noteEditBar' },
|
|
246
|
+
h('span', { className: 'SN_noteEditHint' }, saving ? t('drawer.saving') : t('drawer.shortcutHint')),
|
|
247
|
+
h('div', { className: 'SN_noteEditActions' },
|
|
248
|
+
h('button', {
|
|
249
|
+
type: 'button',
|
|
250
|
+
className: 'SN_ghostBtn',
|
|
251
|
+
disabled: saving,
|
|
252
|
+
onClick: onCancelEdit,
|
|
253
|
+
}, t('drawer.cancel')),
|
|
254
|
+
h('button', {
|
|
255
|
+
type: 'button',
|
|
256
|
+
className: 'SN_primaryBtn',
|
|
257
|
+
disabled: saving,
|
|
258
|
+
onClick: onSave,
|
|
259
|
+
}, saving ? t('drawer.saving') : t('drawer.save')),
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
: h(Fragment, null, actionsNode, bodyNode, commentsNode),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (inline) return drawerNode;
|
|
267
|
+
return h('div', { className: 'SN_noteDrawerLayer' }, drawerNode);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export { NoteDrawer };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** 测量浮动阅读面板宽度,为底部输入和拖拽操作栏预留空间。 */
|
|
2
|
+
import { React } from '../react.js';
|
|
3
|
+
|
|
4
|
+
function useDockOffset({ effectiveDrawerWidth, noteDrawer, panelRef, pinned }) {
|
|
5
|
+
const [dockRight, setDockRight] = React.useState(0);
|
|
6
|
+
|
|
7
|
+
React.useLayoutEffect(() => {
|
|
8
|
+
const panel = panelRef.current;
|
|
9
|
+
const drawer = panel?.querySelector('.SN_noteDrawer');
|
|
10
|
+
if (!drawer || pinned) { setDockRight(0); return undefined; }
|
|
11
|
+
// 进场动画会临时平移面板;宽度不受 transform 影响,避免把动画偏移当作留白。
|
|
12
|
+
const measure = () => setDockRight(Math.max(0, drawer.getBoundingClientRect().width));
|
|
13
|
+
measure();
|
|
14
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null;
|
|
15
|
+
observer?.observe(drawer);
|
|
16
|
+
observer?.observe(panel);
|
|
17
|
+
return () => observer?.disconnect();
|
|
18
|
+
}, [Boolean(noteDrawer), pinned, effectiveDrawerWidth]);
|
|
19
|
+
|
|
20
|
+
return { dockRight };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { useDockOffset };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** 抽屉宽度、拖动监听和钉住偏好;卸载时清理指针监听。 */
|
|
2
|
+
import {
|
|
3
|
+
clampDrawerWidth, clearStoredDrawerWidth, readStoredDrawerWidth, readStoredPinned, storeDrawerWidth,
|
|
4
|
+
storePinned,
|
|
5
|
+
} from '../preferences.js';
|
|
6
|
+
import { React } from '../react.js';
|
|
7
|
+
|
|
8
|
+
function useDrawerLayout({ panelRef }) {
|
|
9
|
+
const [drawerWidth, setDrawerWidth] = React.useState(readStoredDrawerWidth);
|
|
10
|
+
const [resizingDrawer, setResizingDrawer] = React.useState(false);
|
|
11
|
+
const resizingRef = React.useRef(false);
|
|
12
|
+
const resizeCleanupRef = React.useRef(null);
|
|
13
|
+
const [pinned, setPinned] = React.useState(readStoredPinned);
|
|
14
|
+
const pinnedUserClosedRef = React.useRef(false);
|
|
15
|
+
const [panelWidth, setPanelWidth] = React.useState(0);
|
|
16
|
+
const effectiveDrawerWidth = drawerWidth == null ? null : clampDrawerWidth(drawerWidth, panelWidth, pinned);
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
const panel = panelRef.current;
|
|
20
|
+
if (!panel || typeof panel.getBoundingClientRect !== 'function') return undefined;
|
|
21
|
+
const measure = () => {
|
|
22
|
+
const rect = panel.getBoundingClientRect();
|
|
23
|
+
if (rect && Number.isFinite(rect.width) && rect.width > 0) setPanelWidth(rect.width);
|
|
24
|
+
};
|
|
25
|
+
measure();
|
|
26
|
+
if (typeof ResizeObserver === 'function') {
|
|
27
|
+
const observer = new ResizeObserver(measure);
|
|
28
|
+
observer.observe(panel);
|
|
29
|
+
return () => observer.disconnect();
|
|
30
|
+
}
|
|
31
|
+
window.addEventListener('resize', measure);
|
|
32
|
+
return () => window.removeEventListener('resize', measure);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
function startDrawerResize(event) {
|
|
36
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
37
|
+
event.stopPropagation();
|
|
38
|
+
const panel = panelRef.current;
|
|
39
|
+
if (!panel || typeof panel.getBoundingClientRect !== 'function') return;
|
|
40
|
+
const rect = panel.getBoundingClientRect();
|
|
41
|
+
if (!rect || rect.width <= 0) return;
|
|
42
|
+
const handle = event.currentTarget;
|
|
43
|
+
if (handle && typeof handle.setPointerCapture === 'function' && event.pointerId !== undefined) {
|
|
44
|
+
try { handle.setPointerCapture(event.pointerId); } catch (error) { /* 忽略 */ }
|
|
45
|
+
}
|
|
46
|
+
resizingRef.current = true;
|
|
47
|
+
setResizingDrawer(true);
|
|
48
|
+
const removeListeners = () => {
|
|
49
|
+
window.removeEventListener('pointermove', onMove);
|
|
50
|
+
window.removeEventListener('pointerup', onFinish);
|
|
51
|
+
window.removeEventListener('pointercancel', onFinish);
|
|
52
|
+
};
|
|
53
|
+
const onMove = (moveEvent) => {
|
|
54
|
+
if (!resizingRef.current) return;
|
|
55
|
+
setDrawerWidth(clampDrawerWidth(rect.right - moveEvent.clientX, rect.width, pinned));
|
|
56
|
+
};
|
|
57
|
+
const onFinish = () => {
|
|
58
|
+
removeListeners();
|
|
59
|
+
resizingRef.current = false;
|
|
60
|
+
resizeCleanupRef.current = null;
|
|
61
|
+
setResizingDrawer(false);
|
|
62
|
+
setDrawerWidth((current) => {
|
|
63
|
+
if (typeof current === 'number' && Number.isFinite(current)) storeDrawerWidth(current);
|
|
64
|
+
return current;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
resizeCleanupRef.current = removeListeners;
|
|
68
|
+
window.addEventListener('pointermove', onMove);
|
|
69
|
+
window.addEventListener('pointerup', onFinish);
|
|
70
|
+
window.addEventListener('pointercancel', onFinish);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resetDrawerWidth() {
|
|
74
|
+
if (resizeCleanupRef.current) {
|
|
75
|
+
resizeCleanupRef.current();
|
|
76
|
+
resizeCleanupRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
resizingRef.current = false;
|
|
79
|
+
setResizingDrawer(false);
|
|
80
|
+
setDrawerWidth(null);
|
|
81
|
+
clearStoredDrawerWidth();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function togglePinned() {
|
|
85
|
+
const next = !pinned;
|
|
86
|
+
setPinned(next);
|
|
87
|
+
storePinned(next);
|
|
88
|
+
}
|
|
89
|
+
React.useEffect(() => () => {
|
|
90
|
+
resizeCleanupRef.current?.();
|
|
91
|
+
resizeCleanupRef.current = null;
|
|
92
|
+
}, []);
|
|
93
|
+
return { resizingDrawer, setResizingDrawer, resizingRef, resizeCleanupRef, pinned, pinnedUserClosedRef, effectiveDrawerWidth, startDrawerResize, resetDrawerWidth, togglePinned };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { useDrawerLayout };
|