@geekity/cms 0.34.0 → 0.35.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 (192) hide show
  1. package/admin/pages/documents/chooser.njk +30 -0
  2. package/admin/pages/documents/editor.njk +394 -164
  3. package/admin/pages/media/file.njk +142 -0
  4. package/admin/pages/media/library.njk +103 -198
  5. package/admin/pages/settings/general.njk +48 -1
  6. package/admin/pages/settings/privacy.njk +1 -1
  7. package/admin/pages/tools/maintenance.njk +48 -0
  8. package/admin/src/admin.css +1 -0
  9. package/admin/static/admin.css +728 -65
  10. package/admin/static/copy.js +30 -23
  11. package/admin/static/item-lookup.js +179 -0
  12. package/admin/static/media.js +82 -0
  13. package/dist/admin/documents.d.ts.map +1 -1
  14. package/dist/admin/documents.js +141 -22
  15. package/dist/admin/documents.js.map +1 -1
  16. package/dist/admin/editor-actions.d.ts.map +1 -1
  17. package/dist/admin/editor-actions.js +2 -3
  18. package/dist/admin/editor-actions.js.map +1 -1
  19. package/dist/admin/editor-fields.d.ts +20 -8
  20. package/dist/admin/editor-fields.d.ts.map +1 -1
  21. package/dist/admin/editor-fields.js +35 -12
  22. package/dist/admin/editor-fields.js.map +1 -1
  23. package/dist/admin/editor-kinds.d.ts +30 -0
  24. package/dist/admin/editor-kinds.d.ts.map +1 -0
  25. package/dist/admin/editor-kinds.js +136 -0
  26. package/dist/admin/editor-kinds.js.map +1 -0
  27. package/dist/admin/editor-layout.d.ts +10 -1
  28. package/dist/admin/editor-layout.d.ts.map +1 -1
  29. package/dist/admin/editor-layout.js +38 -31
  30. package/dist/admin/editor-layout.js.map +1 -1
  31. package/dist/admin/federation.d.ts +1 -1
  32. package/dist/admin/federation.d.ts.map +1 -1
  33. package/dist/admin/federation.js +2 -1
  34. package/dist/admin/federation.js.map +1 -1
  35. package/dist/admin/formatting.d.ts +2 -0
  36. package/dist/admin/formatting.d.ts.map +1 -1
  37. package/dist/admin/formatting.js +8 -0
  38. package/dist/admin/formatting.js.map +1 -1
  39. package/dist/admin/index.d.ts +1 -1
  40. package/dist/admin/index.d.ts.map +1 -1
  41. package/dist/admin/index.js +1 -1
  42. package/dist/admin/index.js.map +1 -1
  43. package/dist/admin/item-lookup.d.ts +16 -0
  44. package/dist/admin/item-lookup.d.ts.map +1 -0
  45. package/dist/admin/item-lookup.js +192 -0
  46. package/dist/admin/item-lookup.js.map +1 -0
  47. package/dist/admin/maintenance.d.ts +2 -0
  48. package/dist/admin/maintenance.d.ts.map +1 -1
  49. package/dist/admin/maintenance.js +14 -0
  50. package/dist/admin/maintenance.js.map +1 -1
  51. package/dist/admin/media.d.ts +43 -29
  52. package/dist/admin/media.d.ts.map +1 -1
  53. package/dist/admin/media.js +221 -54
  54. package/dist/admin/media.js.map +1 -1
  55. package/dist/admin/preview.d.ts.map +1 -1
  56. package/dist/admin/preview.js +9 -12
  57. package/dist/admin/preview.js.map +1 -1
  58. package/dist/admin/routes.d.ts.map +1 -1
  59. package/dist/admin/routes.js +3 -0
  60. package/dist/admin/routes.js.map +1 -1
  61. package/dist/admin/settings-general.d.ts +6 -0
  62. package/dist/admin/settings-general.d.ts.map +1 -1
  63. package/dist/admin/settings-general.js +46 -1
  64. package/dist/admin/settings-general.js.map +1 -1
  65. package/dist/admin/templates.d.ts +2 -0
  66. package/dist/admin/templates.d.ts.map +1 -1
  67. package/dist/admin/templates.js +4 -0
  68. package/dist/admin/templates.js.map +1 -1
  69. package/dist/cli.d.ts.map +1 -1
  70. package/dist/cli.js +2 -1
  71. package/dist/cli.js.map +1 -1
  72. package/dist/cms/app.d.ts.map +1 -1
  73. package/dist/cms/app.js +1 -0
  74. package/dist/cms/app.js.map +1 -1
  75. package/dist/cms/services.d.ts +2 -0
  76. package/dist/cms/services.d.ts.map +1 -1
  77. package/dist/cms/services.js +13 -0
  78. package/dist/cms/services.js.map +1 -1
  79. package/dist/content/document-form.d.ts +5 -13
  80. package/dist/content/document-form.d.ts.map +1 -1
  81. package/dist/content/document-form.js +2 -3
  82. package/dist/content/document-form.js.map +1 -1
  83. package/dist/content/excerpt.d.ts +1 -3
  84. package/dist/content/excerpt.d.ts.map +1 -1
  85. package/dist/content/excerpt.js +2 -7
  86. package/dist/content/excerpt.js.map +1 -1
  87. package/dist/content/post-fields.d.ts +1 -1
  88. package/dist/content/post-fields.d.ts.map +1 -1
  89. package/dist/content/post-fields.js +30 -35
  90. package/dist/content/post-fields.js.map +1 -1
  91. package/dist/content/post-type.d.ts +2 -3
  92. package/dist/content/post-type.d.ts.map +1 -1
  93. package/dist/content/post-type.js +8 -9
  94. package/dist/content/post-type.js.map +1 -1
  95. package/dist/content/review-form.d.ts +49 -0
  96. package/dist/content/review-form.d.ts.map +1 -0
  97. package/dist/content/review-form.js +282 -0
  98. package/dist/content/review-form.js.map +1 -0
  99. package/dist/content/review.d.ts +57 -0
  100. package/dist/content/review.d.ts.map +1 -0
  101. package/dist/content/review.js +264 -0
  102. package/dist/content/review.js.map +1 -0
  103. package/dist/content/write.d.ts.map +1 -1
  104. package/dist/content/write.js +6 -9
  105. package/dist/content/write.js.map +1 -1
  106. package/dist/exports.d.ts +1 -1
  107. package/dist/exports.d.ts.map +1 -1
  108. package/dist/federation/article.d.ts.map +1 -1
  109. package/dist/federation/article.js +27 -29
  110. package/dist/federation/article.js.map +1 -1
  111. package/dist/federation/federation.d.ts +3 -0
  112. package/dist/federation/federation.d.ts.map +1 -1
  113. package/dist/federation/federation.js +5 -1
  114. package/dist/federation/federation.js.map +1 -1
  115. package/dist/federation/refusals.d.ts +18 -0
  116. package/dist/federation/refusals.d.ts.map +1 -0
  117. package/dist/federation/refusals.js +84 -0
  118. package/dist/federation/refusals.js.map +1 -0
  119. package/dist/images/variants.d.ts +1 -0
  120. package/dist/images/variants.d.ts.map +1 -1
  121. package/dist/images/variants.js +10 -0
  122. package/dist/images/variants.js.map +1 -1
  123. package/dist/item-lookup/item-id.d.ts +33 -0
  124. package/dist/item-lookup/item-id.d.ts.map +1 -0
  125. package/dist/item-lookup/item-id.js +71 -0
  126. package/dist/item-lookup/item-id.js.map +1 -0
  127. package/dist/item-lookup/lookup.d.ts +58 -0
  128. package/dist/item-lookup/lookup.d.ts.map +1 -0
  129. package/dist/item-lookup/lookup.js +532 -0
  130. package/dist/item-lookup/lookup.js.map +1 -0
  131. package/dist/micropub/create.d.ts +1 -1
  132. package/dist/micropub/create.d.ts.map +1 -1
  133. package/dist/micropub/create.js +4 -2
  134. package/dist/micropub/create.js.map +1 -1
  135. package/dist/micropub/endpoint.d.ts.map +1 -1
  136. package/dist/micropub/endpoint.js +3 -10
  137. package/dist/micropub/endpoint.js.map +1 -1
  138. package/dist/micropub/update.d.ts.map +1 -1
  139. package/dist/micropub/update.js +12 -6
  140. package/dist/micropub/update.js.map +1 -1
  141. package/dist/plugin.d.ts +26 -2
  142. package/dist/plugin.d.ts.map +1 -1
  143. package/dist/plugin.js.map +1 -1
  144. package/dist/settings/names.d.ts +1 -1
  145. package/dist/settings/names.d.ts.map +1 -1
  146. package/dist/settings/names.js +8 -1
  147. package/dist/settings/names.js.map +1 -1
  148. package/dist/settings/registry.d.ts.map +1 -1
  149. package/dist/settings/registry.js +2 -1
  150. package/dist/settings/registry.js.map +1 -1
  151. package/dist/settings/tmdb.d.ts +5 -0
  152. package/dist/settings/tmdb.d.ts.map +1 -0
  153. package/dist/settings/tmdb.js +22 -0
  154. package/dist/settings/tmdb.js.map +1 -0
  155. package/dist/store/profiles.d.ts +3 -2
  156. package/dist/store/profiles.d.ts.map +1 -1
  157. package/dist/store/profiles.js +2 -4
  158. package/dist/store/profiles.js.map +1 -1
  159. package/dist/web/context.d.ts +9 -21
  160. package/dist/web/context.d.ts.map +1 -1
  161. package/dist/web/context.js +20 -20
  162. package/dist/web/context.js.map +1 -1
  163. package/dist/web/feed-item.d.ts +6 -6
  164. package/dist/web/feed-item.d.ts.map +1 -1
  165. package/dist/web/feed-item.js +7 -3
  166. package/dist/web/feed-item.js.map +1 -1
  167. package/dist/web/plugin-federation.d.ts +2 -0
  168. package/dist/web/plugin-federation.d.ts.map +1 -1
  169. package/dist/web/plugin-federation.js +3 -0
  170. package/dist/web/plugin-federation.js.map +1 -1
  171. package/dist/webmention/cited-picture.d.ts +1 -0
  172. package/dist/webmention/cited-picture.d.ts.map +1 -1
  173. package/dist/webmention/cited-picture.js +7 -4
  174. package/dist/webmention/cited-picture.js.map +1 -1
  175. package/dist/webmention/fetch-public.d.ts +3 -0
  176. package/dist/webmention/fetch-public.d.ts.map +1 -1
  177. package/dist/webmention/fetch-public.js +1 -1
  178. package/dist/webmention/fetch-public.js.map +1 -1
  179. package/dist/webmention/reply-context/json-ld.d.ts +8 -0
  180. package/dist/webmention/reply-context/json-ld.d.ts.map +1 -1
  181. package/dist/webmention/reply-context/json-ld.js +21 -0
  182. package/dist/webmention/reply-context/json-ld.js.map +1 -1
  183. package/dist/webmention/service.d.ts.map +1 -1
  184. package/dist/webmention/service.js +3 -5
  185. package/dist/webmention/service.js.map +1 -1
  186. package/package.json +1 -1
  187. package/themes/default/layouts/post.njk +6 -5
  188. package/themes/default/partials/post-list.njk +15 -20
  189. package/themes/default/partials/review.njk +1 -0
  190. package/themes/default/src/style.css +24 -4
  191. package/themes/default/static/style.css +33 -5
  192. package/themes/default/partials/read.njk +0 -5
