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,50 @@
|
|
|
1
|
+
/** 空态与 toast 的展示组件;操作由调用方传入。 */
|
|
2
|
+
import { h } from '../react.js';
|
|
3
|
+
|
|
4
|
+
function CenterState(props) {
|
|
5
|
+
return h('div', { className: 'SN_center' },
|
|
6
|
+
props.mark ? h('div', { className: 'SN_centerMark', 'aria-hidden': 'true' }, props.mark) : null,
|
|
7
|
+
h('h2', { className: 'SN_centerTitle' }, props.title),
|
|
8
|
+
props.text ? h('p', { className: 'SN_centerText' }, props.text) : null,
|
|
9
|
+
props.actionLabel
|
|
10
|
+
? h('button', {
|
|
11
|
+
type: 'button',
|
|
12
|
+
className: 'SN_primaryBtn',
|
|
13
|
+
onClick: props.onAction,
|
|
14
|
+
disabled: Boolean(props.actionDisabled),
|
|
15
|
+
}, props.actionLabel)
|
|
16
|
+
: null,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ToastStack(props) {
|
|
21
|
+
const { toasts, onDismiss } = props;
|
|
22
|
+
if (!toasts || toasts.length === 0) return null;
|
|
23
|
+
return h('div', { className: 'SN_toasts', 'aria-live': 'polite' },
|
|
24
|
+
toasts.map((item) => {
|
|
25
|
+
// 0.13:一条 toast 可以带多个操作按钮(如「去设置 / 知道了」);兼容旧的单个 action。
|
|
26
|
+
const actions = Array.isArray(item.actions) && item.actions.length > 0
|
|
27
|
+
? item.actions
|
|
28
|
+
: (item.action ? [item.action] : []);
|
|
29
|
+
return h('div', {
|
|
30
|
+
key: item.id,
|
|
31
|
+
className: 'SN_toast',
|
|
32
|
+
'data-kind': item.kind,
|
|
33
|
+
role: 'status',
|
|
34
|
+
},
|
|
35
|
+
h('span', { className: 'SN_toastText' }, item.text),
|
|
36
|
+
actions.map((action, index) => h('button', {
|
|
37
|
+
key: index,
|
|
38
|
+
type: 'button',
|
|
39
|
+
className: 'SN_toastAction',
|
|
40
|
+
onClick: () => {
|
|
41
|
+
onDismiss(item.id);
|
|
42
|
+
action.run();
|
|
43
|
+
},
|
|
44
|
+
}, action.label)),
|
|
45
|
+
);
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { CenterState, ToastStack };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** 手写 SVG 图标,只负责渲染。 */
|
|
2
|
+
import { h } from '../react.js';
|
|
3
|
+
|
|
4
|
+
function PanelIcon(props) {
|
|
5
|
+
const size = props && typeof props.size === 'number' && props.size > 0 ? props.size : 18;
|
|
6
|
+
const active = Boolean(props && props.active);
|
|
7
|
+
return h('svg', {
|
|
8
|
+
width: size,
|
|
9
|
+
height: size,
|
|
10
|
+
viewBox: '0 0 24 24',
|
|
11
|
+
fill: 'none',
|
|
12
|
+
'aria-hidden': 'true',
|
|
13
|
+
focusable: 'false',
|
|
14
|
+
style: { display: 'block', opacity: active ? 1 : 0.92 },
|
|
15
|
+
},
|
|
16
|
+
h('path', {
|
|
17
|
+
d: 'M6 3.25h11.25a2.5 2.5 0 0 1 2.5 2.5v9.1L14.6 20.75H6a2.5 2.5 0 0 1-2.5-2.5V5.75a2.5 2.5 0 0 1 2.5-2.5Z',
|
|
18
|
+
stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round',
|
|
19
|
+
}),
|
|
20
|
+
h('path', {
|
|
21
|
+
d: 'M19.75 14.85h-3.4a2.5 2.5 0 0 0-2.5 2.5v3.4',
|
|
22
|
+
stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round',
|
|
23
|
+
}),
|
|
24
|
+
h('path', {
|
|
25
|
+
d: 'M7.3 8.4h7.4M7.3 11.9h4.9',
|
|
26
|
+
stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', opacity: 0.8,
|
|
27
|
+
}),
|
|
28
|
+
h('circle', { cx: 17.9, cy: 6.1, r: 2.15, fill: '#B4472F' }),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function RefreshIcon(props) {
|
|
33
|
+
const spinning = Boolean(props && props.spinning);
|
|
34
|
+
return h('svg', {
|
|
35
|
+
width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none',
|
|
36
|
+
'aria-hidden': 'true', focusable: 'false',
|
|
37
|
+
className: spinning ? 'SN_spin' : undefined,
|
|
38
|
+
style: { display: 'block' },
|
|
39
|
+
},
|
|
40
|
+
h('path', {
|
|
41
|
+
d: 'M20 11.2a8 8 0 1 0-.9 4.6',
|
|
42
|
+
stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round',
|
|
43
|
+
}),
|
|
44
|
+
h('path', {
|
|
45
|
+
d: 'M20.2 4.9v5.2h-5.2',
|
|
46
|
+
stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function GearIcon() {
|
|
52
|
+
return h('svg', {
|
|
53
|
+
width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none',
|
|
54
|
+
'aria-hidden': 'true', focusable: 'false', style: { display: 'block' },
|
|
55
|
+
},
|
|
56
|
+
h('circle', { cx: 12, cy: 12, r: 3.1, stroke: 'currentColor', strokeWidth: 1.7 }),
|
|
57
|
+
h('path', {
|
|
58
|
+
d: 'M19.4 12c0-.5-.05-1-.15-1.45l2-1.55-1.9-3.3-2.4 1a8.6 8.6 0 0 0-2.5-1.45L14.2 2.6h-4.4l-.25 2.65A8.6 8.6 0 0 0 7.05 6.7l-2.4-1-1.9 3.3 2 1.55a8.7 8.7 0 0 0 0 2.9l-2 1.55 1.9 3.3 2.4-1a8.6 8.6 0 0 0 2.5 1.45l.25 2.65h4.4l.25-2.65a8.6 8.6 0 0 0 2.5-1.45l2.4 1 1.9-3.3-2-1.55c.1-.45.15-.95.15-1.45Z',
|
|
59
|
+
stroke: 'currentColor', strokeWidth: 1.3, strokeLinejoin: 'round',
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CloseIcon() {
|
|
65
|
+
return h('svg', {
|
|
66
|
+
width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none',
|
|
67
|
+
'aria-hidden': 'true', focusable: 'false', style: { display: 'block' },
|
|
68
|
+
},
|
|
69
|
+
h('path', { d: 'M6.5 6.5l11 11M17.5 6.5l-11 11', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round' }),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function FolderTreeIcon() {
|
|
74
|
+
return h('svg', {
|
|
75
|
+
width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none',
|
|
76
|
+
'aria-hidden': 'true', focusable: 'false', style: { display: 'block' },
|
|
77
|
+
},
|
|
78
|
+
h('path', {
|
|
79
|
+
d: 'M3.2 6.4h5.2l1.9 2.2h10.5a1.7 1.7 0 0 1 1.7 1.7v9.1a1.7 1.7 0 0 1-1.7 1.7H3.2a1.7 1.7 0 0 1-1.7-1.7V8.1a1.7 1.7 0 0 1 1.7-1.7Z',
|
|
80
|
+
stroke: 'currentColor', strokeWidth: 1.5, strokeLinejoin: 'round',
|
|
81
|
+
}),
|
|
82
|
+
h('path', {
|
|
83
|
+
d: 'M7 11.6v5M7 13.9h3.2M7 16.6h3.2',
|
|
84
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function PinIcon(props) {
|
|
90
|
+
const pinned = Boolean(props && props.pinned);
|
|
91
|
+
return h('svg', {
|
|
92
|
+
width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none',
|
|
93
|
+
'aria-hidden': 'true', focusable: 'false', style: { display: 'block' },
|
|
94
|
+
},
|
|
95
|
+
h('path', {
|
|
96
|
+
d: 'M9.3 3.6h5.4l-.8 5.1 3 2.7v1.2H7.1v-1.2l3-2.7-.8-5.1Z',
|
|
97
|
+
fill: pinned ? 'currentColor' : 'none',
|
|
98
|
+
stroke: 'currentColor', strokeWidth: 1.5, strokeLinejoin: 'round',
|
|
99
|
+
}),
|
|
100
|
+
h('path', {
|
|
101
|
+
d: 'M12 12.6v7.2',
|
|
102
|
+
stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round',
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function SortIcon() {
|
|
108
|
+
return h('svg', {
|
|
109
|
+
width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none',
|
|
110
|
+
'aria-hidden': 'true', focusable: 'false', style: { display: 'block' },
|
|
111
|
+
},
|
|
112
|
+
h('path', { d: 'M4 7h11M4 12h8M4 17h5', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round' }),
|
|
113
|
+
h('path', { d: 'M17.5 7.5v9.5M14.7 14.4l2.8 2.8 2.8-2.8', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ObsidianIcon() {
|
|
118
|
+
return h('svg', {
|
|
119
|
+
width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none',
|
|
120
|
+
'aria-hidden': 'true', focusable: 'false', style: { display: 'block', flex: 'none' },
|
|
121
|
+
},
|
|
122
|
+
h('path', { d: 'M13.4 4.4h6.2v6.2', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }),
|
|
123
|
+
h('path', { d: 'M19.6 4.4l-8 8', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
|
|
124
|
+
h('path', { d: 'M18.6 14.2v3.8a2 2 0 0 1-2 2H5.4a2 2 0 0 1-2-2V6.8a2 2 0 0 1 2-2h3.8', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' }),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function CalendarIcon() {
|
|
129
|
+
return h('svg', {
|
|
130
|
+
width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none',
|
|
131
|
+
'aria-hidden': 'true', focusable: 'false', style: { display: 'block' },
|
|
132
|
+
},
|
|
133
|
+
h('rect', {
|
|
134
|
+
x: 3.2, y: 5.2, width: 17.6, height: 15.6, rx: 2.4,
|
|
135
|
+
stroke: 'currentColor', strokeWidth: 1.5,
|
|
136
|
+
}),
|
|
137
|
+
h('path', {
|
|
138
|
+
d: 'M3.4 9.6h17.2M8.2 3.4v3.4M15.8 3.4v3.4',
|
|
139
|
+
stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round',
|
|
140
|
+
}),
|
|
141
|
+
h('path', {
|
|
142
|
+
d: 'M7.4 13.3h2.1M11.1 13.3h2.1M14.8 13.3h2.1M7.4 16.8h2.1M11.1 16.8h2.1',
|
|
143
|
+
stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', opacity: 0.72,
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ActionIcon({ kind, size = 20 }) {
|
|
149
|
+
const paths = {
|
|
150
|
+
delete: 'M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13M10 10v7m4-7v7',
|
|
151
|
+
archive: 'M4 4h16v4H4zM6 8v12h12V8M10 12h4',
|
|
152
|
+
project: 'M3 7V5h6l2 2h10v12H3zM12 10v6m-3-3h6',
|
|
153
|
+
copy: 'M8 8h12v12H8zM16 8V4H4v12h4',
|
|
154
|
+
up: 'M12 19V5m-6 6 6-6 6 6',
|
|
155
|
+
};
|
|
156
|
+
return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': 'true' },
|
|
157
|
+
h('path', { d: paths[kind], stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { PanelIcon, RefreshIcon, GearIcon, CloseIcon, FolderTreeIcon, PinIcon, SortIcon, ObsidianIcon, CalendarIcon, ActionIcon };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** 持有通知列表与计时器,关闭或卸载时清理资源。 */
|
|
2
|
+
import { MAX_TOASTS, TOAST_MS } from '../constants.js';
|
|
3
|
+
import { React } from '../react.js';
|
|
4
|
+
|
|
5
|
+
function useToasts() {
|
|
6
|
+
const [toasts, setToasts] = React.useState([]);
|
|
7
|
+
const toastSeqRef = React.useRef(0);
|
|
8
|
+
const toastTimersRef = React.useRef(new Map());
|
|
9
|
+
|
|
10
|
+
function dismissToast(id) {
|
|
11
|
+
const timer = toastTimersRef.current.get(id);
|
|
12
|
+
if (timer !== undefined) {
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
toastTimersRef.current.delete(id);
|
|
15
|
+
}
|
|
16
|
+
setToasts((list) => list.filter((item) => item.id !== id));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function pushToast(toast) {
|
|
20
|
+
if (!toast || typeof toast.text !== 'string' || toast.text === '') return;
|
|
21
|
+
toastSeqRef.current += 1;
|
|
22
|
+
const id = toastSeqRef.current;
|
|
23
|
+
const item = {
|
|
24
|
+
id: id,
|
|
25
|
+
kind: toast.kind || 'info',
|
|
26
|
+
text: toast.text,
|
|
27
|
+
action: toast.action || null,
|
|
28
|
+
actions: Array.isArray(toast.actions) && toast.actions.length > 0 ? toast.actions : null,
|
|
29
|
+
};
|
|
30
|
+
setToasts((list) => {
|
|
31
|
+
const next = list.concat(item);
|
|
32
|
+
return next.length > MAX_TOASTS ? next.slice(next.length - MAX_TOASTS) : next;
|
|
33
|
+
});
|
|
34
|
+
const timeout = typeof toast.timeout === 'number' ? toast.timeout : TOAST_MS;
|
|
35
|
+
if (timeout > 0) {
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
toastTimersRef.current.delete(id);
|
|
38
|
+
setToasts((list) => list.filter((entry) => entry.id !== id));
|
|
39
|
+
}, timeout);
|
|
40
|
+
toastTimersRef.current.set(id, timer);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pushErrorToast(error) {
|
|
45
|
+
pushToast({
|
|
46
|
+
text: error && error.message ? error.message : String(error),
|
|
47
|
+
kind: 'error',
|
|
48
|
+
timeout: 7000,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
React.useEffect(() => () => {
|
|
52
|
+
for (const timer of toastTimersRef.current.values()) clearTimeout(timer);
|
|
53
|
+
toastTimersRef.current.clear();
|
|
54
|
+
}, []);
|
|
55
|
+
return { toasts, dismissToast, pushToast, pushErrorToast };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { useToasts };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** 浏览器协议标识、展示默认值与资源限额;跨端限额由 parity 测试约束。 */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const NS = 'sticky-notes';
|
|
5
|
+
|
|
6
|
+
const CHANNEL = '/sticky-notes';
|
|
7
|
+
|
|
8
|
+
const PANEL_ID = 'sticky-notes';
|
|
9
|
+
|
|
10
|
+
const POLL_BASE = 4000;
|
|
11
|
+
|
|
12
|
+
const POLL_MAX = 30000;
|
|
13
|
+
|
|
14
|
+
const POLL_HIDDEN = 15000;
|
|
15
|
+
|
|
16
|
+
const TOAST_MS = 5000;
|
|
17
|
+
|
|
18
|
+
const UNDO_MS = 6000;
|
|
19
|
+
|
|
20
|
+
const MAX_CARD_COMMENTS = 2;
|
|
21
|
+
|
|
22
|
+
const MAX_TOASTS = 4;
|
|
23
|
+
|
|
24
|
+
const CAPTURE_IMAGES = 8;
|
|
25
|
+
|
|
26
|
+
const CAPTURE_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
const CAPTURE_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
29
|
+
|
|
30
|
+
const CAPTURE_SOURCE_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
31
|
+
|
|
32
|
+
const CAPTURE_SOURCE_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
const CAPTURE_LIMIT_TEXT = Object.freeze({
|
|
35
|
+
images: CAPTURE_IMAGES,
|
|
36
|
+
imageLimit: `${CAPTURE_IMAGE_BYTES / 1024 / 1024} MB`,
|
|
37
|
+
totalLimit: `${CAPTURE_TOTAL_BYTES / 1024 / 1024} MB`,
|
|
38
|
+
sourceImageLimit: `${CAPTURE_SOURCE_IMAGE_BYTES / 1024 / 1024} MB`,
|
|
39
|
+
sourceTotalLimit: `${CAPTURE_SOURCE_TOTAL_BYTES / 1024 / 1024} MB`,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const CARD_IMAGES = 3;
|
|
43
|
+
|
|
44
|
+
const CARD_IMAGE_MAX_BYTES = 1500000;
|
|
45
|
+
|
|
46
|
+
const IMAGE_CACHE_MAX = 160;
|
|
47
|
+
|
|
48
|
+
const IMAGE_CACHE_BUDGET = 24 * 1024 * 1024;
|
|
49
|
+
|
|
50
|
+
const IMAGE_CONCURRENCY = 3;
|
|
51
|
+
|
|
52
|
+
const WALL_PAGE_SIZE = 50;
|
|
53
|
+
|
|
54
|
+
const WALL_PRELOAD_PX = 320;
|
|
55
|
+
|
|
56
|
+
const SORT_KEY = 'dsh-sticky-notes.sort.v2';
|
|
57
|
+
|
|
58
|
+
const DEFAULT_SORT = 'created-desc';
|
|
59
|
+
|
|
60
|
+
const SORT_OPTIONS = [
|
|
61
|
+
{ id: 'created-desc', labelKey: 'sort.createdDesc' },
|
|
62
|
+
{ id: 'created-asc', labelKey: 'sort.createdAsc' },
|
|
63
|
+
{ id: 'mtime-desc', labelKey: 'sort.mtimeDesc' },
|
|
64
|
+
{ id: 'name-asc', labelKey: 'sort.nameAsc' },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const DATE_FILTER_KEY = 'dsh-sticky-notes.dateFilter.v1';
|
|
68
|
+
|
|
69
|
+
const DATE_FILTER_ENABLED_KEY = 'dsh-sticky-notes.dateFilterEnabled.v1';
|
|
70
|
+
|
|
71
|
+
const DATE_REALTIME_KEY = 'dsh-sticky-notes.dateRealtime.v1';
|
|
72
|
+
|
|
73
|
+
const DEFAULT_DATE_FILTER = 'today';
|
|
74
|
+
|
|
75
|
+
const DEFAULT_DATE_FILTER_ENABLED = true;
|
|
76
|
+
|
|
77
|
+
const DATE_FILTERS = [
|
|
78
|
+
{ id: 'today', labelKey: 'date.today', days: 1 },
|
|
79
|
+
{ id: '3d', labelKey: 'date.3d', days: 3 },
|
|
80
|
+
{ id: 'week', labelKey: 'date.week', days: 7 },
|
|
81
|
+
{ id: 'month', labelKey: 'date.month', days: 30 },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
85
|
+
|
|
86
|
+
const DRAWER_WIDTH_KEY = 'dsh-sticky-notes.drawerWidth';
|
|
87
|
+
|
|
88
|
+
const DRAWER_MIN_WIDTH = 320;
|
|
89
|
+
|
|
90
|
+
const DRAWER_MAX_WIDTH = 960;
|
|
91
|
+
|
|
92
|
+
const PINNED_WALL_MIN_WIDTH = 260;
|
|
93
|
+
|
|
94
|
+
const PINNED_KEY = 'dsh-sticky-notes.panelLocked';
|
|
95
|
+
|
|
96
|
+
const PINNED_NOTE_KEY = 'dsh-sticky-notes.pinnedNote';
|
|
97
|
+
|
|
98
|
+
const EMPTY_SETTINGS = {
|
|
99
|
+
vaultPath: '',
|
|
100
|
+
notesDir: '',
|
|
101
|
+
projectDir: '',
|
|
102
|
+
archiveTargets: [],
|
|
103
|
+
emojis: [],
|
|
104
|
+
projectPrompt: '',
|
|
105
|
+
includeSubfolders: true,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const EMPTY_STATE = {
|
|
109
|
+
loaded: false,
|
|
110
|
+
configured: false,
|
|
111
|
+
revision: null,
|
|
112
|
+
version: '',
|
|
113
|
+
notes: [],
|
|
114
|
+
jobs: [],
|
|
115
|
+
settings: null,
|
|
116
|
+
error: null,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export {
|
|
120
|
+
NS, CHANNEL, PANEL_ID, POLL_BASE, POLL_MAX, POLL_HIDDEN, TOAST_MS, UNDO_MS, MAX_CARD_COMMENTS,
|
|
121
|
+
MAX_TOASTS, CAPTURE_IMAGES, CAPTURE_IMAGE_BYTES, CAPTURE_TOTAL_BYTES, CAPTURE_SOURCE_IMAGE_BYTES,
|
|
122
|
+
CAPTURE_SOURCE_TOTAL_BYTES, CAPTURE_LIMIT_TEXT, CARD_IMAGES, CARD_IMAGE_MAX_BYTES, IMAGE_CACHE_MAX,
|
|
123
|
+
IMAGE_CACHE_BUDGET, IMAGE_CONCURRENCY, WALL_PAGE_SIZE, WALL_PRELOAD_PX, SORT_KEY, DEFAULT_SORT,
|
|
124
|
+
SORT_OPTIONS, DATE_FILTER_KEY, DATE_FILTER_ENABLED_KEY, DATE_REALTIME_KEY, DEFAULT_DATE_FILTER,
|
|
125
|
+
DEFAULT_DATE_FILTER_ENABLED, DATE_FILTERS, DAY_MS, DRAWER_WIDTH_KEY, DRAWER_MIN_WIDTH, DRAWER_MAX_WIDTH,
|
|
126
|
+
PINNED_WALL_MIN_WIDTH, PINNED_KEY, PINNED_NOTE_KEY, EMPTY_SETTINGS, EMPTY_STATE,
|
|
127
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** 合并 Host 快照、复用未变对象并叠加运行中任务状态。 */
|
|
2
|
+
import { EMPTY_STATE } from '../constants.js';
|
|
3
|
+
|
|
4
|
+
function runningCommentJobs(jobs) {
|
|
5
|
+
const running = new Map();
|
|
6
|
+
if (!Array.isArray(jobs)) return running;
|
|
7
|
+
for (const job of jobs) {
|
|
8
|
+
if (!job || job.kind !== 'comment' || job.status !== 'running') continue;
|
|
9
|
+
if (typeof job.path !== 'string') continue;
|
|
10
|
+
running.set(job.path, job);
|
|
11
|
+
}
|
|
12
|
+
return running;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pendingFromJob(job, settings) {
|
|
16
|
+
const emojis = settings && Array.isArray(settings.emojis) ? settings.emojis : [];
|
|
17
|
+
const emoji = emojis.find((item) => item && item.id === job.emojiId);
|
|
18
|
+
if (emoji) return { emoji: emoji.emoji || '✍️', label: emoji.label || '' };
|
|
19
|
+
return { emoji: '✍️', label: '' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function decorateNotes(notes, jobs, settings) {
|
|
23
|
+
if (!Array.isArray(notes)) return [];
|
|
24
|
+
const running = runningCommentJobs(jobs);
|
|
25
|
+
let changed = false;
|
|
26
|
+
const next = notes.map((note) => {
|
|
27
|
+
if (!note || typeof note.path !== 'string') return note;
|
|
28
|
+
const job = running.get(note.path);
|
|
29
|
+
const pending = job ? pendingFromJob(job, settings) : null;
|
|
30
|
+
const current = note.pending || null;
|
|
31
|
+
if (pending === null && current === null) return note;
|
|
32
|
+
if (pending !== null && current !== null && pending.emoji === current.emoji && pending.label === current.label) return note;
|
|
33
|
+
changed = true;
|
|
34
|
+
return Object.assign({}, note, { pending: pending });
|
|
35
|
+
});
|
|
36
|
+
return changed ? next : notes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function jobsSignature(jobs) {
|
|
40
|
+
if (!Array.isArray(jobs)) return '';
|
|
41
|
+
return jobs
|
|
42
|
+
.map((job) => {
|
|
43
|
+
const id = job && job.id != null ? job.id : '';
|
|
44
|
+
const status = job && job.status ? job.status : '';
|
|
45
|
+
const error = job && job.error ? job.error : '';
|
|
46
|
+
const errorCode = job && job.errorCode ? job.errorCode : '';
|
|
47
|
+
const cut = job && job.truncated === true ? 'cut' : '';
|
|
48
|
+
return id + ':' + status + ':' + error + ':' + errorCode + ':' + cut;
|
|
49
|
+
})
|
|
50
|
+
.sort()
|
|
51
|
+
.join('|');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function settingsSignature(settings) {
|
|
55
|
+
if (settings == null) return '';
|
|
56
|
+
try {
|
|
57
|
+
return JSON.stringify(settings);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reuseNotes(previous, incoming) {
|
|
64
|
+
const byPath = new Map(previous.map((note) => [note.path, note]));
|
|
65
|
+
const notes = incoming.map((note) => {
|
|
66
|
+
const old = byPath.get(note.path);
|
|
67
|
+
return old && JSON.stringify(old) === JSON.stringify(note) ? old : note;
|
|
68
|
+
});
|
|
69
|
+
return notes.length === previous.length && notes.every((note, index) => note === previous[index]) ? previous : notes;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mergeState(prev, value) {
|
|
73
|
+
const base = prev || EMPTY_STATE;
|
|
74
|
+
if (!value || typeof value !== 'object') return base;
|
|
75
|
+
const incomingSettings = value.settings && typeof value.settings === 'object' ? value.settings : base.settings;
|
|
76
|
+
const settings = settingsSignature(incomingSettings) === settingsSignature(base.settings) ? base.settings : incomingSettings;
|
|
77
|
+
const incomingJobs = Array.isArray(value.jobs) ? value.jobs : base.jobs;
|
|
78
|
+
const jobs = JSON.stringify(incomingJobs) === JSON.stringify(base.jobs) ? base.jobs : incomingJobs;
|
|
79
|
+
const configured = value.configured === true;
|
|
80
|
+
const error = typeof value.error === 'string' && value.error !== '' ? value.error : null;
|
|
81
|
+
const revision = typeof value.revision === 'string' ? value.revision : base.revision;
|
|
82
|
+
const version = typeof value.version === 'string' && value.version !== '' ? value.version : (base.version || '');
|
|
83
|
+
const unchanged = value.unchanged === true;
|
|
84
|
+
|
|
85
|
+
// 快路径:unchanged 且 configured/error/revision/version/settings/jobs 签名、notes 引用都没变 → 原样返回。
|
|
86
|
+
if (unchanged && base.loaded === true
|
|
87
|
+
&& configured === base.configured
|
|
88
|
+
&& error === base.error
|
|
89
|
+
&& revision === base.revision
|
|
90
|
+
&& version === base.version
|
|
91
|
+
&& settingsSignature(settings) === settingsSignature(base.settings)
|
|
92
|
+
&& jobsSignature(jobs) === jobsSignature(base.jobs)) {
|
|
93
|
+
return base;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let notes;
|
|
97
|
+
if (unchanged) {
|
|
98
|
+
notes = base.notes;
|
|
99
|
+
} else {
|
|
100
|
+
const revisionChanged = base.loaded !== true || revision !== base.revision;
|
|
101
|
+
notes = revisionChanged && Array.isArray(value.notes) ? reuseNotes(base.notes, value.notes) : base.notes;
|
|
102
|
+
}
|
|
103
|
+
if (!configured) notes = [];
|
|
104
|
+
return {
|
|
105
|
+
loaded: true,
|
|
106
|
+
configured,
|
|
107
|
+
revision,
|
|
108
|
+
version,
|
|
109
|
+
notes: decorateNotes(notes, jobs, settings),
|
|
110
|
+
jobs,
|
|
111
|
+
settings,
|
|
112
|
+
error,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { pendingFromJob, decorateNotes, jobsSignature, mergeState };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** 请求代次、取消和可见性轮询;支持仅加载配置的官方设置页。 */
|
|
2
|
+
import { EMPTY_STATE, POLL_BASE, POLL_HIDDEN } from '../constants.js';
|
|
3
|
+
import { mergeState } from './state.js';
|
|
4
|
+
import { panelIsVisible } from './visibility.js';
|
|
5
|
+
import { nextPollDelay } from '../preferences.js';
|
|
6
|
+
import { React } from '../react.js';
|
|
7
|
+
import { callRpc } from '../runtime.js';
|
|
8
|
+
import { isAbortError } from '../utils.js';
|
|
9
|
+
|
|
10
|
+
function createStateReader(options) {
|
|
11
|
+
let generation = 0;
|
|
12
|
+
let active = null;
|
|
13
|
+
function invalidate() {
|
|
14
|
+
generation += 1;
|
|
15
|
+
if (active) active.abort();
|
|
16
|
+
active = null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
invalidate,
|
|
20
|
+
async read(forceFull) {
|
|
21
|
+
invalidate();
|
|
22
|
+
const ticket = generation;
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
active = controller;
|
|
25
|
+
try {
|
|
26
|
+
const value = await options.call({ knownRevision: forceFull ? null : options.getRevision() }, controller.signal);
|
|
27
|
+
if (ticket !== generation || controller.signal.aborted) return null;
|
|
28
|
+
active = null;
|
|
29
|
+
options.onValue(value);
|
|
30
|
+
return value;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (ticket !== generation || controller.signal.aborted) return null;
|
|
33
|
+
throw error;
|
|
34
|
+
} finally {
|
|
35
|
+
if (active === controller) active = null;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function useNotesData(options) {
|
|
42
|
+
const optionsRef = React.useRef(options);
|
|
43
|
+
optionsRef.current = options;
|
|
44
|
+
const [view, setView] = React.useState(EMPTY_STATE);
|
|
45
|
+
const viewRef = React.useRef(EMPTY_STATE);
|
|
46
|
+
const [loadError, setLoadError] = React.useState(null);
|
|
47
|
+
const aliveRef = React.useRef(true);
|
|
48
|
+
const controllersRef = React.useRef(new Set());
|
|
49
|
+
const pollDelayRef = React.useRef(POLL_BASE);
|
|
50
|
+
const lastPollErrorRef = React.useRef(null);
|
|
51
|
+
const readerRef = React.useRef(null);
|
|
52
|
+
if (readerRef.current === null) {
|
|
53
|
+
readerRef.current = createStateReader({
|
|
54
|
+
call: (payload, signal) => callRpc(optionsRef.current.settingsOnly ? 'settings' : 'state', payload, signal),
|
|
55
|
+
getRevision: () => viewRef.current.revision,
|
|
56
|
+
onValue(value) {
|
|
57
|
+
if (!aliveRef.current) return;
|
|
58
|
+
lastPollErrorRef.current = null;
|
|
59
|
+
setLoadError(null);
|
|
60
|
+
const prev = viewRef.current;
|
|
61
|
+
const next = mergeState(prev, value);
|
|
62
|
+
if (next !== prev) {
|
|
63
|
+
viewRef.current = next;
|
|
64
|
+
setView(next);
|
|
65
|
+
}
|
|
66
|
+
optionsRef.current.onState(next, prev);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function updateView(next) {
|
|
71
|
+
// 乐观写入使已有的读取快照过期,防止随后返回的旧列表覆盖本地结果。
|
|
72
|
+
readerRef.current.invalidate();
|
|
73
|
+
viewRef.current = next;
|
|
74
|
+
setView(next);
|
|
75
|
+
}
|
|
76
|
+
async function readState(forceFull) {
|
|
77
|
+
try {
|
|
78
|
+
return await readerRef.current.read(forceFull);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!aliveRef.current || isAbortError(error)) return null;
|
|
81
|
+
const message = error && error.message ? error.message : String(error);
|
|
82
|
+
setLoadError(message);
|
|
83
|
+
if (lastPollErrorRef.current !== message) optionsRef.current.onError(error);
|
|
84
|
+
lastPollErrorRef.current = message;
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function refreshNow(forceFull) {
|
|
89
|
+
pollDelayRef.current = POLL_BASE;
|
|
90
|
+
return readState(forceFull);
|
|
91
|
+
}
|
|
92
|
+
function request(endpoint, payload) {
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
controllersRef.current.add(controller);
|
|
95
|
+
return callRpc(endpoint, payload, controller.signal).finally(() => controllersRef.current.delete(controller));
|
|
96
|
+
}
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
aliveRef.current = true;
|
|
99
|
+
let disposed = false;
|
|
100
|
+
let timer = null;
|
|
101
|
+
function clearTimer() { if (timer !== null) clearTimeout(timer); timer = null; }
|
|
102
|
+
function schedule(delay) {
|
|
103
|
+
if (disposed) return;
|
|
104
|
+
clearTimer();
|
|
105
|
+
timer = setTimeout(run, delay);
|
|
106
|
+
}
|
|
107
|
+
async function run() {
|
|
108
|
+
if (disposed) return;
|
|
109
|
+
if (!panelIsVisible(optionsRef.current.panelRef.current)) { schedule(POLL_HIDDEN); return; }
|
|
110
|
+
const value = await readState(false);
|
|
111
|
+
if (disposed) return;
|
|
112
|
+
if (value) pollDelayRef.current = nextPollDelay(pollDelayRef.current, value.unchanged === true);
|
|
113
|
+
schedule(pollDelayRef.current);
|
|
114
|
+
}
|
|
115
|
+
function onVisibilityChange() {
|
|
116
|
+
clearTimer();
|
|
117
|
+
if (typeof document !== 'undefined' && document.hidden) { readerRef.current.invalidate(); return; }
|
|
118
|
+
pollDelayRef.current = POLL_BASE;
|
|
119
|
+
optionsRef.current.onVisible();
|
|
120
|
+
run();
|
|
121
|
+
}
|
|
122
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisibilityChange);
|
|
123
|
+
run();
|
|
124
|
+
return () => {
|
|
125
|
+
disposed = true;
|
|
126
|
+
aliveRef.current = false;
|
|
127
|
+
clearTimer();
|
|
128
|
+
readerRef.current.invalidate();
|
|
129
|
+
for (const controller of controllersRef.current) controller.abort();
|
|
130
|
+
controllersRef.current.clear();
|
|
131
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
134
|
+
return { view, viewRef, loadError, aliveRef, controllersRef, updateView, request, refreshNow };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { createStateReader, useNotesData };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** 判断面板和文档可见性,供轮询与快捷键使用。 */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function panelIsVisible(panel) {
|
|
5
|
+
if (typeof document !== 'undefined' && document.hidden) return false;
|
|
6
|
+
if (!panel || typeof panel.getClientRects !== 'function') return true;
|
|
7
|
+
try {
|
|
8
|
+
return panel.getClientRects().length > 0;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { panelIsVisible };
|