@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.
- package/admin/pages/documents/chooser.njk +30 -0
- package/admin/pages/documents/editor.njk +394 -164
- package/admin/pages/media/file.njk +142 -0
- package/admin/pages/media/library.njk +103 -198
- package/admin/pages/settings/general.njk +48 -1
- package/admin/pages/settings/privacy.njk +1 -1
- package/admin/pages/tools/maintenance.njk +48 -0
- package/admin/src/admin.css +1 -0
- package/admin/static/admin.css +728 -65
- package/admin/static/copy.js +30 -23
- package/admin/static/item-lookup.js +179 -0
- package/admin/static/media.js +82 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +141 -22
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/editor-actions.d.ts.map +1 -1
- package/dist/admin/editor-actions.js +2 -3
- package/dist/admin/editor-actions.js.map +1 -1
- package/dist/admin/editor-fields.d.ts +20 -8
- package/dist/admin/editor-fields.d.ts.map +1 -1
- package/dist/admin/editor-fields.js +35 -12
- package/dist/admin/editor-fields.js.map +1 -1
- package/dist/admin/editor-kinds.d.ts +30 -0
- package/dist/admin/editor-kinds.d.ts.map +1 -0
- package/dist/admin/editor-kinds.js +136 -0
- package/dist/admin/editor-kinds.js.map +1 -0
- package/dist/admin/editor-layout.d.ts +10 -1
- package/dist/admin/editor-layout.d.ts.map +1 -1
- package/dist/admin/editor-layout.js +38 -31
- package/dist/admin/editor-layout.js.map +1 -1
- package/dist/admin/federation.d.ts +1 -1
- package/dist/admin/federation.d.ts.map +1 -1
- package/dist/admin/federation.js +2 -1
- package/dist/admin/federation.js.map +1 -1
- package/dist/admin/formatting.d.ts +2 -0
- package/dist/admin/formatting.d.ts.map +1 -1
- package/dist/admin/formatting.js +8 -0
- package/dist/admin/formatting.js.map +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/item-lookup.d.ts +16 -0
- package/dist/admin/item-lookup.d.ts.map +1 -0
- package/dist/admin/item-lookup.js +192 -0
- package/dist/admin/item-lookup.js.map +1 -0
- package/dist/admin/maintenance.d.ts +2 -0
- package/dist/admin/maintenance.d.ts.map +1 -1
- package/dist/admin/maintenance.js +14 -0
- package/dist/admin/maintenance.js.map +1 -1
- package/dist/admin/media.d.ts +43 -29
- package/dist/admin/media.d.ts.map +1 -1
- package/dist/admin/media.js +221 -54
- package/dist/admin/media.js.map +1 -1
- package/dist/admin/preview.d.ts.map +1 -1
- package/dist/admin/preview.js +9 -12
- package/dist/admin/preview.js.map +1 -1
- package/dist/admin/routes.d.ts.map +1 -1
- package/dist/admin/routes.js +3 -0
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/settings-general.d.ts +6 -0
- package/dist/admin/settings-general.d.ts.map +1 -1
- package/dist/admin/settings-general.js +46 -1
- package/dist/admin/settings-general.js.map +1 -1
- package/dist/admin/templates.d.ts +2 -0
- package/dist/admin/templates.d.ts.map +1 -1
- package/dist/admin/templates.js +4 -0
- package/dist/admin/templates.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/cms/app.d.ts.map +1 -1
- package/dist/cms/app.js +1 -0
- package/dist/cms/app.js.map +1 -1
- package/dist/cms/services.d.ts +2 -0
- package/dist/cms/services.d.ts.map +1 -1
- package/dist/cms/services.js +13 -0
- package/dist/cms/services.js.map +1 -1
- package/dist/content/document-form.d.ts +5 -13
- package/dist/content/document-form.d.ts.map +1 -1
- package/dist/content/document-form.js +2 -3
- package/dist/content/document-form.js.map +1 -1
- package/dist/content/excerpt.d.ts +1 -3
- package/dist/content/excerpt.d.ts.map +1 -1
- package/dist/content/excerpt.js +2 -7
- package/dist/content/excerpt.js.map +1 -1
- package/dist/content/post-fields.d.ts +1 -1
- package/dist/content/post-fields.d.ts.map +1 -1
- package/dist/content/post-fields.js +30 -35
- package/dist/content/post-fields.js.map +1 -1
- package/dist/content/post-type.d.ts +2 -3
- package/dist/content/post-type.d.ts.map +1 -1
- package/dist/content/post-type.js +8 -9
- package/dist/content/post-type.js.map +1 -1
- package/dist/content/review-form.d.ts +49 -0
- package/dist/content/review-form.d.ts.map +1 -0
- package/dist/content/review-form.js +282 -0
- package/dist/content/review-form.js.map +1 -0
- package/dist/content/review.d.ts +57 -0
- package/dist/content/review.d.ts.map +1 -0
- package/dist/content/review.js +264 -0
- package/dist/content/review.js.map +1 -0
- package/dist/content/write.d.ts.map +1 -1
- package/dist/content/write.js +6 -9
- package/dist/content/write.js.map +1 -1
- package/dist/exports.d.ts +1 -1
- package/dist/exports.d.ts.map +1 -1
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +27 -29
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/federation.d.ts +3 -0
- package/dist/federation/federation.d.ts.map +1 -1
- package/dist/federation/federation.js +5 -1
- package/dist/federation/federation.js.map +1 -1
- package/dist/federation/refusals.d.ts +18 -0
- package/dist/federation/refusals.d.ts.map +1 -0
- package/dist/federation/refusals.js +84 -0
- package/dist/federation/refusals.js.map +1 -0
- package/dist/images/variants.d.ts +1 -0
- package/dist/images/variants.d.ts.map +1 -1
- package/dist/images/variants.js +10 -0
- package/dist/images/variants.js.map +1 -1
- package/dist/item-lookup/item-id.d.ts +33 -0
- package/dist/item-lookup/item-id.d.ts.map +1 -0
- package/dist/item-lookup/item-id.js +71 -0
- package/dist/item-lookup/item-id.js.map +1 -0
- package/dist/item-lookup/lookup.d.ts +58 -0
- package/dist/item-lookup/lookup.d.ts.map +1 -0
- package/dist/item-lookup/lookup.js +532 -0
- package/dist/item-lookup/lookup.js.map +1 -0
- package/dist/micropub/create.d.ts +1 -1
- package/dist/micropub/create.d.ts.map +1 -1
- package/dist/micropub/create.js +4 -2
- package/dist/micropub/create.js.map +1 -1
- package/dist/micropub/endpoint.d.ts.map +1 -1
- package/dist/micropub/endpoint.js +3 -10
- package/dist/micropub/endpoint.js.map +1 -1
- package/dist/micropub/update.d.ts.map +1 -1
- package/dist/micropub/update.js +12 -6
- package/dist/micropub/update.js.map +1 -1
- package/dist/plugin.d.ts +26 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/dist/settings/names.d.ts +1 -1
- package/dist/settings/names.d.ts.map +1 -1
- package/dist/settings/names.js +8 -1
- package/dist/settings/names.js.map +1 -1
- package/dist/settings/registry.d.ts.map +1 -1
- package/dist/settings/registry.js +2 -1
- package/dist/settings/registry.js.map +1 -1
- package/dist/settings/tmdb.d.ts +5 -0
- package/dist/settings/tmdb.d.ts.map +1 -0
- package/dist/settings/tmdb.js +22 -0
- package/dist/settings/tmdb.js.map +1 -0
- package/dist/store/profiles.d.ts +3 -2
- package/dist/store/profiles.d.ts.map +1 -1
- package/dist/store/profiles.js +2 -4
- package/dist/store/profiles.js.map +1 -1
- package/dist/web/context.d.ts +9 -21
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +20 -20
- package/dist/web/context.js.map +1 -1
- package/dist/web/feed-item.d.ts +6 -6
- package/dist/web/feed-item.d.ts.map +1 -1
- package/dist/web/feed-item.js +7 -3
- package/dist/web/feed-item.js.map +1 -1
- package/dist/web/plugin-federation.d.ts +2 -0
- package/dist/web/plugin-federation.d.ts.map +1 -1
- package/dist/web/plugin-federation.js +3 -0
- package/dist/web/plugin-federation.js.map +1 -1
- package/dist/webmention/cited-picture.d.ts +1 -0
- package/dist/webmention/cited-picture.d.ts.map +1 -1
- package/dist/webmention/cited-picture.js +7 -4
- package/dist/webmention/cited-picture.js.map +1 -1
- package/dist/webmention/fetch-public.d.ts +3 -0
- package/dist/webmention/fetch-public.d.ts.map +1 -1
- package/dist/webmention/fetch-public.js +1 -1
- package/dist/webmention/fetch-public.js.map +1 -1
- package/dist/webmention/reply-context/json-ld.d.ts +8 -0
- package/dist/webmention/reply-context/json-ld.d.ts.map +1 -1
- package/dist/webmention/reply-context/json-ld.js +21 -0
- package/dist/webmention/reply-context/json-ld.js.map +1 -1
- package/dist/webmention/service.d.ts.map +1 -1
- package/dist/webmention/service.js +3 -5
- package/dist/webmention/service.js.map +1 -1
- package/package.json +1 -1
- package/themes/default/layouts/post.njk +6 -5
- package/themes/default/partials/post-list.njk +15 -20
- package/themes/default/partials/review.njk +1 -0
- package/themes/default/src/style.css +24 -4
- package/themes/default/static/style.css +33 -5
- 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;
|
|
1
|
+
{"version":3,"file":"maintenance.js","sourceRoot":"","sources":["../../src/admin/maintenance.ts"],"names":[],"mappings":"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"}
|
package/dist/admin/media.d.ts
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The media screen: what is under `content/uploads
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
20
|
+
/** Where a file's Delete button posts. */
|
|
19
21
|
export declare const MEDIA_DELETE_PATH = "/admin/media/delete";
|
|
20
|
-
/** Where a
|
|
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
|
|
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
|
|
38
|
-
readonly
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../src/admin/media.ts"],"names":[],"mappings":"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"}
|
package/dist/admin/media.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The media screen: what is under `content/uploads
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
31
|
+
/** Where a file's Delete button posts. */
|
|
31
32
|
export const MEDIA_DELETE_PATH = `${MEDIA_PATH}/delete`;
|
|
32
|
-
/** Where a
|
|
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
|
|
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
|
|
50
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
259
|
+
return c.redirect(mediaLibraryUrl(view), 303);
|
|
148
260
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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');
|