@delta-comic/plugin 3.0.0-next.7 → 3.0.0-next.9

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.
@@ -1,70 +1,562 @@
1
- import { A as pluginI18n, B as require, C as findCyclePaths, D as pluginStore, E as PluginStore, F as declareDepType, G as definePlugin, H as useConfig, I as defaultDependencyRegistry, L as exposeSymbol, M as translatePluginText, N as DependencyRegistry, O as usePluginStore, P as createDependencyRegistry, R as pluginExposes, S as synchronizeBuiltInPlugins, T as planPluginLoadOrder, U as ConfigPointer, V as ConfigStore, W as defineInnerPlugin, _ as runtimeExtensions, a as PluginLoader, b as isBuiltInPlugin, c as filterPluginsBySelection, d as loader_exports, f as booter_exports, g as registerLoader, h as registerInstaller, i as PluginInstaller, j as pluginMessageKey, k as PluginI18nRegistry, l as pluginKind, m as registerBooter, n as install_exports, o as pluginRuntime, p as PluginRuntimeExtensions, r as PluginBooter, s as failedDependencies, t as Global, u as selectPluginsForPhase, v as BUILT_IN_PLUGIN_LOADER, w as formatPluginLoadPlanError, x as isBuiltInPluginName, y as builtInDefinitionToArchive, z as provide } from "../global-BSAsAv3E.mjs";
2
- import { t as createPluginAssetUrl } from "../storage-DMKDlPzl.mjs";
1
+ import { t as __exportAll } from "../rolldown-runtime-D7D4PA-g.mjs";
2
+ import { isFunction } from "es-toolkit";
3
+ import { DELTA_COMIC_PLUGIN_API_VERSION, DELTA_COMIC_PLUGIN_API_VERSION as DELTA_COMIC_PLUGIN_API_VERSION$1, UniComment, UniContentPage, UniItem, UniResource, UniUser } from "@delta-comic/model";
4
+ import { markRaw, ref, shallowReactive } from "vue";
5
+ import { isTauri } from "@tauri-apps/api/core";
3
6
  import { logger } from "@delta-comic/logger";
4
- import semver from "semver";
5
7
  import ky from "ky";
