@things-factory/gallery 0.0.0-stage → 10.1.226

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 (102) hide show
  1. package/client/bootstrap.ts +16 -0
  2. package/client/components/gallery-forms.ts +188 -0
  3. package/client/components/gallery-uploader.ts +557 -0
  4. package/client/components/gallery-viewer.ts +397 -0
  5. package/client/index.ts +1 -0
  6. package/client/pages/gallery-page.ts +567 -0
  7. package/client/route.ts +8 -0
  8. package/client/tsconfig.json +13 -0
  9. package/client/utils/gallery-api.ts +157 -0
  10. package/client/utils/gallery-badge.ts +44 -0
  11. package/client/utils/gallery-words.ts +22 -0
  12. package/client/utils/photo-prep.ts +93 -0
  13. package/dist-client/bootstrap.d.ts +1 -0
  14. package/dist-client/bootstrap.js +16 -0
  15. package/dist-client/bootstrap.js.map +1 -0
  16. package/dist-client/components/gallery-forms.d.ts +31 -0
  17. package/dist-client/components/gallery-forms.js +223 -0
  18. package/dist-client/components/gallery-forms.js.map +1 -0
  19. package/dist-client/components/gallery-uploader.d.ts +51 -0
  20. package/dist-client/components/gallery-uploader.js +588 -0
  21. package/dist-client/components/gallery-uploader.js.map +1 -0
  22. package/dist-client/components/gallery-viewer.d.ts +30 -0
  23. package/dist-client/components/gallery-viewer.js +416 -0
  24. package/dist-client/components/gallery-viewer.js.map +1 -0
  25. package/dist-client/index.d.ts +1 -0
  26. package/dist-client/index.js +2 -0
  27. package/dist-client/index.js.map +1 -0
  28. package/dist-client/pages/gallery-page.d.ts +55 -0
  29. package/dist-client/pages/gallery-page.js +586 -0
  30. package/dist-client/pages/gallery-page.js.map +1 -0
  31. package/dist-client/route.d.ts +1 -0
  32. package/dist-client/route.js +9 -0
  33. package/dist-client/route.js.map +1 -0
  34. package/dist-client/tsconfig.tsbuildinfo +1 -0
  35. package/dist-client/utils/gallery-api.d.ts +95 -0
  36. package/dist-client/utils/gallery-api.js +46 -0
  37. package/dist-client/utils/gallery-api.js.map +1 -0
  38. package/dist-client/utils/gallery-badge.d.ts +4 -0
  39. package/dist-client/utils/gallery-badge.js +45 -0
  40. package/dist-client/utils/gallery-badge.js.map +1 -0
  41. package/dist-client/utils/gallery-words.d.ts +7 -0
  42. package/dist-client/utils/gallery-words.js +20 -0
  43. package/dist-client/utils/gallery-words.js.map +1 -0
  44. package/dist-client/utils/photo-prep.d.ts +10 -0
  45. package/dist-client/utils/photo-prep.js +102 -0
  46. package/dist-client/utils/photo-prep.js.map +1 -0
  47. package/dist-server/attachment-reader.d.ts +1 -0
  48. package/dist-server/attachment-reader.js +13 -0
  49. package/dist-server/attachment-reader.js.map +1 -0
  50. package/dist-server/index.d.ts +1 -0
  51. package/dist-server/index.js +11 -0
  52. package/dist-server/index.js.map +1 -0
  53. package/dist-server/role-tasks.d.ts +2 -0
  54. package/dist-server/role-tasks.js +15 -0
  55. package/dist-server/role-tasks.js.map +1 -0
  56. package/dist-server/service/gallery/gallery-resolver.d.ts +29 -0
  57. package/dist-server/service/gallery/gallery-resolver.js +602 -0
  58. package/dist-server/service/gallery/gallery-resolver.js.map +1 -0
  59. package/dist-server/service/gallery/gallery-rules.d.ts +88 -0
  60. package/dist-server/service/gallery/gallery-rules.js +181 -0
  61. package/dist-server/service/gallery/gallery-rules.js.map +1 -0
  62. package/dist-server/service/gallery/gallery-service.d.ts +37 -0
  63. package/dist-server/service/gallery/gallery-service.js +234 -0
  64. package/dist-server/service/gallery/gallery-service.js.map +1 -0
  65. package/dist-server/service/gallery/gallery-types.d.ts +62 -0
  66. package/dist-server/service/gallery/gallery-types.js +227 -0
  67. package/dist-server/service/gallery/gallery-types.js.map +1 -0
  68. package/dist-server/service/gallery/gallery.d.ts +83 -0
  69. package/dist-server/service/gallery/gallery.js +330 -0
  70. package/dist-server/service/gallery/gallery.js.map +1 -0
  71. package/dist-server/service/gallery/index.d.ts +4 -0
  72. package/dist-server/service/gallery/index.js +8 -0
  73. package/dist-server/service/gallery/index.js.map +1 -0
  74. package/dist-server/service/index.d.ts +7 -0
  75. package/dist-server/service/index.js +23 -0
  76. package/dist-server/service/index.js.map +1 -0
  77. package/dist-server/tsconfig.tsbuildinfo +1 -0
  78. package/package.json +48 -4
  79. package/server/attachment-reader.ts +14 -0
  80. package/server/index.ts +9 -0
  81. package/server/role-tasks.ts +14 -0
  82. package/server/service/gallery/gallery-resolver.ts +541 -0
  83. package/server/service/gallery/gallery-rules.ts +187 -0
  84. package/server/service/gallery/gallery-service.ts +254 -0
  85. package/server/service/gallery/gallery-types.ts +151 -0
  86. package/server/service/gallery/gallery.ts +266 -0
  87. package/server/service/gallery/index.ts +5 -0
  88. package/server/service/index.ts +19 -0
  89. package/server/tsconfig.json +11 -0
  90. package/test/gallery-persistence.test.ts +372 -0
  91. package/test/gallery-rules.test.ts +140 -0
  92. package/test/open-doors.test.ts +62 -0
  93. package/test/photo-prep.test.ts +61 -0
  94. package/test/translations.test.ts +68 -0
  95. package/things-factory.config.js +8 -0
  96. package/translations/en.json +91 -0
  97. package/translations/es.json +91 -0
  98. package/translations/ja.json +91 -0
  99. package/translations/ko.json +91 -0
  100. package/translations/ms.json +91 -0
  101. package/translations/zh.json +91 -0
  102. package/README.md +0 -3
