@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.
- package/client/bootstrap.ts +16 -0
- package/client/components/gallery-forms.ts +188 -0
- package/client/components/gallery-uploader.ts +557 -0
- package/client/components/gallery-viewer.ts +397 -0
- package/client/index.ts +1 -0
- package/client/pages/gallery-page.ts +567 -0
- package/client/route.ts +8 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/gallery-api.ts +157 -0
- package/client/utils/gallery-badge.ts +44 -0
- package/client/utils/gallery-words.ts +22 -0
- package/client/utils/photo-prep.ts +93 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +16 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/components/gallery-forms.d.ts +31 -0
- package/dist-client/components/gallery-forms.js +223 -0
- package/dist-client/components/gallery-forms.js.map +1 -0
- package/dist-client/components/gallery-uploader.d.ts +51 -0
- package/dist-client/components/gallery-uploader.js +588 -0
- package/dist-client/components/gallery-uploader.js.map +1 -0
- package/dist-client/components/gallery-viewer.d.ts +30 -0
- package/dist-client/components/gallery-viewer.js +416 -0
- package/dist-client/components/gallery-viewer.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/pages/gallery-page.d.ts +55 -0
- package/dist-client/pages/gallery-page.js +586 -0
- package/dist-client/pages/gallery-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +9 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-client/utils/gallery-api.d.ts +95 -0
- package/dist-client/utils/gallery-api.js +46 -0
- package/dist-client/utils/gallery-api.js.map +1 -0
- package/dist-client/utils/gallery-badge.d.ts +4 -0
- package/dist-client/utils/gallery-badge.js +45 -0
- package/dist-client/utils/gallery-badge.js.map +1 -0
- package/dist-client/utils/gallery-words.d.ts +7 -0
- package/dist-client/utils/gallery-words.js +20 -0
- package/dist-client/utils/gallery-words.js.map +1 -0
- package/dist-client/utils/photo-prep.d.ts +10 -0
- package/dist-client/utils/photo-prep.js +102 -0
- package/dist-client/utils/photo-prep.js.map +1 -0
- package/dist-server/attachment-reader.d.ts +1 -0
- package/dist-server/attachment-reader.js +13 -0
- package/dist-server/attachment-reader.js.map +1 -0
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +11 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/role-tasks.d.ts +2 -0
- package/dist-server/role-tasks.js +15 -0
- package/dist-server/role-tasks.js.map +1 -0
- package/dist-server/service/gallery/gallery-resolver.d.ts +29 -0
- package/dist-server/service/gallery/gallery-resolver.js +602 -0
- package/dist-server/service/gallery/gallery-resolver.js.map +1 -0
- package/dist-server/service/gallery/gallery-rules.d.ts +88 -0
- package/dist-server/service/gallery/gallery-rules.js +181 -0
- package/dist-server/service/gallery/gallery-rules.js.map +1 -0
- package/dist-server/service/gallery/gallery-service.d.ts +37 -0
- package/dist-server/service/gallery/gallery-service.js +234 -0
- package/dist-server/service/gallery/gallery-service.js.map +1 -0
- package/dist-server/service/gallery/gallery-types.d.ts +62 -0
- package/dist-server/service/gallery/gallery-types.js +227 -0
- package/dist-server/service/gallery/gallery-types.js.map +1 -0
- package/dist-server/service/gallery/gallery.d.ts +83 -0
- package/dist-server/service/gallery/gallery.js +330 -0
- package/dist-server/service/gallery/gallery.js.map +1 -0
- package/dist-server/service/gallery/index.d.ts +4 -0
- package/dist-server/service/gallery/index.js +8 -0
- package/dist-server/service/gallery/index.js.map +1 -0
- package/dist-server/service/index.d.ts +7 -0
- package/dist-server/service/index.js +23 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -4
- package/server/attachment-reader.ts +14 -0
- package/server/index.ts +9 -0
- package/server/role-tasks.ts +14 -0
- package/server/service/gallery/gallery-resolver.ts +541 -0
- package/server/service/gallery/gallery-rules.ts +187 -0
- package/server/service/gallery/gallery-service.ts +254 -0
- package/server/service/gallery/gallery-types.ts +151 -0
- package/server/service/gallery/gallery.ts +266 -0
- package/server/service/gallery/index.ts +5 -0
- package/server/service/index.ts +19 -0
- package/server/tsconfig.json +11 -0
- package/test/gallery-persistence.test.ts +372 -0
- package/test/gallery-rules.test.ts +140 -0
- package/test/open-doors.test.ts +62 -0
- package/test/photo-prep.test.ts +61 -0
- package/test/translations.test.ts +68 -0
- package/things-factory.config.js +8 -0
- package/translations/en.json +91 -0
- package/translations/es.json +91 -0
- package/translations/ja.json +91 -0
- package/translations/ko.json +91 -0
- package/translations/ms.json +91 -0
- package/translations/zh.json +91 -0
- package/README.md +0 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The gallery doors as the screens call them (ADR-0165). Every call throws the server's words when it is refused.
|
|
3
|
+
*/
|
|
4
|
+
import gql from 'graphql-tag'
|
|
5
|
+
|
|
6
|
+
import { client, uploadContext } from '@operato/graphql'
|
|
7
|
+
|
|
8
|
+
export type Space = 'COMPANY' | 'PERSONAL'
|
|
9
|
+
export type Photo = { id: string; name?: string; thumbnail?: string | null; fullpath: string; mimetype?: string }
|
|
10
|
+
export type EventFacts = { id: string; name: string; startAt: string; endAt: string; location?: string | null }
|
|
11
|
+
export type EventCard = EventFacts & { postCount: number; cover?: Photo | null }
|
|
12
|
+
export type EventChoice = EventFacts & { near: boolean }
|
|
13
|
+
|
|
14
|
+
export interface Post {
|
|
15
|
+
id: string
|
|
16
|
+
space: Space
|
|
17
|
+
caption?: string | null
|
|
18
|
+
takenAt: string
|
|
19
|
+
tags?: string[] | null
|
|
20
|
+
likeCount: number
|
|
21
|
+
liked: boolean
|
|
22
|
+
editable: boolean
|
|
23
|
+
moderatable: boolean
|
|
24
|
+
hiddenAt?: string | null
|
|
25
|
+
hiddenReason?: string | null
|
|
26
|
+
createdAt: string
|
|
27
|
+
creator?: { id: string; name?: string } | null
|
|
28
|
+
event?: EventFacts | null
|
|
29
|
+
photos: Photo[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PostInput {
|
|
33
|
+
space?: Space
|
|
34
|
+
caption?: string
|
|
35
|
+
takenAt?: string
|
|
36
|
+
tags?: string[]
|
|
37
|
+
eventId?: string | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type Filter = { space?: Space; eventId?: string; tag?: string; authorId?: string }
|
|
41
|
+
|
|
42
|
+
const POST_FIELDS = `id space caption takenAt tags likeCount liked editable moderatable hiddenAt hiddenReason createdAt
|
|
43
|
+
creator { id name } event { id name startAt endAt location } photos { id name thumbnail fullpath mimetype }`
|
|
44
|
+
const EVENT_FIELDS = 'id name startAt endAt location'
|
|
45
|
+
|
|
46
|
+
function answer<T>(response: any, field: string): T {
|
|
47
|
+
if (response.errors?.length) throw new Error(response.errors.map((one: any) => one.message).join(' · '))
|
|
48
|
+
return response.data?.[field] as T
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function ask<T>(query: string, variables: Record<string, unknown>, field: string): Promise<T> {
|
|
52
|
+
return answer<T>(await client.query({ query: gql(query), variables, fetchPolicy: 'network-only' }), field)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function change<T>(
|
|
56
|
+
mutation: string,
|
|
57
|
+
variables: Record<string, unknown>,
|
|
58
|
+
field: string,
|
|
59
|
+
uploading?: (fraction: number) => void
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
return answer<T>(
|
|
62
|
+
await client.mutate({
|
|
63
|
+
mutation: gql(mutation),
|
|
64
|
+
variables,
|
|
65
|
+
...(uploading ? { context: uploadContext(uploading) } : {})
|
|
66
|
+
}),
|
|
67
|
+
field
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Tells the open gallery and the menu's count that a post came, changed or went. */
|
|
72
|
+
export const galleryChanged = () => window.dispatchEvent(new CustomEvent('gallery-changed'))
|
|
73
|
+
|
|
74
|
+
export const galleryApi = {
|
|
75
|
+
posts: (filter: Filter, skip = 0, take = 30) =>
|
|
76
|
+
ask<{ items: Post[]; total: number }>(
|
|
77
|
+
`query ($filter: GalleryFilter, $skip: Int, $take: Int) { galleryPosts(filter: $filter, skip: $skip, take: $take) { items { ${POST_FIELDS} } total } }`,
|
|
78
|
+
{ filter, skip, take },
|
|
79
|
+
'galleryPosts'
|
|
80
|
+
),
|
|
81
|
+
post: (id: string) =>
|
|
82
|
+
ask<Post | null>(`query ($id: String!) { galleryPost(id: $id) { ${POST_FIELDS} } }`, { id }, 'galleryPost'),
|
|
83
|
+
likers: (id: string) =>
|
|
84
|
+
ask<{ id: string; name?: string }[]>(
|
|
85
|
+
`query ($id: String!) { galleryLikers(id: $id) { id name } }`,
|
|
86
|
+
{ id },
|
|
87
|
+
'galleryLikers'
|
|
88
|
+
),
|
|
89
|
+
tags: (text = '') =>
|
|
90
|
+
ask<{ tag: string; count: number }[]>(
|
|
91
|
+
`query ($text: String) { galleryTags(text: $text) { tag count } }`,
|
|
92
|
+
{ text },
|
|
93
|
+
'galleryTags'
|
|
94
|
+
),
|
|
95
|
+
events: () =>
|
|
96
|
+
ask<EventCard[]>(
|
|
97
|
+
`query { galleryEvents { ${EVENT_FIELDS} postCount cover { id thumbnail fullpath } } }`,
|
|
98
|
+
{},
|
|
99
|
+
'galleryEvents'
|
|
100
|
+
),
|
|
101
|
+
eventChoices: (takenAt?: string) =>
|
|
102
|
+
ask<EventChoice[]>(
|
|
103
|
+
`query ($takenAt: DateTimeISO) { galleryEventChoices(takenAt: $takenAt) { ${EVENT_FIELDS} near } }`,
|
|
104
|
+
{ takenAt },
|
|
105
|
+
'galleryEventChoices'
|
|
106
|
+
),
|
|
107
|
+
summary: () => ask<{ fresh: number }>(`query { gallerySummary { fresh } }`, {}, 'gallerySummary'),
|
|
108
|
+
create: (input: PostInput, photos: File[], uploading?: (fraction: number) => void) =>
|
|
109
|
+
change<Post>(
|
|
110
|
+
`mutation ($input: GalleryPostInput!, $photos: [Upload!]!) { createGalleryPost(input: $input, photos: $photos) { ${POST_FIELDS} } }`,
|
|
111
|
+
{ input, photos },
|
|
112
|
+
'createGalleryPost',
|
|
113
|
+
uploading ?? (() => {})
|
|
114
|
+
),
|
|
115
|
+
update: (id: string, input: PostInput, attach: File[], remove: string[], uploading?: (fraction: number) => void) =>
|
|
116
|
+
change<Post>(
|
|
117
|
+
`mutation ($id: String!, $input: GalleryPostInput!, $attach: [Upload!], $remove: [String!]) {
|
|
118
|
+
updateGalleryPost(id: $id, input: $input, attach: $attach, remove: $remove) { ${POST_FIELDS} }
|
|
119
|
+
}`,
|
|
120
|
+
{ id, input, attach, remove },
|
|
121
|
+
'updateGalleryPost',
|
|
122
|
+
attach.length ? (uploading ?? (() => {})) : undefined
|
|
123
|
+
),
|
|
124
|
+
remove: (id: string) =>
|
|
125
|
+
change<boolean>(`mutation ($id: String!) { deleteGalleryPost(id: $id) }`, { id }, 'deleteGalleryPost'),
|
|
126
|
+
like: (id: string, like: boolean) =>
|
|
127
|
+
change<{ id: string; likeCount: number; liked: boolean }>(
|
|
128
|
+
`mutation ($id: String!, $like: Boolean!) { likeGalleryPost(id: $id, like: $like) { id likeCount liked } }`,
|
|
129
|
+
{ id, like },
|
|
130
|
+
'likeGalleryPost'
|
|
131
|
+
),
|
|
132
|
+
visit: () => change<boolean>(`mutation { visitGallery }`, {}, 'visitGallery'),
|
|
133
|
+
hide: (id: string, reason: string) =>
|
|
134
|
+
change<Post>(
|
|
135
|
+
`mutation ($id: String!, $reason: String!) { hideGalleryPost(id: $id, reason: $reason) { ${POST_FIELDS} } }`,
|
|
136
|
+
{ id, reason },
|
|
137
|
+
'hideGalleryPost'
|
|
138
|
+
),
|
|
139
|
+
unhide: (id: string) =>
|
|
140
|
+
change<Post>(
|
|
141
|
+
`mutation ($id: String!) { unhideGalleryPost(id: $id) { ${POST_FIELDS} } }`,
|
|
142
|
+
{ id },
|
|
143
|
+
'unhideGalleryPost'
|
|
144
|
+
),
|
|
145
|
+
renameTag: (from: string, to: string) =>
|
|
146
|
+
change<number>(
|
|
147
|
+
`mutation ($from: String!, $to: String!) { renameGalleryTag(from: $from, to: $to) }`,
|
|
148
|
+
{ from, to },
|
|
149
|
+
'renameGalleryTag'
|
|
150
|
+
),
|
|
151
|
+
createEvent: (input: { name: string; startAt: string; endAt: string; location?: string }) =>
|
|
152
|
+
change<EventCard>(
|
|
153
|
+
`mutation ($input: GalleryEventInput!) { createGalleryEvent(input: $input) { ${EVENT_FIELDS} postCount } }`,
|
|
154
|
+
{ input },
|
|
155
|
+
'createGalleryEvent'
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The count the menu shows on 갤러리 - posts others made since one last opened it (ADR-0165 decision 6). An application
|
|
3
|
+
* puts `generateGalleryBadge` on its menu entry; the count is asked again when a post comes or the gallery is opened.
|
|
4
|
+
*/
|
|
5
|
+
import { galleryApi } from './gallery-api.js'
|
|
6
|
+
|
|
7
|
+
let count: number | undefined
|
|
8
|
+
let version = 0
|
|
9
|
+
const waiting = new Set<() => void>()
|
|
10
|
+
|
|
11
|
+
/** Asks the server again and tells every badge. A count that could not be asked is not zero: it draws nothing. */
|
|
12
|
+
export async function refreshGalleryBadge(): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
count = (await galleryApi.summary()).fresh || undefined
|
|
15
|
+
} catch {
|
|
16
|
+
count = undefined
|
|
17
|
+
}
|
|
18
|
+
version++
|
|
19
|
+
for (const wake of [...waiting]) wake()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let generation = 0
|
|
23
|
+
|
|
24
|
+
/** One badge's counts. The menu asks for a new one when it draws again; the one before it then ends. */
|
|
25
|
+
export async function* generateGalleryBadge() {
|
|
26
|
+
const mine = ++generation
|
|
27
|
+
for (const wake of [...waiting]) wake()
|
|
28
|
+
let seen = version
|
|
29
|
+
yield count
|
|
30
|
+
while (true) {
|
|
31
|
+
if (seen === version) {
|
|
32
|
+
await new Promise<void>(resolve => {
|
|
33
|
+
const wake = () => {
|
|
34
|
+
waiting.delete(wake)
|
|
35
|
+
resolve()
|
|
36
|
+
}
|
|
37
|
+
waiting.add(wake)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
if (mine !== generation) return
|
|
41
|
+
seen = version
|
|
42
|
+
yield count
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The words of the gallery - one place for the page, the viewer and the uploader.
|
|
3
|
+
*/
|
|
4
|
+
import { i18next } from '@operato/i18n'
|
|
5
|
+
|
|
6
|
+
export const t = (key: string, options: Record<string, unknown> = {}) => String(i18next.t(key, options))
|
|
7
|
+
|
|
8
|
+
/** A day as the reader's locale writes it. */
|
|
9
|
+
export function day(value?: string | Date | null): string {
|
|
10
|
+
if (!value) return ''
|
|
11
|
+
return new Date(value).toLocaleDateString(i18next.language, { year: 'numeric', month: 'short', day: 'numeric' })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** An event's days - one day, or its first and last. */
|
|
15
|
+
export function days(startAt: string, endAt: string): string {
|
|
16
|
+
const start = new Date(startAt)
|
|
17
|
+
const end = new Date(endAt)
|
|
18
|
+
return start.toDateString() === end.toDateString() ? day(start) : `${day(start)} – ${day(end)}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The words of a refusal the server gave, or the fallback. */
|
|
22
|
+
export const reasonOf = (error: unknown) => (error as Error)?.message || t('gallery.text.failed')
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A photo made ready in the browser before it goes up (ADR-0165 decision 3): when it was taken, read from the camera's
|
|
3
|
+
* own record (EXIF), and the photo shrunk to 2560 px on its long side so a phone's original does not fill the storage.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The longest side a photo keeps. */
|
|
7
|
+
export const LONG_SIDE = 2560
|
|
8
|
+
/* A photo already small enough goes up as it is - shrinking again would only lose quality. */
|
|
9
|
+
const SMALL_ENOUGH = 2.5 * 1024 * 1024
|
|
10
|
+
|
|
11
|
+
/** "YYYY:MM:DD HH:MM:SS" as the camera wrote it, in the camera's local time. */
|
|
12
|
+
export function exifDateOf(text?: string): Date | undefined {
|
|
13
|
+
const match = /^(\d{4}):(\d{2}):(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(text ?? '')
|
|
14
|
+
if (!match) return undefined
|
|
15
|
+
const [, y, mo, d, h, mi, s] = match.map(Number)
|
|
16
|
+
const date = new Date(y, mo - 1, d, h, mi, s)
|
|
17
|
+
return isNaN(date.getTime()) || y < 1990 ? undefined : date
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The date the camera recorded the photo, from a JPEG's EXIF - when it was taken (0x9003), else when it was saved (0x0132). */
|
|
21
|
+
export function exifTakenAt(buffer: ArrayBuffer): Date | undefined {
|
|
22
|
+
try {
|
|
23
|
+
const view = new DataView(buffer)
|
|
24
|
+
if (view.byteLength < 4 || view.getUint16(0) !== 0xffd8) return undefined
|
|
25
|
+
let offset = 2
|
|
26
|
+
while (offset + 10 <= view.byteLength) {
|
|
27
|
+
const marker = view.getUint16(offset)
|
|
28
|
+
if ((marker & 0xff00) !== 0xff00) return undefined
|
|
29
|
+
const size = view.getUint16(offset + 2)
|
|
30
|
+
/* APP1 starting with "Exif\0\0" */
|
|
31
|
+
if (marker === 0xffe1 && view.getUint32(offset + 4) === 0x45786966) return exifDateOf(tiffDate(view, offset + 10))
|
|
32
|
+
offset += 2 + size
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
/* A broken or cut record says nothing. */
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tiffDate(view: DataView, start: number): string | undefined {
|
|
41
|
+
const little = view.getUint16(start) === 0x4949
|
|
42
|
+
const u16 = (at: number) => view.getUint16(start + at, little)
|
|
43
|
+
const u32 = (at: number) => view.getUint32(start + at, little)
|
|
44
|
+
const entries = (ifd: number) => Array.from({ length: u16(ifd) }, (_, i) => ifd + 2 + i * 12)
|
|
45
|
+
const ascii = (entry: number) => {
|
|
46
|
+
const count = u32(entry + 4)
|
|
47
|
+
const at = count > 4 ? u32(entry + 8) : entry + 8
|
|
48
|
+
let text = ''
|
|
49
|
+
for (let i = 0; i < count - 1; i++) text += String.fromCharCode(view.getUint8(start + at + i))
|
|
50
|
+
return text
|
|
51
|
+
}
|
|
52
|
+
let saved: string | undefined
|
|
53
|
+
let exif: number | undefined
|
|
54
|
+
for (const entry of entries(u32(4))) {
|
|
55
|
+
const tag = u16(entry)
|
|
56
|
+
if (tag === 0x0132) saved = ascii(entry)
|
|
57
|
+
if (tag === 0x8769) exif = u32(entry + 8)
|
|
58
|
+
}
|
|
59
|
+
if (exif) for (const entry of entries(exif)) if (u16(entry) === 0x9003) return ascii(entry)
|
|
60
|
+
return saved
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** When the photo was taken - the camera's record, or nothing. */
|
|
64
|
+
export async function takenAtOf(file: File): Promise<Date | undefined> {
|
|
65
|
+
if (!/jpe?g/i.test(file.type) && !/\.jpe?g$/i.test(file.name)) return undefined
|
|
66
|
+
return exifTakenAt(await file.slice(0, 256 * 1024).arrayBuffer())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The photo shrunk to `LONG_SIDE` on its long side as a JPEG - or the photo itself when it is small or cannot be drawn. */
|
|
70
|
+
export async function shrunk(file: File): Promise<File> {
|
|
71
|
+
try {
|
|
72
|
+
const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' } as any)
|
|
73
|
+
const long = Math.max(bitmap.width, bitmap.height)
|
|
74
|
+
if (long <= LONG_SIDE && file.size <= SMALL_ENOUGH) {
|
|
75
|
+
bitmap.close()
|
|
76
|
+
return file
|
|
77
|
+
}
|
|
78
|
+
const scale = Math.min(1, LONG_SIDE / long)
|
|
79
|
+
const canvas = document.createElement('canvas')
|
|
80
|
+
canvas.width = Math.round(bitmap.width * scale)
|
|
81
|
+
canvas.height = Math.round(bitmap.height * scale)
|
|
82
|
+
canvas.getContext('2d')!.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
|
|
83
|
+
bitmap.close()
|
|
84
|
+
const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.88))
|
|
85
|
+
if (!blob) return file
|
|
86
|
+
return new File([blob], file.name.replace(/\.[^.]+$/, '') + '.jpg', {
|
|
87
|
+
type: 'image/jpeg',
|
|
88
|
+
lastModified: file.lastModified
|
|
89
|
+
})
|
|
90
|
+
} catch {
|
|
91
|
+
return file
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function bootstrap(): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { navigate, store } from '@operato/shell';
|
|
2
|
+
import { ADD_USER_MENU } from '@things-factory/apptool-base/client';
|
|
3
|
+
/* One's own gallery is in the menu the header's avatar opens, right after the profile (ADR-0165 decision 6). */
|
|
4
|
+
export default function bootstrap() {
|
|
5
|
+
store.dispatch({
|
|
6
|
+
type: ADD_USER_MENU,
|
|
7
|
+
item: {
|
|
8
|
+
name: 'my-gallery',
|
|
9
|
+
icon: 'photo_library',
|
|
10
|
+
label: 'gallery.title.my-gallery',
|
|
11
|
+
order: 12,
|
|
12
|
+
action: () => navigate('gallery-page?tab=mine')
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=bootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AAEnE,gHAAgH;AAChH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,KAAK,CAAC,QAAQ,CAAC;QACb,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;YACJ,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,0BAA0B;YACjC,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,uBAAuB,CAAC;SAChD;KACF,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { navigate, store } from '@operato/shell'\nimport { ADD_USER_MENU } from '@things-factory/apptool-base/client'\n\n/* One's own gallery is in the menu the header's avatar opens, right after the profile (ADR-0165 decision 6). */\nexport default function bootstrap() {\n store.dispatch({\n type: ADD_USER_MENU,\n item: {\n name: 'my-gallery',\n icon: 'photo_library',\n label: 'gallery.title.my-gallery',\n order: 12,\n action: () => navigate('gallery-page?tab=mine')\n }\n })\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import '@material/web/button/filled-button.js';
|
|
2
|
+
import '@material/web/button/text-button.js';
|
|
3
|
+
import '@operato/popup/ox-popup-actions.js';
|
|
4
|
+
import { LitElement } from 'lit';
|
|
5
|
+
declare const GalleryEventForm_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
6
|
+
export declare class GalleryEventForm extends GalleryEventForm_base {
|
|
7
|
+
static styles: import("lit").CSSResult[];
|
|
8
|
+
private name;
|
|
9
|
+
private startAt;
|
|
10
|
+
private endAt;
|
|
11
|
+
private location;
|
|
12
|
+
private busy;
|
|
13
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
14
|
+
/** A later start carries the end with it, the same length. */
|
|
15
|
+
private start;
|
|
16
|
+
private save;
|
|
17
|
+
/** What was made goes as the popup's result. */
|
|
18
|
+
private close;
|
|
19
|
+
}
|
|
20
|
+
declare const GalleryTagForm_base: (new (...args: any[]) => LitElement) & typeof LitElement;
|
|
21
|
+
export declare class GalleryTagForm extends GalleryTagForm_base {
|
|
22
|
+
static styles: import("lit").CSSResult[];
|
|
23
|
+
tag: string;
|
|
24
|
+
private to;
|
|
25
|
+
private busy;
|
|
26
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
27
|
+
private save;
|
|
28
|
+
/** What was made goes as the popup's result. */
|
|
29
|
+
private close;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
/*
|
|
3
|
+
* The two small forms of the gallery's moderators (ADR-0165 decision 5): an event made from the gallery, for one nobody
|
|
4
|
+
* put on the calendar yet, and a tag renamed on every post - into one already there, the two become one.
|
|
5
|
+
*/
|
|
6
|
+
import '@material/web/button/filled-button.js';
|
|
7
|
+
import '@material/web/button/text-button.js';
|
|
8
|
+
import '@operato/popup/ox-popup-actions.js';
|
|
9
|
+
import { css, html, LitElement } from 'lit';
|
|
10
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
11
|
+
import { i18next, localize } from '@operato/i18n';
|
|
12
|
+
import { notify } from '@operato/layout';
|
|
13
|
+
import { CommonPopupStyles, PopupBodyStyles } from '@operato/styles';
|
|
14
|
+
import { galleryApi, galleryChanged } from '../utils/gallery-api.js';
|
|
15
|
+
import { reasonOf, t } from '../utils/gallery-words.js';
|
|
16
|
+
const FormStyles = css `
|
|
17
|
+
:host {
|
|
18
|
+
height: 100%;
|
|
19
|
+
}
|
|
20
|
+
.body {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--spacing-medium, 8px);
|
|
24
|
+
}
|
|
25
|
+
label {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: 2px;
|
|
29
|
+
font-size: var(--ui-t-small, 12px);
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
32
|
+
}
|
|
33
|
+
input {
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
font: inherit;
|
|
36
|
+
font-size: var(--ui-t-body, 14px);
|
|
37
|
+
font-weight: normal;
|
|
38
|
+
padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
|
|
39
|
+
border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
|
|
40
|
+
border-radius: var(--border-radius, 4px);
|
|
41
|
+
background: var(--md-sys-color-surface);
|
|
42
|
+
color: var(--md-sys-color-on-surface);
|
|
43
|
+
}
|
|
44
|
+
.row {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
gap: var(--spacing-medium, 8px);
|
|
48
|
+
}
|
|
49
|
+
.note {
|
|
50
|
+
font-size: var(--ui-t-small, 12px);
|
|
51
|
+
color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
const local = (value) => {
|
|
55
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
56
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`;
|
|
57
|
+
};
|
|
58
|
+
let GalleryEventForm = class GalleryEventForm extends localize(i18next)(LitElement) {
|
|
59
|
+
constructor() {
|
|
60
|
+
super(...arguments);
|
|
61
|
+
this.name = '';
|
|
62
|
+
this.startAt = local(new Date(new Date().setHours(9, 0, 0, 0)));
|
|
63
|
+
this.endAt = local(new Date(new Date().setHours(18, 0, 0, 0)));
|
|
64
|
+
this.location = '';
|
|
65
|
+
this.busy = false;
|
|
66
|
+
}
|
|
67
|
+
static { this.styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]; }
|
|
68
|
+
render() {
|
|
69
|
+
const ready = this.name.trim() && this.startAt && this.endAt && this.endAt > this.startAt;
|
|
70
|
+
return html `<div class="body">
|
|
71
|
+
<label
|
|
72
|
+
>${t('gallery.field.event-name')}<input
|
|
73
|
+
.value=${this.name}
|
|
74
|
+
@input=${(e) => (this.name = e.target.value)}
|
|
75
|
+
/></label>
|
|
76
|
+
<div class="row">
|
|
77
|
+
<label
|
|
78
|
+
>${t('gallery.field.starts')}<input
|
|
79
|
+
type="datetime-local"
|
|
80
|
+
.value=${this.startAt}
|
|
81
|
+
@change=${(e) => this.start(e.target.value)}
|
|
82
|
+
/></label>
|
|
83
|
+
<label
|
|
84
|
+
>${t('gallery.field.ends')}<input
|
|
85
|
+
type="datetime-local"
|
|
86
|
+
.value=${this.endAt}
|
|
87
|
+
@change=${(e) => (this.endAt = e.target.value)}
|
|
88
|
+
/></label>
|
|
89
|
+
</div>
|
|
90
|
+
<label
|
|
91
|
+
>${t('gallery.field.location')}<input
|
|
92
|
+
.value=${this.location}
|
|
93
|
+
@input=${(e) => (this.location = e.target.value)}
|
|
94
|
+
/></label>
|
|
95
|
+
<div class="note">${t('gallery.text.event-form-note')}</div>
|
|
96
|
+
</div>
|
|
97
|
+
<ox-popup-actions>
|
|
98
|
+
<md-text-button slot="cancel" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>
|
|
99
|
+
<md-filled-button slot="confirm" ?disabled=${this.busy || !ready} @click=${() => this.save()}
|
|
100
|
+
>${t('gallery.button.create-event')}</md-filled-button
|
|
101
|
+
>
|
|
102
|
+
</ox-popup-actions>`;
|
|
103
|
+
}
|
|
104
|
+
/** A later start carries the end with it, the same length. */
|
|
105
|
+
start(value) {
|
|
106
|
+
const length = new Date(this.endAt).getTime() - new Date(this.startAt).getTime();
|
|
107
|
+
this.startAt = value;
|
|
108
|
+
if (value && length > 0)
|
|
109
|
+
this.endAt = local(new Date(new Date(value).getTime() + length));
|
|
110
|
+
}
|
|
111
|
+
async save() {
|
|
112
|
+
this.busy = true;
|
|
113
|
+
try {
|
|
114
|
+
const event = await galleryApi.createEvent({
|
|
115
|
+
name: this.name.trim(),
|
|
116
|
+
startAt: new Date(this.startAt).toISOString(),
|
|
117
|
+
endAt: new Date(this.endAt).toISOString(),
|
|
118
|
+
location: this.location.trim() || undefined
|
|
119
|
+
});
|
|
120
|
+
galleryChanged();
|
|
121
|
+
this.close(event);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
notify({ level: 'error', message: reasonOf(error) });
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
this.busy = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** What was made goes as the popup's result. */
|
|
131
|
+
close(result) {
|
|
132
|
+
this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
__decorate([
|
|
136
|
+
state(),
|
|
137
|
+
__metadata("design:type", Object)
|
|
138
|
+
], GalleryEventForm.prototype, "name", void 0);
|
|
139
|
+
__decorate([
|
|
140
|
+
state(),
|
|
141
|
+
__metadata("design:type", Object)
|
|
142
|
+
], GalleryEventForm.prototype, "startAt", void 0);
|
|
143
|
+
__decorate([
|
|
144
|
+
state(),
|
|
145
|
+
__metadata("design:type", Object)
|
|
146
|
+
], GalleryEventForm.prototype, "endAt", void 0);
|
|
147
|
+
__decorate([
|
|
148
|
+
state(),
|
|
149
|
+
__metadata("design:type", Object)
|
|
150
|
+
], GalleryEventForm.prototype, "location", void 0);
|
|
151
|
+
__decorate([
|
|
152
|
+
state(),
|
|
153
|
+
__metadata("design:type", Object)
|
|
154
|
+
], GalleryEventForm.prototype, "busy", void 0);
|
|
155
|
+
GalleryEventForm = __decorate([
|
|
156
|
+
customElement('gallery-event-form')
|
|
157
|
+
], GalleryEventForm);
|
|
158
|
+
export { GalleryEventForm };
|
|
159
|
+
let GalleryTagForm = class GalleryTagForm extends localize(i18next)(LitElement) {
|
|
160
|
+
constructor() {
|
|
161
|
+
super(...arguments);
|
|
162
|
+
this.tag = '';
|
|
163
|
+
this.to = '';
|
|
164
|
+
this.busy = false;
|
|
165
|
+
}
|
|
166
|
+
static { this.styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]; }
|
|
167
|
+
render() {
|
|
168
|
+
const ready = this.to.trim().replace(/^#+/, '') && this.to.trim().replace(/^#+/, '') !== this.tag;
|
|
169
|
+
return html `<div class="body">
|
|
170
|
+
<label
|
|
171
|
+
>${t('gallery.field.new-tag')}<input
|
|
172
|
+
.value=${this.to}
|
|
173
|
+
placeholder=${this.tag}
|
|
174
|
+
maxlength="31"
|
|
175
|
+
@input=${(e) => (this.to = e.target.value)}
|
|
176
|
+
/></label>
|
|
177
|
+
<div class="note">${t('gallery.text.tag-form-note', { tag: this.tag })}</div>
|
|
178
|
+
</div>
|
|
179
|
+
<ox-popup-actions>
|
|
180
|
+
<md-text-button slot="cancel" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>
|
|
181
|
+
<md-filled-button slot="confirm" ?disabled=${this.busy || !ready} @click=${() => this.save()}
|
|
182
|
+
>${t('gallery.button.rename-tag')}</md-filled-button
|
|
183
|
+
>
|
|
184
|
+
</ox-popup-actions>`;
|
|
185
|
+
}
|
|
186
|
+
async save() {
|
|
187
|
+
this.busy = true;
|
|
188
|
+
try {
|
|
189
|
+
const to = this.to.trim().replace(/^#+/, '');
|
|
190
|
+
const count = await galleryApi.renameTag(this.tag, to);
|
|
191
|
+
notify({ message: t('gallery.text.tag-renamed', { count }) });
|
|
192
|
+
galleryChanged();
|
|
193
|
+
this.close(to);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
notify({ level: 'error', message: reasonOf(error) });
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
this.busy = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** What was made goes as the popup's result. */
|
|
203
|
+
close(result) {
|
|
204
|
+
this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
__decorate([
|
|
208
|
+
property(),
|
|
209
|
+
__metadata("design:type", Object)
|
|
210
|
+
], GalleryTagForm.prototype, "tag", void 0);
|
|
211
|
+
__decorate([
|
|
212
|
+
state(),
|
|
213
|
+
__metadata("design:type", Object)
|
|
214
|
+
], GalleryTagForm.prototype, "to", void 0);
|
|
215
|
+
__decorate([
|
|
216
|
+
state(),
|
|
217
|
+
__metadata("design:type", Object)
|
|
218
|
+
], GalleryTagForm.prototype, "busy", void 0);
|
|
219
|
+
GalleryTagForm = __decorate([
|
|
220
|
+
customElement('gallery-tag-form')
|
|
221
|
+
], GalleryTagForm);
|
|
222
|
+
export { GalleryTagForm };
|
|
223
|
+
//# sourceMappingURL=gallery-forms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gallery-forms.js","sourceRoot":"","sources":["../../client/components/gallery-forms.ts"],"names":[],"mappings":";AAAA;;;GAGG;AACH,OAAO,uCAAuC,CAAA;AAC9C,OAAO,qCAAqC,CAAA;AAC5C,OAAO,oCAAoC,CAAA;AAE3C,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAElE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AACxC,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEpE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AACpE,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,2BAA2B,CAAA;AAEvD,MAAM,UAAU,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCrB,CAAA;AAED,MAAM,KAAK,GAAG,CAAC,KAAW,EAAE,EAAE;IAC5B,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IACrD,OAAO,GAAG,KAAK,CAAC,WAAW,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,EAAE,CAAA;AAC1I,CAAC,CAAA;AAGM,IAAM,gBAAgB,GAAtB,MAAM,gBAAiB,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA5D;;QAGY,SAAI,GAAG,EAAE,CAAA;QACT,YAAO,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1D,UAAK,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;QACzD,aAAQ,GAAG,EAAE,CAAA;QACb,SAAI,GAAG,KAAK,CAAA;IAoE/B,CAAC;aA1EQ,WAAM,GAAG,CAAC,iBAAiB,EAAE,eAAe,EAAE,UAAU,CAAC,AAAnD,CAAmD;IAQhE,MAAM;QACJ,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,OAAO,CAAA;QACzF,OAAO,IAAI,CAAA;;aAEF,CAAC,CAAC,0BAA0B,CAAC;qBACrB,IAAI,CAAC,IAAI;qBACT,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;;;eAI9C,CAAC,CAAC,sBAAsB,CAAC;;uBAEjB,IAAI,CAAC,OAAO;wBACX,CAAC,CAAM,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;;eAG/C,CAAC,CAAC,oBAAoB,CAAC;;uBAEf,IAAI,CAAC,KAAK;wBACT,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;;;aAIpD,CAAC,CAAC,wBAAwB,CAAC;qBACnB,IAAI,CAAC,QAAQ;qBACb,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;4BAErC,CAAC,CAAC,8BAA8B,CAAC;;;+CAGd,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,uBAAuB,CAAC;qDAC1C,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;aACvF,CAAC,CAAC,6BAA6B,CAAC;;0BAEnB,CAAA;IACxB,CAAC;IAED,8DAA8D;IACtD,KAAK,CAAC,KAAa;QACzB,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAA;QAChF,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA;QACpB,IAAI,KAAK,IAAI,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC,CAAA;IAC3F,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,WAAW,CAAC;gBACzC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBACtB,OAAO,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE;gBAC7C,KAAK,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE;gBACzC,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,SAAS;aAC5C,CAAC,CAAA;YACF,cAAc,EAAE,CAAA;YAChB,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QACtD,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,KAAK,CAAA;QACnB,CAAC;IACH,CAAC;IAED,gDAAgD;IACxC,KAAK,CAAC,MAAgB;QAC5B,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAChG,CAAC;;AAvEgB;IAAhB,KAAK,EAAE;;8CAAkB;AACT;IAAhB,KAAK,EAAE;;iDAAmE;AAC1D;IAAhB,KAAK,EAAE;;+CAAkE;AACzD;IAAhB,KAAK,EAAE;;kDAAsB;AACb;IAAhB,KAAK,EAAE;;8CAAqB;AAPlB,gBAAgB;IAD5B,aAAa,CAAC,oBAAoB,CAAC;GACvB,gBAAgB,CA2E5B;;AAGM,IAAM,cAAc,GAApB,MAAM,cAAe,SAAQ,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAA1D;;QAGO,QAAG,GAAG,EAAE,CAAA;QACH,OAAE,GAAG,EAAE,CAAA;QACP,SAAI,GAAG,KAAK,CAAA;IAyC/B,CAAC;aA7CQ,WAAM,GAAG,CAAC,iBAAiB,EAAE,eAAe,EAAE,UAAU,CAAC,AAAnD,CAAmD;IAMhE,MAAM;QACJ,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,GAAG,CAAA;QACjG,OAAO,IAAI,CAAA;;aAEF,CAAC,CAAC,uBAAuB,CAAC;qBAClB,IAAI,CAAC,EAAE;0BACF,IAAI,CAAC,GAAG;;qBAEb,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;;4BAE/B,CAAC,CAAC,4BAA4B,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;;;+CAG/B,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,uBAAuB,CAAC;qDAC1C,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,WAAW,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;aACvF,CAAC,CAAC,2BAA2B,CAAC;;0BAEjB,CAAA;IACxB,CAAC;IAEO,KAAK,CAAC,IAAI;QAChB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;YAC5C,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;YACtD,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,0BAA0B,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;YAC7D,cAAc,EAAE,CAAA;YAChB,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;QACtD,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,KAAK,CAAA;QACnB,CAAC;IACH,CAAC;IAED,gDAAgD;IACxC,KAAK,CAAC,MAAgB;QAC5B,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAChG,CAAC;;AA1CW;IAAX,QAAQ,EAAE;;2CAAS;AACH;IAAhB,KAAK,EAAE;;0CAAgB;AACP;IAAhB,KAAK,EAAE;;4CAAqB;AALlB,cAAc;IAD1B,aAAa,CAAC,kBAAkB,CAAC;GACrB,cAAc,CA8C1B","sourcesContent":["/*\n * The two small forms of the gallery's moderators (ADR-0165 decision 5): an event made from the gallery, for one nobody\n * put on the calendar yet, and a tag renamed on every post - into one already there, the two become one.\n */\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/text-button.js'\nimport '@operato/popup/ox-popup-actions.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { CommonPopupStyles, PopupBodyStyles } from '@operato/styles'\n\nimport { galleryApi, galleryChanged } from '../utils/gallery-api.js'\nimport { reasonOf, t } from '../utils/gallery-words.js'\n\nconst FormStyles = css`\n :host {\n height: 100%;\n }\n .body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n }\n label {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-size: var(--ui-t-small, 12px);\n font-weight: 600;\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n input {\n box-sizing: border-box;\n font: inherit;\n font-size: var(--ui-t-body, 14px);\n font-weight: normal;\n padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));\n border-radius: var(--border-radius, 4px);\n background: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n .row {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-medium, 8px);\n }\n .note {\n font-size: var(--ui-t-small, 12px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n`\n\nconst local = (value: Date) => {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`\n}\n\n@customElement('gallery-event-form')\nexport class GalleryEventForm extends localize(i18next)(LitElement) {\n static styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]\n\n @state() private name = ''\n @state() private startAt = local(new Date(new Date().setHours(9, 0, 0, 0)))\n @state() private endAt = local(new Date(new Date().setHours(18, 0, 0, 0)))\n @state() private location = ''\n @state() private busy = false\n\n render() {\n const ready = this.name.trim() && this.startAt && this.endAt && this.endAt > this.startAt\n return html`<div class=\"body\">\n <label\n >${t('gallery.field.event-name')}<input\n .value=${this.name}\n @input=${(e: any) => (this.name = e.target.value)}\n /></label>\n <div class=\"row\">\n <label\n >${t('gallery.field.starts')}<input\n type=\"datetime-local\"\n .value=${this.startAt}\n @change=${(e: any) => this.start(e.target.value)}\n /></label>\n <label\n >${t('gallery.field.ends')}<input\n type=\"datetime-local\"\n .value=${this.endAt}\n @change=${(e: any) => (this.endAt = e.target.value)}\n /></label>\n </div>\n <label\n >${t('gallery.field.location')}<input\n .value=${this.location}\n @input=${(e: any) => (this.location = e.target.value)}\n /></label>\n <div class=\"note\">${t('gallery.text.event-form-note')}</div>\n </div>\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${this.busy || !ready} @click=${() => this.save()}\n >${t('gallery.button.create-event')}</md-filled-button\n >\n </ox-popup-actions>`\n }\n\n /** A later start carries the end with it, the same length. */\n private start(value: string) {\n const length = new Date(this.endAt).getTime() - new Date(this.startAt).getTime()\n this.startAt = value\n if (value && length > 0) this.endAt = local(new Date(new Date(value).getTime() + length))\n }\n\n private async save() {\n this.busy = true\n try {\n const event = await galleryApi.createEvent({\n name: this.name.trim(),\n startAt: new Date(this.startAt).toISOString(),\n endAt: new Date(this.endAt).toISOString(),\n location: this.location.trim() || undefined\n })\n galleryChanged()\n this.close(event)\n } catch (error) {\n notify({ level: 'error', message: reasonOf(error) })\n } finally {\n this.busy = false\n }\n }\n\n /** What was made goes as the popup's result. */\n private close(result?: unknown) {\n this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }))\n }\n}\n\n@customElement('gallery-tag-form')\nexport class GalleryTagForm extends localize(i18next)(LitElement) {\n static styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]\n\n @property() tag = ''\n @state() private to = ''\n @state() private busy = false\n\n render() {\n const ready = this.to.trim().replace(/^#+/, '') && this.to.trim().replace(/^#+/, '') !== this.tag\n return html`<div class=\"body\">\n <label\n >${t('gallery.field.new-tag')}<input\n .value=${this.to}\n placeholder=${this.tag}\n maxlength=\"31\"\n @input=${(e: any) => (this.to = e.target.value)}\n /></label>\n <div class=\"note\">${t('gallery.text.tag-form-note', { tag: this.tag })}</div>\n </div>\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${this.busy || !ready} @click=${() => this.save()}\n >${t('gallery.button.rename-tag')}</md-filled-button\n >\n </ox-popup-actions>`\n }\n\n private async save() {\n this.busy = true\n try {\n const to = this.to.trim().replace(/^#+/, '')\n const count = await galleryApi.renameTag(this.tag, to)\n notify({ message: t('gallery.text.tag-renamed', { count }) })\n galleryChanged()\n this.close(to)\n } catch (error) {\n notify({ level: 'error', message: reasonOf(error) })\n } finally {\n this.busy = false\n }\n }\n\n /** What was made goes as the popup's result. */\n private close(result?: unknown) {\n this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }))\n }\n}\n"]}
|