@snap-rail/framework-shadcn 0.0.0-stage → 0.2.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 +20 -2
- package/docs/framework-authoring.md +59 -0
- package/lib/fallback.js +307 -0
- package/lib/index.js +3036 -0
- package/lib/runtime.js +2827 -0
- package/lib/semantic.js +96 -0
- package/lib/session.js +48 -0
- package/lib/settings-pages.js +68 -0
- package/lib/slots.js +95 -0
- package/lib/types/Shell.d.ts +13 -0
- package/lib/types/Shell.js +17 -0
- package/lib/types/client.d.ts +41 -0
- package/lib/types/client.js +39 -0
- package/lib/types/fallback.d.ts +15 -0
- package/lib/types/fallback.js +52 -0
- package/lib/types/index.d.ts +17 -0
- package/lib/types/index.js +17 -0
- package/lib/types/runtime.d.ts +53 -0
- package/lib/types/runtime.js +75 -0
- package/lib/types/semantic.d.ts +55 -0
- package/lib/types/semantic.js +63 -0
- package/lib/types/session.d.ts +43 -0
- package/lib/types/session.js +76 -0
- package/lib/types/settings-dialog.d.ts +17 -0
- package/lib/types/settings-dialog.js +138 -0
- package/lib/types/settings-pages.d.ts +68 -0
- package/lib/types/settings-pages.js +85 -0
- package/lib/types/settings-plugins-page.d.ts +29 -0
- package/lib/types/settings-plugins-page.js +466 -0
- package/lib/types/settings-update-page.d.ts +17 -0
- package/lib/types/settings-update-page.js +124 -0
- package/lib/types/slots.d.ts +114 -0
- package/lib/types/slots.js +119 -0
- package/lib/types/ui/badge.d.ts +17 -0
- package/lib/types/ui/badge.js +29 -0
- package/lib/types/ui/button.d.ts +20 -0
- package/lib/types/ui/button.js +39 -0
- package/lib/types/ui/card.d.ts +14 -0
- package/lib/types/ui/card.js +18 -0
- package/lib/types/ui/checkbox.d.ts +10 -0
- package/lib/types/ui/checkbox.js +13 -0
- package/lib/types/ui/collapsible.d.ts +24 -0
- package/lib/types/ui/collapsible.js +31 -0
- package/lib/types/ui/dialog.d.ts +22 -0
- package/lib/types/ui/dialog.js +29 -0
- package/lib/types/ui/drag-scroll.d.ts +13 -0
- package/lib/types/ui/drag-scroll.js +85 -0
- package/lib/types/ui/hooks.d.ts +16 -0
- package/lib/types/ui/hooks.js +26 -0
- package/lib/types/ui/icons.d.ts +21 -0
- package/lib/types/ui/icons.js +18 -0
- package/lib/types/ui/index.d.ts +32 -0
- package/lib/types/ui/index.js +32 -0
- package/lib/types/ui/input.d.ts +8 -0
- package/lib/types/ui/input.js +6 -0
- package/lib/types/ui/label.d.ts +9 -0
- package/lib/types/ui/label.js +12 -0
- package/lib/types/ui/led.d.ts +13 -0
- package/lib/types/ui/led.js +11 -0
- package/lib/types/ui/number-input.d.ts +35 -0
- package/lib/types/ui/number-input.js +48 -0
- package/lib/types/ui/progress.d.ts +10 -0
- package/lib/types/ui/progress.js +14 -0
- package/lib/types/ui/radio-group.d.ts +12 -0
- package/lib/types/ui/radio-group.js +17 -0
- package/lib/types/ui/schema-form.d.ts +51 -0
- package/lib/types/ui/schema-form.js +124 -0
- package/lib/types/ui/scroll-area.d.ts +10 -0
- package/lib/types/ui/scroll-area.js +13 -0
- package/lib/types/ui/switch.d.ts +10 -0
- package/lib/types/ui/switch.js +13 -0
- package/lib/types/ui/table.d.ts +20 -0
- package/lib/types/ui/table.js +28 -0
- package/lib/types/ui/tabs.d.ts +16 -0
- package/lib/types/ui/tabs.js +23 -0
- package/lib/types/ui/textarea.d.ts +10 -0
- package/lib/types/ui/textarea.js +6 -0
- package/lib/types/ui/theme.d.ts +30 -0
- package/lib/types/ui/theme.js +33 -0
- package/lib/types/ui/tooltip.d.ts +23 -0
- package/lib/types/ui/tooltip.js +15 -0
- package/lib/types/ui/touch-select.d.ts +29 -0
- package/lib/types/ui/touch-select.js +37 -0
- package/lib/types/ui/utils.d.ts +13 -0
- package/lib/types/ui/utils.js +16 -0
- package/lib/types/variables.d.ts +168 -0
- package/lib/types/variables.js +282 -0
- package/lib/types/workflows.d.ts +98 -0
- package/lib/types/workflows.js +95 -0
- package/lib/ui/index.js +1101 -0
- package/lib/ui/theme.css +271 -0
- package/lib/variables.js +260 -0
- package/lib/workflows.js +85 -0
- package/package.json +138 -3
- package/src/ui/theme.css +271 -0
- package/templates/ui-extension/README.md +17 -0
- package/templates/ui-view/README.md +17 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The 插件 page — plugin management and discovery fused into one surface. The
|
|
4
|
+
* top row is a single tri-form input: a local path or an http(s) address
|
|
5
|
+
* inspects that zip (a plugin, or an offline combo package with `recipe.yml`
|
|
6
|
+
* at its root; both preview → confirm → install), a dropped file does the
|
|
7
|
+
* same, anything else searches every configured plugin feed (merged, rows
|
|
8
|
+
* carry their source). Below, every installed (non-builtin) row lists in
|
|
9
|
+
* one table — 名称/标识 split into columns, the version cell doubling as
|
|
10
|
+
* the update affordance (a muted pill normally; a success-colored pill with
|
|
11
|
+
* a refresh glyph and the feed version when an update is pending — clicking
|
|
12
|
+
* it updates directly, behind a key-change confirmation when the feed entry
|
|
13
|
+
* is signed by a different key than the installed copy). The preview shows
|
|
14
|
+
* the three-state signature verdict per plugin (a combo shows each member's
|
|
15
|
+
* own verdict — the archive itself is unsigned); an unverified member takes
|
|
16
|
+
* one extra explicit risk step before it installs. The feed sources hide
|
|
17
|
+
* behind a small icon button before the input; they default from the app's
|
|
18
|
+
* shipped `defaults.json` until the user writes their own list. Every write
|
|
19
|
+
* lands in `plugins.yml`/`settings.json` through the plugins and settings
|
|
20
|
+
* domains; the files stay the single interface.
|
|
21
|
+
*
|
|
22
|
+
* @module @snap-rail/framework-shadcn/settings-plugins-page
|
|
23
|
+
*/
|
|
24
|
+
// Wire rows for the plugins-domain methods this page calls.
|
|
25
|
+
import '@snap-rail/app-boot/contract';
|
|
26
|
+
import { rpcErrorText, subscribeFrame } from '@snap-rail/connection';
|
|
27
|
+
import { PLUGINS_FEED_URLS_KEY, pluginUpdatesChangedSchema, } from '@snap-rail/app-boot/contract';
|
|
28
|
+
import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogTitle, Input, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "./ui/index.js";
|
|
29
|
+
import { RefreshCw, Rss, Search } from 'lucide-react';
|
|
30
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
31
|
+
/** The preview's signature row: one line, color carries the verdict. */
|
|
32
|
+
function SignatureRow({ signature, directUrl }) {
|
|
33
|
+
if (signature.state === 'verified') {
|
|
34
|
+
return (_jsxs("div", { className: "flex justify-between gap-4", "data-signature-row": "verified", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7B7E\u540D" }), _jsxs("span", { children: ["\u2713 \u5DF2\u9A8C\u8BC1", signature.publisher !== undefined ? ` · ${signature.publisher}` : ` · ${signature.keyId}`] })] }));
|
|
35
|
+
}
|
|
36
|
+
const detail = signature.keyId !== undefined
|
|
37
|
+
? `未验证 · keyId ${signature.keyId}(未登记)`
|
|
38
|
+
: '未签名';
|
|
39
|
+
return (_jsxs("div", { className: "flex justify-between gap-4 text-warning", "data-signature-row": "unverified", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7B7E\u540D" }), _jsxs("span", { children: ["\u26A0 ", detail, directUrl ? ' · 直链无完整性校验' : ''] })] }));
|
|
40
|
+
}
|
|
41
|
+
/** Kind → display label; shared with the search results' rows. */
|
|
42
|
+
const KIND_LABELS = {
|
|
43
|
+
suite: '业务套件',
|
|
44
|
+
driver: '通讯驱动',
|
|
45
|
+
plugin: '插件',
|
|
46
|
+
};
|
|
47
|
+
/** The section a package groups under: a declared category wins whatever the
|
|
48
|
+
* source (dev's overlay mounts installable packages as protected builtin
|
|
49
|
+
* rows, and they keep their author markers); without one, installed and
|
|
50
|
+
* preset-carried rows fall into 其他 and the protected shipped set — which
|
|
51
|
+
* declares no category — pins into the single 内置 section. */
|
|
52
|
+
const sectionOf = (row) => row.category ?? (row.source === 'pool' || row.source === 'preset' ? '其他' : '内置');
|
|
53
|
+
/** The drag-and-drop bridge: turn a dropped `File` back into its host-side
|
|
54
|
+
* path through the preload's `zipPathOf` (`webUtils.getPathForFile`). Null
|
|
55
|
+
* when the carrier offers no bridge (test hosts, non-Electron shells). */
|
|
56
|
+
function droppedZipPath(file) {
|
|
57
|
+
const zipPathOf = globalThis.snapRail?.zipPathOf;
|
|
58
|
+
if (typeof zipPathOf !== 'function')
|
|
59
|
+
return null;
|
|
60
|
+
const path = zipPathOf(file);
|
|
61
|
+
return typeof path === 'string' && path !== '' ? path : null;
|
|
62
|
+
}
|
|
63
|
+
/** Does the input name an http(s) address? */
|
|
64
|
+
const isHttpUrl = (text) => /^https?:\/\//i.test(text);
|
|
65
|
+
/** Does the input look like a filesystem path (drive letter, UNC, posix
|
|
66
|
+
* root, or a .zip name with a separator in it)? */
|
|
67
|
+
const looksLikePath = (text) => /^[a-zA-Z]:[\\/]/.test(text) || text.startsWith('\\\\') || text.startsWith('/')
|
|
68
|
+
|| (/\.zip$/i.test(text) && /[/\\]/.test(text));
|
|
69
|
+
/** The version cell: a muted pill normally; when a feed update is pending,
|
|
70
|
+
* a success-colored pill carrying the refresh glyph and the feed version —
|
|
71
|
+
* clicking it updates directly (a key change confirmation intercepts first
|
|
72
|
+
* when the feed entry is signed by another key). */
|
|
73
|
+
function VersionPill(props) {
|
|
74
|
+
const { update } = props;
|
|
75
|
+
if (update !== undefined) {
|
|
76
|
+
return (_jsxs(Button, { size: "sm", variant: "outline", "data-update-version": update.name, disabled: props.busy, title: update.keyChanged === true
|
|
77
|
+
? `当前 ${update.installedVersion} → ${update.feedVersion},发布者密钥已变更,点击确认更新`
|
|
78
|
+
: `当前 ${update.installedVersion} → ${update.feedVersion},点击更新`, className: "h-7 bg-success/10 px-2 font-mono text-xs text-success", onClick: () => props.onUpdate(update), children: [_jsx(RefreshCw, { className: "h-4 w-4" }), props.busy ? '更新中…' : update.feedVersion] }));
|
|
79
|
+
}
|
|
80
|
+
return (_jsx("span", { className: "inline-flex h-7 items-center rounded-md border border-border bg-muted px-2 font-mono text-xs text-muted-foreground", children: props.version ?? '—' }));
|
|
81
|
+
}
|
|
82
|
+
/** The plugins page. */
|
|
83
|
+
export function PluginsPage({ ctx }) {
|
|
84
|
+
const [rows, setRows] = useState([]);
|
|
85
|
+
const [failed, setFailed] = useState(false);
|
|
86
|
+
const [rendererNames, setRendererNames] = useState([]);
|
|
87
|
+
/** Rows changed since page open — the restart-required surface. */
|
|
88
|
+
const [touched, setTouched] = useState(new Set());
|
|
89
|
+
const [restartPrompt, setRestartPrompt] = useState(false);
|
|
90
|
+
const [uninstall, setUninstall] = useState({ phase: 'idle' });
|
|
91
|
+
const [installPhase, setInstallPhase] = useState({ phase: 'idle' });
|
|
92
|
+
// The tri-form input and its one non-address outcome: a feed search.
|
|
93
|
+
const [query, setQuery] = useState('');
|
|
94
|
+
const [searchResults, setSearchResults] = useState(undefined);
|
|
95
|
+
const [searching, setSearching] = useState(false);
|
|
96
|
+
const [actionError, setActionError] = useState(undefined);
|
|
97
|
+
/** The name whose update click is in flight (version pill busy state). */
|
|
98
|
+
const [updatingName, setUpdatingName] = useState(undefined);
|
|
99
|
+
/** The pending update whose click hit a key change — confirmed before the
|
|
100
|
+
* install fires. */
|
|
101
|
+
const [keyChange, setKeyChange] = useState(undefined);
|
|
102
|
+
// The feed-source dialog: the draft list, its input, and its own error line.
|
|
103
|
+
const [feedDialogOpen, setFeedDialogOpen] = useState(false);
|
|
104
|
+
const [feedUrls, setFeedUrls] = useState([]);
|
|
105
|
+
const [feedDraft, setFeedDraft] = useState('');
|
|
106
|
+
const [feedError, setFeedError] = useState(undefined);
|
|
107
|
+
/** Pending updates by package name (drives the version pills). */
|
|
108
|
+
const [updates, setUpdates] = useState(new Map());
|
|
109
|
+
const [dragging, setDragging] = useState(false);
|
|
110
|
+
const link = ctx.get('client').link;
|
|
111
|
+
const reload = useCallback(() => {
|
|
112
|
+
void link.call('plugins.list', {}).then(result => {
|
|
113
|
+
if (result.ok) {
|
|
114
|
+
setRows(result.value.plugins.map(info => ({
|
|
115
|
+
name: info.name, source: info.source, enabled: info.enabled,
|
|
116
|
+
packageName: info.packageName,
|
|
117
|
+
...info.kind !== undefined ? { kind: info.kind } : {},
|
|
118
|
+
...info.version !== undefined ? { version: info.version } : {},
|
|
119
|
+
...info.title !== undefined ? { title: info.title } : {},
|
|
120
|
+
...info.category !== undefined ? { category: info.category } : {},
|
|
121
|
+
...info.clientOnly ? { clientOnly: true } : {},
|
|
122
|
+
})));
|
|
123
|
+
}
|
|
124
|
+
else
|
|
125
|
+
setFailed(true);
|
|
126
|
+
}).catch(() => setFailed(true));
|
|
127
|
+
// Rides every reload: installs and uninstalls change which pool faces
|
|
128
|
+
// exist, and the restart prompt keys off this list.
|
|
129
|
+
void link.call('client-config.list', {}).then(result => {
|
|
130
|
+
if (result.ok)
|
|
131
|
+
setRendererNames(result.value.rows.map(row => row.name));
|
|
132
|
+
}).catch(() => undefined);
|
|
133
|
+
// The pending-update set keys the version pills.
|
|
134
|
+
void link.call('plugins.updates-check', {}).then(result => {
|
|
135
|
+
if (result.ok) {
|
|
136
|
+
setUpdates(new Map(result.value.updates.map(update => [update.name, update])));
|
|
137
|
+
}
|
|
138
|
+
}).catch(() => undefined);
|
|
139
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
|
+
}, [link]);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
reload();
|
|
143
|
+
// The user's feed list rides settings.json — read once for the dialog
|
|
144
|
+
// (an empty read means the factory default list is in force).
|
|
145
|
+
void link.call('settings.get', { key: PLUGINS_FEED_URLS_KEY }).then(result => {
|
|
146
|
+
if (result.ok && Array.isArray(result.value.value)) {
|
|
147
|
+
setFeedUrls(result.value.value.filter((entry) => typeof entry === 'string'));
|
|
148
|
+
}
|
|
149
|
+
}).catch(() => undefined);
|
|
150
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
|
+
}, [reload]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
// The host sweep announces pending updates on the frame; the page folds
|
|
154
|
+
// them straight into the version pills.
|
|
155
|
+
const detach = subscribeFrame(link, 'plugins/updates-changed', pluginUpdatesChangedSchema, change => {
|
|
156
|
+
setUpdates(new Map(change.updates.map(update => [update.name, update])));
|
|
157
|
+
});
|
|
158
|
+
return () => { detach(); };
|
|
159
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
160
|
+
}, [link]);
|
|
161
|
+
const toggle = (name, enabled) => {
|
|
162
|
+
setRows(current => current.map(row => row.name === name ? { ...row, enabled } : row));
|
|
163
|
+
if (rendererNames.includes(name)) {
|
|
164
|
+
setTouched(current => new Set([...current, name]));
|
|
165
|
+
setRestartPrompt(true);
|
|
166
|
+
}
|
|
167
|
+
void link.call('plugins.set-enabled', { name, enabled })
|
|
168
|
+
.then(result => {
|
|
169
|
+
if (!result.ok) {
|
|
170
|
+
setRows(current => current.map(row => row.name === name ? { ...row, enabled: !enabled } : row));
|
|
171
|
+
setTouched(current => { const next = new Set(current); next.delete(name); return next; });
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
.catch(() => {
|
|
175
|
+
setRows(current => current.map(row => row.name === name ? { ...row, enabled: !enabled } : row));
|
|
176
|
+
setTouched(current => { const next = new Set(current); next.delete(name); return next; });
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
/** The uninstall affordance opens a confirm first — the package and its
|
|
180
|
+
* data are deleted for real. */
|
|
181
|
+
const requestUninstall = (pkg) => {
|
|
182
|
+
setUninstall({ phase: 'confirm', pkg });
|
|
183
|
+
};
|
|
184
|
+
const runUninstall = (pkg) => {
|
|
185
|
+
void link.call('plugins.uninstall', { name: pkg }).then(result => {
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
setUninstall({ phase: 'error', message: rpcErrorText(result.error) });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
setUninstall({ phase: 'idle' });
|
|
191
|
+
reload();
|
|
192
|
+
// The renderer may still carry the uninstalled face until it reboots.
|
|
193
|
+
if (rendererNames.includes(pkg)) {
|
|
194
|
+
setTouched(current => new Set([...current, pkg]));
|
|
195
|
+
setRestartPrompt(true);
|
|
196
|
+
}
|
|
197
|
+
}).catch(cause => setUninstall({ phase: 'error', message: String(cause) }));
|
|
198
|
+
};
|
|
199
|
+
const markRestartIfNeeded = (name, updated) => {
|
|
200
|
+
if (updated && rendererNames.includes(name)) {
|
|
201
|
+
setTouched(current => new Set([...current, name]));
|
|
202
|
+
setRestartPrompt(true);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/** Inspect a zip the host can already reach (a local path, a picked file,
|
|
206
|
+
* or a dropped one). */
|
|
207
|
+
const inspectPath = (zipPath) => {
|
|
208
|
+
setInstallPhase({ phase: 'idle' });
|
|
209
|
+
void link.call('plugins.inspect', { zipPath }).then(result => {
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
setInstallPhase({ phase: 'error', message: rpcErrorText(result.error) });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
setInstallPhase({ phase: 'confirm', zipPath, archive: result.value.archive, viaUrl: false, risk: false });
|
|
215
|
+
}).catch(cause => setInstallPhase({ phase: 'error', message: String(cause) }));
|
|
216
|
+
};
|
|
217
|
+
/** Download and inspect a direct http(s) address — the host owns the
|
|
218
|
+
* temp file; its path feeds the confirm → install legs. */
|
|
219
|
+
const inspectFromUrl = (url, source) => {
|
|
220
|
+
setInstallPhase({ phase: 'idle' });
|
|
221
|
+
void link.call('plugins.inspect-from-url', { url }).then(result => {
|
|
222
|
+
if (!result.ok) {
|
|
223
|
+
setInstallPhase({ phase: 'error', message: rpcErrorText(result.error) });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
setInstallPhase({
|
|
227
|
+
phase: 'confirm',
|
|
228
|
+
zipPath: result.value.zipPath,
|
|
229
|
+
archive: result.value.archive,
|
|
230
|
+
viaUrl: true,
|
|
231
|
+
...(source !== undefined ? { source } : {}),
|
|
232
|
+
risk: false,
|
|
233
|
+
});
|
|
234
|
+
}).catch(cause => setInstallPhase({ phase: 'error', message: String(cause) }));
|
|
235
|
+
};
|
|
236
|
+
const runInstall = (zipPath, archive) => {
|
|
237
|
+
setInstallPhase({ phase: 'installing' });
|
|
238
|
+
if (archive.kind === 'combo') {
|
|
239
|
+
void link.call('plugins.install-combo', { zipPath }).then(result => {
|
|
240
|
+
if (!result.ok) {
|
|
241
|
+
setInstallPhase({ phase: 'error', message: rpcErrorText(result.error) });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
setInstallPhase({ phase: 'idle' });
|
|
245
|
+
reload();
|
|
246
|
+
// A combo lands a preset that mounts its rows immediately; a renderer
|
|
247
|
+
// face may wait for the next boot, same surface as an in-place update.
|
|
248
|
+
if (archive.combo.hasClient)
|
|
249
|
+
setRestartPrompt(true);
|
|
250
|
+
}).catch(cause => setInstallPhase({ phase: 'error', message: String(cause) }));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
void link.call('plugins.install', { zipPath }).then(result => {
|
|
254
|
+
if (!result.ok) {
|
|
255
|
+
setInstallPhase({ phase: 'error', message: rpcErrorText(result.error) });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
setInstallPhase({ phase: 'idle' });
|
|
259
|
+
reload();
|
|
260
|
+
// A fresh install lands disabled (no restart prompt). An update swaps
|
|
261
|
+
// the package under a possibly-running face, so the renderer may carry
|
|
262
|
+
// the old bundle until it reboots — same surface as the uninstall path.
|
|
263
|
+
markRestartIfNeeded(result.value.installed.name, result.value.installed.updated);
|
|
264
|
+
}).catch(cause => setInstallPhase({ phase: 'error', message: String(cause) }));
|
|
265
|
+
};
|
|
266
|
+
const pickZip = () => {
|
|
267
|
+
void link.call('window.pick-zip', { title: '选择插件包或组合包' }).then(result => {
|
|
268
|
+
if (!result.ok || result.value === null)
|
|
269
|
+
return;
|
|
270
|
+
inspectPath(result.value);
|
|
271
|
+
}).catch(() => undefined);
|
|
272
|
+
};
|
|
273
|
+
/** The tri-form input: address/path → inspect, anything else → feed search. */
|
|
274
|
+
const submitQuery = () => {
|
|
275
|
+
const text = query.trim();
|
|
276
|
+
if (text === '')
|
|
277
|
+
return;
|
|
278
|
+
setActionError(undefined);
|
|
279
|
+
if (isHttpUrl(text)) {
|
|
280
|
+
inspectFromUrl(text);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (looksLikePath(text)) {
|
|
284
|
+
inspectPath(text);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
runSearch(text);
|
|
288
|
+
};
|
|
289
|
+
const runSearch = (text) => {
|
|
290
|
+
setSearching(true);
|
|
291
|
+
setSearchResults(undefined);
|
|
292
|
+
void link.call('plugins.remote-list', {}).then(result => {
|
|
293
|
+
setSearching(false);
|
|
294
|
+
if (!result.ok) {
|
|
295
|
+
setActionError(rpcErrorText(result.error));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const needle = text.toLowerCase();
|
|
299
|
+
setSearchResults(result.value.plugins.filter(item => item.name.toLowerCase().includes(needle)
|
|
300
|
+
|| (item.title ?? '').toLowerCase().includes(needle)
|
|
301
|
+
|| (item.description ?? '').toLowerCase().includes(needle)));
|
|
302
|
+
}).catch(cause => {
|
|
303
|
+
setSearching(false);
|
|
304
|
+
setActionError(String(cause));
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
/** A search-result entry that is not installed yet: preview first. The
|
|
308
|
+
* entry's own coordinates decide where the zip comes from — a federation
|
|
309
|
+
* row downloads from the author's release, a same-directory row from its
|
|
310
|
+
* feed base. */
|
|
311
|
+
const previewRemote = (item) => {
|
|
312
|
+
if (item.url !== undefined) {
|
|
313
|
+
inspectFromUrl(item.url, item.source);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (item.feedUrl === undefined || item.file === undefined)
|
|
317
|
+
return;
|
|
318
|
+
const base = item.feedUrl.endsWith('/') ? item.feedUrl : `${item.feedUrl}/`;
|
|
319
|
+
inspectFromUrl(`${base}${item.file}`, item.source);
|
|
320
|
+
};
|
|
321
|
+
/** The version pill's click: a plain update installs directly; a key
|
|
322
|
+
* change confirmation intercepts first. */
|
|
323
|
+
const requestUpdate = (update) => {
|
|
324
|
+
if (update.keyChanged === true) {
|
|
325
|
+
setKeyChange(update);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
updateNow(update.name);
|
|
329
|
+
};
|
|
330
|
+
/** The update itself: the feed's entry, no preview. */
|
|
331
|
+
const updateNow = (name) => {
|
|
332
|
+
setKeyChange(undefined);
|
|
333
|
+
setUpdatingName(name);
|
|
334
|
+
setActionError(undefined);
|
|
335
|
+
void link.call('plugins.remote-install', { name }).then(result => {
|
|
336
|
+
setUpdatingName(undefined);
|
|
337
|
+
if (!result.ok) {
|
|
338
|
+
setActionError(rpcErrorText(result.error));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
reload();
|
|
342
|
+
markRestartIfNeeded(result.value.installed.name, result.value.installed.updated);
|
|
343
|
+
}).catch(() => setUpdatingName(undefined));
|
|
344
|
+
};
|
|
345
|
+
const saveFeedUrls = () => {
|
|
346
|
+
for (const url of feedUrls) {
|
|
347
|
+
try {
|
|
348
|
+
const parsed = new URL(url);
|
|
349
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
350
|
+
throw new Error('protocol');
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
setFeedError('插件源地址需要以 http(s) 开头');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
setFeedError(undefined);
|
|
358
|
+
void link.call('settings.set', { key: PLUGINS_FEED_URLS_KEY, value: feedUrls }).then(result => {
|
|
359
|
+
if (!result.ok) {
|
|
360
|
+
setFeedError(rpcErrorText(result.error));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
setFeedDialogOpen(false);
|
|
364
|
+
}).catch(cause => setFeedError(String(cause)));
|
|
365
|
+
};
|
|
366
|
+
const appendFeedUrl = () => {
|
|
367
|
+
const value = feedDraft.trim();
|
|
368
|
+
if (value === '')
|
|
369
|
+
return;
|
|
370
|
+
try {
|
|
371
|
+
const parsed = new URL(value);
|
|
372
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
373
|
+
throw new Error('protocol');
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
setFeedError('请填写 http(s) 开头的插件源地址');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
setFeedError(undefined);
|
|
380
|
+
setFeedDraft('');
|
|
381
|
+
setFeedUrls(current => current.includes(value) ? current : [...current, value]);
|
|
382
|
+
};
|
|
383
|
+
const onDrop = (event) => {
|
|
384
|
+
event.preventDefault();
|
|
385
|
+
setDragging(false);
|
|
386
|
+
const file = event.dataTransfer.files[0];
|
|
387
|
+
if (file === undefined)
|
|
388
|
+
return;
|
|
389
|
+
if (!file.name.toLowerCase().endsWith('.zip')) {
|
|
390
|
+
setActionError('只支持 .zip 插件包或组合包');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const zipPath = droppedZipPath(file);
|
|
394
|
+
if (zipPath === null) {
|
|
395
|
+
setActionError('当前环境不支持拖拽安装(缺少路径桥)');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
inspectPath(zipPath);
|
|
399
|
+
};
|
|
400
|
+
const relaunch = () => {
|
|
401
|
+
void link.call('window.relaunch', {}).catch(() => undefined);
|
|
402
|
+
};
|
|
403
|
+
if (failed)
|
|
404
|
+
return _jsx("div", { className: "text-sm text-muted-foreground", children: "\u63D2\u4EF6\u5217\u8868\u8BFB\u53D6\u5931\u8D25\u3002" });
|
|
405
|
+
const needsRestart = (name) => touched.has(name);
|
|
406
|
+
// Built-ins never list: the shipped set is read-only by design (every
|
|
407
|
+
// operation on it is refused), so listing un-actionable rows is noise.
|
|
408
|
+
// Dev's overlay mounts installable packages as protected builtin rows —
|
|
409
|
+
// they hide here too; the layer files remain the development levers.
|
|
410
|
+
const listed = rows.filter(row => row.source !== 'builtin');
|
|
411
|
+
const confirmed = installPhase.phase === 'confirm' ? installPhase : undefined;
|
|
412
|
+
const confirmedPlugin = confirmed?.archive.kind === 'plugin' ? confirmed.archive.plugin : undefined;
|
|
413
|
+
const confirmedCombo = confirmed?.archive.kind === 'combo' ? confirmed.archive.combo : undefined;
|
|
414
|
+
return (_jsxs("div", { "data-region": "plugins-page", className: "flex min-h-0 flex-1 flex-col gap-4", onDragOver: event => {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
setDragging(true);
|
|
417
|
+
}, onDragLeave: () => setDragging(false), onDrop: event => onDrop(event), children: [dragging && (_jsx("div", { className: "pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-background", children: _jsx("div", { className: "rounded-lg border-2 border-dashed border-primary px-8 py-10 text-base text-primary", children: "\u677E\u5F00\u4EE5\u5B89\u88C5\u63D2\u4EF6\u5305\u6216\u7EC4\u5408\u5305\uFF08.zip\uFF09" }) })), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "ghost", size: "icon", "aria-label": "\u914D\u7F6E\u63D2\u4EF6\u6E90", "data-feed-config": true, className: "h-9 w-9 shrink-0 text-muted-foreground", onClick: () => setFeedDialogOpen(true), children: _jsx(Rss, { className: "h-4 w-4" }) }), _jsx(Input, { value: query, placeholder: "\u7C98\u8D34\u672C\u5730/\u7F51\u7EDC\u5730\u5740\u5B89\u88C5\uFF0C\u6216\u8F93\u5165\u540D\u79F0\u641C\u7D22\u2026", "data-plugin-query": true, className: "flex-1", onChange: event => setQuery(event.target.value), onKeyDown: event => { if (event.key === 'Enter')
|
|
418
|
+
submitQuery(); } }), _jsxs(Button, { variant: "outline", "data-plugin-search": true, disabled: searching, onClick: submitQuery, children: [_jsx(Search, { className: "h-4 w-4" }), searching ? '搜索中…' : '搜索'] }), _jsx(Button, { variant: "outline", "data-install-plugin": true, onClick: pickZip, children: "\u9009\u62E9\u6587\u4EF6" })] }), actionError !== undefined && (_jsx("p", { "data-plugin-action-error": true, className: "text-sm text-destructive", children: actionError })), searchResults !== undefined && (_jsxs("div", { "data-region": "plugin-search-results", className: "flex flex-col gap-2 rounded-md border border-border p-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("span", { className: "text-sm font-medium", children: ["\u641C\u7D22\u201C", query.trim(), "\u201D\uFF08", searchResults.length, "\uFF09"] }), _jsx(Button, { variant: "ghost", size: "sm", "aria-label": "\u5173\u95ED\u641C\u7D22\u7ED3\u679C", onClick: () => setSearchResults(undefined), children: "\u2715" })] }), searchResults.length === 0 && (_jsx("p", { className: "py-2 text-center text-sm text-muted-foreground", children: "\u6CA1\u6709\u5339\u914D\u7684\u63D2\u4EF6\uFF08\u63D2\u4EF6\u6E90\u4E2D\u6CA1\u6709\u8BE5\u540D\u79F0\u7684\u6761\u76EE\uFF09\u3002" })), searchResults.map(item => {
|
|
419
|
+
const pillUpdate = item.action === 'update' && item.installed !== undefined
|
|
420
|
+
? {
|
|
421
|
+
name: item.name,
|
|
422
|
+
installedVersion: item.installed.version,
|
|
423
|
+
feedVersion: item.version,
|
|
424
|
+
...(item.source !== undefined ? { title: item.source } : {}),
|
|
425
|
+
}
|
|
426
|
+
: undefined;
|
|
427
|
+
return (_jsxs("div", { "data-search-row": item.name, className: "flex items-center justify-between gap-3 rounded-md border border-border px-4 py-3", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate text-sm font-medium", children: item.title ?? item.name }), item.kind !== undefined && _jsx(Badge, { variant: "secondary", children: KIND_LABELS[item.kind] ?? item.kind }), item.source !== undefined && _jsx(Badge, { variant: "outline", "data-search-source": item.name, children: item.source })] }), _jsxs("span", { className: "truncate font-mono text-xs text-muted-foreground", children: [item.name, " \u00B7 ", item.version] }), item.description !== undefined && item.description !== '' && (_jsx("span", { className: "truncate text-xs text-muted-foreground", children: item.description }))] }), item.action === 'install'
|
|
428
|
+
? _jsx(Button, { size: "sm", "data-search-install": item.name, onClick: () => previewRemote(item), children: "\u5B89\u88C5" })
|
|
429
|
+
: pillUpdate !== undefined
|
|
430
|
+
? _jsx(VersionPill, { update: pillUpdate, busy: updatingName === item.name, onUpdate: requestUpdate })
|
|
431
|
+
: _jsx(Badge, { variant: "secondary", children: item.action === 'current' ? '已是最新' : '本地更新' })] }, item.name));
|
|
432
|
+
})] })), _jsxs("div", { className: "shrink-0 text-sm font-medium text-muted-foreground", children: ["\u5DF2\u5B89\u88C5\u63D2\u4EF6\uFF08", listed.length, "\uFF09"] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto rounded-md border border-border", children: _jsxs(Table, { "data-region": "plugins-table", children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-14", children: "\u542F\u7528" }), _jsx(TableHead, { children: "\u540D\u79F0" }), _jsx(TableHead, { children: "\u6807\u8BC6" }), _jsx(TableHead, { className: "w-24", children: "\u5206\u7C7B" }), _jsx(TableHead, { className: "w-36", children: "\u7248\u672C" }), _jsx(TableHead, { className: "w-24", children: "\u64CD\u4F5C" })] }) }), _jsxs(TableBody, { children: [listed.map(row => {
|
|
433
|
+
const label = row.title ?? row.name;
|
|
434
|
+
return (_jsxs(TableRow, { "data-plugin-row": row.name, children: [_jsx(TableCell, { children: _jsx(Switch, { "aria-label": `启用 ${label}`, checked: row.enabled, onCheckedChange: checked => toggle(row.name, checked) }) }), _jsx(TableCell, { children: _jsxs("span", { className: "flex items-center gap-2 truncate text-sm font-medium", children: [label, needsRestart(row.name) && _jsx(Badge, { variant: "destructive", children: "\u5F85\u91CD\u542F" })] }) }), _jsx(TableCell, { className: "font-mono text-xs text-muted-foreground", children: row.name }), _jsx(TableCell, { className: "text-sm text-muted-foreground", children: row.category ?? '其他' }), _jsx(TableCell, { children: _jsx(VersionPill, { version: row.version, update: updates.get(row.packageName), busy: updatingName === row.packageName, onUpdate: requestUpdate }) }), _jsx(TableCell, { children: row.source === 'pool'
|
|
435
|
+
? (_jsx(Button, { "data-uninstall-plugin": row.packageName, variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", onClick: () => requestUninstall(row.packageName), children: "\u5378\u8F7D" }))
|
|
436
|
+
: _jsx("span", { className: "text-xs text-muted-foreground", children: "\u2014" }) })] }, row.name));
|
|
437
|
+
}), listed.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 6, className: "py-8 text-center text-sm text-muted-foreground", children: "\u6682\u65E0\u5DF2\u5B89\u88C5\u63D2\u4EF6 \u2014\u2014 \u7C98\u8D34\u5730\u5740\u3001\u62D6\u5165\u63D2\u4EF6/\u7EC4\u5408 .zip\uFF0C\u6216\u8F93\u5165\u540D\u79F0\u4ECE\u63D2\u4EF6\u6E90\u641C\u7D22\u5B89\u88C5\u3002" }) }))] })] }) }), restartPrompt && (_jsx(Dialog, { open: true, onOpenChange: next => { if (!next)
|
|
438
|
+
setRestartPrompt(false); }, children: _jsxs(DialogContent, { "aria-describedby": undefined, className: "max-w-sm", children: [_jsx(DialogTitle, { className: "text-base font-medium", children: "\u53D8\u66F4\u5F85\u91CD\u542F\u751F\u6548" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "\u4E1A\u52A1\u5957\u4EF6\u4E0E\u9875\u9762\u7C7B\u63D2\u4EF6\u7684\u542F\u7528\u53D8\u66F4\u5C06\u5728\u4E0B\u6B21\u542F\u52A8\u65F6\u751F\u6548\uFF1B\u63D2\u4EF6\u66F4\u65B0\u6216\u5378\u8F7D\u540E\u7684\u754C\u9762\u6B8B\u7559\u4E5F\u4F1A\u968F\u4E4B\u6D88\u5931\u3002" }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setRestartPrompt(false), children: "\u7A0D\u540E" }), _jsx(Button, { "data-restart-now": true, onClick: () => relaunch(), children: "\u7ACB\u5373\u91CD\u542F" })] })] }) })), confirmed !== undefined && (_jsx(Dialog, { open: true, onOpenChange: next => { if (!next)
|
|
439
|
+
setInstallPhase({ phase: 'idle' }); }, children: _jsx(DialogContent, { "aria-describedby": undefined, className: "max-w-sm", children: confirmed.risk ? (_jsxs(_Fragment, { children: [_jsx(DialogTitle, { className: "text-base font-medium", "data-install-risk": true, children: confirmedCombo !== undefined ? '⚠ 此组合含未经发布者验证的插件' : '⚠ 此插件未经发布者验证' }), _jsxs("div", { className: "grid gap-2 text-sm", children: [confirmedPlugin !== undefined && (_jsxs("div", { className: "rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-sm", children: [_jsx("p", { "data-risk-package": true, className: "font-mono", children: confirmedPlugin.name }), _jsx("p", { className: "mt-1 text-muted-foreground", children: confirmedPlugin.signature.keyId !== undefined
|
|
440
|
+
? `签名 keyId ${confirmedPlugin.signature.keyId} 不在本机信任名单中,无法验证发布者。`
|
|
441
|
+
: '这个包没有携带任何签名,也没有任何完整性校验。' })] })), confirmedCombo !== undefined && (_jsxs("div", { className: "rounded-md border border-warning/30 bg-warning/10 px-3 py-2 text-sm", children: [_jsxs("p", { "data-risk-package": true, className: "font-mono", children: [confirmedCombo.title, " \u00B7 ", confirmedCombo.id] }), _jsx("p", { className: "mt-1 text-muted-foreground", children: "\u7EC4\u5408\u5305\u672C\u8EAB\u6CA1\u6709\u7B7E\u540D\uFF0C\u4EE5\u4E0B\u6210\u5458\u65E0\u6CD5\u9A8C\u8BC1\u53D1\u5E03\u8005\uFF1A" }), _jsx("ul", { className: "mt-1 grid gap-1", children: confirmedCombo.plugins
|
|
442
|
+
.filter(plugin => plugin.signature.state === 'unverified')
|
|
443
|
+
.map(plugin => (_jsxs("li", { className: "font-mono text-xs", children: [plugin.name, " \u00B7 ", plugin.signature.keyId ?? '未签名'] }, plugin.name))) })] })), _jsx("p", { className: "text-muted-foreground", children: "\u00B7 \u8BF7\u786E\u8BA4\u63D2\u4EF6\u6765\u6E90\u53EF\u4FE1\u540E\u518D\u7EE7\u7EED\uFF1B\u5B89\u88C5\u540E\u53EF\u5728\u672C\u673A\u4FE1\u4EFB\u6587\u4EF6\u767B\u8BB0\u5176\u516C\u94A5\u3002" })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallPhase({ ...confirmed, risk: false }), children: "\u8FD4\u56DE" }), _jsx(Button, { "data-confirm-risk-install": true, onClick: () => runInstall(confirmed.zipPath, confirmed.archive), children: "\u6211\u77E5\u6653\u98CE\u9669\uFF0C\u4ECD\u8981\u5B89\u88C5" })] })] })) : confirmedCombo !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DialogTitle, { className: "text-base font-medium", children: "\u5B89\u88C5\u7EC4\u5408\u5957\u4EF6" }), _jsxs("div", { className: "grid gap-2 text-sm", children: [_jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7EC4\u5408\u540D" }), _jsx("span", { className: "truncate", children: confirmedCombo.title })] }), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7EC4\u5408 ID" }), _jsx("span", { className: "truncate font-mono", children: confirmedCombo.id })] }), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7248\u672C" }), _jsx("span", { className: "font-mono", children: confirmedCombo.version })] }), confirmedCombo.description !== undefined && (_jsx("p", { className: "text-xs text-muted-foreground", children: confirmedCombo.description })), confirmed.source !== undefined && (_jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u6765\u6E90" }), _jsx("span", { children: confirmed.source })] })), _jsx("div", { className: "grid max-h-48 gap-1 overflow-auto rounded-md border border-border px-3 py-2", children: confirmedCombo.plugins.map(plugin => {
|
|
444
|
+
const action = plugin.action === 'install' ? '安装' : plugin.action === 'update' ? '更新' : '保留';
|
|
445
|
+
const unverified = plugin.signature.state === 'unverified';
|
|
446
|
+
return (_jsxs("div", { "data-combo-member": plugin.name, className: "flex items-baseline justify-between gap-3 text-xs", children: [_jsx("span", { className: "truncate", children: plugin.title ?? plugin.name }), _jsxs("span", { className: `shrink-0 font-mono ${unverified ? 'text-warning' : 'text-muted-foreground'}`, children: [plugin.version, " \u00B7 ", action, unverified ? ' · ⚠ 未验证' : ''] })] }, plugin.name));
|
|
447
|
+
}) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u7EC4\u5408\u5305\u672C\u8EAB\u4E0D\u7B7E\u540D\uFF1B\u6BCF\u4E2A\u6210\u5458\u90FD\u7ECF\u4E0E\u5355\u63D2\u4EF6\u76F8\u540C\u7684\u7B7E\u540D\u6821\u9A8C\u3002" }), confirmedCombo.missing.length > 0 && (_jsxs("p", { "data-combo-missing": true, className: "rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: ["\u7F3A\u5C11\u4F9D\u8D56\uFF1A", confirmedCombo.missing.join('、'), "\u2014\u2014\u7EC4\u5408\u4F1A\u7ACB\u5373\u542F\u7528\u5168\u90E8\u884C\uFF0C\u88C5\u9F50\u540E\u518D\u5B89\u88C5\u3002"] })), confirmedCombo.hasClient && (_jsx("p", { className: "text-xs text-muted-foreground", children: "\u7EC4\u5408\u542B\u754C\u9762\u63D2\u4EF6\uFF1B\u88C5\u597D\u540E\u91CD\u542F\u4E00\u6B21\u8BA9\u754C\u9762\u751F\u6548\u3002" }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallPhase({ phase: 'idle' }), children: "\u53D6\u6D88" }), _jsx(Button, { "data-confirm-install-combo": true, disabled: confirmedCombo.missing.length > 0, onClick: () => {
|
|
448
|
+
if (confirmedCombo.plugins.some(plugin => plugin.signature.state === 'unverified')) {
|
|
449
|
+
setInstallPhase({ ...confirmed, risk: true });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
runInstall(confirmed.zipPath, confirmed.archive);
|
|
453
|
+
}, children: "\u5B89\u88C5\u7EC4\u5408" })] })] })) : confirmedPlugin !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DialogTitle, { className: "text-base font-medium", children: confirmedPlugin.action === 'update' ? '更新插件' : '安装插件' }), _jsxs("div", { className: "grid gap-2 text-sm", children: [confirmedPlugin.title !== undefined && (_jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u540D\u79F0" }), _jsx("span", { className: "truncate", children: confirmedPlugin.title })] })), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u5305\u540D" }), _jsx("span", { className: "truncate font-mono", children: confirmedPlugin.name })] }), confirmedPlugin.action === 'update' ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u5DF2\u5B89\u88C5\u7248\u672C" }), _jsx("span", { className: "font-mono", children: confirmedPlugin.installed?.version })] }), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u5C06\u66F4\u65B0\u81F3" }), _jsx("span", { className: "font-mono", children: confirmedPlugin.version })] })] })) : (_jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u7248\u672C" }), _jsx("span", { className: "font-mono", children: confirmedPlugin.version })] })), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u5206\u7C7B" }), _jsx("span", { children: confirmedPlugin.category ?? '其他' })] }), confirmed.source !== undefined && (_jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u6765\u6E90" }), _jsx("span", { children: confirmed.source })] })), _jsx(SignatureRow, { signature: confirmedPlugin.signature, directUrl: confirmed.viaUrl }), confirmedPlugin.hasClient && _jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u754C\u9762" }), _jsx("span", { children: "\u542B\u9875\u9762" })] }), _jsxs("div", { className: "flex justify-between gap-4", children: [_jsx("span", { className: "text-muted-foreground", children: "\u6743\u9650" }), _jsx("span", { children: confirmedPlugin.permissions.length === 0 ? '无' : confirmedPlugin.permissions.join('、') })] }), confirmedPlugin.missing.length > 0 && (_jsxs("p", { className: "rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: ["\u5C1A\u672A\u5B89\u88C5\u7684\u4F9D\u8D56\uFF1A", confirmedPlugin.missing.join('、'), "\u2014\u2014\u53EF\u5148\u5B89\u88C5\u672C\u5305\uFF0C\u542F\u7528\u524D\u9700\u88C5\u9F50\u3002"] })), confirmedPlugin.action === 'update' && (_jsx("p", { className: "text-xs text-muted-foreground", children: "\u8986\u76D6\u540E\u4FDD\u7559\u8BE5\u63D2\u4EF6\u7684\u914D\u7F6E\u4E0E\u6570\u636E\uFF1B\u8FD0\u884C\u4E2D\u7684\u754C\u9762\u9700\u91CD\u542F\u540E\u751F\u6548\u3002" })), confirmedPlugin.action === 'blocked' && (_jsxs("p", { "data-install-blocked": true, className: "text-sm text-destructive", children: ["\u5DF2\u5B89\u88C5 v", confirmedPlugin.installed?.version, "\uFF0C\u5B89\u88C5\u5305 v", confirmedPlugin.version, " \u4E0D\u9AD8\u4E8E\u5DF2\u88C5\u7248\u672C\uFF0C\u65E0\u6CD5\u8986\u76D6\uFF08\u5378\u8F7D\u540E\u53EF\u964D\u7EA7\u5B89\u88C5\uFF09\u3002"] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallPhase({ phase: 'idle' }), children: "\u53D6\u6D88" }), _jsx(Button, { "data-confirm-install": true, disabled: confirmedPlugin.action === 'blocked', onClick: () => {
|
|
454
|
+
if (confirmedPlugin.signature.state === 'unverified') {
|
|
455
|
+
setInstallPhase({ ...confirmed, risk: true });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
runInstall(confirmed.zipPath, confirmed.archive);
|
|
459
|
+
}, children: confirmedPlugin.action === 'update' ? '更新' : '安装' })] })] })) : null }) })), keyChange !== undefined && (_jsx(Dialog, { open: true, onOpenChange: next => { if (!next)
|
|
460
|
+
setKeyChange(undefined); }, children: _jsxs(DialogContent, { "aria-describedby": undefined, className: "max-w-sm", children: [_jsx(DialogTitle, { className: "text-base font-medium", children: "\u53D1\u5E03\u8005\u5BC6\u94A5\u5DF2\u53D8\u66F4" }), _jsxs("div", { className: "grid gap-2 text-sm", children: [_jsxs("p", { className: "text-muted-foreground", children: [_jsx("span", { className: "font-mono text-foreground", children: keyChange.name }), ' ', "\u7684\u65B0\u7248\u672C\u7531\u4E0D\u540C\u7684\u5BC6\u94A5\u7B7E\u540D\u3002\u82E5\u53D1\u5E03\u8005\u786E\u5B9E\u8F6E\u6362\u4E86\u5BC6\u94A5\uFF0C\u66F4\u65B0\u662F\u5B89\u5168\u7684\uFF1B\u5426\u5219\u4E0D\u8981\u7EE7\u7EED\u3002"] }), _jsxs("div", { className: "rounded-md border border-warning/30 bg-warning/10 px-3 py-2", children: [_jsxs("p", { children: ["\u5DF2\u5B89\u88C5 v", keyChange.installedVersion, " \u2192 \u6E90\u4E0A v", keyChange.feedVersion] }), _jsx("p", { className: "mt-1 text-muted-foreground", children: "\u8BF7\u6838\u5BF9\u53D1\u5E03\u8005\u516C\u544A\u4E2D\u7684\u65B0 keyId \u540E\u518D\u51B3\u5B9A\u3002" })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setKeyChange(undefined), children: "\u53D6\u6D88" }), _jsx(Button, { "data-confirm-key-change": true, onClick: () => updateNow(keyChange.name), children: "\u4ECD\u8981\u66F4\u65B0" })] })] }) })), installPhase.phase === 'error' && (_jsx(Dialog, { open: true, onOpenChange: next => { if (!next)
|
|
461
|
+
setInstallPhase({ phase: 'idle' }); }, children: _jsxs(DialogContent, { "aria-describedby": undefined, className: "max-w-sm", children: [_jsx(DialogTitle, { className: "text-base font-medium", children: "\u5B89\u88C5\u5931\u8D25" }), _jsx("p", { "data-install-error": true, className: "text-sm text-destructive", children: installPhase.message }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: () => setInstallPhase({ phase: 'idle' }), children: "\u77E5\u9053\u4E86" }) })] }) })), uninstall.phase === 'confirm' && (_jsx(Dialog, { open: true, onOpenChange: next => { if (!next)
|
|
462
|
+
setUninstall({ phase: 'idle' }); }, children: _jsxs(DialogContent, { "aria-describedby": undefined, className: "max-w-sm", children: [_jsx(DialogTitle, { className: "text-base font-medium", children: "\u5378\u8F7D\u63D2\u4EF6" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["\u5C06\u4ECE\u63D2\u4EF6\u6C60\u79FB\u9664 ", _jsx("span", { className: "font-mono text-foreground", children: uninstall.pkg }), "\uFF0C\u5176\u6570\u636E\u4E00\u5E76\u5220\u9664\uFF0C\u4E0D\u53EF\u6062\u590D\u3002"] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setUninstall({ phase: 'idle' }), children: "\u53D6\u6D88" }), _jsx(Button, { variant: "destructive", "data-confirm-uninstall": true, onClick: () => runUninstall(uninstall.pkg), children: "\u5378\u8F7D" })] })] }) })), uninstall.phase === 'error' && (_jsx(Dialog, { open: true, onOpenChange: next => { if (!next)
|
|
463
|
+
setUninstall({ phase: 'idle' }); }, children: _jsxs(DialogContent, { "aria-describedby": undefined, className: "max-w-sm", children: [_jsx(DialogTitle, { className: "text-base font-medium", children: "\u5378\u8F7D\u5931\u8D25" }), _jsx("p", { "data-uninstall-error": true, className: "text-sm text-destructive", children: uninstall.message }), _jsx(DialogFooter, { children: _jsx(Button, { onClick: () => setUninstall({ phase: 'idle' }), children: "\u77E5\u9053\u4E86" }) })] }) })), feedDialogOpen && (_jsx(Dialog, { open: true, onOpenChange: next => { if (!next)
|
|
464
|
+
setFeedDialogOpen(false); }, children: _jsxs(DialogContent, { "aria-describedby": undefined, className: "max-w-md", children: [_jsx(DialogTitle, { className: "text-base font-medium", children: "\u63D2\u4EF6\u6E90" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm text-muted-foreground", htmlFor: "plugin-feed-url", children: "\u641C\u7D22\u4E0E\u5728\u7EBF\u66F4\u65B0\u4F7F\u7528\u7684\u63D2\u4EF6\u6E90\u5217\u8868\uFF1A" }), feedUrls.length === 0 && (_jsx("p", { className: "text-xs text-muted-foreground", children: "\u5C1A\u672A\u6DFB\u52A0\u81EA\u5B9A\u4E49\u6E90\uFF08\u5F53\u524D\u4F7F\u7528\u51FA\u5382\u9ED8\u8BA4\u6E90\uFF09\u3002" })), feedUrls.map((url, index) => (_jsxs("div", { "data-feed-row": url, className: "flex items-center gap-2 rounded-md border border-border px-3 py-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate font-mono text-xs", children: url }), _jsx(Button, { variant: "ghost", size: "sm", "aria-label": `移除 ${url}`, "data-feed-remove": index, onClick: () => setFeedUrls(current => current.filter((_, at) => at !== index)), children: "\u2715" })] }, url))), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { id: "plugin-feed-url", value: feedDraft, placeholder: "https://\u2026\uFF08\u6307\u5411\u542B index.json \u7684\u76EE\u5F55\uFF09", "data-feed-url-input": true, onChange: event => setFeedDraft(event.target.value), onKeyDown: event => { if (event.key === 'Enter')
|
|
465
|
+
appendFeedUrl(); } }), _jsx(Button, { variant: "outline", "data-feed-add": true, onClick: appendFeedUrl, children: "\u6DFB\u52A0\u6E90" })] }), feedError !== undefined && _jsx("p", { className: "text-sm text-destructive", children: feedError }), _jsx("p", { className: "text-xs text-muted-foreground", children: "\u672A\u4FEE\u6539\u65F6\u4F7F\u7528\u51FA\u5382\u9ED8\u8BA4\u6E90\uFF1B\u4FEE\u6539\u540E\u4FDD\u5B58\u5728\u672C\u673A\uFF0C\u4EC5\u5F71\u54CD\u672C\u673A\u3002\u793E\u533A\u63D2\u4EF6\u6E90\u5C31\u662F\u63D2\u4EF6\u4F5C\u8005\u81EA\u5DF1\u4ED3\u5E93\u7684 Release \u5730\u5740\u3002" })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setFeedDialogOpen(false), children: "\u53D6\u6D88" }), _jsx(Button, { "data-feed-save": true, onClick: saveFeedUrls, children: "\u4FDD\u5B58" })] })] }) }))] }));
|
|
466
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 软件更新 settings page: one live snapshot of the update channel plus
|
|
3
|
+
* its two configuration seats. The phase (and progress) arrive as full
|
|
4
|
+
* `update/status` snapshots, so the view is a pure projection — check /
|
|
5
|
+
* download / restart-to-install buttons map one-to-one onto the update
|
|
6
|
+
* domain's methods, and the feed URL / auto-check seats write
|
|
7
|
+
* `settings.json` keys (hot-applied, no restart).
|
|
8
|
+
*
|
|
9
|
+
* @module @snap-rail/framework-shadcn/settings-update-page
|
|
10
|
+
*/
|
|
11
|
+
import '@snap-rail/app-boot/contract';
|
|
12
|
+
import type { Context } from '@snap-rail/cordis';
|
|
13
|
+
import { type ReactNode } from 'react';
|
|
14
|
+
/** The 软件更新 page. */
|
|
15
|
+
export declare function UpdatePage({ ctx }: {
|
|
16
|
+
ctx: Context;
|
|
17
|
+
}): ReactNode;
|