6
- //#region lib/manifest.ts
8
+ import semver from "semver";
9
+ import { db, useConfig as useConfig$1 } from "@delta-comic/db";
10
+ import JSZip from "jszip";
11
+ import { Octokit } from "@octokit/rest";
12
+ import { SharedFunction } from "@delta-comic/utils";
13
+ import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
14
+ //#region lib/api/config.ts
15
+ var ConfigPointer = class {
16
+ pluginName;
17
+ config;
18
+ configName;
19
+ key;
20
+ _type = {};
21
+ constructor(pluginName, config, configName) {
22
+ this.pluginName = pluginName;
23
+ this.config = config;
24
+ this.configName = configName;
25
+ this.key = Symbol.for(`config:${pluginName}`);
26
+ }
27
+ };
28
+ //#endregion
29
+ //#region lib/api/model/content.ts
30
+ var content_exports = /* @__PURE__ */ __exportAll({});
31
+ //#endregion
32
+ //#region lib/api/model/expose.ts
33
+ var expose_exports = /* @__PURE__ */ __exportAll({});
34
+ //#endregion
35
+ //#region lib/api/model/remote.ts
36
+ var remote_exports = /* @__PURE__ */ __exportAll({});
37
+ //#endregion
38
+ //#region lib/api/model/resource.ts
39
+ var resource_exports = /* @__PURE__ */ __exportAll({});
40
+ //#endregion
41
+ //#region lib/api/model/social.ts
42
+ var social_exports = /* @__PURE__ */ __exportAll({});
43
+ //#endregion
44
+ //#region lib/api/model/special.ts
45
+ var special_exports = /* @__PURE__ */ __exportAll({});
46
+ //#endregion
47
+ //#region lib/api/model/user.ts
48
+ var user_exports = /* @__PURE__ */ __exportAll({});
49
+ //#endregion
50
+ //#region lib/api/plugin.ts
51
+ const defineDeltaComicPlugin = (config) => {
52
+ if (isFunction(config)) return config;
53
+ return () => config;
54
+ };
55
+ //#endregion
56
+ //#region lib/kernel/candidate.ts
57
+ const defineInternalPlugin = (definition) => definition;
58
+ //#endregion
59
+ //#region lib/kernel/capability.ts
60
+ const defineCapability = (definition) => ({
61
+ id: definition.id,
62
+ async activate(plugin, context) {
63
+ const model = definition.select(plugin);
64
+ if (model === void 0) return false;
65
+ await definition.activate(model, context);
66
+ return true;
67
+ }
68
+ });
69
+ var ActivationPipeline = class {
70
+ #modules;
71
+ constructor(modules) {
72
+ const ids = /* @__PURE__ */ new Set();
73
+ for (const module of modules) {
74
+ if (!module.id) throw new Error("capability id cannot be empty");
75
+ if (ids.has(module.id)) throw new Error(`duplicate capability "${module.id}"`);
76
+ ids.add(module.id);
77
+ }
78
+ this.#modules = [...modules];
79
+ }
80
+ async activate(plugin, context) {
81
+ const activated = [];
82
+ for (const module of this.#modules) {
83
+ if (context.signal.aborted) throw context.signal.reason;
84
+ context.report({
85
+ description: "",
86
+ name: module.id
87
+ });
88
+ if (await module.activate(plugin, context)) activated.push(module.id);
89
+ }
90
+ return activated;
91
+ }
92
+ };
93
+ //#endregion
94
+ //#region lib/kernel/contribution.ts
95
+ const defineContributionChannel = (key) => ({ key });
96
+ const contributionKey = (owner, id) => JSON.stringify([owner, id]);
97
+ var ContributionRegistry = class {
98
+ #entries = shallowReactive(/* @__PURE__ */ new Map());
99
+ get size() {
100
+ return this.#entries.size;
101
+ }
102
+ get entries() {
103
+ return this.#entries;
104
+ }
105
+ register(owner, id, value) {
106
+ if (!owner) throw new Error("contribution owner cannot be empty");
107
+ if (!id) throw new Error("contribution id cannot be empty");
108
+ const key = contributionKey(owner, id);
109
+ if (this.#entries.has(key)) throw new Error(`duplicate contribution "${owner}:${id}"`);
110
+ const contribution = {
111
+ id,
112
+ owner,
113
+ value
114
+ };
115
+ this.#entries.set(key, contribution);
116
+ let active = true;
117
+ return () => {
118
+ if (!active) return false;
119
+ active = false;
120
+ return this.#entries.delete(key);
121
+ };
122
+ }
123
+ get(owner, id) {
124
+ return this.#entries.get(contributionKey(owner, id));
125
+ }
126
+ byOwner(owner) {
127
+ return [...this.#entries.values()].filter((entry) => entry.owner === owner);
128
+ }
129
+ removeOwner(owner) {
130
+ for (const [key, entry] of this.#entries) if (entry.owner === owner) this.#entries.delete(key);
131
+ }
132
+ values() {
133
+ return this.#entries.values();
134
+ }
135
+ };
136
+ var ContributionHub = class {
137
+ registries = /* @__PURE__ */ new Map();
138
+ channel(channel) {
139
+ let registry = this.registries.get(channel.key);
140
+ if (!registry) {
141
+ registry = new ContributionRegistry();
142
+ this.registries.set(channel.key, registry);
143
+ }
144
+ return registry;
145
+ }
146
+ register(scope, channel, id, value) {
147
+ const unregister = this.channel(channel).register(scope.owner, id, value);
148
+ scope.defer(() => void unregister());
149
+ return unregister;
150
+ }
151
+ removeOwner(owner) {
152
+ for (const registry of this.registries.values()) registry.removeOwner(owner);
153
+ }
154
+ };
155
+ //#endregion
156
+ //#region lib/kernel/dependency.ts
157
+ const dependenciesOf = (candidate) => candidate.manifest.require.map((dependency) => dependency.id);
158
+ const canonicalCycleKey = (cycle) => {
159
+ const nodes = cycle.slice(0, -1);
160
+ const rotations = nodes.map((_, index) => nodes.slice(index).concat(nodes.slice(0, index)));
161
+ rotations.sort((left, right) => left.join("\0").localeCompare(right.join("\0")));
162
+ return rotations[0]?.join("\0") ?? "";
163
+ };
164
+ const findPluginDependencyCycles = (candidates) => {
165
+ const candidateIds = new Set(candidates.map((candidate) => candidate.manifest.name.id));
166
+ const dependencies = new Map(candidates.map((candidate) => [candidate.manifest.name.id, dependenciesOf(candidate).filter((dependency) => candidateIds.has(dependency))]));
167
+ const state = /* @__PURE__ */ new Map();
168
+ const path = [];
169
+ const keys = /* @__PURE__ */ new Set();
170
+ const cycles = [];
171
+ const visit = (plugin) => {
172
+ state.set(plugin, "visiting");
173
+ path.push(plugin);
174
+ for (const dependency of dependencies.get(plugin) ?? []) if (state.get(dependency) === "visiting") {
175
+ const start = path.lastIndexOf(dependency);
176
+ if (start < 0) continue;
177
+ const cycle = path.slice(start).concat(dependency);
178
+ const key = canonicalCycleKey(cycle);
179
+ if (!keys.has(key)) {
180
+ keys.add(key);
181
+ cycles.push(cycle);
182
+ }
183
+ } else if (state.get(dependency) !== "visited") visit(dependency);
184
+ path.pop();
185
+ state.set(plugin, "visited");
186
+ };
187
+ for (const candidate of candidates) {
188
+ const id = candidate.manifest.name.id;
189
+ if (!state.has(id)) visit(id);
190
+ }
191
+ return cycles;
192
+ };
193
+ const planPluginDependencies = (candidates) => {
194
+ const byId = new Map(candidates.map((candidate) => [candidate.manifest.name.id, candidate]));
195
+ const degree = /* @__PURE__ */ new Map();
196
+ const dependents = /* @__PURE__ */ new Map();
197
+ const missing = [];
198
+ for (const candidate of candidates) {
199
+ const id = candidate.manifest.name.id;
200
+ const installed = dependenciesOf(candidate).filter((dependency) => {
201
+ if (byId.has(dependency)) return true;
202
+ missing.push({
203
+ dependency,
204
+ plugin: id
205
+ });
206
+ return false;
207
+ });
208
+ degree.set(id, installed.length);
209
+ for (const dependency of installed) {
210
+ const entries = dependents.get(dependency) ?? [];
211
+ entries.push(id);
212
+ dependents.set(dependency, entries);
213
+ }
214
+ }
215
+ const queue = [...degree].filter(([, value]) => value === 0).map(([id]) => id);
216
+ const levels = [];
217
+ while (queue.length > 0) {
218
+ const current = queue.splice(0);
219
+ const level = current.flatMap((id) => {
220
+ const candidate = byId.get(id);
221
+ return candidate ? [candidate] : [];
222
+ });
223
+ if (level.length > 0) levels.push(level);
224
+ for (const id of current) for (const dependent of dependents.get(id) ?? []) {
225
+ const next = (degree.get(dependent) ?? 0) - 1;
226
+ degree.set(dependent, next);
227
+ if (next === 0) queue.push(dependent);
228
+ }
229
+ }
230
+ const unresolved = candidates.filter((candidate) => (degree.get(candidate.manifest.name.id) ?? 0) > 0);
231
+ return {
232
+ cycles: findPluginDependencyCycles(unresolved),
233
+ levels,
234
+ missing
235
+ };
236
+ };
237
+ //#endregion
238
+ //#region lib/kernel/scope.ts
239
+ var PluginScope = class {
240
+ owner;
241
+ #controller = new AbortController();
242
+ #disposers = [];
243
+ #disposePromise;
244
+ constructor(owner) {
245
+ this.owner = owner;
246
+ }
247
+ get signal() {
248
+ return this.#controller.signal;
249
+ }
250
+ get disposed() {
251
+ return this.#disposePromise !== void 0;
252
+ }
253
+ defer(disposer) {
254
+ if (this.disposed) throw new Error(`plugin scope "${this.owner}" is already disposed`);
255
+ this.#disposers.push(disposer);
256
+ return disposer;
257
+ }
258
+ dispose(reason) {
259
+ return this.#disposePromise ??= this.#dispose(reason);
260
+ }
261
+ async #dispose(reason) {
262
+ this.#controller.abort(reason);
263
+ const errors = [];
264
+ for (const disposer of this.#disposers.reverse()) try {
265
+ await disposer();
266
+ } catch (error) {
267
+ errors.push(error);
268
+ }
269
+ this.#disposers.length = 0;
270
+ if (errors.length > 0) throw new AggregateError(errors, `failed to dispose plugin scope "${this.owner}"`);
271
+ }
272
+ };
273
+ //#endregion
274
+ //#region lib/capabilities/auth.ts
275
+ const createAuthCapability = (services) => defineCapability({
276
+ id: "auth",
277
+ select: (config) => config.model?.user?.auth,
278
+ async activate(auth, context) {
279
+ if (services.phase === "preboot") throw new Error("plugin authentication is only available during normal activation");
280
+ if (!services.auth) throw new Error("plugin authentication requires a host auth gateway");
281
+ context.report({
282
+ name: "auth",
283
+ description: "checking authentication"
284
+ });
285
+ await services.auth.authenticate(context.owner, auth, context.signal);
286
+ }
287
+ });
288
+ //#endregion
289
+ //#region lib/capabilities/config.ts
290
+ const createConfigCapability = (services) => defineCapability({
291
+ id: "config",
292
+ select: (config) => config.config,
293
+ async activate(pointer, context) {
294
+ if (pointer.pluginName !== context.scope.owner) throw new Error(`plugin config owner mismatch: ${context.scope.owner} / ${pointer.pluginName}`);
295
+ context.report({ description: pointer.configName });
296
+ const registered = services.config.register(pointer);
297
+ context.scope.defer(() => services.config.unregister(pointer));
298
+ await registered.ready;
299
+ }
300
+ });
301
+ //#endregion
302
+ //#region lib/capabilities/registryBinding.ts
303
+ /** Bind a host registry entry and restore exactly the value that existed before activation. */
304
+ const bindRegistryValue = (scope, registry, key, value) => {
305
+ if (value === void 0) return;
306
+ const hadPrevious = registry.has(key);
307
+ const previous = registry.get(key);
308
+ registry.set(key, value);
309
+ scope.defer(() => {
310
+ if (hadPrevious) registry.set(key, previous);
311
+ else registry.delete(key);
312
+ });
313
+ };
314
+ //#endregion
315
+ //#region lib/capabilities/content.ts
316
+ const createContentCapability = () => defineCapability({
317
+ id: "content-bindings",
318
+ select: (config) => config.model?.content?.models,
319
+ activate(models, context) {
320
+ const names = /* @__PURE__ */ new Set();
321
+ for (const model of models) {
322
+ if (!model.name) throw new Error("content model name cannot be empty");
323
+ if (names.has(model.name)) throw new Error(`duplicate content model "${model.name}"`);
324
+ names.add(model.name);
325
+ const key = [context.owner, model.name];
326
+ bindRegistryValue(context.scope, UniContentPage.layouts, key, model.Layout);
327
+ bindRegistryValue(context.scope, UniItem.itemCards, key, model.ItemCard);
328
+ bindRegistryValue(context.scope, UniContentPage.contentPages, key, model.ContentPage);
329
+ bindRegistryValue(context.scope, UniContentPage.downloadProviders, key, model.DownloadProvider);
330
+ bindRegistryValue(context.scope, UniComment.commentRow, key, model.CommentRow);
331
+ bindRegistryValue(context.scope, UniItem.itemTranslator, key, model.ItemTranslator);
332
+ }
333
+ }
334
+ });
335
+ //#endregion
336
+ //#region lib/capabilities/i18n.ts
337
+ const createI18nCapability = (services) => defineCapability({
338
+ id: "i18n",
339
+ select: (config) => config.i18n,
340
+ activate(messages, context) {
341
+ services.i18n.register(context.scope.owner, messages);
342
+ context.scope.defer(() => services.i18n.remove(context.scope.owner));
343
+ }
344
+ });
345
+ //#endregion
346
+ //#region lib/capabilities/lifecycle.ts
347
+ const createLifecycleCapability = (services) => defineCapability({
348
+ id: "lifecycle",
349
+ select: (config) => {
350
+ const hooks = config.hooks;
351
+ return hooks?.onBooted || hooks?.onPreboot || hooks?.onUnload || hooks?.onUninstall ? hooks : void 0;
352
+ },
353
+ async activate(hooks, context) {
354
+ if (hooks.onUnload) context.scope.defer(() => hooks.onUnload?.());
355
+ if (services.phase === "preboot") {
356
+ if (!services.app) throw new Error("preboot activation requires a Vue app");
357
+ const cleanup = await hooks.onPreboot?.({ app: services.app });
358
+ if (cleanup) context.scope.defer(cleanup);
359
+ return;
360
+ }
361
+ await hooks.onBooted?.();
362
+ }
363
+ });
364
+ //#endregion
365
+ //#region lib/capabilities/channels.ts
366
+ const pluginModelChannels = {
367
+ content: defineContributionChannel("model:content"),
368
+ expose: defineContributionChannel("model:expose"),
369
+ remote: defineContributionChannel("model:remote"),
370
+ resource: defineContributionChannel("model:resource"),
371
+ social: defineContributionChannel("model:social"),
372
+ special: defineContributionChannel("model:special"),
373
+ user: defineContributionChannel("model:user")
374
+ };
375
+ //#endregion
376
+ //#region lib/capabilities/model.ts
377
+ const createModelCapability = (services) => defineCapability({
378
+ id: "model",
379
+ select: (config) => config.model,
380
+ activate(model, context) {
381
+ const register = (channel, value) => {
382
+ if (value !== void 0) services.contributions.register(context.scope, channel, "default", value);
383
+ };
384
+ register(pluginModelChannels.content, model.content);
385
+ register(pluginModelChannels.expose, model.expose);
386
+ register(pluginModelChannels.remote, model.remotes);
387
+ register(pluginModelChannels.resource, model.resource);
388
+ register(pluginModelChannels.social, model.social);
389
+ register(pluginModelChannels.special, model.special);
390
+ register(pluginModelChannels.user, model.user);
391
+ }
392
+ });
393
+ //#endregion
394
+ //#region lib/capabilities/endpointProbe.ts
395
+ const probeOne = async (candidate, parentSignal, timeoutMs, controllers) => {
396
+ const controller = new AbortController();
397
+ controllers.add(controller);
398
+ const relayAbort = () => controller.abort(parentSignal.reason);
399
+ parentSignal.addEventListener("abort", relayAbort, { once: true });
400
+ const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("endpoint probe timed out")), timeoutMs);
401
+ const startedAt = performance.now();
402
+ try {
403
+ await candidate.test(candidate.url, controller.signal);
404
+ return {
405
+ latencyMs: performance.now() - startedAt,
406
+ url: candidate.url,
407
+ value: candidate.value
408
+ };
409
+ } finally {
410
+ clearTimeout(timeout);
411
+ parentSignal.removeEventListener("abort", relayAbort);
412
+ controllers.delete(controller);
413
+ }
414
+ };
415
+ /** Probe independently in parallel and stop remaining attempts after the first reachable endpoint. */
416
+ const selectFastestEndpoint = async (candidates, signal, timeoutMs = 1e4) => {
417
+ signal.throwIfAborted();
418
+ const controllers = /* @__PURE__ */ new Set();
419
+ try {
420
+ return await Promise.any(candidates.map((candidate) => probeOne(candidate, signal, timeoutMs, controllers)));
421
+ } catch (error) {
422
+ signal.throwIfAborted();
423
+ if (error instanceof AggregateError) return void 0;
424
+ throw error;
425
+ } finally {
426
+ for (const controller of controllers) controller.abort(/* @__PURE__ */ new Error("another endpoint was selected"));
427
+ }
428
+ };
429
+ //#endregion
430
+ //#region lib/capabilities/remote.ts
431
+ const pluginRemoteSelectionChannel = defineContributionChannel("runtime:remote-selection");
432
+ const createRemoteCapability = (services) => defineCapability({
433
+ id: "remote",
434
+ select: (config) => config.model?.remotes ? {
435
+ hooks: config.hooks,
436
+ remotes: config.model.remotes
437
+ } : void 0,
438
+ async activate({ hooks, remotes }, context) {
439
+ const groups = /* @__PURE__ */ new Set();
440
+ for (const group of remotes) {
441
+ if (!group.name) throw new Error("remote group name cannot be empty");
442
+ if (groups.has(group.name)) throw new Error(`duplicate remote group "${group.name}"`);
443
+ groups.add(group.name);
444
+ context.report({
445
+ name: "remote",
446
+ description: `probing ${group.name}`
447
+ });
448
+ const selected = await selectFastestEndpoint(group.remotes.map((remote) => ({
449
+ test: remote.test ?? group.test,
450
+ url: remote.url,
451
+ value: remote
452
+ })), context.signal);
453
+ if (!selected && !group.allowNoConnected) throw new Error(`no reachable endpoint for remote group "${group.name}"`);
454
+ const selection = {
455
+ group,
456
+ latencyMs: selected?.latencyMs,
457
+ remote: selected?.value ?? false
458
+ };
459
+ services.contributions.register(context.scope, pluginRemoteSelectionChannel, group.name, selection);
460
+ hooks?.onRemoteTestDone?.(group, selection.remote);
461
+ }
462
+ }
463
+ });
464
+ //#endregion
465
+ //#region lib/capabilities/resource.ts
466
+ const createResourceCapability = () => defineCapability({
467
+ id: "resource",
468
+ select: (config) => config.model?.resource,
469
+ async activate(resource, context) {
470
+ const names = /* @__PURE__ */ new Set();
471
+ for (const type of resource.types ?? []) {
472
+ if (!type.type) throw new Error("resource type cannot be empty");
473
+ if (names.has(type.type)) throw new Error(`duplicate resource type "${type.type}"`);
474
+ names.add(type.type);
475
+ const key = [context.owner, type.type];
476
+ bindRegistryValue(context.scope, UniResource.fork, key, type);
477
+ context.report({
478
+ name: "resource",
479
+ description: `probing ${type.type}`
480
+ });
481
+ const selected = await selectFastestEndpoint(type.urls.map((url) => ({
482
+ test: type.test,
483
+ url,
484
+ value: url
485
+ })), context.signal);
486
+ if (!selected) throw new Error(`no reachable endpoint for resource "${type.type}"`);
487
+ bindRegistryValue(context.scope, UniResource.precedenceFork, key, selected.url);
488
+ }
489
+ for (const [name, process] of Object.entries(resource.process ?? {})) {
490
+ if (!name) throw new Error("resource process name cannot be empty");
491
+ bindRegistryValue(context.scope, UniResource.processInstances, [context.owner, name], process);
492
+ }
493
+ }
494
+ });
495
+ //#endregion
496
+ //#region lib/capabilities/special.ts
497
+ const createSpecialCapability = () => defineCapability({
498
+ id: "special",
499
+ select: (config) => config.model?.special,
500
+ async activate(steps, context) {
501
+ for (const step of steps) {
502
+ context.signal.throwIfAborted();
503
+ if (!step.name) throw new Error("special step name cannot be empty");
504
+ context.report({
505
+ name: step.name,
506
+ description: ""
507
+ });
508
+ await step.call((description) => context.report({
509
+ name: step.name,
510
+ description
511
+ }));
512
+ }
513
+ }
514
+ });
515
+ //#endregion
516
+ //#region lib/capabilities/user.ts
517
+ const createUserCapability = () => defineCapability({
518
+ id: "user-bindings",
519
+ select: (config) => config.model?.user,
520
+ activate(user, context) {
521
+ bindRegistryValue(context.scope, UniUser.userCards, context.owner, user.card);
522
+ bindRegistryValue(context.scope, UniUser.userEditorBase, context.owner, user.edit);
523
+ }
524
+ });
525
+ //#endregion
526
+ //#region lib/capabilities/index.ts
527
+ /** Fixed host-owned activation topology. Third-party plugins only provide data to it. */
528
+ const createDefaultCapabilities = (services) => [
529
+ createConfigCapability(services),
530
+ createI18nCapability(services),
531
+ createModelCapability(services),
532
+ createContentCapability(),
533
+ createUserCapability(),
534
+ createResourceCapability(),
535
+ createRemoteCapability(services),
536
+ createAuthCapability(services),
537
+ createSpecialCapability(),
538
+ createLifecycleCapability(services)
539
+ ];
540
+ //#endregion
541
+ //#region lib/install/manifest.ts
7
542
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8
- const requiredRecord = (value, path) => {
543
+ const record$1 = (value, path) => {
9
544
  if (!isRecord$1(value)) throw new PluginManifestError(`${path} must be an object`);
10
545
  return value;
11
546
  };
12
- const requiredString = (value, path) => {
547
+ const text = (value, path) => {
13
548
  if (typeof value !== "string" || value.length === 0) throw new PluginManifestError(`${path} must be a non-empty string`);
14
549
  return value;
15
550
  };
16
- const optionalPath = (value, path) => {
17
- const text = requiredString(value, path);
18
- const normalized = text.replaceAll("\\", "/");
19
- if (normalized.startsWith("/") || normalized.split("/").includes("..")) throw new PluginManifestError(`${path} must be a safe relative path`);
20
- return text;
551
+ const pluginId = (value, path) => {
552
+ const id = text(value, path);
553
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(id)) throw new PluginManifestError(`${path} must be a portable 1-64 character plugin identifier`);
554
+ return id;
21
555
  };
22
- const decodeLocalIconPath = (value, path) => {
23
- let decoded = value;
24
- for (let index = 0; index < 5; index += 1) {
25
- let next;
26
- try {
27
- next = decodeURIComponent(decoded);
28
- } catch {
29
- throw new PluginManifestError(`${path} must be a valid URL path`);
30
- }
31
- if (next === decoded) break;
32
- decoded = next;
33
- }
34
- const normalized = decoded.replaceAll("\\", "/");
35
- const segments = normalized.split("/");
36
- if (!normalized || normalized.startsWith("/") || /^[a-z]:($|\/)/i.test(normalized) || normalized.includes("\0") || segments.some((segment) => segment === "..")) throw new PluginManifestError(`${path} must be a safe relative path`);
37
- return segments.filter((segment) => segment && segment !== ".").join("/");
38
- };
39
- /**
40
- * Accepts a credential-free HTTP(S) URL or a safe path inside the plugin archive.
41
- * Query strings and fragments are kept for both forms.
42
- */
43
- const parsePluginIconReference = (value, path = "manifest.icon") => {
44
- const text = requiredString(value, path).trim();
45
- if (!text) throw new PluginManifestError(`${path} must be a non-empty string`);
46
- if (/^[a-z][a-z\d+.-]*:/i.test(text) || text.startsWith("//")) {
47
- let url;
48
- try {
49
- url = new URL(text);
50
- } catch {
51
- throw new PluginManifestError(`${path} must be an HTTP(S) URL or a safe relative path`);
52
- }
53
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new PluginManifestError(`${path} must be a credential-free HTTP(S) URL or a safe relative path`);
54
- return {
55
- type: "remote",
56
- url: text
57
- };
58
- }
59
- const rawPath = text.split(/[?#]/, 1)[0] ?? "";
60
- const resolvedPath = decodeLocalIconPath(rawPath, path);
61
- if (!resolvedPath) throw new PluginManifestError(`${path} must be a safe relative path`);
62
- const fragmentIndex = text.indexOf("#");
63
- return {
64
- fragment: fragmentIndex < 0 ? "" : text.slice(fragmentIndex),
65
- path: resolvedPath,
66
- type: "local"
67
- };
556
+ const safePluginPath = (value, path) => {
557
+ const normalized = text(value, path).replaceAll("\\", "/");
558
+ if (normalized.startsWith("/") || /^[a-z]:($|\/)/i.test(normalized) || normalized.includes("\0") || normalized.split("/").some((segment) => segment === "..")) throw new PluginManifestError(`${path} must be a safe relative path`);
559
+ return normalized.split("/").filter((segment) => segment && segment !== ".").join("/");
68
560
  };
69
561
  var PluginManifestError = class extends Error {
70
562
  constructor(message) {
@@ -72,49 +564,51 @@ var PluginManifestError = class extends Error {
72
564
  this.name = "PluginManifestError";
73
565
  }
74
566
  };
75
- /**
76
- * Validates the exact manifest format emitted by the `deltaComic` Vite plugin.
77
- * The returned value is safe to use as the persisted plugin metadata shape.
78
- */
567
+ const pluginIcon = (value) => {
568
+ const icon = text(value, "manifest.icon").trim();
569
+ if (!/^[a-z][a-z\d+.-]*:/i.test(icon)) return safePluginPath(icon, "manifest.icon");
570
+ let url;
571
+ try {
572
+ url = new URL(icon);
573
+ } catch {
574
+ throw new PluginManifestError("manifest.icon must be an HTTP(S) URL or a safe relative path");
575
+ }
576
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new PluginManifestError("manifest.icon must be a credential-free HTTP(S) URL or a safe relative path");
577
+ return icon;
578
+ };
79
579
  const parsePluginManifest = (value) => {
80
- const manifest = requiredRecord(value, "manifest");
81
- const name = requiredRecord(manifest.name, "manifest.name");
82
- const version = requiredRecord(manifest.version, "manifest.version");
83
- const id = requiredString(name.id, "manifest.name.id");
84
- if (id === "." || id === ".." || /[\\/]/.test(id)) throw new PluginManifestError("manifest.name.id contains an unsafe path segment");
85
- const requireValue = manifest.require;
86
- if (!Array.isArray(requireValue)) throw new PluginManifestError("manifest.require must be an array");
87
- const require = requireValue.map((dependency, index) => {
88
- const record = requiredRecord(dependency, `manifest.require[${index}]`);
89
- const download = record.download;
90
- if (download !== void 0 && typeof download !== "string") throw new PluginManifestError(`manifest.require[${index}].download must be a string`);
91
- return {
92
- id: requiredString(record.id, `manifest.require[${index}].id`),
93
- ...download === void 0 ? {} : { download }
94
- };
95
- });
580
+ const manifest = record$1(value, "manifest");
581
+ if (manifest.apiVersion !== DELTA_COMIC_PLUGIN_API_VERSION$1) throw new PluginManifestError(`manifest.apiVersion must be ${DELTA_COMIC_PLUGIN_API_VERSION$1}`);
582
+ const name = record$1(manifest.name, "manifest.name");
583
+ const version = record$1(manifest.version, "manifest.version");
584
+ const id = pluginId(name.id, "manifest.name.id");
585
+ if (!Array.isArray(manifest.require)) throw new PluginManifestError("manifest.require must be an array");
96
586
  const result = {
97
- author: requiredString(manifest.author, "manifest.author"),
98
- description: requiredString(manifest.description, "manifest.description"),
587
+ apiVersion: DELTA_COMIC_PLUGIN_API_VERSION$1,
588
+ author: text(manifest.author, "manifest.author"),
589
+ description: text(manifest.description, "manifest.description"),
99
590
  name: {
100
- display: requiredString(name.display, "manifest.name.display"),
591
+ display: text(name.display, "manifest.name.display"),
101
592
  id
102
593
  },
103
- require,
594
+ require: manifest.require.map((value, index) => {
595
+ const dependency = record$1(value, `manifest.require[${index}]`);
596
+ return {
597
+ id: pluginId(dependency.id, `manifest.require[${index}].id`),
598
+ ...dependency.download === void 0 ? {} : { download: text(dependency.download, `manifest.require[${index}].download`) }
599
+ };
600
+ }),
104
601
  version: {
105
- plugin: requiredString(version.plugin, "manifest.version.plugin"),
106
- supportCore: requiredString(version.supportCore, "manifest.version.supportCore")
602
+ plugin: text(version.plugin, "manifest.version.plugin"),
603
+ supportCore: text(version.supportCore, "manifest.version.supportCore")
107
604
  }
108
605
  };
109
- if (manifest.icon !== void 0) {
110
- const icon = parsePluginIconReference(manifest.icon);
111
- result.icon = icon.type === "remote" ? icon.url : requiredString(manifest.icon, "manifest.icon").trim();
112
- }
606
+ if (manifest.icon !== void 0) result.icon = pluginIcon(manifest.icon);
113
607
  if (manifest.entry !== void 0) {
114
- const entry = requiredRecord(manifest.entry, "manifest.entry");
608
+ const entry = record$1(manifest.entry, "manifest.entry");
115
609
  result.entry = {
116
- jsPath: optionalPath(entry.jsPath, "manifest.entry.jsPath"),
117
- ...entry.cssPath === void 0 ? {} : { cssPath: optionalPath(entry.cssPath, "manifest.entry.cssPath") }
610
+ jsPath: safePluginPath(entry.jsPath, "manifest.entry.jsPath"),
611
+ ...entry.cssPath === void 0 ? {} : { cssPath: safePluginPath(entry.cssPath, "manifest.entry.cssPath") }
118
612
  };
119
613
  }
120
614
  if (manifest.kind !== void 0) {
@@ -122,23 +616,20 @@ const parsePluginManifest = (value) => {
122
616
  result.kind = manifest.kind;
123
617
  }
124
618
  if (manifest.integrity !== void 0) {
125
- const integrity = requiredRecord(manifest.integrity, "manifest.integrity");
619
+ const integrity = record$1(manifest.integrity, "manifest.integrity");
126
620
  if (integrity.algorithm !== "blake3" && integrity.algorithm !== "sha256") throw new PluginManifestError("manifest.integrity.algorithm is unsupported");
127
621
  result.integrity = {
128
622
  algorithm: integrity.algorithm,
129
- digest: requiredString(integrity.digest, "manifest.integrity.digest")
623
+ digest: text(integrity.digest, "manifest.integrity.digest")
130
624
  };
131
625
  }
132
626
  return result;
133
627
  };
134
628
  const isPluginManifestCompatible = (manifest, coreVersion) => semver.satisfies(coreVersion, manifest.version.supportCore);
135
- //#endregion
136
- //#region lib/marketplace/types.ts
137
- const AWESOME_REGISTRY_BASE_URL = "https://raw.githubusercontent.com/delta-comic/awesome-plugins/main/";
138
629
  const AWESOME_REGISTRY_INDEX_PATH = "registry/index.json";
139
- const AWESOME_REGISTRY_SCHEMA_VERSION = 1;
140
630
  //#endregion
141
- //#region lib/marketplace/validation.ts
631
+ //#region lib/adapters/awesomeRegistry/schema.ts
632
+ /** Paths are validated before they are resolved against the configured registry origin. */
142
633
  const PAGE_PATH_PATTERN = /^registry\/pages\/[1-9][0-9]*\.json$/;
143
634
  const PLUGIN_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
144
635
  const GITHUB_LOGIN_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
@@ -358,7 +849,7 @@ const parseAwesomeRegistryPage = (value) => {
358
849
  };
359
850
  const assertAwesomeRegistryPagePath = (value) => path(value, "page path");
360
851
  //#endregion
361
- //#region lib/marketplace/cache.ts
852
+ //#region lib/adapters/awesomeRegistry/cache.ts
362
853
  const parseEnvelope = (value) => {
363
854
  try {
364
855
  const envelope = JSON.parse(value);
@@ -431,13 +922,14 @@ var AwesomeRegistryCache = class {
431
922
  }
432
923
  };
433
924
  //#endregion
434
- //#region lib/marketplace/client.ts
925
+ //#region lib/adapters/awesomeRegistry/client.ts
435
926
  const marketplaceLogger = logger.scoped("plugin:marketplace");
436
- const defaultRequestJson = async (url) => await ky.get(url, {
927
+ const defaultRequestJson = async (url, signal) => await ky.get(url, {
437
928
  retry: 2,
929
+ signal,
438
930
  timeout: 3e4
439
931
  }).json();
440
- const defaultStorage = () => {
932
+ const defaultStorage$1 = () => {
441
933
  try {
442
934
  return globalThis.localStorage;
443
935
  } catch {
@@ -452,27 +944,56 @@ var AwesomeRegistryNetworkError = class extends Error {
452
944
  this.name = "AwesomeRegistryNetworkError";
453
945
  }
454
946
  };
947
+ const catalogIndex = (index) => ({
948
+ pageSize: index.pageSize,
949
+ pages: index.pages,
950
+ totalItems: index.totalItems,
951
+ totalPages: index.totalPages
952
+ });
953
+ const catalogListing = (listing) => ({
954
+ authors: listing.authors,
955
+ id: listing.id,
956
+ ...listing.release ? { release: listing.release } : {},
957
+ ...listing.repository ? { repository: listing.repository } : {},
958
+ source: listing.download
959
+ });
960
+ const catalogPage = (page) => ({
961
+ items: page.items.map(catalogListing),
962
+ pagination: page.pagination
963
+ });
455
964
  var AwesomeRegistryClient = class {
456
965
  baseUrl;
457
966
  cache;
458
967
  requestJson;
459
968
  constructor(options = {}) {
460
969
  this.baseUrl = new URL(options.baseUrl ?? "https://raw.githubusercontent.com/delta-comic/awesome-plugins/main/").href;
461
- this.cache = options.cache ?? new AwesomeRegistryCache(options.storage ?? defaultStorage());
970
+ this.cache = options.cache ?? new AwesomeRegistryCache(options.storage ?? defaultStorage$1());
462
971
  this.requestJson = options.requestJson ?? defaultRequestJson;
463
972
  }
464
- async loadIndex() {
465
- return await this.load(AWESOME_REGISTRY_INDEX_PATH, parseAwesomeRegistryIndex, () => this.cache.readIndex(), (data) => this.cache.writeIndex(data));
973
+ async loadIndex(signal) {
974
+ const result = await this.load(AWESOME_REGISTRY_INDEX_PATH, parseAwesomeRegistryIndex, () => this.cache.readIndex(), (data) => this.cache.writeIndex(data), signal);
975
+ return {
976
+ ...result,
977
+ data: catalogIndex(result.data)
978
+ };
466
979
  }
467
- async loadPage(path) {
980
+ async loadPage(path, signal) {
468
981
  const safePath = assertAwesomeRegistryPagePath(path);
469
- return await this.load(safePath, parseAwesomeRegistryPage, () => this.cache.readPage(safePath), (data) => this.cache.writePage(safePath, data));
982
+ const result = await this.load(safePath, parseAwesomeRegistryPage, () => this.cache.readPage(safePath), (data) => this.cache.writePage(safePath, data), signal);
983
+ return {
984
+ ...result,
985
+ data: catalogPage(result.data)
986
+ };
470
987
  }
471
- async findListing(id) {
988
+ async resolveInstallInput(id, signal) {
989
+ const listing = await this.findListing(id, signal);
990
+ return listing.source.type === "github" ? `gh:${listing.source.repository}` : listing.source.url;
991
+ }
992
+ async findListing(id, signal) {
472
993
  marketplaceLogger.debug("searching marketplace listing", { plugin: id });
473
- const { data: index } = await this.loadIndex();
994
+ const { data: index } = await this.loadIndex(signal);
474
995
  for (const pageReference of index.pages) {
475
- const { data: page } = await this.loadPage(pageReference.path);
996
+ const { data: page } = await this.loadPage(pageReference.path, signal);
476
997
  const listing = page.items.find((item) => item.id === id);
477
998
  if (listing) {
478
999
  marketplaceLogger.debug("marketplace listing found", { plugin: id });
@@ -481,18 +1002,19 @@ var AwesomeRegistryClient = class {
481
1002
  }
482
1003
  throw new Error(`Plugin "${id}" is not registered in awesome-plugins`);
483
1004
  }
484
- async loadManifest(listing) {
1005
+ async loadManifest(listing, signal) {
485
1006
  const manifestUrl = listing.release?.manifestUrl;
486
1007
  if (!manifestUrl) return void 0;
487
- const manifest = parsePluginManifest(await this.requestJson(manifestUrl));
1008
+ const manifest = parsePluginManifest(await this.requestJson(manifestUrl, signal));
488
1009
  if (manifest.name.id !== listing.id) throw new AwesomeRegistryValidationError(`listing ${listing.id} points to manifest for ${manifest.name.id}`);
489
1010
  return manifest;
490
1011
  }
491
- async load(path, parse, readCache, writeCache) {
1012
+ async load(path, parse, readCache, writeCache, signal) {
492
1013
  let payload;
493
1014
  try {
494
- payload = await this.requestJson(new URL(path, this.baseUrl).href);
1015
+ payload = await this.requestJson(new URL(path, this.baseUrl).href, signal);
495
1016
  } catch (error) {
1017
+ if (signal?.aborted) throw signal.reason;
496
1018
  if (error instanceof AwesomeRegistryValidationError || error instanceof SyntaxError) throw error;
497
1019
  const cached = readCache();
498
1020
  if (cached) {
@@ -515,21 +1037,1375 @@ var AwesomeRegistryClient = class {
515
1037
  }
516
1038
  };
517
1039
  //#endregion
518
- //#region lib/marketplace/index.ts
519
- const marketplaceDownloadToInstallInput = (download) => download.type === "github" ? `gh:${download.repository}` : download.url;
520
- const marketplaceListingInstallId = (listing) => `ap:${listing.id}`;
521
- const marketplaceListingSource = (listing) => listing.download.type === "github" ? `https://github.com/${listing.download.repository}` : listing.download.url;
1040
+ //#region lib/adapters/configStore.ts
1041
+ const loadDatabaseConfig = (pointer) => {
1042
+ const store = useConfig$1(pointer.pluginName, pointer.config);
1043
+ return {
1044
+ data: store,
1045
+ form: pointer.config,
1046
+ name: pointer.configName,
1047
+ ready: store.ready
1048
+ };
1049
+ };
1050
+ var ConfigStore = class {
1051
+ loadConfig;
1052
+ entries = shallowReactive(/* @__PURE__ */ new Map());
1053
+ pointers = /* @__PURE__ */ new Map();
1054
+ isSystemDark = globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
1055
+ constructor(loadConfig = loadDatabaseConfig) {
1056
+ this.loadConfig = loadConfig;
1057
+ }
1058
+ get form() {
1059
+ return this.entries;
1060
+ }
1061
+ get isDark() {
1062
+ const pointer = this.pointers.get("core");
1063
+ if (!pointer) return this.isSystemDark;
1064
+ const mode = this.load(pointer).data.value.darkMode;
1065
+ if (mode === "light") return false;
1066
+ if (mode === "dark") return true;
1067
+ return this.isSystemDark;
1068
+ }
1069
+ load(pointer) {
1070
+ const value = this.entries.get(pointer.key);
1071
+ if (!value) throw new Error(`not found config by plugin "${pointer.pluginName}"`);
1072
+ return value;
1073
+ }
1074
+ has(pointer) {
1075
+ return this.entries.has(pointer.key);
1076
+ }
1077
+ register(pointer) {
1078
+ const registered = this.entries.get(pointer.key);
1079
+ const ownerPointer = this.pointers.get(pointer.pluginName);
1080
+ if (registered && ownerPointer === pointer) return registered;
1081
+ if (ownerPointer) throw new Error(`plugin "${pointer.pluginName}" can only register one config`);
1082
+ const saved = this.loadConfig(pointer);
1083
+ this.entries.set(pointer.key, saved);
1084
+ this.pointers.set(pointer.pluginName, pointer);
1085
+ return saved;
1086
+ }
1087
+ unregister(pointer) {
1088
+ if (this.pointers.get(pointer.pluginName) !== pointer) return;
1089
+ this.pointers.delete(pointer.pluginName);
1090
+ this.entries.delete(pointer.key);
1091
+ }
1092
+ };
1093
+ //#endregion
1094
+ //#region lib/install/candidateProvider.ts
1095
+ /** Normalize persisted archives into the same candidate protocol used by internal plugins. */
1096
+ var InstalledPluginCandidateProvider = class {
1097
+ repository;
1098
+ reader;
1099
+ id = "installed";
1100
+ constructor(repository, reader) {
1101
+ this.repository = repository;
1102
+ this.reader = reader;
1103
+ }
1104
+ async list(signal) {
1105
+ const archives = await this.repository.list();
1106
+ signal.throwIfAborted();
1107
+ return archives.map((archive) => ({
1108
+ enabled: archive.enable,
1109
+ load: async (loadSignal) => await this.reader.read(archive.pluginName, archive.meta, loadSignal),
1110
+ management: {
1111
+ canDisable: true,
1112
+ canUninstall: true,
1113
+ canUpdate: archive.installInput.length > 0
1114
+ },
1115
+ manifest: archive.meta,
1116
+ origin: "installed"
1117
+ }));
1118
+ }
1119
+ };
1120
+ //#endregion
1121
+ //#region lib/install/catalog.ts
1122
+ const catalogInstallInputPattern = /^ap:([A-Za-z0-9][A-Za-z0-9_-]{0,63})$/;
1123
+ const pluginCatalogInstallInput = (plugin) => {
1124
+ const input = `ap:${plugin}`;
1125
+ if (!catalogInstallInputPattern.test(input)) throw new TypeError(`invalid plugin catalog id: ${plugin}`);
1126
+ return input;
1127
+ };
1128
+ const pluginCatalogIdFromInstallInput = (input) => typeof input === "string" ? catalogInstallInputPattern.exec(input)?.[1] : void 0;
522
1129
  //#endregion
523
- //#region lib/pluginIcon.ts
524
- /** Resolves persisted plugin icon metadata into a URL that an `<img>` can consume. */
525
- const resolvePluginIconUrl = async (pluginId, icon) => {
526
- if (icon === void 0) return void 0;
527
- const reference = parsePluginIconReference(icon, "plugin icon");
528
- if (reference.type === "remote") return reference.url;
529
- if (!pluginId) throw new Error("A plugin id is required to resolve a local plugin icon");
530
- return `${await createPluginAssetUrl(pluginId, reference.path)}${reference.fragment}`;
1130
+ //#region lib/install/codec.ts
1131
+ const sha256 = async (bytes) => {
1132
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", Uint8Array.from(bytes).buffer);
1133
+ return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
1134
+ };
1135
+ const withIntegrity = async (manifest, bytes) => ({
1136
+ ...manifest,
1137
+ integrity: {
1138
+ algorithm: "sha256",
1139
+ digest: await sha256(bytes)
1140
+ }
1141
+ });
1142
+ var ZipPackageCodec = class {
1143
+ id = "zip";
1144
+ matches(file) {
1145
+ return file.name.toLowerCase().endsWith(".zip") || file.type === "application/zip";
1146
+ }
1147
+ async decode(file, signal) {
1148
+ const bytes = new Uint8Array(await file.arrayBuffer());
1149
+ if (signal.aborted) throw signal.reason;
1150
+ const archive = await JSZip.loadAsync(bytes);
1151
+ const manifestFile = archive.file("manifest.json");
1152
+ if (!manifestFile) throw new Error("plugin archive does not contain manifest.json");
1153
+ const manifest = await withIntegrity(parsePluginManifest(JSON.parse(await manifestFile.async("text"))), bytes);
1154
+ const files = /* @__PURE__ */ new Map();
1155
+ for (const entry of Object.values(archive.files)) {
1156
+ if (signal.aborted) throw signal.reason;
1157
+ if (entry.dir) continue;
1158
+ const path = safePluginPath(entry.name, `archive entry ${entry.name}`);
1159
+ files.set(path, await entry.async("uint8array"));
1160
+ }
1161
+ return {
1162
+ codecId: this.id,
1163
+ files,
1164
+ manifest
1165
+ };
1166
+ }
1167
+ };
1168
+ const description = "@description";
1169
+ var DevScriptCodec = class {
1170
+ id = "dev-script";
1171
+ matches(file) {
1172
+ return /\.(?:js|mjs|user\.js)$/i.test(file.name);
1173
+ }
1174
+ async decode(file, signal) {
1175
+ const bytes = new Uint8Array(await file.arrayBuffer());
1176
+ if (signal.aborted) throw signal.reason;
1177
+ const code = new TextDecoder().decode(bytes);
1178
+ const start = code.indexOf(description);
1179
+ if (start < 0) throw new Error("development plugin does not contain @description metadata");
1180
+ const [line] = code.slice(start + 12).trimStart().split(/\r?\n/, 1);
1181
+ const manifest = await withIntegrity(parsePluginManifest(JSON.parse(line)), bytes);
1182
+ return {
1183
+ codecId: this.id,
1184
+ files: /* @__PURE__ */ new Map([["index.mjs", bytes]]),
1185
+ manifest: {
1186
+ ...manifest,
1187
+ entry: {
1188
+ ...manifest.entry,
1189
+ jsPath: "index.mjs"
1190
+ }
1191
+ }
1192
+ };
1193
+ }
1194
+ };
1195
+ //#endregion
1196
+ //#region lib/install/moduleReader.ts
1197
+ const asFactory = (value, plugin) => {
1198
+ if (typeof value !== "function") throw new TypeError(`plugin entry has no default factory: ${plugin}`);
1199
+ return value;
1200
+ };
1201
+ var StoredPluginModuleReader = class {
1202
+ files;
1203
+ constructor(files) {
1204
+ this.files = files;
1205
+ }
1206
+ async read(plugin, manifest, signal) {
1207
+ const entry = manifest.entry?.jsPath ?? "index.mjs";
1208
+ const url = await this.files.createModuleUrl(plugin, entry);
1209
+ if (signal.aborted) {
1210
+ this.files.release(plugin);
1211
+ throw signal.reason;
1212
+ }
1213
+ let style;
1214
+ try {
1215
+ const module = await import(
1216
+ /* @vite-ignore */
1217
+ url
1218
+ );
1219
+ signal.throwIfAborted();
1220
+ if (manifest.entry?.cssPath && typeof document !== "undefined") {
1221
+ style = document.createElement("style");
1222
+ style.dataset.plugin = plugin;
1223
+ style.textContent = new TextDecoder().decode(await this.files.read(plugin, manifest.entry.cssPath));
1224
+ signal.throwIfAborted();
1225
+ document.head.append(style);
1226
+ }
1227
+ return {
1228
+ factory: asFactory(module.default, plugin),
1229
+ dispose: () => {
1230
+ style?.remove();
1231
+ this.files.release(plugin);
1232
+ }
1233
+ };
1234
+ } catch (error) {
1235
+ style?.remove();
1236
+ this.files.release(plugin);
1237
+ throw error;
1238
+ }
1239
+ }
1240
+ };
1241
+ //#endregion
1242
+ //#region lib/install/repository.ts
1243
+ var DatabasePluginArchiveRepository = class {
1244
+ async find(plugin) {
1245
+ return await db.selectFrom("plugin").selectAll().where("pluginName", "=", plugin).executeTakeFirst();
1246
+ }
1247
+ async list() {
1248
+ return await db.selectFrom("plugin").selectAll().execute();
1249
+ }
1250
+ async remove(plugin) {
1251
+ await db.deleteFrom("plugin").where("pluginName", "=", plugin).execute();
1252
+ }
1253
+ async upsert(archive) {
1254
+ await db.replaceInto("plugin").values({
1255
+ ...archive,
1256
+ meta: JSON.stringify(archive.meta)
1257
+ }).execute();
1258
+ }
1259
+ };
1260
+ //#endregion
1261
+ //#region lib/install/service.ts
1262
+ var PluginInstallService = class {
1263
+ options;
1264
+ constructor(options) {
1265
+ this.options = options;
1266
+ }
1267
+ async install(input, signal = new AbortController().signal, report = () => {}) {
1268
+ report({
1269
+ phase: "resolve",
1270
+ progress: 0
1271
+ });
1272
+ const resolver = this.options.resolvers.find((candidate) => candidate.matches(input));
1273
+ if (!resolver) throw new Error("no plugin source resolver accepts this input");
1274
+ const source = await resolver.resolve(input, signal);
1275
+ report({
1276
+ description: source.file.name,
1277
+ phase: "resolve",
1278
+ progress: 100
1279
+ });
1280
+ const codec = this.options.codecs.find((candidate) => candidate.matches(source.file));
1281
+ if (!codec) throw new Error("no plugin package codec accepts this file");
1282
+ report({
1283
+ description: codec.id,
1284
+ phase: "decode",
1285
+ progress: 0
1286
+ });
1287
+ const decoded = await codec.decode(source.file, signal);
1288
+ const plugin = decoded.manifest.name.id;
1289
+ if (this.options.reservedIds?.has(plugin)) throw new Error(`plugin id "${plugin}" is reserved by an internal plugin`);
1290
+ report({
1291
+ description: plugin,
1292
+ phase: "decode",
1293
+ progress: 100
1294
+ });
1295
+ const previous = await this.options.repository.find(plugin);
1296
+ const replacement = await this.options.files.replace(plugin, decoded.files);
1297
+ const archive = {
1298
+ displayName: decoded.manifest.name.display,
1299
+ enable: previous?.enable ?? true,
1300
+ installerName: source.resolverId,
1301
+ installInput: source.installInput,
1302
+ loaderName: decoded.codecId,
1303
+ meta: decoded.manifest,
1304
+ pluginName: plugin
1305
+ };
1306
+ try {
1307
+ report({
1308
+ description: plugin,
1309
+ phase: "persist",
1310
+ progress: 50
1311
+ });
1312
+ await this.options.repository.upsert(archive);
1313
+ await replacement.commit();
1314
+ report({
1315
+ description: plugin,
1316
+ phase: "persist",
1317
+ progress: 100
1318
+ });
1319
+ return archive;
1320
+ } catch (error) {
1321
+ const rollbackErrors = [];
1322
+ try {
1323
+ await replacement.rollback();
1324
+ } catch (rollbackError) {
1325
+ rollbackErrors.push(rollbackError);
1326
+ }
1327
+ try {
1328
+ if (previous) await this.options.repository.upsert(previous);
1329
+ else await this.options.repository.remove(plugin);
1330
+ } catch (rollbackError) {
1331
+ rollbackErrors.push(rollbackError);
1332
+ }
1333
+ if (rollbackErrors.length > 0) throw new AggregateError([error, ...rollbackErrors], `failed to install plugin "${plugin}"`);
1334
+ throw error;
1335
+ }
1336
+ }
1337
+ /** Remove archive metadata and files as one compensating transaction. */
1338
+ async uninstall(plugin) {
1339
+ const previous = await this.options.repository.find(plugin);
1340
+ const replacement = await this.options.files.replace(plugin, /* @__PURE__ */ new Map());
1341
+ try {
1342
+ await this.options.repository.remove(plugin);
1343
+ await replacement.commit();
1344
+ } catch (error) {
1345
+ const rollbackErrors = [];
1346
+ try {
1347
+ await replacement.rollback();
1348
+ } catch (rollbackError) {
1349
+ rollbackErrors.push(rollbackError);
1350
+ }
1351
+ try {
1352
+ if (previous) await this.options.repository.upsert(previous);
1353
+ } catch (rollbackError) {
1354
+ rollbackErrors.push(rollbackError);
1355
+ }
1356
+ if (rollbackErrors.length > 0) throw new AggregateError([error, ...rollbackErrors], `failed to uninstall plugin "${plugin}"`);
1357
+ throw error;
1358
+ }
1359
+ }
1360
+ };
1361
+ //#endregion
1362
+ //#region lib/install/source.ts
1363
+ var LocalFileSourceResolver = class {
1364
+ id = "local-file";
1365
+ matches(input) {
1366
+ return typeof input !== "string";
1367
+ }
1368
+ async resolve(input) {
1369
+ if (typeof input === "string") throw new TypeError("local file resolver requires a File");
1370
+ return {
1371
+ file: input,
1372
+ installInput: "",
1373
+ resolverId: this.id
1374
+ };
1375
+ }
1376
+ };
1377
+ var HttpSourceResolver = class {
1378
+ id = "http";
1379
+ matches(input) {
1380
+ return typeof input === "string" && /^https?:\/\//i.test(input);
1381
+ }
1382
+ async resolve(input, signal) {
1383
+ if (typeof input !== "string") throw new TypeError("HTTP resolver requires a URL");
1384
+ const response = await fetch(input, { signal });
1385
+ if (!response.ok) throw new Error(`plugin download failed: ${response.status}`);
1386
+ const name = new URL(input).pathname.split("/").at(-1) || "plugin.zip";
1387
+ return {
1388
+ file: new File([await response.blob()], name),
1389
+ installInput: input,
1390
+ resolverId: this.id
1391
+ };
1392
+ }
1393
+ };
1394
+ var GitHubSourceResolver = class {
1395
+ options;
1396
+ id = "github";
1397
+ constructor(options) {
1398
+ this.options = options;
1399
+ }
1400
+ matches(input) {
1401
+ return typeof input === "string" && /^gh:[^/]+\/[^/]+$/.test(input);
1402
+ }
1403
+ async resolve(input, signal) {
1404
+ if (typeof input !== "string") throw new TypeError("GitHub resolver requires a repository");
1405
+ const [owner, repo] = input.slice(3).split("/");
1406
+ const octokit = new Octokit({ auth: this.options.token });
1407
+ const pages = octokit.paginate.iterator(octokit.rest.repos.listReleases, {
1408
+ owner,
1409
+ per_page: 100,
1410
+ repo,
1411
+ request: { signal }
1412
+ });
1413
+ for await (const page of pages) for (const release of page.data) {
1414
+ if (release.draft || release.prerelease) continue;
1415
+ const manifestAsset = release.assets.find((asset) => asset.name === "manifest.json");
1416
+ const packageAsset = release.assets.find((asset) => asset.name === "plugin.zip");
1417
+ if (!manifestAsset || !packageAsset) continue;
1418
+ const manifestResponse = await fetch(manifestAsset.browser_download_url, { signal });
1419
+ if (!manifestResponse.ok) continue;
1420
+ if (!isPluginManifestCompatible(parsePluginManifest(await manifestResponse.json()), this.options.coreVersion)) continue;
1421
+ const packageResponse = await fetch(packageAsset.browser_download_url, { signal });
1422
+ if (!packageResponse.ok) throw new Error(`plugin download failed: ${packageResponse.status}`);
1423
+ return {
1424
+ file: new File([await packageResponse.blob()], packageAsset.name),
1425
+ installInput: input,
1426
+ resolverId: this.id
1427
+ };
1428
+ }
1429
+ throw new Error(`no compatible plugin release found for ${owner}/${repo}`);
1430
+ }
1431
+ };
1432
+ var MarketplaceSourceResolver = class {
1433
+ catalog;
1434
+ sources;
1435
+ id = "marketplace";
1436
+ constructor(catalog, sources) {
1437
+ this.catalog = catalog;
1438
+ this.sources = sources;
1439
+ }
1440
+ matches(input) {
1441
+ return pluginCatalogIdFromInstallInput(input) !== void 0;
1442
+ }
1443
+ async resolve(input, signal) {
1444
+ if (typeof input !== "string") throw new TypeError("marketplace resolver requires a plugin catalog id");
1445
+ const plugin = pluginCatalogIdFromInstallInput(input);
1446
+ if (!plugin) throw new TypeError("marketplace resolver requires a plugin catalog id");
1447
+ const redirected = await this.catalog.resolveInstallInput(plugin, signal);
1448
+ const resolver = this.sources.find((source) => source.matches(redirected));
1449
+ if (!resolver) throw new Error(`plugin catalog returned an unsupported install source: ${redirected}`);
1450
+ return {
1451
+ ...await resolver.resolve(redirected, signal),
1452
+ installInput: input,
1453
+ resolverId: this.id
1454
+ };
1455
+ }
1456
+ };
1457
+ //#endregion
1458
+ //#region lib/adapters/fileStore.ts
1459
+ const mimeType = (path) => ({
1460
+ avif: "image/avif",
1461
+ gif: "image/gif",
1462
+ jpeg: "image/jpeg",
1463
+ jpg: "image/jpeg",
1464
+ png: "image/png",
1465
+ svg: "image/svg+xml",
1466
+ webp: "image/webp"
1467
+ })[path.split(".").at(-1)?.toLowerCase() ?? ""] ?? "application/octet-stream";
1468
+ var IndexedDbPluginFileBackend = class {
1469
+ #database = "delta-comic-plugin-files-v2";
1470
+ #store = "files";
1471
+ async #open() {
1472
+ return await new Promise((resolve, reject) => {
1473
+ const request = indexedDB.open(this.#database, 1);
1474
+ request.onupgradeneeded = () => {
1475
+ if (!request.result.objectStoreNames.contains(this.#store)) request.result.createObjectStore(this.#store);
1476
+ };
1477
+ request.onsuccess = () => resolve(request.result);
1478
+ request.onerror = () => reject(request.error);
1479
+ });
1480
+ }
1481
+ #key(plugin, path) {
1482
+ return `${plugin}/${path}`;
1483
+ }
1484
+ async read(plugin, path) {
1485
+ const database = await this.#open();
1486
+ try {
1487
+ return await new Promise((resolve, reject) => {
1488
+ const request = database.transaction(this.#store, "readonly").objectStore(this.#store).get(this.#key(plugin, path));
1489
+ request.onsuccess = () => {
1490
+ if (!request.result) reject(/* @__PURE__ */ new Error(`plugin file not found: ${plugin}/${path}`));
1491
+ else resolve(Uint8Array.from(request.result));
1492
+ };
1493
+ request.onerror = () => reject(request.error);
1494
+ });
1495
+ } finally {
1496
+ database.close();
1497
+ }
1498
+ }
1499
+ async snapshot(plugin) {
1500
+ const database = await this.#open();
1501
+ try {
1502
+ return await new Promise((resolve, reject) => {
1503
+ const files = /* @__PURE__ */ new Map();
1504
+ const transaction = database.transaction(this.#store, "readonly");
1505
+ const request = transaction.objectStore(this.#store).openCursor();
1506
+ const prefix = `${plugin}/`;
1507
+ request.onsuccess = () => {
1508
+ const cursor = request.result;
1509
+ if (!cursor) return;
1510
+ const key = String(cursor.key);
1511
+ if (key.startsWith(prefix)) files.set(key.slice(prefix.length), Uint8Array.from(cursor.value));
1512
+ cursor.continue();
1513
+ };
1514
+ request.onerror = () => reject(request.error);
1515
+ transaction.oncomplete = () => resolve(files);
1516
+ transaction.onerror = () => reject(transaction.error);
1517
+ });
1518
+ } finally {
1519
+ database.close();
1520
+ }
1521
+ }
1522
+ async replace(plugin, files) {
1523
+ const database = await this.#open();
1524
+ try {
1525
+ await new Promise((resolve, reject) => {
1526
+ const transaction = database.transaction(this.#store, "readwrite");
1527
+ const store = transaction.objectStore(this.#store);
1528
+ const prefix = `${plugin}/`;
1529
+ const cursor = store.openKeyCursor();
1530
+ cursor.onsuccess = () => {
1531
+ if (cursor.result) {
1532
+ if (String(cursor.result.key).startsWith(prefix)) cursor.result.delete();
1533
+ cursor.result.continue();
1534
+ return;
1535
+ }
1536
+ for (const [path, bytes] of files) store.put(bytes, this.#key(plugin, path));
1537
+ };
1538
+ cursor.onerror = () => reject(cursor.error);
1539
+ transaction.oncomplete = () => resolve();
1540
+ transaction.onerror = () => reject(transaction.error);
1541
+ transaction.onabort = () => reject(transaction.error);
1542
+ });
1543
+ } finally {
1544
+ database.close();
1545
+ }
1546
+ }
1547
+ };
1548
+ var TauriPluginFileBackend = class {
1549
+ async #root(plugin) {
1550
+ const { appLocalDataDir, join } = await import("@tauri-apps/api/path");
1551
+ return await join(await appLocalDataDir(), "plugin", plugin);
1552
+ }
1553
+ async read(plugin, path) {
1554
+ const [{ join }, fs] = await Promise.all([import("@tauri-apps/api/path"), import("@tauri-apps/plugin-fs")]);
1555
+ return await fs.readFile(await join(await this.#root(plugin), path));
1556
+ }
1557
+ async snapshot(plugin) {
1558
+ const [{ join }, fs] = await Promise.all([import("@tauri-apps/api/path"), import("@tauri-apps/plugin-fs")]);
1559
+ const root = await this.#root(plugin);
1560
+ const files = /* @__PURE__ */ new Map();
1561
+ if (!await fs.exists(root)) return files;
1562
+ const visit = async (directory, prefix = "") => {
1563
+ for (const entry of await fs.readDir(directory)) {
1564
+ const path = prefix ? `${prefix}/${entry.name}` : entry.name;
1565
+ const absolute = await join(directory, entry.name);
1566
+ if (entry.isDirectory) await visit(absolute, path);
1567
+ else if (entry.isFile) files.set(path, await fs.readFile(absolute));
1568
+ }
1569
+ };
1570
+ await visit(root);
1571
+ return files;
1572
+ }
1573
+ async replace(plugin, files) {
1574
+ const [{ appLocalDataDir, join }, fs] = await Promise.all([import("@tauri-apps/api/path"), import("@tauri-apps/plugin-fs")]);
1575
+ const appData = await appLocalDataDir();
1576
+ const base = await join(appData, "plugin");
1577
+ const token = crypto.randomUUID();
1578
+ const live = await join(base, plugin);
1579
+ const stagingRoot = await join(appData, "plugin-staging");
1580
+ const backupRoot = await join(appData, "plugin-backup");
1581
+ const staging = await join(stagingRoot, `${plugin}-${token}`);
1582
+ const backup = await join(backupRoot, `${plugin}-${token}`);
1583
+ await fs.mkdir(staging, { recursive: true });
1584
+ try {
1585
+ for (const [path, bytes] of files) {
1586
+ const segments = safePluginPath(path, "plugin file path").split("/");
1587
+ const target = await join(staging, ...segments);
1588
+ const parent = await join(staging, ...segments.slice(0, -1));
1589
+ await fs.mkdir(parent, { recursive: true });
1590
+ await fs.writeFile(target, bytes);
1591
+ }
1592
+ const existed = await fs.exists(live);
1593
+ if (existed) {
1594
+ await fs.mkdir(backupRoot, { recursive: true });
1595
+ await fs.rename(live, backup);
1596
+ }
1597
+ try {
1598
+ await fs.rename(staging, live);
1599
+ } catch (error) {
1600
+ if (existed && await fs.exists(backup)) await fs.rename(backup, live);
1601
+ throw error;
1602
+ }
1603
+ if (await fs.exists(backup)) await fs.remove(backup, { recursive: true }).catch(() => void 0);
1604
+ } catch (error) {
1605
+ if (await fs.exists(staging)) await fs.remove(staging, { recursive: true });
1606
+ throw error;
1607
+ }
1608
+ }
1609
+ async moduleUrl(plugin, path) {
1610
+ const { convertFileSrc } = await import("@tauri-apps/api/core");
1611
+ const { join } = await import("@tauri-apps/api/path");
1612
+ return convertFileSrc(await join(await this.#root(plugin), path));
1613
+ }
1614
+ };
1615
+ var AtomicPluginFileStore = class {
1616
+ backend;
1617
+ #urls = /* @__PURE__ */ new Map();
1618
+ constructor(backend) {
1619
+ this.backend = backend;
1620
+ }
1621
+ async replace(plugin, files) {
1622
+ const previous = await this.backend.snapshot(plugin);
1623
+ await this.backend.replace(plugin, files);
1624
+ let settled = false;
1625
+ return {
1626
+ commit: async () => {
1627
+ settled = true;
1628
+ },
1629
+ rollback: async () => {
1630
+ if (settled) return;
1631
+ settled = true;
1632
+ await this.backend.replace(plugin, previous);
1633
+ }
1634
+ };
1635
+ }
1636
+ async remove(plugin) {
1637
+ this.release(plugin);
1638
+ await this.backend.replace(plugin, /* @__PURE__ */ new Map());
1639
+ }
1640
+ read(plugin, path) {
1641
+ return this.backend.read(plugin, safePluginPath(path, "plugin file path"));
1642
+ }
1643
+ async createModuleUrl(plugin, path) {
1644
+ const safePath = safePluginPath(path, "plugin module path");
1645
+ if (this.backend.moduleUrl) return await this.backend.moduleUrl(plugin, safePath);
1646
+ const bytes = await this.backend.read(plugin, safePath);
1647
+ const url = URL.createObjectURL(new Blob([Uint8Array.from(bytes)], { type: "text/javascript" }));
1648
+ const urls = this.#urls.get(plugin) ?? /* @__PURE__ */ new Set();
1649
+ urls.add(url);
1650
+ this.#urls.set(plugin, urls);
1651
+ return url;
1652
+ }
1653
+ async createAssetUrl(plugin, path) {
1654
+ const safePath = safePluginPath(path, "plugin asset path");
1655
+ if (this.backend.moduleUrl) return await this.backend.moduleUrl(plugin, safePath);
1656
+ const bytes = await this.backend.read(plugin, safePath);
1657
+ const url = URL.createObjectURL(new Blob([Uint8Array.from(bytes)], { type: mimeType(path) }));
1658
+ const urls = this.#urls.get(plugin) ?? /* @__PURE__ */ new Set();
1659
+ urls.add(url);
1660
+ this.#urls.set(plugin, urls);
1661
+ return url;
1662
+ }
1663
+ release(plugin) {
1664
+ for (const url of this.#urls.get(plugin) ?? []) URL.revokeObjectURL(url);
1665
+ this.#urls.delete(plugin);
1666
+ }
1667
+ };
1668
+ const createDefaultPluginFileStore = () => new AtomicPluginFileStore(isTauri() ? new TauriPluginFileBackend() : new IndexedDbPluginFileBackend());
1669
+ //#endregion
1670
+ //#region lib/adapters/i18n.ts
1671
+ const messageKeyPrefix = "i18n:";
1672
+ const unsafeKeys = /* @__PURE__ */ new Set([
1673
+ "__proto__",
1674
+ "constructor",
1675
+ "prototype"
1676
+ ]);
1677
+ const mergeMessages = (target, source) => {
1678
+ if (!source) return target;
1679
+ for (const [key, value] of Object.entries(source)) {
1680
+ if (unsafeKeys.has(key)) continue;
1681
+ if (typeof value === "string") {
1682
+ target[key] = value;
1683
+ continue;
1684
+ }
1685
+ const current = target[key];
1686
+ target[key] = mergeMessages(typeof current === "object" ? { ...current } : {}, value);
1687
+ }
1688
+ return target;
1689
+ };
1690
+ var PluginI18nRegistry = class {
1691
+ adapter;
1692
+ baseMessages = {};
1693
+ pluginMessages = /* @__PURE__ */ new Map();
1694
+ install(adapter, baseMessages) {
1695
+ this.adapter = adapter;
1696
+ this.baseMessages = baseMessages;
1697
+ this.refresh(this.locales());
1698
+ }
1699
+ register(plugin, messages) {
1700
+ const previous = this.pluginMessages.get(plugin);
1701
+ this.pluginMessages.delete(plugin);
1702
+ this.pluginMessages.set(plugin, messages);
1703
+ this.refresh(/* @__PURE__ */ new Set([...Object.keys(previous ?? {}), ...Object.keys(messages)]));
1704
+ }
1705
+ remove(plugin) {
1706
+ const messages = this.pluginMessages.get(plugin);
1707
+ if (!messages) return;
1708
+ this.pluginMessages.delete(plugin);
1709
+ this.refresh(new Set(Object.keys(messages)));
1710
+ }
1711
+ translate(key, params) {
1712
+ return this.adapter?.translate?.(key, params) ?? key;
1713
+ }
1714
+ compose(locale) {
1715
+ const message = mergeMessages({}, this.baseMessages[locale]);
1716
+ for (const messages of this.pluginMessages.values()) mergeMessages(message, messages[locale]);
1717
+ return message;
1718
+ }
1719
+ locales() {
1720
+ const locales = new Set(Object.keys(this.baseMessages));
1721
+ for (const messages of this.pluginMessages.values()) for (const locale of Object.keys(messages)) locales.add(locale);
1722
+ return locales;
1723
+ }
1724
+ refresh(locales) {
1725
+ if (!this.adapter) return;
1726
+ for (const locale of locales) this.adapter.setLocaleMessage(locale, this.compose(locale));
1727
+ }
1728
+ };
1729
+ const pluginI18n = new PluginI18nRegistry();
1730
+ const pluginMessageKey = (key) => `${messageKeyPrefix}${key}`;
1731
+ const translatePluginText = (value) => value.startsWith(messageKeyPrefix) ? pluginI18n.translate(value.slice(5)) : value;
1732
+ //#endregion
1733
+ //#region package.json
1734
+ var version = "3.0.0-next.9";
1735
+ //#endregion
1736
+ //#region lib/core/env.ts
1737
+ const pluginName = "core";
1738
+ //#endregion
1739
+ //#region lib/core/config.ts
1740
+ const cfg = new ConfigPointer(pluginName, {
1741
+ recordHistory: {
1742
+ type: "switch",
1743
+ defaultValue: true,
1744
+ info: "plugin.core.config.recordHistory"
1745
+ },
1746
+ showAIProject: {
1747
+ type: "switch",
1748
+ defaultValue: true,
1749
+ info: "plugin.core.config.showAiWorks"
1750
+ },
1751
+ darkMode: {
1752
+ type: "radio",
1753
+ defaultValue: "system",
1754
+ info: "plugin.core.config.theme.title",
1755
+ comp: "select",
1756
+ selects: [
1757
+ {
1758
+ label: "plugin.core.config.theme.light",
1759
+ value: "light"
1760
+ },
1761
+ {
1762
+ label: "plugin.core.config.theme.dark",
1763
+ value: "dark"
1764
+ },
1765
+ {
1766
+ label: "plugin.core.config.systemDefault",
1767
+ value: "system"
1768
+ }
1769
+ ]
1770
+ },
1771
+ language: {
1772
+ type: "radio",
1773
+ defaultValue: "system",
1774
+ info: "plugin.core.config.language.title",
1775
+ comp: "select",
1776
+ selects: [
1777
+ {
1778
+ label: "plugin.core.config.language.zhCN",
1779
+ value: "zh-CN"
1780
+ },
1781
+ {
1782
+ label: "plugin.core.config.language.zhTW",
1783
+ value: "zh-TW"
1784
+ },
1785
+ {
1786
+ label: "plugin.core.config.language.enUS",
1787
+ value: "en-US"
1788
+ },
1789
+ {
1790
+ label: "plugin.core.config.systemDefault",
1791
+ value: "system"
1792
+ }
1793
+ ]
1794
+ },
1795
+ easilyTitle: {
1796
+ type: "switch",
1797
+ defaultValue: false,
1798
+ info: "plugin.core.config.simplifiedTitle"
1799
+ },
1800
+ githubToken: {
1801
+ type: "string",
1802
+ defaultValue: "",
1803
+ info: "plugin.core.config.githubToken.title",
1804
+ placeholder: "plugin.core.config.githubToken.placeholder"
1805
+ },
1806
+ receivePerReleaseUpdate: {
1807
+ type: "switch",
1808
+ defaultValue: false,
1809
+ info: "plugin.core.config.prereleaseUpdates"
1810
+ },
1811
+ cloudEnabled: {
1812
+ type: "switch",
1813
+ defaultValue: false,
1814
+ info: "plugin.core.config.cloud.enabled"
1815
+ },
1816
+ cloudServerUrl: {
1817
+ type: "string",
1818
+ defaultValue: "",
1819
+ info: "plugin.core.config.cloud.serverUrl",
1820
+ placeholder: "plugin.core.config.cloud.serverUrlPlaceholder"
1821
+ },
1822
+ installOverride: {
1823
+ type: "pairs",
1824
+ defaultValue: [],
1825
+ info: "plugin.core.config.installOverride",
1826
+ required: true
1827
+ }
1828
+ }, "plugin.core.config.name");
1829
+ //#endregion
1830
+ //#region lib/core/share.ts
1831
+ const tokenInit = {
1832
+ filter: (page) => !!page.preload,
1833
+ icon: {},
1834
+ key: "token",
1835
+ name: pluginMessageKey("plugin.share.copyToken"),
1836
+ async call(page) {
1837
+ const item = page.preload?.toJSON();
1838
+ if (!item) throw new Error("Not found preload in content. Maybe not fetch detail?");
1839
+ const compressed = compressToEncodedURIComponent(JSON.stringify({
1840
+ item: {
1841
+ contentType: UniContentPage.contentPages.key.toString(item.contentType),
1842
+ ep: item.thisEp.id,
1843
+ name: item.title
1844
+ },
1845
+ plugin: page.plugin,
1846
+ id: page.id
1847
+ }));
1848
+ return { token: `[${item.title}](复制这条口令,打开Delta Comic)${compressed}` };
1849
+ }
1850
+ };
1851
+ const nativeInit = {
1852
+ filter: (page) => !!page.preload,
1853
+ icon: {},
1854
+ key: "native",
1855
+ name: pluginMessageKey("plugin.share.native"),
1856
+ async call(page) {
1857
+ const item = page.preload?.toJSON();
1858
+ if (!item) throw new Error("Not found preload in content. Maybe not fetch detail?");
1859
+ const compressed = compressToEncodedURIComponent(JSON.stringify({
1860
+ item: {
1861
+ contentType: UniContentPage.contentPages.key.toString(item.contentType),
1862
+ ep: item.thisEp.id,
1863
+ name: item.title
1864
+ },
1865
+ plugin: page.plugin,
1866
+ id: page.id
1867
+ }));
1868
+ const token = `[${item.title}](复制这条口令,打开Delta Comic)${compressed}`;
1869
+ await navigator.share({
1870
+ title: pluginI18n.translate("plugin.share.nativeTitle"),
1871
+ text: token
1872
+ });
1873
+ return { token };
1874
+ }
1875
+ };
1876
+ const tokenShare = {
1877
+ key: "token",
1878
+ name: pluginMessageKey("plugin.share.defaultToken"),
1879
+ isMatched(chipboard) {
1880
+ return /^\[.+\]\(复制这条口令,打开Delta Comic\).+/.test(chipboard);
1881
+ },
1882
+ async show(chipboard) {
1883
+ const meta = JSON.parse(decompressFromEncodedURIComponent(chipboard.replace(/^\[.+\]/, "").replaceAll("(复制这条口令,打开Delta Comic)", "")));
1884
+ return {
1885
+ title: pluginI18n.translate("plugin.share.tokenTitle"),
1886
+ detail: pluginI18n.translate("plugin.share.tokenDetail", { item: meta.item.name }),
1887
+ onNegative() {},
1888
+ onPositive() {
1889
+ return SharedFunction.call("routeToContent", UniContentPage.contentPages.key.toJSON(meta.item.contentType), meta.id, meta.item.ep);
1890
+ }
1891
+ };
1892
+ }
1893
+ };
1894
+ //#endregion
1895
+ //#region lib/core/index.ts
1896
+ var core_exports = /* @__PURE__ */ __exportAll({
1897
+ cfg: () => cfg,
1898
+ default: () => core_default
1899
+ });
1900
+ var core_default = defineDeltaComicPlugin(() => ({
1901
+ config: cfg,
1902
+ name: pluginName,
1903
+ model: { social: { share: {
1904
+ initiative: [tokenInit, nativeInit],
1905
+ tokenListen: [tokenShare]
1906
+ } } }
1907
+ }));
1908
+ //#endregion
1909
+ //#region lib/builtins/core.builtin.ts
1910
+ var core_builtin_exports = /* @__PURE__ */ __exportAll({
1911
+ corePluginDefinition: () => corePluginDefinition,
1912
+ default: () => corePluginDefinition
1913
+ });
1914
+ const corePluginDefinition = defineInternalPlugin({
1915
+ canDisable: false,
1916
+ factory: core_default,
1917
+ manifest: {
1918
+ apiVersion: DELTA_COMIC_PLUGIN_API_VERSION,
1919
+ author: "Delta Comic",
1920
+ description: "Delta Comic host capabilities",
1921
+ kind: "preboot",
1922
+ name: {
1923
+ display: "core",
1924
+ id: "core"
1925
+ },
1926
+ require: [],
1927
+ version: {
1928
+ plugin: version,
1929
+ supportCore: "*"
1930
+ }
1931
+ }
1932
+ });
1933
+ /** Files are the registration boundary: adding a built-in does not change the composition root. */
1934
+ const internalPluginDefinitions = Object.entries(/* @__PURE__ */ Object.assign({ "./core.builtin.ts": core_builtin_exports })).sort(([left], [right]) => left.localeCompare(right)).map(([path, module]) => {
1935
+ if (!module.default) throw new Error(`built-in plugin module has no default definition: ${path}`);
1936
+ return module.default;
1937
+ });
1938
+ //#endregion
1939
+ //#region lib/runtime/store.ts
1940
+ var PluginStore = class {
1941
+ translateText;
1942
+ candidateEntries = shallowReactive(/* @__PURE__ */ new Map());
1943
+ loadingEntries = shallowReactive(/* @__PURE__ */ new Map());
1944
+ pluginEntries = shallowReactive(/* @__PURE__ */ new Map());
1945
+ readyEntries = shallowReactive(/* @__PURE__ */ new Set());
1946
+ constructor(translateText = (value) => value) {
1947
+ this.translateText = translateText;
1948
+ }
1949
+ get candidates() {
1950
+ return this.candidateEntries;
1951
+ }
1952
+ get loading() {
1953
+ return this.loadingEntries;
1954
+ }
1955
+ get plugins() {
1956
+ return this.pluginEntries;
1957
+ }
1958
+ get ready() {
1959
+ return this.readyEntries;
1960
+ }
1961
+ replaceCandidates(candidates) {
1962
+ this.candidateEntries.clear();
1963
+ for (const candidate of candidates) this.candidateEntries.set(candidate.manifest.name.id, candidate);
1964
+ }
1965
+ markLoading(plugin, config) {
1966
+ this.readyEntries.delete(plugin);
1967
+ this.loadingEntries.set(plugin, config);
1968
+ this.pluginEntries.delete(plugin);
1969
+ }
1970
+ markReady(plugin) {
1971
+ const config = this.loadingEntries.get(plugin);
1972
+ if (!config) throw new Error(`plugin "${plugin}" was not marked as loading`);
1973
+ this.loadingEntries.delete(plugin);
1974
+ this.pluginEntries.set(plugin, config);
1975
+ this.readyEntries.add(plugin);
1976
+ }
1977
+ markUnloaded(plugin) {
1978
+ this.readyEntries.delete(plugin);
1979
+ this.loadingEntries.delete(plugin);
1980
+ this.pluginEntries.delete(plugin);
1981
+ }
1982
+ isLoaded(plugin) {
1983
+ return this.readyEntries.has(plugin);
1984
+ }
1985
+ displayName(plugin) {
1986
+ return this.translateText(this.candidateEntries.get(plugin)?.manifest.name.display ?? plugin);
1987
+ }
1988
+ modelEntries(key) {
1989
+ return [...this.pluginEntries].flatMap(([plugin, config]) => {
1990
+ const model = config.model?.[key];
1991
+ return model === void 0 ? [] : [[plugin, model]];
1992
+ });
1993
+ }
1994
+ };
1995
+ //#endregion
1996
+ //#region lib/runtime/engine.ts
1997
+ const recoveryKey = "delta-comic:preboot-recovery:v2";
1998
+ const errorText = (error) => error instanceof Error ? error.message : String(error);
1999
+ const loadingInfo = () => ({
2000
+ progress: {
2001
+ status: "wait",
2002
+ stepsIndex: 0
2003
+ },
2004
+ steps: [{
2005
+ description: "",
2006
+ name: "waiting"
2007
+ }]
2008
+ });
2009
+ var PluginRuntime = class {
2010
+ #active = /* @__PURE__ */ new Map();
2011
+ #options;
2012
+ #normalOperation;
2013
+ #prebootOperation;
2014
+ #prebootReport;
2015
+ store;
2016
+ constructor(options) {
2017
+ this.#options = options;
2018
+ this.store = options.store ?? new PluginStore();
2019
+ }
2020
+ get activeNormalPluginNames() {
2021
+ return [...this.#active].filter(([, value]) => (value.candidate.manifest.kind ?? "normal") === "normal").map(([plugin]) => plugin);
2022
+ }
2023
+ async preparePreboot(app) {
2024
+ if (this.#prebootReport) return this.#prebootReport;
2025
+ if (this.#prebootOperation) return await this.#prebootOperation;
2026
+ const operation = (async () => {
2027
+ const progress = ref({});
2028
+ const report = await this.#load("preboot", progress, void 0, app);
2029
+ this.#prebootReport = report;
2030
+ if (report.failures.length > 0) this.#writeRecovery(report.failures);
2031
+ return report;
2032
+ })();
2033
+ this.#prebootOperation = operation;
2034
+ try {
2035
+ return await operation;
2036
+ } finally {
2037
+ this.#prebootOperation = void 0;
2038
+ }
2039
+ }
2040
+ loadNormal(options = {}) {
2041
+ if (this.#normalOperation) throw new Error("normal plugins are already loading");
2042
+ if (this.activeNormalPluginNames.length > 0) throw new Error("normal plugins are already active; use reloadNormal()");
2043
+ const progress = ref({});
2044
+ const operation = this.#load("normal", progress, options.pluginNames);
2045
+ this.#track(operation);
2046
+ return {
2047
+ operation,
2048
+ progress
2049
+ };
2050
+ }
2051
+ reloadNormal(options = {}) {
2052
+ if (this.#normalOperation) throw new Error("normal plugins are already loading");
2053
+ const progress = ref({});
2054
+ const operation = (async () => {
2055
+ await this.#unload((candidate) => (candidate.manifest.kind ?? "normal") === "normal");
2056
+ return await this.#load("normal", progress, options.pluginNames);
2057
+ })();
2058
+ this.#track(operation);
2059
+ return {
2060
+ operation,
2061
+ progress
2062
+ };
2063
+ }
2064
+ async uninstall(plugin) {
2065
+ const candidate = this.store.candidates.get(plugin);
2066
+ if (!candidate) throw new Error(`plugin "${plugin}" is not a known candidate`);
2067
+ if (!candidate.management.canUninstall) throw new Error(`plugin "${plugin}" cannot be uninstalled`);
2068
+ const active = this.#active.get(plugin);
2069
+ if (active) await this.#deactivate(plugin, active.scope);
2070
+ else {
2071
+ const scope = new PluginScope(plugin);
2072
+ try {
2073
+ const loaded = await candidate.load(scope.signal);
2074
+ if (loaded.dispose) scope.defer(loaded.dispose);
2075
+ await loaded.factory(this.#options.environment()).hooks?.onUninstall?.();
2076
+ } finally {
2077
+ await scope.dispose();
2078
+ }
2079
+ }
2080
+ await this.#options.remove(plugin);
2081
+ await this.refreshCandidates();
2082
+ }
2083
+ async refreshCandidates() {
2084
+ const candidates = await this.#options.provider.list(new AbortController().signal);
2085
+ this.store.replaceCandidates(candidates);
2086
+ return candidates;
2087
+ }
2088
+ readRecovery() {
2089
+ try {
2090
+ const value = globalThis.localStorage?.getItem(recoveryKey);
2091
+ return value ? JSON.parse(value) : null;
2092
+ } catch {
2093
+ return null;
2094
+ }
2095
+ }
2096
+ clearRecovery() {
2097
+ globalThis.localStorage?.removeItem(recoveryKey);
2098
+ }
2099
+ async #load(phase, progress, selected, app) {
2100
+ const candidates = await this.refreshCandidates();
2101
+ const activeDependencies = new Set(this.#active.keys());
2102
+ const phaseCandidates = candidates.filter((candidate) => candidate.enabled && (candidate.manifest.kind ?? "normal") === phase).map((candidate) => ({
2103
+ ...candidate,
2104
+ manifest: {
2105
+ ...candidate.manifest,
2106
+ require: candidate.manifest.require.filter((value) => !activeDependencies.has(value.id))
2107
+ }
2108
+ }));
2109
+ const plan = planPluginDependencies(selected ? this.#selectWithDependencies(phaseCandidates, selected) : phaseCandidates);
2110
+ if (plan.missing.length > 0 || plan.cycles.length > 0) {
2111
+ const missing = plan.missing.map((value) => `${value.plugin} -> ${value.dependency}`).join(", ");
2112
+ const cycles = plan.cycles.map((value) => value.join(" -> ")).join(", ");
2113
+ throw new Error([missing && `missing: ${missing}`, cycles && `cycles: ${cycles}`].filter(Boolean).join("; "));
2114
+ }
2115
+ const activated = [];
2116
+ const failures = [];
2117
+ const failed = /* @__PURE__ */ new Set();
2118
+ for (const level of plan.levels) for (const candidate of level) {
2119
+ const plugin = candidate.manifest.name.id;
2120
+ const info = progress.value[plugin] = loadingInfo();
2121
+ const blockedBy = candidate.manifest.require.map((value) => value.id).filter((dependency) => failed.has(dependency));
2122
+ if (blockedBy.length > 0) {
2123
+ const error = /* @__PURE__ */ new Error(`dependency activation failed: ${blockedBy.join(", ")}`);
2124
+ info.progress = {
2125
+ errorReason: error.message,
2126
+ status: "error",
2127
+ stepsIndex: 0
2128
+ };
2129
+ failures.push({
2130
+ error,
2131
+ phase,
2132
+ plugin
2133
+ });
2134
+ failed.add(plugin);
2135
+ continue;
2136
+ }
2137
+ const scope = new PluginScope(plugin);
2138
+ try {
2139
+ info.progress.status = "process";
2140
+ info.steps[0] = {
2141
+ description: "",
2142
+ name: "module"
2143
+ };
2144
+ const loaded = await candidate.load(scope.signal);
2145
+ if (loaded.dispose) scope.defer(loaded.dispose);
2146
+ const config = markRaw(loaded.factory(this.#options.environment()));
2147
+ if (config.name !== plugin) throw new Error(`plugin name mismatch: ${plugin} / ${config.name}`);
2148
+ this.store.markLoading(plugin, config);
2149
+ scope.defer(() => this.store.markUnloaded(plugin));
2150
+ await new ActivationPipeline(this.#options.capabilities(phase, app)).activate(config, {
2151
+ owner: plugin,
2152
+ report: (update) => {
2153
+ const value = typeof update === "string" ? { description: update } : update;
2154
+ info.steps[0] = {
2155
+ ...info.steps[0],
2156
+ ...value
2157
+ };
2158
+ },
2159
+ scope,
2160
+ signal: scope.signal
2161
+ });
2162
+ this.#active.set(plugin, {
2163
+ candidate,
2164
+ scope
2165
+ });
2166
+ this.store.markReady(plugin);
2167
+ info.progress.status = "done";
2168
+ activated.push(plugin);
2169
+ } catch (error) {
2170
+ info.progress = {
2171
+ errorReason: errorText(error),
2172
+ status: "error",
2173
+ stepsIndex: 0
2174
+ };
2175
+ failures.push({
2176
+ error,
2177
+ phase,
2178
+ plugin
2179
+ });
2180
+ failed.add(plugin);
2181
+ await scope.dispose(error).catch((disposeError) => {
2182
+ failures.push({
2183
+ error: disposeError,
2184
+ phase,
2185
+ plugin
2186
+ });
2187
+ });
2188
+ }
2189
+ }
2190
+ return {
2191
+ activated,
2192
+ failures
2193
+ };
2194
+ }
2195
+ #selectWithDependencies(candidates, selected) {
2196
+ const byId = new Map(candidates.map((candidate) => [candidate.manifest.name.id, candidate]));
2197
+ const result = /* @__PURE__ */ new Map();
2198
+ const visit = (plugin) => {
2199
+ const candidate = byId.get(plugin);
2200
+ if (!candidate || result.has(plugin)) return;
2201
+ for (const dependency of candidate.manifest.require) visit(dependency.id);
2202
+ result.set(plugin, candidate);
2203
+ };
2204
+ for (const plugin of selected) visit(plugin);
2205
+ return [...result.values()];
2206
+ }
2207
+ #track(operation) {
2208
+ this.#normalOperation = operation;
2209
+ operation.then(() => this.#normalOperation = void 0, () => this.#normalOperation = void 0);
2210
+ }
2211
+ async #unload(predicate) {
2212
+ const targets = [...this.#active].filter(([, value]) => predicate(value.candidate)).reverse();
2213
+ const errors = [];
2214
+ for (const [plugin, value] of targets) try {
2215
+ await this.#deactivate(plugin, value.scope);
2216
+ } catch (error) {
2217
+ errors.push(error);
2218
+ }
2219
+ if (errors.length > 0) throw new AggregateError(errors, "some plugins failed to unload");
2220
+ }
2221
+ async #deactivate(plugin, scope) {
2222
+ this.#active.delete(plugin);
2223
+ await scope.dispose();
2224
+ }
2225
+ #writeRecovery(failures) {
2226
+ try {
2227
+ globalThis.localStorage?.setItem(recoveryKey, JSON.stringify({
2228
+ failedAt: Date.now(),
2229
+ plugins: failures.map((value) => value.plugin),
2230
+ reason: failures.map((value) => errorText(value.error)).join("\n")
2231
+ }));
2232
+ } catch {}
2233
+ }
2234
+ };
2235
+ //#endregion
2236
+ //#region lib/runtime/providers.ts
2237
+ const defaultStorage = () => {
2238
+ try {
2239
+ return globalThis.localStorage;
2240
+ } catch {
2241
+ return;
2242
+ }
2243
+ };
2244
+ var LocalInternalPluginPreferences = class {
2245
+ storage;
2246
+ prefix;
2247
+ constructor(storage = defaultStorage(), prefix = "delta-comic:internal-plugin:") {
2248
+ this.storage = storage;
2249
+ this.prefix = prefix;
2250
+ }
2251
+ async enabled(plugin, fallback) {
2252
+ try {
2253
+ const value = this.storage?.getItem(`${this.prefix}${plugin}`);
2254
+ return value === null || value === void 0 ? fallback : value === "true";
2255
+ } catch {
2256
+ return fallback;
2257
+ }
2258
+ }
2259
+ async setEnabled(plugin, enabled) {
2260
+ try {
2261
+ this.storage?.setItem(`${this.prefix}${plugin}`, String(enabled));
2262
+ } catch {}
2263
+ }
2264
+ };
2265
+ var InternalPluginCandidateProvider = class {
2266
+ definitions;
2267
+ preferences;
2268
+ id = "internal";
2269
+ constructor(definitions, preferences = new LocalInternalPluginPreferences()) {
2270
+ this.definitions = definitions;
2271
+ this.preferences = preferences;
2272
+ }
2273
+ async list(signal) {
2274
+ const candidates = [];
2275
+ for (const definition of this.definitions) {
2276
+ if (signal.aborted) throw signal.reason;
2277
+ candidates.push({
2278
+ enabled: definition.canDisable === false ? true : await this.preferences.enabled(definition.manifest.name.id, definition.enabledByDefault ?? true),
2279
+ load: async () => ({ factory: definition.factory }),
2280
+ management: {
2281
+ canDisable: definition.canDisable ?? true,
2282
+ canUninstall: false,
2283
+ canUpdate: false
2284
+ },
2285
+ manifest: definition.manifest,
2286
+ origin: "builtin"
2287
+ });
2288
+ }
2289
+ return candidates;
2290
+ }
2291
+ };
2292
+ var CompositePluginCandidateProvider = class {
2293
+ providers;
2294
+ id = "composite";
2295
+ constructor(providers) {
2296
+ this.providers = providers;
2297
+ }
2298
+ async list(signal) {
2299
+ const candidates = (await Promise.all(this.providers.map((provider) => provider.list(signal)))).flat();
2300
+ const owners = /* @__PURE__ */ new Map();
2301
+ for (const candidate of candidates) {
2302
+ const id = candidate.manifest.name.id;
2303
+ const previous = owners.get(id);
2304
+ if (previous) throw new Error(`duplicate plugin candidate "${id}" from ${previous.origin} and ${candidate.origin}`);
2305
+ owners.set(id, candidate);
2306
+ }
2307
+ return [...owners.values()];
2308
+ }
2309
+ };
2310
+ //#endregion
2311
+ //#region lib/composition.ts
2312
+ const pluginContributions = new ContributionHub();
2313
+ const pluginStore = new PluginStore((value) => value.startsWith("i18n:") ? pluginI18n.translate(value.slice(5)) : value);
2314
+ const pluginConfigStore = new ConfigStore();
2315
+ const useConfig = () => pluginConfigStore;
2316
+ const pluginHostServices = {};
2317
+ /** Install host-only integrations without exposing UI or platform details to the runtime kernel. */
2318
+ const configurePluginHost = (services) => {
2319
+ Object.assign(pluginHostServices, services);
2320
+ };
2321
+ const pluginFiles = createDefaultPluginFileStore();
2322
+ const pluginRepository = new DatabasePluginArchiveRepository();
2323
+ const pluginReader = new StoredPluginModuleReader(pluginFiles);
2324
+ const internalPreferences = new LocalInternalPluginPreferences();
2325
+ const candidateProvider = new CompositePluginCandidateProvider([new InternalPluginCandidateProvider(internalPluginDefinitions, internalPreferences), new InstalledPluginCandidateProvider(pluginRepository, pluginReader)]);
2326
+ const httpSource = new HttpSourceResolver();
2327
+ const githubSource = new GitHubSourceResolver({ coreVersion: corePluginDefinition.manifest.version.plugin });
2328
+ const awesomeRegistry = new AwesomeRegistryClient();
2329
+ const marketplaceSource = new MarketplaceSourceResolver(awesomeRegistry, [githubSource, httpSource]);
2330
+ const pluginCatalog = awesomeRegistry;
2331
+ const pluginInstaller = new PluginInstallService({
2332
+ codecs: [new ZipPackageCodec(), new DevScriptCodec()],
2333
+ files: pluginFiles,
2334
+ repository: pluginRepository,
2335
+ reservedIds: new Set(internalPluginDefinitions.map((definition) => definition.manifest.name.id)),
2336
+ resolvers: [
2337
+ new LocalFileSourceResolver(),
2338
+ marketplaceSource,
2339
+ githubSource,
2340
+ httpSource
2341
+ ]
2342
+ });
2343
+ const pluginRuntime = new PluginRuntime({
2344
+ capabilities: (phase, app) => createDefaultCapabilities({
2345
+ app,
2346
+ auth: pluginHostServices.auth,
2347
+ config: pluginConfigStore,
2348
+ contributions: pluginContributions,
2349
+ i18n: pluginI18n,
2350
+ phase
2351
+ }),
2352
+ environment: () => ({
2353
+ platform: isTauri() ? "tauri" : "web",
2354
+ safe: globalThis.$$safe$$ ?? true
2355
+ }),
2356
+ provider: candidateProvider,
2357
+ remove: (plugin) => pluginInstaller.uninstall(plugin),
2358
+ store: pluginStore
2359
+ });
2360
+ const installPlugin = async (input) => {
2361
+ const archive = await pluginInstaller.install(input);
2362
+ await pluginRuntime.refreshCandidates();
2363
+ return archive;
2364
+ };
2365
+ const updatePlugin = async (archive) => {
2366
+ if (!archive.installInput) throw new Error("plugin has no reusable install source");
2367
+ return await installPlugin(archive.installInput);
2368
+ };
2369
+ const updatePluginByName = async (plugin) => {
2370
+ const archive = await pluginRepository.find(plugin);
2371
+ if (!archive) throw new Error(`installed plugin not found: ${plugin}`);
2372
+ return await updatePlugin(archive);
2373
+ };
2374
+ const setPluginEnabled = async (plugin, enabled) => {
2375
+ const candidate = pluginStore.candidates.get(plugin);
2376
+ if (!candidate?.management.canDisable) throw new Error(`plugin "${plugin}" cannot be disabled`);
2377
+ if (candidate.origin === "builtin") await internalPreferences.setEnabled(plugin, enabled);
2378
+ else {
2379
+ const archive = await pluginRepository.find(plugin);
2380
+ if (!archive) throw new Error(`installed plugin not found: ${plugin}`);
2381
+ await pluginRepository.upsert({
2382
+ ...archive,
2383
+ enable: enabled
2384
+ });
2385
+ }
2386
+ await pluginRuntime.refreshCandidates();
2387
+ };
2388
+ const setPluginKind = async (plugin, kind) => {
2389
+ const archive = await pluginRepository.find(plugin);
2390
+ if (!archive) throw new Error(`installed plugin not found: ${plugin}`);
2391
+ await pluginRepository.upsert({
2392
+ ...archive,
2393
+ meta: {
2394
+ ...archive.meta,
2395
+ kind
2396
+ }
2397
+ });
2398
+ await pluginRuntime.refreshCandidates();
2399
+ };
2400
+ const uninstallPlugin = async (plugin) => await pluginRuntime.uninstall(plugin);
2401
+ const resolvePluginIconUrl = async (plugin, icon) => {
2402
+ if (!icon) return void 0;
2403
+ if (/^https?:\/\//i.test(icon)) return icon;
2404
+ if (!plugin) throw new Error("a plugin id is required to resolve a local plugin icon");
2405
+ return await pluginFiles.createAssetUrl(plugin, icon);
531
2406
  };
2407
+ const usePluginStore = () => pluginStore;
532
2408
  //#endregion
533
- export { AWESOME_REGISTRY_BASE_URL, AWESOME_REGISTRY_INDEX_PATH, AWESOME_REGISTRY_SCHEMA_VERSION, AwesomeRegistryCache, AwesomeRegistryClient, AwesomeRegistryNetworkError, AwesomeRegistryValidationError, AwesomeRegistryVersionError, BUILT_IN_PLUGIN_LOADER, booter_exports as Booter, ConfigPointer, ConfigStore, DependencyRegistry, Global, install_exports as Install, loader_exports as Loader, PluginBooter, PluginI18nRegistry, PluginInstaller, PluginLoader, PluginManifestError, PluginRuntimeExtensions, PluginStore, assertAwesomeRegistryPagePath, builtInDefinitionToArchive, createDependencyRegistry, declareDepType, defaultDependencyRegistry, defineInnerPlugin, definePlugin, exposeSymbol, failedDependencies, filterPluginsBySelection, findCyclePaths, formatPluginLoadPlanError, isBuiltInPlugin, isBuiltInPluginName, isPluginManifestCompatible, marketplaceDownloadToInstallInput, marketplaceListingInstallId, marketplaceListingSource, parseAwesomePluginListing, parseAwesomeRegistryIndex, parseAwesomeRegistryPage, parsePluginIconReference, parsePluginManifest, planPluginLoadOrder, pluginExposes, pluginI18n, pluginKind, pluginMessageKey, pluginRuntime, pluginStore, provide, registerBooter, registerInstaller, registerLoader, require, resolvePluginIconUrl, runtimeExtensions, selectPluginsForPhase, synchronizeBuiltInPlugins, translatePluginText, useConfig, usePluginStore };
2409
+ export { ActivationPipeline, CompositePluginCandidateProvider, ConfigPointer, content_exports as Content, ContributionHub, ContributionRegistry, core_exports as Core, DELTA_COMIC_PLUGIN_API_VERSION, DatabasePluginArchiveRepository, DevScriptCodec, expose_exports as Expose, GitHubSourceResolver, HttpSourceResolver, InstalledPluginCandidateProvider, InternalPluginCandidateProvider, LocalFileSourceResolver, LocalInternalPluginPreferences, MarketplaceSourceResolver, PluginInstallService, PluginManifestError, PluginRuntime, PluginScope, PluginStore, remote_exports as Remote, resource_exports as Resource, social_exports as Social, special_exports as Special, StoredPluginModuleReader, user_exports as User, ZipPackageCodec, configurePluginHost, createAuthCapability, createConfigCapability, createContentCapability, createDefaultCapabilities, createI18nCapability, createLifecycleCapability, createModelCapability, createRemoteCapability, createResourceCapability, createSpecialCapability, createUserCapability, defineCapability, defineContributionChannel, defineDeltaComicPlugin, defineInternalPlugin, findPluginDependencyCycles, installPlugin, isPluginManifestCompatible, parsePluginManifest, planPluginDependencies, pluginCatalog, pluginCatalogIdFromInstallInput, pluginCatalogInstallInput, pluginConfigStore, pluginContributions, pluginI18n, pluginInstaller, pluginMessageKey, pluginModelChannels, pluginRemoteSelectionChannel, pluginRuntime, pluginStore, resolvePluginIconUrl, safePluginPath, setPluginEnabled, setPluginKind, translatePluginText, uninstallPlugin, updatePlugin, updatePluginByName, useConfig, usePluginStore };
534
2410
 
535
2411
  //# sourceMappingURL=index.mjs.map