@openrails/contentkit-ui 0.83.0 → 0.86.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.
Files changed (219) hide show
  1. package/README.md +18 -7
  2. package/dist/client/beacons.d.ts +18 -6
  3. package/dist/client/beacons.js +26 -11
  4. package/dist/client/beacons.js.map +1 -1
  5. package/dist/client/client.d.ts +3 -0
  6. package/dist/client/client.js +29 -27
  7. package/dist/client/client.js.map +1 -1
  8. package/dist/client/codes.d.ts +6 -1
  9. package/dist/client/codes.js +4 -1
  10. package/dist/client/codes.js.map +1 -1
  11. package/dist/client/content/moderation.d.ts +14 -2
  12. package/dist/client/content/moderation.js +34 -0
  13. package/dist/client/content/moderation.js.map +1 -1
  14. package/dist/client/content/types.d.ts +13 -0
  15. package/dist/client/content/types.js.map +1 -1
  16. package/dist/client/discovery.d.ts +6 -0
  17. package/dist/client/discovery.js.map +1 -1
  18. package/dist/client/generated/error-codes.d.ts +1 -1
  19. package/dist/client/generated/error-codes.js +9 -1
  20. package/dist/client/generated/error-codes.js.map +1 -1
  21. package/dist/client/generated/routes.d.ts +91 -13
  22. package/dist/client/generated/routes.js +123 -2
  23. package/dist/client/generated/routes.js.map +1 -1
  24. package/dist/client/generated/wire.d.ts +81 -0
  25. package/dist/client/generated/wire.js.map +1 -1
  26. package/dist/client/index.d.ts +4 -2
  27. package/dist/client/media/queue.d.ts +4 -2
  28. package/dist/client/media/queue.js +18 -12
  29. package/dist/client/media/queue.js.map +1 -1
  30. package/dist/client/media/rules.d.ts +7 -1
  31. package/dist/client/media/rules.js +27 -21
  32. package/dist/client/media/rules.js.map +1 -1
  33. package/dist/client/reader-meta.d.ts +27 -0
  34. package/dist/client/reader-meta.js +99 -0
  35. package/dist/client/reader-meta.js.map +1 -0
  36. package/dist/client/stats.d.ts +32 -0
  37. package/dist/client/stats.js +35 -0
  38. package/dist/client/stats.js.map +1 -0
  39. package/dist/client/versions.d.ts +1 -1
  40. package/dist/client/versions.js +7 -2
  41. package/dist/client/versions.js.map +1 -1
  42. package/dist/client.js +30 -28
  43. package/dist/components/admin/content-manager.js +206 -206
  44. package/dist/components/admin/content-manager.js.map +1 -1
  45. package/dist/components/admin/group-manager.js +71 -70
  46. package/dist/components/admin/group-manager.js.map +1 -1
  47. package/dist/components/admin/media-item-panel.d.ts +24 -7
  48. package/dist/components/admin/media-item-panel.js +209 -197
  49. package/dist/components/admin/media-item-panel.js.map +1 -1
  50. package/dist/components/admin/poll-manager.js +25 -25
  51. package/dist/components/admin/reader-manifest.d.ts +18 -0
  52. package/dist/components/admin/reader-manifest.js +490 -0
  53. package/dist/components/admin/reader-manifest.js.map +1 -0
  54. package/dist/components/admin/stats-dashboard.d.ts +22 -0
  55. package/dist/components/admin/stats-dashboard.js +257 -0
  56. package/dist/components/admin/stats-dashboard.js.map +1 -0
  57. package/dist/components/content/card.js +43 -43
  58. package/dist/components/content/card.js.map +1 -1
  59. package/dist/components/content/editors.d.ts +2 -1
  60. package/dist/components/content/editors.js +479 -416
  61. package/dist/components/content/editors.js.map +1 -1
  62. package/dist/components/content/grid.js +47 -47
  63. package/dist/components/content/index.d.ts +2 -1
  64. package/dist/components/content/listing.js +34 -34
  65. package/dist/components/content/listing.js.map +1 -1
  66. package/dist/components/content/paywall.d.ts +17 -5
  67. package/dist/components/content/paywall.js +115 -100
  68. package/dist/components/content/paywall.js.map +1 -1
  69. package/dist/components/content/reader-stats.d.ts +12 -0
  70. package/dist/components/content/reader-stats.js +47 -0
  71. package/dist/components/content/reader-stats.js.map +1 -0
  72. package/dist/components/groups/banner.d.ts +1 -1
  73. package/dist/components/groups/banner.js +34 -34
  74. package/dist/components/groups/banner.js.map +1 -1
  75. package/dist/components/groups/directory.js +11 -11
  76. package/dist/components/groups/editor.js +294 -294
  77. package/dist/components/groups/editor.js.map +1 -1
  78. package/dist/components/groups/header.js +27 -27
  79. package/dist/components/groups/header.js.map +1 -1
  80. package/dist/components/groups/memberships.js +180 -180
  81. package/dist/components/groups/parts.d.ts +2 -4
  82. package/dist/components/groups/parts.js +69 -88
  83. package/dist/components/groups/parts.js.map +1 -1
  84. package/dist/components/media-folder-editor.js +239 -189
  85. package/dist/components/media-folder-editor.js.map +1 -1
  86. package/dist/components/notifications.js +17 -17
  87. package/dist/components/slot-upload.d.ts +2 -2
  88. package/dist/components/slot-upload.js +84 -79
  89. package/dist/components/slot-upload.js.map +1 -1
  90. package/dist/components/social/bans.js +34 -34
  91. package/dist/components/social/comments.js +59 -45
  92. package/dist/components/social/comments.js.map +1 -1
  93. package/dist/components/social/index.d.ts +1 -0
  94. package/dist/components/social/moderation.d.ts +5 -4
  95. package/dist/components/social/moderation.js +177 -160
  96. package/dist/components/social/moderation.js.map +1 -1
  97. package/dist/components/social/parts.js +22 -14
  98. package/dist/components/social/parts.js.map +1 -1
  99. package/dist/components/social/rules.d.ts +23 -0
  100. package/dist/components/social/rules.js +261 -0
  101. package/dist/components/social/rules.js.map +1 -0
  102. package/dist/components/video-player.d.ts +2 -0
  103. package/dist/components/video-player.js +318 -314
  104. package/dist/components/video-player.js.map +1 -1
  105. package/dist/i18n/context.d.ts +2 -0
  106. package/dist/i18n/context.js +23 -5
  107. package/dist/i18n/context.js.map +1 -1
  108. package/dist/index.d.ts +3 -1
  109. package/dist/index.js +28 -24
  110. package/dist/locales/admin/de.js +105 -0
  111. package/dist/locales/admin/de.js.map +1 -1
  112. package/dist/locales/admin/en.d.ts +105 -0
  113. package/dist/locales/admin/en.js +105 -0
  114. package/dist/locales/admin/en.js.map +1 -1
  115. package/dist/locales/admin/es.js +105 -0
  116. package/dist/locales/admin/es.js.map +1 -1
  117. package/dist/locales/admin/ja.js +105 -0
  118. package/dist/locales/admin/ja.js.map +1 -1
  119. package/dist/locales/admin/ko.js +105 -0
  120. package/dist/locales/admin/ko.js.map +1 -1
  121. package/dist/locales/admin/zh.js +105 -0
  122. package/dist/locales/admin/zh.js.map +1 -1
  123. package/dist/locales/content/de.js +6 -0
  124. package/dist/locales/content/de.js.map +1 -1
  125. package/dist/locales/content/en.d.ts +6 -0
  126. package/dist/locales/content/en.js +6 -0
  127. package/dist/locales/content/en.js.map +1 -1
  128. package/dist/locales/content/es.js +6 -0
  129. package/dist/locales/content/es.js.map +1 -1
  130. package/dist/locales/content/ja.js +6 -0
  131. package/dist/locales/content/ja.js.map +1 -1
  132. package/dist/locales/content/ko.js +6 -0
  133. package/dist/locales/content/ko.js.map +1 -1
  134. package/dist/locales/content/zh.js +6 -0
  135. package/dist/locales/content/zh.js.map +1 -1
  136. package/dist/locales/de.js +4 -1
  137. package/dist/locales/de.js.map +1 -1
  138. package/dist/locales/discovery/de.js +1 -0
  139. package/dist/locales/discovery/de.js.map +1 -1
  140. package/dist/locales/discovery/en.d.ts +1 -0
  141. package/dist/locales/discovery/en.js +1 -0
  142. package/dist/locales/discovery/en.js.map +1 -1
  143. package/dist/locales/discovery/es.js +1 -0
  144. package/dist/locales/discovery/es.js.map +1 -1
  145. package/dist/locales/discovery/ja.js +1 -0
  146. package/dist/locales/discovery/ja.js.map +1 -1
  147. package/dist/locales/discovery/ko.js +1 -0
  148. package/dist/locales/discovery/ko.js.map +1 -1
  149. package/dist/locales/discovery/zh.js +1 -0
  150. package/dist/locales/discovery/zh.js.map +1 -1
  151. package/dist/locales/en.d.ts +118 -0
  152. package/dist/locales/en.js +4 -1
  153. package/dist/locales/en.js.map +1 -1
  154. package/dist/locales/es.js +4 -1
  155. package/dist/locales/es.js.map +1 -1
  156. package/dist/locales/ja.js +4 -1
  157. package/dist/locales/ja.js.map +1 -1
  158. package/dist/locales/ko.js +4 -1
  159. package/dist/locales/ko.js.map +1 -1
  160. package/dist/locales/social/de.js +3 -0
  161. package/dist/locales/social/de.js.map +1 -1
  162. package/dist/locales/social/en.d.ts +3 -0
  163. package/dist/locales/social/en.js +3 -0
  164. package/dist/locales/social/en.js.map +1 -1
  165. package/dist/locales/social/es.js +3 -0
  166. package/dist/locales/social/es.js.map +1 -1
  167. package/dist/locales/social/ja.js +3 -0
  168. package/dist/locales/social/ja.js.map +1 -1
  169. package/dist/locales/social/ko.js +3 -0
  170. package/dist/locales/social/ko.js.map +1 -1
  171. package/dist/locales/social/zh.js +3 -0
  172. package/dist/locales/social/zh.js.map +1 -1
  173. package/dist/locales/zh.js +4 -1
  174. package/dist/locales/zh.js.map +1 -1
  175. package/dist/react/admin.d.ts +1 -1
  176. package/dist/react/admin.js +24 -23
  177. package/dist/react/admin.js.map +1 -1
  178. package/dist/react/canonical.d.ts +4 -3
  179. package/dist/react/canonical.js +34 -33
  180. package/dist/react/canonical.js.map +1 -1
  181. package/dist/react/comments.d.ts +29 -1
  182. package/dist/react/comments.js +30 -2
  183. package/dist/react/comments.js.map +1 -1
  184. package/dist/react/contents.js +144 -144
  185. package/dist/react/contents.js.map +1 -1
  186. package/dist/react/context.d.ts +1 -1
  187. package/dist/react/context.js.map +1 -1
  188. package/dist/react/detail.d.ts +12 -6
  189. package/dist/react/detail.js +31 -24
  190. package/dist/react/detail.js.map +1 -1
  191. package/dist/react/discovery.js +1 -1
  192. package/dist/react/discovery.js.map +1 -1
  193. package/dist/react/folder.d.ts +7 -2
  194. package/dist/react/folder.js +165 -147
  195. package/dist/react/folder.js.map +1 -1
  196. package/dist/react/groups.d.ts +0 -1
  197. package/dist/react/groups.js +13 -12
  198. package/dist/react/groups.js.map +1 -1
  199. package/dist/react/index.d.ts +5 -3
  200. package/dist/react/location.d.ts +11 -0
  201. package/dist/react/location.js +18 -0
  202. package/dist/react/location.js.map +1 -0
  203. package/dist/react/public.d.ts +8 -0
  204. package/dist/react/public.js +32 -27
  205. package/dist/react/public.js.map +1 -1
  206. package/dist/react/resources.js +6 -0
  207. package/dist/react/resources.js.map +1 -1
  208. package/dist/react/stats.d.ts +24 -0
  209. package/dist/react/stats.js +28 -0
  210. package/dist/react/stats.js.map +1 -0
  211. package/dist/react/use-resource.js +8 -7
  212. package/dist/react/use-resource.js.map +1 -1
  213. package/dist/react/versions.js +20 -20
  214. package/dist/react.js +26 -24
  215. package/dist/styles.css +1 -1
  216. package/dist/urls/index.d.ts +35 -10
  217. package/dist/urls.js +100 -70
  218. package/dist/urls.js.map +1 -1
  219. package/package.json +1 -1
