@lapis-notes/workspace 0.1.0 → 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/CHANGELOG.md +26 -0
- package/LICENSE.md +3 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/plugin-management/CommunityPluginsSettingsPage.svelte +152 -73
- package/dist/plugin-management/PluginBrowser.svelte +517 -0
- package/dist/plugin-management/PluginBrowser.svelte.d.ts +17 -0
- package/dist/plugin-management/PluginBrowserCard.svelte +230 -0
- package/dist/plugin-management/PluginBrowserCard.svelte.d.ts +9 -0
- package/dist/plugin-management/PluginBrowserDetail.svelte +1168 -0
- package/dist/plugin-management/PluginBrowserDetail.svelte.d.ts +20 -0
- package/dist/plugin-management/PluginBrowserDirectory.svelte +1069 -0
- package/dist/plugin-management/PluginBrowserDirectory.svelte.d.ts +21 -0
- package/dist/plugin-management/PluginBrowserLanding.svelte +341 -0
- package/dist/plugin-management/PluginBrowserLanding.svelte.d.ts +12 -0
- package/dist/plugin-management/PluginRegistryBadge.svelte +41 -0
- package/dist/plugin-management/PluginRegistryBadge.svelte.d.ts +8 -0
- package/dist/plugin-management/PluginRegistryContentState.svelte +80 -0
- package/dist/plugin-management/PluginRegistryContentState.svelte.d.ts +14 -0
- package/dist/plugin-management/PluginRegistryImage.svelte +64 -0
- package/dist/plugin-management/PluginRegistryImage.svelte.d.ts +9 -0
- package/dist/plugin-management/PluginRegistryMedia.svelte +223 -0
- package/dist/plugin-management/PluginRegistryMedia.svelte.d.ts +8 -0
- package/dist/plugin-management/PluginRegistryMediaViewer.svelte +155 -0
- package/dist/plugin-management/PluginRegistryMediaViewer.svelte.d.ts +13 -0
- package/dist/plugin-management/PluginRegistryRow.svelte +120 -0
- package/dist/plugin-management/PluginRegistryRow.svelte.d.ts +25 -0
- package/dist/plugin-management/PluginRegistrySettingsPage.svelte +1009 -512
- package/dist/plugin-management/PluginRegistryStatus.svelte +33 -0
- package/dist/plugin-management/PluginRegistryStatus.svelte.d.ts +8 -0
- package/dist/plugin-management/PluginRegistryToolbar.svelte +159 -0
- package/dist/plugin-management/PluginRegistryToolbar.svelte.d.ts +21 -0
- package/dist/plugin-management/community-presentation/CommunityChip.svelte +229 -0
- package/dist/plugin-management/community-presentation/CommunityChip.svelte.d.ts +4 -0
- package/dist/plugin-management/community-presentation/CommunityMemberAvatar.svelte +42 -0
- package/dist/plugin-management/community-presentation/CommunityMemberAvatar.svelte.d.ts +13 -0
- package/dist/plugin-management/community-presentation/RegistryMediaGallery.svelte +282 -0
- package/dist/plugin-management/community-presentation/RegistryMediaGallery.svelte.d.ts +8 -0
- package/dist/plugin-management/community-presentation/RegistryMediaViewer.css +179 -0
- package/dist/plugin-management/community-presentation/RegistryMediaViewer.svelte +241 -0
- package/dist/plugin-management/community-presentation/RegistryMediaViewer.svelte.d.ts +14 -0
- package/dist/plugin-management/community-presentation/RegistryPluginChangelog.svelte +216 -0
- package/dist/plugin-management/community-presentation/RegistryPluginChangelog.svelte.d.ts +8 -0
- package/dist/plugin-management/community-presentation/RegistryVerifiedImage.svelte +179 -0
- package/dist/plugin-management/community-presentation/RegistryVerifiedImage.svelte.d.ts +15 -0
- package/dist/plugin-management/community-presentation/avatar-model.d.ts +14 -0
- package/dist/plugin-management/community-presentation/avatar-model.js +21 -0
- package/dist/plugin-management/community-presentation/community-member-avatar.css +137 -0
- package/dist/plugin-management/community-presentation/community-typography.css +30 -0
- package/dist/plugin-management/community-presentation/model-adapter.d.ts +54 -0
- package/dist/plugin-management/community-presentation/model-adapter.js +90 -0
- package/dist/plugin-management/community-presentation/model.d.ts +9 -0
- package/dist/plugin-management/community-presentation/model.js +40 -0
- package/dist/plugin-management/community-presentation/props.d.ts +21 -0
- package/dist/plugin-management/community-presentation/props.js +1 -0
- package/dist/plugin-management/community-presentation/registry-filter-icons.d.ts +3 -0
- package/dist/plugin-management/community-presentation/registry-filter-icons.js +70 -0
- package/dist/plugin-management/community-presentation/registry-markdown.d.ts +1 -0
- package/dist/plugin-management/community-presentation/registry-markdown.js +57 -0
- package/dist/plugin-management/community-presentation/registry-plugin-icons.d.ts +2 -0
- package/dist/plugin-management/community-presentation/registry-plugin-icons.js +31 -0
- package/dist/plugin-management/plugin-browser-model.d.ts +43 -0
- package/dist/plugin-management/plugin-browser-model.js +95 -0
- package/dist/plugin-management/plugin-browser-navigation.d.ts +3 -0
- package/dist/plugin-management/plugin-browser-navigation.js +36 -0
- package/dist/plugin-management/plugin-download-stats.d.ts +1 -0
- package/dist/plugin-management/plugin-download-stats.js +15 -0
- package/dist/plugin-management/plugin-management.css +1088 -117
- package/dist/plugin-management/plugin-registry-controller.svelte.d.ts +29 -0
- package/dist/plugin-management/plugin-registry-controller.svelte.js +154 -0
- package/dist/plugin-management/plugin-registry-detail-session.svelte.d.ts +18 -0
- package/dist/plugin-management/plugin-registry-detail-session.svelte.js +106 -0
- package/dist/plugin-management/plugin-registry-detail.d.ts +17 -0
- package/dist/plugin-management/plugin-registry-detail.js +1 -0
- package/dist/plugin-management/plugins-view.svelte.d.ts +40 -0
- package/dist/plugin-management/plugins-view.svelte.js +105 -0
- package/package.json +5 -5
|
@@ -1,71 +1,71 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import AlertTriangle from "@lucide/svelte/icons/alert-triangle";
|
|
3
2
|
import ArrowUpCircle from "@lucide/svelte/icons/arrow-up-circle";
|
|
4
|
-
import BookOpen from "@lucide/svelte/icons/book-open";
|
|
5
|
-
import CalendarClock from "@lucide/svelte/icons/calendar-clock";
|
|
6
3
|
import Check from "@lucide/svelte/icons/check";
|
|
7
4
|
import Download from "@lucide/svelte/icons/download";
|
|
8
|
-
import
|
|
5
|
+
import Power from "@lucide/svelte/icons/power";
|
|
9
6
|
import RefreshCw from "@lucide/svelte/icons/refresh-cw";
|
|
10
|
-
import ShieldAlert from "@lucide/svelte/icons/shield-alert";
|
|
11
7
|
import Trash2 from "@lucide/svelte/icons/trash-2";
|
|
12
8
|
import Upload from "@lucide/svelte/icons/upload";
|
|
13
9
|
import {
|
|
14
|
-
DEFAULT_OFFICIAL_PLUGIN_REGISTRY_SOURCE,
|
|
15
|
-
isAbortError,
|
|
16
10
|
Notice,
|
|
11
|
+
pluginDownloadCounts,
|
|
17
12
|
PluginDistributionError,
|
|
18
|
-
withPluginInstallProgress,
|
|
19
13
|
type InstalledPluginRecord,
|
|
20
|
-
type PluginCatalogDetail,
|
|
21
14
|
type PluginCatalogEntry,
|
|
15
|
+
type PluginRegistrySourceState,
|
|
22
16
|
type PluginUpdateInfo,
|
|
23
17
|
} from "@lapis-notes/api";
|
|
24
|
-
import * as Alert from "@lapismd/design-core/shadcn/alert";
|
|
25
18
|
import * as AlertDialog from "@lapismd/design-core/shadcn/alert-dialog";
|
|
26
|
-
import * as Badge from "@lapismd/design-core/shadcn/badge";
|
|
27
19
|
import * as Button from "@lapismd/design-core/shadcn/button";
|
|
28
|
-
import * as
|
|
29
|
-
import * as
|
|
30
|
-
import * as Resizable from "@lapismd/design-core/shadcn/resizable";
|
|
31
|
-
import * as Separator from "@lapismd/design-core/shadcn/separator";
|
|
20
|
+
import * as Field from "@lapismd/design-core/shadcn/field";
|
|
21
|
+
import * as Switch from "@lapismd/design-core/shadcn/switch";
|
|
32
22
|
import * as Tabs from "@lapismd/design-core/shadcn/tabs";
|
|
23
|
+
import * as Tooltip from "@lapismd/design-core/shadcn/tooltip";
|
|
33
24
|
import type { WorkspaceSettingsPageProps } from "@lapismd/design-core/workspace/settings";
|
|
34
25
|
import { onMount, tick, untrack } from "svelte";
|
|
26
|
+
import { getPluginRegistryController } from "./plugin-registry-controller.svelte";
|
|
27
|
+
import { openPluginBrowser } from "./plugin-browser-navigation";
|
|
35
28
|
import { getPluginManagementContext } from "./plugin-management-context.svelte";
|
|
36
29
|
import "./plugin-management.css";
|
|
37
|
-
import
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
import PluginRegistryContentState from "./PluginRegistryContentState.svelte";
|
|
31
|
+
import type { PluginRegistryBadgeTone } from "./PluginRegistryBadge.svelte";
|
|
32
|
+
import PluginRegistryRow, {
|
|
33
|
+
type RegistryRowBadge,
|
|
34
|
+
} from "./PluginRegistryRow.svelte";
|
|
35
|
+
import PluginRegistryToolbar from "./PluginRegistryToolbar.svelte";
|
|
36
|
+
import { formatApproximateDownloadCount } from "./plugin-download-stats";
|
|
42
37
|
|
|
43
38
|
type PluginsRegistryTabId = "installed" | "browse" | "updates" | "sources";
|
|
44
39
|
|
|
45
40
|
let { controller }: WorkspaceSettingsPageProps = $props();
|
|
46
41
|
const context = untrack(() => getPluginManagementContext(controller));
|
|
47
42
|
const app = context.app;
|
|
43
|
+
const registry = getPluginRegistryController(app);
|
|
48
44
|
|
|
49
45
|
let activeTab = $state<PluginsRegistryTabId>("installed");
|
|
50
|
-
let searchText = $state("");
|
|
51
|
-
let catalogEntries = $state<PluginCatalogEntry[]>([]);
|
|
52
|
-
let installed = $state<InstalledPluginRecord[]>([]);
|
|
53
|
-
let updates = $state<PluginUpdateInfo[]>([]);
|
|
54
|
-
let lastError = $state<Error | null>(null);
|
|
55
|
-
let refreshing = $state(false);
|
|
56
|
-
let runningAction = $state<string | null>(null);
|
|
57
|
-
let detailDialogOpen = $state(false);
|
|
58
46
|
let uninstallConfirmOpen = $state(false);
|
|
59
47
|
let uninstallTarget = $state<InstalledPluginRecord | null>(null);
|
|
60
|
-
let
|
|
61
|
-
let detailPluginDetail = $state<PluginCatalogDetail | null>(null);
|
|
62
|
-
let detailLoading = $state(false);
|
|
63
|
-
let readmeState = $state<PluginReadmeLoadState>({ status: "idle" });
|
|
64
|
-
let latestReleaseDates = $state<Record<string, string | null>>({});
|
|
65
|
-
let latestReleaseSizes = $state<Record<string, number | null>>({});
|
|
48
|
+
let uninstallDisableInstead = $state(false);
|
|
66
49
|
let bundleFileInput = $state<HTMLInputElement | null>(null);
|
|
67
50
|
|
|
68
|
-
let
|
|
51
|
+
let installedSearch = $state("");
|
|
52
|
+
let installedStatus = $state("all");
|
|
53
|
+
let installedProvenance = $state("all");
|
|
54
|
+
let installedSort = $state("name");
|
|
55
|
+
let browseSearch = $state("");
|
|
56
|
+
let browsePlatform = $state("all");
|
|
57
|
+
let browseChannel = $state("all");
|
|
58
|
+
let browseCategory = $state("all");
|
|
59
|
+
let browseInstallState = $state("all");
|
|
60
|
+
let browseCompatibleOnly = $state(false);
|
|
61
|
+
let browseSort = $state("name");
|
|
62
|
+
let updatesSearch = $state("");
|
|
63
|
+
let updatesStatus = $state("all");
|
|
64
|
+
let updatesSort = $state("status");
|
|
65
|
+
|
|
66
|
+
let installedIds = $derived(
|
|
67
|
+
new Set(registry.installed.map((record) => record.pluginId)),
|
|
68
|
+
);
|
|
69
69
|
let staticPluginIds = $derived(
|
|
70
70
|
new Set(app.plugins.corePluginEntries.map((entry) => entry.manifest.id)),
|
|
71
71
|
);
|
|
@@ -77,17 +77,183 @@
|
|
|
77
77
|
(plugin) => !installedIds.has(plugin.manifest.id),
|
|
78
78
|
),
|
|
79
79
|
);
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
let categories = $derived(
|
|
81
|
+
[
|
|
82
|
+
...new Set(registry.catalogEntries.flatMap((entry) => entry.categories)),
|
|
83
|
+
].sort(),
|
|
84
|
+
);
|
|
85
|
+
let compatiblePluginIds = $derived(
|
|
86
|
+
new Set(
|
|
87
|
+
app.pluginDistribution
|
|
88
|
+
.search({ compatibleOnly: true })
|
|
89
|
+
.map((entry) => entry.id),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
let filteredCatalogEntries = $derived.by(() => {
|
|
93
|
+
const text = browseSearch.trim().toLowerCase();
|
|
94
|
+
return registry.catalogEntries
|
|
95
|
+
.filter((entry) => {
|
|
96
|
+
if (
|
|
97
|
+
text &&
|
|
98
|
+
!`${entry.name} ${entry.id} ${entry.description} ${entry.author}`
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.includes(text)
|
|
101
|
+
) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
browsePlatform !== "all" &&
|
|
106
|
+
!entry.platforms.includes(
|
|
107
|
+
browsePlatform as PluginCatalogEntry["platforms"][number],
|
|
108
|
+
)
|
|
109
|
+
) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (browseChannel !== "all" && entry.channel !== browseChannel) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
browseCategory !== "all" &&
|
|
117
|
+
!entry.categories.includes(browseCategory)
|
|
118
|
+
) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
browseInstallState === "installed" &&
|
|
123
|
+
!availablePluginIds.has(entry.id)
|
|
124
|
+
) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
browseInstallState === "available" &&
|
|
129
|
+
availablePluginIds.has(entry.id)
|
|
130
|
+
) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (browseCompatibleOnly && !compatiblePluginIds.has(entry.id)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
})
|
|
138
|
+
.sort((left, right) =>
|
|
139
|
+
browseSort === "downloads"
|
|
140
|
+
? downloadCount(right.id) - downloadCount(left.id) ||
|
|
141
|
+
left.name.localeCompare(right.name)
|
|
142
|
+
: browseSort === "recent"
|
|
143
|
+
? (right.latestRelease?.releasedAt ?? "").localeCompare(
|
|
144
|
+
left.latestRelease?.releasedAt ?? "",
|
|
145
|
+
) || left.name.localeCompare(right.name)
|
|
146
|
+
: left.name.localeCompare(right.name),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
let filteredInstalled = $derived.by(() => {
|
|
150
|
+
const text = installedSearch.trim().toLowerCase();
|
|
151
|
+
return registry.installed
|
|
152
|
+
.filter((record) => {
|
|
153
|
+
const entry = catalogEntry(record.pluginId);
|
|
154
|
+
const runtime = app.plugins.plugins.get(record.pluginId);
|
|
155
|
+
const name = entry?.name ?? runtime?.manifest.name ?? record.pluginId;
|
|
156
|
+
const description =
|
|
157
|
+
entry?.description ?? runtime?.manifest.description ?? "";
|
|
158
|
+
if (
|
|
159
|
+
text &&
|
|
160
|
+
!`${name} ${record.pluginId} ${description}`
|
|
161
|
+
.toLowerCase()
|
|
162
|
+
.includes(text)
|
|
163
|
+
) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
installedProvenance !== "all" &&
|
|
168
|
+
record.provenance !== installedProvenance
|
|
169
|
+
) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (installedStatus === "enabled" && !runtime?.enabled) return false;
|
|
173
|
+
if (installedStatus === "disabled" && runtime?.enabled) return false;
|
|
174
|
+
if (installedStatus === "revoked" && !record.revoked) return false;
|
|
175
|
+
if (installedStatus === "restart" && !record.restartRequired)
|
|
176
|
+
return false;
|
|
177
|
+
return true;
|
|
178
|
+
})
|
|
179
|
+
.sort((left, right) =>
|
|
180
|
+
installedSort === "recent"
|
|
181
|
+
? right.updatedAt.localeCompare(left.updatedAt)
|
|
182
|
+
: installedName(left).localeCompare(installedName(right)),
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
let filteredCommunityPlugins = $derived.by(() => {
|
|
186
|
+
const text = installedSearch.trim().toLowerCase();
|
|
187
|
+
if (installedProvenance !== "all" && installedProvenance !== "manual") {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
return communityPlugins.filter((plugin) => {
|
|
191
|
+
if (
|
|
192
|
+
text &&
|
|
193
|
+
!`${plugin.manifest.name} ${plugin.manifest.id} ${plugin.manifest.description}`
|
|
194
|
+
.toLowerCase()
|
|
195
|
+
.includes(text)
|
|
196
|
+
) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (installedStatus === "enabled" && !plugin.enabled) return false;
|
|
200
|
+
if (installedStatus === "disabled" && plugin.enabled) return false;
|
|
201
|
+
if (installedStatus === "revoked" || installedStatus === "restart") {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
let filteredUpdates = $derived.by(() => {
|
|
208
|
+
const text = updatesSearch.trim().toLowerCase();
|
|
209
|
+
const priority = (update: PluginUpdateInfo) =>
|
|
210
|
+
update.canUpdate ? 0 : update.status === "revoked" ? 2 : 1;
|
|
211
|
+
return registry.updates
|
|
212
|
+
.filter((update) => {
|
|
213
|
+
if (
|
|
214
|
+
text &&
|
|
215
|
+
!`${update.name} ${update.id}`.toLowerCase().includes(text)
|
|
216
|
+
) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (updatesStatus === "ready" && !update.canUpdate) return false;
|
|
220
|
+
if (
|
|
221
|
+
updatesStatus === "incompatible" &&
|
|
222
|
+
(update.canUpdate || update.status === "revoked")
|
|
223
|
+
) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (updatesStatus === "revoked" && update.status !== "revoked") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
})
|
|
231
|
+
.sort((left, right) =>
|
|
232
|
+
updatesSort === "name"
|
|
233
|
+
? left.name.localeCompare(right.name)
|
|
234
|
+
: priority(left) - priority(right) ||
|
|
235
|
+
left.name.localeCompare(right.name),
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
let browseFilterCount = $derived(
|
|
239
|
+
Number(Boolean(browseSearch.trim())) +
|
|
240
|
+
Number(browsePlatform !== "all") +
|
|
241
|
+
Number(browseChannel !== "all") +
|
|
242
|
+
Number(browseCategory !== "all") +
|
|
243
|
+
Number(browseInstallState !== "all") +
|
|
244
|
+
Number(browseCompatibleOnly) +
|
|
245
|
+
Number(browseSort !== "name"),
|
|
246
|
+
);
|
|
247
|
+
let installedFilterCount = $derived(
|
|
248
|
+
Number(Boolean(installedSearch.trim())) +
|
|
249
|
+
Number(installedStatus !== "all") +
|
|
250
|
+
Number(installedProvenance !== "all") +
|
|
251
|
+
Number(installedSort !== "name"),
|
|
88
252
|
);
|
|
89
|
-
let
|
|
90
|
-
|
|
253
|
+
let updatesFilterCount = $derived(
|
|
254
|
+
Number(Boolean(updatesSearch.trim())) +
|
|
255
|
+
Number(updatesStatus !== "all") +
|
|
256
|
+
Number(updatesSort !== "status"),
|
|
91
257
|
);
|
|
92
258
|
|
|
93
259
|
$effect(() => {
|
|
@@ -102,135 +268,119 @@
|
|
|
102
268
|
const pluginId = context.reveal.entryId.slice("registry:".length);
|
|
103
269
|
activeTab = "browse";
|
|
104
270
|
void tick().then(async () => {
|
|
105
|
-
const entry = catalogEntries.find(
|
|
271
|
+
const entry = registry.catalogEntries.find(
|
|
272
|
+
(candidate) => candidate.id === pluginId,
|
|
273
|
+
);
|
|
106
274
|
if (entry) await openDetails(entry);
|
|
107
275
|
});
|
|
108
276
|
});
|
|
109
277
|
|
|
110
278
|
onMount(() => {
|
|
279
|
+
const release = registry.retain();
|
|
111
280
|
void refresh(false);
|
|
281
|
+
return () => {
|
|
282
|
+
release();
|
|
283
|
+
};
|
|
112
284
|
});
|
|
113
|
-
|
|
114
285
|
async function refresh(force: boolean): Promise<void> {
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
lastError = null;
|
|
118
|
-
if (force || !catalogEntries.length) {
|
|
119
|
-
await app.pluginDistribution.refreshCatalog({ force });
|
|
120
|
-
if (force) {
|
|
121
|
-
latestReleaseDates = {};
|
|
122
|
-
latestReleaseSizes = {};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catalogEntries = app.pluginDistribution.search({ channel: "all" });
|
|
126
|
-
installed = await app.pluginDistribution.listInstalled();
|
|
127
|
-
updates = await app.pluginDistribution.listUpdates();
|
|
128
|
-
void loadLatestReleaseDates(catalogEntries);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
131
|
-
new Notice(`Plugin registry unavailable: ${errorMessage(lastError)}`);
|
|
132
|
-
} finally {
|
|
133
|
-
refreshing = false;
|
|
134
|
-
}
|
|
286
|
+
await registry.refresh(force);
|
|
135
287
|
}
|
|
136
288
|
|
|
137
289
|
async function runAction(
|
|
138
290
|
actionId: string,
|
|
139
291
|
action: () => Promise<unknown>,
|
|
140
292
|
): Promise<void> {
|
|
141
|
-
runningAction
|
|
293
|
+
if (registry.runningAction) return;
|
|
294
|
+
registry.runningAction = actionId;
|
|
142
295
|
try {
|
|
143
|
-
lastError = null;
|
|
296
|
+
registry.lastError = null;
|
|
144
297
|
await action();
|
|
145
298
|
await refresh(false);
|
|
146
299
|
} catch (error) {
|
|
147
|
-
lastError =
|
|
148
|
-
|
|
300
|
+
registry.lastError =
|
|
301
|
+
error instanceof Error ? error : new Error(String(error));
|
|
302
|
+
new Notice(
|
|
303
|
+
`Plugin registry action failed: ${errorMessage(registry.lastError!)}`,
|
|
304
|
+
undefined,
|
|
305
|
+
app,
|
|
306
|
+
);
|
|
149
307
|
} finally {
|
|
150
|
-
runningAction = null;
|
|
308
|
+
registry.runningAction = null;
|
|
151
309
|
}
|
|
152
310
|
}
|
|
153
311
|
|
|
154
|
-
async function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
title: string,
|
|
158
|
-
action: (signal: AbortSignal) => Promise<unknown>,
|
|
312
|
+
async function setRegistrySourceEnabled(
|
|
313
|
+
sourceState: PluginRegistrySourceState,
|
|
314
|
+
enabled: boolean,
|
|
159
315
|
): Promise<void> {
|
|
160
|
-
runningAction
|
|
316
|
+
if (registry.runningAction) return;
|
|
317
|
+
const source = sourceState.source;
|
|
318
|
+
registry.runningAction = `source:${source.id}`;
|
|
161
319
|
try {
|
|
162
|
-
lastError = null;
|
|
163
|
-
await
|
|
164
|
-
|
|
320
|
+
registry.lastError = null;
|
|
321
|
+
await app.pluginDistribution.setSourceEnabled(source.id, enabled);
|
|
322
|
+
registry.catalogEntries = [];
|
|
323
|
+
await refresh(true);
|
|
165
324
|
} catch (error) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
325
|
+
registry.lastError =
|
|
326
|
+
error instanceof Error ? error : new Error(String(error));
|
|
327
|
+
new Notice(
|
|
328
|
+
`Plugin registry source update failed: ${errorMessage(registry.lastError!)}`,
|
|
329
|
+
undefined,
|
|
330
|
+
app,
|
|
331
|
+
);
|
|
332
|
+
registry.registrySources = app.pluginDistribution.listSources();
|
|
172
333
|
} finally {
|
|
173
|
-
runningAction = null;
|
|
334
|
+
registry.runningAction = null;
|
|
174
335
|
}
|
|
175
336
|
}
|
|
176
337
|
|
|
338
|
+
async function installEntry(entry: PluginCatalogEntry) {
|
|
339
|
+
await registry.install(entry);
|
|
340
|
+
}
|
|
341
|
+
async function updatePlugin(update: PluginUpdateInfo) {
|
|
342
|
+
await registry.update(update);
|
|
343
|
+
}
|
|
344
|
+
|
|
177
345
|
async function openDetails(entry: PluginCatalogEntry): Promise<void> {
|
|
178
|
-
|
|
179
|
-
|
|
346
|
+
try {
|
|
347
|
+
await openPluginBrowser(app, entry.id);
|
|
348
|
+
controller.close();
|
|
349
|
+
} catch (error) {
|
|
350
|
+
registry.lastError =
|
|
351
|
+
error instanceof Error ? error : new Error(String(error));
|
|
352
|
+
new Notice(
|
|
353
|
+
`Could not open plugin details: ${registry.lastError.message}`,
|
|
354
|
+
0,
|
|
355
|
+
app,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
180
358
|
}
|
|
181
359
|
|
|
182
360
|
function requestUninstall(record: InstalledPluginRecord): void {
|
|
361
|
+
uninstallDisableInstead = false;
|
|
183
362
|
uninstallTarget = record;
|
|
184
363
|
uninstallConfirmOpen = true;
|
|
185
364
|
}
|
|
186
365
|
|
|
187
|
-
|
|
188
|
-
const record = uninstallTarget;
|
|
189
|
-
if (!record) return;
|
|
366
|
+
function closeUninstallDialog(): void {
|
|
190
367
|
uninstallConfirmOpen = false;
|
|
368
|
+
uninstallDisableInstead = false;
|
|
191
369
|
uninstallTarget = null;
|
|
192
|
-
await runAction(`uninstall:${record.pluginId}`, () =>
|
|
193
|
-
app.pluginDistribution.uninstall(record.pluginId),
|
|
194
|
-
);
|
|
195
370
|
}
|
|
196
371
|
|
|
197
|
-
async function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
};
|
|
210
|
-
latestReleaseSizes = {
|
|
211
|
-
...latestReleaseSizes,
|
|
212
|
-
[entry.id]: latestBundleSize(detail),
|
|
213
|
-
};
|
|
214
|
-
const readmeUrl = detail?.readmeUrl ?? entry.readmeUrl;
|
|
215
|
-
if (!readmeUrl) {
|
|
216
|
-
readmeState = { status: "missing" };
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const markdown = await fetchPluginReadmeMarkdown(readmeUrl, {
|
|
220
|
-
pluginId: entry.id,
|
|
221
|
-
detailUrl: entry.detail,
|
|
222
|
-
});
|
|
223
|
-
if (detailPluginId !== entry.id) return;
|
|
224
|
-
readmeState = { status: "loaded", url: readmeUrl, markdown };
|
|
225
|
-
} catch (error) {
|
|
226
|
-
if (detailPluginId !== entry.id) return;
|
|
227
|
-
readmeState = {
|
|
228
|
-
status: "error",
|
|
229
|
-
message: error instanceof Error ? error.message : String(error),
|
|
230
|
-
};
|
|
231
|
-
} finally {
|
|
232
|
-
if (detailPluginId === entry.id) detailLoading = false;
|
|
233
|
-
}
|
|
372
|
+
async function confirmPluginRemovalChoice(): Promise<void> {
|
|
373
|
+
const record = uninstallTarget;
|
|
374
|
+
if (!record) return;
|
|
375
|
+
const disableInstead = uninstallDisableInstead;
|
|
376
|
+
closeUninstallDialog();
|
|
377
|
+
await runAction(
|
|
378
|
+
`${disableInstead ? "toggle" : "uninstall"}:${record.pluginId}`,
|
|
379
|
+
() =>
|
|
380
|
+
disableInstead
|
|
381
|
+
? app.plugins.disablePlugin(record.pluginId)
|
|
382
|
+
: app.pluginDistribution.uninstall(record.pluginId),
|
|
383
|
+
);
|
|
234
384
|
}
|
|
235
385
|
|
|
236
386
|
function errorMessage(error: Error): string {
|
|
@@ -239,7 +389,7 @@
|
|
|
239
389
|
: error.message;
|
|
240
390
|
}
|
|
241
391
|
|
|
242
|
-
function
|
|
392
|
+
function titleCase(value: string): string {
|
|
243
393
|
return value
|
|
244
394
|
.split(/[\s-]+/)
|
|
245
395
|
.filter(Boolean)
|
|
@@ -247,55 +397,116 @@
|
|
|
247
397
|
.join(" ");
|
|
248
398
|
}
|
|
249
399
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
400
|
+
function provenanceBadgeTone(value: string): PluginRegistryBadgeTone {
|
|
401
|
+
if (value === "official" || value === "community") return value;
|
|
402
|
+
return "neutral";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function catalogEntry(pluginId: string): PluginCatalogEntry | undefined {
|
|
406
|
+
return registry.catalogEntries.find((entry) => entry.id === pluginId);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function installedName(record: InstalledPluginRecord): string {
|
|
410
|
+
return (
|
|
411
|
+
catalogEntry(record.pluginId)?.name ??
|
|
412
|
+
app.plugins.plugins.get(record.pluginId)?.manifest.name ??
|
|
413
|
+
record.pluginId
|
|
257
414
|
);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
{ releasedAt: latestReleaseDate(detail), size: latestBundleSize(detail) },
|
|
266
|
-
] as const;
|
|
267
|
-
} catch {
|
|
268
|
-
return [entry.id, { releasedAt: null, size: null }] as const;
|
|
269
|
-
}
|
|
270
|
-
}),
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function installedDescription(record: InstalledPluginRecord): string {
|
|
418
|
+
return (
|
|
419
|
+
catalogEntry(record.pluginId)?.description ??
|
|
420
|
+
app.plugins.plugins.get(record.pluginId)?.manifest.description ??
|
|
421
|
+
`Installed plugin ${record.pluginId}`
|
|
271
422
|
);
|
|
272
|
-
latestReleaseDates = {
|
|
273
|
-
...latestReleaseDates,
|
|
274
|
-
...Object.fromEntries(summaries.map(([id, value]) => [id, value.releasedAt])),
|
|
275
|
-
};
|
|
276
|
-
latestReleaseSizes = {
|
|
277
|
-
...latestReleaseSizes,
|
|
278
|
-
...Object.fromEntries(summaries.map(([id, value]) => [id, value.size])),
|
|
279
|
-
};
|
|
280
423
|
}
|
|
281
424
|
|
|
282
|
-
function
|
|
283
|
-
return
|
|
425
|
+
function pluginToggleAction(enabled: boolean, name: string): string {
|
|
426
|
+
return `${enabled ? "Disable" : "Enable"} ${name}`;
|
|
284
427
|
}
|
|
285
428
|
|
|
286
|
-
function
|
|
287
|
-
|
|
429
|
+
function resolveRuntimePlugin(pluginId: string) {
|
|
430
|
+
void registry.pluginLifecycleRevision;
|
|
431
|
+
return app.plugins.plugins.get(pluginId);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function pluginDescription(pluginId: string, fallback: string): string {
|
|
435
|
+
return (
|
|
436
|
+
catalogEntry(pluginId)?.description ??
|
|
437
|
+
app.plugins.plugins.get(pluginId)?.manifest.description ??
|
|
438
|
+
fallback
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function registryRowBadges(
|
|
443
|
+
entry: PluginCatalogEntry | undefined,
|
|
444
|
+
provenance: string,
|
|
445
|
+
): RegistryRowBadge[] {
|
|
446
|
+
return [
|
|
447
|
+
{
|
|
448
|
+
label: titleCase(provenance),
|
|
449
|
+
tone: provenanceBadgeTone(provenance),
|
|
450
|
+
},
|
|
451
|
+
...(entry?.platforms.map((platform) => ({
|
|
452
|
+
label: titleCase(platform),
|
|
453
|
+
tone: platform === "web" ? ("web" as const) : ("desktop" as const),
|
|
454
|
+
})) ?? []),
|
|
455
|
+
...(entry?.categories.slice(0, 2).map((category) => ({
|
|
456
|
+
label: titleCase(category),
|
|
457
|
+
tone: "category" as const,
|
|
458
|
+
})) ?? []),
|
|
459
|
+
...(entry?.status === "revoked"
|
|
460
|
+
? [{ label: "Revoked", tone: "danger" as const }]
|
|
461
|
+
: []),
|
|
462
|
+
];
|
|
288
463
|
}
|
|
289
464
|
|
|
290
465
|
function installedSize(record: InstalledPluginRecord): number | null {
|
|
291
|
-
const detailSize = latestReleaseSizes[record.pluginId];
|
|
292
|
-
if (typeof detailSize === "number") return detailSize;
|
|
293
466
|
return record.files.length
|
|
294
467
|
? record.files.reduce((total, file) => total + file.size, 0)
|
|
295
468
|
: null;
|
|
296
469
|
}
|
|
297
470
|
|
|
298
|
-
function
|
|
471
|
+
function downloadCount(pluginId: string): number {
|
|
472
|
+
return registry.downloadStats
|
|
473
|
+
? pluginDownloadCounts(registry.downloadStats, pluginId).recent
|
|
474
|
+
: 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function downloadLabel(pluginId: string): string | null {
|
|
478
|
+
if (!registry.downloadStats) return null;
|
|
479
|
+
const count = pluginDownloadCounts(registry.downloadStats, pluginId).recent;
|
|
480
|
+
return `~${formatApproximateDownloadCount(count)} downloads (30d)`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function browseActionStatus(
|
|
484
|
+
pluginId: string,
|
|
485
|
+
installedRecord: InstalledPluginRecord | undefined,
|
|
486
|
+
) {
|
|
487
|
+
if (registry.actionFeedback?.pluginId === pluginId) {
|
|
488
|
+
if (registry.actionFeedback.state === "running") {
|
|
489
|
+
return {
|
|
490
|
+
label: "Installing",
|
|
491
|
+
reason: registry.actionFeedback.message,
|
|
492
|
+
tone: "neutral" as const,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (registry.actionFeedback.state === "error") {
|
|
496
|
+
return {
|
|
497
|
+
label: "Install failed",
|
|
498
|
+
reason: registry.actionFeedback.message,
|
|
499
|
+
tone: "danger" as const,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return installedRecord
|
|
504
|
+
? { label: "Installed", tone: "success" as const }
|
|
505
|
+
: undefined;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function formatDate(value?: string): string | null {
|
|
509
|
+
if (!value) return null;
|
|
299
510
|
const date = new Date(value);
|
|
300
511
|
if (Number.isNaN(date.getTime())) return null;
|
|
301
512
|
return new Intl.DateTimeFormat(undefined, {
|
|
@@ -305,22 +516,8 @@
|
|
|
305
516
|
}).format(date);
|
|
306
517
|
}
|
|
307
518
|
|
|
308
|
-
function
|
|
309
|
-
return
|
|
310
|
-
`${update.currentVersion} -> ${update.targetVersion}`,
|
|
311
|
-
typeof update.bundleSize === "number"
|
|
312
|
-
? `Size ${formatByteSize(update.bundleSize)}`
|
|
313
|
-
: "",
|
|
314
|
-
update.targetVersion !== update.latestVersion
|
|
315
|
-
? `Latest ${update.latestVersion}`
|
|
316
|
-
: "",
|
|
317
|
-
update.reasons.length ? `Blocked: ${update.reasons.join(", ")}` : "",
|
|
318
|
-
]
|
|
319
|
-
.filter(Boolean)
|
|
320
|
-
.join(" | ");
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function formatByteSize(bytes: number): string {
|
|
519
|
+
function formatByteSize(bytes?: number | null): string | null {
|
|
520
|
+
if (typeof bytes !== "number") return null;
|
|
324
521
|
if (bytes < 1024) return `${bytes} B`;
|
|
325
522
|
const units = ["KiB", "MiB", "GiB"];
|
|
326
523
|
let value = bytes / 1024;
|
|
@@ -334,6 +531,45 @@
|
|
|
334
531
|
}).format(value)} ${units[unitIndex]}`;
|
|
335
532
|
}
|
|
336
533
|
|
|
534
|
+
function updateReason(update: PluginUpdateInfo): string | undefined {
|
|
535
|
+
if (update.status === "revoked") {
|
|
536
|
+
return (
|
|
537
|
+
update.revoked?.message ??
|
|
538
|
+
update.revoked?.reason ??
|
|
539
|
+
"This installed version was revoked by the official registry."
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (!update.canUpdate) {
|
|
543
|
+
return update.reasons.length
|
|
544
|
+
? update.reasons.join(", ")
|
|
545
|
+
: "The latest version is not compatible with this runtime.";
|
|
546
|
+
}
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function resetInstalledFilters(): void {
|
|
551
|
+
installedSearch = "";
|
|
552
|
+
installedStatus = "all";
|
|
553
|
+
installedProvenance = "all";
|
|
554
|
+
installedSort = "name";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function resetBrowseFilters(): void {
|
|
558
|
+
browseSearch = "";
|
|
559
|
+
browsePlatform = "all";
|
|
560
|
+
browseChannel = "all";
|
|
561
|
+
browseCategory = "all";
|
|
562
|
+
browseInstallState = "all";
|
|
563
|
+
browseCompatibleOnly = false;
|
|
564
|
+
browseSort = "name";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function resetUpdateFilters(): void {
|
|
568
|
+
updatesSearch = "";
|
|
569
|
+
updatesStatus = "all";
|
|
570
|
+
updatesSort = "status";
|
|
571
|
+
}
|
|
572
|
+
|
|
337
573
|
function openBundleFilePicker(): void {
|
|
338
574
|
bundleFileInput?.click();
|
|
339
575
|
}
|
|
@@ -347,16 +583,26 @@
|
|
|
347
583
|
new Notice("Select a .lapis-plugin bundle.");
|
|
348
584
|
return;
|
|
349
585
|
}
|
|
350
|
-
|
|
586
|
+
let installedRecord: InstalledPluginRecord | undefined;
|
|
587
|
+
await registry.runPluginAction(
|
|
351
588
|
"install-bundle",
|
|
352
589
|
"*",
|
|
353
590
|
`Installing ${file.name}`,
|
|
354
|
-
async (signal) =>
|
|
355
|
-
app.pluginDistribution.installBundle(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
591
|
+
async (signal) => {
|
|
592
|
+
installedRecord = await app.pluginDistribution.installBundle(
|
|
593
|
+
await file.arrayBuffer(),
|
|
594
|
+
{
|
|
595
|
+
enable: true,
|
|
596
|
+
signal,
|
|
597
|
+
},
|
|
598
|
+
);
|
|
599
|
+
},
|
|
359
600
|
);
|
|
601
|
+
if (installedRecord?.nostrProof?.requiresRelayRefresh) {
|
|
602
|
+
new Notice(
|
|
603
|
+
"Plugin installed from a valid Nostr proof. Connect and refresh its registry source to check for newer revocations.",
|
|
604
|
+
);
|
|
605
|
+
}
|
|
360
606
|
}
|
|
361
607
|
</script>
|
|
362
608
|
|
|
@@ -368,7 +614,10 @@
|
|
|
368
614
|
<header class="lapis-plugin-management__header">
|
|
369
615
|
<div>
|
|
370
616
|
<h1>Plugin registry</h1>
|
|
371
|
-
<p>
|
|
617
|
+
<p>
|
|
618
|
+
Browse verified plugins, manage installed provenance, and review
|
|
619
|
+
updates.
|
|
620
|
+
</p>
|
|
372
621
|
</div>
|
|
373
622
|
<div class="lapis-plugin-management__actions">
|
|
374
623
|
<input
|
|
@@ -382,37 +631,27 @@
|
|
|
382
631
|
<Button.Root
|
|
383
632
|
variant="outline"
|
|
384
633
|
size="sm"
|
|
385
|
-
disabled={runningAction === "install-bundle"}
|
|
634
|
+
disabled={registry.runningAction === "install-bundle"}
|
|
386
635
|
onclick={openBundleFilePicker}
|
|
636
|
+
><Upload />Install from .lapis-plugin</Button.Root
|
|
387
637
|
>
|
|
388
|
-
<Upload class="lapis-plugin-management__icon" />
|
|
389
|
-
Install from .lapis-plugin
|
|
390
|
-
</Button.Root>
|
|
391
638
|
<Button.Root
|
|
639
|
+
class="lapis-plugin-registry__refresh"
|
|
392
640
|
variant="outline"
|
|
393
641
|
size="sm"
|
|
394
|
-
disabled={refreshing}
|
|
395
|
-
aria-busy={refreshing}
|
|
396
|
-
aria-label={refreshing
|
|
642
|
+
disabled={registry.refreshing}
|
|
643
|
+
aria-busy={registry.refreshing}
|
|
644
|
+
aria-label={registry.refreshing
|
|
645
|
+
? "Refreshing plugin registry"
|
|
646
|
+
: "Refresh plugin registry"}
|
|
397
647
|
onclick={() => void refresh(true)}
|
|
398
648
|
>
|
|
399
|
-
<RefreshCw
|
|
400
|
-
|
|
401
|
-
data-spinning={refreshing || undefined}
|
|
402
|
-
/>
|
|
403
|
-
Refresh
|
|
649
|
+
<RefreshCw data-spinning={registry.refreshing || undefined} />
|
|
650
|
+
<span>Refresh</span>
|
|
404
651
|
</Button.Root>
|
|
405
652
|
</div>
|
|
406
653
|
</header>
|
|
407
654
|
|
|
408
|
-
{#if lastError}
|
|
409
|
-
<Alert.Root variant="destructive">
|
|
410
|
-
<AlertTriangle class="lapis-plugin-management__icon" />
|
|
411
|
-
<Alert.Title>Plugin registry unavailable</Alert.Title>
|
|
412
|
-
<Alert.Description>{errorMessage(lastError)}</Alert.Description>
|
|
413
|
-
</Alert.Root>
|
|
414
|
-
{/if}
|
|
415
|
-
|
|
416
655
|
<Tabs.Root bind:value={activeTab} class="lapis-plugin-registry__tabs">
|
|
417
656
|
<Tabs.List class="lapis-plugin-registry__tab-list">
|
|
418
657
|
<Tabs.Trigger class="lapis-plugin-registry__tab" value="installed"
|
|
@@ -429,322 +668,580 @@
|
|
|
429
668
|
>
|
|
430
669
|
</Tabs.List>
|
|
431
670
|
|
|
432
|
-
<Tabs.Content value="installed" class="lapis-plugin-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
{
|
|
440
|
-
{
|
|
441
|
-
{
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
<
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
671
|
+
<Tabs.Content value="installed" class="lapis-plugin-registry__content">
|
|
672
|
+
<PluginRegistryToolbar
|
|
673
|
+
tab="installed"
|
|
674
|
+
bind:search={installedSearch}
|
|
675
|
+
bind:enabledState={installedStatus}
|
|
676
|
+
bind:provenance={installedProvenance}
|
|
677
|
+
bind:sort={installedSort}
|
|
678
|
+
filterCount={installedFilterCount}
|
|
679
|
+
resultCount={filteredInstalled.length + filteredCommunityPlugins.length}
|
|
680
|
+
onreset={resetInstalledFilters}
|
|
681
|
+
/>
|
|
682
|
+
{#if registry.initialLoading}
|
|
683
|
+
<PluginRegistryContentState
|
|
684
|
+
kind="loading"
|
|
685
|
+
heading="Loading installed plugins"
|
|
686
|
+
description="Reading installed plugin state and registry provenance…"
|
|
687
|
+
/>
|
|
688
|
+
{:else if registry.lastError && !registry.installed.length && !communityPlugins.length}
|
|
689
|
+
<PluginRegistryContentState
|
|
690
|
+
kind="error"
|
|
691
|
+
heading="Installed plugins unavailable"
|
|
692
|
+
description={errorMessage(registry.lastError)}
|
|
693
|
+
actionLabel="Retry"
|
|
694
|
+
onaction={() => void refresh(true)}
|
|
695
|
+
/>
|
|
696
|
+
{:else if !registry.installed.length && !communityPlugins.length}
|
|
697
|
+
<PluginRegistryContentState
|
|
698
|
+
part="registry-installed-empty-state"
|
|
699
|
+
kind="empty"
|
|
700
|
+
heading="No plugins installed"
|
|
701
|
+
description="No installed registry or community plugins."
|
|
702
|
+
actionLabel="Browse plugins"
|
|
703
|
+
onaction={() => (activeTab = "browse")}
|
|
704
|
+
/>
|
|
705
|
+
{:else if !filteredInstalled.length && !filteredCommunityPlugins.length}
|
|
706
|
+
<PluginRegistryContentState
|
|
707
|
+
kind="filtered"
|
|
708
|
+
heading="No installed plugins match"
|
|
709
|
+
description="Try changing the search or installed-plugin filters."
|
|
710
|
+
actionLabel="Reset filters"
|
|
711
|
+
onaction={resetInstalledFilters}
|
|
712
|
+
/>
|
|
713
|
+
{:else}
|
|
714
|
+
<div class="lapis-plugin-registry__rows">
|
|
715
|
+
{#each filteredInstalled as record (record.pluginId)}
|
|
716
|
+
{@const runtimePlugin = resolveRuntimePlugin(record.pluginId)}
|
|
717
|
+
{@const entry = catalogEntry(record.pluginId)}
|
|
718
|
+
{@const size = installedSize(record)}
|
|
719
|
+
{@const update = registry.updates.find(
|
|
720
|
+
(candidate) => candidate.id === record.pluginId,
|
|
721
|
+
)}
|
|
722
|
+
{@const toggleAction = pluginToggleAction(
|
|
723
|
+
Boolean(runtimePlugin?.enabled),
|
|
724
|
+
installedName(record),
|
|
725
|
+
)}
|
|
726
|
+
<PluginRegistryRow
|
|
727
|
+
id={record.pluginId}
|
|
728
|
+
name={installedName(record)}
|
|
729
|
+
description={installedDescription(record)}
|
|
730
|
+
version={record.installedVersion}
|
|
731
|
+
metadata={[
|
|
732
|
+
...(size === null ? [] : [`Size ${formatByteSize(size)}`]),
|
|
733
|
+
`Updated ${formatDate(record.updatedAt) ?? record.updatedAt}`,
|
|
734
|
+
...(entry && downloadLabel(entry.id)
|
|
735
|
+
? [downloadLabel(entry.id)!]
|
|
736
|
+
: []),
|
|
737
|
+
]}
|
|
738
|
+
badges={[
|
|
739
|
+
...registryRowBadges(entry, record.provenance),
|
|
740
|
+
...(record.nostrProof
|
|
741
|
+
? [{ label: "Nostr proof", tone: "community" as const }]
|
|
742
|
+
: []),
|
|
743
|
+
...(record.restartRequired
|
|
744
|
+
? [{ label: "Restart required" }]
|
|
745
|
+
: []),
|
|
746
|
+
]}
|
|
747
|
+
status={record.revoked
|
|
748
|
+
? {
|
|
749
|
+
label: "Revoked",
|
|
750
|
+
reason: record.revoked.message ?? record.revoked.reason,
|
|
751
|
+
tone: "danger",
|
|
752
|
+
}
|
|
753
|
+
: update?.canUpdate
|
|
754
|
+
? { label: "Update available", tone: "warning" }
|
|
755
|
+
: record.nostrProof?.requiresRelayRefresh
|
|
756
|
+
? {
|
|
757
|
+
label: "Revocation check needed",
|
|
758
|
+
reason: "Refresh the Nostr registry source when online",
|
|
759
|
+
tone: "warning",
|
|
760
|
+
}
|
|
761
|
+
: undefined}
|
|
762
|
+
>
|
|
763
|
+
{#snippet actions()}
|
|
764
|
+
{#if update?.canUpdate}
|
|
765
|
+
<Tooltip.Provider>
|
|
766
|
+
<Tooltip.Root>
|
|
767
|
+
<Tooltip.Trigger>
|
|
768
|
+
{#snippet child({ props })}
|
|
769
|
+
<Button.Root
|
|
770
|
+
{...props}
|
|
771
|
+
size="icon"
|
|
772
|
+
variant="outline"
|
|
773
|
+
aria-label={`Update ${installedName(record)}`}
|
|
774
|
+
disabled={registry.runningAction ===
|
|
775
|
+
`update:${record.pluginId}`}
|
|
776
|
+
onclick={() => void updatePlugin(update)}
|
|
777
|
+
><ArrowUpCircle /></Button.Root
|
|
778
|
+
>
|
|
779
|
+
{/snippet}
|
|
780
|
+
</Tooltip.Trigger>
|
|
781
|
+
<Tooltip.Content
|
|
782
|
+
>Update {installedName(record)}</Tooltip.Content
|
|
783
|
+
>
|
|
784
|
+
</Tooltip.Root>
|
|
785
|
+
</Tooltip.Provider>
|
|
786
|
+
{/if}
|
|
787
|
+
{#key toggleAction}
|
|
788
|
+
<Tooltip.Provider>
|
|
789
|
+
<Tooltip.Root>
|
|
790
|
+
<Tooltip.Trigger>
|
|
791
|
+
{#snippet child({ props })}
|
|
792
|
+
<Switch.Root
|
|
793
|
+
{...props}
|
|
794
|
+
checked={Boolean(runtimePlugin?.enabled)}
|
|
795
|
+
disabled={!runtimePlugin ||
|
|
796
|
+
registry.runningAction ===
|
|
797
|
+
`toggle:${record.pluginId}`}
|
|
798
|
+
aria-label={toggleAction}
|
|
799
|
+
data-tooltip-action={toggleAction}
|
|
800
|
+
onCheckedChange={(checked) =>
|
|
801
|
+
void runAction(`toggle:${record.pluginId}`, () =>
|
|
802
|
+
checked
|
|
803
|
+
? app.plugins.enablePlugin(record.pluginId)
|
|
804
|
+
: app.plugins.disablePlugin(record.pluginId),
|
|
805
|
+
)}
|
|
806
|
+
/>
|
|
807
|
+
{/snippet}
|
|
808
|
+
</Tooltip.Trigger>
|
|
809
|
+
<Tooltip.Content>{toggleAction}</Tooltip.Content>
|
|
810
|
+
</Tooltip.Root>
|
|
811
|
+
</Tooltip.Provider>
|
|
812
|
+
{/key}
|
|
551
813
|
<Button.Root
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
(signal) =>
|
|
561
|
-
app.pluginDistribution.install(entry.id, {
|
|
562
|
-
enable: true,
|
|
563
|
-
requireOfficial: entry.channel === "official",
|
|
564
|
-
signal,
|
|
565
|
-
}),
|
|
566
|
-
)}
|
|
814
|
+
size="icon"
|
|
815
|
+
variant="ghost"
|
|
816
|
+
aria-label={`Uninstall ${record.pluginId}`}
|
|
817
|
+
title="Uninstall"
|
|
818
|
+
disabled={registry.runningAction ===
|
|
819
|
+
`uninstall:${record.pluginId}`}
|
|
820
|
+
onclick={() => requestUninstall(record)}
|
|
821
|
+
><Trash2 /></Button.Root
|
|
567
822
|
>
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
823
|
+
{/snippet}
|
|
824
|
+
</PluginRegistryRow>
|
|
825
|
+
{/each}
|
|
826
|
+
{#each filteredCommunityPlugins as plugin (plugin.manifest.id)}
|
|
827
|
+
<PluginRegistryRow
|
|
828
|
+
id={plugin.manifest.id}
|
|
829
|
+
name={plugin.manifest.name}
|
|
830
|
+
description={plugin.manifest.description}
|
|
831
|
+
version={plugin.manifest.version}
|
|
832
|
+
badges={[{ label: "Manual" }]}
|
|
833
|
+
/>
|
|
834
|
+
{/each}
|
|
835
|
+
</div>
|
|
836
|
+
{/if}
|
|
575
837
|
</Tabs.Content>
|
|
576
838
|
|
|
577
|
-
<Tabs.Content value="
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
{
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
{
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
<
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
{
|
|
839
|
+
<Tabs.Content value="browse" class="lapis-plugin-registry__content">
|
|
840
|
+
<PluginRegistryToolbar
|
|
841
|
+
tab="browse"
|
|
842
|
+
bind:search={browseSearch}
|
|
843
|
+
bind:platform={browsePlatform}
|
|
844
|
+
bind:channel={browseChannel}
|
|
845
|
+
bind:category={browseCategory}
|
|
846
|
+
bind:installedState={browseInstallState}
|
|
847
|
+
bind:compatibleOnly={browseCompatibleOnly}
|
|
848
|
+
bind:sort={browseSort}
|
|
849
|
+
{categories}
|
|
850
|
+
filterCount={browseFilterCount}
|
|
851
|
+
resultCount={filteredCatalogEntries.length}
|
|
852
|
+
onreset={resetBrowseFilters}
|
|
853
|
+
/>
|
|
854
|
+
{#if registry.initialLoading}
|
|
855
|
+
<PluginRegistryContentState
|
|
856
|
+
kind="loading"
|
|
857
|
+
heading="Loading plugin registry"
|
|
858
|
+
description="Verifying the official catalog and revocation metadata…"
|
|
859
|
+
/>
|
|
860
|
+
{:else if registry.lastError && !registry.catalogEntries.length}
|
|
861
|
+
<PluginRegistryContentState
|
|
862
|
+
kind="error"
|
|
863
|
+
heading="Plugin registry unavailable"
|
|
864
|
+
description={errorMessage(registry.lastError)}
|
|
865
|
+
actionLabel="Retry"
|
|
866
|
+
onaction={() => void refresh(true)}
|
|
867
|
+
/>
|
|
868
|
+
{:else if !registry.catalogEntries.length}
|
|
869
|
+
<PluginRegistryContentState
|
|
870
|
+
kind="empty"
|
|
871
|
+
heading="No registry entries"
|
|
872
|
+
description="The configured registry did not return any plugin entries."
|
|
873
|
+
actionLabel="Refresh"
|
|
874
|
+
onaction={() => void refresh(true)}
|
|
875
|
+
/>
|
|
876
|
+
{:else if !filteredCatalogEntries.length}
|
|
877
|
+
<PluginRegistryContentState
|
|
878
|
+
kind="filtered"
|
|
879
|
+
heading="No plugins match"
|
|
880
|
+
description="Try changing the search or Browse filters."
|
|
881
|
+
actionLabel="Reset filters"
|
|
882
|
+
onaction={resetBrowseFilters}
|
|
883
|
+
/>
|
|
884
|
+
{:else}
|
|
885
|
+
<div class="lapis-plugin-registry__rows">
|
|
886
|
+
{#each filteredCatalogEntries as entry (entry.id)}
|
|
887
|
+
{@const installedRecord = registry.installed.find(
|
|
888
|
+
(record) => record.pluginId === entry.id,
|
|
889
|
+
)}
|
|
890
|
+
{@const isBundled = staticPluginIds.has(entry.id)}
|
|
891
|
+
{@const releaseDate = formatDate(entry.latestRelease?.releasedAt)}
|
|
892
|
+
{@const releaseSize = formatByteSize(
|
|
893
|
+
entry.latestRelease?.bundleSize,
|
|
894
|
+
)}
|
|
895
|
+
<PluginRegistryRow
|
|
896
|
+
id={entry.id}
|
|
897
|
+
name={entry.name}
|
|
898
|
+
description={entry.description}
|
|
899
|
+
version={entry.latestVersion}
|
|
900
|
+
metadata={[
|
|
901
|
+
...(releaseDate ? [`Released ${releaseDate}`] : []),
|
|
902
|
+
...(releaseSize ? [`Size ${releaseSize}`] : []),
|
|
903
|
+
...(downloadLabel(entry.id) ? [downloadLabel(entry.id)!] : []),
|
|
904
|
+
]}
|
|
905
|
+
badges={registryRowBadges(entry, entry.channel)}
|
|
906
|
+
status={browseActionStatus(entry.id, installedRecord)}
|
|
907
|
+
onopen={() => void openDetails(entry)}
|
|
908
|
+
>
|
|
909
|
+
{#snippet actions()}
|
|
910
|
+
{#if installedRecord}
|
|
911
|
+
<Button.Root
|
|
912
|
+
size="sm"
|
|
913
|
+
variant="outline"
|
|
914
|
+
aria-label={`Uninstall ${entry.name}`}
|
|
915
|
+
disabled={registry.runningAction ===
|
|
916
|
+
`uninstall:${entry.id}`}
|
|
917
|
+
onclick={() => requestUninstall(installedRecord)}
|
|
918
|
+
><Trash2 data-icon="inline-start" />Uninstall</Button.Root
|
|
919
|
+
>
|
|
920
|
+
{:else if isBundled}
|
|
921
|
+
<Button.Root size="sm" variant="outline" disabled
|
|
922
|
+
><Check data-icon="inline-start" />Bundled</Button.Root
|
|
923
|
+
>
|
|
924
|
+
{:else}
|
|
925
|
+
{@const installing =
|
|
926
|
+
registry.runningAction === `install:${entry.id}`}
|
|
927
|
+
<Button.Root
|
|
928
|
+
size="sm"
|
|
929
|
+
aria-label={installing
|
|
930
|
+
? `Installing ${entry.name}`
|
|
931
|
+
: undefined}
|
|
932
|
+
aria-busy={installing}
|
|
933
|
+
disabled={installing}
|
|
934
|
+
onclick={() => void installEntry(entry)}
|
|
935
|
+
>{#if installing}<RefreshCw
|
|
936
|
+
data-icon="inline-start"
|
|
937
|
+
data-spinning="true"
|
|
938
|
+
/>Installing…{:else}<Download
|
|
939
|
+
data-icon="inline-start"
|
|
940
|
+
/>Install{/if}</Button.Root
|
|
941
|
+
>
|
|
942
|
+
{/if}
|
|
943
|
+
{/snippet}
|
|
944
|
+
</PluginRegistryRow>
|
|
945
|
+
{/each}
|
|
946
|
+
</div>
|
|
947
|
+
{/if}
|
|
623
948
|
</Tabs.Content>
|
|
624
949
|
|
|
625
|
-
<Tabs.Content value="
|
|
626
|
-
<
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
950
|
+
<Tabs.Content value="updates" class="lapis-plugin-registry__content">
|
|
951
|
+
<PluginRegistryToolbar
|
|
952
|
+
tab="updates"
|
|
953
|
+
bind:search={updatesSearch}
|
|
954
|
+
bind:updateState={updatesStatus}
|
|
955
|
+
bind:sort={updatesSort}
|
|
956
|
+
filterCount={updatesFilterCount}
|
|
957
|
+
resultCount={filteredUpdates.length}
|
|
958
|
+
onreset={resetUpdateFilters}
|
|
959
|
+
/>
|
|
960
|
+
{#if registry.initialLoading}
|
|
961
|
+
<PluginRegistryContentState
|
|
962
|
+
kind="loading"
|
|
963
|
+
heading="Checking for updates"
|
|
964
|
+
description="Comparing installed plugins with verified registry releases…"
|
|
965
|
+
/>
|
|
966
|
+
{:else if registry.lastError && !registry.updates.length}
|
|
967
|
+
<PluginRegistryContentState
|
|
968
|
+
kind="error"
|
|
969
|
+
heading="Updates unavailable"
|
|
970
|
+
description={errorMessage(registry.lastError)}
|
|
971
|
+
actionLabel="Retry"
|
|
972
|
+
onaction={() => void refresh(true)}
|
|
973
|
+
/>
|
|
974
|
+
{:else if !registry.updates.length}
|
|
975
|
+
<PluginRegistryContentState
|
|
976
|
+
part="registry-updates-empty-state"
|
|
977
|
+
kind="success"
|
|
978
|
+
heading="You’re up to date"
|
|
979
|
+
description="No plugin updates available."
|
|
980
|
+
actionLabel="Check for updates"
|
|
981
|
+
onaction={() => void refresh(true)}
|
|
982
|
+
/>
|
|
983
|
+
{:else if !filteredUpdates.length}
|
|
984
|
+
<PluginRegistryContentState
|
|
985
|
+
kind="filtered"
|
|
986
|
+
heading="No updates match"
|
|
987
|
+
description="Try changing the search or update-status filter."
|
|
988
|
+
actionLabel="Reset filters"
|
|
989
|
+
onaction={resetUpdateFilters}
|
|
990
|
+
/>
|
|
991
|
+
{:else}
|
|
992
|
+
<div class="lapis-plugin-registry__rows">
|
|
993
|
+
{#each filteredUpdates as update (update.id)}
|
|
994
|
+
{@const entry = catalogEntry(update.id)}
|
|
995
|
+
{@const releaseDate = formatDate(entry?.latestRelease?.releasedAt)}
|
|
996
|
+
<PluginRegistryRow
|
|
997
|
+
id={update.id}
|
|
998
|
+
onopen={entry ? () => void openDetails(entry) : undefined}
|
|
999
|
+
name={update.name}
|
|
1000
|
+
description={pluginDescription(
|
|
1001
|
+
update.id,
|
|
1002
|
+
`Update available for ${update.name}.`,
|
|
1003
|
+
)}
|
|
1004
|
+
version={`${update.currentVersion} → ${update.targetVersion}`}
|
|
1005
|
+
metadata={[
|
|
1006
|
+
...(releaseDate ? [`Released ${releaseDate}`] : []),
|
|
1007
|
+
...(typeof update.bundleSize === "number"
|
|
1008
|
+
? [`Size ${formatByteSize(update.bundleSize)}`]
|
|
1009
|
+
: []),
|
|
1010
|
+
...(update.targetVersion !== update.latestVersion
|
|
1011
|
+
? [`Latest ${update.latestVersion}`]
|
|
1012
|
+
: []),
|
|
1013
|
+
...(entry && downloadLabel(entry.id)
|
|
1014
|
+
? [downloadLabel(entry.id)!]
|
|
1015
|
+
: []),
|
|
1016
|
+
]}
|
|
1017
|
+
badges={registryRowBadges(entry, update.provenance)}
|
|
1018
|
+
status={{
|
|
1019
|
+
label: update.canUpdate
|
|
1020
|
+
? "Ready"
|
|
1021
|
+
: update.status === "revoked"
|
|
1022
|
+
? "Revoked"
|
|
1023
|
+
: "Incompatible",
|
|
1024
|
+
reason: updateReason(update),
|
|
1025
|
+
tone:
|
|
1026
|
+
update.status === "revoked"
|
|
1027
|
+
? "danger"
|
|
1028
|
+
: update.canUpdate
|
|
1029
|
+
? "success"
|
|
1030
|
+
: "warning",
|
|
1031
|
+
}}
|
|
1032
|
+
>
|
|
1033
|
+
{#snippet actions()}
|
|
1034
|
+
<Tooltip.Provider>
|
|
1035
|
+
<Tooltip.Root>
|
|
1036
|
+
<Tooltip.Trigger>
|
|
1037
|
+
{#snippet child({ props })}
|
|
1038
|
+
<Button.Root
|
|
1039
|
+
{...props}
|
|
1040
|
+
size="icon"
|
|
1041
|
+
variant="outline"
|
|
1042
|
+
aria-label={`Update ${update.name}`}
|
|
1043
|
+
disabled={!update.canUpdate ||
|
|
1044
|
+
registry.runningAction === `update:${update.id}`}
|
|
1045
|
+
onclick={() => void updatePlugin(update)}
|
|
1046
|
+
><ArrowUpCircle /></Button.Root
|
|
1047
|
+
>
|
|
1048
|
+
{/snippet}
|
|
1049
|
+
</Tooltip.Trigger>
|
|
1050
|
+
<Tooltip.Content>Update {update.name}</Tooltip.Content>
|
|
1051
|
+
</Tooltip.Root>
|
|
1052
|
+
</Tooltip.Provider>
|
|
1053
|
+
{/snippet}
|
|
1054
|
+
</PluginRegistryRow>
|
|
1055
|
+
{/each}
|
|
634
1056
|
</div>
|
|
635
|
-
|
|
636
|
-
<Separator.Separator />
|
|
637
|
-
<p class="lapis-plugin-management__empty">Additional third-party registry sources are outside Full Registry V1.</p>
|
|
1057
|
+
{/if}
|
|
638
1058
|
</Tabs.Content>
|
|
639
|
-
</Tabs.Root>
|
|
640
1059
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
{:else}
|
|
726
|
-
<p>No README is available for this registry entry.</p>
|
|
727
|
-
{/if}
|
|
728
|
-
</div>
|
|
729
|
-
{:else}
|
|
730
|
-
<p class="lapis-plugin-management__empty">Select a plugin from the Browse results.</p>
|
|
1060
|
+
<Tabs.Content value="sources" class="lapis-plugin-registry__content">
|
|
1061
|
+
<div class="lapis-plugin-registry__rows">
|
|
1062
|
+
{#each registry.registrySources as sourceState (sourceState.source.id)}
|
|
1063
|
+
{@const source = sourceState.source}
|
|
1064
|
+
{@const isNostr = source.kind === "nostr"}
|
|
1065
|
+
<PluginRegistryRow
|
|
1066
|
+
id={source.id}
|
|
1067
|
+
name={source.name}
|
|
1068
|
+
description={source.url}
|
|
1069
|
+
metadata={[
|
|
1070
|
+
...(source.builtin ? ["Built-in source"] : []),
|
|
1071
|
+
isNostr
|
|
1072
|
+
? `${source.relays?.length ?? 0} relay${source.relays?.length === 1 ? "" : "s"}`
|
|
1073
|
+
: "HTTPS registry",
|
|
1074
|
+
source.trustTier === "official"
|
|
1075
|
+
? "Official source"
|
|
1076
|
+
: "Community source",
|
|
1077
|
+
]}
|
|
1078
|
+
badges={[
|
|
1079
|
+
{ label: source.enabled ? "Enabled" : "Disabled" },
|
|
1080
|
+
...(isNostr
|
|
1081
|
+
? [{ label: "Nostr", tone: "community" as const }]
|
|
1082
|
+
: []),
|
|
1083
|
+
{
|
|
1084
|
+
label:
|
|
1085
|
+
source.trustTier === "official" ? "Official" : "Community",
|
|
1086
|
+
tone:
|
|
1087
|
+
source.trustTier === "official"
|
|
1088
|
+
? ("official" as const)
|
|
1089
|
+
: ("community" as const),
|
|
1090
|
+
},
|
|
1091
|
+
...(source.builtin ? [{ label: "Locked" }] : []),
|
|
1092
|
+
]}
|
|
1093
|
+
status={!source.enabled
|
|
1094
|
+
? { label: "Disabled" }
|
|
1095
|
+
: sourceState.phase === "error"
|
|
1096
|
+
? {
|
|
1097
|
+
label: "Source problem",
|
|
1098
|
+
reason: sourceState.message,
|
|
1099
|
+
tone: "warning",
|
|
1100
|
+
}
|
|
1101
|
+
: isNostr && !sourceState.installReady
|
|
1102
|
+
? {
|
|
1103
|
+
label: "Browse only",
|
|
1104
|
+
reason:
|
|
1105
|
+
sourceState.message ??
|
|
1106
|
+
"Relay install quorum has not been reached",
|
|
1107
|
+
tone: "warning",
|
|
1108
|
+
}
|
|
1109
|
+
: {
|
|
1110
|
+
label: isNostr ? "Install quorum ready" : "Available",
|
|
1111
|
+
tone: "success",
|
|
1112
|
+
}}
|
|
1113
|
+
>
|
|
1114
|
+
{#snippet actions()}
|
|
1115
|
+
{@const sourceToggleAction = `${source.enabled ? "Disable" : "Enable"} ${source.name}`}
|
|
1116
|
+
{#key sourceToggleAction}
|
|
1117
|
+
<Tooltip.Provider>
|
|
1118
|
+
<Tooltip.Root>
|
|
1119
|
+
<Tooltip.Trigger>
|
|
1120
|
+
{#snippet child({ props })}
|
|
1121
|
+
<Switch.Root
|
|
1122
|
+
{...props}
|
|
1123
|
+
checked={source.enabled}
|
|
1124
|
+
disabled={registry.runningAction ===
|
|
1125
|
+
`source:${source.id}`}
|
|
1126
|
+
aria-label={sourceToggleAction}
|
|
1127
|
+
data-tooltip-action={sourceToggleAction}
|
|
1128
|
+
onCheckedChange={(checked) =>
|
|
1129
|
+
void setRegistrySourceEnabled(sourceState, checked)}
|
|
1130
|
+
/>
|
|
1131
|
+
{/snippet}
|
|
1132
|
+
</Tooltip.Trigger>
|
|
1133
|
+
<Tooltip.Content>{sourceToggleAction}</Tooltip.Content>
|
|
1134
|
+
</Tooltip.Root>
|
|
1135
|
+
</Tooltip.Provider>
|
|
1136
|
+
{/key}
|
|
1137
|
+
{#if source.enabled && (sourceState.phase === "error" || !sourceState.installReady)}
|
|
1138
|
+
<Button.Root
|
|
1139
|
+
size="sm"
|
|
1140
|
+
variant="outline"
|
|
1141
|
+
onclick={() => void refresh(true)}
|
|
1142
|
+
><RefreshCw />Retry</Button.Root
|
|
1143
|
+
>
|
|
731
1144
|
{/if}
|
|
732
|
-
|
|
733
|
-
</
|
|
734
|
-
|
|
1145
|
+
{/snippet}
|
|
1146
|
+
</PluginRegistryRow>
|
|
1147
|
+
{/each}
|
|
735
1148
|
</div>
|
|
736
|
-
</
|
|
737
|
-
</
|
|
1149
|
+
</Tabs.Content>
|
|
1150
|
+
</Tabs.Root>
|
|
738
1151
|
|
|
739
1152
|
<AlertDialog.Root bind:open={uninstallConfirmOpen}>
|
|
740
|
-
<AlertDialog.Content
|
|
1153
|
+
<AlertDialog.Content
|
|
1154
|
+
class="lapis-plugin-uninstall-dialog"
|
|
1155
|
+
data-action-mode={uninstallDisableInstead ? "disable" : "uninstall"}
|
|
1156
|
+
>
|
|
741
1157
|
<AlertDialog.Header>
|
|
742
|
-
<AlertDialog.
|
|
743
|
-
|
|
1158
|
+
<AlertDialog.Media class="lapis-plugin-uninstall-dialog__media">
|
|
1159
|
+
<Trash2 aria-hidden="true" />
|
|
1160
|
+
</AlertDialog.Media>
|
|
1161
|
+
<AlertDialog.Title>Uninstall plugin?</AlertDialog.Title>
|
|
1162
|
+
<AlertDialog.Description
|
|
1163
|
+
>{uninstallDisableInstead
|
|
1164
|
+
? "The plugin will stay installed in this vault and can be enabled again later."
|
|
1165
|
+
: "This removes the installed plugin from this vault. You can reinstall it from the registry later."}</AlertDialog.Description
|
|
1166
|
+
>
|
|
744
1167
|
</AlertDialog.Header>
|
|
1168
|
+
{#if uninstallTarget}
|
|
1169
|
+
{@const entry = catalogEntry(uninstallTarget.pluginId)}
|
|
1170
|
+
{@const targetSize = installedSize(uninstallTarget)}
|
|
1171
|
+
<div
|
|
1172
|
+
class="lapis-plugin-uninstall-dialog__target"
|
|
1173
|
+
data-ui-component="plugin-uninstall-target"
|
|
1174
|
+
>
|
|
1175
|
+
<PluginRegistryRow
|
|
1176
|
+
id={uninstallTarget.pluginId}
|
|
1177
|
+
name={installedName(uninstallTarget)}
|
|
1178
|
+
description={installedDescription(uninstallTarget)}
|
|
1179
|
+
version={uninstallTarget.installedVersion}
|
|
1180
|
+
metadata={[
|
|
1181
|
+
...(targetSize === null
|
|
1182
|
+
? []
|
|
1183
|
+
: [`Size ${formatByteSize(targetSize)}`]),
|
|
1184
|
+
]}
|
|
1185
|
+
badges={registryRowBadges(entry, uninstallTarget.provenance)}
|
|
1186
|
+
status={uninstallTarget.revoked
|
|
1187
|
+
? { label: "Revoked", tone: "danger" }
|
|
1188
|
+
: undefined}
|
|
1189
|
+
/>
|
|
1190
|
+
</div>
|
|
1191
|
+
{#if resolveRuntimePlugin(uninstallTarget.pluginId)?.enabled}
|
|
1192
|
+
{@const alternateAction = uninstallDisableInstead
|
|
1193
|
+
? `Choose Uninstall instead for ${installedName(uninstallTarget)}`
|
|
1194
|
+
: `Choose Disable instead for ${installedName(uninstallTarget)}`}
|
|
1195
|
+
<Field.Field
|
|
1196
|
+
orientation="horizontal"
|
|
1197
|
+
class="lapis-plugin-uninstall-dialog__alternative"
|
|
1198
|
+
>
|
|
1199
|
+
<Field.Content>
|
|
1200
|
+
<Field.Title>Disable instead</Field.Title>
|
|
1201
|
+
<Field.Description
|
|
1202
|
+
>Keep the plugin installed and turn it off.</Field.Description
|
|
1203
|
+
>
|
|
1204
|
+
</Field.Content>
|
|
1205
|
+
<Tooltip.Provider>
|
|
1206
|
+
<Tooltip.Root>
|
|
1207
|
+
<Tooltip.Trigger>
|
|
1208
|
+
{#snippet child({ props })}
|
|
1209
|
+
<Switch.Root
|
|
1210
|
+
{...props}
|
|
1211
|
+
bind:checked={uninstallDisableInstead}
|
|
1212
|
+
aria-label={alternateAction}
|
|
1213
|
+
data-tooltip-action={alternateAction}
|
|
1214
|
+
/>
|
|
1215
|
+
{/snippet}
|
|
1216
|
+
</Tooltip.Trigger>
|
|
1217
|
+
<Tooltip.Content>{alternateAction}</Tooltip.Content>
|
|
1218
|
+
</Tooltip.Root>
|
|
1219
|
+
</Tooltip.Provider>
|
|
1220
|
+
</Field.Field>
|
|
1221
|
+
{/if}
|
|
1222
|
+
{/if}
|
|
745
1223
|
<AlertDialog.Footer>
|
|
746
|
-
<AlertDialog.Cancel onclick={
|
|
747
|
-
|
|
1224
|
+
<AlertDialog.Cancel onclick={closeUninstallDialog}
|
|
1225
|
+
>Cancel</AlertDialog.Cancel
|
|
1226
|
+
>
|
|
1227
|
+
<AlertDialog.Action
|
|
1228
|
+
class={uninstallDisableInstead
|
|
1229
|
+
? "lapis-plugin-uninstall-dialog__confirm--disable"
|
|
1230
|
+
: "lapis-plugin-uninstall-dialog__confirm--uninstall"}
|
|
1231
|
+
variant={uninstallDisableInstead ? "default" : "destructive"}
|
|
1232
|
+
data-action-mode={uninstallDisableInstead ? "disable" : "uninstall"}
|
|
1233
|
+
aria-label={uninstallTarget
|
|
1234
|
+
? `${uninstallDisableInstead ? "Disable" : "Uninstall"} ${installedName(uninstallTarget)}`
|
|
1235
|
+
: `${uninstallDisableInstead ? "Disable" : "Uninstall"} plugin`}
|
|
1236
|
+
onclick={() => void confirmPluginRemovalChoice()}
|
|
1237
|
+
>{#if uninstallDisableInstead}<Power
|
|
1238
|
+
data-icon="inline-start"
|
|
1239
|
+
aria-hidden="true"
|
|
1240
|
+
/>Disable plugin{:else}<Trash2
|
|
1241
|
+
data-icon="inline-start"
|
|
1242
|
+
aria-hidden="true"
|
|
1243
|
+
/>Uninstall plugin{/if}</AlertDialog.Action
|
|
1244
|
+
>
|
|
748
1245
|
</AlertDialog.Footer>
|
|
749
1246
|
</AlertDialog.Content>
|
|
750
1247
|
</AlertDialog.Root>
|