opencode-plugin-hub 0.1.2 → 0.1.4
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 +2 -2
- package/dist/gui-experimental.cjs +89 -32
- package/package.json +1 -1
- package/src/gui/catalog.ts +43 -14
- package/src/gui/page.tsx +286 -171
package/src/gui/page.tsx
CHANGED
|
@@ -102,7 +102,7 @@ function noteIcon(tone: Tone): IconName {
|
|
|
102
102
|
function readableError(error: unknown): string {
|
|
103
103
|
const text = error instanceof Error ? error.message : String(error)
|
|
104
104
|
if (/SchemaError|Missing key at/i.test(text)) {
|
|
105
|
-
return "
|
|
105
|
+
return "插件窗口与后台服务版本不一致。点工具栏的「彻底重启」让两边换成同一版本。"
|
|
106
106
|
}
|
|
107
107
|
if (/unknown method|not implemented|no such method|method.*not.*found/i.test(text)) {
|
|
108
108
|
return "后台服务还是旧版本,暂时没有「彻底重启」能力。请先手动完全退出 OpenCode(并结束 serve --service)再打开一次。"
|
|
@@ -348,12 +348,18 @@ const CSS = `
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
/* cards */
|
|
351
|
-
.ph-list {
|
|
351
|
+
.ph-list {
|
|
352
|
+
display: grid;
|
|
353
|
+
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
|
|
354
|
+
gap: 10px;
|
|
355
|
+
align-items: stretch;
|
|
356
|
+
}
|
|
352
357
|
.ph-card {
|
|
358
|
+
position: relative;
|
|
353
359
|
display: flex;
|
|
354
|
-
|
|
355
|
-
gap:
|
|
356
|
-
padding:
|
|
360
|
+
flex-direction: column;
|
|
361
|
+
gap: 9px;
|
|
362
|
+
padding: 12px;
|
|
357
363
|
border: 1px solid var(--ph-border);
|
|
358
364
|
border-radius: 12px;
|
|
359
365
|
background: var(--ph-layer);
|
|
@@ -364,22 +370,59 @@ const CSS = `
|
|
|
364
370
|
background: var(--ph-layer-2);
|
|
365
371
|
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.05);
|
|
366
372
|
}
|
|
373
|
+
/* Highlight cards whose plugin has a pending update. */
|
|
374
|
+
.ph-card[data-stale="true"] {
|
|
375
|
+
border-color: color-mix(in srgb, var(--ph-accent) 36%, transparent);
|
|
376
|
+
background: color-mix(in srgb, var(--ph-accent) 5%, transparent);
|
|
377
|
+
}
|
|
378
|
+
/* While an install or update runs, pulse and sweep the card. */
|
|
379
|
+
.ph-card[data-busy="true"] {
|
|
380
|
+
border-color: color-mix(in srgb, var(--ph-accent) 45%, transparent);
|
|
381
|
+
animation: ph-busy-pulse 1.1s ease-in-out infinite;
|
|
382
|
+
}
|
|
383
|
+
.ph-card[data-busy="true"]::after {
|
|
384
|
+
content: "";
|
|
385
|
+
position: absolute;
|
|
386
|
+
inset: 0;
|
|
387
|
+
border-radius: inherit;
|
|
388
|
+
background: linear-gradient(
|
|
389
|
+
100deg,
|
|
390
|
+
transparent 32%,
|
|
391
|
+
color-mix(in srgb, var(--ph-accent) 12%, transparent) 50%,
|
|
392
|
+
transparent 68%
|
|
393
|
+
);
|
|
394
|
+
background-size: 220% 100%;
|
|
395
|
+
animation: ph-shimmer 1.1s linear infinite;
|
|
396
|
+
pointer-events: none;
|
|
397
|
+
}
|
|
398
|
+
@keyframes ph-busy-pulse {
|
|
399
|
+
0%,
|
|
400
|
+
100% {
|
|
401
|
+
box-shadow: 0 0 0 0 color-mix(in srgb, var(--ph-accent) 20%, transparent);
|
|
402
|
+
}
|
|
403
|
+
50% {
|
|
404
|
+
box-shadow: 0 0 0 5px color-mix(in srgb, var(--ph-accent) 8%, transparent);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
.ph-card-main { display: flex; flex: 1; flex-direction: column; gap: 8px; min-width: 0; }
|
|
408
|
+
.ph-card-body { display: flex; flex: 1; flex-direction: column; gap: 5px; min-width: 0; }
|
|
409
|
+
.ph-card-body[role="button"] { cursor: pointer; border-radius: 6px; }
|
|
410
|
+
.ph-card-head { display: flex; align-items: flex-start; gap: 9px; }
|
|
411
|
+
.ph-card-titles { display: flex; flex: 1; flex-direction: column; gap: 2px; min-width: 0; }
|
|
367
412
|
.ph-tile {
|
|
368
413
|
flex: 0 0 auto;
|
|
369
414
|
display: grid;
|
|
370
415
|
place-items: center;
|
|
371
|
-
width:
|
|
372
|
-
height:
|
|
416
|
+
width: 32px;
|
|
417
|
+
height: 32px;
|
|
373
418
|
border: 1px solid color-mix(in srgb, var(--ph-accent) 20%, transparent);
|
|
374
|
-
border-radius:
|
|
419
|
+
border-radius: 9px;
|
|
375
420
|
background: color-mix(in srgb, var(--ph-accent) 10%, transparent);
|
|
376
421
|
color: var(--ph-accent);
|
|
377
|
-
font-size:
|
|
422
|
+
font-size: 13px;
|
|
378
423
|
font-weight: 700;
|
|
379
424
|
overflow: hidden;
|
|
380
425
|
}
|
|
381
|
-
.ph-card-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
|
382
|
-
.ph-card-body[role="button"] { cursor: pointer; border-radius: 6px; }
|
|
383
426
|
.ph-name-row { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
|
|
384
427
|
.ph-name {
|
|
385
428
|
font-size: 13.5px;
|
|
@@ -409,7 +452,16 @@ const CSS = `
|
|
|
409
452
|
text-transform: uppercase;
|
|
410
453
|
color: var(--ph-faint);
|
|
411
454
|
}
|
|
412
|
-
.ph-actions {
|
|
455
|
+
.ph-actions {
|
|
456
|
+
display: flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
justify-content: flex-end;
|
|
459
|
+
gap: 4px;
|
|
460
|
+
flex-wrap: wrap;
|
|
461
|
+
margin-top: auto;
|
|
462
|
+
padding-top: 8px;
|
|
463
|
+
border-top: 1px solid var(--ph-border);
|
|
464
|
+
}
|
|
413
465
|
|
|
414
466
|
/* badges and chips */
|
|
415
467
|
.ph-badge {
|
|
@@ -471,6 +523,7 @@ button.ph-chip[data-active="true"] {
|
|
|
471
523
|
border-color: color-mix(in srgb, var(--ph-accent) 32%, transparent);
|
|
472
524
|
background: color-mix(in srgb, var(--ph-accent) 10%, transparent);
|
|
473
525
|
}
|
|
526
|
+
.ph-chip[data-disabled="true"] { opacity: 0.45; cursor: default; }
|
|
474
527
|
.ph-chip-count { font-size: 9px; opacity: 0.7; }
|
|
475
528
|
|
|
476
529
|
/* empty and loading */
|
|
@@ -617,10 +670,16 @@ button.ph-chip:focus-visible,
|
|
|
617
670
|
max-height: calc(100vh - 24px);
|
|
618
671
|
resize: both;
|
|
619
672
|
}
|
|
620
|
-
|
|
673
|
+
/* Notices and the toolbar share one sticky head, so a note raised after an
|
|
674
|
+
action (e.g. updating a plugin near the bottom of the list) is always on
|
|
675
|
+
screen instead of appearing far above the current scroll position. */
|
|
676
|
+
.ph-head {
|
|
621
677
|
position: sticky;
|
|
622
678
|
top: 0;
|
|
623
679
|
z-index: 3;
|
|
680
|
+
display: flex;
|
|
681
|
+
flex-direction: column;
|
|
682
|
+
gap: 14px;
|
|
624
683
|
padding: 6px 0;
|
|
625
684
|
margin: -6px 0;
|
|
626
685
|
background: color-mix(in srgb, var(--color-v2-background-bg-base, Canvas) 92%, transparent);
|
|
@@ -639,9 +698,7 @@ button.ph-chip:focus-visible,
|
|
|
639
698
|
|
|
640
699
|
@media (max-width: 760px) {
|
|
641
700
|
.ph-hero { flex-direction: column; }
|
|
642
|
-
.ph-
|
|
643
|
-
.ph-card-body { flex-basis: calc(100% - 50px); }
|
|
644
|
-
.ph-actions { width: 100%; justify-content: flex-start; padding-left: 50px; }
|
|
701
|
+
.ph-list { grid-template-columns: 1fr; }
|
|
645
702
|
.ph-detail-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
646
703
|
}
|
|
647
704
|
`
|
|
@@ -907,6 +964,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
907
964
|
|
|
908
965
|
const [updates, setUpdates] = createSignal<Map<string, UpdateInfo>>(new Map())
|
|
909
966
|
const [checked, setChecked] = createSignal(false)
|
|
967
|
+
const [staleOnly, setStaleOnly] = createSignal(false)
|
|
910
968
|
|
|
911
969
|
const [busyKeys, setBusyKeys] = createSignal<ReadonlySet<string>>(new Set())
|
|
912
970
|
const [note, setNote] = createSignal<Note>()
|
|
@@ -977,7 +1035,9 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
977
1035
|
const result = await props.admin.browse({ force })
|
|
978
1036
|
setCatalog([...result.entries])
|
|
979
1037
|
setMeta(result)
|
|
980
|
-
|
|
1038
|
+
// A fallback source still shows the full list, so a network miss is a downgrade to note,
|
|
1039
|
+
// not an error to alarm about. Only the stale built-in snapshot is worth a warning.
|
|
1040
|
+
if (result.error) notify(result.error, result.source === "builtin" ? "warning" : "info")
|
|
981
1041
|
// Keep the installed view in sync with what the catalog just reported.
|
|
982
1042
|
await refresh()
|
|
983
1043
|
} catch (error) {
|
|
@@ -1009,7 +1069,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1009
1069
|
notify(result.message, "error")
|
|
1010
1070
|
return
|
|
1011
1071
|
}
|
|
1012
|
-
notify(`${result.message}
|
|
1072
|
+
notify(`${result.message}。点工具栏的「彻底重启」后生效`, "success")
|
|
1013
1073
|
const latest = version || hits().find((hit) => hit.name === name)?.version || ""
|
|
1014
1074
|
setCatalog((prev) => prev.map((entry) => (entry.pkg === name ? { ...entry, installed: true } : entry)))
|
|
1015
1075
|
setHits((prev) =>
|
|
@@ -1040,7 +1100,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1040
1100
|
notify(result.message, "error")
|
|
1041
1101
|
return
|
|
1042
1102
|
}
|
|
1043
|
-
notify(`${result.message}
|
|
1103
|
+
notify(`${result.message}。点工具栏的「彻底重启」后生效`, "success")
|
|
1044
1104
|
setCatalog((prev) =>
|
|
1045
1105
|
prev.map((item) => (item.pkg === entry.name ? { ...item, installed: false } : item)),
|
|
1046
1106
|
)
|
|
@@ -1169,6 +1229,11 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1169
1229
|
|
|
1170
1230
|
const registryUrl = () => meta()?.registry || CATALOG_SOURCE_FALLBACK
|
|
1171
1231
|
const outdated = createMemo(() => [...updates().values()].filter((item) => item.hasUpdate).length)
|
|
1232
|
+
const visiblePlugins = createMemo(() => {
|
|
1233
|
+
const plugins = state()?.plugins ?? []
|
|
1234
|
+
if (!staleOnly()) return plugins
|
|
1235
|
+
return plugins.filter((entry) => updates().get(entry.spec)?.hasUpdate === true)
|
|
1236
|
+
})
|
|
1172
1237
|
const bySpec = (spec: string) => updates().get(spec)
|
|
1173
1238
|
const byPackage = createMemo(() => {
|
|
1174
1239
|
const map = new Map<string, UpdateInfo>()
|
|
@@ -1178,6 +1243,8 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1178
1243
|
|
|
1179
1244
|
const switchTab = (next: Tab) => {
|
|
1180
1245
|
setTab(next)
|
|
1246
|
+
// Leaving the installed view resets its "updatable only" filter.
|
|
1247
|
+
if (next !== "installed") setStaleOnly(false)
|
|
1181
1248
|
// The first look at the installed list also checks the registry, once.
|
|
1182
1249
|
if (next !== "catalog" && !checked() && !isBusy("updates")) void checkUpdates(true)
|
|
1183
1250
|
}
|
|
@@ -1215,6 +1282,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1215
1282
|
const catalogRow = (entry: CatalogEntry) => {
|
|
1216
1283
|
const info = () => byPackage().get(entry.pkg)
|
|
1217
1284
|
const stale = () => info()?.hasUpdate === true
|
|
1285
|
+
const busy = () => isBusy(`install:${entry.pkg}`)
|
|
1218
1286
|
const detail = () =>
|
|
1219
1287
|
openDetail(entry.pkg, {
|
|
1220
1288
|
version: entry.version,
|
|
@@ -1222,54 +1290,62 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1222
1290
|
repo: entry.repo,
|
|
1223
1291
|
})
|
|
1224
1292
|
return (
|
|
1225
|
-
<article class="ph-card">
|
|
1226
|
-
<div class="ph-
|
|
1227
|
-
<
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
<
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
</Show>
|
|
1252
|
-
<Show when={stale()}>
|
|
1253
|
-
<span class="ph-badge" data-tone="accent">
|
|
1254
|
-
可更新 → v{info()!.latest}
|
|
1255
|
-
</span>
|
|
1256
|
-
</Show>
|
|
1293
|
+
<article class="ph-card" data-stale={stale() ? "true" : undefined} data-busy={busy() ? "true" : undefined}>
|
|
1294
|
+
<div class="ph-card-main">
|
|
1295
|
+
<div class="ph-card-head">
|
|
1296
|
+
<div class="ph-tile">
|
|
1297
|
+
<ui.Icon name={typeIcon(entry.type)} size="normal" />
|
|
1298
|
+
</div>
|
|
1299
|
+
<div class="ph-card-titles">
|
|
1300
|
+
<div class="ph-name-row">
|
|
1301
|
+
<span class="ph-name">{entry.name}</span>
|
|
1302
|
+
</div>
|
|
1303
|
+
<Show when={entry.pkg}>
|
|
1304
|
+
<span class="ph-pkg">{entry.pkg}</span>
|
|
1305
|
+
</Show>
|
|
1306
|
+
</div>
|
|
1307
|
+
<div class="ph-chips">
|
|
1308
|
+
<Show when={entry.installed}>
|
|
1309
|
+
<span class="ph-badge" data-tone="success">
|
|
1310
|
+
已安装
|
|
1311
|
+
</span>
|
|
1312
|
+
</Show>
|
|
1313
|
+
<Show when={stale()}>
|
|
1314
|
+
<span class="ph-badge" data-tone="accent">
|
|
1315
|
+
可更新 → v{info()!.latest}
|
|
1316
|
+
</span>
|
|
1317
|
+
</Show>
|
|
1318
|
+
</div>
|
|
1257
1319
|
</div>
|
|
1258
|
-
<div
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1320
|
+
<div
|
|
1321
|
+
class="ph-card-body"
|
|
1322
|
+
role="button"
|
|
1323
|
+
tabindex="0"
|
|
1324
|
+
onClick={() => (entry.pkg ? detail() : void openUrl(entry.repo))}
|
|
1325
|
+
onKeyDown={(event) => {
|
|
1326
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1327
|
+
event.preventDefault()
|
|
1328
|
+
if (entry.pkg) detail()
|
|
1329
|
+
else void openUrl(entry.repo)
|
|
1330
|
+
}
|
|
1331
|
+
}}
|
|
1332
|
+
>
|
|
1333
|
+
<div class="ph-desc ph-clamp">{entry.desc || entry.long || "—"}</div>
|
|
1334
|
+
<div class="ph-meta">
|
|
1335
|
+
<span class="ph-type">{TYPE_LABEL[entry.type] ?? entry.type}</span>
|
|
1336
|
+
<Show when={entry.version}>
|
|
1337
|
+
<span class="ph-dot" />
|
|
1338
|
+
<span>v{entry.version}</span>
|
|
1339
|
+
</Show>
|
|
1340
|
+
<Show when={!entry.pkg}>
|
|
1341
|
+
<span class="ph-dot" />
|
|
1342
|
+
<span>仅 GitHub 发布</span>
|
|
1343
|
+
</Show>
|
|
1344
|
+
<Show when={entry.tags.length > 0}>
|
|
1345
|
+
<span class="ph-dot" />
|
|
1346
|
+
<span>{entry.tags.slice(0, 4).join(" / ")}</span>
|
|
1347
|
+
</Show>
|
|
1348
|
+
</div>
|
|
1273
1349
|
</div>
|
|
1274
1350
|
</div>
|
|
1275
1351
|
<div class="ph-actions">
|
|
@@ -1291,7 +1367,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1291
1367
|
variant="ghost"
|
|
1292
1368
|
size="small"
|
|
1293
1369
|
icon="download"
|
|
1294
|
-
busy={
|
|
1370
|
+
busy={busy()}
|
|
1295
1371
|
onClick={() => void install(entry.pkg, info()!.latest)}
|
|
1296
1372
|
>
|
|
1297
1373
|
更新
|
|
@@ -1302,7 +1378,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1302
1378
|
variant="neutral"
|
|
1303
1379
|
size="small"
|
|
1304
1380
|
icon="download"
|
|
1305
|
-
busy={
|
|
1381
|
+
busy={busy()}
|
|
1306
1382
|
onClick={() => void install(entry.pkg)}
|
|
1307
1383
|
>
|
|
1308
1384
|
安装
|
|
@@ -1326,71 +1402,70 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1326
1402
|
npm: hit.npm,
|
|
1327
1403
|
})
|
|
1328
1404
|
return (
|
|
1329
|
-
<article class="ph-card">
|
|
1330
|
-
<div class="ph-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
</
|
|
1350
|
-
</
|
|
1351
|
-
<Show when={hit.hasUpdate}>
|
|
1352
|
-
<span class="ph-badge" data-tone="accent">
|
|
1353
|
-
可更新 → v{hit.version}
|
|
1354
|
-
</span>
|
|
1355
|
-
</Show>
|
|
1405
|
+
<article class="ph-card" data-busy={isBusy(`install:${hit.name}`) ? "true" : undefined}>
|
|
1406
|
+
<div class="ph-card-main">
|
|
1407
|
+
<div class="ph-card-head">
|
|
1408
|
+
<div class="ph-tile">{initial(hit.name)}</div>
|
|
1409
|
+
<div class="ph-card-titles">
|
|
1410
|
+
<div class="ph-name-row">
|
|
1411
|
+
<span class="ph-name">{hit.name}</span>
|
|
1412
|
+
</div>
|
|
1413
|
+
<span class="ph-pkg">v{hit.version}</span>
|
|
1414
|
+
</div>
|
|
1415
|
+
<div class="ph-chips">
|
|
1416
|
+
<Show when={hit.installed}>
|
|
1417
|
+
<span class="ph-badge" data-tone="success">
|
|
1418
|
+
已安装{hit.current ? ` v${hit.current}` : ""}
|
|
1419
|
+
</span>
|
|
1420
|
+
</Show>
|
|
1421
|
+
<Show when={hit.hasUpdate}>
|
|
1422
|
+
<span class="ph-badge" data-tone="accent">
|
|
1423
|
+
可更新 → v{hit.version}
|
|
1424
|
+
</span>
|
|
1425
|
+
</Show>
|
|
1426
|
+
</div>
|
|
1356
1427
|
</div>
|
|
1357
|
-
<div
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
<
|
|
1370
|
-
|
|
1371
|
-
<span
|
|
1372
|
-
|
|
1373
|
-
|
|
1428
|
+
<div
|
|
1429
|
+
class="ph-card-body"
|
|
1430
|
+
role="button"
|
|
1431
|
+
tabindex="0"
|
|
1432
|
+
onClick={detail}
|
|
1433
|
+
onKeyDown={(event) => {
|
|
1434
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1435
|
+
event.preventDefault()
|
|
1436
|
+
detail()
|
|
1437
|
+
}
|
|
1438
|
+
}}
|
|
1439
|
+
>
|
|
1440
|
+
<div class="ph-desc ph-clamp">{hit.description || "—"}</div>
|
|
1441
|
+
<div class="ph-meta">
|
|
1442
|
+
<span title={`周下载 ${hit.weekly.toLocaleString()} 次`}>
|
|
1443
|
+
<ui.Icon name="download" size="small" />
|
|
1444
|
+
{formatCount(hit.weekly)}/周
|
|
1445
|
+
</span>
|
|
1374
1446
|
<span class="ph-dot" />
|
|
1375
|
-
<span>{
|
|
1447
|
+
<span title={`月下载 ${hit.monthly.toLocaleString()} 次`}>{formatCount(hit.monthly)}/月</span>
|
|
1448
|
+
<Show when={hit.publisher}>
|
|
1449
|
+
<span class="ph-dot" />
|
|
1450
|
+
<span>by {hit.publisher}</span>
|
|
1451
|
+
</Show>
|
|
1452
|
+
<Show when={hit.published}>
|
|
1453
|
+
<span class="ph-dot" />
|
|
1454
|
+
<span>{relativeDate(hit.published)}更新</span>
|
|
1455
|
+
</Show>
|
|
1456
|
+
</div>
|
|
1457
|
+
<Show when={hit.keywords.length > 0}>
|
|
1458
|
+
<div class="ph-chips">
|
|
1459
|
+
<For each={hit.keywords.slice(0, 3)}>{(keyword) => <span class="ph-chip">{keyword}</span>}</For>
|
|
1460
|
+
</div>
|
|
1376
1461
|
</Show>
|
|
1377
1462
|
</div>
|
|
1378
|
-
<Show when={hit.keywords.length > 0}>
|
|
1379
|
-
<div class="ph-chips">
|
|
1380
|
-
<For each={hit.keywords}>{(keyword) => <span class="ph-chip">{keyword}</span>}</For>
|
|
1381
|
-
</div>
|
|
1382
|
-
</Show>
|
|
1383
1463
|
</div>
|
|
1384
1464
|
<div class="ph-actions">
|
|
1385
1465
|
<Show when={hit.homepage}>
|
|
1386
|
-
<ui.
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
icon="globe"
|
|
1390
|
-
onClick={() => void openUrl(hit.homepage)}
|
|
1391
|
-
>
|
|
1392
|
-
主页
|
|
1393
|
-
</ui.Button>
|
|
1466
|
+
<ui.Tooltip value="主页">
|
|
1467
|
+
<ui.IconButton icon="globe" size="small" label="主页" onClick={() => void openUrl(hit.homepage)} />
|
|
1468
|
+
</ui.Tooltip>
|
|
1394
1469
|
</Show>
|
|
1395
1470
|
<Show when={hit.repo}>
|
|
1396
1471
|
<ui.Tooltip value="源码仓库">
|
|
@@ -1435,35 +1510,44 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1435
1510
|
const installedRow = (entry: PluginEntry) => {
|
|
1436
1511
|
const info = () => bySpec(entry.spec)
|
|
1437
1512
|
const stale = () => info()?.hasUpdate === true
|
|
1513
|
+
const busy = () => isBusy(`install:${entry.name}`)
|
|
1438
1514
|
const current = () => info()?.current || entry.version
|
|
1439
1515
|
const unchecked = () => (info()?.error ?? "") !== ""
|
|
1440
1516
|
return (
|
|
1441
|
-
<article class="ph-card">
|
|
1442
|
-
<div class="ph-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
<
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1517
|
+
<article class="ph-card" data-stale={stale() ? "true" : undefined} data-busy={busy() ? "true" : undefined}>
|
|
1518
|
+
<div class="ph-card-main">
|
|
1519
|
+
<div class="ph-card-head">
|
|
1520
|
+
<div class="ph-tile">{initial(entry.name)}</div>
|
|
1521
|
+
<div class="ph-card-titles">
|
|
1522
|
+
<div class="ph-name-row">
|
|
1523
|
+
<span class="ph-name">{entry.name}</span>
|
|
1524
|
+
</div>
|
|
1525
|
+
<span class="ph-pkg">{entry.spec}</span>
|
|
1526
|
+
</div>
|
|
1527
|
+
<div class="ph-chips">
|
|
1528
|
+
<Show when={stale()}>
|
|
1529
|
+
<span class="ph-badge" data-tone="accent">
|
|
1530
|
+
可更新 → v{info()!.latest}
|
|
1531
|
+
</span>
|
|
1532
|
+
</Show>
|
|
1533
|
+
<Show when={unchecked()}>
|
|
1534
|
+
<span class="ph-badge" data-tone="warning">
|
|
1535
|
+
未能检查
|
|
1536
|
+
</span>
|
|
1537
|
+
</Show>
|
|
1538
|
+
<Show when={entry.kind === "path"}>
|
|
1539
|
+
<span class="ph-badge">本地目录</span>
|
|
1540
|
+
</Show>
|
|
1541
|
+
</div>
|
|
1460
1542
|
</div>
|
|
1461
|
-
<div class="ph-
|
|
1462
|
-
<
|
|
1463
|
-
|
|
1464
|
-
<
|
|
1465
|
-
|
|
1466
|
-
|
|
1543
|
+
<div class="ph-card-body">
|
|
1544
|
+
<div class="ph-meta">
|
|
1545
|
+
{versionLabel(current(), info()?.latest ?? "", stale())}
|
|
1546
|
+
<Show when={stale() && entry.kind === "path"}>
|
|
1547
|
+
<span class="ph-dot" />
|
|
1548
|
+
<span>更新会覆盖目录内容,原文件自动备份</span>
|
|
1549
|
+
</Show>
|
|
1550
|
+
</div>
|
|
1467
1551
|
</div>
|
|
1468
1552
|
</div>
|
|
1469
1553
|
<div class="ph-actions">
|
|
@@ -1482,7 +1566,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1482
1566
|
variant="ghost"
|
|
1483
1567
|
size="small"
|
|
1484
1568
|
icon="download"
|
|
1485
|
-
busy={
|
|
1569
|
+
busy={busy()}
|
|
1486
1570
|
onClick={() => void install(info()!.name, info()!.latest)}
|
|
1487
1571
|
>
|
|
1488
1572
|
更新
|
|
@@ -1493,7 +1577,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1493
1577
|
variant="ghost-muted"
|
|
1494
1578
|
size="small"
|
|
1495
1579
|
icon="trash"
|
|
1496
|
-
busy={
|
|
1580
|
+
busy={busy()}
|
|
1497
1581
|
onClick={() => void uninstall(entry)}
|
|
1498
1582
|
>
|
|
1499
1583
|
移除
|
|
@@ -1535,21 +1619,22 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1535
1619
|
</div>
|
|
1536
1620
|
</section>
|
|
1537
1621
|
|
|
1538
|
-
<
|
|
1539
|
-
<
|
|
1540
|
-
<
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1622
|
+
<div class="ph-head">
|
|
1623
|
+
<Show when={note()}>
|
|
1624
|
+
<div class="ph-notice" data-tone={note()!.tone}>
|
|
1625
|
+
<ui.Icon name={noteIcon(note()!.tone)} size="small" />
|
|
1626
|
+
<span>{note()!.text}</span>
|
|
1627
|
+
</div>
|
|
1628
|
+
</Show>
|
|
1544
1629
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1630
|
+
<Show when={state()?.error}>
|
|
1631
|
+
<div class="ph-notice" data-tone="error">
|
|
1632
|
+
<ui.Icon name="circle-exclamation" size="small" />
|
|
1633
|
+
<span>{state()!.error}</span>
|
|
1634
|
+
</div>
|
|
1635
|
+
</Show>
|
|
1551
1636
|
|
|
1552
|
-
|
|
1637
|
+
<div class="ph-toolbar">
|
|
1553
1638
|
<div class="ph-tabs" role="tablist">
|
|
1554
1639
|
{tabButton("catalog", "精选目录", "extensions", meta() ? catalog().length : undefined)}
|
|
1555
1640
|
{tabButton("search", "npm 搜索", "magnifying-glass", searched() ? hits().length : undefined)}
|
|
@@ -1621,7 +1706,8 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1621
1706
|
>
|
|
1622
1707
|
检查更新
|
|
1623
1708
|
</ui.Button>
|
|
1624
|
-
|
|
1709
|
+
</Show>
|
|
1710
|
+
</div>
|
|
1625
1711
|
</div>
|
|
1626
1712
|
|
|
1627
1713
|
<Switch>
|
|
@@ -1793,6 +1879,21 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1793
1879
|
<h3 class="ph-h3">
|
|
1794
1880
|
已安装 <span class="ph-count">{state()?.plugins.length ?? 0}</span>
|
|
1795
1881
|
</h3>
|
|
1882
|
+
<div class="ph-chips">
|
|
1883
|
+
<button
|
|
1884
|
+
class="ph-chip"
|
|
1885
|
+
data-active={staleOnly()}
|
|
1886
|
+
data-disabled={outdated() === 0 ? "true" : undefined}
|
|
1887
|
+
title={outdated() === 0 ? "检查更新后再来看可更新的插件" : "只显示有新版本的插件"}
|
|
1888
|
+
onClick={() => {
|
|
1889
|
+
if (outdated() > 0) setStaleOnly(!staleOnly())
|
|
1890
|
+
}}
|
|
1891
|
+
>
|
|
1892
|
+
可更新
|
|
1893
|
+
<span class="ph-chip-count">{outdated()}</span>
|
|
1894
|
+
</button>
|
|
1895
|
+
</div>
|
|
1896
|
+
<span class="ph-spacer" />
|
|
1796
1897
|
<Show when={checked()}>
|
|
1797
1898
|
<span class="ph-meta">
|
|
1798
1899
|
检查过 npm registry{outdated() > 0 ? ` · ${outdated()} 个可更新` : " · 全部已是最新"}
|
|
@@ -1809,9 +1910,23 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1809
1910
|
</div>
|
|
1810
1911
|
}
|
|
1811
1912
|
>
|
|
1812
|
-
<
|
|
1813
|
-
|
|
1814
|
-
|
|
1913
|
+
<Show
|
|
1914
|
+
when={visiblePlugins().length > 0}
|
|
1915
|
+
fallback={
|
|
1916
|
+
<div class="ph-empty">
|
|
1917
|
+
<ui.Icon name="checklist" size="large" />
|
|
1918
|
+
<strong>没有可更新的插件</strong>
|
|
1919
|
+
<span>已安装的插件都已是最新版本。点工具栏的「检查更新」可以再查一次。</span>
|
|
1920
|
+
<ui.Button variant="ghost" size="small" onClick={() => setStaleOnly(false)}>
|
|
1921
|
+
显示全部
|
|
1922
|
+
</ui.Button>
|
|
1923
|
+
</div>
|
|
1924
|
+
}
|
|
1925
|
+
>
|
|
1926
|
+
<div class="ph-list">
|
|
1927
|
+
<For each={visiblePlugins()}>{(entry) => installedRow(entry)}</For>
|
|
1928
|
+
</div>
|
|
1929
|
+
</Show>
|
|
1815
1930
|
</Show>
|
|
1816
1931
|
</section>
|
|
1817
1932
|
</Match>
|
|
@@ -1819,7 +1934,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1819
1934
|
|
|
1820
1935
|
<footer class="ph-footer">
|
|
1821
1936
|
<div class="ph-footer-note">
|
|
1822
|
-
|
|
1937
|
+
安装和移除只改配置,不会立即生效:插件与模型目录都是启动时快照的。改完点工具栏的「彻底重启」,应用会自动退出并重新打开。
|
|
1823
1938
|
</div>
|
|
1824
1939
|
</footer>
|
|
1825
1940
|
</div>
|