@@ -3,19 +3,19 @@ import { fill as t, publicURL as n } from "../public.js";
3
3
  import { ratio as r } from "../aspect.js";
4
4
  import { stem as i } from "./client.js";
5
5
  //#region src/client/media/rules.ts
6
- var a = (e) => e?.uploads ?? [], o = "{name}", s = (e) => e.endsWith(o), c = (e) => s(e) ? e.slice(0, -6) : e;
7
- function l(e, t) {
6
+ var a = [], o = (e) => e?.uploads ?? a, s = "{name}", c = (e) => e.endsWith(s), l = (e) => c(e) ? e.slice(0, -6) : e;
7
+ function u(e, t) {
8
8
  let n = i(t);
9
- return e.find((e) => !s(e.path) && e.path === n) ?? e.find((e) => {
10
- if (!s(e.path)) return !1;
11
- let t = c(e.path);
9
+ return e.find((e) => !c(e.path) && e.path === n) ?? e.find((e) => {
10
+ if (!c(e.path)) return !1;
11
+ let t = l(e.path);
12
12
  return n.startsWith(t) && n.length > t.length && !n.slice(t.length).includes("/");
13
13
  });
14
14
  }
15
- function u(e, t, n = [t]) {
16
- return (e?.files ?? []).filter((e) => e.upload && !e.unattached && l(n, e.path)?.path === t.path);
15
+ function d(e, t, n = [t]) {
16
+ return (e?.files ?? []).filter((e) => e.upload && !e.unattached && u(n, e.path)?.path === t.path);
17
17
  }
18
- var d = {
18
+ var f = {
19
19
  jpg: "image/jpeg",
20
20
  jpeg: "image/jpeg",
21
21
  png: "image/png",
@@ -35,27 +35,27 @@ var d = {
35
35
  ssa: "text/x-ssa",
36
36
  zip: "application/zip"
37
37
  };
38
- function f(e, t) {
38
+ function p(e, t) {
39
39
  if (e.type && (!t || t.includes(e.type))) return e;
40
- let n = d[e.name.split(".").pop()?.toLowerCase() ?? ""];
40
+ let n = f[e.name.split(".").pop()?.toLowerCase() ?? ""];
41
41
  return n && n !== e.type ? new File([e], e.name, {
42
42
  type: n,
43
43
  lastModified: e.lastModified
44
44
  }) : e;
45
45
  }
46
- function p(e) {
46
+ function m(e) {
47
47
  let t = [...new Set(e.flatMap((e) => e.types))];
48
48
  if (!t.length) return;
49
- let n = Object.entries(d).filter(([, e]) => t.includes(e)).map(([e]) => `.${e}`);
49
+ let n = Object.entries(f).filter(([, e]) => t.includes(e)).map(([e]) => `.${e}`);
50
50
  return [...t, ...n].join(",");
51
51
  }
52
- function m(t, n, r = () => 0) {
52
+ function h(t, n, r = () => 0) {
53
53
  let i = {
54
54
  accepted: [],
55
55
  refused: []
56
56
  }, a = /* @__PURE__ */ new Map(), o = [...new Set(n.flatMap((e) => e.types))];
57
57
  for (let s of t) {
58
- let t = f(s, o), c = n.find((e) => !e.types.length || e.types.includes(t.type));
58
+ let t = p(s, o), c = n.find((e) => !e.types.length || e.types.includes(t.type));
59
59
  if (!c) {
60
60
  let n = {
61
61
  type: t.type || void 0,
@@ -104,7 +104,7 @@ function m(t, n, r = () => 0) {
104
104
  }
105
105
  return i;
106
106
  }
107
- function h(e, t) {
107
+ function g(e, t) {
108
108
  let n = new Set([...t].map(i));
109
109
  if (!n.has(i(e))) return e;
110
110
  let r = e.lastIndexOf("."), a = r > 0 ? e.slice(0, r) : e, o = r > 0 ? e.slice(r) : "";
@@ -113,17 +113,23 @@ function h(e, t) {
113
113
  if (!n.has(i(t))) return t;
114
114
  }
115
115
  }
116
- function g(e) {
116
+ function _(e) {
117
117
  let t = (e) => String(Math.round(e * 100) / 100);
118
118
  return e >= 1 ? `${t(e)}:1` : `1:${t(1 / e)}`;
119
119
  }
120
- function _(e, t, n, r) {
121
- let i = e.filter((e) => e.kind === t && l([{ path: e.from }], n));
120
+ function v(e, t, n, r) {
121
+ let i = e.filter((e) => e.kind === t && u([{ path: e.from }], n));
122
122
  return (r ? i.find((e) => e.name === r) : void 0) ?? i.find((e) => e.aspect) ?? i[0];
123
123
  }
124
- function v(e, i) {
124
+ function y(e, t) {
125
+ return !e.default || e.first || e.to.includes(s) ? null : x(e, t);
126
+ }
127
+ function b(e, t) {
128
+ return e.stable ? x(e, t) : null;
129
+ }
130
+ function x(e, i) {
125
131
  let a = r(e.aspect);
126
- return !e.default || e.first || e.to.includes(o) ? null : {
132
+ return {
127
133
  preset: e.name,
128
134
  aspect: e.aspect,
129
135
  renditions: e.widths.map((r) => ({
@@ -134,6 +140,6 @@ function v(e, i) {
134
140
  };
135
141
  }
136
142
  //#endregion
137
- export { p as acceptOf, v as defaultImage, u as filesFor, s as isPattern, _ as presetFor, g as ratioLabel, c as ruleDir, l as ruleFor, m as screenFiles, h as uniqueName, a as uploadRules, f as withMediaType };
143
+ export { m as acceptOf, y as defaultImage, d as filesFor, c as isPattern, v as presetFor, _ as ratioLabel, l as ruleDir, u as ruleFor, h as screenFiles, b as stableImage, g as uniqueName, o as uploadRules, p as withMediaType };
138
144
 
139
145
  //# sourceMappingURL=rules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\n/** An editor read's upload rules (a viewer read has none). */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? [];\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */\nexport function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined {\n const at = presets.filter((p) => p.kind === kind && ruleFor([{ path: p.from }], path));\n return (name ? at.find((p) => p.name === name) : undefined) ?? at.find((p) => p.aspect) ?? at[0];\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). null when the preset has no default (`rule.default`).\n */\nexport function defaultImage(rule: PresetRule, id: string): PublicPreset | null {\n const r = ratio(rule.aspect);\n if (!rule.default || rule.first || rule.to.includes(NAME)) return null;\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w })), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rules.js","names":[],"sources":["../../../src/client/media/rules.ts"],"sourcesContent":["import { ContentKitError } from \"../errors.js\";\nimport type { ErrorDetails, PresetRule, ReadResult, UploadRule } from \"../generated/wire.js\";\nimport { fill, publicURL, type PublicPreset } from \"../public.js\";\nimport { ratio } from \"../aspect.js\";\nimport { stem } from \"./client.js\";\n\nconst NO_RULES: UploadRule[] = [];\n\n/** An editor read's upload rules (a viewer read has none): one empty list, so memos keyed on them hold. */\nexport const uploadRules = (read: ReadResult | null | undefined): UploadRule[] => read?.uploads ?? NO_RULES;\n\nconst NAME = \"{name}\";\n\n/** A pattern path (\"originals/{name}\") holds many files; a literal (\"cover\") one. */\nexport const isPattern = (path: string) => path.endsWith(NAME);\n\n/** The directory of a pattern (\"originals/\"), or the literal itself. */\nexport const ruleDir = (path: string) => (isPattern(path) ? path.slice(0, -NAME.length) : path);\n\n/** The rule an upload path (with or without its extension) falls under: a literal wins over a pattern. */\nexport function ruleFor<R extends { path: string }>(rules: readonly R[], path: string): R | undefined {\n const s = stem(path);\n return (\n rules.find((r) => !isPattern(r.path) && r.path === s) ??\n rules.find((r) => {\n if (!isPattern(r.path)) return false;\n const dir = ruleDir(r.path);\n return s.startsWith(dir) && s.length > dir.length && !s.slice(dir.length).includes(\"/\");\n })\n );\n}\n\n/** Uploads of the read under rule (attached, in manifest order). */\nexport function filesFor(read: ReadResult | null | undefined, rule: { path: string }, rules: readonly { path: string }[] = [rule]) {\n return (read?.files ?? []).filter((f) => f.upload && !f.unattached && ruleFor(rules, f.path)?.path === rule.path);\n}\n\nconst BY_EXTENSION: Record<string, string> = {\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n webp: \"image/webp\",\n gif: \"image/gif\",\n avif: \"image/avif\",\n mp4: \"video/mp4\",\n m4v: \"video/mp4\",\n mov: \"video/quicktime\",\n mkv: \"video/x-matroska\",\n webm: \"video/webm\",\n m4a: \"audio/mp4\",\n mp3: \"audio/mpeg\",\n vtt: \"text/vtt\",\n srt: \"application/x-subrip\",\n ass: \"text/x-ass\",\n ssa: \"text/x-ssa\",\n zip: \"application/zip\",\n};\n\n/** The file with a content type from its extension when the browser left it empty or nonstandard (.mkv, .mov, .srt). */\nexport function withMediaType(file: File, allowed?: readonly string[]): File {\n if (file.type && (!allowed || allowed.includes(file.type))) return file;\n const type = BY_EXTENSION[file.name.split(\".\").pop()?.toLowerCase() ?? \"\"];\n return type && type !== file.type ? new File([file], file.name, { type, lastModified: file.lastModified }) : file;\n}\n\n/** `accept` for a file input from the rules' types (plus the extensions browsers leave untyped). */\nexport function acceptOf(rules: readonly UploadRule[]): string | undefined {\n const types = [...new Set(rules.flatMap((r) => r.types))];\n if (!types.length) return undefined;\n const exts = Object.entries(BY_EXTENSION).filter(([, t]) => types.includes(t)).map(([e]) => `.${e}`);\n return [...types, ...exts].join(\",\");\n}\n\nexport interface Screened {\n /** Files to upload, each with its rule (typed by extension when needed). */\n accepted: { file: File; rule: UploadRule }[];\n /** Files the server would refuse, with the refusal it would answer. */\n refused: { file: File; error: ContentKitError }[];\n}\n\n/**\n * Checks files against the rules before any upload: a type some rule takes\n * (the first that does), its size, and the rule's file cap counting\n * have(rule) files already there. A rule without types, size or cap lets\n * that check through; the server decides.\n */\nexport function screenFiles(files: Iterable<File>, rules: readonly UploadRule[], have: (rule: UploadRule) => number = () => 0): Screened {\n const out: Screened = { accepted: [], refused: [] };\n const counts = new Map<UploadRule, number>();\n const allowed = [...new Set(rules.flatMap((r) => r.types))];\n for (const picked of files) {\n const file = withMediaType(picked, allowed);\n const rule = rules.find((r) => !r.types.length || r.types.includes(file.type));\n if (!rule) {\n const details: ErrorDetails = { type: file.type || undefined, allowed };\n out.refused.push({ file, error: new ContentKitError(\"type_not_allowed\", `${file.name}: type not allowed`, { status: 415, details }) });\n continue;\n }\n if (rule.max_bytes > 0 && file.size > rule.max_bytes) {\n const details: ErrorDetails = { type: file.type, size: file.size, max_bytes: rule.max_bytes };\n out.refused.push({ file, error: new ContentKitError(\"too_large\", `${file.name}: too large`, { status: 413, details }) });\n continue;\n }\n const n = counts.get(rule) ?? have(rule);\n if (rule.max && n >= rule.max) {\n const details: ErrorDetails = { max: rule.max };\n out.refused.push({ file, error: new ContentKitError(\"too_many_files\", `${file.name}: at most ${rule.max} files`, { status: 409, details }) });\n continue;\n }\n counts.set(rule, n + 1);\n out.accepted.push({ file, rule });\n }\n return out;\n}\n\n/** name, or name-2, name-3… so its stem is not among taken (names or paths without the directory). */\nexport function uniqueName(name: string, taken: Iterable<string>): string {\n const stems = new Set([...taken].map(stem));\n if (!stems.has(stem(name))) return name;\n const dot = name.lastIndexOf(\".\");\n const base = dot > 0 ? name.slice(0, dot) : name;\n const ext = dot > 0 ? name.slice(dot) : \"\";\n for (let n = 2; ; n++) {\n const candidate = `${base}-${n}${ext}`;\n if (!stems.has(stem(candidate))) return candidate;\n }\n}\n\n/** \"16:9\" style label of a width/height ratio: 2.4 → \"2.4:1\", 0.4167 → \"1:2.4\". */\nexport function ratioLabel(r: number): string {\n const round = (v: number) => String(Math.round(v * 100) / 100);\n return r >= 1 ? `${round(r)}:1` : `1:${round(1 / r)}`;\n}\n\n/** The kind's public preset for an upload path: the one named name, else the first with an aspect, else the first. */\nexport function presetFor(presets: readonly PresetRule[], kind: string, path: string, name?: string): PresetRule | undefined {\n const at = presets.filter((p) => p.kind === kind && ruleFor([{ path: p.from }], path));\n return (name ? at.find((p) => p.name === name) : undefined) ?? at.find((p) => p.aspect) ?? at[0];\n}\n\n/**\n * The kind's default image for a preset, at every width: what the gateway\n * serves for an item without its own (a URL from the template never names a\n * current file). null when the preset has no default (`rule.default`).\n */\nexport function defaultImage(rule: PresetRule, id: string): PublicPreset | null {\n if (!rule.default || rule.first || rule.to.includes(NAME)) return null;\n return logicalImage(rule, id);\n}\n\n/**\n * A Stable preset's image of an item (`rule.stable`, e.g. the content\n * model's `avatars`): URLs built from the id that show its current image,\n * else the default. null for any other preset.\n */\nexport function stableImage(rule: PresetRule, id: string): PublicPreset | null {\n return rule.stable ? logicalImage(rule, id) : null;\n}\n\nfunction logicalImage(rule: PresetRule, id: string): PublicPreset {\n const r = ratio(rule.aspect);\n return {\n preset: rule.name,\n aspect: rule.aspect,\n renditions: rule.widths.map((w) => ({ url: publicURL(rule.base, rule.namespace, rule.kind, id, fill(rule.to, { w })), w, ...(r ? { h: Math.round(w / r) } : {}) })),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { ForegroundCue, ReaderMeta, ReaderPlan } from './generated/wire.js';
2
+ /** What a version's reader metadata may name: its pages in reading order and its audio tracks. */
3
+ export interface ReaderNames {
4
+ pages: string[];
5
+ files: string[];
6
+ }
7
+ /** The names an editor's reader plan lists: every page of every segment, in order, and the playback tracks. */
8
+ export declare function readerNames(plan: ReaderPlan): ReaderNames;
9
+ /** An empty schema-1 reader metadata. */
10
+ export declare const emptyReaderMeta: () => ReaderMeta;
11
+ export interface AudioMatch {
12
+ /** One cue per matched file: on the anchor its stem names, else the page. */
13
+ foreground: ForegroundCue[];
14
+ unmatchedPages: string[];
15
+ unmatchedFiles: string[];
16
+ }
17
+ /** One foreground cue per audio file whose stem names an anchor (first) or a page, case-insensitively ("0002.mp3" plays at page "0002"). */
18
+ export declare function matchAudio(names: ReaderNames, anchors?: readonly string[]): AudioMatch;
19
+ /** Where a problem is: an anchor, a cue or the defaults, by index. */
20
+ export interface ReaderMetaProblem {
21
+ at: "anchors" | "background" | "foreground" | "defaults";
22
+ index: number;
23
+ /** id: empty or repeated; offset: outside 0-100; duration, seconds: not positive; page, anchor, file: names nothing; target: a foreground cue without a page or an anchor. */
24
+ problem: "id" | "offset" | "duration" | "seconds" | "page" | "anchor" | "file" | "target";
25
+ }
26
+ /** What the server refuses, and a foreground cue without a place: so an editor sees it before saving. */
27
+ export declare function readerMetaProblems(meta: ReaderMeta, names: ReaderNames): ReaderMetaProblem[];
@@ -0,0 +1,99 @@
1
+ //#region src/client/reader-meta.ts
2
+ function e(e) {
3
+ return {
4
+ pages: e.segments.flatMap((e) => (e.pages ?? []).map((e) => e.name)),
5
+ files: (e.playback?.tracks ?? []).map((e) => e.name)
6
+ };
7
+ }
8
+ var t = () => ({
9
+ schema_version: 1,
10
+ anchors: [],
11
+ background: [],
12
+ foreground: []
13
+ }), n = (e) => {
14
+ let t = e.lastIndexOf(".");
15
+ return (t > 0 ? e.slice(0, t) : e).toLowerCase();
16
+ };
17
+ function r(e, t = []) {
18
+ let r = /* @__PURE__ */ new Map();
19
+ for (let t of e.files) r.has(n(t)) || r.set(n(t), t);
20
+ let i = /* @__PURE__ */ new Set(), a = [];
21
+ for (let e of t) {
22
+ let t = r.get(n(e));
23
+ t && !i.has(t) && (a.push({
24
+ file: t,
25
+ anchor: e
26
+ }), i.add(t));
27
+ }
28
+ let o = [];
29
+ for (let t of e.pages) {
30
+ let e = r.get(n(t));
31
+ e && !i.has(e) ? (a.push({
32
+ file: e,
33
+ page: t
34
+ }), i.add(e)) : o.push(t);
35
+ }
36
+ return {
37
+ foreground: a,
38
+ unmatchedPages: o,
39
+ unmatchedFiles: e.files.filter((e) => !i.has(e))
40
+ };
41
+ }
42
+ function i(e, t) {
43
+ let n = [], r = new Set(t.pages), i = new Set(t.files), a = /* @__PURE__ */ new Set(), o = e.defaults?.auto_advance_seconds;
44
+ o !== void 0 && !(o > 0) && n.push({
45
+ at: "defaults",
46
+ index: 0,
47
+ problem: "seconds"
48
+ }), (e.anchors ?? []).forEach((e, t) => {
49
+ (!e.id.trim() || a.has(e.id)) && n.push({
50
+ at: "anchors",
51
+ index: t,
52
+ problem: "id"
53
+ }), a.add(e.id), r.has(e.page) || n.push({
54
+ at: "anchors",
55
+ index: t,
56
+ problem: "page"
57
+ }), e.offset_percent >= 0 && e.offset_percent <= 100 || n.push({
58
+ at: "anchors",
59
+ index: t,
60
+ problem: "offset"
61
+ }), e.duration_seconds !== void 0 && !(e.duration_seconds > 0) && n.push({
62
+ at: "anchors",
63
+ index: t,
64
+ problem: "duration"
65
+ });
66
+ });
67
+ let s = (e, t, i, o) => {
68
+ i !== void 0 && !r.has(i) && n.push({
69
+ at: e,
70
+ index: t,
71
+ problem: "page"
72
+ }), o !== void 0 && !a.has(o) && n.push({
73
+ at: e,
74
+ index: t,
75
+ problem: "anchor"
76
+ });
77
+ };
78
+ return (e.background ?? []).forEach((e, t) => {
79
+ i.has(e.file) || n.push({
80
+ at: "background",
81
+ index: t,
82
+ problem: "file"
83
+ }), s("background", t, e.from_page, e.from_anchor), s("background", t, e.to_page, e.to_anchor);
84
+ }), (e.foreground ?? []).forEach((e, t) => {
85
+ i.has(e.file) || n.push({
86
+ at: "foreground",
87
+ index: t,
88
+ problem: "file"
89
+ }), e.page === void 0 == (e.anchor === void 0) && n.push({
90
+ at: "foreground",
91
+ index: t,
92
+ problem: "target"
93
+ }), s("foreground", t, e.page, e.anchor);
94
+ }), n;
95
+ }
96
+ //#endregion
97
+ export { t as emptyReaderMeta, r as matchAudio, i as readerMetaProblems, e as readerNames };
98
+
99
+ //# sourceMappingURL=reader-meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader-meta.js","names":[],"sources":["../../src/client/reader-meta.ts"],"sourcesContent":["import type { ForegroundCue, ReaderMeta, ReaderPlan } from \"./generated/wire.js\";\n\n/** What a version's reader metadata may name: its pages in reading order and its audio tracks. */\nexport interface ReaderNames {\n pages: string[];\n files: string[];\n}\n\n/** The names an editor's reader plan lists: every page of every segment, in order, and the playback tracks. */\nexport function readerNames(plan: ReaderPlan): ReaderNames {\n return { pages: plan.segments.flatMap((s) => (s.pages ?? []).map((p) => p.name)), files: (plan.playback?.tracks ?? []).map((t) => t.name) };\n}\n\n/** An empty schema-1 reader metadata. */\nexport const emptyReaderMeta = (): ReaderMeta => ({ schema_version: 1, anchors: [], background: [], foreground: [] });\n\nconst stem = (name: string) => {\n const i = name.lastIndexOf(\".\");\n return (i > 0 ? name.slice(0, i) : name).toLowerCase();\n};\n\nexport interface AudioMatch {\n /** One cue per matched file: on the anchor its stem names, else the page. */\n foreground: ForegroundCue[];\n unmatchedPages: string[];\n unmatchedFiles: string[];\n}\n\n/** One foreground cue per audio file whose stem names an anchor (first) or a page, case-insensitively (\"0002.mp3\" plays at page \"0002\"). */\nexport function matchAudio(names: ReaderNames, anchors: readonly string[] = []): AudioMatch {\n const byStem = new Map<string, string>();\n for (const f of names.files) if (!byStem.has(stem(f))) byStem.set(stem(f), f);\n const used = new Set<string>();\n const foreground: ForegroundCue[] = [];\n for (const anchor of anchors) {\n const file = byStem.get(stem(anchor));\n if (file && !used.has(file)) {\n foreground.push({ file, anchor });\n used.add(file);\n }\n }\n const unmatchedPages: string[] = [];\n for (const page of names.pages) {\n const file = byStem.get(stem(page));\n if (file && !used.has(file)) {\n foreground.push({ file, page });\n used.add(file);\n } else unmatchedPages.push(page);\n }\n return { foreground, unmatchedPages, unmatchedFiles: names.files.filter((f) => !used.has(f)) };\n}\n\n/** Where a problem is: an anchor, a cue or the defaults, by index. */\nexport interface ReaderMetaProblem {\n at: \"anchors\" | \"background\" | \"foreground\" | \"defaults\";\n index: number;\n /** id: empty or repeated; offset: outside 0-100; duration, seconds: not positive; page, anchor, file: names nothing; target: a foreground cue without a page or an anchor. */\n problem: \"id\" | \"offset\" | \"duration\" | \"seconds\" | \"page\" | \"anchor\" | \"file\" | \"target\";\n}\n\n/** What the server refuses, and a foreground cue without a place: so an editor sees it before saving. */\nexport function readerMetaProblems(meta: ReaderMeta, names: ReaderNames): ReaderMetaProblem[] {\n const out: ReaderMetaProblem[] = [];\n const pages = new Set(names.pages);\n const files = new Set(names.files);\n const ids = new Set<string>();\n const auto = meta.defaults?.auto_advance_seconds;\n if (auto !== undefined && !(auto > 0)) out.push({ at: \"defaults\", index: 0, problem: \"seconds\" });\n (meta.anchors ?? []).forEach((a, index) => {\n if (!a.id.trim() || ids.has(a.id)) out.push({ at: \"anchors\", index, problem: \"id\" });\n ids.add(a.id);\n if (!pages.has(a.page)) out.push({ at: \"anchors\", index, problem: \"page\" });\n if (!(a.offset_percent >= 0 && a.offset_percent <= 100)) out.push({ at: \"anchors\", index, problem: \"offset\" });\n if (a.duration_seconds !== undefined && !(a.duration_seconds > 0)) out.push({ at: \"anchors\", index, problem: \"duration\" });\n });\n const place = (at: ReaderMetaProblem[\"at\"], index: number, page?: string, anchor?: string) => {\n if (page !== undefined && !pages.has(page)) out.push({ at, index, problem: \"page\" });\n if (anchor !== undefined && !ids.has(anchor)) out.push({ at, index, problem: \"anchor\" });\n };\n (meta.background ?? []).forEach((c, index) => {\n if (!files.has(c.file)) out.push({ at: \"background\", index, problem: \"file\" });\n place(\"background\", index, c.from_page, c.from_anchor);\n place(\"background\", index, c.to_page, c.to_anchor);\n });\n (meta.foreground ?? []).forEach((c, index) => {\n if (!files.has(c.file)) out.push({ at: \"foreground\", index, problem: \"file\" });\n if ((c.page === undefined) === (c.anchor === undefined)) out.push({ at: \"foreground\", index, problem: \"target\" });\n place(\"foreground\", index, c.page, c.anchor);\n });\n return out;\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { Activity, MyStats, TopContents } from './generated/wire.js';
2
+ import { Period } from './discovery.js';
3
+ import { Http } from './http.js';
4
+ export interface ActivityQuery {
5
+ kind: string;
6
+ /** The last days, today included: 7 by default, at most 90. */
7
+ days?: number;
8
+ }
9
+ export interface TopQuery {
10
+ kind: string;
11
+ /** views (the default) or comments. */
12
+ metric?: "views" | "comments";
13
+ /** 30d by default. */
14
+ period?: Period;
15
+ access?: readonly ("public" | "tier" | "group" | "ppv" | "group_ppv")[];
16
+ tiers?: readonly string[];
17
+ /** Titles and paths in this language; the site's first by default. */
18
+ language?: string;
19
+ /** 10 by default, at most 100. */
20
+ limit?: number;
21
+ }
22
+ /** Content statistics: staff activity and rankings (root:content:view-restricted), and the signed-in reader's own. */
23
+ export declare class StatsClient {
24
+ private readonly http;
25
+ constructor(http: Http);
26
+ /** A kind's views per UTC day, newest first: view sessions, distinct viewers and signed-in viewers. */
27
+ activity(q: ActivityQuery, signal?: AbortSignal): Promise<Activity>;
28
+ /** A kind's live, listed content ranked by views or published comments over a period. */
29
+ top(q: TopQuery, signal?: AbortSignal): Promise<TopContents>;
30
+ /** The signed-in reader's statistics: contents finished per kind, comments and the reactions they received. */
31
+ mine(signal?: AbortSignal): Promise<MyStats>;
32
+ }
@@ -0,0 +1,35 @@
1
+ import { call as e } from "./route.js";
2
+ //#region src/client/stats.ts
3
+ var t = class {
4
+ http;
5
+ constructor(e) {
6
+ this.http = e;
7
+ }
8
+ activity(t, n) {
9
+ return e(this.http, "GET", "/stats/activity", {
10
+ query: {
11
+ kind: t.kind,
12
+ days: t.days
13
+ },
14
+ signal: n
15
+ });
16
+ }
17
+ top(t, n) {
18
+ let { access: r, tiers: i, ...a } = t;
19
+ return e(this.http, "GET", "/stats/top", {
20
+ query: {
21
+ ...a,
22
+ access: r?.join(","),
23
+ tiers: i?.join(",")
24
+ },
25
+ signal: n
26
+ });
27
+ }
28
+ mine(t) {
29
+ return e(this.http, "GET", "/me/stats", { signal: t });
30
+ }
31
+ };
32
+ //#endregion
33
+ export { t as StatsClient };
34
+
35
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","names":[],"sources":["../../src/client/stats.ts"],"sourcesContent":["import type { Activity, MyStats, TopContents } from \"./generated/wire.js\";\nimport type { Period } from \"./discovery.js\";\nimport type { Http } from \"./http.js\";\nimport { call } from \"./route.js\";\n\nexport interface ActivityQuery {\n kind: string;\n /** The last days, today included: 7 by default, at most 90. */\n days?: number;\n}\n\nexport interface TopQuery {\n kind: string;\n /** views (the default) or comments. */\n metric?: \"views\" | \"comments\";\n /** 30d by default. */\n period?: Period;\n access?: readonly (\"public\" | \"tier\" | \"group\" | \"ppv\" | \"group_ppv\")[];\n tiers?: readonly string[];\n /** Titles and paths in this language; the site's first by default. */\n language?: string;\n /** 10 by default, at most 100. */\n limit?: number;\n}\n\n/** Content statistics: staff activity and rankings (root:content:view-restricted), and the signed-in reader's own. */\nexport class StatsClient {\n constructor(private readonly http: Http) {}\n\n /** A kind's views per UTC day, newest first: view sessions, distinct viewers and signed-in viewers. */\n activity(q: ActivityQuery, signal?: AbortSignal): Promise<Activity> {\n return call(this.http, \"GET\", \"/stats/activity\", { query: { kind: q.kind, days: q.days }, signal });\n }\n /** A kind's live, listed content ranked by views or published comments over a period. */\n top(q: TopQuery, signal?: AbortSignal): Promise<TopContents> {\n const { access, tiers, ...rest } = q;\n return call(this.http, \"GET\", \"/stats/top\", { query: { ...rest, access: access?.join(\",\"), tiers: tiers?.join(\",\") }, signal });\n }\n /** The signed-in reader's statistics: contents finished per kind, comments and the reactions they received. */\n mine(signal?: AbortSignal): Promise<MyStats> {\n return call(this.http, \"GET\", \"/me/stats\", { signal });\n }\n}\n"],"mappings":";;AA0BA,IAAa,IAAb,MAAyB;CACM;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,SAAS,GAAkB,GAAyC;EAClE,OAAO,EAAK,KAAK,MAAM,OAAO,mBAAmB;GAAE,OAAO;IAAE,MAAM,EAAE;IAAM,MAAM,EAAE;GAAK;GAAG;EAAO,CAAC;CACpG;CAEA,IAAI,GAAa,GAA4C;EAC3D,IAAM,EAAE,WAAQ,UAAO,GAAG,MAAS;EACnC,OAAO,EAAK,KAAK,MAAM,OAAO,cAAc;GAAE,OAAO;IAAE,GAAG;IAAM,QAAQ,GAAQ,KAAK,GAAG;IAAG,OAAO,GAAO,KAAK,GAAG;GAAE;GAAG;EAAO,CAAC;CAChI;CAEA,KAAK,GAAwC;EAC3C,OAAO,EAAK,KAAK,MAAM,OAAO,aAAa,EAAE,UAAO,CAAC;CACvD;AACF"}
@@ -8,7 +8,7 @@ export declare class VersionsClient {
8
8
  constructor(http: Http);
9
9
  /** The version's reader plan for this viewer; Pending while it has no current publication. */
10
10
  reader(content: string, version: string, signal?: AbortSignal): Promise<ReaderPlan | Pending>;
11
- /** Editors: the version's anchors and audio cues. */
11
+ /** Editors: replaces the version's reader metadata (anchors, audio cues, defaults). */
12
12
  setReader(content: string, version: string, meta: ReaderMeta): Promise<void>;
13
13
  /** Editors: fills a version without media from a sibling, in a durable job. */
14
14
  fork(content: string, version: string, body: ForkBody): Promise<Pending>;
@@ -15,13 +15,18 @@ var r = (e) => !!e && typeof e == "object" && "status" in e && !("pages" in e),
15
15
  signal: r
16
16
  });
17
17
  }
18
- setReader(e, n, r) {
19
- return t(this.http, "PUT", "/contents/{id}/versions/{version}/reader", {
18
+ async setReader(e, n, r) {
19
+ let i = this.http.captureChanges();
20
+ await t(this.http, "PUT", "/contents/{id}/versions/{version}/reader", {
20
21
  params: {
21
22
  id: e,
22
23
  version: n
23
24
  },
24
25
  body: r
26
+ }), i({
27
+ type: "reader.changed",
28
+ content: e,
29
+ version: n
25
30
  });
26
31
  }
27
32
  fork(e, n, r) {
@@ -1 +1 @@
1
- {"version":3,"file":"versions.js","names":[],"sources":["../../src/client/versions.ts"],"sourcesContent":["import { ContentKitError } from \"./errors.js\";\nimport type { ForkBody, Pending, ReaderMeta, ReaderPlan } from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, routeURL } from \"./route.js\";\n\n/** A version's answer while it is prepared (202). */\nexport const isPending = (x: unknown): x is Pending => !!x && typeof x === \"object\" && \"status\" in x && !(\"pages\" in x);\n\n/** Content versions: reader plans, reader metadata, forks and downloads. */\nexport class VersionsClient {\n constructor(private readonly http: Http) {}\n\n /** The version's reader plan for this viewer; Pending while it has no current publication. */\n reader(content: string, version: string, signal?: AbortSignal): Promise<ReaderPlan | Pending> {\n return call(this.http, \"GET\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, signal });\n }\n /** Editors: the version's anchors and audio cues. */\n setReader(content: string, version: string, meta: ReaderMeta): Promise<void> {\n return call(this.http, \"PUT\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, body: meta });\n }\n /** Editors: fills a version without media from a sibling, in a durable job. */\n fork(content: string, version: string, body: ForkBody): Promise<Pending> {\n return call(this.http, \"POST\", \"/contents/{id}/versions/{version}/fork\", { params: { id: content, version }, body });\n }\n /**\n * The version's pages as a zip, charged to the viewer's download quota;\n * Pending while it is prepared (ask again).\n */\n async download(content: string, version: string, signal?: AbortSignal): Promise<Blob | Pending> {\n const url = routeURL(this.http, \"GET\", \"/contents/{id}/versions/{version}/download\", { params: { id: content, version } });\n const out = await this.http.request<Blob>(url, { signal, blob: true });\n if (!out.type.startsWith(\"application/json\")) return out;\n const body: unknown = JSON.parse(await out.text());\n if (isPending(body)) return body;\n throw new ContentKitError(\"internal_error\", \"unexpected download answer\");\n }\n}\n"],"mappings":";;;AAMA,IAAa,KAAa,MAA6B,CAAC,CAAC,KAAK,OAAO,KAAM,YAAY,YAAY,KAAK,EAAE,WAAW,IAGxG,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,OAAO,GAAiB,GAAiB,GAAqD;EAC5F,OAAO,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAO,CAAC;CACxH;CAEA,UAAU,GAAiB,GAAiB,GAAiC;EAC3E,OAAO,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG,MAAM;EAAK,CAAC;CAC5H;CAEA,KAAK,GAAiB,GAAiB,GAAkC;EACvE,OAAO,EAAK,KAAK,MAAM,QAAQ,0CAA0C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAK,CAAC;CACrH;CAKA,MAAM,SAAS,GAAiB,GAAiB,GAA+C;EAC9F,IAAM,IAAM,EAAS,KAAK,MAAM,OAAO,8CAA8C,EAAE,QAAQ;GAAE,IAAI;GAAS;EAAQ,EAAE,CAAC,GACnH,IAAM,MAAM,KAAK,KAAK,QAAc,GAAK;GAAE;GAAQ,MAAM;EAAK,CAAC;EACrE,IAAI,CAAC,EAAI,KAAK,WAAW,kBAAkB,GAAG,OAAO;EACrD,IAAM,IAAgB,KAAK,MAAM,MAAM,EAAI,KAAK,CAAC;EACjD,IAAI,EAAU,CAAI,GAAG,OAAO;EAC5B,MAAM,IAAI,EAAgB,kBAAkB,4BAA4B;CAC1E;AACF"}
1
+ {"version":3,"file":"versions.js","names":[],"sources":["../../src/client/versions.ts"],"sourcesContent":["import { ContentKitError } from \"./errors.js\";\nimport type { ForkBody, Pending, ReaderMeta, ReaderPlan } from \"./generated/wire.js\";\nimport type { Http } from \"./http.js\";\nimport { call, routeURL } from \"./route.js\";\n\n/** A version's answer while it is prepared (202). */\nexport const isPending = (x: unknown): x is Pending => !!x && typeof x === \"object\" && \"status\" in x && !(\"pages\" in x);\n\n/** Content versions: reader plans, reader metadata, forks and downloads. */\nexport class VersionsClient {\n constructor(private readonly http: Http) {}\n\n /** The version's reader plan for this viewer; Pending while it has no current publication. */\n reader(content: string, version: string, signal?: AbortSignal): Promise<ReaderPlan | Pending> {\n return call(this.http, \"GET\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, signal });\n }\n /** Editors: replaces the version's reader metadata (anchors, audio cues, defaults). */\n async setReader(content: string, version: string, meta: ReaderMeta): Promise<void> {\n const emit = this.http.captureChanges();\n await call(this.http, \"PUT\", \"/contents/{id}/versions/{version}/reader\", { params: { id: content, version }, body: meta });\n emit({ type: \"reader.changed\", content, version });\n }\n /** Editors: fills a version without media from a sibling, in a durable job. */\n fork(content: string, version: string, body: ForkBody): Promise<Pending> {\n return call(this.http, \"POST\", \"/contents/{id}/versions/{version}/fork\", { params: { id: content, version }, body });\n }\n /**\n * The version's pages as a zip, charged to the viewer's download quota;\n * Pending while it is prepared (ask again).\n */\n async download(content: string, version: string, signal?: AbortSignal): Promise<Blob | Pending> {\n const url = routeURL(this.http, \"GET\", \"/contents/{id}/versions/{version}/download\", { params: { id: content, version } });\n const out = await this.http.request<Blob>(url, { signal, blob: true });\n if (!out.type.startsWith(\"application/json\")) return out;\n const body: unknown = JSON.parse(await out.text());\n if (isPending(body)) return body;\n throw new ContentKitError(\"internal_error\", \"unexpected download answer\");\n }\n}\n"],"mappings":";;;AAMA,IAAa,KAAa,MAA6B,CAAC,CAAC,KAAK,OAAO,KAAM,YAAY,YAAY,KAAK,EAAE,WAAW,IAGxG,IAAb,MAA4B;CACG;CAA7B,YAAY,GAA6B;EAAZ,KAAA,OAAA;CAAa;CAG1C,OAAO,GAAiB,GAAiB,GAAqD;EAC5F,OAAO,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAO,CAAC;CACxH;CAEA,MAAM,UAAU,GAAiB,GAAiB,GAAiC;EACjF,IAAM,IAAO,KAAK,KAAK,eAAe;EAEtC,AADA,MAAM,EAAK,KAAK,MAAM,OAAO,4CAA4C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG,MAAM;EAAK,CAAC,GACzH,EAAK;GAAE,MAAM;GAAkB;GAAS;EAAQ,CAAC;CACnD;CAEA,KAAK,GAAiB,GAAiB,GAAkC;EACvE,OAAO,EAAK,KAAK,MAAM,QAAQ,0CAA0C;GAAE,QAAQ;IAAE,IAAI;IAAS;GAAQ;GAAG;EAAK,CAAC;CACrH;CAKA,MAAM,SAAS,GAAiB,GAAiB,GAA+C;EAC9F,IAAM,IAAM,EAAS,KAAK,MAAM,OAAO,8CAA8C,EAAE,QAAQ;GAAE,IAAI;GAAS;EAAQ,EAAE,CAAC,GACnH,IAAM,MAAM,KAAK,KAAK,QAAc,GAAK;GAAE;GAAQ,MAAM;EAAK,CAAC;EACrE,IAAI,CAAC,EAAI,KAAK,WAAW,kBAAkB,GAAG,OAAO;EACrD,IAAM,IAAgB,KAAK,MAAM,MAAM,EAAI,KAAK,CAAC;EACjD,IAAI,EAAU,CAAI,GAAG,OAAO;EAC5B,MAAM,IAAI,EAAgB,kBAAkB,4BAA4B;CAC1E;AACF"}
package/dist/client.js CHANGED
@@ -10,31 +10,33 @@ import { sha256Hex as D } from "./client/media/hash.js";
10
10
  import { Pacer as O } from "./client/media/pacer.js";
11
11
  import { defaultTransport as k, fetchTransport as A, xhrTransport as j } from "./client/media/transport.js";
12
12
  import { MediaClient as M, backoff as N, samePath as P, stem as F } from "./client/media/client.js";
13
- import { acceptOf as I, defaultImage as L, filesFor as R, isPattern as z, ratioLabel as B, ruleDir as V, ruleFor as H, screenFiles as U, uniqueName as W, uploadRules as G, withMediaType as K } from "./client/media/rules.js";
14
- import { centeredCrop as q, constrainCrop as J, editOf as Y, editedSize as X, fromRotated as Z, rotation as Q, sameEdit as $, toOriginal as ee, toRotated as te } from "./client/crop.js";
15
- import { encodeRemaining as ne } from "./client/media/encode.js";
16
- import { ItemUploadQueue as re } from "./client/media/queue.js";
17
- import { decodeImage as ie, isAnimatedImage as ae } from "./client/image.js";
18
- import { READ_CHUNK as oe, chunkOffsets as se, isProcessing as ce, mergeReads as le, processing as ue } from "./client/media/windows.js";
19
- import { ABR_DEFAULTS as de, NETWORK_FAILURES_BEFORE_ERROR as fe, REFRESH_BEFORE_EXPIRY as pe, abrHlsConfig as me, capRung as he, classifyHlsError as ge, classifyMediaError as _e, expiryDelay as ve, hlsConfig as ye, initialEstimate as be, refreshable as xe, startRung as Se, statusKind as Ce } from "./client/playback.js";
20
- import { formatDuration as we, galleryItems as Te, isAudioType as Ee, isImageType as De, isSubtitleType as Oe, isVideoType as ke, stageAspect as Ae } from "./client/gallery.js";
21
- import { PLAYBACK_SPEEDS as je, PLAYER_TRACKS_KEY as Me, PLAYER_VOLUME_KEY as Ne, parseSpriteVtt as Pe, playerKeyAction as Fe, resumeAt as Ie } from "./client/player.js";
22
- import { reactionVerb as Le, sameTarget as Re } from "./client/content/types.js";
23
- import { CatalogClient as ze } from "./client/catalog.js";
24
- import { CodesClient as Be } from "./client/codes.js";
25
- import { BODY_SUFFIX as Ve, ContentsClient as He, IMAGE_SCHEME as Ue, imageRef as We, imageRefs as Ge } from "./client/contents.js";
26
- import { BansClient as Ke } from "./client/content/bans.js";
27
- import { CommentsClient as qe } from "./client/content/comments.js";
28
- import { Beacons as Je, eventId as Ye, readerSession as Xe } from "./client/beacons.js";
29
- import { DiscoveryClient as Ze } from "./client/discovery.js";
30
- import { FavoritesClient as Qe } from "./client/content/favorites.js";
31
- import { ModerationClient as $e } from "./client/content/moderation.js";
32
- import { PollsClient as et } from "./client/content/polls.js";
33
- import { ReactionsClient as tt } from "./client/content/reactions.js";
34
- import { GroupsClient as nt } from "./client/groups.js";
35
- import { OffersClient as rt } from "./client/offers.js";
36
- import { VersionsClient as it, isPending as at } from "./client/versions.js";
37
- import { NotificationsClient as ot } from "./client/notifications.js";
38
- import { createContentKitClient as st } from "./client/client.js";
39
- import { LETTERS as ct, attributeInput as lt, fold as ut, fromLocalInput as dt, humanize as ft, letterOf as pt, parseAttribute as mt, parseAttributes as ht, slugify as gt, toLocalInput as _t } from "./client/group-fields.js";
40
- export { de as ABR_DEFAULTS, Ve as BODY_SUFFIX, Ke as BansClient, Je as Beacons, i as CLIENT_ERROR_CODES, ze as CatalogClient, Be as CodesClient, qe as CommentsClient, a as ContentKitError, He as ContentsClient, e as DEFAULT_DENSITY, Ze as DiscoveryClient, o as ERROR_CODES, Qe as FavoritesClient, nt as GroupsClient, Ue as IMAGE_SCHEME, re as ItemUploadQueue, ct as LETTERS, w as MAX_PART_SIZE, T as MAX_SINGLE_PUT, E as MIN_PART_SIZE, C as MediaApi, M as MediaClient, $e as ModerationClient, fe as NETWORK_FAILURES_BEFORE_ERROR, ot as NotificationsClient, rt as OffersClient, je as PLAYBACK_SPEEDS, Me as PLAYER_TRACKS_KEY, Ne as PLAYER_VOLUME_KEY, O as Pacer, et as PollsClient, oe as READ_CHUNK, pe as REFRESH_BEFORE_EXPIRY, tt as ReactionsClient, it as VersionsClient, me as abrHlsConfig, I as acceptOf, h as aspectOf, lt as attributeInput, N as backoff, _ as call, he as capRung, q as centeredCrop, y as checkRef, se as chunkOffsets, ge as classifyHlsError, _e as classifyMediaError, J as constrainCrop, st as createContentKitClient, ie as decodeImage, L as defaultImage, k as defaultTransport, t as densityFor, Y as editOf, X as editedSize, ne as encodeRemaining, Ye as eventId, ve as expiryDelay, s as failureError, A as fetchTransport, R as filesFor, d as fill, ut as fold, we as formatDuration, dt as fromLocalInput, Z as fromRotated, Te as galleryItems, ye as hlsConfig, ft as humanize, We as imageRef, Ge as imageRefs, be as initialEstimate, ae as isAnimatedImage, Ee as isAudioType, b as isContentId, c as isContentKitError, De as isImageType, z as isPattern, at as isPending, ce as isProcessing, Oe as isSubtitleType, ke as isVideoType, x as itemKey, pt as letterOf, le as mergeReads, mt as parseAttribute, ht as parseAttributes, Pe as parseSpriteVtt, n as pickRendition, Fe as playerKeyAction, ue as processing, f as publicRenditions, p as publicURL, g as ratio, B as ratioLabel, Le as reactionVerb, l as readContentKitError, Xe as readerSession, xe as refreshable, Ie as resumeAt, Q as rotation, v as routeURL, V as ruleDir, H as ruleFor, $ as sameEdit, S as sameItem, P as samePath, Re as sameTarget, U as screenFiles, D as sha256Hex, gt as slugify, r as sortRenditions, m as srcSet, Ae as stageAspect, Se as startRung, Ce as statusKind, F as stem, u as toContentKitError, _t as toLocalInput, ee as toOriginal, te as toRotated, W as uniqueName, G as uploadRules, K as withMediaType, j as xhrTransport };
13
+ import { acceptOf as I, defaultImage as L, filesFor as R, isPattern as z, ratioLabel as B, ruleDir as V, ruleFor as H, screenFiles as U, stableImage as W, uniqueName as G, uploadRules as K, withMediaType as q } from "./client/media/rules.js";
14
+ import { centeredCrop as J, constrainCrop as Y, editOf as X, editedSize as Z, fromRotated as Q, rotation as $, sameEdit as ee, toOriginal as te, toRotated as ne } from "./client/crop.js";
15
+ import { encodeRemaining as re } from "./client/media/encode.js";
16
+ import { ItemUploadQueue as ie } from "./client/media/queue.js";
17
+ import { decodeImage as ae, isAnimatedImage as oe } from "./client/image.js";
18
+ import { reactionVerb as se, sameTarget as ce } from "./client/content/types.js";
19
+ import { READ_CHUNK as le, chunkOffsets as ue, isProcessing as de, mergeReads as fe, processing as pe } from "./client/media/windows.js";
20
+ import { ABR_DEFAULTS as me, NETWORK_FAILURES_BEFORE_ERROR as he, REFRESH_BEFORE_EXPIRY as ge, abrHlsConfig as _e, capRung as ve, classifyHlsError as ye, classifyMediaError as be, expiryDelay as xe, hlsConfig as Se, initialEstimate as Ce, refreshable as we, startRung as Te, statusKind as Ee } from "./client/playback.js";
21
+ import { formatDuration as De, galleryItems as Oe, isAudioType as ke, isImageType as Ae, isSubtitleType as je, isVideoType as Me, stageAspect as Ne } from "./client/gallery.js";
22
+ import { PLAYBACK_SPEEDS as Pe, PLAYER_TRACKS_KEY as Fe, PLAYER_VOLUME_KEY as Ie, parseSpriteVtt as Le, playerKeyAction as Re, resumeAt as ze } from "./client/player.js";
23
+ import { CatalogClient as Be } from "./client/catalog.js";
24
+ import { CodesClient as Ve } from "./client/codes.js";
25
+ import { BODY_SUFFIX as He, ContentsClient as Ue, IMAGE_SCHEME as We, imageRef as Ge, imageRefs as Ke } from "./client/contents.js";
26
+ import { BansClient as qe } from "./client/content/bans.js";
27
+ import { CommentsClient as Je } from "./client/content/comments.js";
28
+ import { Beacons as Ye, eventId as Xe, readerSession as Ze } from "./client/beacons.js";
29
+ import { DiscoveryClient as Qe } from "./client/discovery.js";
30
+ import { FavoritesClient as $e } from "./client/content/favorites.js";
31
+ import { ModerationClient as et } from "./client/content/moderation.js";
32
+ import { PollsClient as tt } from "./client/content/polls.js";
33
+ import { ReactionsClient as nt } from "./client/content/reactions.js";
34
+ import { GroupsClient as rt } from "./client/groups.js";
35
+ import { OffersClient as it } from "./client/offers.js";
36
+ import { StatsClient as at } from "./client/stats.js";
37
+ import { VersionsClient as ot, isPending as st } from "./client/versions.js";
38
+ import { NotificationsClient as ct } from "./client/notifications.js";
39
+ import { createContentKitClient as lt } from "./client/client.js";
40
+ import { LETTERS as ut, attributeInput as dt, fold as ft, fromLocalInput as pt, humanize as mt, letterOf as ht, parseAttribute as gt, parseAttributes as _t, slugify as vt, toLocalInput as yt } from "./client/group-fields.js";
41
+ import { emptyReaderMeta as bt, matchAudio as xt, readerMetaProblems as St, readerNames as Ct } from "./client/reader-meta.js";
42
+ export { me as ABR_DEFAULTS, He as BODY_SUFFIX, qe as BansClient, Ye as Beacons, i as CLIENT_ERROR_CODES, Be as CatalogClient, Ve as CodesClient, Je as CommentsClient, a as ContentKitError, Ue as ContentsClient, e as DEFAULT_DENSITY, Qe as DiscoveryClient, o as ERROR_CODES, $e as FavoritesClient, rt as GroupsClient, We as IMAGE_SCHEME, ie as ItemUploadQueue, ut as LETTERS, w as MAX_PART_SIZE, T as MAX_SINGLE_PUT, E as MIN_PART_SIZE, C as MediaApi, M as MediaClient, et as ModerationClient, he as NETWORK_FAILURES_BEFORE_ERROR, ct as NotificationsClient, it as OffersClient, Pe as PLAYBACK_SPEEDS, Fe as PLAYER_TRACKS_KEY, Ie as PLAYER_VOLUME_KEY, O as Pacer, tt as PollsClient, le as READ_CHUNK, ge as REFRESH_BEFORE_EXPIRY, nt as ReactionsClient, at as StatsClient, ot as VersionsClient, _e as abrHlsConfig, I as acceptOf, h as aspectOf, dt as attributeInput, N as backoff, _ as call, ve as capRung, J as centeredCrop, y as checkRef, ue as chunkOffsets, ye as classifyHlsError, be as classifyMediaError, Y as constrainCrop, lt as createContentKitClient, ae as decodeImage, L as defaultImage, k as defaultTransport, t as densityFor, X as editOf, Z as editedSize, bt as emptyReaderMeta, re as encodeRemaining, Xe as eventId, xe as expiryDelay, s as failureError, A as fetchTransport, R as filesFor, d as fill, ft as fold, De as formatDuration, pt as fromLocalInput, Q as fromRotated, Oe as galleryItems, Se as hlsConfig, mt as humanize, Ge as imageRef, Ke as imageRefs, Ce as initialEstimate, oe as isAnimatedImage, ke as isAudioType, b as isContentId, c as isContentKitError, Ae as isImageType, z as isPattern, st as isPending, de as isProcessing, je as isSubtitleType, Me as isVideoType, x as itemKey, ht as letterOf, xt as matchAudio, fe as mergeReads, gt as parseAttribute, _t as parseAttributes, Le as parseSpriteVtt, n as pickRendition, Re as playerKeyAction, pe as processing, f as publicRenditions, p as publicURL, g as ratio, B as ratioLabel, se as reactionVerb, l as readContentKitError, St as readerMetaProblems, Ct as readerNames, Ze as readerSession, we as refreshable, ze as resumeAt, $ as rotation, v as routeURL, V as ruleDir, H as ruleFor, ee as sameEdit, S as sameItem, P as samePath, ce as sameTarget, U as screenFiles, D as sha256Hex, vt as slugify, r as sortRenditions, m as srcSet, W as stableImage, Ne as stageAspect, Te as startRung, Ee as statusKind, F as stem, u as toContentKitError, yt as toLocalInput, te as toOriginal, ne as toRotated, G as uniqueName, K as uploadRules, q as withMediaType, j as xhrTransport };