@hoardodile/sdk-react 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +61 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +481 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
- package/src/context.tsx +28 -0
- package/src/define-api.ts +100 -0
- package/src/fixtures.tsx +25 -0
- package/src/i18n.test.tsx +147 -0
- package/src/i18n.ts +96 -0
- package/src/index.ts +23 -0
- package/src/query.ts +407 -0
- package/src/root.tsx +144 -0
- package/src/use-cache-writer.ts +61 -0
- package/src/use-extract-progress.test.tsx +118 -0
- package/src/use-extract-progress.ts +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { createContext, useContext, useSyncExternalStore, createElement, useRef, useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import { createWebPluginAPI, getPluginContext, subscribeToVisibility, getVisibilitySnapshot, mountPlugin, ensureHostBridge, createIframeHostAPI, applyTheme, applyFonts, getPluginPrefStore, subscribeToPrefChanges, extractPrefPayload, setPluginPref, invalidatePushKeys, extractFontsPayload, extractThemePayload } from '@hoardodile/sdk-web';
|
|
3
|
+
export { createWebPluginAPI } from '@hoardodile/sdk-web';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
import { uiCatalogFor } from '@hoardodile/i18n/catalogs/ui';
|
|
6
|
+
import { SUPPORTED_LANGUAGES, isSupportedLanguage } from '@hoardodile/i18n/core';
|
|
7
|
+
import { createI18n } from '@hoardodile/i18n/create-i18n';
|
|
8
|
+
import { setI18n, useTranslation } from 'react-i18next';
|
|
9
|
+
import { flushSync } from 'react-dom';
|
|
10
|
+
import { createRoot } from 'react-dom/client';
|
|
11
|
+
|
|
12
|
+
// src/context.tsx
|
|
13
|
+
var PluginAPIContext = createContext(null);
|
|
14
|
+
var PluginAPIProvider = PluginAPIContext.Provider;
|
|
15
|
+
function usePluginAPI() {
|
|
16
|
+
const api = useContext(PluginAPIContext);
|
|
17
|
+
if (api === null) {
|
|
18
|
+
throw new Error("usePluginAPI must be used within a PluginAPIProvider");
|
|
19
|
+
}
|
|
20
|
+
return api;
|
|
21
|
+
}
|
|
22
|
+
function definePluginAPI(options) {
|
|
23
|
+
const decodeAnchor = options?.decodeAnchor;
|
|
24
|
+
function useTypedPluginAPI() {
|
|
25
|
+
const api = useContext(PluginAPIContext);
|
|
26
|
+
if (api === null) {
|
|
27
|
+
throw new Error("usePluginAPI must be used within a PluginAPIProvider");
|
|
28
|
+
}
|
|
29
|
+
return api;
|
|
30
|
+
}
|
|
31
|
+
function useTypedAnchorJump(cb) {
|
|
32
|
+
const api = useTypedPluginAPI();
|
|
33
|
+
const cbRef = useRef(cb);
|
|
34
|
+
cbRef.current = cb;
|
|
35
|
+
useEffect(
|
|
36
|
+
function subscribe() {
|
|
37
|
+
return api.onAnchorJump(function handle(anchor) {
|
|
38
|
+
if (decodeAnchor === void 0) {
|
|
39
|
+
cbRef.current(anchor.data);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const data = decodeAnchor(anchor.data);
|
|
43
|
+
if (data === void 0) return;
|
|
44
|
+
cbRef.current(data);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
[api]
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
PluginAPIProvider: PluginAPIContext.Provider,
|
|
52
|
+
usePluginAPI: useTypedPluginAPI,
|
|
53
|
+
useAnchorJump: useTypedAnchorJump
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function StubPluginAPIProvider({
|
|
57
|
+
api,
|
|
58
|
+
children
|
|
59
|
+
}) {
|
|
60
|
+
return /* @__PURE__ */ jsx(PluginAPIProvider, { value: createWebPluginAPI(api), children });
|
|
61
|
+
}
|
|
62
|
+
function resolveLocale(lang, available) {
|
|
63
|
+
if (available.has(lang)) return lang;
|
|
64
|
+
const base = lang.split("-")[0];
|
|
65
|
+
if (available.has(base)) return base;
|
|
66
|
+
return "en";
|
|
67
|
+
}
|
|
68
|
+
function createPluginTranslation(bundles) {
|
|
69
|
+
const availableLangs = /* @__PURE__ */ new Set([
|
|
70
|
+
...Object.keys(bundles),
|
|
71
|
+
...SUPPORTED_LANGUAGES
|
|
72
|
+
]);
|
|
73
|
+
const resources = {};
|
|
74
|
+
for (const language of availableLangs) {
|
|
75
|
+
const base = language.split("-")[0];
|
|
76
|
+
resources[language] = {
|
|
77
|
+
...isSupportedLanguage(base) ? { ui: uiCatalogFor(base) } : {},
|
|
78
|
+
...bundles[language] === void 0 ? {} : { plugin: bundles[language] }
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const initial = resolveLocale(
|
|
82
|
+
getPluginContext()?.language ?? "en",
|
|
83
|
+
availableLangs
|
|
84
|
+
);
|
|
85
|
+
const instance = createI18n({ lng: initial, resources });
|
|
86
|
+
setI18n(instance);
|
|
87
|
+
let subscribed = false;
|
|
88
|
+
function subscribeToLanguageChanges() {
|
|
89
|
+
if (subscribed) return;
|
|
90
|
+
subscribed = true;
|
|
91
|
+
ensureHostBridge().subscribe("languageChanged", (data) => {
|
|
92
|
+
const language = typeof data === "string" ? data : String(data.language ?? "");
|
|
93
|
+
void instance.changeLanguage(resolveLocale(language, availableLangs));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function useTranslation$1() {
|
|
97
|
+
useEffect(subscribeToLanguageChanges, []);
|
|
98
|
+
const { t, i18n } = useTranslation("plugin", {
|
|
99
|
+
i18n: instance,
|
|
100
|
+
useSuspense: false
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
t,
|
|
104
|
+
language: i18n.resolvedLanguage ?? i18n.language
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return { useTranslation: useTranslation$1 };
|
|
108
|
+
}
|
|
109
|
+
function buildQuerySuccessState(data) {
|
|
110
|
+
return { data, isLoading: false, isError: false, error: null };
|
|
111
|
+
}
|
|
112
|
+
function buildQueryErrorState(err) {
|
|
113
|
+
return {
|
|
114
|
+
data: void 0,
|
|
115
|
+
isLoading: false,
|
|
116
|
+
isError: true,
|
|
117
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function buildQueryLoadingState() {
|
|
121
|
+
return { data: void 0, isLoading: true, isError: false, error: null };
|
|
122
|
+
}
|
|
123
|
+
function useHostQuery(host, options) {
|
|
124
|
+
const { method, params, invalidateKey, extraDeps = [] } = options;
|
|
125
|
+
const [state, setState] = useState(buildQueryLoadingState);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
let cancelled = false;
|
|
128
|
+
setState(buildQueryLoadingState());
|
|
129
|
+
function fetchData() {
|
|
130
|
+
const args = params === void 0 ? [] : [params];
|
|
131
|
+
host.request(method, ...args).then((result) => {
|
|
132
|
+
if (!cancelled) {
|
|
133
|
+
setState(buildQuerySuccessState(result));
|
|
134
|
+
}
|
|
135
|
+
}).catch((err) => {
|
|
136
|
+
if (!cancelled) {
|
|
137
|
+
setState(buildQueryErrorState(err));
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
fetchData();
|
|
142
|
+
const unsub = host.subscribe(invalidateKey, fetchData);
|
|
143
|
+
return function cleanup() {
|
|
144
|
+
cancelled = true;
|
|
145
|
+
unsub();
|
|
146
|
+
};
|
|
147
|
+
}, [host, method, invalidateKey, ...extraDeps]);
|
|
148
|
+
return state;
|
|
149
|
+
}
|
|
150
|
+
function useFileList(host, contextDeps) {
|
|
151
|
+
return useHostQuery(host, {
|
|
152
|
+
method: "listFiles",
|
|
153
|
+
params: void 0,
|
|
154
|
+
invalidateKey: invalidatePushKeys.resource,
|
|
155
|
+
extraDeps: contextDeps
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function useMessageList(host, contextDeps) {
|
|
159
|
+
return useHostQuery(host, {
|
|
160
|
+
method: "listMessages",
|
|
161
|
+
params: void 0,
|
|
162
|
+
invalidateKey: invalidatePushKeys.messages,
|
|
163
|
+
extraDeps: contextDeps
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function useDanmakuList(host, contextDeps, filter) {
|
|
167
|
+
return useHostQuery(host, {
|
|
168
|
+
method: "listDanmaku",
|
|
169
|
+
params: { filter },
|
|
170
|
+
invalidateKey: invalidatePushKeys.danmaku,
|
|
171
|
+
// The filter object is a fresh literal on every render; a stable
|
|
172
|
+
// serialization keeps the effect from refetching in a loop while
|
|
173
|
+
// still refetching when any filter value actually changes.
|
|
174
|
+
extraDeps: [
|
|
175
|
+
...contextDeps,
|
|
176
|
+
filter === void 0 ? void 0 : JSON.stringify(filter)
|
|
177
|
+
]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function useHostMutation(host, method) {
|
|
181
|
+
const [isPending, setIsPending] = useState(false);
|
|
182
|
+
async function mutate(args) {
|
|
183
|
+
setIsPending(true);
|
|
184
|
+
try {
|
|
185
|
+
const requestArgs = args === void 0 ? [] : [args];
|
|
186
|
+
return await host.request(
|
|
187
|
+
method,
|
|
188
|
+
...requestArgs
|
|
189
|
+
);
|
|
190
|
+
} finally {
|
|
191
|
+
setIsPending(false);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { mutate, isPending };
|
|
195
|
+
}
|
|
196
|
+
function useCreateMessage(host) {
|
|
197
|
+
const base = useHostMutation(host, "createMessage");
|
|
198
|
+
return {
|
|
199
|
+
isPending: base.isPending,
|
|
200
|
+
async mutate(input) {
|
|
201
|
+
return base.mutate({
|
|
202
|
+
body: input.body,
|
|
203
|
+
anchor: input.anchor === void 0 ? void 0 : { data: input.anchor }
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function useCreateDanmaku(host) {
|
|
209
|
+
const base = useHostMutation(host, "createDanmaku");
|
|
210
|
+
return {
|
|
211
|
+
isPending: base.isPending,
|
|
212
|
+
async mutate(input) {
|
|
213
|
+
return base.mutate({
|
|
214
|
+
text: input.text,
|
|
215
|
+
anchor: { data: input.anchor },
|
|
216
|
+
mode: input.mode
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function encodePrefValue(codec, value) {
|
|
222
|
+
return codec !== void 0 ? codec.encode(value) : String(value);
|
|
223
|
+
}
|
|
224
|
+
function decodePrefValue(codec, raw, fallback) {
|
|
225
|
+
if (codec === void 0) return raw;
|
|
226
|
+
const decoded = codec.decode(raw);
|
|
227
|
+
return decoded !== void 0 ? decoded : fallback;
|
|
228
|
+
}
|
|
229
|
+
function usePref(host, key, defaultValue, codec) {
|
|
230
|
+
const store = getPluginPrefStore();
|
|
231
|
+
const encodedDefault = useMemo(
|
|
232
|
+
function computeEncodedDefault() {
|
|
233
|
+
return encodePrefValue(codec, defaultValue);
|
|
234
|
+
},
|
|
235
|
+
[codec, defaultValue]
|
|
236
|
+
);
|
|
237
|
+
const [raw, setRawState] = useState(function getInitial() {
|
|
238
|
+
return store.get(key) ?? encodedDefault;
|
|
239
|
+
});
|
|
240
|
+
useEffect(
|
|
241
|
+
function subscribeToStoreChanges() {
|
|
242
|
+
return subscribeToPrefChanges(key, function onChange() {
|
|
243
|
+
setRawState(getPluginPrefStore().get(key) ?? encodedDefault);
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
[key, encodedDefault]
|
|
247
|
+
);
|
|
248
|
+
useEffect(
|
|
249
|
+
function subscribeToHostPush() {
|
|
250
|
+
return host.subscribe("prefsChanged", function handlePrefPush(data) {
|
|
251
|
+
const payload = extractPrefPayload(data);
|
|
252
|
+
if (payload === void 0 || payload.key !== key) return;
|
|
253
|
+
if (payload.value !== void 0) {
|
|
254
|
+
setPluginPref(key, payload.value);
|
|
255
|
+
setRawState(payload.value);
|
|
256
|
+
} else {
|
|
257
|
+
setRawState(encodedDefault);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
[host, key, encodedDefault]
|
|
262
|
+
);
|
|
263
|
+
const value = useMemo(
|
|
264
|
+
function decodeValue() {
|
|
265
|
+
return decodePrefValue(codec, raw, defaultValue);
|
|
266
|
+
},
|
|
267
|
+
[raw, codec, defaultValue]
|
|
268
|
+
);
|
|
269
|
+
function setValue(next) {
|
|
270
|
+
const encoded = encodePrefValue(codec, next);
|
|
271
|
+
setPluginPref(key, encoded);
|
|
272
|
+
setRawState(encoded);
|
|
273
|
+
host.request("setPref", { key, value: encoded }).catch(() => {
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return [value, setValue];
|
|
277
|
+
}
|
|
278
|
+
function useHostPush(host, key, extract, initial) {
|
|
279
|
+
const [value, setValue] = useState(initial);
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
const unsub = host.subscribe(key, function handlePush(data) {
|
|
282
|
+
const patch = extract(data);
|
|
283
|
+
if (patch !== void 0) {
|
|
284
|
+
setValue((prev) => ({ ...prev, ...patch }));
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
return function cleanup() {
|
|
288
|
+
unsub();
|
|
289
|
+
};
|
|
290
|
+
}, [host, key, extract]);
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
function extractThemePatch(data) {
|
|
294
|
+
const { resolvedTheme, palette, iconStyle } = extractThemePayload(data);
|
|
295
|
+
if (resolvedTheme === void 0 && palette === void 0 && iconStyle === void 0) {
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
...resolvedTheme !== void 0 ? { resolvedTheme } : {},
|
|
300
|
+
...palette !== void 0 ? { palette } : {},
|
|
301
|
+
...iconStyle !== void 0 ? { iconStyle } : {}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function useTheme(host, initialResolvedTheme, initialPalette, initialIconStyle) {
|
|
305
|
+
return useHostPush(host, "themeChanged", extractThemePatch, {
|
|
306
|
+
resolvedTheme: initialResolvedTheme,
|
|
307
|
+
palette: initialPalette,
|
|
308
|
+
iconStyle: initialIconStyle
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function useFont(host, initialFonts) {
|
|
312
|
+
return useHostPush(host, "fontsChanged", extractFontsPayload, initialFonts);
|
|
313
|
+
}
|
|
314
|
+
function createPluginQueryAPI(host, ctx) {
|
|
315
|
+
const contextDeps = [ctx.resId];
|
|
316
|
+
return {
|
|
317
|
+
useFileList: () => useFileList(host, contextDeps),
|
|
318
|
+
useMessageList: () => useMessageList(host, contextDeps),
|
|
319
|
+
useCreateMessage: () => useCreateMessage(host),
|
|
320
|
+
useDanmakuList: (filter) => useDanmakuList(host, contextDeps, filter),
|
|
321
|
+
useCreateDanmaku: () => useCreateDanmaku(host),
|
|
322
|
+
usePref: (key, defaultValue, codec) => usePref(host, key, defaultValue, codec),
|
|
323
|
+
useTheme: () => useTheme(host, ctx.resolvedTheme, ctx.palette, ctx.iconStyle),
|
|
324
|
+
useFont: () => useFont(host, ctx.fonts)
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function ThemeSync({ children }) {
|
|
328
|
+
const api = usePluginAPI();
|
|
329
|
+
const { resolvedTheme, palette, iconStyle } = api.useTheme();
|
|
330
|
+
useEffect(
|
|
331
|
+
function applyOnChange() {
|
|
332
|
+
applyTheme(resolvedTheme, palette, iconStyle);
|
|
333
|
+
},
|
|
334
|
+
[resolvedTheme, palette, iconStyle]
|
|
335
|
+
);
|
|
336
|
+
return children;
|
|
337
|
+
}
|
|
338
|
+
function FontSync({ children }) {
|
|
339
|
+
const api = usePluginAPI();
|
|
340
|
+
const { family, cssPaths } = api.useFont();
|
|
341
|
+
useEffect(
|
|
342
|
+
function applyOnChange() {
|
|
343
|
+
applyFonts(family, cssPaths);
|
|
344
|
+
},
|
|
345
|
+
[family, cssPaths]
|
|
346
|
+
);
|
|
347
|
+
return children;
|
|
348
|
+
}
|
|
349
|
+
function useVisibility() {
|
|
350
|
+
return useSyncExternalStore(subscribeToVisibility, getVisibilitySnapshot);
|
|
351
|
+
}
|
|
352
|
+
function createPluginRoot(config) {
|
|
353
|
+
let root;
|
|
354
|
+
mountPlugin(function onContext(ctx) {
|
|
355
|
+
flushSync(() => {
|
|
356
|
+
if (root === void 0) {
|
|
357
|
+
const el = document.getElementById("root");
|
|
358
|
+
if (el === null) {
|
|
359
|
+
console.error("[plugin] #root element not found \u2014 cannot mount");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
root = createRoot(el);
|
|
363
|
+
}
|
|
364
|
+
const host = ensureHostBridge();
|
|
365
|
+
const baseApi = createIframeHostAPI(ctx);
|
|
366
|
+
const api = {
|
|
367
|
+
...baseApi,
|
|
368
|
+
...createPluginQueryAPI(host, {
|
|
369
|
+
resolvedTheme: ctx.resolvedTheme,
|
|
370
|
+
palette: ctx.palette,
|
|
371
|
+
iconStyle: ctx.iconStyle,
|
|
372
|
+
fonts: ctx.fonts,
|
|
373
|
+
resId: ctx.resId
|
|
374
|
+
})
|
|
375
|
+
};
|
|
376
|
+
applyTheme(ctx.resolvedTheme, ctx.palette, ctx.iconStyle);
|
|
377
|
+
applyFonts(ctx.fonts.family, ctx.fonts.cssPaths);
|
|
378
|
+
root.render(
|
|
379
|
+
createElement(
|
|
380
|
+
config.provider,
|
|
381
|
+
{ value: api },
|
|
382
|
+
createElement(
|
|
383
|
+
ThemeSync,
|
|
384
|
+
null,
|
|
385
|
+
createElement(
|
|
386
|
+
FontSync,
|
|
387
|
+
null,
|
|
388
|
+
createElement(
|
|
389
|
+
config.render,
|
|
390
|
+
config.remountOnResourceChange === false ? {} : { key: ctx.resId }
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
var DEFAULT_DEBOUNCE_MS = 500;
|
|
400
|
+
function useCacheWriter(options) {
|
|
401
|
+
const api = usePluginAPI();
|
|
402
|
+
const {
|
|
403
|
+
key,
|
|
404
|
+
value,
|
|
405
|
+
encode,
|
|
406
|
+
disabled = false,
|
|
407
|
+
debounceMs = DEFAULT_DEBOUNCE_MS
|
|
408
|
+
} = options;
|
|
409
|
+
const latestRef = useRef(value);
|
|
410
|
+
latestRef.current = value;
|
|
411
|
+
const encodeRef = useRef(encode);
|
|
412
|
+
encodeRef.current = encode;
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
if (disabled || value === void 0) return;
|
|
415
|
+
const timer = setTimeout(() => {
|
|
416
|
+
const snap = latestRef.current;
|
|
417
|
+
if (snap === void 0) return;
|
|
418
|
+
api.setCache(key, encodeRef.current(snap));
|
|
419
|
+
}, debounceMs);
|
|
420
|
+
return () => clearTimeout(timer);
|
|
421
|
+
}, [api, key, value, disabled, debounceMs]);
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
if (disabled) return;
|
|
424
|
+
function flush() {
|
|
425
|
+
const snap = latestRef.current;
|
|
426
|
+
if (snap === void 0) return;
|
|
427
|
+
api.setCache(key, encodeRef.current(snap));
|
|
428
|
+
}
|
|
429
|
+
window.addEventListener("pagehide", flush);
|
|
430
|
+
window.addEventListener("beforeunload", flush);
|
|
431
|
+
return () => {
|
|
432
|
+
window.removeEventListener("pagehide", flush);
|
|
433
|
+
window.removeEventListener("beforeunload", flush);
|
|
434
|
+
flush();
|
|
435
|
+
};
|
|
436
|
+
}, [api, key, disabled]);
|
|
437
|
+
}
|
|
438
|
+
var POLL_INTERVAL_MS = 300;
|
|
439
|
+
function useExtractProgress() {
|
|
440
|
+
const api = usePluginAPI();
|
|
441
|
+
const [state, setState] = useState({ state: "idle" });
|
|
442
|
+
const seenProgress = useRef(false);
|
|
443
|
+
useEffect(
|
|
444
|
+
function pollProgress() {
|
|
445
|
+
let cancelled = false;
|
|
446
|
+
seenProgress.current = false;
|
|
447
|
+
setState({ state: "idle" });
|
|
448
|
+
async function poll() {
|
|
449
|
+
let payload;
|
|
450
|
+
try {
|
|
451
|
+
const response = await fetch(api.extractProgressUrl());
|
|
452
|
+
payload = await response.json();
|
|
453
|
+
} catch {
|
|
454
|
+
payload = null;
|
|
455
|
+
}
|
|
456
|
+
if (cancelled) return;
|
|
457
|
+
if (payload !== null && typeof payload === "object") {
|
|
458
|
+
const { done, total } = payload;
|
|
459
|
+
if (typeof done === "number" && typeof total === "number") {
|
|
460
|
+
seenProgress.current = true;
|
|
461
|
+
setState({ state: "extracting", done, total });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
setState(seenProgress.current ? { state: "done" } : { state: "idle" });
|
|
466
|
+
}
|
|
467
|
+
void poll();
|
|
468
|
+
const timer = setInterval(() => void poll(), POLL_INTERVAL_MS);
|
|
469
|
+
return () => {
|
|
470
|
+
cancelled = true;
|
|
471
|
+
clearInterval(timer);
|
|
472
|
+
};
|
|
473
|
+
},
|
|
474
|
+
[api]
|
|
475
|
+
);
|
|
476
|
+
return state;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export { PluginAPIProvider, StubPluginAPIProvider, createPluginQueryAPI, createPluginRoot, createPluginTranslation, definePluginAPI, useCacheWriter, useExtractProgress, usePluginAPI, useVisibility };
|
|
480
|
+
//# sourceMappingURL=index.js.map
|
|
481
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/define-api.ts","../src/fixtures.tsx","../src/i18n.ts","../src/query.ts","../src/root.tsx","../src/use-cache-writer.ts","../src/use-extract-progress.ts"],"names":["useContext","useTranslation","useEffect","useReactTranslation","ensureHostBridge","useRef","useState"],"mappings":"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","file":"index.js","sourcesContent":["import type { ReactivePluginAPI, WebPluginAPI } from \"@hoardodile/sdk-web\"\nimport { createContext, useContext } from \"react\"\n\n/**\n * The full plugin API as delivered to React plugin components: the\n * imperative {@link WebPluginAPI} plus the reactive hooks implemented by\n * this package's adapter. Typed with default (unknown) schema slots; the\n * typed provider from `definePluginAPI` narrows it per plugin.\n */\nexport type BasePluginAPI = WebPluginAPI & ReactivePluginAPI\n\nexport const PluginAPIContext = createContext<BasePluginAPI | null>(null)\n\n/**\n * Provides the plugin API to the component tree. Usually created\n * implicitly via {@link createPluginRoot}; the typed provider from\n * {@link definePluginAPI} narrows `BasePluginAPI` to the plugin's\n * schema.\n */\nexport const PluginAPIProvider = PluginAPIContext.Provider\n\nexport function usePluginAPI(): BasePluginAPI {\n\tconst api = useContext(PluginAPIContext)\n\tif (api === null) {\n\t\tthrow new Error(\"usePluginAPI must be used within a PluginAPIProvider\")\n\t}\n\treturn api\n}\n","import type { PluginSchema } from \"@hoardodile/sdk-types\"\nimport type {\n\tAnchorData,\n\tReactivePluginAPI,\n\tWebPluginAPI,\n} from \"@hoardodile/sdk-web\"\nimport type { Provider } from \"react\"\nimport { useContext, useEffect, useRef } from \"react\"\nimport { PluginAPIContext } from \"./context.tsx\"\n\n/** The full API seen by React plugin components: imperative + hooks. */\nexport type FullPluginAPI<TSchema extends PluginSchema> =\n\tWebPluginAPI<TSchema> & ReactivePluginAPI<TSchema>\n\nexport type DefinePluginAPIOptions<TSchema extends PluginSchema> = {\n\t/**\n\t * Validate incoming anchor data (host → plugin) against the schema's\n\t * `anchor` slot. Anchors that fail decoding are dropped silently and\n\t * never reach the `useAnchorJump` callback. Declare this whenever the\n\t * schema declares an `anchor` type.\n\t */\n\treadonly decodeAnchor?: (data: unknown) => TSchema[\"anchor\"] | undefined\n}\n\n/**\n * Define a typed plugin API context. The schema is declared once at module\n * level — every consumer below gets properly typed access without repeating\n * generics.\n *\n * The returned provider and hook share the same React context as the default\n * {@link PluginAPIProvider}, so plugin roots created by `createPluginRoot`\n * automatically satisfy typed consumers when the same provider is passed in.\n *\n * ```typescript\n * interface VideoSchema { file: VideoFile; sourceMeta: VideoSourceMeta; anchor: VideoTimeAnchor }\n * const { PluginAPIProvider, usePluginAPI, useAnchorJump } = definePluginAPI<VideoSchema>({\n * decodeAnchor: decodeVideoTimeAnchor,\n * })\n *\n * function Viewer() {\n * const api = usePluginAPI()\n * const { data: files } = api.useFileList()\n * // files → readonly VideoFile[] | undefined\n * }\n * ```\n */\nexport function definePluginAPI<TSchema extends PluginSchema = PluginSchema>(\n\toptions?: DefinePluginAPIOptions<TSchema>,\n): {\n\treadonly PluginAPIProvider: Provider<FullPluginAPI<TSchema> | null>\n\treadonly usePluginAPI: () => FullPluginAPI<TSchema>\n\treadonly useAnchorJump: (cb: (anchor: TSchema[\"anchor\"]) => void) => void\n} {\n\tconst decodeAnchor = options?.decodeAnchor\n\n\tfunction useTypedPluginAPI(): FullPluginAPI<TSchema> {\n\t\tconst api = useContext(PluginAPIContext)\n\t\tif (api === null) {\n\t\t\tthrow new Error(\"usePluginAPI must be used within a PluginAPIProvider\")\n\t\t}\n\t\t// SDK boundary: the shared context stores the base API; the schema\n\t\t// slots narrow it for this plugin. Declared once here so consumers\n\t\t// stay cast-free.\n\t\treturn api as unknown as FullPluginAPI<TSchema>\n\t}\n\n\t/**\n\t * Typed anchor-jump hook: incoming anchor data is decoded once at the\n\t * SDK boundary, so the callback receives the schema's anchor type\n\t * directly with no manual narrowing. The latest callback is invoked\n\t * without resubscribing on every render.\n\t */\n\tfunction useTypedAnchorJump(cb: (anchor: TSchema[\"anchor\"]) => void) {\n\t\tconst api = useTypedPluginAPI()\n\t\tconst cbRef = useRef(cb)\n\t\tcbRef.current = cb\n\n\t\tuseEffect(\n\t\t\tfunction subscribe() {\n\t\t\t\treturn api.onAnchorJump(function handle(anchor: AnchorData) {\n\t\t\t\t\tif (decodeAnchor === undefined) {\n\t\t\t\t\t\tcbRef.current(anchor.data as TSchema[\"anchor\"])\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst data = decodeAnchor(anchor.data)\n\t\t\t\t\tif (data === undefined) return\n\t\t\t\t\tcbRef.current(data)\n\t\t\t\t})\n\t\t\t},\n\t\t\t[api],\n\t\t)\n\t}\n\n\treturn {\n\t\tPluginAPIProvider:\n\t\t\tPluginAPIContext.Provider as Provider<FullPluginAPI<TSchema> | null>,\n\t\tusePluginAPI: useTypedPluginAPI,\n\t\tuseAnchorJump: useTypedAnchorJump,\n\t}\n}\n","import { createWebPluginAPI, type DeepPartial } from \"@hoardodile/sdk-web\"\nimport type { ReactNode } from \"react\"\nimport { type BasePluginAPI, PluginAPIProvider } from \"./context.tsx\"\n\nexport { createWebPluginAPI, type DeepPartial } from \"@hoardodile/sdk-web\"\n\n/**\n * Wrap children with a stubbed API provider for tests. Builds the stub\n * via `createWebPluginAPI` from `@hoardodile/sdk-web` (re-exported\n * below) — the imperative surface plus no-op reactive hooks, overridable\n * via `api`.\n */\nexport function StubPluginAPIProvider({\n\tapi,\n\tchildren,\n}: {\n\treadonly api?: DeepPartial<BasePluginAPI>\n\treadonly children: ReactNode\n}) {\n\treturn (\n\t\t<PluginAPIProvider value={createWebPluginAPI(api)}>\n\t\t\t{children}\n\t\t</PluginAPIProvider>\n\t)\n}\n","import { uiCatalogFor } from \"@hoardodile/i18n/catalogs/ui\"\nimport { isSupportedLanguage, SUPPORTED_LANGUAGES } from \"@hoardodile/i18n/core\"\nimport { createI18n } from \"@hoardodile/i18n/create-i18n\"\nimport { ensureHostBridge, getPluginContext } from \"@hoardodile/sdk-web\"\nimport type { Resource } from \"i18next\"\nimport { useEffect } from \"react\"\nimport { setI18n, useTranslation as useReactTranslation } from \"react-i18next\"\n\ntype RawBundle = Record<string, unknown>\n\ntype InterpolationVars = Record<string, string | number>\n\ntype PluginTranslation = {\n\treadonly t: (key: string, vars?: InterpolationVars) => string\n\treadonly language: string\n}\n\nfunction resolveLocale(lang: string, available: Set<string>): string {\n\tif (available.has(lang)) return lang\n\tconst base = lang.split(\"-\")[0]!\n\tif (available.has(base)) return base\n\treturn \"en\"\n}\n\n/**\n * Creates a `useTranslation` hook backed by the given locale bundles plus\n * the shared `ui` catalog namespace (so `@hoardodile/ui` components\n * render localized chrome in every supported host language).\n *\n * Backed by i18next/react-i18next with the same options as the host\n * surfaces: the language follows the plugin context, updates when the\n * host sends a `languageChanged` push, interpolates `{{var}}`\n * placeholders, and falls back to English (via `fallbackLng`) for\n * languages the plugin's own bundle does not ship.\n */\nexport function createPluginTranslation(bundles: Record<string, RawBundle>): {\n\treadonly useTranslation: () => PluginTranslation\n} {\n\tconst availableLangs = new Set<string>([\n\t\t...Object.keys(bundles),\n\t\t...SUPPORTED_LANGUAGES,\n\t])\n\n\t// The small shared `ui` namespace (every supported language, so ui\n\t// chrome always matches the host language) plus the plugin's own\n\t// `plugin` namespace — never the full app catalog (the iframe bundle\n\t// stays a fraction of the SPA's i18n payload).\n\tconst resources: Resource = {}\n\tfor (const language of availableLangs) {\n\t\tconst base = language.split(\"-\")[0]!\n\t\tresources[language] = {\n\t\t\t...(isSupportedLanguage(base) ? { ui: uiCatalogFor(base) } : {}),\n\t\t\t...(bundles[language] === undefined ? {} : { plugin: bundles[language] }),\n\t\t}\n\t}\n\n\tconst initial = resolveLocale(\n\t\tgetPluginContext()?.language ?? \"en\",\n\t\tavailableLangs,\n\t)\n\tconst instance = createI18n({ lng: initial, resources })\n\n\t// Bind as react-i18next's default instance so every `@hoardodile/ui`\n\t// component rendered in this iframe resolves the same instance.\n\tsetI18n(instance)\n\n\tlet subscribed = false\n\tfunction subscribeToLanguageChanges(): void {\n\t\tif (subscribed) return\n\t\tsubscribed = true\n\t\tensureHostBridge().subscribe(\"languageChanged\", (data) => {\n\t\t\t// The wire payload is a bare language-code string (predates the\n\t\t\t// typed protocol table); accept the legacy object shape too so\n\t\t\t// plugins compiled against either contract keep switching.\n\t\t\tconst language =\n\t\t\t\ttypeof data === \"string\"\n\t\t\t\t\t? data\n\t\t\t\t\t: String((data as { language?: string }).language ?? \"\")\n\t\t\tvoid instance.changeLanguage(resolveLocale(language, availableLangs))\n\t\t})\n\t}\n\n\tfunction useTranslation(): PluginTranslation {\n\t\tuseEffect(subscribeToLanguageChanges, [])\n\t\tconst { t, i18n } = useReactTranslation(\"plugin\", {\n\t\t\ti18n: instance,\n\t\t\tuseSuspense: false,\n\t\t})\n\t\treturn {\n\t\t\tt: t as unknown as PluginTranslation[\"t\"],\n\t\t\tlanguage: i18n.resolvedLanguage ?? i18n.language,\n\t\t}\n\t}\n\n\treturn { useTranslation }\n}\n","import type {\n\tAnchorData,\n\tDanmaku,\n\tDanmakuListFilter,\n\tDanmakuMode,\n\tMessage,\n\tPluginSchema,\n} from \"@hoardodile/sdk-types\"\nimport type {\n\tCodec,\n\tHost,\n\tMutationState,\n\tPluginFonts,\n\tQueryState,\n\tReactivePluginAPI,\n\tTheme,\n} from \"@hoardodile/sdk-web\"\nimport {\n\textractFontsPayload,\n\textractPrefPayload,\n\textractThemePayload,\n\tgetPluginPrefStore,\n\tinvalidatePushKeys,\n\tsetPluginPref,\n\tsubscribeToPrefChanges,\n} from \"@hoardodile/sdk-web\"\nimport { useEffect, useMemo, useState } from \"react\"\n\n// ── Query state helpers ──────────────────────────────────────────────────\n\nfunction buildQuerySuccessState<T>(data: T): QueryState<T> {\n\treturn { data, isLoading: false, isError: false, error: null }\n}\n\nfunction buildQueryErrorState(err: unknown): QueryState<never> {\n\treturn {\n\t\tdata: undefined,\n\t\tisLoading: false,\n\t\tisError: true,\n\t\terror: err instanceof Error ? err : new Error(String(err)),\n\t}\n}\n\nfunction buildQueryLoadingState(): QueryState<never> {\n\treturn { data: undefined, isLoading: true, isError: false, error: null }\n}\n\n// ── Base query hook ──────────────────────────────────────────────────────\n\ntype PluginRequestKey = keyof import(\"@hoardodile/sdk-web\").PluginRequests\ntype HostPushKey = keyof import(\"@hoardodile/sdk-web\").HostPushes\n\ntype UseHostQueryOptions<K extends PluginRequestKey> = {\n\treadonly method: K\n\treadonly params: import(\"@hoardodile/sdk-web\").RequestInput<K>\n\treadonly invalidateKey: HostPushKey\n\treadonly extraDeps?: readonly unknown[]\n}\n\nfunction useHostQuery<K extends PluginRequestKey, T>(\n\thost: Host,\n\toptions: UseHostQueryOptions<K>,\n): QueryState<T> {\n\tconst { method, params, invalidateKey, extraDeps = [] } = options\n\tconst [state, setState] = useState<QueryState<T>>(buildQueryLoadingState)\n\n\tuseEffect(() => {\n\t\tlet cancelled = false\n\t\tsetState(buildQueryLoadingState())\n\n\t\tfunction fetchData() {\n\t\t\tconst args = params === undefined ? [] : [params]\n\t\t\thost\n\t\t\t\t.request(method, ...(args as never))\n\t\t\t\t.then((result) => {\n\t\t\t\t\tif (!cancelled) {\n\t\t\t\t\t\tsetState(buildQuerySuccessState(result as T))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\tif (!cancelled) {\n\t\t\t\t\t\tsetState(buildQueryErrorState(err))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t}\n\n\t\tfetchData()\n\t\tconst unsub = host.subscribe(invalidateKey, fetchData as never)\n\t\treturn function cleanup() {\n\t\t\tcancelled = true\n\t\t\tunsub()\n\t\t}\n\t}, [host, method, invalidateKey, ...extraDeps])\n\n\treturn state\n}\n\n// ── File queries ─────────────────────────────────────────────────────────\n\nfunction useFileList(host: Host, contextDeps: readonly unknown[]) {\n\treturn useHostQuery<\"listFiles\", readonly string[]>(host, {\n\t\tmethod: \"listFiles\",\n\t\tparams: undefined,\n\t\tinvalidateKey: invalidatePushKeys.resource,\n\t\textraDeps: contextDeps,\n\t})\n}\n// ── Message queries ──────────────────────────────────────────────────────\n\nfunction useMessageList(host: Host, contextDeps: readonly unknown[]) {\n\treturn useHostQuery<\"listMessages\", readonly Message[]>(host, {\n\t\tmethod: \"listMessages\",\n\t\tparams: undefined,\n\t\tinvalidateKey: invalidatePushKeys.messages,\n\t\textraDeps: contextDeps,\n\t})\n}\n\n// ── Danmaku queries ───────────────────────────────────────────────────────\n\nfunction useDanmakuList(\n\thost: Host,\n\tcontextDeps: readonly unknown[],\n\tfilter?: DanmakuListFilter,\n) {\n\treturn useHostQuery<\"listDanmaku\", readonly Danmaku[]>(host, {\n\t\tmethod: \"listDanmaku\",\n\t\tparams: { filter },\n\t\tinvalidateKey: invalidatePushKeys.danmaku,\n\t\t// The filter object is a fresh literal on every render; a stable\n\t\t// serialization keeps the effect from refetching in a loop while\n\t\t// still refetching when any filter value actually changes.\n\t\textraDeps: [\n\t\t\t...contextDeps,\n\t\t\tfilter === undefined ? undefined : JSON.stringify(filter),\n\t\t],\n\t})\n}\n\n// ── Mutations ────────────────────────────────────────────────────────────\n\nfunction useHostMutation<\n\tK extends PluginRequestKey,\n\tTArgs extends import(\"@hoardodile/sdk-web\").RequestInput<K>,\n\tTResult extends import(\"@hoardodile/sdk-web\").RequestOutput<K>,\n>(host: Host, method: K): MutationState<TArgs, TResult> {\n\tconst [isPending, setIsPending] = useState(false)\n\n\tasync function mutate(args: TArgs): Promise<TResult> {\n\t\tsetIsPending(true)\n\t\ttry {\n\t\t\tconst requestArgs = args === undefined ? [] : [args]\n\t\t\treturn (await host.request(\n\t\t\t\tmethod,\n\t\t\t\t...(requestArgs as never),\n\t\t\t)) as unknown as TResult\n\t\t} finally {\n\t\t\tsetIsPending(false)\n\t\t}\n\t}\n\n\treturn { mutate, isPending }\n}\n\nfunction useCreateMessage(\n\thost: Host,\n): MutationState<\n\t{ readonly body: string; readonly anchor?: unknown },\n\tMessage\n> {\n\tconst base = useHostMutation<\n\t\t\"createMessage\",\n\t\t{ readonly body: string; readonly anchor?: AnchorData },\n\t\tMessage\n\t>(host, \"createMessage\")\n\t// The hook input is the raw plugin location data; the wire anchor is\n\t// the `{ data }` envelope (see sdk-web runtime).\n\treturn {\n\t\tisPending: base.isPending,\n\t\tasync mutate(input) {\n\t\t\treturn base.mutate({\n\t\t\t\tbody: input.body,\n\t\t\t\tanchor: input.anchor === undefined ? undefined : { data: input.anchor },\n\t\t\t})\n\t\t},\n\t}\n}\n\nfunction useCreateDanmaku(host: Host): MutationState<\n\t{\n\t\treadonly text: string\n\t\treadonly anchor: unknown\n\t\treadonly mode?: DanmakuMode\n\t},\n\tDanmaku\n> {\n\tconst base = useHostMutation<\n\t\t\"createDanmaku\",\n\t\t{\n\t\t\treadonly text: string\n\t\t\treadonly anchor: AnchorData\n\t\t\treadonly mode?: DanmakuMode\n\t\t},\n\t\tDanmaku\n\t>(host, \"createDanmaku\")\n\treturn {\n\t\tisPending: base.isPending,\n\t\tasync mutate(input) {\n\t\t\treturn base.mutate({\n\t\t\t\ttext: input.text,\n\t\t\t\tanchor: { data: input.anchor },\n\t\t\t\tmode: input.mode,\n\t\t\t})\n\t\t},\n\t}\n}\n\n// ── Preferences hook ─────────────────────────────────────────────────────\n\n/** Serialize a typed value to its stored string form (codec or String()). */\nfunction encodePrefValue<T>(codec: Codec<T> | undefined, value: T): string {\n\treturn codec !== undefined ? codec.encode(value) : String(value)\n}\n\n/** Parse a stored string back to its typed form, falling back on malformed input. */\nfunction decodePrefValue<T>(\n\tcodec: Codec<T> | undefined,\n\traw: string,\n\tfallback: T,\n): T {\n\tif (codec === undefined) return raw as unknown as T\n\tconst decoded = codec.decode(raw)\n\treturn decoded !== undefined ? decoded : fallback\n}\n\nfunction usePref<T>(\n\thost: Host,\n\tkey: string,\n\tdefaultValue: T,\n\tcodec?: Codec<T>,\n): readonly [T, (value: T) => void] {\n\tconst store = getPluginPrefStore()\n\tconst encodedDefault = useMemo(\n\t\tfunction computeEncodedDefault() {\n\t\t\treturn encodePrefValue(codec, defaultValue)\n\t\t},\n\t\t[codec, defaultValue],\n\t)\n\n\tconst [raw, setRawState] = useState(function getInitial() {\n\t\treturn store.get(key) ?? encodedDefault\n\t})\n\n\tuseEffect(\n\t\tfunction subscribeToStoreChanges() {\n\t\t\treturn subscribeToPrefChanges(key, function onChange() {\n\t\t\t\tsetRawState(getPluginPrefStore().get(key) ?? encodedDefault)\n\t\t\t})\n\t\t},\n\t\t[key, encodedDefault],\n\t)\n\n\tuseEffect(\n\t\tfunction subscribeToHostPush() {\n\t\t\treturn host.subscribe(\"prefsChanged\", function handlePrefPush(data) {\n\t\t\t\tconst payload = extractPrefPayload(data)\n\t\t\t\tif (payload === undefined || payload.key !== key) return\n\t\t\t\tif (payload.value !== undefined) {\n\t\t\t\t\tsetPluginPref(key, payload.value)\n\t\t\t\t\tsetRawState(payload.value)\n\t\t\t\t} else {\n\t\t\t\t\tsetRawState(encodedDefault)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[host, key, encodedDefault],\n\t)\n\n\tconst value = useMemo(\n\t\tfunction decodeValue() {\n\t\t\treturn decodePrefValue(codec, raw, defaultValue)\n\t\t},\n\t\t[raw, codec, defaultValue],\n\t)\n\n\tfunction setValue(next: T): void {\n\t\tconst encoded = encodePrefValue(codec, next)\n\t\tsetPluginPref(key, encoded)\n\t\tsetRawState(encoded)\n\t\thost.request(\"setPref\", { key, value: encoded }).catch(() => {})\n\t}\n\n\treturn [value, setValue] as const\n}\n\n// ── Host push hooks ──────────────────────────────────────────────────────\n\n/**\n * Subscribe to a host push and merge the extracted patch into state. The\n * extractor returns `undefined` (or an empty patch) when the push carries\n * no applicable change, so spurious pushes never re-render.\n */\nfunction useHostPush<T>(\n\thost: Host,\n\tkey: HostPushKey,\n\textract: (data: unknown) => Partial<T> | undefined,\n\tinitial: T,\n): T {\n\tconst [value, setValue] = useState(initial)\n\n\tuseEffect(() => {\n\t\tconst unsub = host.subscribe(key, function handlePush(data) {\n\t\t\tconst patch = extract(data)\n\t\t\tif (patch !== undefined) {\n\t\t\t\tsetValue((prev) => ({ ...prev, ...patch }))\n\t\t\t}\n\t\t})\n\t\treturn function cleanup() {\n\t\t\tunsub()\n\t\t}\n\t}, [host, key, extract])\n\n\treturn value\n}\n\n// ── Theme hook ───────────────────────────────────────────────────────────\n\nfunction extractThemePatch(data: unknown): Partial<Theme> | undefined {\n\tconst { resolvedTheme, palette, iconStyle } = extractThemePayload(data)\n\tif (\n\t\tresolvedTheme === undefined &&\n\t\tpalette === undefined &&\n\t\ticonStyle === undefined\n\t) {\n\t\treturn undefined\n\t}\n\treturn {\n\t\t...(resolvedTheme !== undefined ? { resolvedTheme } : {}),\n\t\t...(palette !== undefined ? { palette } : {}),\n\t\t...(iconStyle !== undefined ? { iconStyle } : {}),\n\t}\n}\n\nfunction useTheme(\n\thost: Host,\n\tinitialResolvedTheme: string,\n\tinitialPalette: string,\n\tinitialIconStyle: string,\n): Theme {\n\treturn useHostPush(host, \"themeChanged\", extractThemePatch, {\n\t\tresolvedTheme: initialResolvedTheme,\n\t\tpalette: initialPalette,\n\t\ticonStyle: initialIconStyle,\n\t})\n}\n\n// ── Font hook ────────────────────────────────────────────────────────────\n\nfunction useFont(host: Host, initialFonts: PluginFonts): PluginFonts {\n\treturn useHostPush(host, \"fontsChanged\", extractFontsPayload, initialFonts)\n}\n\n// ── Public factory ───────────────────────────────────────────────────────\n\n/**\n * Builds the reactive half of the plugin API (`useFileList`,\n * `useMessageList`, `useCreateMessage`, `useDanmakuList`,\n * `useCreateDanmaku`, `usePref`, `useTheme`, `useFont`) on top of the\n * imperative `WebPluginAPI` and the host bridge. Queries refetch\n * automatically on the matching host invalidation push and when the\n * iframe is rebound to another resource.\n *\n * Consumed by {@link createPluginRoot}; call it directly only when\n * composing your own runtime. The returned hooks are bound to the\n * `host` passed in — the one from `ensureHostBridge()`.\n */\nexport function createPluginQueryAPI<\n\tTSchema extends PluginSchema = PluginSchema,\n>(\n\thost: Host,\n\tctx: {\n\t\treadonly resolvedTheme: string\n\t\treadonly palette: string\n\t\treadonly iconStyle: string\n\t\treadonly fonts: PluginFonts\n\t\treadonly resId: string\n\t},\n): ReactivePluginAPI<TSchema> {\n\t// Refetch when the iframe is rebound to another resource without a\n\t// remount (createPluginRoot's `remountOnResourceChange: false`). With\n\t// the default remount this dep is constant for the mount's lifetime.\n\tconst contextDeps = [ctx.resId]\n\treturn {\n\t\tuseFileList: () =>\n\t\t\tuseFileList(host, contextDeps) as QueryState<readonly TSchema[\"file\"][]>,\n\t\tuseMessageList: () => useMessageList(host, contextDeps),\n\t\tuseCreateMessage: () => useCreateMessage(host),\n\t\tuseDanmakuList: (filter?: DanmakuListFilter) =>\n\t\t\tuseDanmakuList(host, contextDeps, filter),\n\t\tuseCreateDanmaku: () => useCreateDanmaku(host),\n\t\tusePref: <T>(key: string, defaultValue: T, codec?: Codec<T>) =>\n\t\t\tusePref(host, key, defaultValue, codec),\n\t\tuseTheme: () =>\n\t\t\tuseTheme(host, ctx.resolvedTheme, ctx.palette, ctx.iconStyle),\n\t\tuseFont: () => useFont(host, ctx.fonts),\n\t}\n}\n","import type { PluginSchema } from \"@hoardodile/sdk-types\"\nimport {\n\tapplyFonts,\n\tapplyTheme,\n\tcreateIframeHostAPI,\n\tensureHostBridge,\n\tgetVisibilitySnapshot,\n\tmountPlugin,\n\tsubscribeToVisibility,\n} from \"@hoardodile/sdk-web\"\nimport type { ComponentType, Provider, ReactNode } from \"react\"\nimport { createElement, useEffect, useSyncExternalStore } from \"react\"\nimport { flushSync } from \"react-dom\"\nimport { createRoot } from \"react-dom/client\"\nimport { usePluginAPI } from \"./context.tsx\"\nimport type { FullPluginAPI } from \"./define-api.ts\"\nimport { createPluginQueryAPI } from \"./query.ts\"\n\nexport type PluginRootConfig<TSchema extends PluginSchema = PluginSchema> = {\n\t/** Root component rendered inside the plugin iframe. */\n\treadonly render: ComponentType\n\t/**\n\t * Typed provider returned by {@link definePluginAPI}. This is the single\n\t * source of truth for the plugin schema type.\n\t */\n\treadonly provider: Provider<FullPluginAPI<TSchema> | null>\n\t/**\n\t * When `true` (default), the whole plugin tree remounts whenever the\n\t * iframe is rebound to another resource — the safe choice: all\n\t * per-resource state resets automatically.\n\t *\n\t * Set to `false` for fine-grained updates (e.g. cheap same-plugin\n\t * navigation): the mounted tree stays alive and only re-renders with\n\t * the new `api`. Queries refetch automatically, but every piece of\n\t * per-resource state becomes the plugin's own responsibility — key\n\t * subtrees and memos by `api.resource.id` and reset any hydration\n\t * flags yourself.\n\t */\n\treadonly remountOnResourceChange?: boolean\n}\n\nfunction ThemeSync({ children }: { readonly children: ReactNode }) {\n\tconst api = usePluginAPI()\n\tconst { resolvedTheme, palette, iconStyle } = api.useTheme()\n\n\tuseEffect(\n\t\tfunction applyOnChange() {\n\t\t\tapplyTheme(resolvedTheme, palette, iconStyle)\n\t\t},\n\t\t[resolvedTheme, palette, iconStyle],\n\t)\n\n\treturn children\n}\n\nfunction FontSync({ children }: { readonly children: ReactNode }) {\n\tconst api = usePluginAPI()\n\tconst { family, cssPaths } = api.useFont()\n\n\tuseEffect(\n\t\tfunction applyOnChange() {\n\t\t\tapplyFonts(family, cssPaths)\n\t\t},\n\t\t[family, cssPaths],\n\t)\n\n\treturn children\n}\n\n/**\n * Subscribe to the iframe visibility state from the host: `false` while\n * the iframe is parked offscreen in the preview window. Do NOT gate\n * rendering on it — parked slots are meant to pre-paint so a flip is a\n * style swap, and an empty tree defeats that. Use visibility only to\n * pause active behavior: media playback, autoplay, timers.\n */\nexport function useVisibility(): boolean {\n\treturn useSyncExternalStore(subscribeToVisibility, getVisibilitySnapshot)\n}\n\n/**\n * One-call plugin bootstrap. Handles `mountPlugin`, `createRoot` caching,\n * iframe host API, typed `PluginAPIProvider`, reactive theme application, and\n * visibility subscription.\n *\n * The supplied component receives no props; it should call `usePluginAPI()`\n * and `useVisibility()` internally as needed. By default the root remounts\n * when the resource changes (see\n * {@link PluginRootConfig.remountOnResourceChange}); even then, use\n * `api.resource.id` as a key inside your component if you need finer\n * control.\n */\nexport function createPluginRoot<TSchema extends PluginSchema = PluginSchema>(\n\tconfig: PluginRootConfig<TSchema>,\n): void {\n\tlet root: ReturnType<typeof createRoot> | undefined\n\n\tmountPlugin(function onContext(ctx) {\n\t\tflushSync(() => {\n\t\t\tif (root === undefined) {\n\t\t\t\tconst el = document.getElementById(\"root\")\n\t\t\t\tif (el === null) {\n\t\t\t\t\tconsole.error(\"[plugin] #root element not found — cannot mount\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\troot = createRoot(el)\n\t\t\t}\n\t\t\tconst host = ensureHostBridge()\n\t\t\tconst baseApi = createIframeHostAPI<TSchema>(ctx)\n\t\t\tconst api: FullPluginAPI<TSchema> = {\n\t\t\t\t...baseApi,\n\t\t\t\t...createPluginQueryAPI(host, {\n\t\t\t\t\tresolvedTheme: ctx.resolvedTheme,\n\t\t\t\t\tpalette: ctx.palette,\n\t\t\t\t\ticonStyle: ctx.iconStyle,\n\t\t\t\t\tfonts: ctx.fonts,\n\t\t\t\t\tresId: ctx.resId,\n\t\t\t\t}),\n\t\t\t}\n\t\t\tapplyTheme(ctx.resolvedTheme, ctx.palette, ctx.iconStyle)\n\t\t\tapplyFonts(ctx.fonts.family, ctx.fonts.cssPaths)\n\t\t\troot.render(\n\t\t\t\tcreateElement(\n\t\t\t\t\tconfig.provider,\n\t\t\t\t\t{ value: api },\n\t\t\t\t\tcreateElement(\n\t\t\t\t\t\tThemeSync,\n\t\t\t\t\t\tnull,\n\t\t\t\t\t\tcreateElement(\n\t\t\t\t\t\t\tFontSync,\n\t\t\t\t\t\t\tnull,\n\t\t\t\t\t\t\tcreateElement(\n\t\t\t\t\t\t\t\tconfig.render,\n\t\t\t\t\t\t\t\tconfig.remountOnResourceChange === false\n\t\t\t\t\t\t\t\t\t? {}\n\t\t\t\t\t\t\t\t\t: { key: ctx.resId },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t)\n\t\t})\n\t})\n}\n","import { useEffect, useRef } from \"react\"\nimport { usePluginAPI } from \"./context.tsx\"\n\nconst DEFAULT_DEBOUNCE_MS = 500\n\n/**\n * Persist a value to the per-resource plugin cache: debounced writes while\n * the value changes, plus a flush on `pagehide` / `beforeunload` / unmount\n * so no pending update is lost.\n *\n * Use this for reader positions, resume timestamps, and similar\n * continuously-changing state. Pass `undefined` as the value (or\n * `disabled: true`) to skip persistence while the real value is loading.\n */\nexport function useCacheWriter<T>(options: {\n\treadonly key: string\n\treadonly value: T | undefined\n\treadonly encode: (value: T) => string\n\treadonly disabled?: boolean\n\treadonly debounceMs?: number\n}): void {\n\tconst api = usePluginAPI()\n\tconst {\n\t\tkey,\n\t\tvalue,\n\t\tencode,\n\t\tdisabled = false,\n\t\tdebounceMs = DEFAULT_DEBOUNCE_MS,\n\t} = options\n\tconst latestRef = useRef(value)\n\tlatestRef.current = value\n\tconst encodeRef = useRef(encode)\n\tencodeRef.current = encode\n\n\tuseEffect(() => {\n\t\tif (disabled || value === undefined) return\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst snap = latestRef.current\n\t\t\tif (snap === undefined) return\n\t\t\tapi.setCache(key, encodeRef.current(snap))\n\t\t}, debounceMs)\n\t\treturn () => clearTimeout(timer)\n\t}, [api, key, value, disabled, debounceMs])\n\n\tuseEffect(() => {\n\t\tif (disabled) return\n\t\tfunction flush() {\n\t\t\tconst snap = latestRef.current\n\t\t\tif (snap === undefined) return\n\t\t\tapi.setCache(key, encodeRef.current(snap))\n\t\t}\n\t\twindow.addEventListener(\"pagehide\", flush)\n\t\twindow.addEventListener(\"beforeunload\", flush)\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"pagehide\", flush)\n\t\t\twindow.removeEventListener(\"beforeunload\", flush)\n\t\t\tflush()\n\t\t}\n\t\t// api is structurally stable; flush reads the latest value via ref.\n\t}, [api, key, disabled])\n}\n","import { useEffect, useRef, useState } from \"react\"\nimport { usePluginAPI } from \"./context.tsx\"\n\n/**\n * Polling interval for {@link useExtractProgress}. The host's progress\n * record lives in memory with a short TTL, so the poll must be tight\n * enough to catch a row before it expires.\n */\nconst POLL_INTERVAL_MS = 300\n\n/**\n * Materialization progress of the plugin's `extractArchive` hook:\n * `\"extracting\"` while the host reports in-flight work, `\"done\"` once\n * progress was seen and the record went idle again, `\"idle\"` when no\n * extraction has ever been observed (or a poll failed — the host may\n * not be serving yet).\n */\nexport type ExtractProgressState =\n\t| { readonly state: \"idle\" }\n\t| {\n\t\t\treadonly state: \"extracting\"\n\t\t\treadonly done: number\n\t\t\treadonly total: number\n\t }\n\t| { readonly state: \"done\" }\n\n/**\n * Reactive materialization progress for the current resource. Polls\n * `api.extractProgressUrl()` and tracks the seen-progress transition:\n * a plugin that called `extractArchive` can show \"extracting\" while the\n * host materializes, then switch to \"done\" when the record expires.\n */\nexport function useExtractProgress(): ExtractProgressState {\n\tconst api = usePluginAPI()\n\tconst [state, setState] = useState<ExtractProgressState>({ state: \"idle\" })\n\tconst seenProgress = useRef(false)\n\n\tuseEffect(\n\t\tfunction pollProgress() {\n\t\t\tlet cancelled = false\n\t\t\tseenProgress.current = false\n\t\t\tsetState({ state: \"idle\" })\n\n\t\t\tasync function poll(): Promise<void> {\n\t\t\t\tlet payload: unknown\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await fetch(api.extractProgressUrl())\n\t\t\t\t\tpayload = await response.json()\n\t\t\t\t} catch {\n\t\t\t\t\t// A failed poll (host not serving yet) is treated as idle.\n\t\t\t\t\tpayload = null\n\t\t\t\t}\n\t\t\t\tif (cancelled) return\n\t\t\t\tif (payload !== null && typeof payload === \"object\") {\n\t\t\t\t\tconst { done, total } = payload as Record<string, unknown>\n\t\t\t\t\tif (typeof done === \"number\" && typeof total === \"number\") {\n\t\t\t\t\t\tseenProgress.current = true\n\t\t\t\t\t\tsetState({ state: \"extracting\", done, total })\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsetState(seenProgress.current ? { state: \"done\" } : { state: \"idle\" })\n\t\t\t}\n\n\t\t\tvoid poll()\n\t\t\tconst timer = setInterval(() => void poll(), POLL_INTERVAL_MS)\n\t\t\treturn () => {\n\t\t\t\tcancelled = true\n\t\t\t\tclearInterval(timer)\n\t\t\t}\n\t\t},\n\t\t[api],\n\t)\n\n\treturn state\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hoardodile/sdk-react",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "React bindings for hoardodile content plugins.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"hoardodile",
|
|
8
|
+
"plugin",
|
|
9
|
+
"sdk",
|
|
10
|
+
"react"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/hoardodile/hoardodile.git",
|
|
15
|
+
"directory": "plugins/sdk-react"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"development": "./src/index.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=24"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"i18next": "^26.4.0",
|
|
38
|
+
"react-i18next": "^17.0.12",
|
|
39
|
+
"@hoardodile/sdk-types": "0.0.0",
|
|
40
|
+
"@hoardodile/sdk-web": "0.0.0",
|
|
41
|
+
"@hoardodile/i18n": "0.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "^19.2.8",
|
|
45
|
+
"react-dom": "^19.2.8"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^26.2.0",
|
|
49
|
+
"@types/react": "^19.2.18",
|
|
50
|
+
"@types/react-dom": "^19.2.4",
|
|
51
|
+
"jsdom": "^30.0.1",
|
|
52
|
+
"tsup": "^8.5.1",
|
|
53
|
+
"typescript": "5.9.3",
|
|
54
|
+
"vitest": "^4.1.11"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"lint": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/context.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactivePluginAPI, WebPluginAPI } from "@hoardodile/sdk-web"
|
|
2
|
+
import { createContext, useContext } from "react"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The full plugin API as delivered to React plugin components: the
|
|
6
|
+
* imperative {@link WebPluginAPI} plus the reactive hooks implemented by
|
|
7
|
+
* this package's adapter. Typed with default (unknown) schema slots; the
|
|
8
|
+
* typed provider from `definePluginAPI` narrows it per plugin.
|
|
9
|
+
*/
|
|
10
|
+
export type BasePluginAPI = WebPluginAPI & ReactivePluginAPI
|
|
11
|
+
|
|
12
|
+
export const PluginAPIContext = createContext<BasePluginAPI | null>(null)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Provides the plugin API to the component tree. Usually created
|
|
16
|
+
* implicitly via {@link createPluginRoot}; the typed provider from
|
|
17
|
+
* {@link definePluginAPI} narrows `BasePluginAPI` to the plugin's
|
|
18
|
+
* schema.
|
|
19
|
+
*/
|
|
20
|
+
export const PluginAPIProvider = PluginAPIContext.Provider
|
|
21
|
+
|
|
22
|
+
export function usePluginAPI(): BasePluginAPI {
|
|
23
|
+
const api = useContext(PluginAPIContext)
|
|
24
|
+
if (api === null) {
|
|
25
|
+
throw new Error("usePluginAPI must be used within a PluginAPIProvider")
|
|
26
|
+
}
|
|
27
|
+
return api
|
|
28
|
+
}
|