opencode-plugin-hub 0.1.2 → 0.1.3
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/page.tsx +283 -170
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>()
|
|
@@ -1009,7 +1067,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1009
1067
|
notify(result.message, "error")
|
|
1010
1068
|
return
|
|
1011
1069
|
}
|
|
1012
|
-
notify(`${result.message}
|
|
1070
|
+
notify(`${result.message}。点工具栏的「彻底重启」后生效`, "success")
|
|
1013
1071
|
const latest = version || hits().find((hit) => hit.name === name)?.version || ""
|
|
1014
1072
|
setCatalog((prev) => prev.map((entry) => (entry.pkg === name ? { ...entry, installed: true } : entry)))
|
|
1015
1073
|
setHits((prev) =>
|
|
@@ -1040,7 +1098,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1040
1098
|
notify(result.message, "error")
|
|
1041
1099
|
return
|
|
1042
1100
|
}
|
|
1043
|
-
notify(`${result.message}
|
|
1101
|
+
notify(`${result.message}。点工具栏的「彻底重启」后生效`, "success")
|
|
1044
1102
|
setCatalog((prev) =>
|
|
1045
1103
|
prev.map((item) => (item.pkg === entry.name ? { ...item, installed: false } : item)),
|
|
1046
1104
|
)
|
|
@@ -1169,6 +1227,11 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1169
1227
|
|
|
1170
1228
|
const registryUrl = () => meta()?.registry || CATALOG_SOURCE_FALLBACK
|
|
1171
1229
|
const outdated = createMemo(() => [...updates().values()].filter((item) => item.hasUpdate).length)
|
|
1230
|
+
const visiblePlugins = createMemo(() => {
|
|
1231
|
+
const plugins = state()?.plugins ?? []
|
|
1232
|
+
if (!staleOnly()) return plugins
|
|
1233
|
+
return plugins.filter((entry) => updates().get(entry.spec)?.hasUpdate === true)
|
|
1234
|
+
})
|
|
1172
1235
|
const bySpec = (spec: string) => updates().get(spec)
|
|
1173
1236
|
const byPackage = createMemo(() => {
|
|
1174
1237
|
const map = new Map<string, UpdateInfo>()
|
|
@@ -1178,6 +1241,8 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1178
1241
|
|
|
1179
1242
|
const switchTab = (next: Tab) => {
|
|
1180
1243
|
setTab(next)
|
|
1244
|
+
// Leaving the installed view resets its "updatable only" filter.
|
|
1245
|
+
if (next !== "installed") setStaleOnly(false)
|
|
1181
1246
|
// The first look at the installed list also checks the registry, once.
|
|
1182
1247
|
if (next !== "catalog" && !checked() && !isBusy("updates")) void checkUpdates(true)
|
|
1183
1248
|
}
|
|
@@ -1215,6 +1280,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1215
1280
|
const catalogRow = (entry: CatalogEntry) => {
|
|
1216
1281
|
const info = () => byPackage().get(entry.pkg)
|
|
1217
1282
|
const stale = () => info()?.hasUpdate === true
|
|
1283
|
+
const busy = () => isBusy(`install:${entry.pkg}`)
|
|
1218
1284
|
const detail = () =>
|
|
1219
1285
|
openDetail(entry.pkg, {
|
|
1220
1286
|
version: entry.version,
|
|
@@ -1222,54 +1288,62 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1222
1288
|
repo: entry.repo,
|
|
1223
1289
|
})
|
|
1224
1290
|
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>
|
|
1291
|
+
<article class="ph-card" data-stale={stale() ? "true" : undefined} data-busy={busy() ? "true" : undefined}>
|
|
1292
|
+
<div class="ph-card-main">
|
|
1293
|
+
<div class="ph-card-head">
|
|
1294
|
+
<div class="ph-tile">
|
|
1295
|
+
<ui.Icon name={typeIcon(entry.type)} size="normal" />
|
|
1296
|
+
</div>
|
|
1297
|
+
<div class="ph-card-titles">
|
|
1298
|
+
<div class="ph-name-row">
|
|
1299
|
+
<span class="ph-name">{entry.name}</span>
|
|
1300
|
+
</div>
|
|
1301
|
+
<Show when={entry.pkg}>
|
|
1302
|
+
<span class="ph-pkg">{entry.pkg}</span>
|
|
1303
|
+
</Show>
|
|
1304
|
+
</div>
|
|
1305
|
+
<div class="ph-chips">
|
|
1306
|
+
<Show when={entry.installed}>
|
|
1307
|
+
<span class="ph-badge" data-tone="success">
|
|
1308
|
+
已安装
|
|
1309
|
+
</span>
|
|
1310
|
+
</Show>
|
|
1311
|
+
<Show when={stale()}>
|
|
1312
|
+
<span class="ph-badge" data-tone="accent">
|
|
1313
|
+
可更新 → v{info()!.latest}
|
|
1314
|
+
</span>
|
|
1315
|
+
</Show>
|
|
1316
|
+
</div>
|
|
1257
1317
|
</div>
|
|
1258
|
-
<div
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1318
|
+
<div
|
|
1319
|
+
class="ph-card-body"
|
|
1320
|
+
role="button"
|
|
1321
|
+
tabindex="0"
|
|
1322
|
+
onClick={() => (entry.pkg ? detail() : void openUrl(entry.repo))}
|
|
1323
|
+
onKeyDown={(event) => {
|
|
1324
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1325
|
+
event.preventDefault()
|
|
1326
|
+
if (entry.pkg) detail()
|
|
1327
|
+
else void openUrl(entry.repo)
|
|
1328
|
+
}
|
|
1329
|
+
}}
|
|
1330
|
+
>
|
|
1331
|
+
<div class="ph-desc ph-clamp">{entry.desc || entry.long || "—"}</div>
|
|
1332
|
+
<div class="ph-meta">
|
|
1333
|
+
<span class="ph-type">{TYPE_LABEL[entry.type] ?? entry.type}</span>
|
|
1334
|
+
<Show when={entry.version}>
|
|
1335
|
+
<span class="ph-dot" />
|
|
1336
|
+
<span>v{entry.version}</span>
|
|
1337
|
+
</Show>
|
|
1338
|
+
<Show when={!entry.pkg}>
|
|
1339
|
+
<span class="ph-dot" />
|
|
1340
|
+
<span>仅 GitHub 发布</span>
|
|
1341
|
+
</Show>
|
|
1342
|
+
<Show when={entry.tags.length > 0}>
|
|
1343
|
+
<span class="ph-dot" />
|
|
1344
|
+
<span>{entry.tags.slice(0, 4).join(" / ")}</span>
|
|
1345
|
+
</Show>
|
|
1346
|
+
</div>
|
|
1273
1347
|
</div>
|
|
1274
1348
|
</div>
|
|
1275
1349
|
<div class="ph-actions">
|
|
@@ -1291,7 +1365,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1291
1365
|
variant="ghost"
|
|
1292
1366
|
size="small"
|
|
1293
1367
|
icon="download"
|
|
1294
|
-
busy={
|
|
1368
|
+
busy={busy()}
|
|
1295
1369
|
onClick={() => void install(entry.pkg, info()!.latest)}
|
|
1296
1370
|
>
|
|
1297
1371
|
更新
|
|
@@ -1302,7 +1376,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1302
1376
|
variant="neutral"
|
|
1303
1377
|
size="small"
|
|
1304
1378
|
icon="download"
|
|
1305
|
-
busy={
|
|
1379
|
+
busy={busy()}
|
|
1306
1380
|
onClick={() => void install(entry.pkg)}
|
|
1307
1381
|
>
|
|
1308
1382
|
安装
|
|
@@ -1326,71 +1400,70 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1326
1400
|
npm: hit.npm,
|
|
1327
1401
|
})
|
|
1328
1402
|
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>
|
|
1403
|
+
<article class="ph-card" data-busy={isBusy(`install:${hit.name}`) ? "true" : undefined}>
|
|
1404
|
+
<div class="ph-card-main">
|
|
1405
|
+
<div class="ph-card-head">
|
|
1406
|
+
<div class="ph-tile">{initial(hit.name)}</div>
|
|
1407
|
+
<div class="ph-card-titles">
|
|
1408
|
+
<div class="ph-name-row">
|
|
1409
|
+
<span class="ph-name">{hit.name}</span>
|
|
1410
|
+
</div>
|
|
1411
|
+
<span class="ph-pkg">v{hit.version}</span>
|
|
1412
|
+
</div>
|
|
1413
|
+
<div class="ph-chips">
|
|
1414
|
+
<Show when={hit.installed}>
|
|
1415
|
+
<span class="ph-badge" data-tone="success">
|
|
1416
|
+
已安装{hit.current ? ` v${hit.current}` : ""}
|
|
1417
|
+
</span>
|
|
1418
|
+
</Show>
|
|
1419
|
+
<Show when={hit.hasUpdate}>
|
|
1420
|
+
<span class="ph-badge" data-tone="accent">
|
|
1421
|
+
可更新 → v{hit.version}
|
|
1422
|
+
</span>
|
|
1423
|
+
</Show>
|
|
1424
|
+
</div>
|
|
1356
1425
|
</div>
|
|
1357
|
-
<div
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
<
|
|
1370
|
-
|
|
1371
|
-
<span
|
|
1372
|
-
|
|
1373
|
-
|
|
1426
|
+
<div
|
|
1427
|
+
class="ph-card-body"
|
|
1428
|
+
role="button"
|
|
1429
|
+
tabindex="0"
|
|
1430
|
+
onClick={detail}
|
|
1431
|
+
onKeyDown={(event) => {
|
|
1432
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1433
|
+
event.preventDefault()
|
|
1434
|
+
detail()
|
|
1435
|
+
}
|
|
1436
|
+
}}
|
|
1437
|
+
>
|
|
1438
|
+
<div class="ph-desc ph-clamp">{hit.description || "—"}</div>
|
|
1439
|
+
<div class="ph-meta">
|
|
1440
|
+
<span title={`周下载 ${hit.weekly.toLocaleString()} 次`}>
|
|
1441
|
+
<ui.Icon name="download" size="small" />
|
|
1442
|
+
{formatCount(hit.weekly)}/周
|
|
1443
|
+
</span>
|
|
1374
1444
|
<span class="ph-dot" />
|
|
1375
|
-
<span>{
|
|
1445
|
+
<span title={`月下载 ${hit.monthly.toLocaleString()} 次`}>{formatCount(hit.monthly)}/月</span>
|
|
1446
|
+
<Show when={hit.publisher}>
|
|
1447
|
+
<span class="ph-dot" />
|
|
1448
|
+
<span>by {hit.publisher}</span>
|
|
1449
|
+
</Show>
|
|
1450
|
+
<Show when={hit.published}>
|
|
1451
|
+
<span class="ph-dot" />
|
|
1452
|
+
<span>{relativeDate(hit.published)}更新</span>
|
|
1453
|
+
</Show>
|
|
1454
|
+
</div>
|
|
1455
|
+
<Show when={hit.keywords.length > 0}>
|
|
1456
|
+
<div class="ph-chips">
|
|
1457
|
+
<For each={hit.keywords.slice(0, 3)}>{(keyword) => <span class="ph-chip">{keyword}</span>}</For>
|
|
1458
|
+
</div>
|
|
1376
1459
|
</Show>
|
|
1377
1460
|
</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
1461
|
</div>
|
|
1384
1462
|
<div class="ph-actions">
|
|
1385
1463
|
<Show when={hit.homepage}>
|
|
1386
|
-
<ui.
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
icon="globe"
|
|
1390
|
-
onClick={() => void openUrl(hit.homepage)}
|
|
1391
|
-
>
|
|
1392
|
-
主页
|
|
1393
|
-
</ui.Button>
|
|
1464
|
+
<ui.Tooltip value="主页">
|
|
1465
|
+
<ui.IconButton icon="globe" size="small" label="主页" onClick={() => void openUrl(hit.homepage)} />
|
|
1466
|
+
</ui.Tooltip>
|
|
1394
1467
|
</Show>
|
|
1395
1468
|
<Show when={hit.repo}>
|
|
1396
1469
|
<ui.Tooltip value="源码仓库">
|
|
@@ -1435,35 +1508,44 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1435
1508
|
const installedRow = (entry: PluginEntry) => {
|
|
1436
1509
|
const info = () => bySpec(entry.spec)
|
|
1437
1510
|
const stale = () => info()?.hasUpdate === true
|
|
1511
|
+
const busy = () => isBusy(`install:${entry.name}`)
|
|
1438
1512
|
const current = () => info()?.current || entry.version
|
|
1439
1513
|
const unchecked = () => (info()?.error ?? "") !== ""
|
|
1440
1514
|
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
|
-
|
|
1515
|
+
<article class="ph-card" data-stale={stale() ? "true" : undefined} data-busy={busy() ? "true" : undefined}>
|
|
1516
|
+
<div class="ph-card-main">
|
|
1517
|
+
<div class="ph-card-head">
|
|
1518
|
+
<div class="ph-tile">{initial(entry.name)}</div>
|
|
1519
|
+
<div class="ph-card-titles">
|
|
1520
|
+
<div class="ph-name-row">
|
|
1521
|
+
<span class="ph-name">{entry.name}</span>
|
|
1522
|
+
</div>
|
|
1523
|
+
<span class="ph-pkg">{entry.spec}</span>
|
|
1524
|
+
</div>
|
|
1525
|
+
<div class="ph-chips">
|
|
1526
|
+
<Show when={stale()}>
|
|
1527
|
+
<span class="ph-badge" data-tone="accent">
|
|
1528
|
+
可更新 → v{info()!.latest}
|
|
1529
|
+
</span>
|
|
1530
|
+
</Show>
|
|
1531
|
+
<Show when={unchecked()}>
|
|
1532
|
+
<span class="ph-badge" data-tone="warning">
|
|
1533
|
+
未能检查
|
|
1534
|
+
</span>
|
|
1535
|
+
</Show>
|
|
1536
|
+
<Show when={entry.kind === "path"}>
|
|
1537
|
+
<span class="ph-badge">本地目录</span>
|
|
1538
|
+
</Show>
|
|
1539
|
+
</div>
|
|
1460
1540
|
</div>
|
|
1461
|
-
<div class="ph-
|
|
1462
|
-
<
|
|
1463
|
-
|
|
1464
|
-
<
|
|
1465
|
-
|
|
1466
|
-
|
|
1541
|
+
<div class="ph-card-body">
|
|
1542
|
+
<div class="ph-meta">
|
|
1543
|
+
{versionLabel(current(), info()?.latest ?? "", stale())}
|
|
1544
|
+
<Show when={stale() && entry.kind === "path"}>
|
|
1545
|
+
<span class="ph-dot" />
|
|
1546
|
+
<span>更新会覆盖目录内容,原文件自动备份</span>
|
|
1547
|
+
</Show>
|
|
1548
|
+
</div>
|
|
1467
1549
|
</div>
|
|
1468
1550
|
</div>
|
|
1469
1551
|
<div class="ph-actions">
|
|
@@ -1482,7 +1564,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1482
1564
|
variant="ghost"
|
|
1483
1565
|
size="small"
|
|
1484
1566
|
icon="download"
|
|
1485
|
-
busy={
|
|
1567
|
+
busy={busy()}
|
|
1486
1568
|
onClick={() => void install(info()!.name, info()!.latest)}
|
|
1487
1569
|
>
|
|
1488
1570
|
更新
|
|
@@ -1493,7 +1575,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1493
1575
|
variant="ghost-muted"
|
|
1494
1576
|
size="small"
|
|
1495
1577
|
icon="trash"
|
|
1496
|
-
busy={
|
|
1578
|
+
busy={busy()}
|
|
1497
1579
|
onClick={() => void uninstall(entry)}
|
|
1498
1580
|
>
|
|
1499
1581
|
移除
|
|
@@ -1535,21 +1617,22 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1535
1617
|
</div>
|
|
1536
1618
|
</section>
|
|
1537
1619
|
|
|
1538
|
-
<
|
|
1539
|
-
<
|
|
1540
|
-
<
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1620
|
+
<div class="ph-head">
|
|
1621
|
+
<Show when={note()}>
|
|
1622
|
+
<div class="ph-notice" data-tone={note()!.tone}>
|
|
1623
|
+
<ui.Icon name={noteIcon(note()!.tone)} size="small" />
|
|
1624
|
+
<span>{note()!.text}</span>
|
|
1625
|
+
</div>
|
|
1626
|
+
</Show>
|
|
1544
1627
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1628
|
+
<Show when={state()?.error}>
|
|
1629
|
+
<div class="ph-notice" data-tone="error">
|
|
1630
|
+
<ui.Icon name="circle-exclamation" size="small" />
|
|
1631
|
+
<span>{state()!.error}</span>
|
|
1632
|
+
</div>
|
|
1633
|
+
</Show>
|
|
1551
1634
|
|
|
1552
|
-
|
|
1635
|
+
<div class="ph-toolbar">
|
|
1553
1636
|
<div class="ph-tabs" role="tablist">
|
|
1554
1637
|
{tabButton("catalog", "精选目录", "extensions", meta() ? catalog().length : undefined)}
|
|
1555
1638
|
{tabButton("search", "npm 搜索", "magnifying-glass", searched() ? hits().length : undefined)}
|
|
@@ -1621,7 +1704,8 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1621
1704
|
>
|
|
1622
1705
|
检查更新
|
|
1623
1706
|
</ui.Button>
|
|
1624
|
-
|
|
1707
|
+
</Show>
|
|
1708
|
+
</div>
|
|
1625
1709
|
</div>
|
|
1626
1710
|
|
|
1627
1711
|
<Switch>
|
|
@@ -1793,6 +1877,21 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1793
1877
|
<h3 class="ph-h3">
|
|
1794
1878
|
已安装 <span class="ph-count">{state()?.plugins.length ?? 0}</span>
|
|
1795
1879
|
</h3>
|
|
1880
|
+
<div class="ph-chips">
|
|
1881
|
+
<button
|
|
1882
|
+
class="ph-chip"
|
|
1883
|
+
data-active={staleOnly()}
|
|
1884
|
+
data-disabled={outdated() === 0 ? "true" : undefined}
|
|
1885
|
+
title={outdated() === 0 ? "检查更新后再来看可更新的插件" : "只显示有新版本的插件"}
|
|
1886
|
+
onClick={() => {
|
|
1887
|
+
if (outdated() > 0) setStaleOnly(!staleOnly())
|
|
1888
|
+
}}
|
|
1889
|
+
>
|
|
1890
|
+
可更新
|
|
1891
|
+
<span class="ph-chip-count">{outdated()}</span>
|
|
1892
|
+
</button>
|
|
1893
|
+
</div>
|
|
1894
|
+
<span class="ph-spacer" />
|
|
1796
1895
|
<Show when={checked()}>
|
|
1797
1896
|
<span class="ph-meta">
|
|
1798
1897
|
检查过 npm registry{outdated() > 0 ? ` · ${outdated()} 个可更新` : " · 全部已是最新"}
|
|
@@ -1809,9 +1908,23 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1809
1908
|
</div>
|
|
1810
1909
|
}
|
|
1811
1910
|
>
|
|
1812
|
-
<
|
|
1813
|
-
|
|
1814
|
-
|
|
1911
|
+
<Show
|
|
1912
|
+
when={visiblePlugins().length > 0}
|
|
1913
|
+
fallback={
|
|
1914
|
+
<div class="ph-empty">
|
|
1915
|
+
<ui.Icon name="checklist" size="large" />
|
|
1916
|
+
<strong>没有可更新的插件</strong>
|
|
1917
|
+
<span>已安装的插件都已是最新版本。点工具栏的「检查更新」可以再查一次。</span>
|
|
1918
|
+
<ui.Button variant="ghost" size="small" onClick={() => setStaleOnly(false)}>
|
|
1919
|
+
显示全部
|
|
1920
|
+
</ui.Button>
|
|
1921
|
+
</div>
|
|
1922
|
+
}
|
|
1923
|
+
>
|
|
1924
|
+
<div class="ph-list">
|
|
1925
|
+
<For each={visiblePlugins()}>{(entry) => installedRow(entry)}</For>
|
|
1926
|
+
</div>
|
|
1927
|
+
</Show>
|
|
1815
1928
|
</Show>
|
|
1816
1929
|
</section>
|
|
1817
1930
|
</Match>
|
|
@@ -1819,7 +1932,7 @@ export function PluginHubPage(props: { ctx: Context; admin: AdminClient; embedde
|
|
|
1819
1932
|
|
|
1820
1933
|
<footer class="ph-footer">
|
|
1821
1934
|
<div class="ph-footer-note">
|
|
1822
|
-
|
|
1935
|
+
安装和移除只改配置,不会立即生效:插件与模型目录都是启动时快照的。改完点工具栏的「彻底重启」,应用会自动退出并重新打开。
|
|
1823
1936
|
</div>
|
|
1824
1937
|
</footer>
|
|
1825
1938
|
</div>
|