@@ -0,0 +1,51 @@
1
+ import '@material/web/button/filled-button.js';
2
+ import '@material/web/button/text-button.js';
3
+ import '@material/web/icon/icon.js';
4
+ import '@operato/input/ox-input-file.js';
5
+ import '@operato/popup/ox-popup-actions.js';
6
+ import { LitElement } from 'lit';
7
+ import { type Post, type Space } from '../utils/gallery-api.js';
8
+ declare const GalleryUploader_base: (new (...args: any[]) => LitElement) & typeof LitElement;
9
+ export declare class GalleryUploader extends GalleryUploader_base {
10
+ static styles: import("lit").CSSResult[];
11
+ /** The post being changed - nothing when posting a new one. */
12
+ post?: Post;
13
+ /** The photos the page picked before the window opened. */
14
+ files: File[];
15
+ /** Where a new post goes - the screen it was opened from. */
16
+ space: Space;
17
+ eventId?: string | null;
18
+ private added;
19
+ private kept;
20
+ private caption;
21
+ private tags;
22
+ private tagText;
23
+ private suggestions;
24
+ private chosenSpace;
25
+ private chosenEvent;
26
+ private takenOn;
27
+ private choices;
28
+ private allChoices;
29
+ private progress?;
30
+ private busy;
31
+ private picker;
32
+ private camera;
33
+ private suggestTimer?;
34
+ render(): import("lit-html").TemplateResult<1>;
35
+ private choiceChip;
36
+ firstUpdated(): Promise<void>;
37
+ disconnectedCallback(): void;
38
+ private take;
39
+ private add;
40
+ private drop;
41
+ private takenAt;
42
+ private loadChoices;
43
+ private changeDay;
44
+ private chooseSpace;
45
+ private typeTag;
46
+ private tagKey;
47
+ private addTag;
48
+ private save;
49
+ private close;
50
+ }
51
+ export {};
@@ -0,0 +1,588 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ /*
3
+ * Posting photos (ADR-0165 decision 3) - and changing one's own post. Photos are all it needs; the caption, the tags
4
+ * and the event are there to add. When the photos were taken is read from the first one, and the events near that day
5
+ * are offered first - one press puts the post on one.
6
+ */
7
+ import '@material/web/button/filled-button.js';
8
+ import '@material/web/button/text-button.js';
9
+ import '@material/web/icon/icon.js';
10
+ import '@operato/input/ox-input-file.js';
11
+ import '@operato/popup/ox-popup-actions.js';
12
+ import { css, html, LitElement, nothing } from 'lit';
13
+ import { customElement, property, query, state } from 'lit/decorators.js';
14
+ import { i18next, localize } from '@operato/i18n';
15
+ import { notify } from '@operato/layout';
16
+ import { CommonPopupStyles, PopupBodyStyles } from '@operato/styles';
17
+ import { isMobileDevice } from '@operato/utils';
18
+ import { galleryApi, galleryChanged } from '../utils/gallery-api.js';
19
+ import { day, reasonOf, t } from '../utils/gallery-words.js';
20
+ import { shrunk, takenAtOf } from '../utils/photo-prep.js';
21
+ const MAX_PHOTOS = 20;
22
+ const MAX_CAPTION = 500;
23
+ const MAX_TAGS = 10;
24
+ /* The events shown before 「more」 - the near ones are among them. */
25
+ const FIRST_CHOICES = 4;
26
+ /** yyyy-mm-dd of a date in the reader's own time. */
27
+ const dateText = (value) => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
28
+ let GalleryUploader = class GalleryUploader extends localize(i18next)(LitElement) {
29
+ constructor() {
30
+ super(...arguments);
31
+ /** The photos the page picked before the window opened. */
32
+ this.files = [];
33
+ /** Where a new post goes - the screen it was opened from. */
34
+ this.space = 'COMPANY';
35
+ this.added = [];
36
+ this.kept = [];
37
+ this.caption = '';
38
+ this.tags = [];
39
+ this.tagText = '';
40
+ this.suggestions = [];
41
+ this.chosenSpace = 'COMPANY';
42
+ this.chosenEvent = null;
43
+ this.takenOn = dateText(new Date());
44
+ this.choices = [];
45
+ this.allChoices = false;
46
+ this.busy = false;
47
+ }
48
+ static { this.styles = [
49
+ CommonPopupStyles,
50
+ PopupBodyStyles,
51
+ css `
52
+ :host {
53
+ height: 100%;
54
+ }
55
+ .body {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: var(--spacing-large, 12px);
59
+ }
60
+ h4 {
61
+ margin: 0 0 var(--spacing-small, 4px);
62
+ font-size: var(--ui-t-small, 12px);
63
+ font-weight: 600;
64
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
65
+ }
66
+ .photos {
67
+ display: grid;
68
+ grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
69
+ gap: var(--spacing-small, 4px);
70
+ }
71
+ .photo {
72
+ position: relative;
73
+ aspect-ratio: 1;
74
+ border-radius: var(--border-radius, 4px);
75
+ overflow: hidden;
76
+ background: var(--md-sys-color-surface-container-high);
77
+ }
78
+ .photo img {
79
+ width: 100%;
80
+ height: 100%;
81
+ object-fit: cover;
82
+ display: block;
83
+ }
84
+ .photo button {
85
+ position: absolute;
86
+ top: 2px;
87
+ inset-inline-end: 2px;
88
+ display: inline-flex;
89
+ padding: 2px;
90
+ border: 0;
91
+ border-radius: 50%;
92
+ background: rgb(0 0 0 / 55%);
93
+ color: #fff;
94
+ cursor: pointer;
95
+ }
96
+ .photo button md-icon {
97
+ --md-icon-size: 16px;
98
+ }
99
+ .add {
100
+ display: flex;
101
+ flex-direction: column;
102
+ align-items: center;
103
+ justify-content: center;
104
+ gap: 2px;
105
+ aspect-ratio: 1;
106
+ border: 1px dashed var(--ui-line, var(--md-sys-color-outline));
107
+ border-radius: var(--border-radius, 4px);
108
+ background: none;
109
+ color: var(--md-sys-color-primary);
110
+ font: inherit;
111
+ font-size: var(--ui-t-small, 12px);
112
+ cursor: pointer;
113
+ }
114
+ .add[disabled] {
115
+ opacity: 0.4;
116
+ cursor: default;
117
+ }
118
+ ox-input-file {
119
+ display: contents;
120
+ }
121
+ textarea,
122
+ input {
123
+ box-sizing: border-box;
124
+ width: 100%;
125
+ font: inherit;
126
+ font-size: var(--ui-t-body, 14px);
127
+ padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
128
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
129
+ border-radius: var(--border-radius, 4px);
130
+ background: var(--md-sys-color-surface);
131
+ color: var(--md-sys-color-on-surface);
132
+ }
133
+ textarea {
134
+ min-height: 64px;
135
+ resize: vertical;
136
+ }
137
+ input[type='date'] {
138
+ width: auto;
139
+ }
140
+ .count {
141
+ text-align: end;
142
+ font-size: var(--ui-t-small, 12px);
143
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
144
+ }
145
+ .chips {
146
+ display: flex;
147
+ flex-wrap: wrap;
148
+ gap: var(--spacing-small, 4px);
149
+ align-items: center;
150
+ }
151
+ .chip {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ gap: 2px;
155
+ padding: 2px var(--spacing-medium, 8px);
156
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
157
+ border-radius: 999px;
158
+ background: none;
159
+ color: var(--md-sys-color-on-surface);
160
+ font: inherit;
161
+ font-size: var(--ui-t-small, 12px);
162
+ line-height: 1.6;
163
+ cursor: pointer;
164
+ }
165
+ .chip[aria-pressed='true'] {
166
+ border-color: var(--md-sys-color-primary);
167
+ background: var(--md-sys-color-primary-container);
168
+ color: var(--md-sys-color-on-primary-container);
169
+ }
170
+ .chip md-icon {
171
+ --md-icon-size: 14px;
172
+ }
173
+ .chip .near {
174
+ color: var(--md-sys-color-primary);
175
+ }
176
+ .tag-input {
177
+ flex: 1;
178
+ min-width: 120px;
179
+ width: auto;
180
+ border: 0;
181
+ padding: 2px 0;
182
+ background: none;
183
+ }
184
+ .tag-input:focus {
185
+ outline: none;
186
+ }
187
+ .tags {
188
+ padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
189
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
190
+ border-radius: var(--border-radius, 4px);
191
+ }
192
+ .suggest {
193
+ margin-top: var(--spacing-small, 4px);
194
+ }
195
+ .suggest .chip {
196
+ border-style: dashed;
197
+ }
198
+ .muted {
199
+ font-size: var(--ui-t-small, 12px);
200
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
201
+ }
202
+ .row {
203
+ display: flex;
204
+ flex-wrap: wrap;
205
+ gap: var(--spacing-large, 12px);
206
+ align-items: flex-end;
207
+ }
208
+ `
209
+ ]; }
210
+ render() {
211
+ const total = this.kept.length + this.added.length;
212
+ const full = total >= MAX_PHOTOS;
213
+ const company = this.chosenSpace === 'COMPANY';
214
+ const shown = this.allChoices ? this.choices : this.choices.slice(0, FIRST_CHOICES);
215
+ const chosen = this.choices.find(one => one.id === this.chosenEvent);
216
+ return html `<div class="body">
217
+ <section>
218
+ <div class="photos">
219
+ ${this.kept.map(one => html `<div class="photo">
220
+ <img src=${one.thumbnail || one.fullpath} alt="" />
221
+ <button
222
+ aria-label=${t('gallery.button.remove-photo')}
223
+ @click=${() => (this.kept = this.kept.filter(other => other !== one))}
224
+ >
225
+ <md-icon>close</md-icon>
226
+ </button>
227
+ </div>`)}
228
+ ${this.added.map(one => html `<div class="photo">
229
+ <img src=${one.url} alt="" />
230
+ <button aria-label=${t('gallery.button.remove-photo')} @click=${() => this.drop(one)}>
231
+ <md-icon>close</md-icon>
232
+ </button>
233
+ </div>`)}
234
+ <button class="add" ?disabled=${full || this.busy} @click=${() => this.picker?.pick()}>
235
+ <md-icon>add_photo_alternate</md-icon>${t('gallery.button.add-photos')}
236
+ </button>
237
+ ${isMobileDevice()
238
+ ? html `<button class="add" ?disabled=${full || this.busy} @click=${() => this.camera?.pick()}>
239
+ <md-icon>photo_camera</md-icon>${t('gallery.button.shoot-more')}
240
+ </button>`
241
+ : nothing}
242
+ </div>
243
+ <ox-input-file
244
+ id="pick"
245
+ quiet
246
+ outside-trigger
247
+ hide-filelist
248
+ multiple
249
+ accept="image/*"
250
+ .progress=${this.progress}
251
+ @change=${(e) => this.take(e)}
252
+ ></ox-input-file>
253
+ <ox-input-file
254
+ id="shoot"
255
+ quiet
256
+ outside-trigger
257
+ hide-filelist
258
+ accept="image/*"
259
+ capture="environment"
260
+ @change=${(e) => this.take(e)}
261
+ ></ox-input-file>
262
+ ${full ? html `<div class="muted">${t('gallery.text.photos-full', { count: MAX_PHOTOS })}</div>` : nothing}
263
+ </section>
264
+
265
+ <section>
266
+ <textarea
267
+ maxlength=${MAX_CAPTION}
268
+ placeholder=${t('gallery.text.caption-placeholder')}
269
+ .value=${this.caption}
270
+ @input=${(e) => (this.caption = e.target.value)}
271
+ ></textarea>
272
+ ${this.caption.length > MAX_CAPTION * 0.8 ? html `<div class="count">${this.caption.length} / ${MAX_CAPTION}</div>` : nothing}
273
+ </section>
274
+
275
+ <section>
276
+ <h4>${t('gallery.field.tags')}</h4>
277
+ <div class="chips tags">
278
+ ${this.tags.map(tag => html `<span class="chip">#${tag}<md-icon role="button" aria-label=${t('gallery.button.remove-tag')} @click=${() => (this.tags = this.tags.filter(one => one !== tag))}>close</md-icon></span>`)}
279
+ ${this.tags.length < MAX_TAGS
280
+ ? html `<input
281
+ class="tag-input"
282
+ .value=${this.tagText}
283
+ placeholder=${this.tags.length ? '' : t('gallery.text.tag-placeholder')}
284
+ @input=${(e) => this.typeTag(e.target.value)}
285
+ @keydown=${(e) => this.tagKey(e)}
286
+ @blur=${() => this.addTag(this.tagText)}
287
+ />`
288
+ : nothing}
289
+ </div>
290
+ ${this.suggestions.length
291
+ ? html `<div class="chips suggest">
292
+ ${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>`)}
293
+ </div>`
294
+ : nothing}
295
+ </section>
296
+
297
+ <section class="row">
298
+ <div>
299
+ <h4>${t('gallery.field.space')}</h4>
300
+ <div class="chips">
301
+ ${['COMPANY', 'PERSONAL'].map(space => html `<button
302
+ class="chip"
303
+ aria-pressed=${String(this.chosenSpace === space)}
304
+ @click=${() => this.chooseSpace(space)}
305
+ >
306
+ ${t(`gallery.space.${space}`)}
307
+ </button>`)}
308
+ </div>
309
+ </div>
310
+ <div>
311
+ <h4>${t('gallery.field.taken-on')}</h4>
312
+ <input type="date" .value=${this.takenOn} @change=${(e) => this.changeDay(e.target.value)} />
313
+ </div>
314
+ </section>
315
+
316
+ ${company
317
+ ? html `<section>
318
+ <h4>${t('gallery.field.event')}</h4>
319
+ <div class="chips">
320
+ <button
321
+ class="chip"
322
+ aria-pressed=${String(!this.chosenEvent)}
323
+ @click=${() => (this.chosenEvent = null)}
324
+ >
325
+ ${t('gallery.text.no-event')}
326
+ </button>
327
+ ${chosen && !shown.includes(chosen) ? this.choiceChip(chosen) : nothing}
328
+ ${shown.map(one => this.choiceChip(one))}
329
+ ${this.choices.length > FIRST_CHOICES && !this.allChoices
330
+ ? html `<button class="chip" @click=${() => (this.allChoices = true)}>
331
+ ${t('gallery.button.more-events', { count: this.choices.length - FIRST_CHOICES })}
332
+ </button>`
333
+ : nothing}
334
+ </div>
335
+ </section>`
336
+ : html `<div class="muted">${t('gallery.text.personal-note')}</div>`}
337
+ </div>
338
+ <ox-popup-actions>
339
+ <md-text-button slot="cancel" ?disabled=${this.busy} @click=${() => this.close()}
340
+ >${t('gallery.button.cancel')}</md-text-button
341
+ >
342
+ <md-filled-button slot="confirm" ?disabled=${this.busy || total < 1} @click=${() => this.save()}
343
+ >${this.post ? t('gallery.button.save') : t('gallery.button.post-count', { count: total })}</md-filled-button
344
+ >
345
+ </ox-popup-actions>`;
346
+ }
347
+ choiceChip(one) {
348
+ return html `<button
349
+ class="chip"
350
+ aria-pressed=${String(this.chosenEvent === one.id)}
351
+ @click=${() => (this.chosenEvent = one.id)}
352
+ >
353
+ ${one.near ? html `<md-icon class="near">event_available</md-icon>` : nothing}${one.name}<span class="muted"
354
+ >· ${day(one.startAt)}</span
355
+ >
356
+ </button>`;
357
+ }
358
+ async firstUpdated() {
359
+ const post = this.post;
360
+ if (post) {
361
+ this.kept = [...post.photos];
362
+ this.caption = post.caption ?? '';
363
+ this.tags = [...(post.tags ?? [])];
364
+ this.chosenSpace = post.space;
365
+ this.chosenEvent = post.event?.id ?? null;
366
+ this.takenOn = dateText(new Date(post.takenAt));
367
+ }
368
+ else {
369
+ this.chosenSpace = this.eventId ? 'COMPANY' : this.space;
370
+ this.chosenEvent = this.eventId ?? null;
371
+ }
372
+ await this.add(this.files ?? []);
373
+ await this.loadChoices();
374
+ }
375
+ disconnectedCallback() {
376
+ for (const one of this.added)
377
+ URL.revokeObjectURL(one.url);
378
+ clearTimeout(this.suggestTimer);
379
+ super.disconnectedCallback();
380
+ }
381
+ async take(e) {
382
+ const picked = [...(e.detail ?? [])];
383
+ /* The picker's own reset says `change` again, with nothing - that one is let go. */
384
+ if (!picked.length)
385
+ return;
386
+ e.target.reset?.();
387
+ const before = this.kept.length + this.added.length;
388
+ await this.add(picked);
389
+ /* The first photo says the day - the events near it come first. */
390
+ if (!before && !this.post)
391
+ await this.loadChoices();
392
+ }
393
+ async add(files) {
394
+ const room = MAX_PHOTOS - this.kept.length - this.added.length;
395
+ const photos = files
396
+ .filter(file => /^image\//i.test(file.type) || /\.(jpe?g|png|gif|webp|heic|heif)$/i.test(file.name))
397
+ .slice(0, Math.max(0, room));
398
+ if (photos.length < files.length)
399
+ notify({ level: 'warn', message: t('gallery.text.photos-skipped', { count: files.length - photos.length }) });
400
+ if (!photos.length)
401
+ return;
402
+ const first = !this.kept.length && !this.added.length && !this.post;
403
+ this.added = [...this.added, ...photos.map(file => ({ file, url: URL.createObjectURL(file) }))];
404
+ if (first) {
405
+ const takenAt = await takenAtOf(photos[0]);
406
+ if (takenAt)
407
+ this.takenOn = dateText(takenAt);
408
+ }
409
+ }
410
+ drop(one) {
411
+ URL.revokeObjectURL(one.url);
412
+ this.added = this.added.filter(other => other !== one);
413
+ }
414
+ takenAt() {
415
+ const [y, m, d] = this.takenOn.split('-').map(Number);
416
+ const now = new Date();
417
+ /* A day chosen without its hour keeps the hour it was posted - today's photo is not dated midnight. */
418
+ const at = y ? new Date(y, m - 1, d, now.getHours(), now.getMinutes()) : now;
419
+ return at.toISOString();
420
+ }
421
+ async loadChoices() {
422
+ try {
423
+ /* Offered, not chosen: the events near that day come first, and one press puts the post on one. */
424
+ this.choices = await galleryApi.eventChoices(this.takenAt());
425
+ }
426
+ catch {
427
+ this.choices = [];
428
+ }
429
+ }
430
+ async changeDay(value) {
431
+ if (!value)
432
+ return;
433
+ this.takenOn = value;
434
+ await this.loadChoices();
435
+ }
436
+ chooseSpace(space) {
437
+ this.chosenSpace = space;
438
+ if (space === 'PERSONAL')
439
+ this.chosenEvent = null;
440
+ }
441
+ typeTag(value) {
442
+ if (/[,\s]$/.test(value) && value.trim()) {
443
+ this.addTag(value);
444
+ return;
445
+ }
446
+ this.tagText = value;
447
+ clearTimeout(this.suggestTimer);
448
+ this.suggestTimer = setTimeout(async () => {
449
+ try {
450
+ this.suggestions = await galleryApi.tags(this.tagText);
451
+ }
452
+ catch {
453
+ this.suggestions = [];
454
+ }
455
+ }, 200);
456
+ }
457
+ tagKey(e) {
458
+ if (e.isComposing)
459
+ return;
460
+ if (e.key === 'Enter') {
461
+ e.preventDefault();
462
+ this.addTag(this.tagText);
463
+ }
464
+ else if (e.key === 'Backspace' && !this.tagText && this.tags.length) {
465
+ this.tags = this.tags.slice(0, -1);
466
+ }
467
+ }
468
+ addTag(raw) {
469
+ const tag = raw.replace(/[,]/g, ' ').trim().replace(/^#+/, '').replace(/\s+/g, ' ').trim();
470
+ this.tagText = '';
471
+ if (!tag || this.tags.length >= MAX_TAGS || this.tags.some(one => one.toLowerCase() === tag.toLowerCase()))
472
+ return;
473
+ this.tags = [...this.tags, tag.slice(0, 30)];
474
+ }
475
+ async save() {
476
+ this.addTag(this.tagText);
477
+ this.busy = true;
478
+ try {
479
+ const files = await Promise.all(this.added.map(one => shrunk(one.file)));
480
+ const input = {
481
+ space: this.chosenSpace,
482
+ caption: this.caption.trim(),
483
+ takenAt: this.takenAt(),
484
+ tags: this.tags,
485
+ eventId: this.chosenSpace === 'COMPANY' ? this.chosenEvent : null
486
+ };
487
+ const uploading = (fraction) => (this.progress = fraction);
488
+ const post = this.post
489
+ ? await galleryApi.update(this.post.id, input, files, this.post.photos.filter(one => !this.kept.includes(one)).map(one => one.id), uploading)
490
+ : await galleryApi.create(input, files, uploading);
491
+ notify({ message: this.post ? t('gallery.text.saved') : t('gallery.text.posted') });
492
+ galleryChanged();
493
+ this.dispatchEvent(new CustomEvent('gallery-posted', { detail: post, bubbles: true, composed: true }));
494
+ this.close();
495
+ }
496
+ catch (error) {
497
+ notify({ level: 'error', message: reasonOf(error) });
498
+ }
499
+ finally {
500
+ this.progress = undefined;
501
+ this.busy = false;
502
+ }
503
+ }
504
+ close() {
505
+ this.dispatchEvent(new CustomEvent('done', { bubbles: true, composed: true }));
506
+ }
507
+ };
508
+ __decorate([
509
+ property({ attribute: false }),
510
+ __metadata("design:type", Object)
511
+ ], GalleryUploader.prototype, "post", void 0);
512
+ __decorate([
513
+ property({ attribute: false }),
514
+ __metadata("design:type", Array)
515
+ ], GalleryUploader.prototype, "files", void 0);
516
+ __decorate([
517
+ property(),
518
+ __metadata("design:type", String)
519
+ ], GalleryUploader.prototype, "space", void 0);
520
+ __decorate([
521
+ property(),
522
+ __metadata("design:type", Object)
523
+ ], GalleryUploader.prototype, "eventId", void 0);
524
+ __decorate([
525
+ state(),
526
+ __metadata("design:type", Array)
527
+ ], GalleryUploader.prototype, "added", void 0);
528
+ __decorate([
529
+ state(),
530
+ __metadata("design:type", Array)
531
+ ], GalleryUploader.prototype, "kept", void 0);
532
+ __decorate([
533
+ state(),
534
+ __metadata("design:type", Object)
535
+ ], GalleryUploader.prototype, "caption", void 0);
536
+ __decorate([
537
+ state(),
538
+ __metadata("design:type", Array)
539
+ ], GalleryUploader.prototype, "tags", void 0);
540
+ __decorate([
541
+ state(),
542
+ __metadata("design:type", Object)
543
+ ], GalleryUploader.prototype, "tagText", void 0);
544
+ __decorate([
545
+ state(),
546
+ __metadata("design:type", Array)
547
+ ], GalleryUploader.prototype, "suggestions", void 0);
548
+ __decorate([
549
+ state(),
550
+ __metadata("design:type", String)
551
+ ], GalleryUploader.prototype, "chosenSpace", void 0);
552
+ __decorate([
553
+ state(),
554
+ __metadata("design:type", Object)
555
+ ], GalleryUploader.prototype, "chosenEvent", void 0);
556
+ __decorate([
557
+ state(),
558
+ __metadata("design:type", Object)
559
+ ], GalleryUploader.prototype, "takenOn", void 0);
560
+ __decorate([
561
+ state(),
562
+ __metadata("design:type", Array)
563
+ ], GalleryUploader.prototype, "choices", void 0);
564
+ __decorate([
565
+ state(),
566
+ __metadata("design:type", Object)
567
+ ], GalleryUploader.prototype, "allChoices", void 0);
568
+ __decorate([
569
+ state(),
570
+ __metadata("design:type", Number)
571
+ ], GalleryUploader.prototype, "progress", void 0);
572
+ __decorate([
573
+ state(),
574
+ __metadata("design:type", Object)
575
+ ], GalleryUploader.prototype, "busy", void 0);
576
+ __decorate([
577
+ query('#pick'),
578
+ __metadata("design:type", Object)
579
+ ], GalleryUploader.prototype, "picker", void 0);
580
+ __decorate([
581
+ query('#shoot'),
582
+ __metadata("design:type", Object)
583
+ ], GalleryUploader.prototype, "camera", void 0);
584
+ GalleryUploader = __decorate([
585
+ customElement('gallery-uploader')
586
+ ], GalleryUploader);
587
+ export { GalleryUploader };
588
+ //# sourceMappingURL=gallery-uploader.js.map