@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.
- package/dist/lib/index.d.mts +945 -748
- package/dist/lib/index.mjs +2002 -126
- package/dist/lib/index.mjs.map +1 -1
- package/dist/vite/index.d.mts +2 -3
- package/dist/vite/index.mjs +1 -1
- package/dist/vite/index.mjs.map +1 -1
- package/package.json +18 -17
- package/dist/global-BSAsAv3E.mjs +0 -1443
- package/dist/global-BSAsAv3E.mjs.map +0 -1
- package/dist/native-CHH-Olpj.mjs +0 -63
- package/dist/native-CHH-Olpj.mjs.map +0 -1
- package/dist/runtime-Dk50hXwU.mjs +0 -663
- package/dist/runtime-Dk50hXwU.mjs.map +0 -1
- package/dist/storage-DMKDlPzl.mjs +0 -351
- package/dist/storage-DMKDlPzl.mjs.map +0 -1
package/dist/lib/index.mjs
CHANGED
|
@@ -1,70 +1,562 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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 =
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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:
|
|
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:
|
|
106
|
-
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 =
|
|
608
|
+
const entry = record$1(manifest.entry, "manifest.entry");
|
|
115
609
|
result.entry = {
|
|
116
|
-
jsPath:
|
|
117
|
-
...entry.cssPath === void 0 ? {} : { 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 =
|
|
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:
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
|
|
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/
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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 {
|
|
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
|