@openrails/contentkit-ui 0.93.1 → 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -5
- package/dist/client/browser-reader-preferences.d.ts +38 -0
- package/dist/client/browser-reader-preferences.js +172 -0
- package/dist/client/browser-reader-preferences.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +27 -25
- package/dist/client/client.js.map +1 -1
- package/dist/client/discovery.d.ts +14 -1
- package/dist/client/discovery.js +36 -22
- package/dist/client/discovery.js.map +1 -1
- package/dist/client/generated/routes.d.ts +33 -7
- package/dist/client/generated/routes.js +57 -2
- package/dist/client/generated/routes.js.map +1 -1
- package/dist/client/generated/wire.d.ts +15 -0
- package/dist/client/generated/wire.js.map +1 -1
- package/dist/client/http.d.ts +18 -1
- package/dist/client/http.js +67 -26
- package/dist/client/http.js.map +1 -1
- package/dist/client/index.d.ts +3 -1
- package/dist/client/reader-preferences.d.ts +46 -0
- package/dist/client/reader-preferences.js +147 -0
- package/dist/client/reader-preferences.js.map +1 -0
- package/dist/client.js +17 -15
- package/dist/components/content/card.d.ts +21 -5
- package/dist/components/content/card.js +80 -67
- package/dist/components/content/card.js.map +1 -1
- package/dist/components/content/daily-releases.d.ts +15 -0
- package/dist/components/content/daily-releases.js +167 -0
- package/dist/components/content/daily-releases.js.map +1 -0
- package/dist/components/content/filters.d.ts +1 -1
- package/dist/components/content/filters.js +1 -5
- package/dist/components/content/filters.js.map +1 -1
- package/dist/components/content/grid.d.ts +14 -1
- package/dist/components/content/grid.js +58 -32
- package/dist/components/content/grid.js.map +1 -1
- package/dist/components/content/index.d.ts +4 -2
- package/dist/components/content/popular.d.ts +11 -0
- package/dist/components/content/popular.js +49 -0
- package/dist/components/content/popular.js.map +1 -0
- package/dist/components/content/reader-settings.d.ts +1 -1
- package/dist/components/content/reader-settings.js +67 -44
- package/dist/components/content/reader-settings.js.map +1 -1
- package/dist/index.js +22 -20
- package/dist/locales/discovery/de.js +6 -0
- package/dist/locales/discovery/de.js.map +1 -1
- package/dist/locales/discovery/en.d.ts +6 -0
- package/dist/locales/discovery/en.js +6 -0
- package/dist/locales/discovery/en.js.map +1 -1
- package/dist/locales/discovery/es.js +6 -0
- package/dist/locales/discovery/es.js.map +1 -1
- package/dist/locales/discovery/ja.js +6 -0
- package/dist/locales/discovery/ja.js.map +1 -1
- package/dist/locales/discovery/ko.js +6 -0
- package/dist/locales/discovery/ko.js.map +1 -1
- package/dist/locales/discovery/zh.js +6 -0
- package/dist/locales/discovery/zh.js.map +1 -1
- package/dist/locales/en.d.ts +6 -0
- package/dist/react/context.d.ts +1 -1
- package/dist/react/context.js.map +1 -1
- package/dist/react/discovery.d.ts +14 -4
- package/dist/react/discovery.js +196 -149
- package/dist/react/discovery.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/provider.d.ts +4 -1
- package/dist/react/provider.js +38 -31
- package/dist/react/provider.js.map +1 -1
- package/dist/react/reader-preferences.d.ts +13 -0
- package/dist/react/reader-preferences.js +15 -0
- package/dist/react/reader-preferences.js.map +1 -0
- package/dist/react/resources.js +11 -11
- package/dist/react/resources.js.map +1 -1
- package/dist/react.js +10 -9
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -290,18 +290,60 @@ cover at 3:1.
|
|
|
290
290
|
|
|
291
291
|
### Card lists: decorations and locked cards
|
|
292
292
|
|
|
293
|
-
Every card list (`ContentGrid`, `ContentListing`, `ContentRail`,
|
|
293
|
+
Every card list (`ContentGrid`, `ContentListing`, `ContentRail`, `DailyReleases`,
|
|
294
294
|
`SearchResults`, `ContentHistory`) and `ContentCard` take `decorate(card)`:
|
|
295
295
|
the host's badges, drawn over the cover (place them yourself; clicks reach
|
|
296
296
|
the card, and click signals stay intact). Cards carry what badges need:
|
|
297
297
|
`access.level` and `access.tiers` (the effective rule, the same for every
|
|
298
298
|
reader), `version.has_audio`, `viewer.favorited` and `entities`.
|
|
299
299
|
|
|
300
|
+
Hosts can tailor the details without replacing the card. For example,
|
|
301
|
+
`metadata={{ languages: false, traits: ["colored"], duration: false }}` keeps
|
|
302
|
+
the page count and the Colored trait, and `showCounts={false}` hides reaction
|
|
303
|
+
totals. `groupKinds={["series"]}` selects series chips;
|
|
304
|
+
`filterEntity={(entity, kind) => ...}` filters them before the chip limit.
|
|
305
|
+
Defaults continue to show all normal metadata, entity chips and totals.
|
|
306
|
+
|
|
307
|
+
Set `grid={{ minCardWidth: 320, mobileColumns: 2 }}` for large covers in a fluid
|
|
308
|
+
grid: two columns on small screens, then as many columns as the available
|
|
309
|
+
width supports. This is supported by card grids, daily releases, Popular and
|
|
310
|
+
`ContentRail` previews; an unconfigured rail remains horizontal. There is no
|
|
311
|
+
desktop column ceiling, and other apps keep their existing layout by default.
|
|
312
|
+
|
|
300
313
|
`ContentGrid` and `ContentListing` take `locked`: `show` (default) marks the
|
|
301
314
|
cards the reader doesn't open, `hide` leaves them out, and `collapse` lists
|
|
302
315
|
the open ones and puts the rest behind a counted toggle that loads them on
|
|
303
316
|
opening (`useLockedCount` counts them).
|
|
304
317
|
|
|
318
|
+
### Daily releases
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
<DailyReleases query={{ kind: "gallery", days: 10 }} initialVisible={30} />
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`DailyReleases` fetches all card metadata for the calendar window in one
|
|
325
|
+
`GET /releases` request. It uses the browser's IANA time zone unless
|
|
326
|
+
`query.time_zone` is supplied. Days appear newest first as keyboard-accessible
|
|
327
|
+
disclosures, with reduced-motion support. The initial card budget is shared
|
|
328
|
+
across days, newest first. Opening older days or choosing **Show more** reveals
|
|
329
|
+
already-fetched cards; it does not make another listing request.
|
|
330
|
+
|
|
331
|
+
Only revealed cards occupy the grid, and images/previews mount when those cards
|
|
332
|
+
enter the viewport. Collapsing a day removes its media. The standard card
|
|
333
|
+
properties (`href`, `decorate`, `groupKinds`, `tiers`, and so on) remain available.
|
|
334
|
+
For a custom layout, `useDailyReleases(query)` exposes the same shared read and
|
|
335
|
+
`client.discovery.releases(query)` exposes the typed request directly.
|
|
336
|
+
|
|
337
|
+
`published_within` filters a listing by publication age independently of its
|
|
338
|
+
ranking window. For example, `{ sort: "popular:7d", published_within: "7d" }`
|
|
339
|
+
ranks only content published during the past seven days.
|
|
340
|
+
|
|
341
|
+
`<PopularContentGrid query={{ kind: "gallery" }} />` supplies the 7-day,
|
|
342
|
+
90-day, 365-day and all-time selector, keeps score and age windows matched,
|
|
343
|
+
and loads further pages as the reader scrolls. `period` and `onPeriodChange`
|
|
344
|
+
optionally keep its state in the host's URL. For a homepage preview, pass
|
|
345
|
+
`popularQuery("7d", { kind: "gallery", limit: 6 })` to a `ContentRail`'s `list`.
|
|
346
|
+
|
|
305
347
|
### MediaFolderEditor
|
|
306
348
|
|
|
307
349
|
```tsx
|
|
@@ -761,7 +803,37 @@ the host owns search-page navigation.
|
|
|
761
803
|
/>
|
|
762
804
|
```
|
|
763
805
|
|
|
764
|
-
Render these under `ContentKitProvider
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
806
|
+
Render these under `ContentKitProvider` with `viewer={null}` for guests. Guest
|
|
807
|
+
language choices and filter overrides are stored per app in localStorage and
|
|
808
|
+
shared across tabs. Account filter choices come from ContentKit's database;
|
|
809
|
+
account languages use the provider's `languagePreference` adapter:
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
<ContentKitProvider
|
|
813
|
+
client={contentkit}
|
|
814
|
+
viewer={user?.id ?? null}
|
|
815
|
+
language={routeLanguage}
|
|
816
|
+
languagePreference={{
|
|
817
|
+
load: (accountId) => loadAccountLanguage(accountId),
|
|
818
|
+
save: (language, accountId) => saveAccountLanguage(accountId, language),
|
|
819
|
+
}}
|
|
820
|
+
>
|
|
821
|
+
{children}
|
|
822
|
+
</ContentKitProvider>
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
The adapter must check the expected account before reading or writing. Return
|
|
826
|
+
null from `load` when no language is saved. A localized URL does not change a
|
|
827
|
+
stored preference: only an explicit language selection does. The language
|
|
828
|
+
callback still belongs to the host's router.
|
|
829
|
+
|
|
830
|
+
On a confirmed **new registration**, call
|
|
831
|
+
`contentkit.preferences.inheritAfterRegistration(userId)`. Never call it for an
|
|
832
|
+
ordinary sign-in. The captured guest choices are retried once that account's
|
|
833
|
+
authenticated transport is ready; failed transfers remain pending in localStorage.
|
|
834
|
+
Existing account filter records are preserved. A confirmed new registration
|
|
835
|
+
inherits the guest language over its initial default; retries preserve a later
|
|
836
|
+
language choice. Ordinary sign-ins never import guest preferences.
|
|
837
|
+
`useReaderPreferences()` exposes the preferred language, pending/error state and
|
|
838
|
+
a retry action. ReaderSettings shows an incomplete transfer and lets the reader
|
|
839
|
+
retry it. Guest preferences are retained separately after registration and logout.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { FilterSetting } from './generated/wire.js';
|
|
2
|
+
export interface GuestReaderPreferences {
|
|
3
|
+
language?: string;
|
|
4
|
+
filters: readonly FilterSetting[];
|
|
5
|
+
}
|
|
6
|
+
export interface PendingReaderPreferences extends GuestReaderPreferences {
|
|
7
|
+
account: string;
|
|
8
|
+
languageDone: boolean;
|
|
9
|
+
filtersDone: boolean;
|
|
10
|
+
languageObserved?: boolean;
|
|
11
|
+
initialLanguage?: string | null;
|
|
12
|
+
}
|
|
13
|
+
/** Anonymous preferences and registration retries, isolated to one app URL. */
|
|
14
|
+
export declare class BrowserReaderPreferences {
|
|
15
|
+
private readonly key;
|
|
16
|
+
private readonly browser?;
|
|
17
|
+
private value;
|
|
18
|
+
private publicValue;
|
|
19
|
+
private encoded;
|
|
20
|
+
private unavailable;
|
|
21
|
+
private readonly listeners;
|
|
22
|
+
constructor(key: string, browser?: Window | undefined);
|
|
23
|
+
readonly snapshot: () => GuestReaderPreferences;
|
|
24
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
25
|
+
setLanguage(value: string): void;
|
|
26
|
+
setFilters(value: readonly FilterSetting[]): void;
|
|
27
|
+
pending(account: string): PendingReaderPreferences | undefined;
|
|
28
|
+
/** Capture only on a confirmed registration, never on a login transition. */
|
|
29
|
+
register(account: string): void;
|
|
30
|
+
complete(account: string, step: "language" | "filters"): void;
|
|
31
|
+
observeLanguage(account: string, initialLanguage: string | null): void;
|
|
32
|
+
private readonly onStorage;
|
|
33
|
+
private sync;
|
|
34
|
+
private save;
|
|
35
|
+
private replace;
|
|
36
|
+
}
|
|
37
|
+
/** Same-app clients share a store; server-rendered requests never do. */
|
|
38
|
+
export declare function browserReaderPreferences(mount: string): BrowserReaderPreferences;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//#region src/client/browser-reader-preferences.ts
|
|
2
|
+
var e = Object.freeze({
|
|
3
|
+
filters: Object.freeze([]),
|
|
4
|
+
registrations: Object.freeze([])
|
|
5
|
+
});
|
|
6
|
+
function t(e) {
|
|
7
|
+
if (!(typeof e != "string" || e.length > 64)) try {
|
|
8
|
+
return Intl.getCanonicalLocales(e)[0];
|
|
9
|
+
} catch {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function n(e) {
|
|
14
|
+
if (!Array.isArray(e)) return [];
|
|
15
|
+
let t = /* @__PURE__ */ new Map();
|
|
16
|
+
for (let n of e.slice(0, 100)) n && typeof n.group_id == "string" && n.group_id.length > 0 && n.group_id.length <= 128 && typeof n.visible == "boolean" && t.set(n.group_id, n.visible);
|
|
17
|
+
return [...t].sort(([e], [t]) => e.localeCompare(t)).map(([e, t]) => ({
|
|
18
|
+
group_id: e,
|
|
19
|
+
visible: t
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
function r(r) {
|
|
23
|
+
try {
|
|
24
|
+
let i = JSON.parse(r ?? "{}");
|
|
25
|
+
if (!i || typeof i != "object") return e;
|
|
26
|
+
let a = i, o = [];
|
|
27
|
+
if (Array.isArray(a.registrations)) for (let e of a.registrations.slice(0, 20)) !e || typeof e.account != "string" || !e.account || e.account.length > 128 || o.some((t) => t.account === e.account) || o.push({
|
|
28
|
+
account: e.account,
|
|
29
|
+
language: t(e.language),
|
|
30
|
+
filters: n(e.filters),
|
|
31
|
+
languageDone: e.languageDone === !0,
|
|
32
|
+
filtersDone: e.filtersDone === !0,
|
|
33
|
+
languageObserved: e.languageObserved === !0,
|
|
34
|
+
initialLanguage: e.initialLanguage === null ? null : t(e.initialLanguage)
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
language: t(a.language),
|
|
38
|
+
filters: n(a.filters),
|
|
39
|
+
registrations: o
|
|
40
|
+
};
|
|
41
|
+
} catch {
|
|
42
|
+
return e;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var i = class {
|
|
46
|
+
key;
|
|
47
|
+
browser;
|
|
48
|
+
value = e;
|
|
49
|
+
publicValue = e;
|
|
50
|
+
encoded = JSON.stringify(e);
|
|
51
|
+
unavailable = !1;
|
|
52
|
+
listeners = /* @__PURE__ */ new Set();
|
|
53
|
+
constructor(e, t) {
|
|
54
|
+
this.key = e, this.browser = t, this.sync();
|
|
55
|
+
}
|
|
56
|
+
snapshot = () => this.publicValue;
|
|
57
|
+
subscribe = (e) => (this.sync(), this.listeners.size || this.browser?.addEventListener("storage", this.onStorage), this.listeners.add(e), () => {
|
|
58
|
+
this.listeners.delete(e), this.listeners.size || this.browser?.removeEventListener("storage", this.onStorage);
|
|
59
|
+
});
|
|
60
|
+
setLanguage(e) {
|
|
61
|
+
let n = t(e);
|
|
62
|
+
if (!n) throw Error("contentkit: invalid preferred language");
|
|
63
|
+
this.sync(), this.save({
|
|
64
|
+
...this.value,
|
|
65
|
+
language: n
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
setFilters(e) {
|
|
69
|
+
this.sync(), this.save({
|
|
70
|
+
...this.value,
|
|
71
|
+
filters: n(e)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
pending(e) {
|
|
75
|
+
return this.sync(), this.value.registrations.find((t) => t.account === e);
|
|
76
|
+
}
|
|
77
|
+
register(e) {
|
|
78
|
+
if (this.sync(), !e || e.length > 128) throw Error("contentkit: registration account is required");
|
|
79
|
+
if (!this.pending(e) && (this.value.language || this.value.filters.length)) {
|
|
80
|
+
if (this.value.registrations.length >= 20) throw Error("contentkit: too many pending preference registrations");
|
|
81
|
+
this.save({
|
|
82
|
+
...this.value,
|
|
83
|
+
registrations: [...this.value.registrations, {
|
|
84
|
+
account: e,
|
|
85
|
+
language: this.value.language,
|
|
86
|
+
filters: this.value.filters,
|
|
87
|
+
languageDone: !this.value.language,
|
|
88
|
+
filtersDone: !this.value.filters.length
|
|
89
|
+
}]
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
complete(e, t) {
|
|
94
|
+
this.sync();
|
|
95
|
+
let n = this.value.registrations.map((n) => n.account === e ? {
|
|
96
|
+
...n,
|
|
97
|
+
[t === "language" ? "languageDone" : "filtersDone"]: !0
|
|
98
|
+
} : n).filter((e) => !e.languageDone || !e.filtersDone);
|
|
99
|
+
this.save({
|
|
100
|
+
...this.value,
|
|
101
|
+
registrations: n
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
observeLanguage(e, t) {
|
|
105
|
+
this.sync(), this.save({
|
|
106
|
+
...this.value,
|
|
107
|
+
registrations: this.value.registrations.map((n) => n.account === e && !n.languageObserved ? {
|
|
108
|
+
...n,
|
|
109
|
+
languageObserved: !0,
|
|
110
|
+
initialLanguage: t
|
|
111
|
+
} : n)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
onStorage = (e) => {
|
|
115
|
+
(e.key === null || e.key === this.key) && this.sync();
|
|
116
|
+
};
|
|
117
|
+
sync() {
|
|
118
|
+
if (this.browser && !this.unavailable) try {
|
|
119
|
+
this.replace(r(this.browser.localStorage.getItem(this.key)));
|
|
120
|
+
} catch {
|
|
121
|
+
this.unavailable = !0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
save(e) {
|
|
125
|
+
if (this.browser && !this.unavailable) try {
|
|
126
|
+
this.browser.localStorage.setItem(this.key, JSON.stringify(e));
|
|
127
|
+
} catch {
|
|
128
|
+
this.unavailable = !0;
|
|
129
|
+
}
|
|
130
|
+
this.replace(e);
|
|
131
|
+
}
|
|
132
|
+
replace(e) {
|
|
133
|
+
let t = JSON.stringify({
|
|
134
|
+
language: e.language,
|
|
135
|
+
filters: e.filters,
|
|
136
|
+
registrations: e.registrations
|
|
137
|
+
});
|
|
138
|
+
if (t === this.encoded) return;
|
|
139
|
+
this.encoded = t, this.value = Object.freeze({
|
|
140
|
+
...e,
|
|
141
|
+
filters: Object.freeze(e.filters.map((e) => Object.freeze({ ...e }))),
|
|
142
|
+
registrations: Object.freeze(e.registrations.map((e) => Object.freeze({
|
|
143
|
+
...e,
|
|
144
|
+
filters: Object.freeze(e.filters.map((e) => Object.freeze({ ...e })))
|
|
145
|
+
})))
|
|
146
|
+
});
|
|
147
|
+
let n = {
|
|
148
|
+
language: this.value.language,
|
|
149
|
+
filters: this.value.filters
|
|
150
|
+
};
|
|
151
|
+
JSON.stringify(n) !== JSON.stringify(this.publicValue) && (this.publicValue = Object.freeze(n));
|
|
152
|
+
for (let e of this.listeners) try {
|
|
153
|
+
e();
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
}, a = /* @__PURE__ */ new WeakMap();
|
|
157
|
+
function o(e) {
|
|
158
|
+
let t = typeof window > "u" ? void 0 : window, n = e.replace(/\/+$/, "");
|
|
159
|
+
if (t) try {
|
|
160
|
+
n = new URL(n, t.location.href).href;
|
|
161
|
+
} catch {}
|
|
162
|
+
let r = `contentkit:reader-preferences:v1:${n}`;
|
|
163
|
+
if (!t) return new i(r);
|
|
164
|
+
let o = a.get(t);
|
|
165
|
+
o || a.set(t, o = /* @__PURE__ */ new Map());
|
|
166
|
+
let s = o.get(r);
|
|
167
|
+
return s || o.set(r, s = new i(r, t)), s;
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
export { i as BrowserReaderPreferences, o as browserReaderPreferences };
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=browser-reader-preferences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-reader-preferences.js","names":[],"sources":["../../src/client/browser-reader-preferences.ts"],"sourcesContent":["import type { FilterSetting } from \"./generated/wire.js\";\n\nexport interface GuestReaderPreferences {\n language?: string;\n filters: readonly FilterSetting[];\n}\n\nexport interface PendingReaderPreferences extends GuestReaderPreferences {\n account: string;\n languageDone: boolean;\n filtersDone: boolean;\n languageObserved?: boolean;\n initialLanguage?: string | null;\n}\n\ntype Document = GuestReaderPreferences & { registrations: readonly PendingReaderPreferences[] };\nconst EMPTY: Document = Object.freeze({ filters: Object.freeze([]), registrations: Object.freeze([]) });\n\nfunction language(value: unknown): string | undefined {\n if (typeof value !== \"string\" || value.length > 64) return;\n try { return Intl.getCanonicalLocales(value)[0]; } catch { return; }\n}\n\nfunction filters(value: unknown): FilterSetting[] {\n if (!Array.isArray(value)) return [];\n const entries = new Map<string, boolean>();\n for (const row of value.slice(0, 100)) {\n if (row && typeof row.group_id === \"string\" && row.group_id.length > 0 && row.group_id.length <= 128 && typeof row.visible === \"boolean\") entries.set(row.group_id, row.visible);\n }\n return [...entries].sort(([a], [b]) => a.localeCompare(b)).map(([group_id, visible]) => ({ group_id, visible }));\n}\n\nfunction decode(raw: string | null): Document {\n try {\n const data: unknown = JSON.parse(raw ?? \"{}\");\n if (!data || typeof data !== \"object\") return EMPTY;\n const value = data as Record<string, unknown>;\n const registrations: PendingReaderPreferences[] = [];\n if (Array.isArray(value.registrations)) {\n for (const entry of value.registrations.slice(0, 20)) {\n if (!entry || typeof entry.account !== \"string\" || !entry.account || entry.account.length > 128 || registrations.some((r) => r.account === entry.account)) continue;\n registrations.push({ account: entry.account, language: language(entry.language), filters: filters(entry.filters), languageDone: entry.languageDone === true, filtersDone: entry.filtersDone === true,\n languageObserved: entry.languageObserved === true, initialLanguage: entry.initialLanguage === null ? null : language(entry.initialLanguage) });\n }\n }\n return { language: language(value.language), filters: filters(value.filters), registrations };\n } catch { return EMPTY; }\n}\n\n/** Anonymous preferences and registration retries, isolated to one app URL. */\nexport class BrowserReaderPreferences {\n private value: Document = EMPTY;\n private publicValue: GuestReaderPreferences = EMPTY;\n private encoded = JSON.stringify(EMPTY);\n private unavailable = false;\n private readonly listeners = new Set<() => void>();\n\n constructor(private readonly key: string, private readonly browser?: Window) { this.sync(); }\n\n readonly snapshot = (): GuestReaderPreferences => this.publicValue;\n\n readonly subscribe = (listener: () => void): (() => void) => {\n this.sync();\n if (!this.listeners.size) this.browser?.addEventListener(\"storage\", this.onStorage);\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n if (!this.listeners.size) this.browser?.removeEventListener(\"storage\", this.onStorage);\n };\n };\n\n setLanguage(value: string): void {\n const next = language(value);\n if (!next) throw new Error(\"contentkit: invalid preferred language\");\n this.sync();\n this.save({ ...this.value, language: next });\n }\n\n setFilters(value: readonly FilterSetting[]): void {\n this.sync();\n this.save({ ...this.value, filters: filters(value) });\n }\n\n pending(account: string): PendingReaderPreferences | undefined {\n this.sync();\n return this.value.registrations.find((r) => r.account === account);\n }\n\n /** Capture only on a confirmed registration, never on a login transition. */\n register(account: string): void {\n this.sync();\n if (!account || account.length > 128) throw new Error(\"contentkit: registration account is required\");\n if (this.pending(account)) return;\n if (!this.value.language && !this.value.filters.length) return;\n if (this.value.registrations.length >= 20) throw new Error(\"contentkit: too many pending preference registrations\");\n this.save({ ...this.value, registrations: [...this.value.registrations, {\n account, language: this.value.language, filters: this.value.filters,\n languageDone: !this.value.language, filtersDone: !this.value.filters.length,\n }] });\n }\n\n complete(account: string, step: \"language\" | \"filters\"): void {\n this.sync();\n const registrations = this.value.registrations.map((r) => r.account === account ? { ...r, [step === \"language\" ? \"languageDone\" : \"filtersDone\"]: true } : r)\n .filter((r) => !r.languageDone || !r.filtersDone);\n this.save({ ...this.value, registrations });\n }\n\n observeLanguage(account: string, initialLanguage: string | null): void {\n this.sync();\n this.save({ ...this.value, registrations: this.value.registrations.map((r) => r.account === account && !r.languageObserved ? { ...r, languageObserved: true, initialLanguage } : r) });\n }\n\n private readonly onStorage = (event: StorageEvent): void => {\n if (event.key === null || event.key === this.key) this.sync();\n };\n\n private sync(): void {\n if (!this.browser || this.unavailable) return;\n try { this.replace(decode(this.browser.localStorage.getItem(this.key))); }\n catch { this.unavailable = true; }\n }\n\n private save(next: Document): void {\n if (this.browser && !this.unavailable) {\n try { this.browser.localStorage.setItem(this.key, JSON.stringify(next)); }\n catch { this.unavailable = true; }\n }\n this.replace(next);\n }\n\n private replace(next: Document): void {\n const encoded = JSON.stringify({ language: next.language, filters: next.filters, registrations: next.registrations });\n if (encoded === this.encoded) return;\n this.encoded = encoded;\n this.value = Object.freeze({ ...next, filters: Object.freeze(next.filters.map((f) => Object.freeze({ ...f }))), registrations: Object.freeze(next.registrations.map((r) => Object.freeze({ ...r, filters: Object.freeze(r.filters.map((f) => Object.freeze({ ...f }))) }))) });\n const publicValue = { language: this.value.language, filters: this.value.filters };\n if (JSON.stringify(publicValue) !== JSON.stringify(this.publicValue)) this.publicValue = Object.freeze(publicValue);\n for (const listener of this.listeners) { try { listener(); } catch { /* Observers do not own persistence. */ } }\n }\n}\n\nconst stores = new WeakMap<Window, Map<string, BrowserReaderPreferences>>();\n\n/** Same-app clients share a store; server-rendered requests never do. */\nexport function browserReaderPreferences(mount: string): BrowserReaderPreferences {\n const browser = typeof window === \"undefined\" ? undefined : window;\n let scope = mount.replace(/\\/+$/, \"\");\n if (browser) { try { scope = new URL(scope, browser.location.href).href; } catch { /* Keep the explicit app key. */ } }\n const key = `contentkit:reader-preferences:v1:${scope}`;\n if (!browser) return new BrowserReaderPreferences(key);\n let scoped = stores.get(browser);\n if (!scoped) stores.set(browser, (scoped = new Map()));\n let store = scoped.get(key);\n if (!store) scoped.set(key, (store = new BrowserReaderPreferences(key, browser)));\n return store;\n}\n"],"mappings":"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"}
|
package/dist/client/client.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { VersionsClient } from './versions.js';
|
|
|
16
16
|
import { ContentKitChange, ContentKitModule, HttpOptions } from './http.js';
|
|
17
17
|
import { MediaClient, ContentFolders, MediaOptions } from './media/client.js';
|
|
18
18
|
import { NotificationsClient } from './notifications.js';
|
|
19
|
+
import { ReaderPreferencesClient } from './reader-preferences.js';
|
|
19
20
|
export interface ContentKitClientOptions extends HttpOptions {
|
|
20
21
|
/** Upload tuning (transport, part concurrency, retries). */
|
|
21
22
|
media?: MediaOptions;
|
|
@@ -59,6 +60,8 @@ export interface ContentKitClient {
|
|
|
59
60
|
readonly stats: StatsClient;
|
|
60
61
|
/** The signed-in person's native and imported notifications. */
|
|
61
62
|
readonly notifications: NotificationsClient;
|
|
63
|
+
/** Browser guest preferences and explicit registration inheritance. */
|
|
64
|
+
readonly preferences: ReaderPreferencesClient;
|
|
62
65
|
/** What the content module allows: which interactions signed-out visitors may make. */
|
|
63
66
|
config(signal?: AbortSignal): Promise<Config>;
|
|
64
67
|
/** The site's content model: languages, tiers, kinds, group kinds and content filters. */
|
package/dist/client/client.js
CHANGED
|
@@ -16,35 +16,37 @@ import { StatsClient as m } from "./stats.js";
|
|
|
16
16
|
import { VersionsClient as h } from "./versions.js";
|
|
17
17
|
import { Http as g } from "./http.js";
|
|
18
18
|
import { NotificationsClient as _ } from "./notifications.js";
|
|
19
|
+
import { ReaderPreferencesClient as v } from "./reader-preferences.js";
|
|
19
20
|
//#region src/client/client.ts
|
|
20
|
-
function
|
|
21
|
-
let { media:
|
|
21
|
+
function y(y) {
|
|
22
|
+
let { media: b, folders: x, ...S } = y, C = new g(S), w = new t(C, b, x);
|
|
22
23
|
return {
|
|
23
|
-
media:
|
|
24
|
-
contents: new i(
|
|
25
|
-
catalog: new n(
|
|
26
|
-
comments: new o(
|
|
27
|
-
reactions: new d(
|
|
28
|
-
favorites: new c(
|
|
29
|
-
polls: new u(
|
|
30
|
-
bans: new a(
|
|
31
|
-
moderation: new l(
|
|
32
|
-
entities: new f(
|
|
33
|
-
discovery: new s(
|
|
34
|
-
versions: new h(
|
|
35
|
-
offers: new p(
|
|
36
|
-
codes: new r(
|
|
37
|
-
stats: new m(
|
|
38
|
-
notifications: new _(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
24
|
+
media: w,
|
|
25
|
+
contents: new i(C),
|
|
26
|
+
catalog: new n(C),
|
|
27
|
+
comments: new o(C),
|
|
28
|
+
reactions: new d(C),
|
|
29
|
+
favorites: new c(C),
|
|
30
|
+
polls: new u(C, w),
|
|
31
|
+
bans: new a(C),
|
|
32
|
+
moderation: new l(C),
|
|
33
|
+
entities: new f(C, w),
|
|
34
|
+
discovery: new s(C),
|
|
35
|
+
versions: new h(C),
|
|
36
|
+
offers: new p(C),
|
|
37
|
+
codes: new r(C),
|
|
38
|
+
stats: new m(C),
|
|
39
|
+
notifications: new _(C),
|
|
40
|
+
preferences: new v(C),
|
|
41
|
+
config: (t) => e(C, "GET", "/config", { signal: t }),
|
|
42
|
+
kinds: (t) => e(C, "GET", "/kinds", { signal: t }),
|
|
43
|
+
import: (t) => e(C, "POST", "/import", { body: t }),
|
|
44
|
+
url: (e, t) => C.mount(e, t),
|
|
45
|
+
setReadScope: (e) => C.setReadScope(e),
|
|
46
|
+
subscribe: (e) => C.subscribe(e)
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
//#endregion
|
|
48
|
-
export {
|
|
50
|
+
export { y as createContentKitClient };
|
|
49
51
|
|
|
50
52
|
//# sourceMappingURL=client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import { CatalogClient } from \"./catalog.js\";\nimport { CodesClient } from \"./codes.js\";\nimport { ContentsClient } from \"./contents.js\";\nimport { BansClient } from \"./content/bans.js\";\nimport { CommentsClient } from \"./content/comments.js\";\nimport { FavoritesClient } from \"./content/favorites.js\";\nimport { ModerationClient } from \"./content/moderation.js\";\nimport { PollsClient } from \"./content/polls.js\";\nimport { ReactionsClient } from \"./content/reactions.js\";\nimport type { Batch, Config, ContentModel, RefBody, Result } from \"./generated/wire.js\";\nimport { DiscoveryClient } from \"./discovery.js\";\nimport { EntitiesClient } from \"./entities.js\";\nimport { OffersClient } from \"./offers.js\";\nimport { StatsClient } from \"./stats.js\";\nimport { VersionsClient } from \"./versions.js\";\nimport { Http, type ContentKitChange, type ContentKitModule, type HttpOptions } from \"./http.js\";\nimport { MediaClient, type ContentFolders, type MediaOptions } from \"./media/client.js\";\nimport { call } from \"./route.js\";\nimport { NotificationsClient } from \"./notifications.js\";\n\nexport interface ContentKitClientOptions extends HttpOptions {\n /** Upload tuning (transport, part concurrency, retries). */\n media?: MediaOptions;\n /** The media kind of poll folders, when the host renamed it (content.Media). */\n folders?: ContentFolders;\n}\n\n/**\n * One ContentKit deployment as the browser sees it. Each module is a property\n * over one shared transport (mounts, auth, language, errors, change events).\n */\nexport interface ContentKitClient {\n /** Uploads, multipart, commits, reads, frames, HLS bases, the HLS loader and inline images. */\n readonly media: MediaClient;\n /** Content items: reads, and writes for their creators, owner-group editors and staff. */\n readonly contents: ContentsClient;\n /** The catalog's totals by kind, language and audience. */\n readonly catalog: CatalogClient;\n /** Threads, replies, edits, tombstones, reactions, standing, the latest feed, moderation. */\n readonly comments: CommentsClient;\n /** Likes and dislikes of an item. */\n readonly reactions: ReactionsClient;\n /** The signed-in caller's favorites. */\n readonly favorites: FavoritesClient;\n /** Polls: votes, free-text answers and the staff editor. */\n readonly polls: PollsClient;\n /** Comment bans, owner and global. */\n readonly bans: BansClient;\n /** The review queue of held comments and content. */\n readonly moderation: ModerationClient;\n /** Groups: listings, pages, staff writes and memberships. */\n readonly entities: EntitiesClient;\n /** Lists, search, typeahead, rails, release dates, content filters, history, signals and exposures. */\n readonly discovery: DiscoveryClient;\n /** Content versions: reader plans, reader metadata, forks and downloads. */\n readonly versions: VersionsClient;\n /** Offers on contents, versions and groups; group joins; the reader's library. */\n readonly offers: OffersClient;\n /** Content-code lookup. */\n readonly codes: CodesClient;\n /** Staff activity and rankings; the signed-in reader's own statistics. */\n readonly stats: StatsClient;\n /** The signed-in person's native and imported notifications. */\n readonly notifications: NotificationsClient;\n\n /** What the content module allows: which interactions signed-out visitors may make. */\n config(signal?: AbortSignal): Promise<Config>;\n /** The site's content model: languages, tiers, kinds, group kinds and content filters. */\n kinds(signal?: AbortSignal): Promise<ContentModel>;\n /** Staff bulk import (root:import:write): groups, contents, memberships, offers and aliases, idempotent per source. */\n import(batch: Batch): Promise<Result>;\n /** A module's base URL as configured (no trailing slash). */\n url(module: ContentKitModule, ref?: RefBody): string;\n /** Cache context only, not authentication. The React provider keeps this in sync. */\n setReadScope(scope: string): void;\n /** Every successful mutation; a null scope means its data predates a context change. */\n subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void;\n}\n\nexport function createContentKitClient(options: ContentKitClientOptions): ContentKitClient {\n const { media: mediaOptions, folders, ...http } = options;\n const h = new Http(http);\n const media = new MediaClient(h, mediaOptions, folders);\n return {\n media,\n contents: new ContentsClient(h),\n catalog: new CatalogClient(h),\n comments: new CommentsClient(h),\n reactions: new ReactionsClient(h),\n favorites: new FavoritesClient(h),\n polls: new PollsClient(h, media),\n bans: new BansClient(h),\n moderation: new ModerationClient(h),\n entities: new EntitiesClient(h, media),\n discovery: new DiscoveryClient(h),\n versions: new VersionsClient(h),\n offers: new OffersClient(h),\n codes: new CodesClient(h),\n stats: new StatsClient(h),\n notifications: new NotificationsClient(h),\n config: (signal) => call(h, \"GET\", \"/config\", { signal }),\n kinds: (signal) => call(h, \"GET\", \"/kinds\", { signal }),\n import: (batch) => call(h, \"POST\", \"/import\", { body: batch }),\n url: (module, ref) => h.mount(module, ref),\n setReadScope: (scope) => h.setReadScope(scope),\n subscribe: (listener) => h.subscribe(listener),\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../src/client/client.ts"],"sourcesContent":["import { CatalogClient } from \"./catalog.js\";\nimport { CodesClient } from \"./codes.js\";\nimport { ContentsClient } from \"./contents.js\";\nimport { BansClient } from \"./content/bans.js\";\nimport { CommentsClient } from \"./content/comments.js\";\nimport { FavoritesClient } from \"./content/favorites.js\";\nimport { ModerationClient } from \"./content/moderation.js\";\nimport { PollsClient } from \"./content/polls.js\";\nimport { ReactionsClient } from \"./content/reactions.js\";\nimport type { Batch, Config, ContentModel, RefBody, Result } from \"./generated/wire.js\";\nimport { DiscoveryClient } from \"./discovery.js\";\nimport { EntitiesClient } from \"./entities.js\";\nimport { OffersClient } from \"./offers.js\";\nimport { StatsClient } from \"./stats.js\";\nimport { VersionsClient } from \"./versions.js\";\nimport { Http, type ContentKitChange, type ContentKitModule, type HttpOptions } from \"./http.js\";\nimport { MediaClient, type ContentFolders, type MediaOptions } from \"./media/client.js\";\nimport { call } from \"./route.js\";\nimport { NotificationsClient } from \"./notifications.js\";\nimport { ReaderPreferencesClient } from \"./reader-preferences.js\";\n\nexport interface ContentKitClientOptions extends HttpOptions {\n /** Upload tuning (transport, part concurrency, retries). */\n media?: MediaOptions;\n /** The media kind of poll folders, when the host renamed it (content.Media). */\n folders?: ContentFolders;\n}\n\n/**\n * One ContentKit deployment as the browser sees it. Each module is a property\n * over one shared transport (mounts, auth, language, errors, change events).\n */\nexport interface ContentKitClient {\n /** Uploads, multipart, commits, reads, frames, HLS bases, the HLS loader and inline images. */\n readonly media: MediaClient;\n /** Content items: reads, and writes for their creators, owner-group editors and staff. */\n readonly contents: ContentsClient;\n /** The catalog's totals by kind, language and audience. */\n readonly catalog: CatalogClient;\n /** Threads, replies, edits, tombstones, reactions, standing, the latest feed, moderation. */\n readonly comments: CommentsClient;\n /** Likes and dislikes of an item. */\n readonly reactions: ReactionsClient;\n /** The signed-in caller's favorites. */\n readonly favorites: FavoritesClient;\n /** Polls: votes, free-text answers and the staff editor. */\n readonly polls: PollsClient;\n /** Comment bans, owner and global. */\n readonly bans: BansClient;\n /** The review queue of held comments and content. */\n readonly moderation: ModerationClient;\n /** Groups: listings, pages, staff writes and memberships. */\n readonly entities: EntitiesClient;\n /** Lists, search, typeahead, rails, release dates, content filters, history, signals and exposures. */\n readonly discovery: DiscoveryClient;\n /** Content versions: reader plans, reader metadata, forks and downloads. */\n readonly versions: VersionsClient;\n /** Offers on contents, versions and groups; group joins; the reader's library. */\n readonly offers: OffersClient;\n /** Content-code lookup. */\n readonly codes: CodesClient;\n /** Staff activity and rankings; the signed-in reader's own statistics. */\n readonly stats: StatsClient;\n /** The signed-in person's native and imported notifications. */\n readonly notifications: NotificationsClient;\n /** Browser guest preferences and explicit registration inheritance. */\n readonly preferences: ReaderPreferencesClient;\n\n /** What the content module allows: which interactions signed-out visitors may make. */\n config(signal?: AbortSignal): Promise<Config>;\n /** The site's content model: languages, tiers, kinds, group kinds and content filters. */\n kinds(signal?: AbortSignal): Promise<ContentModel>;\n /** Staff bulk import (root:import:write): groups, contents, memberships, offers and aliases, idempotent per source. */\n import(batch: Batch): Promise<Result>;\n /** A module's base URL as configured (no trailing slash). */\n url(module: ContentKitModule, ref?: RefBody): string;\n /** Cache context only, not authentication. The React provider keeps this in sync. */\n setReadScope(scope: string): void;\n /** Every successful mutation; a null scope means its data predates a context change. */\n subscribe(listener: (change: ContentKitChange, scope: string | null) => void): () => void;\n}\n\nexport function createContentKitClient(options: ContentKitClientOptions): ContentKitClient {\n const { media: mediaOptions, folders, ...http } = options;\n const h = new Http(http);\n const media = new MediaClient(h, mediaOptions, folders);\n return {\n media,\n contents: new ContentsClient(h),\n catalog: new CatalogClient(h),\n comments: new CommentsClient(h),\n reactions: new ReactionsClient(h),\n favorites: new FavoritesClient(h),\n polls: new PollsClient(h, media),\n bans: new BansClient(h),\n moderation: new ModerationClient(h),\n entities: new EntitiesClient(h, media),\n discovery: new DiscoveryClient(h),\n versions: new VersionsClient(h),\n offers: new OffersClient(h),\n codes: new CodesClient(h),\n stats: new StatsClient(h),\n notifications: new NotificationsClient(h),\n preferences: new ReaderPreferencesClient(h),\n config: (signal) => call(h, \"GET\", \"/config\", { signal }),\n kinds: (signal) => call(h, \"GET\", \"/kinds\", { signal }),\n import: (batch) => call(h, \"POST\", \"/import\", { body: batch }),\n url: (module, ref) => h.mount(module, ref),\n setReadScope: (scope) => h.setReadScope(scope),\n subscribe: (listener) => h.subscribe(listener),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkFA,SAAgB,EAAuB,GAAoD;CACzF,IAAM,EAAE,OAAO,GAAc,YAAS,GAAG,MAAS,GAC5C,IAAI,IAAI,EAAK,CAAI,GACjB,IAAQ,IAAI,EAAY,GAAG,GAAc,CAAO;CACtD,OAAO;EACL;EACA,UAAU,IAAI,EAAe,CAAC;EAC9B,SAAS,IAAI,EAAc,CAAC;EAC5B,UAAU,IAAI,EAAe,CAAC;EAC9B,WAAW,IAAI,EAAgB,CAAC;EAChC,WAAW,IAAI,EAAgB,CAAC;EAChC,OAAO,IAAI,EAAY,GAAG,CAAK;EAC/B,MAAM,IAAI,EAAW,CAAC;EACtB,YAAY,IAAI,EAAiB,CAAC;EAClC,UAAU,IAAI,EAAe,GAAG,CAAK;EACrC,WAAW,IAAI,EAAgB,CAAC;EAChC,UAAU,IAAI,EAAe,CAAC;EAC9B,QAAQ,IAAI,EAAa,CAAC;EAC1B,OAAO,IAAI,EAAY,CAAC;EACxB,OAAO,IAAI,EAAY,CAAC;EACxB,eAAe,IAAI,EAAoB,CAAC;EACxC,aAAa,IAAI,EAAwB,CAAC;EAC1C,SAAS,MAAW,EAAK,GAAG,OAAO,WAAW,EAAE,UAAO,CAAC;EACxD,QAAQ,MAAW,EAAK,GAAG,OAAO,UAAU,EAAE,UAAO,CAAC;EACtD,SAAS,MAAU,EAAK,GAAG,QAAQ,WAAW,EAAE,MAAM,EAAM,CAAC;EAC7D,MAAM,GAAQ,MAAQ,EAAE,MAAM,GAAQ,CAAG;EACzC,eAAe,MAAU,EAAE,aAAa,CAAK;EAC7C,YAAY,MAAa,EAAE,UAAU,CAAQ;CAC/C;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContentFilters, ContentFiltersUpdate, ContentPage, ExposureInput, RecentSearches, ReleaseDates, SignalInput, Typeahead } from './generated/wire.js';
|
|
1
|
+
import { ContentFilters, ContentFiltersUpdate, ContentPage, DailyReleases, ExposureInput, RecentSearches, ReleaseDates, SignalInput, Typeahead } from './generated/wire.js';
|
|
2
2
|
import { Http } from './http.js';
|
|
3
3
|
import { BrowserSearchHistory } from './browser-search-history.js';
|
|
4
4
|
/** A popularity period. */
|
|
@@ -28,6 +28,8 @@ export interface ContentFilterQuery {
|
|
|
28
28
|
attribute?: readonly string[];
|
|
29
29
|
/** Published on this day (YYYY-MM-DD) or month (YYYY-MM). */
|
|
30
30
|
released_on?: string;
|
|
31
|
+
/** Only contents published within this age window; independent of the popularity score window. */
|
|
32
|
+
published_within?: Period;
|
|
31
33
|
/** IANA time zone of released_on and release dates; UTC by default. */
|
|
32
34
|
tz?: string;
|
|
33
35
|
/** The signed-in reader's unseen, in-progress or completed contents. */
|
|
@@ -52,6 +54,10 @@ export interface PageQuery {
|
|
|
52
54
|
export type ContentQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & {
|
|
53
55
|
sort?: ListSort;
|
|
54
56
|
};
|
|
57
|
+
export type PopularPeriod = "7d" | "90d" | "365d" | "all";
|
|
58
|
+
export type PopularQuery = Omit<ContentQuery, "sort" | "published_within">;
|
|
59
|
+
/** Match the popularity score window and publication age window. */
|
|
60
|
+
export declare function popularQuery(period?: PopularPeriod, filters?: PopularQuery): ContentQuery;
|
|
55
61
|
export type SearchQuery = ContentFilterQuery & ReaderQuery & CardQuery & PageQuery & {
|
|
56
62
|
q: string;
|
|
57
63
|
sort?: "relevance" | ListSort;
|
|
@@ -71,6 +77,11 @@ export type RailQuery = ContentFilterQuery & ReaderQuery & CardQuery & {
|
|
|
71
77
|
export type ReleaseDatesQuery = ContentFilterQuery & ReaderQuery & PageQuery & {
|
|
72
78
|
by?: "month" | "day";
|
|
73
79
|
};
|
|
80
|
+
/** Complete daily metadata for one calendar window, without pagination. */
|
|
81
|
+
export type DailyReleasesQuery = Omit<ContentFilterQuery, "released_on" | "tz"> & ReaderQuery & CardQuery & {
|
|
82
|
+
days?: number;
|
|
83
|
+
time_zone?: string;
|
|
84
|
+
};
|
|
74
85
|
export type HistoryQuery = ReaderQuery & CardQuery & PageQuery & {
|
|
75
86
|
kind?: string;
|
|
76
87
|
status?: "seen" | "in_progress" | "completed";
|
|
@@ -114,6 +125,8 @@ export declare class DiscoveryClient {
|
|
|
114
125
|
recommendations(q?: RailQuery, signal?: AbortSignal): Promise<ContentPage>;
|
|
115
126
|
/** Per month or day, newest first: how many contents the reader's listing shows released then, and how many are free. */
|
|
116
127
|
releaseDates(q?: ReleaseDatesQuery, signal?: AbortSignal): Promise<ReleaseDates>;
|
|
128
|
+
/** Every visible card in each day; opening or expanding a day needs no further request. */
|
|
129
|
+
releases(q?: DailyReleasesQuery, signal?: AbortSignal): Promise<DailyReleases>;
|
|
117
130
|
contentFilters(language?: string, signal?: AbortSignal): Promise<ContentFilters>;
|
|
118
131
|
/** Replaces the reader's filter settings; every read applies them. */
|
|
119
132
|
setContentFilters(update: ContentFiltersUpdate, language?: string): Promise<ContentFilters>;
|
package/dist/client/discovery.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { call as e } from "./route.js";
|
|
2
|
-
import { browserSearchHistory as
|
|
1
|
+
import { call as e, routeURL as t } from "./route.js";
|
|
2
|
+
import { browserSearchHistory as n } from "./browser-search-history.js";
|
|
3
3
|
//#region src/client/discovery.ts
|
|
4
|
-
|
|
4
|
+
function r(e = "7d", t = {}) {
|
|
5
|
+
return {
|
|
6
|
+
...t,
|
|
7
|
+
sort: `popular:${e}`,
|
|
8
|
+
published_within: e
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
var i = /* @__PURE__ */ new Set([
|
|
5
12
|
"groups",
|
|
6
13
|
"exclude_groups",
|
|
7
14
|
"creators",
|
|
@@ -11,29 +18,29 @@ var n = /* @__PURE__ */ new Set([
|
|
|
11
18
|
"card_entities",
|
|
12
19
|
"entity_kinds"
|
|
13
20
|
]);
|
|
14
|
-
function
|
|
21
|
+
function a(e) {
|
|
15
22
|
let t = {};
|
|
16
|
-
for (let [
|
|
23
|
+
for (let [n, r] of Object.entries(e)) t[n] = i.has(n) && Array.isArray(r) ? r.length ? r.join(",") : void 0 : r;
|
|
17
24
|
return t;
|
|
18
25
|
}
|
|
19
|
-
var
|
|
26
|
+
var o = class {
|
|
20
27
|
http;
|
|
21
28
|
browserSearches;
|
|
22
29
|
constructor(e) {
|
|
23
|
-
this.http = e, this.browserSearches =
|
|
30
|
+
this.http = e, this.browserSearches = n(e.mount("discovery"));
|
|
24
31
|
}
|
|
25
32
|
list(t = {}, n) {
|
|
26
33
|
return e(this.http, "GET", "/contents", {
|
|
27
|
-
query:
|
|
34
|
+
query: a(t),
|
|
28
35
|
signal: n
|
|
29
36
|
});
|
|
30
37
|
}
|
|
31
38
|
async search(t, n) {
|
|
32
|
-
let
|
|
33
|
-
query:
|
|
39
|
+
let r = t.cursor ? null : this.http.captureChanges(), i = await e(this.http, "GET", "/search", {
|
|
40
|
+
query: a(t),
|
|
34
41
|
signal: n
|
|
35
42
|
});
|
|
36
|
-
return
|
|
43
|
+
return r?.({ type: "searches.changed" }), i;
|
|
37
44
|
}
|
|
38
45
|
recentSearches(t = {}, n) {
|
|
39
46
|
return e(this.http, "GET", "/me/searches", {
|
|
@@ -47,35 +54,42 @@ var i = class {
|
|
|
47
54
|
}
|
|
48
55
|
typeahead(t, n) {
|
|
49
56
|
return e(this.http, "GET", "/typeahead", {
|
|
50
|
-
query:
|
|
57
|
+
query: a(t),
|
|
51
58
|
signal: n
|
|
52
59
|
});
|
|
53
60
|
}
|
|
54
|
-
similar(t, n = {},
|
|
61
|
+
similar(t, n = {}, r) {
|
|
55
62
|
return e(this.http, "GET", "/contents/{id}/similar", {
|
|
56
63
|
params: { id: t },
|
|
57
|
-
query:
|
|
58
|
-
signal:
|
|
64
|
+
query: a(n),
|
|
65
|
+
signal: r
|
|
59
66
|
});
|
|
60
67
|
}
|
|
61
68
|
recommendations(t = {}, n) {
|
|
62
69
|
return e(this.http, "GET", "/recommendations", {
|
|
63
|
-
query:
|
|
70
|
+
query: a(t),
|
|
64
71
|
signal: n
|
|
65
72
|
});
|
|
66
73
|
}
|
|
67
74
|
releaseDates(t = {}, n) {
|
|
68
75
|
return e(this.http, "GET", "/release-dates", {
|
|
69
|
-
query:
|
|
76
|
+
query: a(t),
|
|
70
77
|
signal: n
|
|
71
78
|
});
|
|
72
79
|
}
|
|
73
|
-
|
|
74
|
-
return e(this.http, "GET", "/
|
|
75
|
-
query:
|
|
80
|
+
releases(t = {}, n) {
|
|
81
|
+
return e(this.http, "GET", "/releases", {
|
|
82
|
+
query: a(t),
|
|
76
83
|
signal: n
|
|
77
84
|
});
|
|
78
85
|
}
|
|
86
|
+
async contentFilters(e, n) {
|
|
87
|
+
let r = await this.http.request(t(this.http, "GET", "/content-filters", { query: { language: e } }), {
|
|
88
|
+
signal: n,
|
|
89
|
+
guestPreferences: !1
|
|
90
|
+
});
|
|
91
|
+
return this.http.acceptGuestFilters(r.filters), r;
|
|
92
|
+
}
|
|
79
93
|
async setContentFilters(t, n) {
|
|
80
94
|
let r = this.http.captureChanges(), i = await e(this.http, "PUT", "/me/content-filters", {
|
|
81
95
|
query: { language: n },
|
|
@@ -85,7 +99,7 @@ var i = class {
|
|
|
85
99
|
}
|
|
86
100
|
history(t = {}, n) {
|
|
87
101
|
return e(this.http, "GET", "/me/history", {
|
|
88
|
-
query:
|
|
102
|
+
query: a(t),
|
|
89
103
|
signal: n
|
|
90
104
|
});
|
|
91
105
|
}
|
|
@@ -115,6 +129,6 @@ var i = class {
|
|
|
115
129
|
}
|
|
116
130
|
};
|
|
117
131
|
//#endregion
|
|
118
|
-
export {
|
|
132
|
+
export { o as DiscoveryClient, r as popularQuery, a as query };
|
|
119
133
|
|
|
120
134
|
//# sourceMappingURL=discovery.js.map
|