@things-factory/gallery 10.1.234 → 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-saver-setting-let.ts +78 -0
- package/client/components/gallery-show.ts +312 -0
- package/client/components/gallery-uploader.ts +16 -1
- 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-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 +2 -0
- package/dist-client/components/gallery-uploader.js +20 -1
- 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 +11 -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 +5 -4
- package/server/service/gallery/gallery-resolver.ts +9 -2
- 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/translations/en.json +15 -2
- package/translations/es.json +15 -2
- package/translations/ja.json +15 -2
- package/translations/ko.json +15 -2
- package/translations/ms.json +15 -2
- package/translations/zh.json +15 -2
package/client/bootstrap.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { html } from 'lit'
|
|
2
|
+
|
|
1
3
|
import { navigate, store } from '@operato/shell'
|
|
2
4
|
import { ADD_USER_MENU } from '@things-factory/apptool-base/client'
|
|
5
|
+
import { ADD_SETTING } from '@things-factory/setting-base/dist-client'
|
|
6
|
+
|
|
7
|
+
import './components/gallery-saver-setting-let.js'
|
|
8
|
+
import { watchForSaver } from './utils/gallery-saver.js'
|
|
3
9
|
|
|
4
10
|
/* One's own gallery is in the menu the header's avatar opens, right after the profile (ADR-0165 decision 6). */
|
|
5
11
|
export default function bootstrap() {
|
|
@@ -13,4 +19,10 @@ export default function bootstrap() {
|
|
|
13
19
|
action: () => navigate('gallery-page?tab=mine')
|
|
14
20
|
}
|
|
15
21
|
})
|
|
22
|
+
/* The screen saver is the device's, so it is set among one's own settings (ADR-0165 decision 9). */
|
|
23
|
+
store.dispatch({
|
|
24
|
+
type: ADD_SETTING,
|
|
25
|
+
setting: { seq: 30, template: html`<gallery-saver-setting-let></gallery-saver-setting-let>` }
|
|
26
|
+
})
|
|
27
|
+
watchForSaver()
|
|
16
28
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Settings › the gallery as the screen saver on this device (ADR-0165 decision 9): off, or after a few quiet minutes.
|
|
3
|
+
*/
|
|
4
|
+
import '@material/web/radio/radio.js'
|
|
5
|
+
|
|
6
|
+
import { css, html, LitElement } from 'lit'
|
|
7
|
+
import { customElement, state } from 'lit/decorators.js'
|
|
8
|
+
|
|
9
|
+
import { i18next, localize } from '@operato/i18n'
|
|
10
|
+
import { notify } from '@operato/layout'
|
|
11
|
+
|
|
12
|
+
import { SAVER_MINUTES, saverMinutes, setSaverMinutes } from '../utils/gallery-saver.js'
|
|
13
|
+
import { reasonOf, t } from '../utils/gallery-words.js'
|
|
14
|
+
|
|
15
|
+
@customElement('gallery-saver-setting-let')
|
|
16
|
+
export class GallerySaverSettingLet extends localize(i18next)(LitElement) {
|
|
17
|
+
static styles = css`
|
|
18
|
+
div[slot='content'] {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--spacing-medium, 8px);
|
|
22
|
+
}
|
|
23
|
+
.choices {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-wrap: wrap;
|
|
26
|
+
gap: var(--spacing-small, 4px) var(--spacing-large, 12px);
|
|
27
|
+
}
|
|
28
|
+
label {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 6px;
|
|
31
|
+
align-items: center;
|
|
32
|
+
font: var(--label-font);
|
|
33
|
+
color: var(--md-sys-color-on-surface);
|
|
34
|
+
}
|
|
35
|
+
.note {
|
|
36
|
+
margin: 0;
|
|
37
|
+
font-size: var(--ui-t-small, 12px);
|
|
38
|
+
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
39
|
+
}
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
@state() private minutes = 0
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
return html`<setting-let>
|
|
46
|
+
<span slot="title">${t('gallery.title.saver')}</span>
|
|
47
|
+
<div slot="content">
|
|
48
|
+
<div class="choices" role="radiogroup">
|
|
49
|
+
${SAVER_MINUTES.map(
|
|
50
|
+
minutes => html`<label>
|
|
51
|
+
<md-radio
|
|
52
|
+
name="gallery-saver"
|
|
53
|
+
value=${String(minutes)}
|
|
54
|
+
?checked=${this.minutes === minutes}
|
|
55
|
+
@change=${() => this.choose(minutes)}
|
|
56
|
+
></md-radio>
|
|
57
|
+
${minutes ? t('gallery.text.saver-after', { minutes }) : t('gallery.text.saver-off')}
|
|
58
|
+
</label>`
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
<p class="note">${t('gallery.text.saver-note')}</p>
|
|
62
|
+
</div>
|
|
63
|
+
</setting-let>`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async firstUpdated() {
|
|
67
|
+
this.minutes = await saverMinutes()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async choose(minutes: number) {
|
|
71
|
+
try {
|
|
72
|
+
await setSaverMinutes(minutes)
|
|
73
|
+
this.minutes = minutes
|
|
74
|
+
} catch (error) {
|
|
75
|
+
notify({ level: 'error', message: reasonOf(error) })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The gallery's photos across the whole window, one at a time (ADR-0165 decision 9): seen large from a post, played as
|
|
3
|
+
* a slideshow from the gallery, and the screen saver after the person has been away. One show for the three - what
|
|
4
|
+
* differs is where it starts, whether it plays, and whether anything the person does closes it (the saver).
|
|
5
|
+
*/
|
|
6
|
+
import '@material/web/icon/icon.js'
|
|
7
|
+
|
|
8
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
9
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
10
|
+
|
|
11
|
+
import { i18next, localize } from '@operato/i18n'
|
|
12
|
+
|
|
13
|
+
import { galleryApi, type Filter, type Photo, type Post } from '../utils/gallery-api.js'
|
|
14
|
+
import { day, t } from '../utils/gallery-words.js'
|
|
15
|
+
|
|
16
|
+
/** How long one photo stays while it plays. */
|
|
17
|
+
const DWELL = 7000
|
|
18
|
+
/** The most photos one show walks through - pages of the gallery's own size are asked until this. */
|
|
19
|
+
const MOST = 300
|
|
20
|
+
const PAGE = 60
|
|
21
|
+
|
|
22
|
+
type Frame = { post: Post; photo: Photo }
|
|
23
|
+
|
|
24
|
+
export type ShowOptions = {
|
|
25
|
+
/** The posts the show walks through - the gallery's narrowing where it was opened. */
|
|
26
|
+
filter: Filter
|
|
27
|
+
/** Where it starts: a post and the photo in it. Without it, the first photo. */
|
|
28
|
+
start?: { postId: string; index: number }
|
|
29
|
+
/** Plays at once. */
|
|
30
|
+
playing?: boolean
|
|
31
|
+
/** The screen saver: plays in a random order, and closes at anything the person does. */
|
|
32
|
+
saver?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@customElement('gallery-show')
|
|
36
|
+
export class GalleryShow extends localize(i18next)(LitElement) {
|
|
37
|
+
static styles = css`
|
|
38
|
+
:host {
|
|
39
|
+
position: fixed;
|
|
40
|
+
inset: 0;
|
|
41
|
+
z-index: 10000;
|
|
42
|
+
display: block;
|
|
43
|
+
background: #000;
|
|
44
|
+
color: #fff;
|
|
45
|
+
user-select: none;
|
|
46
|
+
}
|
|
47
|
+
:host([saver]) {
|
|
48
|
+
cursor: none;
|
|
49
|
+
}
|
|
50
|
+
img {
|
|
51
|
+
position: absolute;
|
|
52
|
+
inset: 0;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 100%;
|
|
55
|
+
object-fit: contain;
|
|
56
|
+
animation: appear 800ms ease-out;
|
|
57
|
+
}
|
|
58
|
+
@keyframes appear {
|
|
59
|
+
from {
|
|
60
|
+
opacity: 0;
|
|
61
|
+
}
|
|
62
|
+
to {
|
|
63
|
+
opacity: 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
.about {
|
|
67
|
+
position: absolute;
|
|
68
|
+
inset-inline: 0;
|
|
69
|
+
bottom: 0;
|
|
70
|
+
padding: 48px 24px 20px;
|
|
71
|
+
background: linear-gradient(transparent, rgb(0 0 0 / 60%));
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
line-height: 1.5;
|
|
74
|
+
}
|
|
75
|
+
.about .caption {
|
|
76
|
+
display: -webkit-box;
|
|
77
|
+
-webkit-line-clamp: 2;
|
|
78
|
+
-webkit-box-orient: vertical;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
white-space: pre-wrap;
|
|
81
|
+
font-size: 16px;
|
|
82
|
+
}
|
|
83
|
+
.about .who {
|
|
84
|
+
opacity: 0.75;
|
|
85
|
+
}
|
|
86
|
+
.bar {
|
|
87
|
+
position: absolute;
|
|
88
|
+
top: 12px;
|
|
89
|
+
inset-inline-end: 12px;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 8px;
|
|
93
|
+
}
|
|
94
|
+
.bar .count {
|
|
95
|
+
padding: 0 8px;
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
opacity: 0.8;
|
|
98
|
+
}
|
|
99
|
+
button {
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
padding: 8px;
|
|
102
|
+
border: 0;
|
|
103
|
+
border-radius: 50%;
|
|
104
|
+
background: rgb(0 0 0 / 45%);
|
|
105
|
+
color: #fff;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
}
|
|
108
|
+
button:hover {
|
|
109
|
+
background: rgb(255 255 255 / 20%);
|
|
110
|
+
}
|
|
111
|
+
.turn {
|
|
112
|
+
position: absolute;
|
|
113
|
+
top: 50%;
|
|
114
|
+
transform: translateY(-50%);
|
|
115
|
+
}
|
|
116
|
+
.previous {
|
|
117
|
+
inset-inline-start: 12px;
|
|
118
|
+
}
|
|
119
|
+
.next {
|
|
120
|
+
inset-inline-end: 12px;
|
|
121
|
+
}
|
|
122
|
+
.empty {
|
|
123
|
+
position: absolute;
|
|
124
|
+
inset: 0;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
opacity: 0.8;
|
|
129
|
+
}
|
|
130
|
+
`
|
|
131
|
+
|
|
132
|
+
/** Opens a show over the whole window, and the whole screen when the browser lets it. */
|
|
133
|
+
static open(options: ShowOptions): GalleryShow {
|
|
134
|
+
const show = document.createElement('gallery-show') as GalleryShow
|
|
135
|
+
show.options = options
|
|
136
|
+
document.body.appendChild(show)
|
|
137
|
+
return show
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@property({ type: Boolean, reflect: true }) saver = false
|
|
141
|
+
options!: ShowOptions
|
|
142
|
+
|
|
143
|
+
@state() private frames: Frame[] = []
|
|
144
|
+
@state() private at = 0
|
|
145
|
+
@state() private playing = false
|
|
146
|
+
@state() private loaded = false
|
|
147
|
+
|
|
148
|
+
private timer?: ReturnType<typeof setTimeout>
|
|
149
|
+
private wholeScreen = false
|
|
150
|
+
private disposed = false
|
|
151
|
+
|
|
152
|
+
render() {
|
|
153
|
+
const frame = this.frames[this.at]
|
|
154
|
+
if (!frame)
|
|
155
|
+
return this.loaded
|
|
156
|
+
? html`<div class="empty">${t('gallery.text.show-empty')}</div>${this.saver ? nothing : this.renderBar()}`
|
|
157
|
+
: nothing
|
|
158
|
+
const { post, photo } = frame
|
|
159
|
+
return html`
|
|
160
|
+
${/* A new element for each photo, so it fades in. */ [photo.id].map(() => html`<img src=${photo.fullpath} alt=${post.caption ?? ''} />`)}
|
|
161
|
+
<div class="about">
|
|
162
|
+
${post.caption ? html`<div class="caption">${post.caption}</div>` : nothing}
|
|
163
|
+
<div class="who">${post.creator?.name ?? ''} · ${day(post.takenAt)}${post.event ? ` · ${post.event.name}` : ''}</div>
|
|
164
|
+
</div>
|
|
165
|
+
${
|
|
166
|
+
this.saver
|
|
167
|
+
? nothing
|
|
168
|
+
: html`${this.renderBar()}
|
|
169
|
+
${this.frames.length > 1
|
|
170
|
+
? html`<button class="turn previous" aria-label=${t('gallery.button.previous')} @click=${() => this.step(-1)}>
|
|
171
|
+
<md-icon>chevron_left</md-icon>
|
|
172
|
+
</button>
|
|
173
|
+
<button class="turn next" aria-label=${t('gallery.button.next')} @click=${() => this.step(1)}>
|
|
174
|
+
<md-icon>chevron_right</md-icon>
|
|
175
|
+
</button>`
|
|
176
|
+
: nothing}`
|
|
177
|
+
}
|
|
178
|
+
`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private renderBar() {
|
|
182
|
+
return html`<div class="bar">
|
|
183
|
+
${this.frames.length ? html`<span class="count">${this.at + 1} / ${this.frames.length}</span>` : nothing}
|
|
184
|
+
${
|
|
185
|
+
this.frames.length > 1
|
|
186
|
+
? html`<button
|
|
187
|
+
aria-label=${t(this.playing ? 'gallery.button.pause' : 'gallery.button.play')}
|
|
188
|
+
title=${t(this.playing ? 'gallery.button.pause' : 'gallery.button.play')}
|
|
189
|
+
@click=${() => this.play(!this.playing)}
|
|
190
|
+
>
|
|
191
|
+
<md-icon>${this.playing ? 'pause' : 'play_arrow'}</md-icon>
|
|
192
|
+
</button>`
|
|
193
|
+
: nothing
|
|
194
|
+
}
|
|
195
|
+
<button aria-label=${t('gallery.button.close')} title=${t('gallery.button.close')} @click=${() => this.close()}>
|
|
196
|
+
<md-icon>close</md-icon>
|
|
197
|
+
</button>
|
|
198
|
+
</div>`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
connectedCallback() {
|
|
202
|
+
super.connectedCallback()
|
|
203
|
+
this.saver = !!this.options?.saver
|
|
204
|
+
window.addEventListener('keydown', this.onKey, true)
|
|
205
|
+
document.addEventListener('fullscreenchange', this.onFullscreen)
|
|
206
|
+
if (this.saver) for (const name of WAKE) window.addEventListener(name, this.onWake, true)
|
|
207
|
+
/* The whole screen needs the press that opened it - the saver has none, and covers the window only. */
|
|
208
|
+
if (!this.saver && this.requestFullscreen)
|
|
209
|
+
this.requestFullscreen()
|
|
210
|
+
.then(() => (this.wholeScreen = true))
|
|
211
|
+
.catch(() => {})
|
|
212
|
+
this.load()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
disconnectedCallback() {
|
|
216
|
+
super.disconnectedCallback()
|
|
217
|
+
this.disposed = true
|
|
218
|
+
clearTimeout(this.timer)
|
|
219
|
+
window.removeEventListener('keydown', this.onKey, true)
|
|
220
|
+
document.removeEventListener('fullscreenchange', this.onFullscreen)
|
|
221
|
+
for (const name of WAKE) window.removeEventListener(name, this.onWake, true)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async load() {
|
|
225
|
+
const { filter, start, saver } = this.options
|
|
226
|
+
const frames: Frame[] = []
|
|
227
|
+
try {
|
|
228
|
+
for (let skip = 0; frames.length < MOST; skip += PAGE) {
|
|
229
|
+
const page = await galleryApi.posts(filter, skip, PAGE)
|
|
230
|
+
for (const post of page.items) if (!post.hiddenAt) for (const photo of post.photos) frames.push({ post, photo })
|
|
231
|
+
if (this.disposed || skip + PAGE >= page.total) break
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
/* The show has what came; a saver with nothing closes itself below. */
|
|
235
|
+
}
|
|
236
|
+
if (this.disposed) return
|
|
237
|
+
if (saver) shuffle(frames)
|
|
238
|
+
this.frames = frames.slice(0, MOST)
|
|
239
|
+
const found = start ? this.frames.findIndex(one => one.post.id === start.postId) : -1
|
|
240
|
+
this.at = found < 0 ? 0 : Math.min(found + start!.index, this.frames.length - 1)
|
|
241
|
+
this.loaded = true
|
|
242
|
+
if (saver && !this.frames.length) return this.close()
|
|
243
|
+
this.play(!!this.options.playing || !!saver)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private play(on: boolean) {
|
|
247
|
+
this.playing = on
|
|
248
|
+
this.schedule()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The next photo after the dwell, asked ahead so it is there when it turns. */
|
|
252
|
+
private schedule() {
|
|
253
|
+
clearTimeout(this.timer)
|
|
254
|
+
if (!this.playing || this.frames.length < 2) return
|
|
255
|
+
const next = this.frames[(this.at + 1) % this.frames.length]
|
|
256
|
+
new Image().src = next.photo.fullpath
|
|
257
|
+
this.timer = setTimeout(() => this.step(1), DWELL)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private step(by: number) {
|
|
261
|
+
const count = this.frames.length
|
|
262
|
+
if (!count) return
|
|
263
|
+
this.at = (this.at + by + count) % count
|
|
264
|
+
this.schedule()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private onKey = (e: KeyboardEvent) => {
|
|
268
|
+
if (this.saver) return
|
|
269
|
+
const handled: Record<string, () => void> = {
|
|
270
|
+
ArrowLeft: () => this.step(-1),
|
|
271
|
+
ArrowRight: () => this.step(1),
|
|
272
|
+
' ': () => this.play(!this.playing),
|
|
273
|
+
Escape: () => this.close()
|
|
274
|
+
}
|
|
275
|
+
const act = handled[e.key]
|
|
276
|
+
if (!act) return
|
|
277
|
+
e.preventDefault()
|
|
278
|
+
e.stopPropagation()
|
|
279
|
+
act()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Leaving the whole screen (Esc there is the browser's) leaves the show too. */
|
|
283
|
+
private onFullscreen = () => {
|
|
284
|
+
if (this.wholeScreen && !document.fullscreenElement) this.close()
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private onWake = (e: Event) => {
|
|
288
|
+
/* The pointer resting on the screen still says it moved, by a pixel or so - a real move is farther. */
|
|
289
|
+
if (e.type === 'pointermove' && Math.abs((e as PointerEvent).movementX) + Math.abs((e as PointerEvent).movementY) < 4)
|
|
290
|
+
return
|
|
291
|
+
e.preventDefault()
|
|
292
|
+
e.stopPropagation()
|
|
293
|
+
this.close()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
close() {
|
|
297
|
+
if (this.disposed) return
|
|
298
|
+
if (this.wholeScreen && document.fullscreenElement) document.exitFullscreen().catch(() => {})
|
|
299
|
+
this.remove()
|
|
300
|
+
this.dispatchEvent(new CustomEvent('closed'))
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** What wakes the screen from the saver. */
|
|
305
|
+
const WAKE = ['pointermove', 'pointerdown', 'keydown', 'wheel', 'touchstart']
|
|
306
|
+
|
|
307
|
+
function shuffle<T>(list: T[]) {
|
|
308
|
+
for (let i = list.length - 1; i > 0; i--) {
|
|
309
|
+
const j = Math.floor(Math.random() * (i + 1))
|
|
310
|
+
;[list[i], list[j]] = [list[j], list[i]]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -227,6 +227,8 @@ export class GalleryUploader extends localize(i18next)(LitElement) {
|
|
|
227
227
|
@state() private tagRefusal = ''
|
|
228
228
|
@state() private suggestions: { tag: string; count: number }[] = []
|
|
229
229
|
@state() private chosenSpace: Space = 'COMPANY'
|
|
230
|
+
/** A post of the own gallery is the person's alone until shared (decision 2) - off when posting. */
|
|
231
|
+
@state() private shared = false
|
|
230
232
|
@state() private chosenEvent: string | null = null
|
|
231
233
|
@state() private takenOn = dateText(new Date())
|
|
232
234
|
@state() private choices: EventChoice[] = []
|
|
@@ -384,7 +386,18 @@ export class GalleryUploader extends localize(i18next)(LitElement) {
|
|
|
384
386
|
}
|
|
385
387
|
</div>
|
|
386
388
|
</section>`
|
|
387
|
-
: html`<
|
|
389
|
+
: html`<section>
|
|
390
|
+
<h4>${t('gallery.field.shared')}</h4>
|
|
391
|
+
<div class="chips">
|
|
392
|
+
<button class="chip" aria-pressed=${String(!this.shared)} @click=${() => (this.shared = false)}>
|
|
393
|
+
<md-icon>lock</md-icon>${t('gallery.text.only-me')}
|
|
394
|
+
</button>
|
|
395
|
+
<button class="chip" aria-pressed=${String(this.shared)} @click=${() => (this.shared = true)}>
|
|
396
|
+
<md-icon>group</md-icon>${t('gallery.text.everyone')}
|
|
397
|
+
</button>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="muted">${t(this.shared ? 'gallery.text.shared-note' : 'gallery.text.only-me-note')}</div>
|
|
400
|
+
</section>`
|
|
388
401
|
}
|
|
389
402
|
</div>
|
|
390
403
|
<ox-popup-actions>
|
|
@@ -416,6 +429,7 @@ export class GalleryUploader extends localize(i18next)(LitElement) {
|
|
|
416
429
|
this.caption = post.caption ?? ''
|
|
417
430
|
this.tags = [...(post.tags ?? [])]
|
|
418
431
|
this.chosenSpace = post.space
|
|
432
|
+
this.shared = post.shared
|
|
419
433
|
this.chosenEvent = post.event?.id ?? null
|
|
420
434
|
this.takenOn = dateText(new Date(post.takenAt))
|
|
421
435
|
} else {
|
|
@@ -539,6 +553,7 @@ export class GalleryUploader extends localize(i18next)(LitElement) {
|
|
|
539
553
|
const files = await Promise.all(this.added.map(one => shrunk(one.file)))
|
|
540
554
|
const input = {
|
|
541
555
|
space: this.chosenSpace,
|
|
556
|
+
shared: this.chosenSpace === 'PERSONAL' && this.shared,
|
|
542
557
|
caption: this.caption.trim(),
|
|
543
558
|
takenAt: this.takenAt(),
|
|
544
559
|
tags: this.tags,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* One post seen large (ADR-0165 decisions 4-6): its photos one by one
|
|
2
|
+
* One post seen large (ADR-0165 decisions 4-6): its photos one by one - and across the whole screen (decision 9) - the like (the heart, or a double press on the
|
|
3
3
|
* photo), who posted it and when, its event and tags - each opens the gallery narrowed to it. Its owner changes or
|
|
4
4
|
* deletes it here; a moderator hides it here, with the reason, or shows it again. Where to go next is the popup's result
|
|
5
5
|
* - a popup closes by going back in the history, so a page opened before it is gone would be gone with it.
|
|
@@ -70,6 +70,18 @@ export class GalleryViewer extends localize(i18next)(LitElement) {
|
|
|
70
70
|
.stage .next {
|
|
71
71
|
inset-inline-end: var(--spacing-small, 4px);
|
|
72
72
|
}
|
|
73
|
+
.stage .large {
|
|
74
|
+
position: absolute;
|
|
75
|
+
top: var(--spacing-small, 4px);
|
|
76
|
+
inset-inline-end: var(--spacing-small, 4px);
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
padding: 4px;
|
|
79
|
+
border: 0;
|
|
80
|
+
border-radius: 50%;
|
|
81
|
+
background: rgb(0 0 0 / 45%);
|
|
82
|
+
color: #fff;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
}
|
|
73
85
|
.stage .index {
|
|
74
86
|
position: absolute;
|
|
75
87
|
bottom: var(--spacing-small, 4px);
|
|
@@ -198,6 +210,7 @@ export class GalleryViewer extends localize(i18next)(LitElement) {
|
|
|
198
210
|
${post.hiddenAt ? html`<div class="hidden">${t('gallery.text.hidden-note')}${post.hiddenReason ? ` · ${post.hiddenReason}` : ''}</div>` : nothing}
|
|
199
211
|
<div class="stage" @dblclick=${() => this.like(true, true)}>
|
|
200
212
|
${photo ? html`<img src=${photo.fullpath} alt=${post.caption ?? ''} />` : nothing}
|
|
213
|
+
${photo ? html`<button class="large" aria-label=${t('gallery.button.large')} title=${t('gallery.button.large')} @click=${() => this.close({ show: { postId: post.id, index: this.index } })}><md-icon>fullscreen</md-icon></button>` : nothing}
|
|
201
214
|
${this.burst ? html`<md-icon class="burst" style="font-variation-settings:'FILL' 1">favorite</md-icon>` : nothing}
|
|
202
215
|
${
|
|
203
216
|
many
|
|
@@ -239,7 +252,7 @@ export class GalleryViewer extends localize(i18next)(LitElement) {
|
|
|
239
252
|
: nothing
|
|
240
253
|
}
|
|
241
254
|
${(post.tags ?? []).map(tag => html`<button class="link" @click=${() => this.go(`gallery-page?tag=${encodeURIComponent(tag)}`)}>#${tag}</button>`)}
|
|
242
|
-
${post.space === 'PERSONAL' ? html`<span>${t('gallery.space.PERSONAL')}</span>` : nothing}
|
|
255
|
+
${post.space === 'PERSONAL' ? html`<span>${t('gallery.space.PERSONAL')}${post.shared ? '' : ` · ${t('gallery.text.only-me')}`}</span>` : nothing}
|
|
243
256
|
</div>
|
|
244
257
|
|
|
245
258
|
${
|
|
@@ -390,8 +403,8 @@ export class GalleryViewer extends localize(i18next)(LitElement) {
|
|
|
390
403
|
this.close({ go: address })
|
|
391
404
|
}
|
|
392
405
|
|
|
393
|
-
/** The next step - a page to go to,
|
|
394
|
-
private close(next?: { go?: string; edit?: Post }) {
|
|
406
|
+
/** The next step - a page to go to, the post to change, or the photo to see large - goes as the popup's result: the page takes it once the popup is gone. */
|
|
407
|
+
private close(next?: { go?: string; edit?: Post; show?: { postId: string; index: number } }) {
|
|
395
408
|
this.dispatchEvent(new CustomEvent('done', { detail: next, bubbles: true, composed: true }))
|
|
396
409
|
}
|
|
397
410
|
}
|
|
@@ -9,11 +9,12 @@ import { customElement, query, state } from 'lit/decorators.js'
|
|
|
9
9
|
|
|
10
10
|
import { i18next, localize } from '@operato/i18n'
|
|
11
11
|
import { notify, openPopup } from '@operato/layout'
|
|
12
|
-
import { navigate, PageView
|
|
12
|
+
import { navigate, PageView } from '@operato/shell'
|
|
13
13
|
import { ScrollbarStyles } from '@operato/styles'
|
|
14
14
|
import { isMobileDevice } from '@operato/utils'
|
|
15
15
|
import { hasPrivilege } from '@things-factory/auth-base/dist-client'
|
|
16
16
|
|
|
17
|
+
import { GalleryShow } from '../components/gallery-show.js'
|
|
17
18
|
import {
|
|
18
19
|
galleryApi,
|
|
19
20
|
type EventCard,
|
|
@@ -186,6 +187,10 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
186
187
|
top: 4px;
|
|
187
188
|
inset-inline-end: 4px;
|
|
188
189
|
}
|
|
190
|
+
.badge.private {
|
|
191
|
+
bottom: 4px;
|
|
192
|
+
inset-inline-end: 4px;
|
|
193
|
+
}
|
|
189
194
|
.badge.hidden {
|
|
190
195
|
top: 4px;
|
|
191
196
|
inset-inline-start: 4px;
|
|
@@ -268,10 +273,6 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
268
273
|
|
|
269
274
|
private onChanged = () => this.load()
|
|
270
275
|
|
|
271
|
-
private get me(): string | undefined {
|
|
272
|
-
return (store.getState() as any).auth?.user?.id
|
|
273
|
-
}
|
|
274
|
-
|
|
275
276
|
get context() {
|
|
276
277
|
const actions: any[] = []
|
|
277
278
|
if (this.tag && this.moderator)
|
|
@@ -281,6 +282,8 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
281
282
|
if (this.tab !== 'events' || this.eventId) {
|
|
282
283
|
if (isMobileDevice())
|
|
283
284
|
actions.push({ title: t('gallery.button.shoot'), icon: 'photo_camera', action: () => this.camera?.pick() })
|
|
285
|
+
if (this.total)
|
|
286
|
+
actions.push({ title: t('gallery.button.slideshow'), icon: 'slideshow', action: () => GalleryShow.open({ filter: this.filter(), playing: true }) })
|
|
284
287
|
actions.push({
|
|
285
288
|
title: t('gallery.button.post'),
|
|
286
289
|
icon: 'add_photo_alternate',
|
|
@@ -319,7 +322,7 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
319
322
|
}
|
|
320
323
|
|
|
321
324
|
private get narrowed() {
|
|
322
|
-
return !!(this.eventId || this.tag ||
|
|
325
|
+
return !!(this.eventId || this.tag || this.authorId)
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
private renderNarrowing() {
|
|
@@ -333,7 +336,7 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
333
336
|
</button>
|
|
334
337
|
</div>`
|
|
335
338
|
}
|
|
336
|
-
if (this.authorId
|
|
339
|
+
if (this.authorId) {
|
|
337
340
|
const name = this.posts[0]?.creator?.name ?? ''
|
|
338
341
|
return html`<div class="narrow"><h2>${t('gallery.text.persons-gallery', { name })}</h2></div>`
|
|
339
342
|
}
|
|
@@ -371,6 +374,7 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
371
374
|
${post.photos[0] ? html`<img loading="lazy" src=${post.photos[0].thumbnail || post.photos[0].fullpath} alt=${post.caption ?? ''} />` : nothing}
|
|
372
375
|
${post.hiddenAt ? html`<span class="badge hidden"><md-icon>visibility_off</md-icon>${t('gallery.text.hidden')}</span>` : nothing}
|
|
373
376
|
${post.photos.length > 1 ? html`<span class="badge count"><md-icon>photo_library</md-icon>${post.photos.length}</span>` : nothing}
|
|
377
|
+
${post.space === 'PERSONAL' && !post.shared ? html`<span class="badge private" title=${t('gallery.text.only-me')}><md-icon>lock</md-icon></span>` : nothing}
|
|
374
378
|
${post.likeCount || post.liked ? html`<span class="badge likes" ?liked=${post.liked}><md-icon>favorite</md-icon>${post.likeCount}</span>` : nothing}
|
|
375
379
|
</div>
|
|
376
380
|
<div class="under">
|
|
@@ -432,14 +436,9 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
432
436
|
const params = lifecycle?.params ?? {}
|
|
433
437
|
this.eventId = params.event || undefined
|
|
434
438
|
this.tag = params.tag || undefined
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
|
|
438
|
-
: this.authorId === this.me && this.authorId
|
|
439
|
-
? 'mine'
|
|
440
|
-
: TABS.includes(params.tab)
|
|
441
|
-
? params.tab
|
|
442
|
-
: 'company'
|
|
439
|
+
/* 「me」 is the own gallery - the server knows who asks; the client holds no user id. */
|
|
440
|
+
this.authorId = params.author === 'me' ? undefined : params.author || undefined
|
|
441
|
+
this.tab = this.eventId ? 'events' : params.author === 'me' ? 'mine' : TABS.includes(params.tab) ? params.tab : 'company'
|
|
443
442
|
this.event = undefined
|
|
444
443
|
this.posts = []
|
|
445
444
|
this.updateContext()
|
|
@@ -457,7 +456,7 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
457
456
|
if (this.tag) filter.tag = this.tag
|
|
458
457
|
if (this.authorId) filter.authorId = this.authorId
|
|
459
458
|
else if (this.tab === 'company') filter.space = 'COMPANY'
|
|
460
|
-
else if (this.tab === 'mine'
|
|
459
|
+
else if (this.tab === 'mine') filter.mine = true
|
|
461
460
|
return filter
|
|
462
461
|
}
|
|
463
462
|
|
|
@@ -471,6 +470,8 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
471
470
|
const page = await galleryApi.posts(this.filter(), more ? this.posts.length : 0, PAGE)
|
|
472
471
|
this.posts = more ? [...this.posts, ...page.items] : page.items
|
|
473
472
|
this.total = page.total
|
|
473
|
+
/* The slideshow is offered once there is something to show. */
|
|
474
|
+
this.updateContext()
|
|
474
475
|
if (this.eventId && !this.event)
|
|
475
476
|
this.event =
|
|
476
477
|
page.items.find(one => one.event)?.event ?? (await galleryApi.events()).find(one => one.id === this.eventId)
|
|
@@ -523,6 +524,7 @@ export class GalleryPage extends localize(i18next)(PageView) {
|
|
|
523
524
|
const next = await popup.result
|
|
524
525
|
if (next?.edit) this.upload([], next.edit)
|
|
525
526
|
else if (next?.go) navigate(next.go)
|
|
527
|
+
else if (next?.show) GalleryShow.open({ filter: this.filter(), start: next.show })
|
|
526
528
|
}
|
|
527
529
|
|
|
528
530
|
private async openById(id: string) {
|
|
@@ -14,6 +14,7 @@ export type EventChoice = EventFacts & { near: boolean }
|
|
|
14
14
|
export interface Post {
|
|
15
15
|
id: string
|
|
16
16
|
space: Space
|
|
17
|
+
shared: boolean
|
|
17
18
|
caption?: string | null
|
|
18
19
|
takenAt: string
|
|
19
20
|
tags?: string[] | null
|
|
@@ -31,15 +32,16 @@ export interface Post {
|
|
|
31
32
|
|
|
32
33
|
export interface PostInput {
|
|
33
34
|
space?: Space
|
|
35
|
+
shared?: boolean
|
|
34
36
|
caption?: string
|
|
35
37
|
takenAt?: string
|
|
36
38
|
tags?: string[]
|
|
37
39
|
eventId?: string | null
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
export type Filter = { space?: Space; eventId?: string; tag?: string; authorId?: string }
|
|
42
|
+
export type Filter = { space?: Space; eventId?: string; tag?: string; authorId?: string; mine?: boolean; everyone?: boolean }
|
|
41
43
|
|
|
42
|
-
const POST_FIELDS = `id space caption takenAt tags likeCount liked editable moderatable hiddenAt hiddenReason createdAt
|
|
44
|
+
const POST_FIELDS = `id space shared caption takenAt tags likeCount liked editable moderatable hiddenAt hiddenReason createdAt
|
|
43
45
|
creator { id name } event { id name startAt endAt location } photos { id name thumbnail fullpath mimetype }`
|
|
44
46
|
const EVENT_FIELDS = 'id name startAt endAt location'
|
|
45
47
|
|