@things-factory/gallery 10.1.233 → 10.1.236
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/client/bootstrap.ts +12 -0
- package/client/components/gallery-forms.ts +1 -1
- package/client/components/gallery-saver-setting-let.ts +78 -0
- package/client/components/gallery-show.ts +312 -0
- package/client/components/gallery-uploader.ts +35 -5
- package/client/components/gallery-viewer.ts +17 -4
- package/client/pages/gallery-page.ts +18 -16
- package/client/utils/gallery-api.ts +4 -2
- package/client/utils/gallery-saver.ts +72 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +10 -0
- package/dist-client/bootstrap.js.map +1 -1
- package/dist-client/components/gallery-forms.js +1 -1
- package/dist-client/components/gallery-forms.js.map +1 -1
- package/dist-client/components/gallery-saver-setting-let.d.ts +11 -0
- package/dist-client/components/gallery-saver-setting-let.js +81 -0
- package/dist-client/components/gallery-saver-setting-let.js.map +1 -0
- package/dist-client/components/gallery-show.d.ts +46 -0
- package/dist-client/components/gallery-show.js +316 -0
- package/dist-client/components/gallery-show.js.map +1 -0
- package/dist-client/components/gallery-uploader.d.ts +5 -0
- package/dist-client/components/gallery-uploader.js +43 -4
- package/dist-client/components/gallery-uploader.js.map +1 -1
- package/dist-client/components/gallery-viewer.d.ts +2 -2
- package/dist-client/components/gallery-viewer.js +16 -3
- package/dist-client/components/gallery-viewer.js.map +1 -1
- package/dist-client/pages/gallery-page.d.ts +0 -1
- package/dist-client/pages/gallery-page.js +20 -16
- package/dist-client/pages/gallery-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/gallery-api.d.ts +4 -0
- package/dist-client/utils/gallery-api.js +1 -1
- package/dist-client/utils/gallery-api.js.map +1 -1
- package/dist-client/utils/gallery-saver.d.ts +6 -0
- package/dist-client/utils/gallery-saver.js +69 -0
- package/dist-client/utils/gallery-saver.js.map +1 -0
- package/dist-server/service/gallery/gallery-resolver.js +13 -2
- package/dist-server/service/gallery/gallery-resolver.js.map +1 -1
- package/dist-server/service/gallery/gallery-rules.d.ts +4 -2
- package/dist-server/service/gallery/gallery-rules.js +4 -3
- package/dist-server/service/gallery/gallery-rules.js.map +1 -1
- package/dist-server/service/gallery/gallery-types.d.ts +3 -0
- package/dist-server/service/gallery/gallery-types.js +12 -0
- package/dist-server/service/gallery/gallery-types.js.map +1 -1
- package/dist-server/service/gallery/gallery.d.ts +2 -1
- package/dist-server/service/gallery/gallery.js +7 -2
- package/dist-server/service/gallery/gallery.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/server/service/gallery/gallery-resolver.ts +13 -4
- package/server/service/gallery/gallery-rules.ts +5 -4
- package/server/service/gallery/gallery-types.ts +9 -0
- package/server/service/gallery/gallery.ts +7 -2
- package/test/gallery-persistence.test.ts +24 -3
- package/test/gallery-rules.test.ts +10 -4
- package/test/refusals-and-bounds.test.ts +28 -0
- package/translations/en.json +16 -2
- package/translations/es.json +16 -2
- package/translations/ja.json +16 -2
- package/translations/ko.json +16 -2
- package/translations/ms.json +16 -2
- package/translations/zh.json +16 -2
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
/*
|
|
3
|
+
* The gallery's photos across the whole window, one at a time (ADR-0165 decision 9): seen large from a post, played as
|
|
4
|
+
* a slideshow from the gallery, and the screen saver after the person has been away. One show for the three - what
|
|
5
|
+
* differs is where it starts, whether it plays, and whether anything the person does closes it (the saver).
|
|
6
|
+
*/
|
|
7
|
+
import '@material/web/icon/icon.js';
|
|
8
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
9
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
10
|
+
import { i18next, localize } from '@operato/i18n';
|
|
11
|
+
import { galleryApi } from '../utils/gallery-api.js';
|
|
12
|
+
import { day, t } from '../utils/gallery-words.js';
|
|
13
|
+
/** How long one photo stays while it plays. */
|
|
14
|
+
const DWELL = 7000;
|
|
15
|
+
/** The most photos one show walks through - pages of the gallery's own size are asked until this. */
|
|
16
|
+
const MOST = 300;
|
|
17
|
+
const PAGE = 60;
|
|
18
|
+
let GalleryShow = class GalleryShow extends localize(i18next)(LitElement) {
|
|
19
|
+
constructor() {
|
|
20
|
+
super(...arguments);
|
|
21
|
+
this.saver = false;
|
|
22
|
+
this.frames = [];
|
|
23
|
+
this.at = 0;
|
|
24
|
+
this.playing = false;
|
|
25
|
+
this.loaded = false;
|
|
26
|
+
this.wholeScreen = false;
|
|
27
|
+
this.disposed = false;
|
|
28
|
+
this.onKey = (e) => {
|
|
29
|
+
if (this.saver)
|
|
30
|
+
return;
|
|
31
|
+
const handled = {
|
|
32
|
+
ArrowLeft: () => this.step(-1),
|
|
33
|
+
ArrowRight: () => this.step(1),
|
|
34
|
+
' ': () => this.play(!this.playing),
|
|
35
|
+
Escape: () => this.close()
|
|
36
|
+
};
|
|
37
|
+
const act = handled[e.key];
|
|
38
|
+
if (!act)
|
|
39
|
+
return;
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
act();
|
|
43
|
+
};
|
|
44
|
+
/** Leaving the whole screen (Esc there is the browser's) leaves the show too. */
|
|
45
|
+
this.onFullscreen = () => {
|
|
46
|
+
if (this.wholeScreen && !document.fullscreenElement)
|
|
47
|
+
this.close();
|
|
48
|
+
};
|
|
49
|
+
this.onWake = (e) => {
|
|
50
|
+
/* The pointer resting on the screen still says it moved, by a pixel or so - a real move is farther. */
|
|
51
|
+
if (e.type === 'pointermove' && Math.abs(e.movementX) + Math.abs(e.movementY) < 4)
|
|
52
|
+
return;
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
this.close();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
static { this.styles = css `
|
|
59
|
+
:host {
|
|
60
|
+
position: fixed;
|
|
61
|
+
inset: 0;
|
|
62
|
+
z-index: 10000;
|
|
63
|
+
display: block;
|
|
64
|
+
background: #000;
|
|
65
|
+
color: #fff;
|
|
66
|
+
user-select: none;
|
|
67
|
+
}
|
|
68
|
+
:host([saver]) {
|
|
69
|
+
cursor: none;
|
|
70
|
+
}
|
|
71
|
+
img {
|
|
72
|
+
position: absolute;
|
|
73
|
+
inset: 0;
|
|
74
|
+
width: 100%;
|
|
75
|
+
height: 100%;
|
|
76
|
+
object-fit: contain;
|
|
77
|
+
animation: appear 800ms ease-out;
|
|
78
|
+
}
|
|
79
|
+
@keyframes appear {
|
|
80
|
+
from {
|
|
81
|
+
opacity: 0;
|
|
82
|
+
}
|
|
83
|
+
to {
|
|
84
|
+
opacity: 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
.about {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset-inline: 0;
|
|
90
|
+
bottom: 0;
|
|
91
|
+
padding: 48px 24px 20px;
|
|
92
|
+
background: linear-gradient(transparent, rgb(0 0 0 / 60%));
|
|
93
|
+
font-size: 14px;
|
|
94
|
+
line-height: 1.5;
|
|
95
|
+
}
|
|
96
|
+
.about .caption {
|
|
97
|
+
display: -webkit-box;
|
|
98
|
+
-webkit-line-clamp: 2;
|
|
99
|
+
-webkit-box-orient: vertical;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
white-space: pre-wrap;
|
|
102
|
+
font-size: 16px;
|
|
103
|
+
}
|
|
104
|
+
.about .who {
|
|
105
|
+
opacity: 0.75;
|
|
106
|
+
}
|
|
107
|
+
.bar {
|
|
108
|
+
position: absolute;
|
|
109
|
+
top: 12px;
|
|
110
|
+
inset-inline-end: 12px;
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
}
|
|
115
|
+
.bar .count {
|
|
116
|
+
padding: 0 8px;
|
|
117
|
+
font-size: 13px;
|
|
118
|
+
opacity: 0.8;
|
|
119
|
+
}
|
|
120
|
+
button {
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
padding: 8px;
|
|
123
|
+
border: 0;
|
|
124
|
+
border-radius: 50%;
|
|
125
|
+
background: rgb(0 0 0 / 45%);
|
|
126
|
+
color: #fff;
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
}
|
|
129
|
+
button:hover {
|
|
130
|
+
background: rgb(255 255 255 / 20%);
|
|
131
|
+
}
|
|
132
|
+
.turn {
|
|
133
|
+
position: absolute;
|
|
134
|
+
top: 50%;
|
|
135
|
+
transform: translateY(-50%);
|
|
136
|
+
}
|
|
137
|
+
.previous {
|
|
138
|
+
inset-inline-start: 12px;
|
|
139
|
+
}
|
|
140
|
+
.next {
|
|
141
|
+
inset-inline-end: 12px;
|
|
142
|
+
}
|
|
143
|
+
.empty {
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset: 0;
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
justify-content: center;
|
|
149
|
+
opacity: 0.8;
|
|
150
|
+
}
|
|
151
|
+
`; }
|
|
152
|
+
/** Opens a show over the whole window, and the whole screen when the browser lets it. */
|
|
153
|
+
static open(options) {
|
|
154
|
+
const show = document.createElement('gallery-show');
|
|
155
|
+
show.options = options;
|
|
156
|
+
document.body.appendChild(show);
|
|
157
|
+
return show;
|
|
158
|
+
}
|
|
159
|
+
render() {
|
|
160
|
+
const frame = this.frames[this.at];
|
|
161
|
+
if (!frame)
|
|
162
|
+
return this.loaded
|
|
163
|
+
? html `<div class="empty">${t('gallery.text.show-empty')}</div>${this.saver ? nothing : this.renderBar()}`
|
|
164
|
+
: nothing;
|
|
165
|
+
const { post, photo } = frame;
|
|
166
|
+
return html `
|
|
167
|
+
${ /* A new element for each photo, so it fades in. */[photo.id].map(() => html `<img src=${photo.fullpath} alt=${post.caption ?? ''} />`)}
|
|
168
|
+
<div class="about">
|
|
169
|
+
${post.caption ? html `<div class="caption">${post.caption}</div>` : nothing}
|
|
170
|
+
<div class="who">${post.creator?.name ?? ''} · ${day(post.takenAt)}${post.event ? ` · ${post.event.name}` : ''}</div>
|
|
171
|
+
</div>
|
|
172
|
+
${this.saver
|
|
173
|
+
? nothing
|
|
174
|
+
: html `${this.renderBar()}
|
|
175
|
+
${this.frames.length > 1
|
|
176
|
+
? html `<button class="turn previous" aria-label=${t('gallery.button.previous')} @click=${() => this.step(-1)}>
|
|
177
|
+
<md-icon>chevron_left</md-icon>
|
|
178
|
+
</button>
|
|
179
|
+
<button class="turn next" aria-label=${t('gallery.button.next')} @click=${() => this.step(1)}>
|
|
180
|
+
<md-icon>chevron_right</md-icon>
|
|
181
|
+
</button>`
|
|
182
|
+
: nothing}`}
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
renderBar() {
|
|
186
|
+
return html `<div class="bar">
|
|
187
|
+
${this.frames.length ? html `<span class="count">${this.at + 1} / ${this.frames.length}</span>` : nothing}
|
|
188
|
+
${this.frames.length > 1
|
|
189
|
+
? html `<button
|
|
190
|
+
aria-label=${t(this.playing ? 'gallery.button.pause' : 'gallery.button.play')}
|
|
191
|
+
title=${t(this.playing ? 'gallery.button.pause' : 'gallery.button.play')}
|
|
192
|
+
@click=${() => this.play(!this.playing)}
|
|
193
|
+
>
|
|
194
|
+
<md-icon>${this.playing ? 'pause' : 'play_arrow'}</md-icon>
|
|
195
|
+
</button>`
|
|
196
|
+
: nothing}
|
|
197
|
+
<button aria-label=${t('gallery.button.close')} title=${t('gallery.button.close')} @click=${() => this.close()}>
|
|
198
|
+
<md-icon>close</md-icon>
|
|
199
|
+
</button>
|
|
200
|
+
</div>`;
|
|
201
|
+
}
|
|
202
|
+
connectedCallback() {
|
|
203
|
+
super.connectedCallback();
|
|
204
|
+
this.saver = !!this.options?.saver;
|
|
205
|
+
window.addEventListener('keydown', this.onKey, true);
|
|
206
|
+
document.addEventListener('fullscreenchange', this.onFullscreen);
|
|
207
|
+
if (this.saver)
|
|
208
|
+
for (const name of WAKE)
|
|
209
|
+
window.addEventListener(name, this.onWake, true);
|
|
210
|
+
/* The whole screen needs the press that opened it - the saver has none, and covers the window only. */
|
|
211
|
+
if (!this.saver && this.requestFullscreen)
|
|
212
|
+
this.requestFullscreen()
|
|
213
|
+
.then(() => (this.wholeScreen = true))
|
|
214
|
+
.catch(() => { });
|
|
215
|
+
this.load();
|
|
216
|
+
}
|
|
217
|
+
disconnectedCallback() {
|
|
218
|
+
super.disconnectedCallback();
|
|
219
|
+
this.disposed = true;
|
|
220
|
+
clearTimeout(this.timer);
|
|
221
|
+
window.removeEventListener('keydown', this.onKey, true);
|
|
222
|
+
document.removeEventListener('fullscreenchange', this.onFullscreen);
|
|
223
|
+
for (const name of WAKE)
|
|
224
|
+
window.removeEventListener(name, this.onWake, true);
|
|
225
|
+
}
|
|
226
|
+
async load() {
|
|
227
|
+
const { filter, start, saver } = this.options;
|
|
228
|
+
const frames = [];
|
|
229
|
+
try {
|
|
230
|
+
for (let skip = 0; frames.length < MOST; skip += PAGE) {
|
|
231
|
+
const page = await galleryApi.posts(filter, skip, PAGE);
|
|
232
|
+
for (const post of page.items)
|
|
233
|
+
if (!post.hiddenAt)
|
|
234
|
+
for (const photo of post.photos)
|
|
235
|
+
frames.push({ post, photo });
|
|
236
|
+
if (this.disposed || skip + PAGE >= page.total)
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
/* The show has what came; a saver with nothing closes itself below. */
|
|
242
|
+
}
|
|
243
|
+
if (this.disposed)
|
|
244
|
+
return;
|
|
245
|
+
if (saver)
|
|
246
|
+
shuffle(frames);
|
|
247
|
+
this.frames = frames.slice(0, MOST);
|
|
248
|
+
const found = start ? this.frames.findIndex(one => one.post.id === start.postId) : -1;
|
|
249
|
+
this.at = found < 0 ? 0 : Math.min(found + start.index, this.frames.length - 1);
|
|
250
|
+
this.loaded = true;
|
|
251
|
+
if (saver && !this.frames.length)
|
|
252
|
+
return this.close();
|
|
253
|
+
this.play(!!this.options.playing || !!saver);
|
|
254
|
+
}
|
|
255
|
+
play(on) {
|
|
256
|
+
this.playing = on;
|
|
257
|
+
this.schedule();
|
|
258
|
+
}
|
|
259
|
+
/** The next photo after the dwell, asked ahead so it is there when it turns. */
|
|
260
|
+
schedule() {
|
|
261
|
+
clearTimeout(this.timer);
|
|
262
|
+
if (!this.playing || this.frames.length < 2)
|
|
263
|
+
return;
|
|
264
|
+
const next = this.frames[(this.at + 1) % this.frames.length];
|
|
265
|
+
new Image().src = next.photo.fullpath;
|
|
266
|
+
this.timer = setTimeout(() => this.step(1), DWELL);
|
|
267
|
+
}
|
|
268
|
+
step(by) {
|
|
269
|
+
const count = this.frames.length;
|
|
270
|
+
if (!count)
|
|
271
|
+
return;
|
|
272
|
+
this.at = (this.at + by + count) % count;
|
|
273
|
+
this.schedule();
|
|
274
|
+
}
|
|
275
|
+
close() {
|
|
276
|
+
if (this.disposed)
|
|
277
|
+
return;
|
|
278
|
+
if (this.wholeScreen && document.fullscreenElement)
|
|
279
|
+
document.exitFullscreen().catch(() => { });
|
|
280
|
+
this.remove();
|
|
281
|
+
this.dispatchEvent(new CustomEvent('closed'));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
__decorate([
|
|
285
|
+
property({ type: Boolean, reflect: true }),
|
|
286
|
+
__metadata("design:type", Object)
|
|
287
|
+
], GalleryShow.prototype, "saver", void 0);
|
|
288
|
+
__decorate([
|
|
289
|
+
state(),
|
|
290
|
+
__metadata("design:type", Array)
|
|
291
|
+
], GalleryShow.prototype, "frames", void 0);
|
|
292
|
+
__decorate([
|
|
293
|
+
state(),
|
|
294
|
+
__metadata("design:type", Object)
|
|
295
|
+
], GalleryShow.prototype, "at", void 0);
|
|
296
|
+
__decorate([
|
|
297
|
+
state(),
|
|
298
|
+
__metadata("design:type", Object)
|
|
299
|
+
], GalleryShow.prototype, "playing", void 0);
|
|
300
|
+
__decorate([
|
|
301
|
+
state(),
|
|
302
|
+
__metadata("design:type", Object)
|
|
303
|
+
], GalleryShow.prototype, "loaded", void 0);
|
|
304
|
+
GalleryShow = __decorate([
|
|
305
|
+
customElement('gallery-show')
|
|
306
|
+
], GalleryShow);
|
|
307
|
+
export { GalleryShow };
|
|
308
|
+
/** What wakes the screen from the saver. */
|
|
309
|
+
const WAKE = ['pointermove', 'pointerdown', 'keydown', 'wheel', 'touchstart'];
|
|
310
|
+
function shuffle(list) {
|
|
311
|
+
for (let i = list.length - 1; i > 0; i--) {
|
|
312
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
313
|
+
[list[i], list[j]] = [list[j], list[i]];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=gallery-show.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gallery-show.js","sourceRoot":"","sources":["../../client/components/gallery-show.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The gallery's photos across the whole window, one at a time (ADR-0165 decision 9): seen large from a post, played as\n * a slideshow from the gallery, and the screen saver after the person has been away. One show for the three - what\n * differs is where it starts, whether it plays, and whether anything the person does closes it (the saver).\n */\nimport '@material/web/icon/icon.js'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\n\nimport { galleryApi, type Filter, type Photo, type Post } from '../utils/gallery-api.js'\nimport { day, t } from '../utils/gallery-words.js'\n\n/** How long one photo stays while it plays. */\nconst DWELL = 7000\n/** The most photos one show walks through - pages of the gallery's own size are asked until this. */\nconst MOST = 300\nconst PAGE = 60\n\ntype Frame = { post: Post; photo: Photo }\n\nexport type ShowOptions = {\n /** The posts the show walks through - the gallery's narrowing where it was opened. */\n filter: Filter\n /** Where it starts: a post and the photo in it. Without it, the first photo. */\n start?: { postId: string; index: number }\n /** Plays at once. */\n playing?: boolean\n /** The screen saver: plays in a random order, and closes at anything the person does. */\n saver?: boolean\n}\n\n@customElement('gallery-show')\nexport class GalleryShow extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n position: fixed;\n inset: 0;\n z-index: 10000;\n display: block;\n background: #000;\n color: #fff;\n user-select: none;\n }\n :host([saver]) {\n cursor: none;\n }\n img {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: contain;\n animation: appear 800ms ease-out;\n }\n @keyframes appear {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n .about {\n position: absolute;\n inset-inline: 0;\n bottom: 0;\n padding: 48px 24px 20px;\n background: linear-gradient(transparent, rgb(0 0 0 / 60%));\n font-size: 14px;\n line-height: 1.5;\n }\n .about .caption {\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n white-space: pre-wrap;\n font-size: 16px;\n }\n .about .who {\n opacity: 0.75;\n }\n .bar {\n position: absolute;\n top: 12px;\n inset-inline-end: 12px;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n .bar .count {\n padding: 0 8px;\n font-size: 13px;\n opacity: 0.8;\n }\n button {\n display: inline-flex;\n padding: 8px;\n border: 0;\n border-radius: 50%;\n background: rgb(0 0 0 / 45%);\n color: #fff;\n cursor: pointer;\n }\n button:hover {\n background: rgb(255 255 255 / 20%);\n }\n .turn {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n }\n .previous {\n inset-inline-start: 12px;\n }\n .next {\n inset-inline-end: 12px;\n }\n .empty {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0.8;\n }\n `\n\n /** Opens a show over the whole window, and the whole screen when the browser lets it. */\n static open(options: ShowOptions): GalleryShow {\n const show = document.createElement('gallery-show') as GalleryShow\n show.options = options\n document.body.appendChild(show)\n return show\n }\n\n @property({ type: Boolean, reflect: true }) saver = false\n options!: ShowOptions\n\n @state() private frames: Frame[] = []\n @state() private at = 0\n @state() private playing = false\n @state() private loaded = false\n\n private timer?: ReturnType<typeof setTimeout>\n private wholeScreen = false\n private disposed = false\n\n render() {\n const frame = this.frames[this.at]\n if (!frame)\n return this.loaded\n ? html`<div class=\"empty\">${t('gallery.text.show-empty')}</div>${this.saver ? nothing : this.renderBar()}`\n : nothing\n const { post, photo } = frame\n return html`\n ${/* A new element for each photo, so it fades in. */ [photo.id].map(() => html`<img src=${photo.fullpath} alt=${post.caption ?? ''} />`)}\n <div class=\"about\">\n ${post.caption ? html`<div class=\"caption\">${post.caption}</div>` : nothing}\n <div class=\"who\">${post.creator?.name ?? ''} · ${day(post.takenAt)}${post.event ? ` · ${post.event.name}` : ''}</div>\n </div>\n ${\n this.saver\n ? nothing\n : html`${this.renderBar()}\n ${this.frames.length > 1\n ? html`<button class=\"turn previous\" aria-label=${t('gallery.button.previous')} @click=${() => this.step(-1)}>\n <md-icon>chevron_left</md-icon>\n </button>\n <button class=\"turn next\" aria-label=${t('gallery.button.next')} @click=${() => this.step(1)}>\n <md-icon>chevron_right</md-icon>\n </button>`\n : nothing}`\n }\n `\n }\n\n private renderBar() {\n return html`<div class=\"bar\">\n ${this.frames.length ? html`<span class=\"count\">${this.at + 1} / ${this.frames.length}</span>` : nothing}\n ${\n this.frames.length > 1\n ? html`<button\n aria-label=${t(this.playing ? 'gallery.button.pause' : 'gallery.button.play')}\n title=${t(this.playing ? 'gallery.button.pause' : 'gallery.button.play')}\n @click=${() => this.play(!this.playing)}\n >\n <md-icon>${this.playing ? 'pause' : 'play_arrow'}</md-icon>\n </button>`\n : nothing\n }\n <button aria-label=${t('gallery.button.close')} title=${t('gallery.button.close')} @click=${() => this.close()}>\n <md-icon>close</md-icon>\n </button>\n </div>`\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.saver = !!this.options?.saver\n window.addEventListener('keydown', this.onKey, true)\n document.addEventListener('fullscreenchange', this.onFullscreen)\n if (this.saver) for (const name of WAKE) window.addEventListener(name, this.onWake, true)\n /* The whole screen needs the press that opened it - the saver has none, and covers the window only. */\n if (!this.saver && this.requestFullscreen)\n this.requestFullscreen()\n .then(() => (this.wholeScreen = true))\n .catch(() => {})\n this.load()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.disposed = true\n clearTimeout(this.timer)\n window.removeEventListener('keydown', this.onKey, true)\n document.removeEventListener('fullscreenchange', this.onFullscreen)\n for (const name of WAKE) window.removeEventListener(name, this.onWake, true)\n }\n\n private async load() {\n const { filter, start, saver } = this.options\n const frames: Frame[] = []\n try {\n for (let skip = 0; frames.length < MOST; skip += PAGE) {\n const page = await galleryApi.posts(filter, skip, PAGE)\n for (const post of page.items) if (!post.hiddenAt) for (const photo of post.photos) frames.push({ post, photo })\n if (this.disposed || skip + PAGE >= page.total) break\n }\n } catch {\n /* The show has what came; a saver with nothing closes itself below. */\n }\n if (this.disposed) return\n if (saver) shuffle(frames)\n this.frames = frames.slice(0, MOST)\n const found = start ? this.frames.findIndex(one => one.post.id === start.postId) : -1\n this.at = found < 0 ? 0 : Math.min(found + start!.index, this.frames.length - 1)\n this.loaded = true\n if (saver && !this.frames.length) return this.close()\n this.play(!!this.options.playing || !!saver)\n }\n\n private play(on: boolean) {\n this.playing = on\n this.schedule()\n }\n\n /** The next photo after the dwell, asked ahead so it is there when it turns. */\n private schedule() {\n clearTimeout(this.timer)\n if (!this.playing || this.frames.length < 2) return\n const next = this.frames[(this.at + 1) % this.frames.length]\n new Image().src = next.photo.fullpath\n this.timer = setTimeout(() => this.step(1), DWELL)\n }\n\n private step(by: number) {\n const count = this.frames.length\n if (!count) return\n this.at = (this.at + by + count) % count\n this.schedule()\n }\n\n private onKey = (e: KeyboardEvent) => {\n if (this.saver) return\n const handled: Record<string, () => void> = {\n ArrowLeft: () => this.step(-1),\n ArrowRight: () => this.step(1),\n ' ': () => this.play(!this.playing),\n Escape: () => this.close()\n }\n const act = handled[e.key]\n if (!act) return\n e.preventDefault()\n e.stopPropagation()\n act()\n }\n\n /** Leaving the whole screen (Esc there is the browser's) leaves the show too. */\n private onFullscreen = () => {\n if (this.wholeScreen && !document.fullscreenElement) this.close()\n }\n\n private onWake = (e: Event) => {\n /* The pointer resting on the screen still says it moved, by a pixel or so - a real move is farther. */\n if (e.type === 'pointermove' && Math.abs((e as PointerEvent).movementX) + Math.abs((e as PointerEvent).movementY) < 4)\n return\n e.preventDefault()\n e.stopPropagation()\n this.close()\n }\n\n close() {\n if (this.disposed) return\n if (this.wholeScreen && document.fullscreenElement) document.exitFullscreen().catch(() => {})\n this.remove()\n this.dispatchEvent(new CustomEvent('closed'))\n }\n}\n\n/** What wakes the screen from the saver. */\nconst WAKE = ['pointermove', 'pointerdown', 'keydown', 'wheel', 'touchstart']\n\nfunction shuffle<T>(list: T[]) {\n for (let i = list.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[list[i], list[j]] = [list[j], list[i]]\n }\n}\n"]}
|
|
@@ -20,8 +20,12 @@ export declare class GalleryUploader extends GalleryUploader_base {
|
|
|
20
20
|
private caption;
|
|
21
21
|
private tags;
|
|
22
22
|
private tagText;
|
|
23
|
+
/** Why the tag being typed was not taken - said under the tags, the text kept so it can be shortened. */
|
|
24
|
+
private tagRefusal;
|
|
23
25
|
private suggestions;
|
|
24
26
|
private chosenSpace;
|
|
27
|
+
/** A post of the own gallery is the person's alone until shared (decision 2) - off when posting. */
|
|
28
|
+
private shared;
|
|
25
29
|
private chosenEvent;
|
|
26
30
|
private takenOn;
|
|
27
31
|
private choices;
|
|
@@ -44,6 +48,7 @@ export declare class GalleryUploader extends GalleryUploader_base {
|
|
|
44
48
|
private chooseSpace;
|
|
45
49
|
private typeTag;
|
|
46
50
|
private tagKey;
|
|
51
|
+
/** Takes the typed tag, or says why not and keeps it - a longer tag was cut at 30 without a word (2026-10-30). */
|
|
47
52
|
private addTag;
|
|
48
53
|
private save;
|
|
49
54
|
private close;
|
|
@@ -21,6 +21,7 @@ import { shrunk, takenAtOf } from '../utils/photo-prep.js';
|
|
|
21
21
|
const MAX_PHOTOS = 20;
|
|
22
22
|
const MAX_CAPTION = 500;
|
|
23
23
|
const MAX_TAGS = 10;
|
|
24
|
+
const MAX_TAG = 30;
|
|
24
25
|
/* The events shown before 「more」 - the near ones are among them. */
|
|
25
26
|
const FIRST_CHOICES = 4;
|
|
26
27
|
/** yyyy-mm-dd of a date in the reader's own time. */
|
|
@@ -37,8 +38,12 @@ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement
|
|
|
37
38
|
this.caption = '';
|
|
38
39
|
this.tags = [];
|
|
39
40
|
this.tagText = '';
|
|
41
|
+
/** Why the tag being typed was not taken - said under the tags, the text kept so it can be shortened. */
|
|
42
|
+
this.tagRefusal = '';
|
|
40
43
|
this.suggestions = [];
|
|
41
44
|
this.chosenSpace = 'COMPANY';
|
|
45
|
+
/** A post of the own gallery is the person's alone until shared (decision 2) - off when posting. */
|
|
46
|
+
this.shared = false;
|
|
42
47
|
this.chosenEvent = null;
|
|
43
48
|
this.takenOn = dateText(new Date());
|
|
44
49
|
this.choices = [];
|
|
@@ -142,6 +147,10 @@ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement
|
|
|
142
147
|
font-size: var(--ui-t-small, 12px);
|
|
143
148
|
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
144
149
|
}
|
|
150
|
+
.refusal {
|
|
151
|
+
font-size: var(--ui-t-small, 12px);
|
|
152
|
+
color: var(--md-sys-color-error);
|
|
153
|
+
}
|
|
145
154
|
.chips {
|
|
146
155
|
display: flex;
|
|
147
156
|
flex-wrap: wrap;
|
|
@@ -287,6 +296,7 @@ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement
|
|
|
287
296
|
/>`
|
|
288
297
|
: nothing}
|
|
289
298
|
</div>
|
|
299
|
+
${this.tagRefusal ? html `<div class="refusal" role="alert">${this.tagRefusal}</div>` : nothing}
|
|
290
300
|
${this.suggestions.length
|
|
291
301
|
? html `<div class="chips suggest">
|
|
292
302
|
${this.suggestions.filter(one => !this.tags.some(tag => tag.toLowerCase() === one.tag.toLowerCase())).map(one => html `<button class="chip" @mousedown=${(e) => e.preventDefault()} @click=${() => this.addTag(one.tag)}>#${one.tag}</button>`)}
|
|
@@ -333,7 +343,18 @@ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement
|
|
|
333
343
|
: nothing}
|
|
334
344
|
</div>
|
|
335
345
|
</section>`
|
|
336
|
-
: html `<
|
|
346
|
+
: html `<section>
|
|
347
|
+
<h4>${t('gallery.field.shared')}</h4>
|
|
348
|
+
<div class="chips">
|
|
349
|
+
<button class="chip" aria-pressed=${String(!this.shared)} @click=${() => (this.shared = false)}>
|
|
350
|
+
<md-icon>lock</md-icon>${t('gallery.text.only-me')}
|
|
351
|
+
</button>
|
|
352
|
+
<button class="chip" aria-pressed=${String(this.shared)} @click=${() => (this.shared = true)}>
|
|
353
|
+
<md-icon>group</md-icon>${t('gallery.text.everyone')}
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
<div class="muted">${t(this.shared ? 'gallery.text.shared-note' : 'gallery.text.only-me-note')}</div>
|
|
357
|
+
</section>`}
|
|
337
358
|
</div>
|
|
338
359
|
<ox-popup-actions>
|
|
339
360
|
<md-text-button slot="cancel" ?disabled=${this.busy} @click=${() => this.close()}
|
|
@@ -362,6 +383,7 @@ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement
|
|
|
362
383
|
this.caption = post.caption ?? '';
|
|
363
384
|
this.tags = [...(post.tags ?? [])];
|
|
364
385
|
this.chosenSpace = post.space;
|
|
386
|
+
this.shared = post.shared;
|
|
365
387
|
this.chosenEvent = post.event?.id ?? null;
|
|
366
388
|
this.takenOn = dateText(new Date(post.takenAt));
|
|
367
389
|
}
|
|
@@ -465,20 +487,29 @@ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement
|
|
|
465
487
|
this.tags = this.tags.slice(0, -1);
|
|
466
488
|
}
|
|
467
489
|
}
|
|
490
|
+
/** Takes the typed tag, or says why not and keeps it - a longer tag was cut at 30 without a word (2026-10-30). */
|
|
468
491
|
addTag(raw) {
|
|
469
492
|
const tag = raw.replace(/[,]/g, ' ').trim().replace(/^#+/, '').replace(/\s+/g, ' ').trim();
|
|
493
|
+
if (tag.length > MAX_TAG) {
|
|
494
|
+
this.tagRefusal = t('gallery.text.tag-too-long-typed', { max: MAX_TAG, length: tag.length });
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
this.tagRefusal = '';
|
|
470
498
|
this.tagText = '';
|
|
471
499
|
if (!tag || this.tags.length >= MAX_TAGS || this.tags.some(one => one.toLowerCase() === tag.toLowerCase()))
|
|
472
|
-
return;
|
|
473
|
-
this.tags = [...this.tags, tag
|
|
500
|
+
return true;
|
|
501
|
+
this.tags = [...this.tags, tag];
|
|
502
|
+
return true;
|
|
474
503
|
}
|
|
475
504
|
async save() {
|
|
476
|
-
this.addTag(this.tagText)
|
|
505
|
+
if (!this.addTag(this.tagText))
|
|
506
|
+
return;
|
|
477
507
|
this.busy = true;
|
|
478
508
|
try {
|
|
479
509
|
const files = await Promise.all(this.added.map(one => shrunk(one.file)));
|
|
480
510
|
const input = {
|
|
481
511
|
space: this.chosenSpace,
|
|
512
|
+
shared: this.chosenSpace === 'PERSONAL' && this.shared,
|
|
482
513
|
caption: this.caption.trim(),
|
|
483
514
|
takenAt: this.takenAt(),
|
|
484
515
|
tags: this.tags,
|
|
@@ -541,6 +572,10 @@ __decorate([
|
|
|
541
572
|
state(),
|
|
542
573
|
__metadata("design:type", Object)
|
|
543
574
|
], GalleryUploader.prototype, "tagText", void 0);
|
|
575
|
+
__decorate([
|
|
576
|
+
state(),
|
|
577
|
+
__metadata("design:type", Object)
|
|
578
|
+
], GalleryUploader.prototype, "tagRefusal", void 0);
|
|
544
579
|
__decorate([
|
|
545
580
|
state(),
|
|
546
581
|
__metadata("design:type", Array)
|
|
@@ -549,6 +584,10 @@ __decorate([
|
|
|
549
584
|
state(),
|
|
550
585
|
__metadata("design:type", String)
|
|
551
586
|
], GalleryUploader.prototype, "chosenSpace", void 0);
|
|
587
|
+
__decorate([
|
|
588
|
+
state(),
|
|
589
|
+
__metadata("design:type", Object)
|
|
590
|
+
], GalleryUploader.prototype, "shared", void 0);
|
|
552
591
|
__decorate([
|
|
553
592
|
state(),
|
|
554
593
|
__metadata("design:type", Object)
|