dsh-tiddlywiki 0.2.1 → 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 +10 -4
- package/cordis.patch.yml +13 -13
- package/lib/client.bundle.js +2173 -1671
- package/lib/client.js +2183 -1681
- package/lib/index.js +2424 -2225
- 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/settings-page.ts +31 -1
- package/src/client/sidebar-entry.ts +148 -148
- package/src/client/state.ts +39 -39
- package/src/client/styles.ts +88 -5
- 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 +2 -0
- package/src/host/git.ts +13 -3
- package/src/host/routes.ts +151 -1
- 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 +9 -5
- package/src/sdk.ts +198 -198
|
@@ -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
|
+
}
|