dshmarket 1.0.2 → 1.1.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.md +4 -0
- package/README.zh.md +4 -0
- package/client/client.js +254 -44
- package/lib/hot.js +117 -9
- package/lib/index.js +14 -2
- package/lib/routes.js +287 -19
- package/lib/types/hot.d.ts +17 -1
- package/lib/types/index.d.ts +0 -5
- package/lib/types/routes.d.ts +22 -0
- package/package.json +3 -2
- package/src/hot.ts +126 -12
- package/src/index.ts +14 -2
- package/src/routes.ts +293 -22
package/README.md
CHANGED
package/README.zh.md
CHANGED
package/client/client.js
CHANGED
|
@@ -58,8 +58,14 @@ const zh = {
|
|
|
58
58
|
sortHot: '最热',
|
|
59
59
|
sortNew: '最新',
|
|
60
60
|
marketUpdate: '市场有新版本,升级',
|
|
61
|
+
updateAll: '全部更新',
|
|
62
|
+
tabThemes: '主题',
|
|
63
|
+
themeApply: '使用',
|
|
64
|
+
themeActive: '使用中',
|
|
65
|
+
themeEmpty: '目录里暂时还没有主题,敬请期待',
|
|
61
66
|
progressHint: '首次安装需要下载与解析依赖,大插件可能要 1-3 分钟',
|
|
62
67
|
toastReady: '已装好并已生效',
|
|
68
|
+
toastTheme: '已启用。到 设置 → 插件市场 → 主题 可随时切换',
|
|
63
69
|
gotIt: '知道了',
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -107,8 +113,14 @@ const en = {
|
|
|
107
113
|
sortHot: 'Top',
|
|
108
114
|
sortNew: 'New',
|
|
109
115
|
marketUpdate: 'Market update available — upgrade',
|
|
116
|
+
updateAll: 'Update all',
|
|
117
|
+
tabThemes: 'Themes',
|
|
118
|
+
themeApply: 'Use',
|
|
119
|
+
themeActive: 'Active',
|
|
120
|
+
themeEmpty: 'No more theme plugins in the catalog yet — stay tuned',
|
|
110
121
|
progressHint: 'First installs download and resolve dependencies — large plugins can take 1-3 minutes',
|
|
111
122
|
toastReady: 'installed and live',
|
|
123
|
+
toastTheme: 'is now active. Switch any time in Settings → Plugin Market → Themes',
|
|
112
124
|
gotIt: 'Got it',
|
|
113
125
|
}
|
|
114
126
|
|
|
@@ -133,8 +145,12 @@ const CSS = `
|
|
|
133
145
|
.dshm-top{position:absolute;right:18px;bottom:18px;z-index:20;width:38px;height:38px;border-radius:99px;border:1px solid var(--dsw-alias-border-l1,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#6b7280);font-size:16px;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.12)}
|
|
134
146
|
.dshm-top:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
|
|
135
147
|
.dshm-chip{font:inherit;font-size:12px;border:1px solid var(--dsw-alias-border-l1,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#fff);border-radius:99px;padding:3px 11px;cursor:pointer;color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
136
|
-
.dshm-chip.on{background:var(--dsw-alias-
|
|
148
|
+
.dshm-chip.on{background:var(--dsw-alias-button-primary-fill,#4f6ef7);border-color:var(--dsw-alias-button-primary-fill,#4f6ef7);color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
137
149
|
.dshm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px}
|
|
150
|
+
.dshm-sect{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);margin:14px 2px 8px;font-weight:600}
|
|
151
|
+
.dshm-sect:first-child{margin-top:2px}
|
|
152
|
+
.dshm-swatches{display:flex;gap:0;height:34px;border-radius:8px;overflow:hidden;border:1px solid var(--dsw-alias-border-l1,#e5e7eb)}
|
|
153
|
+
.dshm-swatches i{flex:1}
|
|
138
154
|
.dshm-card{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#e5e7eb);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:6px}
|
|
139
155
|
.dshm-row1{display:flex;align-items:center;gap:9px;min-width:0}
|
|
140
156
|
.dshm-av{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-weight:700;color:#fff;font-size:14px;flex-shrink:0}
|
|
@@ -147,7 +163,7 @@ const CSS = `
|
|
|
147
163
|
.dshm-src{font-size:11px;color:var(--dsw-alias-label-secondary,#9ca3af);text-decoration:none}
|
|
148
164
|
.dshm-src:hover{color:var(--dsw-alias-brand-primary,#4f6ef7)}
|
|
149
165
|
.dshm-btn{border:none;border-radius:7px;padding:5px 14px;font:inherit;font-size:12px;cursor:pointer;font-weight:600}
|
|
150
|
-
.dshm-btn.install{background:var(--dsw-alias-
|
|
166
|
+
.dshm-btn.install{background:var(--dsw-alias-button-primary-fill,#4f6ef7);color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
151
167
|
.dshm-btn.busy{opacity:.65;cursor:default}
|
|
152
168
|
.dshm-btn.done{background:transparent;color:var(--dsw-alias-state-success-primary,#16a34a);cursor:default}
|
|
153
169
|
.dshm-btn.ghost{background:var(--dsw-alias-bg-layer-2,#f3f4f6);color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
@@ -162,6 +178,7 @@ const CSS = `
|
|
|
162
178
|
.dshm-progress .dshm-spin{width:14px;height:14px;border-width:2px;flex-shrink:0}
|
|
163
179
|
.dshm-progress code{font-family:ui-monospace,Menlo,monospace;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
164
180
|
.dshm-toast{position:fixed;right:22px;bottom:22px;z-index:2000;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l1,#e5e7eb);border-radius:12px;padding:13px 16px;box-shadow:0 12px 40px rgba(0,0,0,.18);display:flex;align-items:center;gap:10px;font-size:13px;color:var(--dsw-alias-label-primary,#1f2328);pointer-events:auto;max-width:340px}
|
|
181
|
+
.dshm-toast .dshm-btn{white-space:nowrap;flex-shrink:0}
|
|
165
182
|
.dshm-empty{color:var(--dsw-alias-label-secondary,#9ca3af);font-size:13px;padding:32px;text-align:center}
|
|
166
183
|
.dshm-err{color:var(--dsw-alias-state-error-primary,#dc2626);font-size:12px;margin:8px 0;white-space:pre-wrap;word-break:break-all}
|
|
167
184
|
.dshm-mask{position:fixed;inset:0;background:rgba(15,18,25,.4);display:flex;align-items:center;justify-content:center;z-index:1000}
|
|
@@ -216,6 +233,25 @@ function isInstalled(plugin, installed) {
|
|
|
216
233
|
return Object.values(installed).some(spec => String(spec).toLowerCase().includes(needle))
|
|
217
234
|
}
|
|
218
235
|
|
|
236
|
+
/**
|
|
237
|
+
* The brand mark (assets/logo.svg — shared block-grid mark with
|
|
238
|
+
* awesome-dsh-plugin), inlined so the header needs no extra request.
|
|
239
|
+
*/
|
|
240
|
+
const LOGO_URI = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><rect width="128" height="128" rx="28" fill="#f6f2ea"/><g transform="translate(15.7 16.7) scale(0.3) translate(-112 -78)"><g fill="#2b2620"><rect x="112" y="112" width="88" height="88" rx="14"/><rect x="212" y="112" width="88" height="88" rx="14"/><rect x="112" y="212" width="88" height="88" rx="14"/><rect x="212" y="212" width="88" height="88" rx="14"/><rect x="112" y="312" width="88" height="88" rx="14"/><rect x="212" y="312" width="88" height="88" rx="14"/><rect x="312" y="212" width="88" height="88" rx="14"/><rect x="312" y="312" width="88" height="88" rx="14"/></g><rect x="346" y="78" width="88" height="88" rx="14" fill="#c0392b" transform="rotate(9 390 122)"/></g></svg>')
|
|
241
|
+
|
|
242
|
+
/** Four representative colors for a theme card's preview strip. */
|
|
243
|
+
function themeSwatch(def) {
|
|
244
|
+
const tk = def.tokens || {}
|
|
245
|
+
const pick = (names) => { for (const n of names) { if (tk[n]) return tk[n] } return null }
|
|
246
|
+
const dark = def.colorScheme === 'dark'
|
|
247
|
+
return [
|
|
248
|
+
pick(['--dsw-alias-bg-base', '--dsw-alias-bg-layer-1']) || (dark ? '#0f1115' : '#ffffff'),
|
|
249
|
+
pick(['--dsw-alias-bg-layer-2', '--dsw-alias-bg-overlay']) || (dark ? '#1a1d23' : '#f3f4f6'),
|
|
250
|
+
pick(['--dsw-alias-brand-primary']) || '#4f6ef7',
|
|
251
|
+
pick(['--dsw-alias-label-primary']) || (dark ? '#e5e7eb' : '#1f2328'),
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
|
|
219
255
|
function MarketSection(props) {
|
|
220
256
|
const t = props.t
|
|
221
257
|
const localeSnap = React.useSyncExternalStore(
|
|
@@ -223,9 +259,15 @@ function MarketSection(props) {
|
|
|
223
259
|
() => props.locale.getSnapshot(),
|
|
224
260
|
)
|
|
225
261
|
const lang = String(localeSnap.active).toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
262
|
+
// null when the composition has no theme service — the Themes tab hides.
|
|
263
|
+
const themeSnap = React.useSyncExternalStore(
|
|
264
|
+
props.themeStore.subscribe,
|
|
265
|
+
props.themeStore.getSnapshot,
|
|
266
|
+
)
|
|
226
267
|
const [data, setData] = useState(null)
|
|
227
268
|
const [loadError, setLoadError] = useState(false)
|
|
228
269
|
const [installed, setInstalled] = useState({})
|
|
270
|
+
const [skins, setSkins] = useState([])
|
|
229
271
|
const [tab, setTab] = useState(() => {
|
|
230
272
|
const saved = sessionStorage.getItem('dshm-tab')
|
|
231
273
|
if (saved !== null) sessionStorage.removeItem('dshm-tab')
|
|
@@ -239,6 +281,7 @@ function MarketSection(props) {
|
|
|
239
281
|
const [installError, setInstallError] = useState(null)
|
|
240
282
|
const [updates, setUpdates] = useState({})
|
|
241
283
|
const [updatingName, setUpdatingName] = useState(null)
|
|
284
|
+
const [updatingAll, setUpdatingAll] = useState(false)
|
|
242
285
|
const [updatedNames, setUpdatedNames] = useState([])
|
|
243
286
|
const [hotUrls, setHotUrls] = useState([])
|
|
244
287
|
const [hotNames, setHotNames] = useState([])
|
|
@@ -257,7 +300,10 @@ function MarketSection(props) {
|
|
|
257
300
|
const refreshInstalled = useCallback((force) => {
|
|
258
301
|
fetch('/dsh-market/installed', { cache: 'no-store' })
|
|
259
302
|
.then(res => res.json())
|
|
260
|
-
.then(body =>
|
|
303
|
+
.then(body => {
|
|
304
|
+
setInstalled(body.installed || {})
|
|
305
|
+
setSkins(body.live || [])
|
|
306
|
+
})
|
|
261
307
|
.catch(() => {})
|
|
262
308
|
fetch('/dsh-market/updates' + (force === true ? '?force=1' : ''), { cache: 'no-store' })
|
|
263
309
|
.then(res => res.json())
|
|
@@ -393,6 +439,14 @@ function MarketSection(props) {
|
|
|
393
439
|
.then(res => res.json().then(body => ({ status: res.status, body })))
|
|
394
440
|
.then(({ status, body }) => {
|
|
395
441
|
sessionStorage.removeItem('dshm-pending')
|
|
442
|
+
if (status === 200 && body.ok && body.hot && plugin.category === 'theme') {
|
|
443
|
+
// Themes auto-activate on install; reload straight into the Themes
|
|
444
|
+
// tab so the new look is on screen immediately.
|
|
445
|
+
sessionStorage.setItem('dshm-toast', JSON.stringify([plugin.name]))
|
|
446
|
+
sessionStorage.setItem('dshm-tab', 'themes')
|
|
447
|
+
location.reload()
|
|
448
|
+
return
|
|
449
|
+
}
|
|
396
450
|
if (status === 200 && body.ok) {
|
|
397
451
|
sessionStorage.setItem('dshm-tab', 'installed')
|
|
398
452
|
if (body.hot) {
|
|
@@ -418,7 +472,7 @@ function MarketSection(props) {
|
|
|
418
472
|
const doUpdate = useCallback((name) => {
|
|
419
473
|
setInstallError(null)
|
|
420
474
|
setUpdatingName(name)
|
|
421
|
-
fetch('/dsh-market/update', {
|
|
475
|
+
return fetch('/dsh-market/update', {
|
|
422
476
|
method: 'POST',
|
|
423
477
|
headers: { 'content-type': 'application/json' },
|
|
424
478
|
body: JSON.stringify({ name }),
|
|
@@ -438,11 +492,32 @@ function MarketSection(props) {
|
|
|
438
492
|
.finally(() => setUpdatingName(null))
|
|
439
493
|
}, [refreshInstalled, t])
|
|
440
494
|
|
|
495
|
+
const doUseSkin = useCallback((name) => {
|
|
496
|
+
setInstallError(null)
|
|
497
|
+
fetch('/dsh-market/use-skin', {
|
|
498
|
+
method: 'POST',
|
|
499
|
+
headers: { 'content-type': 'application/json' },
|
|
500
|
+
body: JSON.stringify({ name }),
|
|
501
|
+
})
|
|
502
|
+
.then(res => res.json().then(body => ({ status: res.status, body })))
|
|
503
|
+
.then(({ status, body }) => {
|
|
504
|
+
if (status === 200 && body.ok) {
|
|
505
|
+
sessionStorage.setItem('dshm-toast', JSON.stringify([name]))
|
|
506
|
+
sessionStorage.setItem('dshm-toast-mode', 'theme')
|
|
507
|
+
sessionStorage.setItem('dshm-tab', 'themes')
|
|
508
|
+
location.reload()
|
|
509
|
+
} else {
|
|
510
|
+
setInstallError(String(body.error || 'failed'))
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
.catch(error => setInstallError(String(error)))
|
|
514
|
+
}, [])
|
|
515
|
+
|
|
441
516
|
const doUninstall = useCallback((name) => {
|
|
442
517
|
setRemoveArmed(null)
|
|
443
518
|
setInstallError(null)
|
|
444
519
|
setRemovingName(name)
|
|
445
|
-
fetch('/dsh-market/uninstall', {
|
|
520
|
+
return fetch('/dsh-market/uninstall', {
|
|
446
521
|
method: 'POST',
|
|
447
522
|
headers: { 'content-type': 'application/json' },
|
|
448
523
|
body: JSON.stringify({ name }),
|
|
@@ -461,16 +536,147 @@ function MarketSection(props) {
|
|
|
461
536
|
.finally(() => setRemovingName(null))
|
|
462
537
|
}, [refreshInstalled])
|
|
463
538
|
|
|
539
|
+
// The market itself stays out of the batch: its update reloads this page
|
|
540
|
+
// mid-run, which would strand the remaining items.
|
|
541
|
+
const selfName = installed['dshmarket'] !== undefined ? 'dshmarket' : 'dsh-market'
|
|
542
|
+
const updatableNames = Object.keys(installed).filter(
|
|
543
|
+
name => name !== selfName && !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
const doUpdateAll = useCallback(() => {
|
|
547
|
+
const names = updatableNames.slice()
|
|
548
|
+
setUpdatingAll(true)
|
|
549
|
+
const next = () => {
|
|
550
|
+
const name = names.shift()
|
|
551
|
+
if (name === undefined) {
|
|
552
|
+
setUpdatingAll(false)
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
doUpdate(name).then(next, next)
|
|
556
|
+
}
|
|
557
|
+
next()
|
|
558
|
+
}, [updatableNames, doUpdate])
|
|
559
|
+
|
|
464
560
|
const pendingRestart = doneUrls.length + updatedNames.length + removedCount
|
|
465
561
|
const hasUpdates = Object.keys(installed).some(
|
|
466
562
|
name => !updatedNames.includes(name) && updates[name] && updates[name].updateAvailable,
|
|
467
563
|
)
|
|
468
564
|
|
|
565
|
+
const themePlugins = data === null ? [] : data.plugins
|
|
566
|
+
.filter(p => p.category === 'theme')
|
|
567
|
+
.sort((a, b) => (b.stars || 0) - (a.stars || 0))
|
|
568
|
+
|
|
569
|
+
const pluginCard = (p) => {
|
|
570
|
+
const desc = (p.description && (p.description[lang] || p.description.en)) || ''
|
|
571
|
+
const done = doneUrls.includes(p.url) || hotUrls.includes(p.url)
|
|
572
|
+
const already = isInstalled(p, installed)
|
|
573
|
+
const busy = busyUrl === p.url
|
|
574
|
+
return h('div', { key: p.url, className: 'dshm-card' },
|
|
575
|
+
h('div', { className: 'dshm-row1' },
|
|
576
|
+
h('div', { className: 'dshm-av', style: { background: avatarColor(p.name) } },
|
|
577
|
+
p.name.replace(/^dsh[-_]/i, '').charAt(0).toUpperCase() || 'P'),
|
|
578
|
+
h('div', { style: { minWidth: 0 } },
|
|
579
|
+
h('div', { className: 'dshm-nm' }, p.name),
|
|
580
|
+
h('div', { className: 'dshm-owner' }, p.owner,
|
|
581
|
+
typeof p.stars === 'number' && h('span', { className: 'dshm-star' }, ' · ★ ' + p.stars))),
|
|
582
|
+
h('span', { className: 'dshm-grow' }),
|
|
583
|
+
h('a', { className: 'dshm-src', href: p.url, target: '_blank', rel: 'noreferrer', style: { alignSelf: 'flex-start', flexShrink: 0 } }, t('viewSource'))),
|
|
584
|
+
h('div', { className: 'dshm-desc' }, desc),
|
|
585
|
+
h('div', { className: 'dshm-foot' },
|
|
586
|
+
h('span', { className: 'dshm-cat' },
|
|
587
|
+
(data.categories[p.category] && (data.categories[p.category][lang] || data.categories[p.category].en)) || p.category),
|
|
588
|
+
h('span', { className: 'dshm-grow' }),
|
|
589
|
+
done
|
|
590
|
+
? h('button', { className: 'dshm-btn done' }, t('installedBadge'))
|
|
591
|
+
: already
|
|
592
|
+
? h('button', { className: 'dshm-btn done' }, t('alreadyInstalled'))
|
|
593
|
+
: busy
|
|
594
|
+
? h('button', { className: 'dshm-btn install busy' }, t('installing'))
|
|
595
|
+
: h('button', {
|
|
596
|
+
className: 'dshm-btn install',
|
|
597
|
+
disabled: busyUrl !== null || !envReady,
|
|
598
|
+
onClick: () => setConfirming(p),
|
|
599
|
+
}, t('install'))),
|
|
600
|
+
busy && h('div', { className: 'dshm-progress', style: { margin: '6px 0 0' } },
|
|
601
|
+
h('span', { className: 'dshm-spin' }),
|
|
602
|
+
h('code', { className: 'dshm-grow' }, progressLine || t('progressHint'))))
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const installedNameOf = (p) => {
|
|
606
|
+
if (installed[p.name] !== undefined) return p.name
|
|
607
|
+
const repo = repoOf(p.url)
|
|
608
|
+
if (repo === null) return null
|
|
609
|
+
const needle = ('github:' + repo).toLowerCase()
|
|
610
|
+
for (const [name, spec] of Object.entries(installed)) {
|
|
611
|
+
if (String(spec).toLowerCase().includes(needle)) return name
|
|
612
|
+
}
|
|
613
|
+
return null
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Plugins loaded at boot (bundle-layer skins) aren't in the shim list but
|
|
617
|
+
// are just as live; the boot manifest is the page's own record of them.
|
|
618
|
+
const bootEntries = (typeof window !== 'undefined' && window.__DSH_BOOT__ && Array.isArray(window.__DSH_BOOT__.entries))
|
|
619
|
+
? window.__DSH_BOOT__.entries
|
|
620
|
+
: []
|
|
621
|
+
|
|
622
|
+
// Unified card for the Themes tab: install → use/in-use → uninstall.
|
|
623
|
+
const themePluginCard = (p) => {
|
|
624
|
+
const instName = installedNameOf(p)
|
|
625
|
+
if (instName === null) return pluginCard(p)
|
|
626
|
+
const mounted = skins.includes(instName) || bootEntries.some(e => e.id === instName)
|
|
627
|
+
const desc = (p.description && (p.description[lang] || p.description.en)) || ''
|
|
628
|
+
return h('div', { key: p.url, className: 'dshm-card' },
|
|
629
|
+
h('div', { className: 'dshm-row1' },
|
|
630
|
+
h('div', { className: 'dshm-av', style: { background: avatarColor(p.name) } },
|
|
631
|
+
p.name.replace(/^dsh[-_]/i, '').charAt(0).toUpperCase() || 'P'),
|
|
632
|
+
h('div', { style: { minWidth: 0 } },
|
|
633
|
+
h('div', { className: 'dshm-nm' }, p.name),
|
|
634
|
+
h('div', { className: 'dshm-owner' }, p.owner,
|
|
635
|
+
typeof p.stars === 'number' && h('span', { className: 'dshm-star' }, ' · ★ ' + p.stars))),
|
|
636
|
+
h('span', { className: 'dshm-grow' }),
|
|
637
|
+
h('a', { className: 'dshm-src', href: p.url, target: '_blank', rel: 'noreferrer', style: { alignSelf: 'flex-start', flexShrink: 0 } }, t('viewSource'))),
|
|
638
|
+
h('div', { className: 'dshm-desc' }, desc),
|
|
639
|
+
h('div', { className: 'dshm-foot' },
|
|
640
|
+
h('span', { className: 'dshm-grow' }),
|
|
641
|
+
removingName === instName
|
|
642
|
+
? h('button', { className: 'dshm-btn danger busy' }, t('uninstalling'))
|
|
643
|
+
: removeArmed === instName
|
|
644
|
+
? h('button', {
|
|
645
|
+
className: 'dshm-btn danger armed',
|
|
646
|
+
onClick: () => doUninstall(instName).then(() => {
|
|
647
|
+
if (mounted) {
|
|
648
|
+
sessionStorage.setItem('dshm-tab', 'themes')
|
|
649
|
+
location.reload()
|
|
650
|
+
}
|
|
651
|
+
}),
|
|
652
|
+
}, t('confirmRemove'))
|
|
653
|
+
: h('button', { className: 'dshm-btn danger', onClick: () => setRemoveArmed(instName) }, t('uninstall')),
|
|
654
|
+
mounted
|
|
655
|
+
? h('button', { className: 'dshm-btn done' }, t('themeActive'))
|
|
656
|
+
: h('button', { className: 'dshm-btn install', onClick: () => doUseSkin(instName) }, t('themeApply'))))
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const themeCard = (id, label, swatch) => {
|
|
660
|
+
const active = themeSnap !== null && themeSnap.preference === id
|
|
661
|
+
return h('div', { key: 'th-' + id, className: 'dshm-card' },
|
|
662
|
+
h('div', { className: 'dshm-swatches' }, swatch.map((c, i) => h('i', { key: i, style: { background: c } }))),
|
|
663
|
+
h('div', { className: 'dshm-foot' },
|
|
664
|
+
h('span', { className: 'dshm-nm' }, label),
|
|
665
|
+
h('span', { className: 'dshm-grow' }),
|
|
666
|
+
active
|
|
667
|
+
? h('button', { className: 'dshm-btn done' }, t('themeActive'))
|
|
668
|
+
: h('button', {
|
|
669
|
+
className: 'dshm-btn install',
|
|
670
|
+
onClick: () => { try { props.theme.setTheme(id) } catch (error) { setInstallError(String(error)) } },
|
|
671
|
+
}, t('themeApply'))))
|
|
672
|
+
}
|
|
673
|
+
|
|
469
674
|
const categories = data === null ? [] : Object.keys(data.categories)
|
|
470
675
|
|
|
471
676
|
return h('div', { className: 'dshm-root' },
|
|
472
677
|
h('div', { className: 'dshm-head' },
|
|
473
678
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
|
|
679
|
+
h('img', { src: LOGO_URI, width: 22, height: 22, alt: '', style: { borderRadius: '5px', flexShrink: 0 } }),
|
|
474
680
|
h('h2', { className: 'dshm-title' }, t('nav')),
|
|
475
681
|
(() => {
|
|
476
682
|
const self = installed['dshmarket'] !== undefined ? 'dshmarket' : 'dsh-market'
|
|
@@ -481,7 +687,13 @@ function MarketSection(props) {
|
|
|
481
687
|
disabled: updatingName !== null || busyUrl !== null,
|
|
482
688
|
onClick: () => { setTab('installed'); doUpdate(self) },
|
|
483
689
|
}, updatingName === self ? t('updating') : t('marketUpdate'))
|
|
484
|
-
})()
|
|
690
|
+
})(),
|
|
691
|
+
updatableNames.length >= 2 && h('button', {
|
|
692
|
+
className: 'dshm-btn upd',
|
|
693
|
+
style: { fontSize: '11px', padding: '3px 10px' },
|
|
694
|
+
disabled: updatingAll || updatingName !== null || busyUrl !== null || removingName !== null,
|
|
695
|
+
onClick: () => { setTab('installed'); doUpdateAll() },
|
|
696
|
+
}, updatingAll ? t('updating') : t('updateAll') + ' (' + updatableNames.length + ')')),
|
|
485
697
|
h('div', { className: 'dshm-sub' },
|
|
486
698
|
t('subtitle') + (data ? ' · ' + data.count : '') + ' · ',
|
|
487
699
|
h('a', { className: 'dshm-src', href: '/dsh-market/logs', download: 'dsh-market-log.txt' }, t('exportLog'))),
|
|
@@ -489,6 +701,7 @@ function MarketSection(props) {
|
|
|
489
701
|
h('input', { placeholder: t('searchPh'), value: q, onChange: e => setQ(e.target.value) })),
|
|
490
702
|
h('div', { className: 'dshm-tabs' },
|
|
491
703
|
h('button', { className: 'dshm-tab' + (tab === 'discover' ? ' on' : ''), onClick: () => setTab('discover') }, t('tabDiscover')),
|
|
704
|
+
themeSnap !== null && h('button', { className: 'dshm-tab' + (tab === 'themes' ? ' on' : ''), onClick: () => setTab('themes') }, t('tabThemes')),
|
|
492
705
|
h('button', { className: 'dshm-tab' + (tab === 'installed' ? ' on' : ''), onClick: () => { setTab('installed'); refreshInstalled(true) } },
|
|
493
706
|
t('tabInstalled') + (Object.keys(installed).length > 0 ? ' (' + Object.keys(installed).length + ')' : ''),
|
|
494
707
|
hasUpdates && h('span', { className: 'dshm-dot' }))),
|
|
@@ -542,41 +755,22 @@ function MarketSection(props) {
|
|
|
542
755
|
}, t(key === 'hot' ? 'sortHot' : 'sortNew'))))),
|
|
543
756
|
plugins.length === 0
|
|
544
757
|
? h('div', { className: 'dshm-empty' }, t('empty'))
|
|
545
|
-
: h('div', { className: 'dshm-grid' }, plugins.map(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
h('div', { className: 'dshm-foot' },
|
|
562
|
-
h('span', { className: 'dshm-cat' },
|
|
563
|
-
(data.categories[p.category] && (data.categories[p.category][lang] || data.categories[p.category].en)) || p.category),
|
|
564
|
-
h('span', { className: 'dshm-grow' }),
|
|
565
|
-
done
|
|
566
|
-
? h('button', { className: 'dshm-btn done' }, t('installedBadge'))
|
|
567
|
-
: already
|
|
568
|
-
? h('button', { className: 'dshm-btn done' }, t('alreadyInstalled'))
|
|
569
|
-
: busy
|
|
570
|
-
? h('button', { className: 'dshm-btn install busy' }, t('installing'))
|
|
571
|
-
: h('button', {
|
|
572
|
-
className: 'dshm-btn install',
|
|
573
|
-
disabled: busyUrl !== null || !envReady,
|
|
574
|
-
onClick: () => setConfirming(p),
|
|
575
|
-
}, t('install'))),
|
|
576
|
-
busy && h('div', { className: 'dshm-progress', style: { margin: '6px 0 0' } },
|
|
577
|
-
h('span', { className: 'dshm-spin' }),
|
|
578
|
-
h('code', { className: 'dshm-grow' }, progressLine || t('progressHint'))))
|
|
579
|
-
})))
|
|
758
|
+
: h('div', { className: 'dshm-grid' }, plugins.map(pluginCard)))
|
|
759
|
+
: tab === 'themes' && themeSnap !== null
|
|
760
|
+
? h(React.Fragment, null,
|
|
761
|
+
// Light/dark/system live in the official Appearance setting; this
|
|
762
|
+
// tab only shows what that setting can't: registered third-party
|
|
763
|
+
// palettes (none in the wild yet) and installable theme plugins.
|
|
764
|
+
(() => {
|
|
765
|
+
const extra = themeSnap.themes.filter(def => def.id !== 'light' && def.id !== 'dark')
|
|
766
|
+
return extra.length > 0 && h('div', { className: 'dshm-grid', style: { marginBottom: 10 } },
|
|
767
|
+
extra.map(def => themeCard(def.id, def.id, themeSwatch(def))))
|
|
768
|
+
})(),
|
|
769
|
+
data === null
|
|
770
|
+
? h('div', { className: 'dshm-loading' }, h('span', { className: 'dshm-spin' }), t('loading'))
|
|
771
|
+
: themePlugins.length === 0
|
|
772
|
+
? h('div', { className: 'dshm-empty' }, t('themeEmpty'))
|
|
773
|
+
: h('div', { className: 'dshm-grid' }, themePlugins.map(themePluginCard)))
|
|
580
774
|
: Object.keys(installed).length === 0
|
|
581
775
|
? h('div', { className: 'dshm-empty' }, t('installedEmpty'))
|
|
582
776
|
: Object.entries(installed).map(([name, spec]) => {
|
|
@@ -648,7 +842,10 @@ function MarketSection(props) {
|
|
|
648
842
|
}
|
|
649
843
|
|
|
650
844
|
exports.name = 'dsh-market'
|
|
651
|
-
|
|
845
|
+
// 'theme' is safe to require: ui-layout (mandatory in every web composition)
|
|
846
|
+
// already hard-depends on it. This cordis's object-form inject means
|
|
847
|
+
// intercept config, NOT {required,optional} — do not use it here.
|
|
848
|
+
exports.inject = ['slots', 'locale', 'theme']
|
|
652
849
|
exports.apply = function apply(ctx) {
|
|
653
850
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-market: dictionaries')
|
|
654
851
|
const t = ctx.locale.bind(NS)
|
|
@@ -660,12 +857,25 @@ exports.apply = function apply(ctx) {
|
|
|
660
857
|
label: () => t('nav'),
|
|
661
858
|
locale: NS,
|
|
662
859
|
inject: () => ({ t }),
|
|
663
|
-
}, () => h(MarketSection, {
|
|
860
|
+
}, () => h(MarketSection, {
|
|
861
|
+
t,
|
|
862
|
+
locale: ctx.locale,
|
|
863
|
+
theme: ctx.theme,
|
|
864
|
+
themeStore: {
|
|
865
|
+
subscribe: (cb) => ctx.on('theme/change', cb),
|
|
866
|
+
getSnapshot: () => ctx.theme.getTheme(),
|
|
867
|
+
},
|
|
868
|
+
})))
|
|
664
869
|
|
|
665
870
|
// Post-reload confirmation: a floating "installed and live" card in the
|
|
666
871
|
// shell overlay layer, shown once after the refresh that follows a hot
|
|
667
872
|
// install, so the user lands back in their flow with visible proof.
|
|
668
873
|
function InstallToast() {
|
|
874
|
+
const [mode] = useState(() => {
|
|
875
|
+
const value = sessionStorage.getItem('dshm-toast-mode')
|
|
876
|
+
sessionStorage.removeItem('dshm-toast-mode')
|
|
877
|
+
return value
|
|
878
|
+
})
|
|
669
879
|
const [names, setNames] = useState(() => {
|
|
670
880
|
const value = readSession('dshm-toast')
|
|
671
881
|
sessionStorage.removeItem('dshm-toast')
|
|
@@ -680,7 +890,7 @@ exports.apply = function apply(ctx) {
|
|
|
680
890
|
if (names.length === 0) return null
|
|
681
891
|
return h('div', { className: 'dshm-toast' },
|
|
682
892
|
h('span', null, '✨'),
|
|
683
|
-
h('span', null, names.join(', ') + ' ' + t('toastReady')),
|
|
893
|
+
h('span', null, names.join(', ') + ' ' + t(mode === 'theme' ? 'toastTheme' : 'toastReady')),
|
|
684
894
|
h('button', { className: 'dshm-btn install', onClick: () => setNames([]) }, t('gotIt')))
|
|
685
895
|
}
|
|
686
896
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
package/lib/hot.js
CHANGED
|
@@ -21,7 +21,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
21
21
|
}
|
|
22
22
|
return path;
|
|
23
23
|
};
|
|
24
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { join } from 'node:path';
|
|
26
26
|
import { pathToFileURL } from 'node:url';
|
|
27
27
|
import { logEvent } from './log.js';
|
|
@@ -31,6 +31,13 @@ let hotTreeClass;
|
|
|
31
31
|
* The Include subclass, built once per process; null when the loader's include
|
|
32
32
|
* plugin is not importable (older harness) — callers fall back to restart.
|
|
33
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* Packages whose host import is replaced by a no-op shim. Client-only plugins
|
|
36
|
+
* (`dsh.client` without `dsh.bundle`) have no importable host half, but
|
|
37
|
+
* client-modules only serves bundles for packages with a live loader entry —
|
|
38
|
+
* the shim fiber exists purely to satisfy that registration.
|
|
39
|
+
*/
|
|
40
|
+
const shimNames = new Set();
|
|
34
41
|
async function loadHotTreeClass() {
|
|
35
42
|
if (hotTreeClass !== undefined)
|
|
36
43
|
return hotTreeClass;
|
|
@@ -46,6 +53,11 @@ async function loadHotTreeClass() {
|
|
|
46
53
|
class MarketHotTree extends Include {
|
|
47
54
|
/** Runtime-only mount list; the bundle layer owns persistence. */
|
|
48
55
|
write() { }
|
|
56
|
+
import(name, getOuterStack) {
|
|
57
|
+
if (shimNames.has(name))
|
|
58
|
+
return { name, apply: () => { } };
|
|
59
|
+
return super.import(name, getOuterStack);
|
|
60
|
+
}
|
|
49
61
|
}
|
|
50
62
|
hotTreeClass = MarketHotTree;
|
|
51
63
|
}
|
|
@@ -54,6 +66,16 @@ async function loadHotTreeClass() {
|
|
|
54
66
|
}
|
|
55
67
|
return hotTreeClass;
|
|
56
68
|
}
|
|
69
|
+
/** The `dsh` declaration block of an installed package, or null when unreadable. */
|
|
70
|
+
function readPkgDsh(profileDir, packageName) {
|
|
71
|
+
try {
|
|
72
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'node_modules', packageName, 'package.json'), 'utf8'));
|
|
73
|
+
return manifest.dsh ?? {};
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
57
79
|
/**
|
|
58
80
|
* Insert rows of a plugin's bundle patch, or null when the patch contains
|
|
59
81
|
* anything beyond plain `id`/`name` insert rows (config blocks, disables,
|
|
@@ -87,9 +109,44 @@ export function parseSimplePatch(patchText) {
|
|
|
87
109
|
return null;
|
|
88
110
|
return rows;
|
|
89
111
|
}
|
|
90
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Wipe leftover hot-mount inputs; call once when the market host starts.
|
|
114
|
+
* `state.json` (skin enable/disable choices) deliberately survives.
|
|
115
|
+
*/
|
|
91
116
|
export function cleanHotDir(profileDir) {
|
|
92
|
-
|
|
117
|
+
const dir = join(profileDir, HOT_DIR);
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = readdirSync(dir);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const name of entries) {
|
|
126
|
+
if (/^hot-\d+\.yml$/.test(name))
|
|
127
|
+
rmSync(join(dir, name), { force: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function stateFile(profileDir) {
|
|
131
|
+
return join(profileDir, HOT_DIR, 'state.json');
|
|
132
|
+
}
|
|
133
|
+
/** Themes the user switched away from; skipped by the boot re-mount. */
|
|
134
|
+
export function readDisabledThemes(profileDir) {
|
|
135
|
+
try {
|
|
136
|
+
const state = JSON.parse(readFileSync(stateFile(profileDir), 'utf8'));
|
|
137
|
+
return new Set(Array.isArray(state.disabledSkins) ? state.disabledSkins : []);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return new Set();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function writeDisabledThemes(profileDir, disabled) {
|
|
144
|
+
mkdirSync(join(profileDir, HOT_DIR), { recursive: true, mode: 0o700 });
|
|
145
|
+
writeFileSync(stateFile(profileDir), JSON.stringify({ disabledSkins: [...disabled] }));
|
|
146
|
+
}
|
|
147
|
+
/** Package names currently live through a market hot mount (patch or shim). */
|
|
148
|
+
export function listHotMounts() {
|
|
149
|
+
return [...hotHandles.keys()];
|
|
93
150
|
}
|
|
94
151
|
let hotSequence = 0;
|
|
95
152
|
const hotHandles = new Map();
|
|
@@ -104,6 +161,7 @@ export async function hotUnmount(packageName) {
|
|
|
104
161
|
if (handle === undefined)
|
|
105
162
|
return false;
|
|
106
163
|
hotHandles.delete(packageName);
|
|
164
|
+
shimNames.delete(packageName);
|
|
107
165
|
try {
|
|
108
166
|
await handle.dispose();
|
|
109
167
|
logEvent('info', 'hot-unmount', `${packageName}: removed live`);
|
|
@@ -128,23 +186,43 @@ export async function hotMount(ctx, profileDir, packageName) {
|
|
|
128
186
|
const HotTree = await loadHotTreeClass();
|
|
129
187
|
if (HotTree === null)
|
|
130
188
|
return false;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
189
|
+
let patchText;
|
|
190
|
+
try {
|
|
191
|
+
patchText = readFileSync(join(profileDir, 'node_modules', packageName, 'cordis.patch.yml'), 'utf8');
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
patchText = null;
|
|
195
|
+
}
|
|
196
|
+
let rows;
|
|
197
|
+
if (patchText !== null) {
|
|
198
|
+
rows = parseSimplePatch(patchText);
|
|
199
|
+
if (rows === null)
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// No host patch. Client-only packages (dsh.client, no dsh.bundle) never
|
|
204
|
+
// get a loader entry — not even after a restart — so client-modules
|
|
205
|
+
// would never serve their bundle. A shim entry under the package's name
|
|
206
|
+
// is the whole activation.
|
|
207
|
+
const dsh = readPkgDsh(profileDir, packageName);
|
|
208
|
+
if (dsh === null || dsh.client === undefined || dsh.bundle !== undefined)
|
|
209
|
+
return false;
|
|
210
|
+
shimNames.add(packageName);
|
|
211
|
+
rows = [{ id: `client-${packageName.replace(/[^A-Za-z0-9_.-]/g, '-')}`, name: packageName }];
|
|
212
|
+
}
|
|
135
213
|
const dir = join(profileDir, HOT_DIR);
|
|
136
214
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
137
215
|
hotSequence += 1;
|
|
138
216
|
const file = join(dir, `hot-${String(hotSequence)}.yml`);
|
|
139
217
|
const yml = rows
|
|
140
|
-
.map(row => `- id: mkt-${row.id}\n name: '${row.name}'\n`)
|
|
218
|
+
.map(row => `- id: 'mkt-${row.id}'\n name: '${row.name}'\n`)
|
|
141
219
|
.join('');
|
|
142
220
|
writeFileSync(file, yml);
|
|
143
221
|
const handle = ctx.plugin(HotTree, { path: pathToFileURL(file).href });
|
|
144
222
|
await handle.await();
|
|
145
223
|
hotHandles.set(packageName, handle);
|
|
146
224
|
ctx.logger?.info?.(`[dsh-market] hot-mounted ${packageName}`);
|
|
147
|
-
logEvent('info', 'hot-mount', `${packageName}: live`);
|
|
225
|
+
logEvent('info', 'hot-mount', `${packageName}: live${shimNames.has(packageName) ? ' (client-only shim)' : ''}`);
|
|
148
226
|
return true;
|
|
149
227
|
}
|
|
150
228
|
catch (error) {
|
|
@@ -154,3 +232,33 @@ export async function hotMount(ctx, profileDir, packageName) {
|
|
|
154
232
|
return false;
|
|
155
233
|
}
|
|
156
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Mount every installed client-only package (`dsh.client` without
|
|
237
|
+
* `dsh.bundle`) at market startup. The bundle reconcile skips these packages
|
|
238
|
+
* entirely, so without the market's shim their client bundles are unreachable
|
|
239
|
+
* in every boot — this is what makes them behave like normal plugins.
|
|
240
|
+
* @returns names that were mounted.
|
|
241
|
+
*/
|
|
242
|
+
export async function mountClientOnlyDeps(ctx, profileDir) {
|
|
243
|
+
let deps;
|
|
244
|
+
try {
|
|
245
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
246
|
+
const bundles = new Set(manifest.dsh?.profile?.bundles ?? []);
|
|
247
|
+
deps = Object.keys(manifest.dependencies ?? {}).filter(name => !bundles.has(name));
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
const disabled = readDisabledThemes(profileDir);
|
|
253
|
+
const mounted = [];
|
|
254
|
+
for (const name of deps) {
|
|
255
|
+
if (hotHandles.has(name) || disabled.has(name))
|
|
256
|
+
continue;
|
|
257
|
+
const dsh = readPkgDsh(profileDir, name);
|
|
258
|
+
if (dsh === null || dsh.client === undefined || dsh.bundle !== undefined)
|
|
259
|
+
continue;
|
|
260
|
+
if (await hotMount(ctx, profileDir, name))
|
|
261
|
+
mounted.push(name);
|
|
262
|
+
}
|
|
263
|
+
return mounted;
|
|
264
|
+
}
|