dsh-plugin 0.1.1 → 1.0.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/README.en.md +49 -26
- package/README.md +49 -26
- package/client/client.js +2148 -229
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +396 -0
- package/lib/index.js +17 -8
- package/lib/services/install-types.js +11 -0
- package/lib/services/install.js +4 -0
- package/lib/services/installed-versions.js +51 -0
- package/lib/services/loader.js +98 -0
- package/lib/services/pending-restart.js +34 -0
- package/lib/services/preflight.js +94 -0
- package/lib/services/profile.js +88 -0
- package/lib/services/progress.js +50 -0
- package/lib/services/task-queue.js +417 -0
- package/lib/types/http/routes.d.ts +23 -0
- package/lib/types/index.d.ts +27 -0
- package/lib/types/services/install-types.d.ts +63 -0
- package/lib/types/services/install.d.ts +18 -0
- package/lib/types/services/installed-versions.d.ts +14 -0
- package/lib/types/services/loader.d.ts +43 -0
- package/lib/types/services/pending-restart.d.ts +13 -0
- package/lib/types/services/preflight.d.ts +6 -0
- package/lib/types/services/profile.d.ts +24 -0
- package/lib/types/services/progress.d.ts +17 -0
- package/lib/types/services/task-queue.d.ts +76 -0
- package/package.json +27 -6
- package/src/client/components/CatalogControls.tsx +104 -0
- package/src/client/components/CatalogHeader.tsx +78 -0
- package/src/client/components/CatalogList.tsx +75 -0
- package/src/client/components/CategoryTabs.tsx +35 -0
- package/src/client/components/Dropdown.tsx +80 -0
- package/src/client/components/NotificationsModal.tsx +230 -0
- package/src/client/components/PluginCard.tsx +104 -0
- package/src/client/components/PluginHubSection.tsx +339 -0
- package/src/client/components/ProgressView.tsx +23 -0
- package/src/client/components/icons.tsx +125 -0
- package/src/client/components/modals.tsx +365 -0
- package/src/client/hooks/useCatalog.ts +192 -0
- package/src/client/hooks/useTaskQueue.ts +405 -0
- package/src/client/index.tsx +8 -422
- package/src/client/lib/catalog.ts +204 -0
- package/src/client/lib/env.ts +16 -0
- package/src/client/lib/failures.ts +153 -0
- package/src/client/lib/format.ts +22 -0
- package/src/client/locales.ts +186 -18
- package/src/client/styles/Dropdown.module.css +123 -0
- package/src/client/styles/Header.module.css +432 -0
- package/src/client/styles/List.module.css +324 -0
- package/src/client/styles/Modal.module.css +965 -0
- package/src/client/styles/tokens.module.css +86 -0
- package/src/client/types.ts +80 -0
- package/src/server/http/routes.ts +397 -0
- package/src/server/index.ts +35 -0
- package/src/server/services/install-types.ts +63 -0
- package/src/server/services/install.ts +18 -0
- package/src/server/services/installed-versions.ts +65 -0
- package/src/server/services/loader.ts +110 -0
- package/src/server/services/pending-restart.ts +36 -0
- package/src/server/services/preflight.ts +97 -0
- package/src/server/services/profile.ts +89 -0
- package/src/server/services/progress.ts +48 -0
- package/src/server/services/task-queue.ts +450 -0
- package/src/client/Section.module.css +0 -589
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Hub section: wires the catalog data pipeline, the background task
|
|
3
|
+
* queue and the feedback state together. Data/queue logic lives in hooks/,
|
|
4
|
+
* rendering is delegated to the small presentational components in this
|
|
5
|
+
* folder; only the dialogs/toast and the section-level copy actions remain.
|
|
6
|
+
*/
|
|
7
|
+
import { createElement as h, useEffect, useState } from 'react'
|
|
8
|
+
import styles from '../styles/Header.module.css'
|
|
9
|
+
import { en, zh } from '../locales.ts'
|
|
10
|
+
import type { EnvInfo, HubPlugin, LocaleId, SectionProps, ToastState } from '../types.ts'
|
|
11
|
+
import { langPathOf } from '../lib/catalog.ts'
|
|
12
|
+
import { getEnv } from '../lib/env.ts'
|
|
13
|
+
import { useCatalog } from '../hooks/useCatalog.ts'
|
|
14
|
+
import { useTaskQueue } from '../hooks/useTaskQueue.ts'
|
|
15
|
+
import { ErrorModal, InstallModal, UninstallModal, Toast } from './modals.tsx'
|
|
16
|
+
import { NotificationsModal } from './NotificationsModal.tsx'
|
|
17
|
+
import { addFailure, addSuccess, clearNotifications, loadNotifications } from '../lib/failures.ts'
|
|
18
|
+
import type { NotificationRecord } from '../lib/failures.ts'
|
|
19
|
+
import { CatalogHeader } from './CatalogHeader.tsx'
|
|
20
|
+
import { CategoryTabs } from './CategoryTabs.tsx'
|
|
21
|
+
import { CatalogControls } from './CatalogControls.tsx'
|
|
22
|
+
import { CatalogList } from './CatalogList.tsx'
|
|
23
|
+
|
|
24
|
+
export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
25
|
+
/** 界面语言:默认跟随宿主(系统)语言;右上角按钮可手动切换,切换后以手动选择为准 */
|
|
26
|
+
const [manualLang, setManualLang] = useState<LocaleId | null>(null)
|
|
27
|
+
const lang: LocaleId = manualLang ?? locale.getSnapshot().active
|
|
28
|
+
const langKey: LocaleId = lang === 'en' ? 'en' : 'zh'
|
|
29
|
+
// dsh-plugin.org keeps zh pages under the /zh/ prefix; en is the root.
|
|
30
|
+
const langPath = langPathOf(lang)
|
|
31
|
+
|
|
32
|
+
// 界面语言跟随宿主(系统)locale 自动切换;宿主的 t() 绑定宿主 locale,
|
|
33
|
+
// 这里基于本地字典自建翻译函数,与宿主 locale 保持一致。
|
|
34
|
+
// (settings.section 的导航 label 仍在 apply 里用宿主 t(),跟随宿主语言。)
|
|
35
|
+
const dict: Record<string, string> = langKey === 'en' ? en : zh
|
|
36
|
+
const t = (key: string, params?: Record<string, string | number>): string => {
|
|
37
|
+
const raw = dict[key] ?? key
|
|
38
|
+
return params ? raw.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? '')) : raw
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const catalog = useCatalog(lang)
|
|
42
|
+
|
|
43
|
+
/** 全局反馈 Toast:{id} 用于重复触发时重新走入场动画,kind 决定文案与配色 */
|
|
44
|
+
const [toast, setToast] = useState<ToastState | null>(null)
|
|
45
|
+
/** 复制反馈:记录刚复制安装命令的仓库,按钮短暂切换为「已复制」样式 */
|
|
46
|
+
const [copied, setCopied] = useState<string | null>(null)
|
|
47
|
+
/** 信任确认弹窗:记录待安装的插件,确认后才执行复制 */
|
|
48
|
+
const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
|
|
49
|
+
/** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
|
|
50
|
+
const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
|
|
51
|
+
/** 卸载确认弹窗:记录待卸载的插件 */
|
|
52
|
+
const [uninstallPlugin, setUninstallPlugin] = useState<HubPlugin | null>(null)
|
|
53
|
+
/** 安装/卸载完成后的结果视图:停留弹窗内,点「完成」关闭 */
|
|
54
|
+
const [installDone, setInstallDone] = useState(false)
|
|
55
|
+
const [uninstallDone, setUninstallDone] = useState(false)
|
|
56
|
+
/** 结果视图「立即重启」:请求宿主重启后进入等待,服务回来后整页刷新 */
|
|
57
|
+
const [restarting, setRestarting] = useState(false)
|
|
58
|
+
/** 操作失败完整信息 + 所属插件仓库 + 失败类型(决定弹窗标题「安装失败/卸载失败」) */
|
|
59
|
+
const [errorMsg, setErrorMsg] = useState<{ message: string; repo: string | null; kind: 'install' | 'uninstall' } | null>(null)
|
|
60
|
+
/** 安装/卸载任务通知记录:localStorage 持久化,成败即落盘,即使错过弹窗也能回来查看 */
|
|
61
|
+
const [notifications, setNotifications] = useState<NotificationRecord[]>(() => loadNotifications())
|
|
62
|
+
const [showNotifications, setShowNotifications] = useState(false)
|
|
63
|
+
/** 宿主机器环境快照:提交 bug 的 issue 正文附带;取不到为 null(链接少环境段,不阻塞) */
|
|
64
|
+
const [env, setEnv] = useState<EnvInfo | null>(null)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
let alive = true
|
|
67
|
+
void getEnv().then((info) => { if (alive) setEnv(info) })
|
|
68
|
+
return () => { alive = false }
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
const queue = useTaskQueue({
|
|
72
|
+
t,
|
|
73
|
+
refreshInstalled: catalog.refreshInstalled,
|
|
74
|
+
onInstallDone: (viaModal, repo) => {
|
|
75
|
+
if (viaModal) setInstallDone(true)
|
|
76
|
+
else setToast({ id: Date.now(), kind: 'done' })
|
|
77
|
+
// 安装成功也写入通知记录:通知中心里成功与失败都能看到
|
|
78
|
+
setNotifications(addSuccess({ kind: 'install', repo: repo ?? '' }))
|
|
79
|
+
},
|
|
80
|
+
onUninstallDone: (viaModal, repo) => {
|
|
81
|
+
if (viaModal) setUninstallDone(true)
|
|
82
|
+
else setToast({ id: Date.now(), kind: 'removed' })
|
|
83
|
+
setNotifications(addSuccess({ kind: 'uninstall', repo: repo ?? '' }))
|
|
84
|
+
},
|
|
85
|
+
onError: (message, repo, kind) => {
|
|
86
|
+
setErrorMsg({ message, repo, kind })
|
|
87
|
+
// 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕
|
|
88
|
+
setNotifications(addFailure({ kind, repo: repo ?? '', message }))
|
|
89
|
+
},
|
|
90
|
+
installPlugin: confirmPlugin,
|
|
91
|
+
uninstallPlugin,
|
|
92
|
+
installedName: catalog.installedName,
|
|
93
|
+
// 待重启项展示信息补齐:从目录按 owner/repo 解析插件简介/版本(找不到返回 null,行内只显示仓库名)
|
|
94
|
+
resolvePending: (repo) => {
|
|
95
|
+
const p = catalog.plugins?.find((x) => x.source?.repo === repo)
|
|
96
|
+
return p ? { desc: p.description, version: p.version } : null
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// 信任/卸载/通知记录弹窗打开时按 Esc 关闭;任务进入后台队列后可随时关闭(任务继续)
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!confirmPlugin && !uninstallPlugin && !showNotifications) return
|
|
103
|
+
const onKey = (e: KeyboardEvent) => {
|
|
104
|
+
if (e.key === 'Escape') {
|
|
105
|
+
setConfirmPlugin(null)
|
|
106
|
+
setUninstallPlugin(null)
|
|
107
|
+
setUninstallDone(false)
|
|
108
|
+
setShowNotifications(false)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
window.addEventListener('keydown', onKey)
|
|
112
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
113
|
+
}, [confirmPlugin, uninstallPlugin, showNotifications])
|
|
114
|
+
|
|
115
|
+
// Toast 统一自动消失
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!toast) return
|
|
118
|
+
const timer = window.setTimeout(() => setToast(null), 2400)
|
|
119
|
+
return () => window.clearTimeout(timer)
|
|
120
|
+
}, [toast])
|
|
121
|
+
|
|
122
|
+
/** 复制文本到剪贴板,返回是否成功(Clipboard API + 隐藏 textarea 兜底)。 */
|
|
123
|
+
const doCopy = async (text: string): Promise<boolean> => {
|
|
124
|
+
try {
|
|
125
|
+
await navigator.clipboard.writeText(text)
|
|
126
|
+
return true
|
|
127
|
+
} catch {
|
|
128
|
+
// Clipboard API unavailable (permissions/iframe) — fall back to the
|
|
129
|
+
// legacy hidden-textarea trick, which works in any context.
|
|
130
|
+
const ta = document.createElement('textarea')
|
|
131
|
+
ta.value = text
|
|
132
|
+
ta.style.position = 'fixed'
|
|
133
|
+
ta.style.opacity = '0'
|
|
134
|
+
document.body.appendChild(ta)
|
|
135
|
+
ta.select()
|
|
136
|
+
let ok = false
|
|
137
|
+
try {
|
|
138
|
+
ok = document.execCommand('copy')
|
|
139
|
+
} catch {
|
|
140
|
+
ok = false
|
|
141
|
+
}
|
|
142
|
+
document.body.removeChild(ta)
|
|
143
|
+
return ok
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 弹窗动作一:复制安装命令到剪贴板,引导去终端粘贴执行。 */
|
|
148
|
+
const copyCommand = async (p: HubPlugin) => {
|
|
149
|
+
const repo = p.source?.repo ?? ''
|
|
150
|
+
const ok = await doCopy(`dsh plugin add github:${repo}`)
|
|
151
|
+
if (ok) {
|
|
152
|
+
setCopied(repo)
|
|
153
|
+
setToast({ id: Date.now(), kind: 'copied' })
|
|
154
|
+
window.setTimeout(() => setCopied((cur) => (cur === repo ? null : cur)), 1600)
|
|
155
|
+
}
|
|
156
|
+
setConfirmPlugin(null)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 卸载弹窗动作:复制卸载命令,万一直接卸载失败可去终端手动执行。 */
|
|
160
|
+
const copyUninstallCommand = async () => {
|
|
161
|
+
const name = uninstallPlugin ? catalog.installedName(uninstallPlugin) : null
|
|
162
|
+
if (!name) return
|
|
163
|
+
const ok = await doCopy(`dsh plugin remove ${name}`)
|
|
164
|
+
if (ok) setToast({ id: Date.now(), kind: 'copied' })
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 「立即重启」:POST 同源 /restart,宿主进程自杀重启;随后轮询服务恢复后整页刷新。 */
|
|
168
|
+
const requestRestart = async () => {
|
|
169
|
+
if (restarting) return
|
|
170
|
+
setRestarting(true)
|
|
171
|
+
try {
|
|
172
|
+
// 响应可能在宿主进程被 kill 前返回,也可能直接断连 —— 两种都属正常
|
|
173
|
+
await fetch('/dsh-plugin-hub/restart', { method: 'POST' })
|
|
174
|
+
} catch {
|
|
175
|
+
/* 服务已终止,无需处理 */
|
|
176
|
+
}
|
|
177
|
+
// 轮询同源端点,服务恢复后整页 reload(让新挂载的 bundle 完全初始化)
|
|
178
|
+
let attempts = 0
|
|
179
|
+
const poll = async () => {
|
|
180
|
+
attempts += 1
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch('/dsh-plugin-hub/active', { method: 'GET', cache: 'no-store' })
|
|
183
|
+
if (res.ok) {
|
|
184
|
+
window.location.reload()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
/* 服务尚未恢复 */
|
|
189
|
+
}
|
|
190
|
+
if (attempts < 60) {
|
|
191
|
+
window.setTimeout(poll, 1000)
|
|
192
|
+
} else {
|
|
193
|
+
setRestarting(false)
|
|
194
|
+
setToast({ id: Date.now(), kind: 'fail' })
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
window.setTimeout(poll, 1200)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { total, installedName, installedVersion, hasUpdate } = catalog
|
|
201
|
+
// 统计数字:官网 /api/stats.json 优先;拉取失败时用已加载列表兜底,避免展示 undefined。
|
|
202
|
+
const statsTotal = catalog.stats?.total ?? total
|
|
203
|
+
const statsVerified = catalog.stats?.verified ?? 0
|
|
204
|
+
|
|
205
|
+
const count = catalog.visible.length
|
|
206
|
+
|
|
207
|
+
return h('div', { className: styles.root },
|
|
208
|
+
h(CatalogHeader, {
|
|
209
|
+
t,
|
|
210
|
+
langPath,
|
|
211
|
+
statsTotal,
|
|
212
|
+
statsVerified,
|
|
213
|
+
onToggleLang: () => setManualLang(lang === 'en' ? 'zh' : 'en'),
|
|
214
|
+
}),
|
|
215
|
+
h(CategoryTabs, {
|
|
216
|
+
category: catalog.category,
|
|
217
|
+
setCategory: catalog.setCategory,
|
|
218
|
+
allLabel: t('all'),
|
|
219
|
+
totalCount: catalog.total,
|
|
220
|
+
langKey,
|
|
221
|
+
}),
|
|
222
|
+
h(CatalogControls, {
|
|
223
|
+
query: catalog.query,
|
|
224
|
+
setQuery: catalog.setQuery,
|
|
225
|
+
sort: catalog.sort,
|
|
226
|
+
setSort: catalog.setSort,
|
|
227
|
+
installedFilter: catalog.installedFilter,
|
|
228
|
+
setInstalledFilter: catalog.setInstalledFilter,
|
|
229
|
+
// 已安装/未安装按钮计数跟随当前分类:切到某分类即显示该分类下的已装/未装数量
|
|
230
|
+
installedCount: catalog.installedCountInCategory,
|
|
231
|
+
notInstalledCount: catalog.notInstalledCountInCategory,
|
|
232
|
+
t,
|
|
233
|
+
// 列表头部「筛选出 N 条」:分类/搜索/安装状态筛选后的结果数;全部视图且未筛选时显示总数
|
|
234
|
+
resultText: catalog.plugins === null || catalog.failed
|
|
235
|
+
? null
|
|
236
|
+
: catalog.category === 'all' && count === total
|
|
237
|
+
? t('pluginsTotal', { n: count })
|
|
238
|
+
: t('filterResults', { n: count }),
|
|
239
|
+
// 通知入口计数:历史通知记录数 + 进行中任务数 + 待重启数 ——
|
|
240
|
+
// 待重启不可清除,装完等重启时必须持续亮着提示「必须重启」,不能因为队列空了就归零
|
|
241
|
+
// 红圈白字数字(进行中的任务见通知中心「进行中」分区,待重启见「待重启」分区)
|
|
242
|
+
noticeCount: notifications.length + queue.queue.length + queue.pendingRestarts.length,
|
|
243
|
+
onOpenNotifications: () => setShowNotifications(true),
|
|
244
|
+
}),
|
|
245
|
+
h(CatalogList, {
|
|
246
|
+
plugins: catalog.plugins,
|
|
247
|
+
failed: catalog.failed,
|
|
248
|
+
visible: catalog.visible,
|
|
249
|
+
total,
|
|
250
|
+
t,
|
|
251
|
+
langPath,
|
|
252
|
+
reload: catalog.reload,
|
|
253
|
+
category: catalog.category,
|
|
254
|
+
installedFilter: catalog.installedFilter,
|
|
255
|
+
copied,
|
|
256
|
+
installedName,
|
|
257
|
+
installedVersion,
|
|
258
|
+
hasUpdate,
|
|
259
|
+
langKey,
|
|
260
|
+
onInstall: (p, opts) => {
|
|
261
|
+
setInstallDone(false)
|
|
262
|
+
setConfirmIsUpdate(opts?.update ?? false)
|
|
263
|
+
// 与卸载一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务而禁用安装按钮
|
|
264
|
+
queue.clearModalTask()
|
|
265
|
+
setConfirmPlugin(p)
|
|
266
|
+
},
|
|
267
|
+
onUninstall: (p) => {
|
|
268
|
+
setUninstallDone(false)
|
|
269
|
+
queue.clearModalTask()
|
|
270
|
+
setUninstallPlugin(p)
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
// 安装确认弹窗:锁定/进度/结果视图逻辑收敛在 modals.tsx;更新与安装同入口,仅标记 update
|
|
274
|
+
confirmPlugin && h(InstallModal, {
|
|
275
|
+
plugin: confirmPlugin,
|
|
276
|
+
done: installDone,
|
|
277
|
+
task: queue.installModalTask,
|
|
278
|
+
t,
|
|
279
|
+
langPath,
|
|
280
|
+
restarting,
|
|
281
|
+
update: confirmIsUpdate,
|
|
282
|
+
submitting: queue.submitting,
|
|
283
|
+
onClose: () => setConfirmPlugin(null),
|
|
284
|
+
onCopy: () => copyCommand(confirmPlugin),
|
|
285
|
+
onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true } : undefined),
|
|
286
|
+
onRestart: () => { void requestRestart() },
|
|
287
|
+
}),
|
|
288
|
+
// 卸载确认弹窗:确认/进度/结果视图逻辑收敛在 modals.tsx
|
|
289
|
+
uninstallPlugin && h(UninstallModal, {
|
|
290
|
+
plugin: uninstallPlugin,
|
|
291
|
+
done: uninstallDone,
|
|
292
|
+
task: queue.uninstallModalTask,
|
|
293
|
+
t,
|
|
294
|
+
langPath,
|
|
295
|
+
restarting,
|
|
296
|
+
submitting: queue.submitting,
|
|
297
|
+
onClose: () => {
|
|
298
|
+
setUninstallDone(false)
|
|
299
|
+
setUninstallPlugin(null)
|
|
300
|
+
},
|
|
301
|
+
onCancel: () => setUninstallPlugin(null),
|
|
302
|
+
onCopyCommand: copyUninstallCommand,
|
|
303
|
+
onConfirm: () => queue.uninstallNow(uninstallPlugin),
|
|
304
|
+
onRestart: () => { void requestRestart() },
|
|
305
|
+
}),
|
|
306
|
+
toast && h(Toast, { toast, t }),
|
|
307
|
+
// 操作失败:完整错误弹窗(重复安装、CLI 失败、请求错误等),可复制并去插件仓库反馈
|
|
308
|
+
errorMsg && h(ErrorModal, {
|
|
309
|
+
message: errorMsg.message,
|
|
310
|
+
repo: errorMsg.repo,
|
|
311
|
+
kind: errorMsg.kind,
|
|
312
|
+
t,
|
|
313
|
+
env,
|
|
314
|
+
onCopy: (text: string) => {
|
|
315
|
+
doCopy(text)
|
|
316
|
+
setToast({ id: Date.now(), kind: 'errCopied' })
|
|
317
|
+
},
|
|
318
|
+
onClose: () => setErrorMsg(null),
|
|
319
|
+
}),
|
|
320
|
+
// 安装/卸载任务通知记录:本地持久化的成败清单,随时可回来查看/复制/清空;
|
|
321
|
+
// 进行中的任务(实时进度)与待重启插件也一并展示,关掉任务弹窗后仍可盯着
|
|
322
|
+
showNotifications && h(NotificationsModal, {
|
|
323
|
+
records: notifications,
|
|
324
|
+
tasks: queue.queue,
|
|
325
|
+
pendingRestarts: queue.pendingRestarts,
|
|
326
|
+
t,
|
|
327
|
+
env,
|
|
328
|
+
onClose: () => setShowNotifications(false),
|
|
329
|
+
onCopy: (text) => {
|
|
330
|
+
doCopy(text)
|
|
331
|
+
setToast({ id: Date.now(), kind: 'errCopied' })
|
|
332
|
+
},
|
|
333
|
+
onClear: () => setNotifications(clearNotifications()),
|
|
334
|
+
cancelTask: queue.cancelTask,
|
|
335
|
+
restarting,
|
|
336
|
+
onRestart: () => { void requestRestart() },
|
|
337
|
+
}),
|
|
338
|
+
)
|
|
339
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline progress block shared by the install and uninstall dialogs:
|
|
3
|
+
* a real percentage bar (server-estimated, client-polled). No live terminal
|
|
4
|
+
* window — pnpm emits nothing during git clone/build, so a log pane would be
|
|
5
|
+
* empty for most of the run; failures surface their full output in ErrorModal.
|
|
6
|
+
*/
|
|
7
|
+
import { createElement as h } from 'react'
|
|
8
|
+
import styles from '../styles/Modal.module.css'
|
|
9
|
+
import type { TaskState } from '../types.ts'
|
|
10
|
+
|
|
11
|
+
export function ProgressView({ task }: { task: TaskState }) {
|
|
12
|
+
return h('div', { className: styles.progress },
|
|
13
|
+
h('div', { className: styles.progressHead },
|
|
14
|
+
h('span', { className: styles.progressText }, `${Math.round(task.progress)}%`),
|
|
15
|
+
),
|
|
16
|
+
h('div', { className: styles.progressTrack },
|
|
17
|
+
h('div', {
|
|
18
|
+
className: task.status === 'failed' ? `${styles.progressFill} ${styles.progressFillFail}` : styles.progressFill,
|
|
19
|
+
style: { width: `${task.progress}%` },
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline SVG icons used across the client. Icons inherit currentColor so
|
|
3
|
+
* they adapt to themes and disabled states automatically.
|
|
4
|
+
*/
|
|
5
|
+
import { createElement as h } from 'react'
|
|
6
|
+
import headerStyles from '../styles/Header.module.css'
|
|
7
|
+
import modalStyles from '../styles/Modal.module.css'
|
|
8
|
+
|
|
9
|
+
/** 弹窗右上角关闭按钮图标:内联 SVG 十字(stroke 继承 currentColor),随按钮禁用态一起变淡。 */
|
|
10
|
+
export function CloseIcon() {
|
|
11
|
+
return h('svg', {
|
|
12
|
+
className: modalStyles.modalCloseIcon,
|
|
13
|
+
viewBox: '0 0 16 16',
|
|
14
|
+
width: 14,
|
|
15
|
+
height: 14,
|
|
16
|
+
fill: 'none',
|
|
17
|
+
'aria-hidden': 'true',
|
|
18
|
+
}, h('path', {
|
|
19
|
+
d: 'M3 3l10 10M13 3L3 13',
|
|
20
|
+
stroke: 'currentColor',
|
|
21
|
+
strokeWidth: 1.6,
|
|
22
|
+
strokeLinecap: 'round',
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 来源链接图标:链节(stroke 继承 currentColor),弹窗里官网收录地址旁提示「可点击跳转」。 */
|
|
27
|
+
export function LinkIcon() {
|
|
28
|
+
return h('svg', {
|
|
29
|
+
className: modalStyles.linkIcon,
|
|
30
|
+
viewBox: '0 0 24 24',
|
|
31
|
+
width: 13,
|
|
32
|
+
height: 13,
|
|
33
|
+
fill: 'none',
|
|
34
|
+
'aria-hidden': 'true',
|
|
35
|
+
},
|
|
36
|
+
h('path', {
|
|
37
|
+
d: 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',
|
|
38
|
+
stroke: 'currentColor',
|
|
39
|
+
strokeWidth: 1.8,
|
|
40
|
+
strokeLinecap: 'round',
|
|
41
|
+
strokeLinejoin: 'round',
|
|
42
|
+
}),
|
|
43
|
+
h('path', {
|
|
44
|
+
d: 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',
|
|
45
|
+
stroke: 'currentColor',
|
|
46
|
+
strokeWidth: 1.8,
|
|
47
|
+
strokeLinecap: 'round',
|
|
48
|
+
strokeLinejoin: 'round',
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 品牌 logo 图标:蓝紫渐变圆角方块 + 白色拼图块(2x2 错落),标题左侧的品牌标识。 */
|
|
53
|
+
export function LogoIcon() {
|
|
54
|
+
return h('svg', {
|
|
55
|
+
className: headerStyles.logoIcon,
|
|
56
|
+
viewBox: '0 0 24 24',
|
|
57
|
+
width: 20,
|
|
58
|
+
height: 20,
|
|
59
|
+
'aria-hidden': 'true',
|
|
60
|
+
},
|
|
61
|
+
h('defs', null,
|
|
62
|
+
h('linearGradient', { id: 'dshLogoGrad', x1: '0', y1: '0', x2: '1', y2: '1' },
|
|
63
|
+
h('stop', { offset: '0%', stopColor: '#4f46e5' }),
|
|
64
|
+
h('stop', { offset: '100%', stopColor: '#7c3aed' }),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
h('rect', { x: 1, y: 1, width: 22, height: 22, rx: 6, fill: 'url(#dshLogoGrad)' }),
|
|
68
|
+
h('rect', { x: 5.5, y: 5.5, width: 5.5, height: 5.5, rx: 1.5, fill: '#ffffff' }),
|
|
69
|
+
h('rect', { x: 13, y: 5.5, width: 5.5, height: 5.5, rx: 1.5, fill: '#ffffff', opacity: 0.55 }),
|
|
70
|
+
h('rect', { x: 5.5, y: 13, width: 5.5, height: 5.5, rx: 1.5, fill: '#ffffff', opacity: 0.55 }),
|
|
71
|
+
h('rect', { x: 13, y: 13, width: 5.5, height: 5.5, rx: 1.5, fill: '#ffffff' }),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** GitHub 图标:官方 GitHub Mark(octocat),fill 继承 currentColor,头部右上角源码链接用。 */
|
|
76
|
+
export function GitHubIcon() {
|
|
77
|
+
return h('svg', {
|
|
78
|
+
className: headerStyles.githubIcon,
|
|
79
|
+
viewBox: '0 0 24 24',
|
|
80
|
+
width: 18,
|
|
81
|
+
height: 18,
|
|
82
|
+
fill: 'currentColor',
|
|
83
|
+
'aria-hidden': 'true',
|
|
84
|
+
}, h('path', {
|
|
85
|
+
d: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 复制图标:双层矩形(stroke 继承 currentColor),手动命令旁的复制按钮用。 */
|
|
90
|
+
export function CopyIcon() {
|
|
91
|
+
return h('svg', {
|
|
92
|
+
className: headerStyles.copyIcon,
|
|
93
|
+
viewBox: '0 0 16 16',
|
|
94
|
+
width: 12,
|
|
95
|
+
height: 12,
|
|
96
|
+
fill: 'none',
|
|
97
|
+
'aria-hidden': 'true',
|
|
98
|
+
},
|
|
99
|
+
h('rect', {
|
|
100
|
+
x: 5.5, y: 5.5, width: 7, height: 7, rx: 1.5,
|
|
101
|
+
stroke: 'currentColor', strokeWidth: 1.4,
|
|
102
|
+
}),
|
|
103
|
+
h('path', {
|
|
104
|
+
d: 'M10.5 5.5v-2a1.5 1.5 0 0 0-1.5-1.5H4.5A1.5 1.5 0 0 0 3 3.5v4A1.5 1.5 0 0 0 4.5 9h1',
|
|
105
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round',
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 下拉箭头图标:向下 V 形(stroke 继承 currentColor),下拉框触发器右侧提示可展开。 */
|
|
111
|
+
export function ChevronDownIcon() {
|
|
112
|
+
return h('svg', {
|
|
113
|
+
viewBox: '0 0 16 16',
|
|
114
|
+
width: 12,
|
|
115
|
+
height: 12,
|
|
116
|
+
fill: 'none',
|
|
117
|
+
'aria-hidden': 'true',
|
|
118
|
+
}, h('path', {
|
|
119
|
+
d: 'M3.5 5.5L8 10l4.5-4.5',
|
|
120
|
+
stroke: 'currentColor',
|
|
121
|
+
strokeWidth: 1.6,
|
|
122
|
+
strokeLinecap: 'round',
|
|
123
|
+
strokeLinejoin: 'round',
|
|
124
|
+
}))
|
|
125
|
+
}
|