dsh-tiddlywiki 0.2.0 → 0.3.0
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 -21
- package/README.md +12 -6
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +2173 -1652
- package/lib/client.js +2183 -1662
- package/lib/index.js +2424 -2263
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/editor-popup.ts +121 -121
- package/src/client/index.ts +2 -0
- package/src/client/markdown.ts +137 -0
- package/src/client/note-widget.ts +515 -373
- package/src/client/panel.ts +51 -25
- package/src/client/settings-page.ts +32 -2
- package/src/client/sidebar-entry.ts +148 -148
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +111 -28
- package/src/client/sync-button.ts +188 -0
- package/src/client/toast.ts +22 -22
- package/src/host/admin.ts +408 -408
- package/src/host/config.ts +4 -2
- package/src/host/git.ts +13 -3
- package/src/host/routes.ts +151 -1
- package/src/host/seed-notes.ts +79 -122
- package/src/host/tools.ts +28 -1
- package/src/host/tw-api.ts +157 -157
- package/src/host/wiki.ts +287 -287
- package/src/index.ts +13 -12
- package/src/sdk.ts +198 -198
package/src/client/styles.ts
CHANGED
|
@@ -51,31 +51,31 @@ html[data-dsh-tw-active] [data-pane="conversation"] > :not([data-dsh-tw-view]),
|
|
|
51
51
|
html[data-dsh-tw-active] [class*="centerCol"] > :not([data-dsh-tw-view]),
|
|
52
52
|
html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view]) { display: none !important; }
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
.dsh-tw-panel-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.dsh-tw-
|
|
75
|
-
.dsh-tw-status-
|
|
76
|
-
.dsh-tw-status-
|
|
77
|
-
.dsh-tw-status-
|
|
78
|
-
.dsh-tw-status-
|
|
54
|
+
/* ── bottom-right status / reload floating buttons ───────────────────
|
|
55
|
+
The top panel bar (title + status + reload row) is gone so TW's menubar
|
|
56
|
+
reaches the panel top; status/reload now live here as floating buttons,
|
|
57
|
+
same visual language as the quick-note toggle. The ui.showPanelStatus
|
|
58
|
+
setting controls whether they mount at all. */
|
|
59
|
+
.dsh-tw-panel-status {
|
|
60
|
+
/* 状态/重载悬浮按钮:右对齐到与快速笔记按钮同一条竖线(right:24),
|
|
61
|
+
垂直排在「同步」按钮(bottom:140)上方;
|
|
62
|
+
两个按钮竖直依次排列(状态在上、重载在下),与其他插件按钮列对齐、不堆叠。 */
|
|
63
|
+
position: fixed; right: 24px; bottom: 192px; z-index: 950;
|
|
64
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
|
65
|
+
}
|
|
66
|
+
.dsh-tw-panel-status-btn {
|
|
67
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
68
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
69
|
+
font: inherit; font-size: 12px; padding: 7px 12px; border-radius: 999px;
|
|
70
|
+
box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16)); cursor: pointer;
|
|
71
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
72
|
+
transition: background-color 120ms ease;
|
|
73
|
+
}
|
|
74
|
+
.dsh-tw-panel-status-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
75
|
+
.dsh-tw-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
76
|
+
.dsh-tw-status-dot[data-state="running"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
77
|
+
.dsh-tw-status-dot[data-state="starting"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
78
|
+
.dsh-tw-status-dot[data-state="failed"], .dsh-tw-status-dot[data-state="stopped"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
79
79
|
|
|
80
80
|
.dsh-tw-panel-frame { flex: 1; min-height: 0; border: 0; width: 100%; display: block; background: #fff; }
|
|
81
81
|
/* [hidden] must beat the author display rules above (UA hidden is overridden). */
|
|
@@ -131,14 +131,74 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
131
131
|
outline: none; border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
132
132
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
133
133
|
}
|
|
134
|
-
.dsh-tw-note-text {
|
|
134
|
+
.dsh-tw-note-text::placeholder { color: var(--dsw-alias-label-dimmed, #999); }
|
|
135
|
+
/* ── Markdown editor (highlighted-textarea overlay) ────────────────
|
|
136
|
+
The <pre> highlight layer and the transparent <textarea> share identical
|
|
137
|
+
metrics (border/padding/font/line-height/white-space) so lines align 1:1. */
|
|
138
|
+
.dsh-tw-note-editor { position: relative; min-width: 0; }
|
|
139
|
+
.dsh-tw-note-editor pre,
|
|
140
|
+
.dsh-tw-note-editor textarea {
|
|
141
|
+
margin: 0; box-sizing: border-box; width: 100%;
|
|
135
142
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18)); border-radius: 8px;
|
|
136
|
-
padding: 8px 9px; font: inherit; font-size: 13px;
|
|
137
|
-
|
|
143
|
+
padding: 8px 9px; font: inherit; font-size: 13px; line-height: 1.5; min-height: 120px;
|
|
144
|
+
white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; tab-size: 4;
|
|
138
145
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
139
146
|
}
|
|
147
|
+
.dsh-tw-note-editor .dsh-tw-note-hl {
|
|
148
|
+
position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0;
|
|
149
|
+
background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
150
|
+
pointer-events: none; overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
.dsh-tw-note-editor .dsh-tw-note-text {
|
|
153
|
+
position: relative; z-index: 1; resize: vertical;
|
|
154
|
+
background: transparent; color: transparent; caret-color: var(--dsw-alias-label-primary, #222);
|
|
155
|
+
overflow: auto;
|
|
156
|
+
}
|
|
157
|
+
.dsh-tw-note-editor .dsh-tw-note-text:focus { outline: none; }
|
|
158
|
+
.dsh-tw-note-drop .dsh-tw-note-text,
|
|
159
|
+
.dsh-tw-note-drop .dsh-tw-note-hl {
|
|
160
|
+
border-color: var(--dsw-alias-brand-primary, #3e63dd);
|
|
161
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 25%, transparent);
|
|
162
|
+
}
|
|
163
|
+
/* ── markdown highlight tokens (colors only — never size/height, so the
|
|
164
|
+
overlay keeps line-for-line alignment with the invisible textarea) ── */
|
|
165
|
+
.dsh-tw-note-hl .md-heading { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
166
|
+
.dsh-tw-note-hl .md-hash { font-weight: 700; color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent); }
|
|
167
|
+
.dsh-tw-note-hl .md-code,
|
|
168
|
+
.dsh-tw-note-hl .md-code-block,
|
|
169
|
+
.dsh-tw-note-hl .md-fence {
|
|
170
|
+
font-family: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace;
|
|
171
|
+
}
|
|
172
|
+
.dsh-tw-note-hl .md-code {
|
|
173
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 14%, transparent);
|
|
174
|
+
border-radius: 4px; padding: 0 3px;
|
|
175
|
+
}
|
|
176
|
+
.dsh-tw-note-hl .md-code-block,
|
|
177
|
+
.dsh-tw-note-hl .md-fence {
|
|
178
|
+
display: block; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent);
|
|
179
|
+
}
|
|
180
|
+
.dsh-tw-note-hl .md-bold { font-weight: 700; }
|
|
181
|
+
.dsh-tw-note-hl .md-italic { font-style: italic; }
|
|
182
|
+
.dsh-tw-note-hl .md-strike { text-decoration: line-through; opacity: .75; }
|
|
183
|
+
.dsh-tw-note-hl .md-link { color: var(--dsw-alias-brand-primary, #3e63dd); text-decoration: underline; }
|
|
184
|
+
.dsh-tw-note-hl .md-url { color: color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 70%, #999); text-decoration: underline dotted; }
|
|
185
|
+
.dsh-tw-note-hl .md-image { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
186
|
+
.dsh-tw-note-hl .md-bullet { font-weight: 700; color: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
187
|
+
.dsh-tw-note-hl .md-number { font-weight: 700; color: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
188
|
+
.dsh-tw-note-hl .md-quote { font-style: italic; color: var(--dsw-alias-label-secondary, #888); }
|
|
189
|
+
.dsh-tw-note-hl .md-hr { color: var(--dsw-alias-label-dimmed, #999); text-decoration: line-through; }
|
|
190
|
+
.dsh-tw-note-hl .md-task { color: var(--dsw-alias-label-secondary, #888); }
|
|
191
|
+
.dsh-tw-note-hl .md-task-checked { color: var(--dsw-alias-state-success-primary, #3eaa5f); font-weight: 600; }
|
|
140
192
|
.dsh-tw-note-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
193
|
+
.dsh-tw-note-foot-left, .dsh-tw-note-foot-right { display: flex; align-items: center; gap: 8px; }
|
|
141
194
|
.dsh-tw-note-hint { font-size: 11px; color: var(--dsw-alias-label-dimmed, #999); }
|
|
195
|
+
.dsh-tw-note-upload {
|
|
196
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
197
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
198
|
+
font: inherit; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer;
|
|
199
|
+
transition: filter 120ms ease;
|
|
200
|
+
}
|
|
201
|
+
.dsh-tw-note-upload:hover { filter: brightness(.96); }
|
|
142
202
|
.dsh-tw-note-save {
|
|
143
203
|
border: 1px solid transparent; background: var(--dsw-alias-brand-primary, #3e63dd); color: #fff;
|
|
144
204
|
font: inherit; font-size: 12px; padding: 6px 16px; border-radius: 8px; cursor: pointer;
|
|
@@ -216,6 +276,29 @@ html[data-dsh-tw-active] .dshDesktopConversationSurface > :not([data-dsh-tw-view
|
|
|
216
276
|
}
|
|
217
277
|
.dsh-tw-note-toggle:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
218
278
|
|
|
279
|
+
/* ── floating sync button (bottom-right) ───────────────────────
|
|
280
|
+
Pill FAB between the quick-note toggle (bottom:88) and the TW panel
|
|
281
|
+
status/reload floaters (bottom:192); dot reflects git state. */
|
|
282
|
+
.dsh-tw-sync {
|
|
283
|
+
position: fixed; right: 24px; bottom: 140px; z-index: 950;
|
|
284
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
285
|
+
padding: 9px 14px; border-radius: 999px;
|
|
286
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0,0,0,.18));
|
|
287
|
+
background: var(--dsw-alias-bg-layer-2, #fff); color: var(--dsw-alias-label-primary, #222);
|
|
288
|
+
font: inherit; font-size: 13px; box-shadow: var(--dsw-shadow-lv3, 0 4px 16px rgba(0,0,0,.16));
|
|
289
|
+
cursor: pointer; transition: background-color 120ms ease;
|
|
290
|
+
}
|
|
291
|
+
.dsh-tw-sync:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.1)); }
|
|
292
|
+
.dsh-tw-sync:disabled { opacity: .65; cursor: default; }
|
|
293
|
+
.dsh-tw-sync-icon { display: inline-flex; flex: none; }
|
|
294
|
+
.dsh-tw-sync-spin .dsh-tw-sync-icon svg { animation: dsh-tw-spin 1s linear infinite; transform-origin: center; }
|
|
295
|
+
@keyframes dsh-tw-spin { to { transform: rotate(360deg); } }
|
|
296
|
+
.dsh-tw-sync-dot { width: 8px; height: 8px; border-radius: 50%; background: #999; flex: none; }
|
|
297
|
+
.dsh-tw-sync-dot[data-state="clean"] { background: var(--dsw-alias-state-success-primary, #3eaa5f); }
|
|
298
|
+
.dsh-tw-sync-dot[data-state="dirty"] { background: var(--dsw-alias-state-warning-primary, #d9822b); }
|
|
299
|
+
.dsh-tw-sync-dot[data-state="behind"] { background: var(--dsw-alias-state-error-primary, #d13b3b); }
|
|
300
|
+
.dsh-tw-sync-dot[data-state="syncing"] { background: var(--dsw-alias-brand-primary, #3e63dd); }
|
|
301
|
+
|
|
219
302
|
/* ── toast ──────────────────────────────────────────────────── */
|
|
220
303
|
.dsh-tw-toast {
|
|
221
304
|
position: fixed; left: 50%; bottom: 96px; transform: translateX(-50%);
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating "sync" button (bottom-right) — one click pulls + commits + pushes
|
|
3
|
+
* the wiki git repository and reports the result, with a live status dot so
|
|
4
|
+
* the human always sees whether the knowledge base is in sync.
|
|
5
|
+
*
|
|
6
|
+
* - honors `ui.showSyncButton`: the button is mounted async and never created
|
|
7
|
+
* when the option is off (settings page toggle);
|
|
8
|
+
* - polls /dsh-tiddlywiki/status every 30s for the git state;
|
|
9
|
+
* - clicking POSTs /dsh-tiddlywiki/sync (pull → commit → push) and toasts the
|
|
10
|
+
* outcome, then refreshes the dot immediately.
|
|
11
|
+
*
|
|
12
|
+
* Status dot mapping (git summary from /status):
|
|
13
|
+
* offline → gray (no repo / service unreachable)
|
|
14
|
+
* dirty → amber (uncommitted changes)
|
|
15
|
+
* behind → red (remote has commits we don't — pull will rebase)
|
|
16
|
+
* clean → green (worktree clean)
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-tiddlywiki/client/sync-button
|
|
19
|
+
*/
|
|
20
|
+
import { toast } from './toast.ts'
|
|
21
|
+
|
|
22
|
+
const STATUS_ENDPOINT = '/dsh-tiddlywiki/status'
|
|
23
|
+
const SYNC_ENDPOINT = '/dsh-tiddlywiki/sync'
|
|
24
|
+
const POLL_MS = 30_000
|
|
25
|
+
|
|
26
|
+
/** Public git summary shape (mirrors GitStatusViewPublic on the host). */
|
|
27
|
+
interface GitView {
|
|
28
|
+
exists?: boolean
|
|
29
|
+
branch?: string
|
|
30
|
+
dirty?: boolean
|
|
31
|
+
dirtyFiles?: string[]
|
|
32
|
+
remote?: string
|
|
33
|
+
lastCommit?: string
|
|
34
|
+
ahead?: number
|
|
35
|
+
behind?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface StatusPayload {
|
|
39
|
+
ok?: boolean
|
|
40
|
+
status?: string
|
|
41
|
+
git?: GitView | null
|
|
42
|
+
ui?: { showSyncButton?: boolean }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Inline sync icon (circular arrows, stroke-based, theme-colored). */
|
|
46
|
+
const SYNC_ICON = '<svg viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 8a5 5 0 1 1-1.4-3.5"/><path d="M13 2.5v3h-3"/></svg>'
|
|
47
|
+
|
|
48
|
+
function pad(n: number): string {
|
|
49
|
+
return n < 10 ? `0${n}` : String(n)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Compact local time for the tooltip (e.g. "10:32"). */
|
|
53
|
+
function clock(date: Date): string {
|
|
54
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchStatus(): Promise<StatusPayload | null> {
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(STATUS_ENDPOINT, { signal: AbortSignal.timeout(8_000) })
|
|
60
|
+
if (!res.ok) return null
|
|
61
|
+
return (await res.json()) as StatusPayload
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Mount the floating sync button. Fetches /status first: when
|
|
69
|
+
* `ui.showSyncButton` is off the button is never created. Returns a disposer.
|
|
70
|
+
*/
|
|
71
|
+
export function mountSyncButton(): () => void {
|
|
72
|
+
let disposed = false
|
|
73
|
+
let root: HTMLButtonElement | undefined
|
|
74
|
+
let dot: HTMLSpanElement | undefined
|
|
75
|
+
let label: HTMLSpanElement | undefined
|
|
76
|
+
let lastSync: Date | undefined
|
|
77
|
+
let timer: number | undefined
|
|
78
|
+
|
|
79
|
+
const build = (): HTMLButtonElement => {
|
|
80
|
+
const btn = document.createElement('button')
|
|
81
|
+
btn.type = 'button'
|
|
82
|
+
btn.className = 'dsh-tw-sync'
|
|
83
|
+
btn.title = '同步知识库(pull → commit → push)'
|
|
84
|
+
|
|
85
|
+
const icon = document.createElement('span')
|
|
86
|
+
icon.className = 'dsh-tw-sync-icon'
|
|
87
|
+
icon.innerHTML = SYNC_ICON
|
|
88
|
+
|
|
89
|
+
dot = document.createElement('span')
|
|
90
|
+
dot.className = 'dsh-tw-sync-dot'
|
|
91
|
+
dot.dataset.state = 'offline'
|
|
92
|
+
|
|
93
|
+
label = document.createElement('span')
|
|
94
|
+
label.className = 'dsh-tw-sync-label'
|
|
95
|
+
label.textContent = '同步'
|
|
96
|
+
|
|
97
|
+
btn.append(icon, label, dot)
|
|
98
|
+
btn.addEventListener('click', () => { void doSync() })
|
|
99
|
+
document.body.append(btn)
|
|
100
|
+
return btn
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Map a git summary onto the dot + label + tooltip. */
|
|
104
|
+
const applyStatus = (payload: StatusPayload | null): void => {
|
|
105
|
+
if (root === undefined || dot === undefined || label === undefined) return
|
|
106
|
+
const git = payload?.git
|
|
107
|
+
const bits: string[] = ['同步知识库']
|
|
108
|
+
let state = 'offline'
|
|
109
|
+
let word = '离线'
|
|
110
|
+
|
|
111
|
+
if (git === null || git === undefined || git.exists !== true) {
|
|
112
|
+
bits.push('git 仓库不可用')
|
|
113
|
+
} else {
|
|
114
|
+
bits.push(`分支 ${git.branch ?? '?'}`)
|
|
115
|
+
if (typeof git.lastCommit === 'string') bits.push(git.lastCommit)
|
|
116
|
+
const behind = typeof git.behind === 'number' ? git.behind : 0
|
|
117
|
+
const ahead = typeof git.ahead === 'number' ? git.ahead : 0
|
|
118
|
+
if (ahead > 0) bits.push(`领先 ${ahead}`)
|
|
119
|
+
if (behind > 0) bits.push(`落后 ${behind}`)
|
|
120
|
+
if (git.dirty === true) {
|
|
121
|
+
state = 'dirty'
|
|
122
|
+
word = '待提交'
|
|
123
|
+
bits.push(`有 ${git.dirtyFiles?.length ?? 0} 个未提交改动`)
|
|
124
|
+
} else if (behind > 0) {
|
|
125
|
+
state = 'behind'
|
|
126
|
+
word = '可更新'
|
|
127
|
+
} else {
|
|
128
|
+
state = 'clean'
|
|
129
|
+
word = '已同步'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (lastSync !== undefined) bits.push(`上次同步 ${clock(lastSync)}`)
|
|
133
|
+
root.title = bits.join(' · ')
|
|
134
|
+
dot.dataset.state = state
|
|
135
|
+
label.textContent = word
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const doSync = async (): Promise<void> => {
|
|
139
|
+
if (root === undefined) return
|
|
140
|
+
root.disabled = true
|
|
141
|
+
root.classList.add('dsh-tw-sync-spin')
|
|
142
|
+
if (label !== undefined) label.textContent = '同步中…'
|
|
143
|
+
if (dot !== undefined) dot.dataset.state = 'syncing'
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(SYNC_ENDPOINT, { method: 'POST', signal: AbortSignal.timeout(120_000) })
|
|
146
|
+
const payload = (await res.json().catch(() => null)) as
|
|
147
|
+
| { ok?: boolean; message?: string; error?: string; status?: GitView; conflictFiles?: string[]; push?: string; changed?: boolean; restarted?: boolean; restartError?: string }
|
|
148
|
+
| null
|
|
149
|
+
lastSync = new Date()
|
|
150
|
+
if (payload === null || payload.ok !== true) {
|
|
151
|
+
const msg = payload?.error ?? payload?.message ?? `HTTP ${res.status}`
|
|
152
|
+
toast(`同步失败:${msg}`)
|
|
153
|
+
} else {
|
|
154
|
+
let detail = ''
|
|
155
|
+
if (payload.push && payload.push !== 'nothing to commit') detail = `(${payload.push})`
|
|
156
|
+
if (payload.changed === true) {
|
|
157
|
+
detail += payload.restarted === true ? ',TW 已重启' : ',TW 未自动重启'
|
|
158
|
+
if (payload.restartError) detail += `(${payload.restartError})`
|
|
159
|
+
}
|
|
160
|
+
toast(`同步完成:${payload.message ?? 'OK'}${detail}`)
|
|
161
|
+
}
|
|
162
|
+
applyStatus(await fetchStatus())
|
|
163
|
+
} catch (err) {
|
|
164
|
+
toast(`同步失败:${err instanceof Error ? err.message : String(err)}`)
|
|
165
|
+
} finally {
|
|
166
|
+
root.disabled = false
|
|
167
|
+
root.classList.remove('dsh-tw-sync-spin')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const poll = async (): Promise<void> => {
|
|
172
|
+
applyStatus(await fetchStatus())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
void (async () => {
|
|
176
|
+
const payload = await fetchStatus()
|
|
177
|
+
if (disposed || payload?.ui?.showSyncButton === false) return
|
|
178
|
+
root = build()
|
|
179
|
+
applyStatus(payload)
|
|
180
|
+
timer = window.setInterval(() => { void poll() }, POLL_MS)
|
|
181
|
+
})()
|
|
182
|
+
|
|
183
|
+
return () => {
|
|
184
|
+
disposed = true
|
|
185
|
+
if (timer !== undefined) window.clearInterval(timer)
|
|
186
|
+
root?.remove()
|
|
187
|
+
}
|
|
188
|
+
}
|
package/src/client/toast.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One-shot transient toast, shared by the quick-note widget and the settings
|
|
3
|
-
* page. Reuses a single body-level element (`.dsh-tw-toast`) with a show
|
|
4
|
-
* class; the element is created lazily and styled by styles.ts.
|
|
5
|
-
*
|
|
6
|
-
* @module dsh-tiddlywiki/client/toast
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export function toast(message: string): void {
|
|
10
|
-
let el = document.querySelector<HTMLElement>('.dsh-tw-toast')
|
|
11
|
-
if (el === null) {
|
|
12
|
-
el = document.createElement('div')
|
|
13
|
-
el.className = 'dsh-tw-toast'
|
|
14
|
-
document.body.append(el)
|
|
15
|
-
}
|
|
16
|
-
el.textContent = message
|
|
17
|
-
el.classList.add('dsh-tw-toast-show')
|
|
18
|
-
clearTimeout((el as HTMLElement & { __twToastTimer?: number }).__twToastTimer)
|
|
19
|
-
;(el as HTMLElement & { __twToastTimer?: number }).__twToastTimer = window.setTimeout(() => {
|
|
20
|
-
el?.classList.remove('dsh-tw-toast-show')
|
|
21
|
-
}, 2_500)
|
|
22
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* One-shot transient toast, shared by the quick-note widget and the settings
|
|
3
|
+
* page. Reuses a single body-level element (`.dsh-tw-toast`) with a show
|
|
4
|
+
* class; the element is created lazily and styled by styles.ts.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-tiddlywiki/client/toast
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function toast(message: string): void {
|
|
10
|
+
let el = document.querySelector<HTMLElement>('.dsh-tw-toast')
|
|
11
|
+
if (el === null) {
|
|
12
|
+
el = document.createElement('div')
|
|
13
|
+
el.className = 'dsh-tw-toast'
|
|
14
|
+
document.body.append(el)
|
|
15
|
+
}
|
|
16
|
+
el.textContent = message
|
|
17
|
+
el.classList.add('dsh-tw-toast-show')
|
|
18
|
+
clearTimeout((el as HTMLElement & { __twToastTimer?: number }).__twToastTimer)
|
|
19
|
+
;(el as HTMLElement & { __twToastTimer?: number }).__twToastTimer = window.setTimeout(() => {
|
|
20
|
+
el?.classList.remove('dsh-tw-toast-show')
|
|
21
|
+
}, 2_500)
|
|
22
|
+
}
|