@geekity/cms 0.34.0 → 0.36.1
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/components/job-status.njk +15 -10
- package/admin/components/upload.njk +26 -0
- package/admin/pages/documents/chooser.njk +30 -0
- package/admin/pages/documents/editor.njk +410 -165
- package/admin/pages/federation/followers.njk +2 -1
- package/admin/pages/media/file.njk +143 -0
- package/admin/pages/media/library.njk +107 -198
- package/admin/pages/settings/general.njk +52 -2
- package/admin/pages/settings/privacy.njk +1 -1
- package/admin/pages/tools/export.njk +54 -51
- package/admin/pages/tools/maintenance.njk +49 -0
- package/admin/pages/users/edit.njk +3 -0
- package/admin/src/admin.css +2 -0
- package/admin/static/admin.css +843 -106
- package/admin/static/copy.js +30 -23
- package/admin/static/item-lookup.js +179 -0
- package/admin/static/job-status.js +158 -0
- package/admin/static/media.js +82 -0
- package/admin/static/upload-drop.js +225 -0
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +141 -24
- 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/job-status.d.ts +1 -0
- package/dist/admin/job-status.d.ts.map +1 -1
- package/dist/admin/job-status.js +1 -0
- package/dist/admin/job-status.js.map +1 -1
- 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 +47 -36
- package/dist/admin/media.d.ts.map +1 -1
- package/dist/admin/media.js +293 -68
- 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 +4 -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/admin/uploads.d.ts +2 -1
- package/dist/admin/uploads.d.ts.map +1 -1
- package/dist/admin/uploads.js +5 -2
- package/dist/admin/uploads.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/media/library.d.ts +9 -0
- package/dist/media/library.d.ts.map +1 -1
- package/dist/media/library.js +25 -1
- package/dist/media/library.js.map +1 -1
- 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 +6 -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/dist/comments/routes.d.ts +0 -65
- package/dist/comments/routes.d.ts.map +0 -1
- package/dist/comments/routes.js +0 -235
- package/dist/comments/routes.js.map +0 -1
- package/dist/content/media.d.ts +0 -85
- package/dist/content/media.d.ts.map +0 -1
- package/dist/content/media.js +0 -237
- package/dist/content/media.js.map +0 -1
- package/dist/content/read-form.d.ts +0 -31
- package/dist/content/read-form.d.ts.map +0 -1
- package/dist/content/read-form.js +0 -157
- package/dist/content/read-form.js.map +0 -1
- package/dist/content/read.d.ts +0 -29
- package/dist/content/read.d.ts.map +0 -1
- package/dist/content/read.js +0 -82
- package/dist/content/read.js.map +0 -1
- package/dist/export/export-archives.d.ts +0 -29
- package/dist/export/export-archives.d.ts.map +0 -1
- package/dist/export/export-archives.js +0 -88
- package/dist/export/export-archives.js.map +0 -1
- package/dist/federation/mount.d.ts +0 -53
- package/dist/federation/mount.d.ts.map +0 -1
- package/dist/federation/mount.js +0 -344
- package/dist/federation/mount.js.map +0 -1
- package/dist/mail/credentials.d.ts +0 -62
- package/dist/mail/credentials.d.ts.map +0 -1
- package/dist/mail/credentials.js +0 -86
- package/dist/mail/credentials.js.map +0 -1
- package/dist/site/setting-rows.d.ts +0 -45
- package/dist/site/setting-rows.d.ts.map +0 -1
- package/dist/site/setting-rows.js +0 -361
- package/dist/site/setting-rows.js.map +0 -1
- package/dist/site/settings-form.d.ts +0 -30
- package/dist/site/settings-form.d.ts.map +0 -1
- package/dist/site/settings-form.js +0 -54
- package/dist/site/settings-form.js.map +0 -1
- package/dist/site/site-json.d.ts +0 -38
- package/dist/site/site-json.d.ts.map +0 -1
- package/dist/site/site-json.js +0 -69
- package/dist/site/site-json.js.map +0 -1
- package/dist/webmention/routes.d.ts +0 -37
- package/dist/webmention/routes.d.ts.map +0 -1
- package/dist/webmention/routes.js +0 -75
- package/dist/webmention/routes.js.map +0 -1
- package/themes/default/partials/read.njk +0 -5
package/dist/admin/media.d.ts
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
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
|
-
import type { CatalogReader } from '../content/store.ts';
|
|
9
7
|
import type { GeekityEnv } from '../web/env.ts';
|
|
8
|
+
import type { UploadKind } from '../files/upload-types.ts';
|
|
10
9
|
import type { RemoveDerived } from '../media/library.ts';
|
|
11
10
|
import type { AdminRender } from './documents.ts';
|
|
12
11
|
/** Where the media screen lives. */
|
|
13
12
|
export declare const MEDIA_PATH = "/admin/media";
|
|
14
13
|
/** The navigation section the screen marks as current. */
|
|
15
14
|
export declare const MEDIA_SECTION = "media";
|
|
15
|
+
/** Where one file's details screen lives, with the file as `?path=`. */
|
|
16
|
+
export declare const MEDIA_FILE_PATH = "/admin/media/file";
|
|
16
17
|
/** Where the screen's upload form posts. */
|
|
17
18
|
export declare const MEDIA_UPLOAD_PATH = "/admin/media/upload";
|
|
18
|
-
/** Where a
|
|
19
|
+
/** Where a file's Delete button posts. */
|
|
19
20
|
export declare const MEDIA_DELETE_PATH = "/admin/media/delete";
|
|
20
|
-
/** Where a
|
|
21
|
+
/** Where a file's alt-text form posts. */
|
|
21
22
|
export declare const MEDIA_ALT_PATH = "/admin/media/alt";
|
|
22
|
-
export declare const MEDIA_STRIP_METADATA_PATH = "/admin/media/strip-metadata";
|
|
23
23
|
/** How many files one page of the library shows. */
|
|
24
24
|
export declare const MEDIA_PER_PAGE = 24;
|
|
25
|
-
/** The fields the forms on the media
|
|
25
|
+
/** The fields the forms on the media screens submit. */
|
|
26
26
|
export declare const MEDIA_FIELDS: {
|
|
27
27
|
/** The file being added, on the upload form. */
|
|
28
28
|
readonly file: 'file';
|
|
29
|
-
/** The upload being deleted, as a path under `content/uploads/`. */
|
|
29
|
+
/** The upload being deleted or described, as a path under `content/uploads/`. */
|
|
30
30
|
readonly path: 'path';
|
|
31
31
|
/** Set once the admin has been shown what still references the file. */
|
|
32
32
|
readonly confirm: 'confirm';
|
|
@@ -34,35 +34,46 @@ export declare const MEDIA_FIELDS: {
|
|
|
34
34
|
readonly alt: 'alt';
|
|
35
35
|
/** Present when an image is decorative, which wins over any alt text. */
|
|
36
36
|
readonly decorative: 'decorative';
|
|
37
|
-
/** The library
|
|
38
|
-
readonly
|
|
37
|
+
/** The library view a form was posted from, as a query string, so the answer goes back to it. */
|
|
38
|
+
readonly from: 'from';
|
|
39
39
|
};
|
|
40
|
-
/**
|
|
40
|
+
/** The orders the library can be sorted in. Newest is the default and is never written into a URL. */
|
|
41
|
+
declare const SORTS: {
|
|
42
|
+
readonly newest: 'Newest first';
|
|
43
|
+
readonly oldest: 'Oldest first';
|
|
44
|
+
readonly largest: 'Largest first';
|
|
45
|
+
readonly name: 'Name';
|
|
46
|
+
};
|
|
47
|
+
type MediaSort = keyof typeof SORTS;
|
|
48
|
+
/** One view of the library: what the tabs, the filter form and the page links say. */
|
|
49
|
+
export interface MediaView {
|
|
50
|
+
kind: UploadKind | undefined;
|
|
51
|
+
/** Matched against the path and the alt text, ignoring case. Empty matches everything. */
|
|
52
|
+
search: string;
|
|
53
|
+
needsAlt: boolean;
|
|
54
|
+
unused: boolean;
|
|
55
|
+
sort: MediaSort;
|
|
56
|
+
page: number;
|
|
57
|
+
}
|
|
58
|
+
/** A view from a query string. Anything it does not recognise is left at its default. */
|
|
59
|
+
export declare function parseMediaView(query: URLSearchParams): MediaView;
|
|
60
|
+
/** A view as a query string, without the `?`, leaving out everything at its default. */
|
|
61
|
+
export declare function mediaViewQuery(view: MediaView): string;
|
|
62
|
+
/** The library showing a view. */
|
|
63
|
+
export declare function mediaLibraryUrl(view: MediaView): string;
|
|
64
|
+
/** One file's details screen, reached from a view of the library. */
|
|
65
|
+
export declare function mediaFileUrl(path: string, view: MediaView): string;
|
|
66
|
+
/** One thing that uses an upload — a document, a setting, a profile — as the screens name it. */
|
|
41
67
|
export interface MediaReference {
|
|
42
|
-
/**
|
|
43
|
-
path: string;
|
|
44
|
-
/** Its title, or its path when it has none worth showing. */
|
|
68
|
+
/** A document's title, or what the site uses the file as: "The site icon". */
|
|
45
69
|
title: string;
|
|
46
|
-
/** Where to
|
|
70
|
+
/** Where to change it: the document's editor, the settings page, the user's screen. */
|
|
47
71
|
editUrl: string;
|
|
48
|
-
/**
|
|
72
|
+
/** Where it is: a document's path relative to the content directory, or the screen that sets it. */
|
|
73
|
+
where: string;
|
|
74
|
+
/** Whether it is a document in the trash rather than the live tree. */
|
|
49
75
|
trashed: boolean;
|
|
50
76
|
}
|
|
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
|
-
*/
|
|
65
|
-
export declare function referencesTo(store: CatalogReader, url: string): MediaReference[];
|
|
66
77
|
/** What {@link mountMediaScreen} needs from the admin around it. */
|
|
67
78
|
export interface MountMediaScreenOptions {
|
|
68
79
|
/** The admin's renderer, which injects the chrome, the CSRF token and the flash. */
|
|
@@ -71,13 +82,13 @@ export interface MountMediaScreenOptions {
|
|
|
71
82
|
removeDerived?: RemoveDerived | undefined;
|
|
72
83
|
}
|
|
73
84
|
/**
|
|
74
|
-
* Register the media
|
|
85
|
+
* Register the media screens: the library, one file's details, the upload,
|
|
86
|
+
* the alt text and the delete.
|
|
75
87
|
*
|
|
76
88
|
* Uploading here is {@link storeUpload}, exactly as the editor's control and
|
|
77
89
|
* the avatar are, so what a site accepts is one answer given in one place
|
|
78
90
|
* rather than three screens that might disagree.
|
|
79
91
|
*/
|
|
80
92
|
export declare function mountMediaScreen(app: Hono<GeekityEnv>, options: MountMediaScreenOptions): void;
|
|
81
|
-
|
|
82
|
-
export declare function mediaPageUrl(page: number): string;
|
|
93
|
+
export {};
|
|
83
94
|
//# 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":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAW,IAAI,EAAE,MAAM,MAAM,CAAC;AAM1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAIhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAU3D,OAAO,KAAK,EAAa,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASpE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAWlD,oCAAoC;AACpC,eAAO,MAAM,UAAU,iBAA0B,CAAC;AAElD,0DAA0D;AAC1D,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,wEAAwE;AACxE,eAAO,MAAM,eAAe,sBAAuB,CAAC;AAEpD,4CAA4C;AAC5C,eAAO,MAAM,iBAAiB,wBAAyB,CAAC;AAExD,0CAA0C;AAC1C,eAAO,MAAM,iBAAiB,wBAAyB,CAAC;AAExD,0CAA0C;AAC1C,eAAO,MAAM,cAAc,qBAAsB,CAAC;AAElD,oDAAoD;AACpD,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,wDAAwD;AACxD,eAAO,MAAM,YAAY;IACvB,gDAAgD;aAChD,IAAI,EAAE,MAAM;IACZ,iFAAiF;aACjF,IAAI,EAAE,MAAM;IACZ,wEAAwE;aACxE,OAAO,EAAE,SAAS;IAClB,2BAA2B;aAC3B,GAAG,EAAE,KAAK;IACV,yEAAyE;aACzE,UAAU,EAAE,YAAY;IACxB,iGAAiG;aACjG,IAAI,EAAE,MAAM;CACJ,CAAC;AAWX,sGAAsG;AACtG,QAAA,MAAM,KAAK;aACT,MAAM,EAAE,cAAc;aACtB,MAAM,EAAE,cAAc;aACtB,OAAO,EAAE,eAAe;aACxB,IAAI,EAAE,MAAM;CACJ,CAAC;AAEX,KAAK,SAAS,GAAG,MAAM,OAAO,KAAK,CAAC;AAEpC,sFAAsF;AACtF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,UAAU,GAAG,SAAS,CAAC;IAC7B,0FAA0F;IAC1F,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,CAWhE;AAED,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAStD;AAED,kCAAkC;AAClC,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAGvD;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,CAGlE;AAED,iGAAiG;AACjG,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;IAChB,oGAAoG;IACpG,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,OAAO,EAAE,OAAO,CAAC;CAClB;AAsJD,oEAAoE;AACpE,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,MAAM,EAAE,WAAW,CAAC;IACpB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAC3C;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAkN9F"}
|
package/dist/admin/media.js
CHANGED
|
@@ -1,44 +1,49 @@
|
|
|
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 {
|
|
14
|
-
import { deleteUpload, listUploads, mentionsUpload, resolveUpload } from '../media/library.js';
|
|
11
|
+
import { describeImage, removeImageVariants, thumbnailUrl } from '../images/variants.js';
|
|
12
|
+
import { deleteUpload, listUploads, resolveUpload, usageText, usesUpload, } from '../media/library.js';
|
|
15
13
|
import { refusedUpload, storeUpload } from '../media/uploads.js';
|
|
14
|
+
import { SITE } from '../site/settings.js';
|
|
15
|
+
import { listUsers } from '../site/users.js';
|
|
16
16
|
import { isFile } from '../util/files.js';
|
|
17
17
|
import { stringOrEmpty } from '../util/forms.js';
|
|
18
18
|
import { pageCount } from '../web/pagination.js';
|
|
19
19
|
import { documentEditorPath, PAGE_SCREENS, POST_SCREENS } from './documents.js';
|
|
20
20
|
import { flash } from './flash.js';
|
|
21
|
-
import { jobStatusView
|
|
22
|
-
import {
|
|
21
|
+
import { jobStatusView } from './job-status.js';
|
|
22
|
+
import { STRIP_METADATA_PANEL, STRIP_METADATA_PATH } from './maintenance.js';
|
|
23
|
+
import { ADMIN_PREFIX, editUserPath } from '../site/admin-paths.js';
|
|
24
|
+
import { SETTINGS_PATH } from './settings.js';
|
|
25
|
+
import { GENERAL_SETTINGS } from './settings-general.js';
|
|
26
|
+
import { ALL_SETTINGS_PAGES } from './settings-pages.js';
|
|
23
27
|
import { ADMIN_TEMPLATES } from './templates.js';
|
|
24
28
|
/** Where the media screen lives. */
|
|
25
29
|
export const MEDIA_PATH = `${ADMIN_PREFIX}/media`;
|
|
26
30
|
/** The navigation section the screen marks as current. */
|
|
27
31
|
export const MEDIA_SECTION = 'media';
|
|
32
|
+
/** Where one file's details screen lives, with the file as `?path=`. */
|
|
33
|
+
export const MEDIA_FILE_PATH = `${MEDIA_PATH}/file`;
|
|
28
34
|
/** Where the screen's upload form posts. */
|
|
29
35
|
export const MEDIA_UPLOAD_PATH = `${MEDIA_PATH}/upload`;
|
|
30
|
-
/** Where a
|
|
36
|
+
/** Where a file's Delete button posts. */
|
|
31
37
|
export const MEDIA_DELETE_PATH = `${MEDIA_PATH}/delete`;
|
|
32
|
-
/** Where a
|
|
38
|
+
/** Where a file's alt-text form posts. */
|
|
33
39
|
export const MEDIA_ALT_PATH = `${MEDIA_PATH}/alt`;
|
|
34
|
-
export const MEDIA_STRIP_METADATA_PATH = `${MEDIA_PATH}/strip-metadata`;
|
|
35
40
|
/** How many files one page of the library shows. */
|
|
36
41
|
export const MEDIA_PER_PAGE = 24;
|
|
37
|
-
/** The fields the forms on the media
|
|
42
|
+
/** The fields the forms on the media screens submit. */
|
|
38
43
|
export const MEDIA_FIELDS = {
|
|
39
44
|
/** The file being added, on the upload form. */
|
|
40
45
|
file: 'file',
|
|
41
|
-
/** The upload being deleted, as a path under `content/uploads/`. */
|
|
46
|
+
/** The upload being deleted or described, as a path under `content/uploads/`. */
|
|
42
47
|
path: 'path',
|
|
43
48
|
/** Set once the admin has been shown what still references the file. */
|
|
44
49
|
confirm: 'confirm',
|
|
@@ -46,38 +51,192 @@ export const MEDIA_FIELDS = {
|
|
|
46
51
|
alt: 'alt',
|
|
47
52
|
/** Present when an image is decorative, which wins over any alt text. */
|
|
48
53
|
decorative: 'decorative',
|
|
49
|
-
/** The library
|
|
50
|
-
|
|
54
|
+
/** The library view a form was posted from, as a query string, so the answer goes back to it. */
|
|
55
|
+
from: 'from',
|
|
51
56
|
};
|
|
57
|
+
/** The kinds the library's tabs pick between. */
|
|
58
|
+
const KIND_TABS = [
|
|
59
|
+
{ kind: undefined, label: 'All' },
|
|
60
|
+
{ kind: 'image', label: 'Images' },
|
|
61
|
+
{ kind: 'audio', label: 'Audio' },
|
|
62
|
+
{ kind: 'video', label: 'Video' },
|
|
63
|
+
{ kind: 'document', label: 'Documents' },
|
|
64
|
+
];
|
|
65
|
+
/** The orders the library can be sorted in. Newest is the default and is never written into a URL. */
|
|
66
|
+
const SORTS = {
|
|
67
|
+
newest: 'Newest first',
|
|
68
|
+
oldest: 'Oldest first',
|
|
69
|
+
largest: 'Largest first',
|
|
70
|
+
name: 'Name',
|
|
71
|
+
};
|
|
72
|
+
/** A view from a query string. Anything it does not recognise is left at its default. */
|
|
73
|
+
export function parseMediaView(query) {
|
|
74
|
+
const kind = query.get('kind');
|
|
75
|
+
const sort = query.get('sort');
|
|
76
|
+
return {
|
|
77
|
+
kind: KIND_TABS.find((tab) => tab.kind !== undefined && tab.kind === kind)?.kind,
|
|
78
|
+
search: (query.get('q') ?? '').trim(),
|
|
79
|
+
needsAlt: query.get('alt') === 'missing',
|
|
80
|
+
unused: query.get('unused') === '1',
|
|
81
|
+
sort: sort !== null && Object.hasOwn(SORTS, sort) ? sort : 'newest',
|
|
82
|
+
page: pageNumber(query.get('page') ?? undefined),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** A view as a query string, without the `?`, leaving out everything at its default. */
|
|
86
|
+
export function mediaViewQuery(view) {
|
|
87
|
+
const query = new URLSearchParams();
|
|
88
|
+
if (view.kind !== undefined)
|
|
89
|
+
query.set('kind', view.kind);
|
|
90
|
+
if (view.search !== '')
|
|
91
|
+
query.set('q', view.search);
|
|
92
|
+
if (view.needsAlt)
|
|
93
|
+
query.set('alt', 'missing');
|
|
94
|
+
if (view.unused)
|
|
95
|
+
query.set('unused', '1');
|
|
96
|
+
if (view.sort !== 'newest')
|
|
97
|
+
query.set('sort', view.sort);
|
|
98
|
+
if (view.page > 1)
|
|
99
|
+
query.set('page', String(view.page));
|
|
100
|
+
return query.toString();
|
|
101
|
+
}
|
|
102
|
+
/** The library showing a view. */
|
|
103
|
+
export function mediaLibraryUrl(view) {
|
|
104
|
+
const query = mediaViewQuery(view);
|
|
105
|
+
return query === '' ? MEDIA_PATH : `${MEDIA_PATH}?${query}`;
|
|
106
|
+
}
|
|
107
|
+
/** One file's details screen, reached from a view of the library. */
|
|
108
|
+
export function mediaFileUrl(path, view) {
|
|
109
|
+
const query = mediaViewQuery(view);
|
|
110
|
+
return `${MEDIA_FILE_PATH}?path=${encodeURIComponent(path)}${query === '' ? '' : `&${query}`}`;
|
|
111
|
+
}
|
|
52
112
|
/**
|
|
53
|
-
*
|
|
113
|
+
* Everything that could use an upload: every document, live and trashed, and
|
|
114
|
+
* the site itself — its settings and its users' profiles.
|
|
54
115
|
*
|
|
55
116
|
* The trash counts. A trashed post can be restored tomorrow, and restoring one
|
|
56
117
|
* whose picture was deleted in the meantime is a broken post nobody was warned
|
|
57
118
|
* about — so a file the trash still points at asks for confirmation exactly as
|
|
58
119
|
* a published one does.
|
|
59
120
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
121
|
+
* The documents come out of the index rather than off the disk. The index is
|
|
122
|
+
* a cache of the files (decision-1), so it may be a moment behind a hand edit;
|
|
123
|
+
* that is acceptable here because the answer is a warning rather than a
|
|
124
|
+
* decision, and the confirmation step is what makes an out-of-date answer
|
|
125
|
+
* harmless in both directions.
|
|
65
126
|
*/
|
|
66
|
-
|
|
127
|
+
async function referenceSources(c) {
|
|
67
128
|
// Two queries rather than one because `listAll` shows the live tree or the
|
|
68
129
|
// trash and never both, which is the same reason the taxonomy screens ask
|
|
69
130
|
// twice.
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
131
|
+
const documents = [
|
|
132
|
+
...c.var.store.listAll({ trashed: false }),
|
|
133
|
+
...c.var.store.listAll({ trashed: true }),
|
|
134
|
+
];
|
|
135
|
+
return [
|
|
136
|
+
...documents.map(documentSource),
|
|
137
|
+
...(await settingSources(c.var.settings)),
|
|
138
|
+
...userSources(c.var.config.dataDir),
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
function documentSource(document) {
|
|
142
|
+
const { html: _html, hash: _hash, path: where, ...written } = document;
|
|
143
|
+
return {
|
|
144
|
+
reference: {
|
|
145
|
+
title: postLabel(document),
|
|
146
|
+
editUrl: documentEditorPath(document.type === 'page' ? PAGE_SCREENS : POST_SCREENS, document),
|
|
147
|
+
where,
|
|
148
|
+
trashed: isTrashedPath(document.path),
|
|
149
|
+
},
|
|
150
|
+
text: usageText(written),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const SITE_SETTING_TITLES = {
|
|
154
|
+
icon: 'The site icon',
|
|
155
|
+
avatar: 'The site’s old avatar, which the site icon falls back to',
|
|
156
|
+
};
|
|
157
|
+
async function settingSources(settings) {
|
|
158
|
+
const sources = [];
|
|
159
|
+
for (const ref of settings.list()) {
|
|
160
|
+
if (ref.declaration.secret === true)
|
|
161
|
+
continue;
|
|
162
|
+
const text = usageText(await settings.get(ref));
|
|
163
|
+
if (!text.includes(UPLOAD_ASSET_PREFIX))
|
|
164
|
+
continue;
|
|
165
|
+
const page = ref.namespace === SITE.declaration.namespace ? settingsPageOf(ref.key) : undefined;
|
|
166
|
+
sources.push({
|
|
167
|
+
reference: {
|
|
168
|
+
title: SITE_SETTING_TITLES[ref.key] ?? `The ${ref.namespace} setting ${ref.key}`,
|
|
169
|
+
editUrl: page?.path ?? SETTINGS_PATH,
|
|
170
|
+
where: page === undefined ? 'Settings' : (page.heading ?? `Settings › ${page.label}`),
|
|
171
|
+
trashed: false,
|
|
172
|
+
},
|
|
173
|
+
text,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return sources;
|
|
177
|
+
}
|
|
178
|
+
function settingsPageOf(key) {
|
|
179
|
+
if (key === 'avatar')
|
|
180
|
+
return GENERAL_SETTINGS;
|
|
181
|
+
return ALL_SETTINGS_PAGES.find((page) => page.fields.includes(key));
|
|
182
|
+
}
|
|
183
|
+
function userSources(dataDir) {
|
|
184
|
+
return listUsers(dataDir).flatMap((user) => {
|
|
185
|
+
const { avatar, ...rest } = user.profile ?? {};
|
|
186
|
+
const name = user.profile?.displayName ?? user.username;
|
|
187
|
+
const reference = { editUrl: editUserPath(user.id), where: 'Users', trashed: false };
|
|
188
|
+
return [
|
|
189
|
+
{ reference: { ...reference, title: `${name}’s avatar` }, text: avatar ?? '' },
|
|
190
|
+
{ reference: { ...reference, title: `${name}’s profile` }, text: usageText(rest) },
|
|
191
|
+
];
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function referencesAmong(sources, relative) {
|
|
195
|
+
return sources
|
|
196
|
+
.filter((source) => usesUpload(source.text, relative))
|
|
197
|
+
.map((source) => source.reference);
|
|
198
|
+
}
|
|
199
|
+
function libraryFile(config, sources, file) {
|
|
200
|
+
const image = file.kind === 'image' ? describeImage(config, file.path) : undefined;
|
|
201
|
+
return {
|
|
202
|
+
...file,
|
|
203
|
+
references: referencesAmong(sources, file.path),
|
|
204
|
+
thumbnailUrl: file.kind === 'image' ? thumbnailUrl(config, file.path, file.url) : file.url,
|
|
205
|
+
width: image?.width,
|
|
206
|
+
height: image?.height,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function needsAlt(file) {
|
|
210
|
+
return file.kind === 'image' && file.alt === undefined;
|
|
211
|
+
}
|
|
212
|
+
/** Whether a file is in a view, leaving the kind tabs and the page aside. */
|
|
213
|
+
function matches(file, view) {
|
|
214
|
+
if (view.needsAlt && !needsAlt(file))
|
|
215
|
+
return false;
|
|
216
|
+
if (view.unused && file.references.length > 0)
|
|
217
|
+
return false;
|
|
218
|
+
if (view.search === '')
|
|
219
|
+
return true;
|
|
220
|
+
const search = view.search.toLowerCase();
|
|
221
|
+
const alt = file.alt?.kind === 'described' ? file.alt.text : '';
|
|
222
|
+
return file.path.toLowerCase().includes(search) || alt.toLowerCase().includes(search);
|
|
223
|
+
}
|
|
224
|
+
/** `listUploads` is newest first already; the other orders are stable sorts of it. */
|
|
225
|
+
function sorted(files, sort) {
|
|
226
|
+
switch (sort) {
|
|
227
|
+
case 'newest':
|
|
228
|
+
return files;
|
|
229
|
+
case 'oldest':
|
|
230
|
+
return files.toReversed();
|
|
231
|
+
case 'largest':
|
|
232
|
+
return files.toSorted((left, right) => right.bytes - left.bytes);
|
|
233
|
+
case 'name':
|
|
234
|
+
return files.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
235
|
+
}
|
|
78
236
|
}
|
|
79
237
|
/**
|
|
80
|
-
* Register the media
|
|
238
|
+
* Register the media screens: the library, one file's details, the upload,
|
|
239
|
+
* the alt text and the delete.
|
|
81
240
|
*
|
|
82
241
|
* Uploading here is {@link storeUpload}, exactly as the editor's control and
|
|
83
242
|
* the avatar are, so what a site accepts is one answer given in one place
|
|
@@ -85,7 +244,16 @@ export function referencesTo(store, url) {
|
|
|
85
244
|
*/
|
|
86
245
|
export function mountMediaScreen(app, options) {
|
|
87
246
|
const { render, removeDerived } = options;
|
|
88
|
-
app.get(MEDIA_PATH, async (c) => render(c, ADMIN_TEMPLATES.media, await
|
|
247
|
+
app.get(MEDIA_PATH, async (c) => render(c, ADMIN_TEMPLATES.media, await library(c, viewOf(c.req.url))));
|
|
248
|
+
app.get(MEDIA_FILE_PATH, async (c) => {
|
|
249
|
+
const view = viewOf(c.req.url);
|
|
250
|
+
const details = await fileScreen(c, c.req.query('path') ?? '', view);
|
|
251
|
+
if (details === undefined) {
|
|
252
|
+
flash(c, 'error', 'That is not a file in this site’s uploads.');
|
|
253
|
+
return c.redirect(mediaLibraryUrl(view), 303);
|
|
254
|
+
}
|
|
255
|
+
return render(c, ADMIN_TEMPLATES.mediaFile, details);
|
|
256
|
+
});
|
|
89
257
|
app.post(MEDIA_UPLOAD_PATH, async (c) => {
|
|
90
258
|
const body = await c.req.parseBody();
|
|
91
259
|
const outcome = await storeUpload(body[MEDIA_FIELDS.file], c.var.config);
|
|
@@ -98,11 +266,12 @@ export function mountMediaScreen(app, options) {
|
|
|
98
266
|
});
|
|
99
267
|
app.post(MEDIA_ALT_PATH, async (c) => {
|
|
100
268
|
const body = await c.req.parseBody();
|
|
269
|
+
const view = postedView(body);
|
|
101
270
|
const relative = stringOrEmpty(body[MEDIA_FIELDS.path]).trim();
|
|
102
271
|
const file = relative === '' ? undefined : resolveUpload(c.var.config.contentDir, relative);
|
|
103
272
|
if (file === undefined || !(await isFile(file))) {
|
|
104
273
|
flash(c, 'error', 'That is not a file in this site’s uploads.');
|
|
105
|
-
return c.redirect(
|
|
274
|
+
return c.redirect(mediaLibraryUrl(view), 303);
|
|
106
275
|
}
|
|
107
276
|
const text = stringOrEmpty(body[MEDIA_FIELDS.alt]).trim().replace(/\s+/g, ' ');
|
|
108
277
|
const alt = body[MEDIA_FIELDS.decorative] !== undefined
|
|
@@ -112,28 +281,29 @@ export function mountMediaScreen(app, options) {
|
|
|
112
281
|
: { kind: 'described', text };
|
|
113
282
|
await writeAltText(c.var.config.contentDir, relative, alt);
|
|
114
283
|
flash(c, 'notice', altSavedMessage(relative, alt));
|
|
115
|
-
return c.redirect(
|
|
284
|
+
return c.redirect(mediaFileUrl(relative, view), 303);
|
|
116
285
|
});
|
|
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
286
|
app.post(MEDIA_DELETE_PATH, async (c) => {
|
|
123
287
|
const body = await c.req.parseBody();
|
|
288
|
+
const view = postedView(body);
|
|
124
289
|
const relative = stringOrEmpty(body[MEDIA_FIELDS.path]).trim();
|
|
125
290
|
const file = relative === '' ? undefined : resolveUpload(c.var.config.contentDir, relative);
|
|
126
291
|
if (file === undefined) {
|
|
127
292
|
flash(c, 'error', 'That is not a file in this site’s uploads.');
|
|
128
|
-
return c.redirect(
|
|
293
|
+
return c.redirect(mediaLibraryUrl(view), 303);
|
|
129
294
|
}
|
|
130
295
|
const url = `${UPLOAD_ASSET_PREFIX}${relative}`;
|
|
131
|
-
const references =
|
|
296
|
+
const references = referencesAmong(await referenceSources(c), relative);
|
|
132
297
|
// A file something still points at is offered rather than deleted: the
|
|
133
298
|
// bytes do not come back, and the documents naming them are the whole
|
|
134
299
|
// reason to think twice.
|
|
135
300
|
if (references.length > 0 && stringOrEmpty(body[MEDIA_FIELDS.confirm]).trim() === '') {
|
|
136
|
-
|
|
301
|
+
const details = await fileScreen(c, relative, view);
|
|
302
|
+
if (details === undefined) {
|
|
303
|
+
flash(c, 'error', 'That is not a file in this site’s uploads.');
|
|
304
|
+
return c.redirect(mediaLibraryUrl(view), 303);
|
|
305
|
+
}
|
|
306
|
+
return render(c, ADMIN_TEMPLATES.mediaFile, { ...details, confirm: { url, references } });
|
|
137
307
|
}
|
|
138
308
|
const deleted = await deleteUpload({
|
|
139
309
|
contentDir: c.var.config.contentDir,
|
|
@@ -144,43 +314,102 @@ export function mountMediaScreen(app, options) {
|
|
|
144
314
|
removeDerived: removeDerived ?? ((uploadPath) => removeImageVariants(c.var.config, uploadPath)),
|
|
145
315
|
});
|
|
146
316
|
flash(c, deleted ? 'notice' : 'error', deleted ? `Deleted ${url}.` : `There is no longer a file at ${url}.`);
|
|
147
|
-
return c.redirect(
|
|
317
|
+
return c.redirect(mediaLibraryUrl(view), 303);
|
|
148
318
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
319
|
+
async function libraryFiles(c) {
|
|
320
|
+
const sources = await referenceSources(c);
|
|
321
|
+
return (await listUploads(c.var.config.contentDir)).map((file) => libraryFile(c.var.config, sources, file));
|
|
322
|
+
}
|
|
323
|
+
/** Everything the library template renders. */
|
|
324
|
+
async function library(c, view) {
|
|
325
|
+
const all = await libraryFiles(c);
|
|
326
|
+
const matching = all.filter((file) => matches(file, view));
|
|
327
|
+
const files = sorted(matching.filter((file) => view.kind === undefined || file.kind === view.kind), view.sort);
|
|
328
|
+
const pages = pageCount(files.length, MEDIA_PER_PAGE);
|
|
329
|
+
const start = (view.page - 1) * MEDIA_PER_PAGE;
|
|
330
|
+
const latestSweep = jobStatusView(await c.var.jobs.latest(STRIP_METADATA_JOB.name));
|
|
155
331
|
return {
|
|
156
332
|
section: MEDIA_SECTION,
|
|
157
333
|
child: 'library',
|
|
158
334
|
heading: 'Media',
|
|
159
335
|
listUrl: MEDIA_PATH,
|
|
160
336
|
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
337
|
fields: MEDIA_FIELDS,
|
|
167
338
|
accepts: c.var.config.uploadTypes.join(','),
|
|
168
339
|
maxBytes: c.var.config.uploadMaxBytes,
|
|
169
340
|
mediaMaxBytes: c.var.config.uploadMediaMaxBytes,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
341
|
+
// Until a sweep has finished, the older uploads may still carry where
|
|
342
|
+
// they were taken; after one has, everything since was cleaned on arrival.
|
|
343
|
+
sweepNotice: latestSweep?.state === 'finished'
|
|
344
|
+
? undefined
|
|
345
|
+
: { sweep: latestSweep, url: STRIP_METADATA_PATH, panelUrl: STRIP_METADATA_PANEL },
|
|
346
|
+
view,
|
|
347
|
+
sorts: SORTS,
|
|
348
|
+
kindTabs: KIND_TABS.map((tab) => ({
|
|
349
|
+
label: tab.label,
|
|
350
|
+
url: mediaLibraryUrl({ ...view, kind: tab.kind, page: 1 }),
|
|
351
|
+
current: tab.kind === view.kind,
|
|
352
|
+
count: matching.filter((file) => tab.kind === undefined || file.kind === tab.kind).length,
|
|
353
|
+
})),
|
|
354
|
+
filtered: view.search !== '' || view.needsAlt || view.unused,
|
|
355
|
+
clearUrl: mediaLibraryUrl({ ...parseMediaView(new URLSearchParams()), kind: view.kind }),
|
|
356
|
+
total: all.length,
|
|
357
|
+
shown: files.length,
|
|
358
|
+
page: view.page,
|
|
359
|
+
pages,
|
|
360
|
+
previousUrl: view.page > 1 ? mediaLibraryUrl({ ...view, page: view.page - 1 }) : undefined,
|
|
361
|
+
nextUrl: start + MEDIA_PER_PAGE < files.length
|
|
362
|
+
? mediaLibraryUrl({ ...view, page: view.page + 1 })
|
|
363
|
+
: undefined,
|
|
177
364
|
files: files.slice(start, start + MEDIA_PER_PAGE).map((file) => ({
|
|
178
365
|
...file,
|
|
179
|
-
|
|
366
|
+
needsAlt: needsAlt(file),
|
|
367
|
+
detailsUrl: mediaFileUrl(file.path, view),
|
|
180
368
|
})),
|
|
181
|
-
...extra,
|
|
182
369
|
};
|
|
183
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Everything the details template renders, or `undefined` when the path is
|
|
373
|
+
* not a file in the library.
|
|
374
|
+
*
|
|
375
|
+
* Previous and next step through the view the file was opened from. The
|
|
376
|
+
* file itself always counts as part of that view, so describing the last
|
|
377
|
+
* image a "needs alt text" view showed does not strand it.
|
|
378
|
+
*/
|
|
379
|
+
async function fileScreen(c, relative, view) {
|
|
380
|
+
const all = await libraryFiles(c);
|
|
381
|
+
const file = all.find((candidate) => candidate.path === relative);
|
|
382
|
+
if (file === undefined)
|
|
383
|
+
return undefined;
|
|
384
|
+
const around = sorted(all.filter((candidate) => candidate === file ||
|
|
385
|
+
(matches(candidate, view) && (view.kind === undefined || candidate.kind === view.kind))), view.sort);
|
|
386
|
+
const at = around.indexOf(file);
|
|
387
|
+
const previous = around[at - 1];
|
|
388
|
+
const next = around[at + 1];
|
|
389
|
+
const viewAroundFile = { ...view, page: Math.floor(Math.max(at, 0) / MEDIA_PER_PAGE) + 1 };
|
|
390
|
+
return {
|
|
391
|
+
section: MEDIA_SECTION,
|
|
392
|
+
child: 'library',
|
|
393
|
+
heading: file.name,
|
|
394
|
+
file: { ...file, needsAlt: needsAlt(file) },
|
|
395
|
+
fields: MEDIA_FIELDS,
|
|
396
|
+
from: mediaViewQuery(view),
|
|
397
|
+
altUrl: MEDIA_ALT_PATH,
|
|
398
|
+
deleteUrl: MEDIA_DELETE_PATH,
|
|
399
|
+
libraryUrl: mediaLibraryUrl(viewAroundFile),
|
|
400
|
+
selfUrl: mediaFileUrl(file.path, view),
|
|
401
|
+
previousUrl: previous === undefined ? undefined : mediaFileUrl(previous.path, view),
|
|
402
|
+
nextUrl: next === undefined ? undefined : mediaFileUrl(next.path, view),
|
|
403
|
+
position: at + 1,
|
|
404
|
+
count: around.length,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function viewOf(url) {
|
|
409
|
+
return parseMediaView(new URL(url).searchParams);
|
|
410
|
+
}
|
|
411
|
+
function postedView(body) {
|
|
412
|
+
return parseMediaView(new URLSearchParams(stringOrEmpty(body[MEDIA_FIELDS.from])));
|
|
184
413
|
}
|
|
185
414
|
/** What the flash says once an image's alt text has been saved. */
|
|
186
415
|
function altSavedMessage(relative, alt) {
|
|
@@ -190,10 +419,6 @@ function altSavedMessage(relative, alt) {
|
|
|
190
419
|
return `${relative} is marked decorative.`;
|
|
191
420
|
return `Saved the alt text for ${relative}.`;
|
|
192
421
|
}
|
|
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
422
|
/** A `page` query as a page number. Anything that is not one is page one. */
|
|
198
423
|
function pageNumber(value) {
|
|
199
424
|
const parsed = Number(value ?? '1');
|