@@ -1,16 +1,22 @@
1
+ import { STRIP_METADATA_JOB } from '../content/metadata/strip-job.js';
2
+ import { finishedResult } from '../jobs/types.js';
1
3
  import { overridden, sourceText } from '../settings/registry.js';
2
4
  import { MAINTENANCE } from '../settings/maintenance.js';
3
5
  import { stringOrEmpty } from '../util/forms.js';
4
6
  import { flash } from './flash.js';
7
+ import { jobStatusView, queueJobFromForm } from './job-status.js';
5
8
  import { ADMIN_TEMPLATES } from './templates.js';
6
9
  import { TOOLS_PATH, TOOLS_SECTION } from './tools.js';
7
10
  export const MAINTENANCE_PATH = `${TOOLS_PATH}/maintenance`;
8
11
  export const MAINTENANCE_CHILD = 'maintenance';
12
+ export const STRIP_METADATA_PATH = `${MAINTENANCE_PATH}/strip-metadata`;
13
+ export const STRIP_METADATA_PANEL = `${MAINTENANCE_PATH}#strip-metadata`;
9
14
  export function mountMaintenanceScreen(app, options) {
10
15
  async function show(c, problems = {}) {
11
16
  const { settings } = c.var;
12
17
  const on = await settings.resolve(MAINTENANCE.on);
13
18
  const until = await settings.get(MAINTENANCE.until);
19
+ const latestSweep = await c.var.jobs.latest(STRIP_METADATA_JOB.name);
14
20
  return options.render(c, ADMIN_TEMPLATES.toolsMaintenance, {
15
21
  section: TOOLS_SECTION,
16
22
  child: MAINTENANCE_CHILD,
@@ -22,9 +28,17 @@ export function mountMaintenanceScreen(app, options) {
22
28
  : undefined,
23
29
  until: MAINTENANCE.until.declaration.type.toText(until),
24
30
  problems,
31
+ stripMetadataUrl: STRIP_METADATA_PATH,
32
+ sweep: jobStatusView(latestSweep),
33
+ sweepReport: finishedResult(STRIP_METADATA_JOB, latestSweep),
25
34
  });
26
35
  }
27
36
  app.get(MAINTENANCE_PATH, (c) => show(c));
37
+ app.post(STRIP_METADATA_PATH, (c) => queueJobFromForm(c, STRIP_METADATA_JOB, null, {
38
+ panel: STRIP_METADATA_PANEL,
39
+ queued: 'Queued the sweep. It runs in the background; its progress is below.',
40
+ busy: 'A sweep is already queued or running. Nothing new was queued.',
41
+ }));
28
42
  app.post(MAINTENANCE_PATH, async (c) => {
29
43
  const body = await c.req.parseBody();
30
44
  const result = await c.var.settings.set('maintenance', new Map([
@@ -1 +1 @@
1
- {"version":3,"file":"maintenance.js","sourceRoot":"","sources":["../../src/admin/maintenance.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,UAAU,cAAc,CAAC;AAE5D,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAC;AAE/C,MAAM,UAAU,sBAAsB,CACpC,GAAqB,EACrB,OAAgC;IAEhC,KAAK,UAAU,IAAI,CACjB,CAAsB,EACtB,QAAQ,GAAqC,EAAE;QAE/C,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC;QAC3B,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QAClD,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACpD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,eAAe,CAAC,gBAAgB,EAAE;YACzD,OAAO,EAAE,aAAa;YACtB,KAAK,EAAE,iBAAiB;YACxB,OAAO,EAAE,aAAa;YACtB,OAAO,EAAE,gBAAgB;YACzB,EAAE,EAAE,EAAE,CAAC,KAAK;YACZ,YAAY,EAAE,UAAU,CAAC,EAAE,CAAC,MAAM,CAAC;gBACjC,CAAC,CAAC,4BAA4B,UAAU,CAAC,EAAE,CAAC,MAAM,CAAC,8BAA8B;gBACjF,CAAC,CAAC,SAAS;YACb,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACvD,QAAQ;SACT,CAAC,CAAC;IACL,CAAC;IAED,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAE1C,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CACrC,aAAa,EACb,IAAI,GAAG,CAAkB;YACvB,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;YAChC,CAAC,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SAC/C,CAAC,EACF,OAAO,CACR,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,OAAO,MAAM,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxC,CAAC;QACD,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;QACzC,OAAO,CAAC,CAAC,QAAQ,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"maintenance.js","sourceRoot":"","sources":["../../src/admin/maintenance.ts"],"names":[],"mappings":"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"}
@@ -1,32 +1,33 @@
1
1
  /**
2
- * The media screen: what is under `content/uploads/`, what links to it, and
3
- * the things that can be done to it — add one, describe one, take one away,
4
- * and queue the sweep that strips metadata from the ones uploaded before
5
- * uploads were stripped on arrival.
2
+ * The media screen: what is under `content/uploads/` as a grid of tiles, a
3
+ * details screen per file with what links to it, and the things that can be
4
+ * done to one — add one, describe one, take one away.
6
5
  */
7
6
  import type { Hono } from 'hono';
8
7
  import type { CatalogReader } from '../content/store.ts';
9
8
  import type { GeekityEnv } from '../web/env.ts';
9
+ import type { UploadKind } from '../files/upload-types.ts';
10
10
  import type { RemoveDerived } from '../media/library.ts';
11
11
  import type { AdminRender } from './documents.ts';
12
12
  /** Where the media screen lives. */
13
13
  export declare const MEDIA_PATH = "/admin/media";
14
14
  /** The navigation section the screen marks as current. */
15
15
  export declare const MEDIA_SECTION = "media";
16
+ /** Where one file's details screen lives, with the file as `?path=`. */
17
+ export declare const MEDIA_FILE_PATH = "/admin/media/file";
16
18
  /** Where the screen's upload form posts. */
17
19
  export declare const MEDIA_UPLOAD_PATH = "/admin/media/upload";
18
- /** Where a row's Delete button posts. */
20
+ /** Where a file's Delete button posts. */
19
21
  export declare const MEDIA_DELETE_PATH = "/admin/media/delete";
20
- /** Where a row's alt-text form posts. */
22
+ /** Where a file's alt-text form posts. */
21
23
  export declare const MEDIA_ALT_PATH = "/admin/media/alt";
22
- export declare const MEDIA_STRIP_METADATA_PATH = "/admin/media/strip-metadata";
23
24
  /** How many files one page of the library shows. */
24
25
  export declare const MEDIA_PER_PAGE = 24;
25
- /** The fields the forms on the media screen submit. */
26
+ /** The fields the forms on the media screens submit. */
26
27
  export declare const MEDIA_FIELDS: {
27
28
  /** The file being added, on the upload form. */
28
29
  readonly file: 'file';
29
- /** The upload being deleted, as a path under `content/uploads/`. */
30
+ /** The upload being deleted or described, as a path under `content/uploads/`. */
30
31
  readonly path: 'path';
31
32
  /** Set once the admin has been shown what still references the file. */
32
33
  readonly confirm: 'confirm';
@@ -34,10 +35,36 @@ export declare const MEDIA_FIELDS: {
34
35
  readonly alt: 'alt';
35
36
  /** Present when an image is decorative, which wins over any alt text. */
36
37
  readonly decorative: 'decorative';
37
- /** The library page an alt-text form was on, so saving it goes back there. */
38
- readonly page: 'page';
38
+ /** The library view a form was posted from, as a query string, so the answer goes back to it. */
39
+ readonly from: 'from';
39
40
  };
40
- /** One document that mentions an upload, as the confirmation names it. */
41
+ /** The orders the library can be sorted in. Newest is the default and is never written into a URL. */
42
+ declare const SORTS: {
43
+ readonly newest: 'Newest first';
44
+ readonly oldest: 'Oldest first';
45
+ readonly largest: 'Largest first';
46
+ readonly name: 'Name';
47
+ };
48
+ type MediaSort = keyof typeof SORTS;
49
+ /** One view of the library: what the tabs, the filter form and the page links say. */
50
+ export interface MediaView {
51
+ kind: UploadKind | undefined;
52
+ /** Matched against the path and the alt text, ignoring case. Empty matches everything. */
53
+ search: string;
54
+ needsAlt: boolean;
55
+ unused: boolean;
56
+ sort: MediaSort;
57
+ page: number;
58
+ }
59
+ /** A view from a query string. Anything it does not recognise is left at its default. */
60
+ export declare function parseMediaView(query: URLSearchParams): MediaView;
61
+ /** A view as a query string, without the `?`, leaving out everything at its default. */
62
+ export declare function mediaViewQuery(view: MediaView): string;
63
+ /** The library showing a view. */
64
+ export declare function mediaLibraryUrl(view: MediaView): string;
65
+ /** One file's details screen, reached from a view of the library. */
66
+ export declare function mediaFileUrl(path: string, view: MediaView): string;
67
+ /** One document that mentions an upload, as the screens name it. */
41
68
  export interface MediaReference {
42
69
  /** Path relative to the content directory: what identifies the document. */
43
70
  path: string;
@@ -48,20 +75,7 @@ export interface MediaReference {
48
75
  /** Whether it is in the trash rather than the live tree. */
49
76
  trashed: boolean;
50
77
  }
51
- /**
52
- * Every document whose body mentions an upload's URL, trash included.
53
- *
54
- * The trash counts. A trashed post can be restored tomorrow, and restoring one
55
- * whose picture was deleted in the meantime is a broken post nobody was warned
56
- * about — so a file the trash still points at asks for confirmation exactly as
57
- * a published one does.
58
- *
59
- * This is the one thing on the screen that comes out of the index rather than
60
- * off the disk. The index is a cache of the files (decision-1), so it may be a
61
- * moment behind a hand edit; that is acceptable here because the answer is a
62
- * warning rather than a decision, and the confirmation step is what makes an
63
- * out-of-date answer harmless in both directions.
64
- */
78
+ /** Every document whose body mentions an upload's URL, trash included. */
65
79
  export declare function referencesTo(store: CatalogReader, url: string): MediaReference[];
66
80
  /** What {@link mountMediaScreen} needs from the admin around it. */
67
81
  export interface MountMediaScreenOptions {
@@ -71,13 +85,13 @@ export interface MountMediaScreenOptions {
71
85
  removeDerived?: RemoveDerived | undefined;
72
86
  }
73
87
  /**
74
- * Register the media screen: the library, the upload form and the delete.
88
+ * Register the media screens: the library, one file's details, the upload,
89
+ * the alt text and the delete.
75
90
  *
76
91
  * Uploading here is {@link storeUpload}, exactly as the editor's control and
77
92
  * the avatar are, so what a site accepts is one answer given in one place
78
93
  * rather than three screens that might disagree.
79
94
  */
80
95
  export declare function mountMediaScreen(app: Hono<GeekityEnv>, options: MountMediaScreenOptions): void;
81
- /** The URL of one page of the library. Page one is the screen's own URL. */
82
- export declare function mediaPageUrl(page: number): string;
96
+ export {};
83
97
  //# sourceMappingURL=media.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/admin/media.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAI1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAOhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAMzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAMlD,oCAAoC;AACpC,eAAO,MAAM,UAAU,iBAA0B,CAAC;AAElD,0DAA0D;AAC1D,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,wBAAyB,CAAC;AAExD,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,wBAAyB,CAAC;AAExD,yCAAyC;AACzC,eAAO,MAAM,cAAc,qBAAsB,CAAC;AAElD,eAAO,MAAM,yBAAyB,gCAAiC,CAAC;AAExE,oDAAoD;AACpD,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,uDAAuD;AACvD,eAAO,MAAM,YAAY;IACvB,gDAAgD;aAChD,IAAI,EAAE,MAAM;IACZ,oEAAoE;aACpE,IAAI,EAAE,MAAM;IACZ,wEAAwE;aACxE,OAAO,EAAE,SAAS;IAClB,2BAA2B;aAC3B,GAAG,EAAE,KAAK;IACV,yEAAyE;aACzE,UAAU,EAAE,YAAY;IACxB,8EAA8E;aAC9E,IAAI,EAAE,MAAM;CACJ,CAAC;AAEX,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,cAAc,EAAE,CAYhF;AAED,oEAAoE;AACpE,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,MAAM,EAAE,WAAW,CAAC;IACpB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAC3C;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAkI9F;AASD,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD"}
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/admin/media.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,14 @@
1
1
  /**
2
- * The media screen: what is under `content/uploads/`, what links to it, and
3
- * the things that can be done to it — add one, describe one, take one away,
4
- * and queue the sweep that strips metadata from the ones uploaded before
5
- * uploads were stripped on arrival.
2
+ * The media screen: what is under `content/uploads/` as a grid of tiles, a
3
+ * details screen per file with what links to it, and the things that can be
4
+ * done to one — add one, describe one, take one away.
6
5
  */
7
6
  import { postLabel } from '../content/post-type.js';
8
7
  import { isTrashedPath } from '../content/store.js';
9
8
  import { UPLOAD_ASSET_PREFIX } from '../content/uploads.js';
10
9
  import { writeAltText } from '../content/alt-text.js';
11
10
  import { STRIP_METADATA_JOB } from '../content/metadata/strip-job.js';
12
- import { finishedResult } from '../jobs/types.js';
13
- import { removeImageVariants } from '../images/variants.js';
11
+ import { describeImage, removeImageVariants, thumbnailUrl } from '../images/variants.js';
14
12
  import { deleteUpload, listUploads, mentionsUpload, resolveUpload } from '../media/library.js';
15
13
  import { refusedUpload, storeUpload } from '../media/uploads.js';
16
14
  import { isFile } from '../util/files.js';
@@ -18,27 +16,29 @@ import { stringOrEmpty } from '../util/forms.js';
18
16
  import { pageCount } from '../web/pagination.js';
19
17
  import { documentEditorPath, PAGE_SCREENS, POST_SCREENS } from './documents.js';
20
18
  import { flash } from './flash.js';
21
- import { jobStatusView, queueJobFromForm } from './job-status.js';
19
+ import { jobStatusView } from './job-status.js';
20
+ import { STRIP_METADATA_PANEL, STRIP_METADATA_PATH } from './maintenance.js';
22
21
  import { ADMIN_PREFIX } from '../site/admin-paths.js';
23
22
  import { ADMIN_TEMPLATES } from './templates.js';
24
23
  /** Where the media screen lives. */
25
24
  export const MEDIA_PATH = `${ADMIN_PREFIX}/media`;
26
25
  /** The navigation section the screen marks as current. */
27
26
  export const MEDIA_SECTION = 'media';
27
+ /** Where one file's details screen lives, with the file as `?path=`. */
28
+ export const MEDIA_FILE_PATH = `${MEDIA_PATH}/file`;
28
29
  /** Where the screen's upload form posts. */
29
30
  export const MEDIA_UPLOAD_PATH = `${MEDIA_PATH}/upload`;
30
- /** Where a row's Delete button posts. */
31
+ /** Where a file's Delete button posts. */
31
32
  export const MEDIA_DELETE_PATH = `${MEDIA_PATH}/delete`;
32
- /** Where a row's alt-text form posts. */
33
+ /** Where a file's alt-text form posts. */
33
34
  export const MEDIA_ALT_PATH = `${MEDIA_PATH}/alt`;
34
- export const MEDIA_STRIP_METADATA_PATH = `${MEDIA_PATH}/strip-metadata`;
35
35
  /** How many files one page of the library shows. */
36
36
  export const MEDIA_PER_PAGE = 24;
37
- /** The fields the forms on the media screen submit. */
37
+ /** The fields the forms on the media screens submit. */
38
38
  export const MEDIA_FIELDS = {
39
39
  /** The file being added, on the upload form. */
40
40
  file: 'file',
41
- /** The upload being deleted, as a path under `content/uploads/`. */
41
+ /** The upload being deleted or described, as a path under `content/uploads/`. */
42
42
  path: 'path',
43
43
  /** Set once the admin has been shown what still references the file. */
44
44
  confirm: 'confirm',
@@ -46,28 +46,86 @@ export const MEDIA_FIELDS = {
46
46
  alt: 'alt',
47
47
  /** Present when an image is decorative, which wins over any alt text. */
48
48
  decorative: 'decorative',
49
- /** The library page an alt-text form was on, so saving it goes back there. */
50
- page: 'page',
49
+ /** The library view a form was posted from, as a query string, so the answer goes back to it. */
50
+ from: 'from',
51
51
  };
52
+ /** The kinds the library's tabs pick between. */
53
+ const KIND_TABS = [
54
+ { kind: undefined, label: 'All' },
55
+ { kind: 'image', label: 'Images' },
56
+ { kind: 'audio', label: 'Audio' },
57
+ { kind: 'video', label: 'Video' },
58
+ { kind: 'document', label: 'Documents' },
59
+ ];
60
+ /** The orders the library can be sorted in. Newest is the default and is never written into a URL. */
61
+ const SORTS = {
62
+ newest: 'Newest first',
63
+ oldest: 'Oldest first',
64
+ largest: 'Largest first',
65
+ name: 'Name',
66
+ };
67
+ /** A view from a query string. Anything it does not recognise is left at its default. */
68
+ export function parseMediaView(query) {
69
+ const kind = query.get('kind');
70
+ const sort = query.get('sort');
71
+ return {
72
+ kind: KIND_TABS.find((tab) => tab.kind !== undefined && tab.kind === kind)?.kind,
73
+ search: (query.get('q') ?? '').trim(),
74
+ needsAlt: query.get('alt') === 'missing',
75
+ unused: query.get('unused') === '1',
76
+ sort: sort !== null && Object.hasOwn(SORTS, sort) ? sort : 'newest',
77
+ page: pageNumber(query.get('page') ?? undefined),
78
+ };
79
+ }
80
+ /** A view as a query string, without the `?`, leaving out everything at its default. */
81
+ export function mediaViewQuery(view) {
82
+ const query = new URLSearchParams();
83
+ if (view.kind !== undefined)
84
+ query.set('kind', view.kind);
85
+ if (view.search !== '')
86
+ query.set('q', view.search);
87
+ if (view.needsAlt)
88
+ query.set('alt', 'missing');
89
+ if (view.unused)
90
+ query.set('unused', '1');
91
+ if (view.sort !== 'newest')
92
+ query.set('sort', view.sort);
93
+ if (view.page > 1)
94
+ query.set('page', String(view.page));
95
+ return query.toString();
96
+ }
97
+ /** The library showing a view. */
98
+ export function mediaLibraryUrl(view) {
99
+ const query = mediaViewQuery(view);
100
+ return query === '' ? MEDIA_PATH : `${MEDIA_PATH}?${query}`;
101
+ }
102
+ /** One file's details screen, reached from a view of the library. */
103
+ export function mediaFileUrl(path, view) {
104
+ const query = mediaViewQuery(view);
105
+ return `${MEDIA_FILE_PATH}?path=${encodeURIComponent(path)}${query === '' ? '' : `&${query}`}`;
106
+ }
52
107
  /**
53
- * Every document whose body mentions an upload's URL, trash included.
108
+ * Every document, live and trashed, that a reference could come from.
54
109
  *
55
110
  * The trash counts. A trashed post can be restored tomorrow, and restoring one
56
111
  * whose picture was deleted in the meantime is a broken post nobody was warned
57
112
  * about — so a file the trash still points at asks for confirmation exactly as
58
113
  * a published one does.
59
114
  *
60
- * This is the one thing on the screen that comes out of the index rather than
115
+ * This is the one thing on the screens that comes out of the index rather than
61
116
  * off the disk. The index is a cache of the files (decision-1), so it may be a
62
117
  * moment behind a hand edit; that is acceptable here because the answer is a
63
118
  * warning rather than a decision, and the confirmation step is what makes an
64
119
  * out-of-date answer harmless in both directions.
65
120
  */
66
- export function referencesTo(store, url) {
121
+ function referenceSources(store) {
67
122
  // Two queries rather than one because `listAll` shows the live tree or the
68
123
  // trash and never both, which is the same reason the taxonomy screens ask
69
124
  // twice.
70
- return [...store.listAll({ trashed: false }), ...store.listAll({ trashed: true })]
125
+ return [...store.listAll({ trashed: false }), ...store.listAll({ trashed: true })];
126
+ }
127
+ function referencesAmong(documents, url) {
128
+ return documents
71
129
  .filter((document) => mentionsUpload(document.body, url))
72
130
  .map((document) => ({
73
131
  path: document.path,
@@ -76,8 +134,51 @@ export function referencesTo(store, url) {
76
134
  trashed: isTrashedPath(document.path),
77
135
  }));
78
136
  }
137
+ /** Every document whose body mentions an upload's URL, trash included. */
138
+ export function referencesTo(store, url) {
139
+ return referencesAmong(referenceSources(store), url);
140
+ }
141
+ function libraryFile(config, documents, file) {
142
+ const image = file.kind === 'image' ? describeImage(config, file.path) : undefined;
143
+ return {
144
+ ...file,
145
+ references: referencesAmong(documents, file.url),
146
+ thumbnailUrl: file.kind === 'image' ? thumbnailUrl(config, file.path, file.url) : file.url,
147
+ width: image?.width,
148
+ height: image?.height,
149
+ };
150
+ }
151
+ function needsAlt(file) {
152
+ return file.kind === 'image' && file.alt === undefined;
153
+ }
154
+ /** Whether a file is in a view, leaving the kind tabs and the page aside. */
155
+ function matches(file, view) {
156
+ if (view.needsAlt && !needsAlt(file))
157
+ return false;
158
+ if (view.unused && file.references.length > 0)
159
+ return false;
160
+ if (view.search === '')
161
+ return true;
162
+ const search = view.search.toLowerCase();
163
+ const alt = file.alt?.kind === 'described' ? file.alt.text : '';
164
+ return file.path.toLowerCase().includes(search) || alt.toLowerCase().includes(search);
165
+ }
166
+ /** `listUploads` is newest first already; the other orders are stable sorts of it. */
167
+ function sorted(files, sort) {
168
+ switch (sort) {
169
+ case 'newest':
170
+ return files;
171
+ case 'oldest':
172
+ return files.toReversed();
173
+ case 'largest':
174
+ return files.toSorted((left, right) => right.bytes - left.bytes);
175
+ case 'name':
176
+ return files.toSorted((left, right) => left.name.localeCompare(right.name));
177
+ }
178
+ }
79
179
  /**
80
- * Register the media screen: the library, the upload form and the delete.
180
+ * Register the media screens: the library, one file's details, the upload,
181
+ * the alt text and the delete.
81
182
  *
82
183
  * Uploading here is {@link storeUpload}, exactly as the editor's control and
83
184
  * the avatar are, so what a site accepts is one answer given in one place
@@ -85,7 +186,16 @@ export function referencesTo(store, url) {
85
186
  */
86
187
  export function mountMediaScreen(app, options) {
87
188
  const { render, removeDerived } = options;
88
- app.get(MEDIA_PATH, async (c) => render(c, ADMIN_TEMPLATES.media, await screen(c)));
189
+ app.get(MEDIA_PATH, async (c) => render(c, ADMIN_TEMPLATES.media, await library(c, viewOf(c.req.url))));
190
+ app.get(MEDIA_FILE_PATH, async (c) => {
191
+ const view = viewOf(c.req.url);
192
+ const details = await fileScreen(c, c.req.query('path') ?? '', view);
193
+ if (details === undefined) {
194
+ flash(c, 'error', 'That is not a file in this site’s uploads.');
195
+ return c.redirect(mediaLibraryUrl(view), 303);
196
+ }
197
+ return render(c, ADMIN_TEMPLATES.mediaFile, details);
198
+ });
89
199
  app.post(MEDIA_UPLOAD_PATH, async (c) => {
90
200
  const body = await c.req.parseBody();
91
201
  const outcome = await storeUpload(body[MEDIA_FIELDS.file], c.var.config);
@@ -98,11 +208,12 @@ export function mountMediaScreen(app, options) {
98
208
  });
99
209
  app.post(MEDIA_ALT_PATH, async (c) => {
100
210
  const body = await c.req.parseBody();
211
+ const view = postedView(body);
101
212
  const relative = stringOrEmpty(body[MEDIA_FIELDS.path]).trim();
102
213
  const file = relative === '' ? undefined : resolveUpload(c.var.config.contentDir, relative);
103
214
  if (file === undefined || !(await isFile(file))) {
104
215
  flash(c, 'error', 'That is not a file in this site’s uploads.');
105
- return c.redirect(MEDIA_PATH, 303);
216
+ return c.redirect(mediaLibraryUrl(view), 303);
106
217
  }
107
218
  const text = stringOrEmpty(body[MEDIA_FIELDS.alt]).trim().replace(/\s+/g, ' ');
108
219
  const alt = body[MEDIA_FIELDS.decorative] !== undefined
@@ -112,20 +223,16 @@ export function mountMediaScreen(app, options) {
112
223
  : { kind: 'described', text };
113
224
  await writeAltText(c.var.config.contentDir, relative, alt);
114
225
  flash(c, 'notice', altSavedMessage(relative, alt));
115
- return c.redirect(mediaPageUrl(pageNumber(stringOrEmpty(body[MEDIA_FIELDS.page]).trim())), 303);
226
+ return c.redirect(mediaFileUrl(relative, view), 303);
116
227
  });
117
- app.post(MEDIA_STRIP_METADATA_PATH, (c) => queueJobFromForm(c, STRIP_METADATA_JOB, null, {
118
- panel: `${MEDIA_PATH}#strip-metadata`,
119
- queued: 'Queued the sweep. It runs in the background; its progress is below.',
120
- busy: 'A sweep is already queued or running. Nothing new was queued.',
121
- }));
122
228
  app.post(MEDIA_DELETE_PATH, async (c) => {
123
229
  const body = await c.req.parseBody();
230
+ const view = postedView(body);
124
231
  const relative = stringOrEmpty(body[MEDIA_FIELDS.path]).trim();
125
232
  const file = relative === '' ? undefined : resolveUpload(c.var.config.contentDir, relative);
126
233
  if (file === undefined) {
127
234
  flash(c, 'error', 'That is not a file in this site’s uploads.');
128
- return c.redirect(MEDIA_PATH, 303);
235
+ return c.redirect(mediaLibraryUrl(view), 303);
129
236
  }
130
237
  const url = `${UPLOAD_ASSET_PREFIX}${relative}`;
131
238
  const references = referencesTo(c.var.store, url);
@@ -133,7 +240,12 @@ export function mountMediaScreen(app, options) {
133
240
  // bytes do not come back, and the documents naming them are the whole
134
241
  // reason to think twice.
135
242
  if (references.length > 0 && stringOrEmpty(body[MEDIA_FIELDS.confirm]).trim() === '') {
136
- return render(c, ADMIN_TEMPLATES.media, await screen(c, { confirm: { path: relative, url, references } }));
243
+ const details = await fileScreen(c, relative, view);
244
+ if (details === undefined) {
245
+ flash(c, 'error', 'That is not a file in this site’s uploads.');
246
+ return c.redirect(mediaLibraryUrl(view), 303);
247
+ }
248
+ return render(c, ADMIN_TEMPLATES.mediaFile, { ...details, confirm: { url, references } });
137
249
  }
138
250
  const deleted = await deleteUpload({
139
251
  contentDir: c.var.config.contentDir,
@@ -144,43 +256,102 @@ export function mountMediaScreen(app, options) {
144
256
  removeDerived: removeDerived ?? ((uploadPath) => removeImageVariants(c.var.config, uploadPath)),
145
257
  });
146
258
  flash(c, deleted ? 'notice' : 'error', deleted ? `Deleted ${url}.` : `There is no longer a file at ${url}.`);
147
- return c.redirect(MEDIA_PATH, 303);
259
+ return c.redirect(mediaLibraryUrl(view), 303);
148
260
  });
149
- /** Everything the media template renders. */
150
- async function screen(c, extra = {}) {
151
- const files = await listUploads(c.var.config.contentDir);
152
- const latestSweep = await c.var.jobs.latest(STRIP_METADATA_JOB.name);
153
- const page = pageNumber(c.req.query('page'));
154
- const start = (page - 1) * MEDIA_PER_PAGE;
261
+ async function libraryFiles(c) {
262
+ const documents = referenceSources(c.var.store);
263
+ return (await listUploads(c.var.config.contentDir)).map((file) => libraryFile(c.var.config, documents, file));
264
+ }
265
+ /** Everything the library template renders. */
266
+ async function library(c, view) {
267
+ const all = await libraryFiles(c);
268
+ const matching = all.filter((file) => matches(file, view));
269
+ const files = sorted(matching.filter((file) => view.kind === undefined || file.kind === view.kind), view.sort);
270
+ const pages = pageCount(files.length, MEDIA_PER_PAGE);
271
+ const start = (view.page - 1) * MEDIA_PER_PAGE;
272
+ const latestSweep = jobStatusView(await c.var.jobs.latest(STRIP_METADATA_JOB.name));
155
273
  return {
156
274
  section: MEDIA_SECTION,
157
275
  child: 'library',
158
276
  heading: 'Media',
159
277
  listUrl: MEDIA_PATH,
160
278
  uploadUrl: MEDIA_UPLOAD_PATH,
161
- deleteUrl: MEDIA_DELETE_PATH,
162
- altUrl: MEDIA_ALT_PATH,
163
- stripMetadataUrl: MEDIA_STRIP_METADATA_PATH,
164
- sweep: jobStatusView(latestSweep),
165
- sweepReport: finishedResult(STRIP_METADATA_JOB, latestSweep),
166
279
  fields: MEDIA_FIELDS,
167
280
  accepts: c.var.config.uploadTypes.join(','),
168
281
  maxBytes: c.var.config.uploadMaxBytes,
169
282
  mediaMaxBytes: c.var.config.uploadMediaMaxBytes,
170
- total: files.length,
171
- page,
172
- pages: pageCount(files.length, MEDIA_PER_PAGE),
173
- previousUrl: page > 1 ? mediaPageUrl(page - 1) : undefined,
174
- nextUrl: start + MEDIA_PER_PAGE < files.length ? mediaPageUrl(page + 1) : undefined,
175
- // Reference counts are one index scan per file on the page rather than
176
- // per file in the library, which is what paging is for.
283
+ // Until a sweep has finished, the older uploads may still carry where
284
+ // they were taken; after one has, everything since was cleaned on arrival.
285
+ sweepNotice: latestSweep?.state === 'finished'
286
+ ? undefined
287
+ : { sweep: latestSweep, url: STRIP_METADATA_PATH, panelUrl: STRIP_METADATA_PANEL },
288
+ view,
289
+ sorts: SORTS,
290
+ kindTabs: KIND_TABS.map((tab) => ({
291
+ label: tab.label,
292
+ url: mediaLibraryUrl({ ...view, kind: tab.kind, page: 1 }),
293
+ current: tab.kind === view.kind,
294
+ count: matching.filter((file) => tab.kind === undefined || file.kind === tab.kind).length,
295
+ })),
296
+ filtered: view.search !== '' || view.needsAlt || view.unused,
297
+ clearUrl: mediaLibraryUrl({ ...parseMediaView(new URLSearchParams()), kind: view.kind }),
298
+ total: all.length,
299
+ shown: files.length,
300
+ page: view.page,
301
+ pages,
302
+ previousUrl: view.page > 1 ? mediaLibraryUrl({ ...view, page: view.page - 1 }) : undefined,
303
+ nextUrl: start + MEDIA_PER_PAGE < files.length
304
+ ? mediaLibraryUrl({ ...view, page: view.page + 1 })
305
+ : undefined,
177
306
  files: files.slice(start, start + MEDIA_PER_PAGE).map((file) => ({
178
307
  ...file,
179
- references: referencesTo(c.var.store, file.url),
308
+ needsAlt: needsAlt(file),
309
+ detailsUrl: mediaFileUrl(file.path, view),
180
310
  })),
181
- ...extra,
182
311
  };
183
312
  }
313
+ /**
314
+ * Everything the details template renders, or `undefined` when the path is
315
+ * not a file in the library.
316
+ *
317
+ * Previous and next step through the view the file was opened from. The
318
+ * file itself always counts as part of that view, so describing the last
319
+ * image a "needs alt text" view showed does not strand it.
320
+ */
321
+ async function fileScreen(c, relative, view) {
322
+ const all = await libraryFiles(c);
323
+ const file = all.find((candidate) => candidate.path === relative);
324
+ if (file === undefined)
325
+ return undefined;
326
+ const around = sorted(all.filter((candidate) => candidate === file ||
327
+ (matches(candidate, view) && (view.kind === undefined || candidate.kind === view.kind))), view.sort);
328
+ const at = around.indexOf(file);
329
+ const previous = around[at - 1];
330
+ const next = around[at + 1];
331
+ const viewAroundFile = { ...view, page: Math.floor(Math.max(at, 0) / MEDIA_PER_PAGE) + 1 };
332
+ return {
333
+ section: MEDIA_SECTION,
334
+ child: 'library',
335
+ heading: file.name,
336
+ file: { ...file, needsAlt: needsAlt(file) },
337
+ fields: MEDIA_FIELDS,
338
+ from: mediaViewQuery(view),
339
+ altUrl: MEDIA_ALT_PATH,
340
+ deleteUrl: MEDIA_DELETE_PATH,
341
+ libraryUrl: mediaLibraryUrl(viewAroundFile),
342
+ selfUrl: mediaFileUrl(file.path, view),
343
+ previousUrl: previous === undefined ? undefined : mediaFileUrl(previous.path, view),
344
+ nextUrl: next === undefined ? undefined : mediaFileUrl(next.path, view),
345
+ position: at + 1,
346
+ count: around.length,
347
+ };
348
+ }
349
+ }
350
+ function viewOf(url) {
351
+ return parseMediaView(new URL(url).searchParams);
352
+ }
353
+ function postedView(body) {
354
+ return parseMediaView(new URLSearchParams(stringOrEmpty(body[MEDIA_FIELDS.from])));
184
355
  }
185
356
  /** What the flash says once an image's alt text has been saved. */
186
357
  function altSavedMessage(relative, alt) {
@@ -190,10 +361,6 @@ function altSavedMessage(relative, alt) {
190
361
  return `${relative} is marked decorative.`;
191
362
  return `Saved the alt text for ${relative}.`;
192
363
  }
193
- /** The URL of one page of the library. Page one is the screen's own URL. */
194
- export function mediaPageUrl(page) {
195
- return page > 1 ? `${MEDIA_PATH}?page=${String(page)}` : MEDIA_PATH;
196
- }
197
364
  /** A `page` query as a page number. Anything that is not one is page one. */
198
365
  function pageNumber(value) {
199
366
  const parsed = Number(value ?? '1');