@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
@@ -0,0 +1,142 @@
1
+ {# /admin/media/file?path=…: one upload, everything that can be said about it
2
+ and done to it (TASK-429). The rest of the query is the library view it was
3
+ opened from, which previous, next and the way back keep.
4
+
5
+ Everything inside [data-media-details] is what media.js shows in its dialog
6
+ when a tile is opened from the library, so it is the whole screen apart
7
+ from the shell. #}
8
+ {% extends "layouts/shell.njk" %}
9
+ {% from "components/badge.njk" import status %}
10
+ {% from "components/button.njk" import button, buttonLink %}
11
+ {% from "components/card.njk" import card, cardActions %}
12
+ {% from "components/copy.njk" import copyField %}
13
+
14
+ {% block title %}{{ heading }} · Media{% endblock %}
15
+
16
+ {% block content %}
17
+ <div data-media-details>
18
+ <div class="mb-4 flex flex-wrap items-center gap-2">
19
+ <a class="link link-hover text-sm" href="{{ libraryUrl }}" data-media-library>Media</a>
20
+ <span class="flex-1"></span>
21
+ <span class="text-sm">{{ position }} of {{ count }}</span>
22
+ <nav class="join" aria-label="Files">
23
+ {% if previousUrl %}
24
+ <a class="join-item btn btn-sm" rel="prev" href="{{ previousUrl }}" data-media-step>Previous</a>
25
+ {% else %}
26
+ <span class="join-item btn btn-sm btn-disabled" aria-disabled="true">Previous</span>
27
+ {% endif %}
28
+ {% if nextUrl %}
29
+ <a class="join-item btn btn-sm" rel="next" href="{{ nextUrl }}" data-media-step>Next</a>
30
+ {% else %}
31
+ <span class="join-item btn btn-sm btn-disabled" aria-disabled="true">Next</span>
32
+ {% endif %}
33
+ </nav>
34
+ </div>
35
+
36
+ <h1 class="mb-6 text-2xl font-bold break-all">{{ heading }}</h1>
37
+
38
+ {% if confirm %}
39
+ <form class="mb-6 max-w-2xl" method="post" action="{{ deleteUrl }}">
40
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
41
+ <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
42
+ <input type="hidden" name="{{ fields.from }}" value="{{ from }}" />
43
+ <input type="hidden" name="{{ fields.confirm }}" value="1" />
44
+ {% call card(title='Delete ' + confirm.url + '?', style='border') %}
45
+ <p>
46
+ {{ confirm.references.length }}
47
+ {% if confirm.references.length == 1 %}document still points{% else %}documents still point{% endif %}
48
+ at it. Deleting the file does not change them, so they will point at nothing.
49
+ </p>
50
+ <ul class="list-disc pl-6">
51
+ {% for reference in confirm.references %}
52
+ <li>
53
+ <a class="link" href="{{ reference.editUrl }}">{{ reference.title }}</a>
54
+ {% if reference.trashed %}{{ status('trashed', 'Trash') }}{% endif %}
55
+ <code>{{ reference.path }}</code>
56
+ </li>
57
+ {% endfor %}
58
+ </ul>
59
+ {% call cardActions() %}
60
+ {{ buttonLink('Cancel', selfUrl, style='ghost') }}
61
+ {{ button('Delete it anyway', type='submit', color='error') }}
62
+ {% endcall %}
63
+ {% endcall %}
64
+ </form>
65
+ {% endif %}
66
+
67
+ <div class="grid gap-6 overflow-hidden rounded-box border border-base-300 bg-base-100 lg:grid-cols-[3fr_2fr]">
68
+ <div class="flex min-h-64 items-center justify-center bg-base-200 p-4">
69
+ {% if file.kind == 'image' %}
70
+ <a href="{{ file.url }}"><img class="max-h-[70vh] rounded-box object-contain" src="{{ file.url }}" alt=""{% if file.width %} width="{{ file.width }}" height="{{ file.height }}"{% endif %} /></a>
71
+ {% elif file.kind == 'audio' %}
72
+ <audio class="w-full" controls preload="none" src="{{ file.url }}"></audio>
73
+ {% elif file.kind == 'video' %}
74
+ <video class="max-h-[70vh] w-full" controls preload="metadata" src="{{ file.url }}"></video>
75
+ {% else %}
76
+ <a class="flex size-40 items-center justify-center rounded-box bg-base-300 font-bold" href="{{ file.url }}">{{ file.extension | replace('.', '') | upper or 'FILE' }}</a>
77
+ {% endif %}
78
+ </div>
79
+
80
+ <div class="flex flex-col gap-5 p-4">
81
+ <dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
82
+ <dt>Uploaded</dt><dd>{{ file.modified | date('html') }}</dd>
83
+ <dt>Size</dt><dd>{{ file.bytes | fileSize }}{% if file.width %} · {{ file.width }} × {{ file.height }}{% endif %}</dd>
84
+ <dt>Path</dt><dd><code class="break-all">{{ file.path }}</code></dd>
85
+ </dl>
86
+
87
+ {% if file.kind == 'image' %}
88
+ <form method="post" action="{{ altUrl }}">
89
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
90
+ <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
91
+ <input type="hidden" name="{{ fields.from }}" value="{{ from }}" />
92
+ <div class="fieldset">
93
+ <label class="fieldset-legend" for="media-alt">Alt text</label>
94
+ <textarea id="media-alt" name="{{ fields.alt }}" rows="3" placeholder="Describe what the picture shows" aria-describedby="media-alt-state" class="textarea w-full">{% if file.alt and file.alt.kind == 'described' %}{{ file.alt.text }}{% endif %}</textarea>
95
+ <label class="label text-sm text-base-content" for="media-decorative">
96
+ <input id="media-decorative" name="{{ fields.decorative }}" type="checkbox" value="1"{% if file.alt and file.alt.kind == 'decorative' %} checked{% endif %} class="checkbox checkbox-sm" />
97
+ Decorative
98
+ </label>
99
+ <p id="media-alt-state" class="text-sm">
100
+ {%- if not file.alt %}{{ status('missing', 'Missing') }} Describe it, or mark it decorative.
101
+ {%- elif file.alt.kind == 'decorative' %}{{ status('decorative', 'Decorative') }} Inserted with an empty alt.
102
+ {%- else %}{{ status('described', 'Described') }} Offered when it is inserted.{% endif -%}
103
+ </p>
104
+ </div>
105
+ <div class="mt-2">{{ button('Save', type='submit', color='primary') }}</div>
106
+ </form>
107
+ {% endif %}
108
+
109
+ <div class="flex flex-col gap-2">
110
+ <h2 class="text-sm font-semibold">Link</h2>
111
+ {{ copyField('media-url', 'URL of ' + file.name, file.url) }}
112
+ {{ copyField('media-markdown', 'Markdown for ' + file.name, file.markdown) }}
113
+ </div>
114
+
115
+ <div>
116
+ <h2 class="text-sm font-semibold">Used by</h2>
117
+ {% if file.references.length %}
118
+ <ul class="text-sm">
119
+ {% for reference in file.references %}
120
+ <li>
121
+ <a class="link link-hover" href="{{ reference.editUrl }}">{{ reference.title }}</a>
122
+ {% if reference.trashed %}{{ status('trashed', 'Trash') }}{% endif %}
123
+ </li>
124
+ {% endfor %}
125
+ </ul>
126
+ {% else %}
127
+ <p class="text-sm">Nothing links to it.</p>
128
+ {% endif %}
129
+ </div>
130
+
131
+ <form class="border-t border-base-300 pt-4 text-right" method="post" action="{{ deleteUrl }}">
132
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
133
+ <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
134
+ <input type="hidden" name="{{ fields.from }}" value="{{ from }}" />
135
+ {{ button('Delete', type='submit', style='ghost', color='error') }}
136
+ </form>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <script defer src="{{ assetPrefix }}copy.js"></script>
142
+ {% endblock %}
@@ -1,228 +1,133 @@
1
- {# /admin/media: everything under content/uploads, what links to it, the
2
- things that can be done to it, and the latest metadata sweep (TASK-392).
3
- The directory is walked on every request, so a file copied in by hand is
4
- here without a restart.
1
+ {# /admin/media: everything under content/uploads as a grid of tiles, each a
2
+ link to its details screen (TASK-429). The directory is walked on every
3
+ request, so a file copied in by hand is here without a restart.
5
4
 
6
- Everything works with JavaScript switched off. The URL and the Markdown are
7
- readonly fields, which select and copy like any other text; copy.js adds a
8
- button beside each of them and nothing else. #}
5
+ Everything works with JavaScript switched off: the upload form opens from a
6
+ <details>, the filters are a GET form, and a tile is a link. media.js opens
7
+ a tile's details in a dialog instead and steps through them with the arrow
8
+ keys. #}
9
9
  {% extends "layouts/shell.njk" %}
10
- {% from "components/badge.njk" import status %}
10
+ {% from "components/alert.njk" import alert %}
11
+ {% from "components/badge.njk" import badge %}
11
12
  {% from "components/button.njk" import button, buttonLink %}
12
- {% from "components/card.njk" import card, cardActions %}
13
- {% from "components/copy.njk" import copyField %}
14
- {% from "components/job-status.njk" import jobStatus %}
15
13
  {% from "components/pagination.njk" import pagination %}
16
- {% from "components/table.njk" import table %}
14
+ {% from "components/tabs.njk" import tabs %}
17
15
 
18
16
  {% block title %}{{ heading }}{% endblock %}
19
17
 
20
18
  {% block content %}
21
- <h1 class="mb-6 text-2xl font-bold">{{ heading }}</h1>
19
+ <div class="mb-6 flex flex-wrap items-center gap-3">
20
+ <h1 class="text-2xl font-bold">{{ heading }}</h1>
21
+ <details class="dropdown">
22
+ <summary class="btn btn-primary btn-sm">Upload</summary>
23
+ <form class="dropdown-content z-10 mt-2 w-80 rounded-box bg-base-100 p-4 shadow sm:w-96" method="post" action="{{ uploadUrl }}" enctype="multipart/form-data">
24
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
25
+ <div class="fieldset">
26
+ <label class="fieldset-legend" for="media-file">Add a file</label>
27
+ <input id="media-file" name="{{ fields.file }}" type="file" accept="{{ accepts }}" required class="file-input file-input-sm w-full" />
28
+ <p class="label block whitespace-normal">
29
+ This site accepts {{ accepts | replace(',', ', ') }}, up to {{ maxBytes | fileSize }}, or
30
+ {{ mediaMaxBytes | fileSize }} for audio and video. The same rules the editor’s upload
31
+ control enforces.
32
+ </p>
33
+ </div>
34
+ <div class="mt-2">{{ button('Upload', type='submit', color='primary') }}</div>
35
+ </form>
36
+ </details>
37
+ </div>
22
38
 
23
- {% if confirm %}
24
- <form class="mb-6 max-w-2xl" method="post" action="{{ deleteUrl }}">
25
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
26
- <input type="hidden" name="{{ fields.path }}" value="{{ confirm.path }}" />
27
- <input type="hidden" name="{{ fields.confirm }}" value="1" />
28
- {% call card(title='Delete ' + confirm.url + '?', style='border') %}
29
- <p>
30
- {{ confirm.references.length }}
31
- {% if confirm.references.length == 1 %}document still points{% else %}documents still point{% endif %}
32
- at it. Deleting the file does not change them, so they will point at nothing.
33
- </p>
34
- <ul class="list-disc pl-6">
35
- {% for reference in confirm.references %}
36
- <li>
37
- <a class="link" href="{{ reference.editUrl }}">{{ reference.title }}</a>
38
- {% if reference.trashed %}{{ status('trashed', 'Trash') }}{% endif %}
39
- <code>{{ reference.path }}</code>
40
- </li>
41
- {% endfor %}
42
- </ul>
43
- {% call cardActions() %}
44
- {{ buttonLink('Cancel', listUrl, style='ghost') }}
45
- {{ button('Delete it anyway', type='submit', color='error') }}
46
- {% endcall %}
39
+ {% if sweepNotice %}
40
+ <section id="strip-metadata-notice" class="mb-6 max-w-4xl" aria-label="Location and camera metadata">
41
+ {% call alert(color='info', style='soft', direction='responsive', polite=true) %}
42
+ <span>
43
+ {% if sweepNotice.sweep and sweepNotice.sweep.active %}
44
+ The sweep that strips location and camera metadata from older uploads is {{ sweepNotice.sweep.label | lower }}.
45
+ {% else %}
46
+ Uploads from before files were cleaned on arrival may still carry location or camera metadata.
47
+ {% endif %}
48
+ </span>
49
+ <div class="flex flex-wrap gap-2">
50
+ {% if not sweepNotice.sweep or not sweepNotice.sweep.active %}
51
+ <form method="post" action="{{ sweepNotice.url }}">
52
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
53
+ {{ button('Strip it now', type='submit') }}
54
+ </form>
55
+ {% endif %}
56
+ {{ buttonLink('Details in Maintenance', sweepNotice.panelUrl, style='ghost') }}
57
+ </div>
47
58
  {% endcall %}
48
- </form>
59
+ </section>
49
60
  {% endif %}
50
61
 
51
- <form class="mb-8 max-w-2xl" method="post" action="{{ uploadUrl }}" enctype="multipart/form-data">
52
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
53
- <div class="fieldset">
54
- <label class="fieldset-legend" for="media-file">Add a file</label>
55
- <input id="media-file" name="{{ fields.file }}" type="file" accept="{{ accepts }}" required class="file-input file-input-sm w-full" />
56
- <p class="label block whitespace-normal">
57
- This site accepts {{ accepts | replace(',', ', ') }}, up to {{ maxBytes }} bytes, or
58
- {{ mediaMaxBytes }} bytes for audio and video. The same
59
- rules the editor’s upload control enforces.
60
- </p>
61
- </div>
62
- <div class="mt-2">{{ button('Upload', type='submit', color='primary') }}</div>
63
- </form>
64
-
65
- <section id="strip-metadata" class="mb-8 max-w-2xl" aria-labelledby="strip-metadata-heading">
66
- {% call card() %}
67
- <h2 id="strip-metadata-heading" class="card-title">Location and camera metadata</h2>
68
- <p class="max-w-prose">
69
- Location and camera metadata is removed from pictures and videos as they are
70
- uploaded. Files uploaded before that keep theirs until this sweep runs over
71
- <code>content/uploads/</code>. It runs in the background, leaves a clean file
72
- as it is, and keeps each file's modification time.
73
- </p>
74
- {% if sweep %}
75
- {% call jobStatus(sweep, listUrl + '#strip-metadata', verb='Checked', unit='files') %}
76
- {% if sweepReport %}
77
- <p>
78
- Checked {{ sweepReport.checked }} {% if sweepReport.checked == 1 %}file{% else %}files{% endif %}:
79
- {{ sweepReport.stripped.length }} stripped, {{ sweepReport.clean }} already clean,
80
- {{ sweepReport.unreadable.length }} unreadable.
81
- </p>
82
- {% if sweepReport.stripped.length %}
83
- <h3 class="font-semibold">Stripped</h3>
84
- <ul class="list-disc pl-6">
85
- {% for file in sweepReport.stripped %}
86
- <li><code class="break-all">{{ file.path }}</code>: removed {{ file.removed | join(', ') }}</li>
87
- {% endfor %}
88
- </ul>
89
- {% endif %}
90
- {% if sweepReport.unreadable.length %}
91
- <h3 class="font-semibold">Left as they were</h3>
92
- <ul class="list-disc pl-6">
93
- {% for file in sweepReport.unreadable %}
94
- <li><code class="break-all">{{ file.path }}</code>: {{ file.problem }}</li>
95
- {% endfor %}
96
- </ul>
97
- {% endif %}
98
- {% endif %}
99
- {% endcall %}
100
- {% endif %}
101
- {% if not sweep or not sweep.active %}
102
- <form method="post" action="{{ stripMetadataUrl }}">
103
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
104
- {% call cardActions() %}
105
- {{ button('Run the sweep again' if sweep else 'Strip metadata from every upload', type='submit', color='primary') }}
106
- {% endcall %}
107
- </form>
108
- {% endif %}
109
- {% endcall %}
110
- </section>
111
-
112
62
  {% if total == 0 %}
113
63
  <p>Nothing has been uploaded yet.</p>
114
64
  {% else %}
115
- {% call table(heading) %}
116
- <thead>
117
- <tr>
118
- <th scope="col">File</th>
119
- <th scope="col">Alt text</th>
120
- <th scope="col">Uploaded</th>
121
- <th scope="col">Size</th>
122
- <th scope="col">Link</th>
123
- <th scope="col">Used by</th>
124
- <th scope="col">Actions</th>
125
- </tr>
126
- </thead>
127
- <tbody>
65
+ <div class="mb-4 flex flex-col gap-3">
66
+ {{ tabs(kindTabs, 'Kinds of file', size='sm') }}
67
+ <form class="flex flex-wrap items-center gap-3" method="get" action="{{ listUrl }}">
68
+ {% if view.kind %}<input type="hidden" name="kind" value="{{ view.kind }}" />{% endif %}
69
+ <label class="sr-only" for="media-search">Search file names and alt text</label>
70
+ <input id="media-search" name="q" type="search" value="{{ view.search }}" placeholder="Search file names and alt text" class="input input-sm w-64" />
71
+ <label class="label text-sm text-base-content">
72
+ <input name="alt" type="checkbox" value="missing" class="checkbox checkbox-sm"{% if view.needsAlt %} checked{% endif %} />
73
+ Needs alt text
74
+ </label>
75
+ <label class="label text-sm text-base-content">
76
+ <input name="unused" type="checkbox" value="1" class="checkbox checkbox-sm"{% if view.unused %} checked{% endif %} />
77
+ Not used anywhere
78
+ </label>
79
+ <label class="sr-only" for="media-sort">Sort</label>
80
+ <select id="media-sort" name="sort" class="select select-sm w-40">
81
+ {% for value, label in sorts %}
82
+ <option value="{{ value }}"{% if value == view.sort %} selected{% endif %}>{{ label }}</option>
83
+ {% endfor %}
84
+ </select>
85
+ {{ button('Filter', type='submit') }}
86
+ {% if filtered %}{{ buttonLink('Clear', clearUrl, style='ghost') }}{% endif %}
87
+ </form>
88
+ </div>
89
+
90
+ <p class="mb-3 text-sm">{{ shown }} of {{ total }} {% if total == 1 %}file{% else %}files{% endif %}</p>
91
+
92
+ {% if shown == 0 %}
93
+ <p>Nothing matches.</p>
94
+ {% else %}
95
+ <ul class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-6">
128
96
  {% for file in files %}
129
- <tr>
130
- <td>
131
- <div class="flex min-w-56 items-center gap-3">
97
+ <li>
98
+ <a class="block overflow-hidden rounded-box bg-base-100 shadow-sm ring-primary hover:ring-2 focus-visible:ring-2 focus-visible:outline-none" href="{{ file.detailsUrl }}" data-media-tile>
99
+ <span class="relative block aspect-square bg-base-200">
132
100
  {% if file.kind == 'image' %}
133
- <a class="shrink-0" href="{{ file.url }}">
134
- <img class="size-15 rounded-box bg-base-200 object-cover" src="{{ file.url }}" alt="" width="60" height="60" />
135
- </a>
101
+ <img class="size-full object-cover" src="{{ file.thumbnailUrl }}" alt="" loading="lazy" />
136
102
  {% else %}
137
- <span class="flex size-15 shrink-0 items-center justify-center rounded-box bg-base-200 text-xs font-bold" aria-hidden="true">{{ file.extension | replace('.', '') | upper }}</span>
103
+ <span class="flex size-full items-center justify-center text-sm font-bold" aria-hidden="true">{{ file.extension | replace('.', '') | upper }}</span>
104
+ {% endif %}
105
+ {% if file.needsAlt or not file.references.length %}
106
+ <span class="absolute top-2 left-2 flex flex-col items-start gap-1">
107
+ {% if file.needsAlt %}{{ badge('No alt text', color='warning') }}{% endif %}
108
+ {% if not file.references.length %}{{ badge('Unused', color='neutral') }}{% endif %}
109
+ </span>
138
110
  {% endif %}
139
- <a class="link link-hover break-all" href="{{ file.url }}">{{ file.path }}</a>
140
- </div>
141
- </td>
142
- <td>
143
- {% if file.kind == 'image' %}
144
- <form class="flex min-w-72 flex-wrap items-center gap-2" method="post" action="{{ altUrl }}">
145
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
146
- <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
147
- <input type="hidden" name="{{ fields.page }}" value="{{ page }}" />
148
- <label class="sr-only" for="media-alt-{{ loop.index }}">Alt text for {{ file.name }}</label>
149
- <input
150
- id="media-alt-{{ loop.index }}"
151
- name="{{ fields.alt }}"
152
- type="text"
153
- value="{% if file.alt and file.alt.kind == 'described' %}{{ file.alt.text }}{% endif %}"
154
- aria-describedby="media-alt-state-{{ loop.index }}"
155
- class="input input-sm min-w-40 flex-1"
156
- />
157
- <label class="label text-sm text-base-content" for="media-decorative-{{ loop.index }}">
158
- <input
159
- id="media-decorative-{{ loop.index }}"
160
- name="{{ fields.decorative }}"
161
- type="checkbox"
162
- value="1"
163
- {% if file.alt and file.alt.kind == 'decorative' %}checked{% endif %}
164
- class="checkbox checkbox-sm"
165
- />
166
- Decorative
167
- </label>
168
- {{ button('Save', type='submit', size='sm') }}
169
- <p id="media-alt-state-{{ loop.index }}" class="w-full text-sm">
170
- {%- if not file.alt %}{{ status('missing', 'Missing') }} Describe it, or mark it decorative.
171
- {%- elif file.alt.kind == 'decorative' %}{{ status('decorative', 'Decorative') }} Inserted with an empty alt.
172
- {%- else %}{{ status('described', 'Described') }} Offered when it is inserted.{% endif -%}
173
- </p>
174
- </form>
175
- {% else %}
176
- Not an image
177
- {% endif %}
178
- </td>
179
- <td class="whitespace-nowrap">{{ file.modified | date('html') }}</td>
180
- <td class="whitespace-nowrap">{{ file.bytes }}</td>
181
- <td>
182
- <div class="flex min-w-60 flex-col gap-2">
183
- {{ copyField('media-url-' + loop.index, 'URL of ' + file.name, file.url) }}
184
- {{ copyField('media-markdown-' + loop.index, 'Markdown for ' + file.name, file.markdown) }}
185
- </div>
186
- </td>
187
- <td>
188
- {% if file.references.length %}
189
- <ul>
190
- {% for reference in file.references %}
191
- <li class="whitespace-nowrap">
192
- <a class="link link-hover" href="{{ reference.editUrl }}">{{ reference.title }}</a>
193
- {% if reference.trashed %}{{ status('trashed', 'Trash') }}{% endif %}
194
- </li>
195
- {% endfor %}
196
- </ul>
197
- {% else %}
198
- Nothing
199
- {% endif %}
200
- </td>
201
- <td>
202
- <form method="post" action="{{ deleteUrl }}">
203
- <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
204
- <input type="hidden" name="{{ fields.path }}" value="{{ file.path }}" />
205
- {{ button('Delete', type='submit', style='ghost', size='xs') }}
206
- </form>
207
- </td>
208
- </tr>
111
+ </span>
112
+ <span class="block truncate px-2 py-1.5 text-xs">{{ file.name }}</span>
113
+ </a>
114
+ </li>
209
115
  {% endfor %}
210
- </tbody>
211
- {% endcall %}
116
+ </ul>
212
117
 
213
- <div class="mt-4">
214
- {{ pagination(page, pages=pages, previousUrl=previousUrl, nextUrl=nextUrl) }}
215
- </div>
118
+ <div class="mt-6">
119
+ {{ pagination(page, pages=pages, previousUrl=previousUrl, nextUrl=nextUrl) }}
120
+ </div>
121
+ {% endif %}
216
122
 
217
- <p class="mt-4 max-w-prose text-sm">
218
- The list is the <code>content/uploads</code> directory itself, read afresh on every request,
123
+ <p class="mt-6 max-w-prose text-sm">
124
+ The library is the <code>content/uploads</code> directory itself, read afresh on every request,
219
125
  so a file copied in by hand is here without a restart. Deleting one removes the file and
220
126
  whatever was derived from it; it does not touch the documents that link to it, which is why
221
127
  a file something still points at asks first.
222
128
  </p>
223
129
  {% endif %}
224
130
 
225
- {# The one enhancement on this screen. The fields above are readonly inputs
226
- and already copy by hand; this adds a button beside each of them. #}
227
131
  <script defer src="{{ assetPrefix }}copy.js"></script>
132
+ <script defer src="{{ assetPrefix }}media.js"></script>
228
133
  {% endblock %}
@@ -1,6 +1,9 @@
1
1
  {# /admin/settings: what the site is called, where it lives, and its picture. #}
2
2
  {% extends "layouts/settings-page.njk" %}
3
3
  {% import "components/fields.njk" as field %}
4
+ {% from "components/badge.njk" import status %}
5
+ {% from "components/button.njk" import button %}
6
+ {% from "components/card.njk" import card, cardActions %}
4
7
 
5
8
  {% block settingsProblems %}
6
9
  {{ field.problem('settings-title', problems.title) }}
@@ -93,4 +96,48 @@
93
96
  error=problems.license_name, hint='What the footer calls it. Only read for a custom license.') }}
94
97
  {% endblock %}
95
98
 
96
- {% block settingsPanels %}{% endblock %}
99
+ {% block settingsPanels %}
100
+ {% call card(title='Movies and TV shows') %}
101
+ <p>
102
+ {% if tmdbPresent %}
103
+ {{ status('connected', 'Connected') }} Key {{ tmdbKeyHint }}.
104
+ {% else %}
105
+ {{ status('disconnected', 'Not connected') }} This site has no TMDb key.
106
+ {% endif %}
107
+ </p>
108
+
109
+ {% if tmdbSetBy %}
110
+ <p class="text-sm">The key is set by <code>{{ tmdbSetBy }}</code> and cannot be changed here.</p>
111
+ {% else %}
112
+ <form method="post" action="{{ tmdbUrl }}">
113
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
114
+ {{ field.password('settings-tmdb-key', tmdbFields.key, 'TMDb API key',
115
+ autocomplete='off', spellcheck='false',
116
+ placeholder=('Replace the stored key' if tmdbPresent else 'Paste your key')) }}
117
+ <div class="mt-2">
118
+ {{ button('Replace key' if tmdbPresent else 'Save key', type='submit', color='primary') }}
119
+ </div>
120
+ </form>
121
+ {% endif %}
122
+
123
+ <p class="text-sm">
124
+ With a key from <a class="link" href="https://www.themoviedb.org/settings/api">themoviedb.org</a>,
125
+ Look up on a review fills in a movie or TV show from its TMDb or IMDb link:
126
+ its title, director or network, year and poster. Either the API key or
127
+ the API read access token works. It is kept in <code>data/tmdb.json</code>,
128
+ never in <code>site.json</code> and never on the public site. A deployment
129
+ can set it instead with <code>GEEKITY_TMDB__KEY</code>. This site uses TMDB
130
+ and the TMDB APIs but is not endorsed, certified or otherwise approved by TMDB.
131
+ </p>
132
+
133
+ {% if tmdbPresent and not tmdbSetBy %}
134
+ <form method="post" action="{{ tmdbUrl }}">
135
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
136
+ <input type="hidden" name="{{ tmdbFields.action }}" value="{{ tmdbRemove }}" />
137
+ {% call cardActions() %}
138
+ {{ button('Remove key', type='submit', color='error', style='ghost') }}
139
+ {% endcall %}
140
+ </form>
141
+ {% endif %}
142
+ {% endcall %}
143
+ {% endblock %}
@@ -31,7 +31,7 @@
31
31
  <p class="max-w-prose text-sm">
32
32
  Location and camera metadata is always removed from pictures and videos as
33
33
  they are uploaded, whatever is chosen above. Files uploaded before this
34
- version keep theirs until the sweep on the <a class="link" href="{{ adminUrl }}/media#strip-metadata">Media</a>
34
+ version keep theirs until the sweep on the <a class="link" href="{{ adminUrl }}/tools/maintenance#strip-metadata">Maintenance</a>
35
35
  screen, or <code>geekity strip-metadata</code>, is run over <code>content/uploads/</code>.
36
36
  </p>
37
37
  {% endblock %}
@@ -3,6 +3,7 @@
3
3
  {% from "components/alert.njk" import alert %}
4
4
  {% from "components/button.njk" import button %}
5
5
  {% from "components/card.njk" import card, cardActions %}
6
+ {% from "components/job-status.njk" import jobStatus %}
6
7
 
7
8
  {% block title %}{{ heading }}{% endblock %}
8
9
 
@@ -47,5 +48,52 @@
47
48
  <code>geekity maintenance on</code> and <code>off</code> at the command line turn the
48
49
  same switch, and a running site notices either within a second.
49
50
  </p>
51
+
52
+ <section id="strip-metadata" aria-labelledby="strip-metadata-heading">
53
+ {% call card() %}
54
+ <h2 id="strip-metadata-heading" class="card-title">Location and camera metadata</h2>
55
+ <p class="max-w-prose">
56
+ Location and camera metadata is removed from pictures and videos as they are
57
+ uploaded. Files uploaded before that keep theirs until this sweep runs over
58
+ <code>content/uploads/</code>. It runs in the background, leaves a clean file
59
+ as it is, and keeps each file's modification time.
60
+ </p>
61
+ {% if sweep %}
62
+ {% call jobStatus(sweep, saveUrl + '#strip-metadata', verb='Checked', unit='files') %}
63
+ {% if sweepReport %}
64
+ <p>
65
+ Checked {{ sweepReport.checked }} {% if sweepReport.checked == 1 %}file{% else %}files{% endif %}:
66
+ {{ sweepReport.stripped.length }} stripped, {{ sweepReport.clean }} already clean,
67
+ {{ sweepReport.unreadable.length }} unreadable.
68
+ </p>
69
+ {% if sweepReport.stripped.length %}
70
+ <h3 class="font-semibold">Stripped</h3>
71
+ <ul class="list-disc pl-6">
72
+ {% for file in sweepReport.stripped %}
73
+ <li><code class="break-all">{{ file.path }}</code>: removed {{ file.removed | join(', ') }}</li>
74
+ {% endfor %}
75
+ </ul>
76
+ {% endif %}
77
+ {% if sweepReport.unreadable.length %}
78
+ <h3 class="font-semibold">Left as they were</h3>
79
+ <ul class="list-disc pl-6">
80
+ {% for file in sweepReport.unreadable %}
81
+ <li><code class="break-all">{{ file.path }}</code>: {{ file.problem }}</li>
82
+ {% endfor %}
83
+ </ul>
84
+ {% endif %}
85
+ {% endif %}
86
+ {% endcall %}
87
+ {% endif %}
88
+ {% if not sweep or not sweep.active %}
89
+ <form method="post" action="{{ stripMetadataUrl }}">
90
+ <input type="hidden" name="csrf_token" value="{{ csrfToken }}" />
91
+ {% call cardActions() %}
92
+ {{ button('Run the sweep again' if sweep else 'Strip metadata from every upload', type='submit', color='primary') }}
93
+ {% endcall %}
94
+ </form>
95
+ {% endif %}
96
+ {% endcall %}
97
+ </section>
50
98
  </div>
51
99
  {% endblock %}
@@ -3,6 +3,7 @@
3
3
  @source '../components';
4
4
  @source '../pages';
5
5
  @source '../../editor/look.ts';
6
+ @source '../static/media.js';
6
7
 
7
8
  @plugin 'daisyui' {
8
9
  themes: