@things-factory/gallery 0.0.0-stage → 10.1.223

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,45 @@
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
+ let count;
7
+ let version = 0;
8
+ const waiting = new Set();
9
+ /** Asks the server again and tells every badge. A count that could not be asked is not zero: it draws nothing. */
10
+ export async function refreshGalleryBadge() {
11
+ try {
12
+ count = (await galleryApi.summary()).fresh || undefined;
13
+ }
14
+ catch {
15
+ count = undefined;
16
+ }
17
+ version++;
18
+ for (const wake of [...waiting])
19
+ wake();
20
+ }
21
+ let generation = 0;
22
+ /** One badge's counts. The menu asks for a new one when it draws again; the one before it then ends. */
23
+ export async function* generateGalleryBadge() {
24
+ const mine = ++generation;
25
+ for (const wake of [...waiting])
26
+ wake();
27
+ let seen = version;
28
+ yield count;
29
+ while (true) {
30
+ if (seen === version) {
31
+ await new Promise(resolve => {
32
+ const wake = () => {
33
+ waiting.delete(wake);
34
+ resolve();
35
+ };
36
+ waiting.add(wake);
37
+ });
38
+ }
39
+ if (mine !== generation)
40
+ return;
41
+ seen = version;
42
+ yield count;
43
+ }
44
+ }
45
+ //# sourceMappingURL=gallery-badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gallery-badge.js","sourceRoot":"","sources":["../../client/utils/gallery-badge.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,IAAI,KAAyB,CAAA;AAC7B,IAAI,OAAO,GAAG,CAAC,CAAA;AACf,MAAM,OAAO,GAAG,IAAI,GAAG,EAAc,CAAA;AAErC,kHAAkH;AAClH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,IAAI,CAAC;QACH,KAAK,GAAG,CAAC,MAAM,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,IAAI,SAAS,CAAA;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,GAAG,SAAS,CAAA;IACnB,CAAC;IACD,OAAO,EAAE,CAAA;IACT,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAA;AACzC,CAAC;AAED,IAAI,UAAU,GAAG,CAAC,CAAA;AAElB,wGAAwG;AACxG,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,oBAAoB;IACzC,MAAM,IAAI,GAAG,EAAE,UAAU,CAAA;IACzB,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,CAAA;IACvC,IAAI,IAAI,GAAG,OAAO,CAAA;IAClB,MAAM,KAAK,CAAA;IACX,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,MAAM,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;gBAChC,MAAM,IAAI,GAAG,GAAG,EAAE;oBAChB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;oBACpB,OAAO,EAAE,CAAA;gBACX,CAAC,CAAA;gBACD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACnB,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,IAAI,IAAI,KAAK,UAAU;YAAE,OAAM;QAC/B,IAAI,GAAG,OAAO,CAAA;QACd,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC","sourcesContent":["/*\n * The count the menu shows on 갤러리 - posts others made since one last opened it (ADR-0165 decision 6). An application\n * puts `generateGalleryBadge` on its menu entry; the count is asked again when a post comes or the gallery is opened.\n */\nimport { galleryApi } from './gallery-api.js'\n\nlet count: number | undefined\nlet version = 0\nconst waiting = new Set<() => void>()\n\n/** Asks the server again and tells every badge. A count that could not be asked is not zero: it draws nothing. */\nexport async function refreshGalleryBadge(): Promise<void> {\n try {\n count = (await galleryApi.summary()).fresh || undefined\n } catch {\n count = undefined\n }\n version++\n for (const wake of [...waiting]) wake()\n}\n\nlet generation = 0\n\n/** One badge's counts. The menu asks for a new one when it draws again; the one before it then ends. */\nexport async function* generateGalleryBadge() {\n const mine = ++generation\n for (const wake of [...waiting]) wake()\n let seen = version\n yield count\n while (true) {\n if (seen === version) {\n await new Promise<void>(resolve => {\n const wake = () => {\n waiting.delete(wake)\n resolve()\n }\n waiting.add(wake)\n })\n }\n if (mine !== generation) return\n seen = version\n yield count\n }\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export declare const t: (key: string, options?: Record<string, unknown>) => string;
2
+ /** A day as the reader's locale writes it. */
3
+ export declare function day(value?: string | Date | null): string;
4
+ /** An event's days - one day, or its first and last. */
5
+ export declare function days(startAt: string, endAt: string): string;
6
+ /** The words of a refusal the server gave, or the fallback. */
7
+ export declare const reasonOf: (error: unknown) => string;
@@ -0,0 +1,20 @@
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
+ export const t = (key, options = {}) => String(i18next.t(key, options));
6
+ /** A day as the reader's locale writes it. */
7
+ export function day(value) {
8
+ if (!value)
9
+ return '';
10
+ return new Date(value).toLocaleDateString(i18next.language, { year: 'numeric', month: 'short', day: 'numeric' });
11
+ }
12
+ /** An event's days - one day, or its first and last. */
13
+ export function days(startAt, endAt) {
14
+ const start = new Date(startAt);
15
+ const end = new Date(endAt);
16
+ return start.toDateString() === end.toDateString() ? day(start) : `${day(start)} – ${day(end)}`;
17
+ }
18
+ /** The words of a refusal the server gave, or the fallback. */
19
+ export const reasonOf = (error) => error?.message || t('gallery.text.failed');
20
+ //# sourceMappingURL=gallery-words.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gallery-words.js","sourceRoot":"","sources":["../../client/utils/gallery-words.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAW,EAAE,UAAmC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAA;AAExG,8CAA8C;AAC9C,MAAM,UAAU,GAAG,CAAC,KAA4B;IAC9C,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAA;IACrB,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAA;AAClH,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,IAAI,CAAC,OAAe,EAAE,KAAa;IACjD,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/B,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAA;IAC3B,OAAO,KAAK,CAAC,YAAY,EAAE,KAAK,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,EAAE,CAAA;AACjG,CAAC;AAED,+DAA+D;AAC/D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAE,EAAE,CAAE,KAAe,EAAE,OAAO,IAAI,CAAC,CAAC,qBAAqB,CAAC,CAAA","sourcesContent":["/*\n * The words of the gallery - one place for the page, the viewer and the uploader.\n */\nimport { i18next } from '@operato/i18n'\n\nexport const t = (key: string, options: Record<string, unknown> = {}) => String(i18next.t(key, options))\n\n/** A day as the reader's locale writes it. */\nexport function day(value?: string | Date | null): string {\n if (!value) return ''\n return new Date(value).toLocaleDateString(i18next.language, { year: 'numeric', month: 'short', day: 'numeric' })\n}\n\n/** An event's days - one day, or its first and last. */\nexport function days(startAt: string, endAt: string): string {\n const start = new Date(startAt)\n const end = new Date(endAt)\n return start.toDateString() === end.toDateString() ? day(start) : `${day(start)} – ${day(end)}`\n}\n\n/** The words of a refusal the server gave, or the fallback. */\nexport const reasonOf = (error: unknown) => (error as Error)?.message || t('gallery.text.failed')\n"]}
@@ -0,0 +1,10 @@
1
+ /** The longest side a photo keeps. */
2
+ export declare const LONG_SIDE = 2560;
3
+ /** "YYYY:MM:DD HH:MM:SS" as the camera wrote it, in the camera's local time. */
4
+ export declare function exifDateOf(text?: string): Date | undefined;
5
+ /** The date the camera recorded the photo, from a JPEG's EXIF - when it was taken (0x9003), else when it was saved (0x0132). */
6
+ export declare function exifTakenAt(buffer: ArrayBuffer): Date | undefined;
7
+ /** When the photo was taken - the camera's record, or nothing. */
8
+ export declare function takenAtOf(file: File): Promise<Date | undefined>;
9
+ /** 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. */
10
+ export declare function shrunk(file: File): Promise<File>;
@@ -0,0 +1,102 @@
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
+ /** The longest side a photo keeps. */
6
+ export const LONG_SIDE = 2560;
7
+ /* A photo already small enough goes up as it is - shrinking again would only lose quality. */
8
+ const SMALL_ENOUGH = 2.5 * 1024 * 1024;
9
+ /** "YYYY:MM:DD HH:MM:SS" as the camera wrote it, in the camera's local time. */
10
+ export function exifDateOf(text) {
11
+ const match = /^(\d{4}):(\d{2}):(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(text ?? '');
12
+ if (!match)
13
+ return undefined;
14
+ const [, y, mo, d, h, mi, s] = match.map(Number);
15
+ const date = new Date(y, mo - 1, d, h, mi, s);
16
+ return isNaN(date.getTime()) || y < 1990 ? undefined : date;
17
+ }
18
+ /** The date the camera recorded the photo, from a JPEG's EXIF - when it was taken (0x9003), else when it was saved (0x0132). */
19
+ export function exifTakenAt(buffer) {
20
+ try {
21
+ const view = new DataView(buffer);
22
+ if (view.byteLength < 4 || view.getUint16(0) !== 0xffd8)
23
+ return undefined;
24
+ let offset = 2;
25
+ while (offset + 10 <= view.byteLength) {
26
+ const marker = view.getUint16(offset);
27
+ if ((marker & 0xff00) !== 0xff00)
28
+ 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)
32
+ return exifDateOf(tiffDate(view, offset + 10));
33
+ offset += 2 + size;
34
+ }
35
+ }
36
+ catch {
37
+ /* A broken or cut record says nothing. */
38
+ }
39
+ return undefined;
40
+ }
41
+ function tiffDate(view, start) {
42
+ const little = view.getUint16(start) === 0x4949;
43
+ const u16 = (at) => view.getUint16(start + at, little);
44
+ const u32 = (at) => view.getUint32(start + at, little);
45
+ const entries = (ifd) => Array.from({ length: u16(ifd) }, (_, i) => ifd + 2 + i * 12);
46
+ const ascii = (entry) => {
47
+ const count = u32(entry + 4);
48
+ const at = count > 4 ? u32(entry + 8) : entry + 8;
49
+ let text = '';
50
+ for (let i = 0; i < count - 1; i++)
51
+ text += String.fromCharCode(view.getUint8(start + at + i));
52
+ return text;
53
+ };
54
+ let saved;
55
+ let exif;
56
+ for (const entry of entries(u32(4))) {
57
+ const tag = u16(entry);
58
+ if (tag === 0x0132)
59
+ saved = ascii(entry);
60
+ if (tag === 0x8769)
61
+ exif = u32(entry + 8);
62
+ }
63
+ if (exif)
64
+ for (const entry of entries(exif))
65
+ if (u16(entry) === 0x9003)
66
+ return ascii(entry);
67
+ return saved;
68
+ }
69
+ /** When the photo was taken - the camera's record, or nothing. */
70
+ export async function takenAtOf(file) {
71
+ if (!/jpe?g/i.test(file.type) && !/\.jpe?g$/i.test(file.name))
72
+ return undefined;
73
+ return exifTakenAt(await file.slice(0, 256 * 1024).arrayBuffer());
74
+ }
75
+ /** 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. */
76
+ export async function shrunk(file) {
77
+ try {
78
+ const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' });
79
+ const long = Math.max(bitmap.width, bitmap.height);
80
+ if (long <= LONG_SIDE && file.size <= SMALL_ENOUGH) {
81
+ bitmap.close();
82
+ return file;
83
+ }
84
+ const scale = Math.min(1, LONG_SIDE / long);
85
+ const canvas = document.createElement('canvas');
86
+ canvas.width = Math.round(bitmap.width * scale);
87
+ canvas.height = Math.round(bitmap.height * scale);
88
+ canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height);
89
+ bitmap.close();
90
+ const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.88));
91
+ if (!blob)
92
+ return file;
93
+ return new File([blob], file.name.replace(/\.[^.]+$/, '') + '.jpg', {
94
+ type: 'image/jpeg',
95
+ lastModified: file.lastModified
96
+ });
97
+ }
98
+ catch {
99
+ return file;
100
+ }
101
+ }
102
+ //# sourceMappingURL=photo-prep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"photo-prep.js","sourceRoot":"","sources":["../../client/utils/photo-prep.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A photo made ready in the browser before it goes up (ADR-0165 decision 3): when it was taken, read from the camera's\n * own record (EXIF), and the photo shrunk to 2560 px on its long side so a phone's original does not fill the storage.\n */\n\n/** The longest side a photo keeps. */\nexport const LONG_SIDE = 2560\n/* A photo already small enough goes up as it is - shrinking again would only lose quality. */\nconst SMALL_ENOUGH = 2.5 * 1024 * 1024\n\n/** \"YYYY:MM:DD HH:MM:SS\" as the camera wrote it, in the camera's local time. */\nexport function exifDateOf(text?: string): Date | undefined {\n const match = /^(\\d{4}):(\\d{2}):(\\d{2})[ T](\\d{2}):(\\d{2}):(\\d{2})/.exec(text ?? '')\n if (!match) return undefined\n const [, y, mo, d, h, mi, s] = match.map(Number)\n const date = new Date(y, mo - 1, d, h, mi, s)\n return isNaN(date.getTime()) || y < 1990 ? undefined : date\n}\n\n/** The date the camera recorded the photo, from a JPEG's EXIF - when it was taken (0x9003), else when it was saved (0x0132). */\nexport function exifTakenAt(buffer: ArrayBuffer): Date | undefined {\n try {\n const view = new DataView(buffer)\n if (view.byteLength < 4 || view.getUint16(0) !== 0xffd8) return undefined\n let offset = 2\n while (offset + 10 <= view.byteLength) {\n const marker = view.getUint16(offset)\n if ((marker & 0xff00) !== 0xff00) return undefined\n const size = view.getUint16(offset + 2)\n /* APP1 starting with \"Exif\\0\\0\" */\n if (marker === 0xffe1 && view.getUint32(offset + 4) === 0x45786966) return exifDateOf(tiffDate(view, offset + 10))\n offset += 2 + size\n }\n } catch {\n /* A broken or cut record says nothing. */\n }\n return undefined\n}\n\nfunction tiffDate(view: DataView, start: number): string | undefined {\n const little = view.getUint16(start) === 0x4949\n const u16 = (at: number) => view.getUint16(start + at, little)\n const u32 = (at: number) => view.getUint32(start + at, little)\n const entries = (ifd: number) => Array.from({ length: u16(ifd) }, (_, i) => ifd + 2 + i * 12)\n const ascii = (entry: number) => {\n const count = u32(entry + 4)\n const at = count > 4 ? u32(entry + 8) : entry + 8\n let text = ''\n for (let i = 0; i < count - 1; i++) text += String.fromCharCode(view.getUint8(start + at + i))\n return text\n }\n let saved: string | undefined\n let exif: number | undefined\n for (const entry of entries(u32(4))) {\n const tag = u16(entry)\n if (tag === 0x0132) saved = ascii(entry)\n if (tag === 0x8769) exif = u32(entry + 8)\n }\n if (exif) for (const entry of entries(exif)) if (u16(entry) === 0x9003) return ascii(entry)\n return saved\n}\n\n/** When the photo was taken - the camera's record, or nothing. */\nexport async function takenAtOf(file: File): Promise<Date | undefined> {\n if (!/jpe?g/i.test(file.type) && !/\\.jpe?g$/i.test(file.name)) return undefined\n return exifTakenAt(await file.slice(0, 256 * 1024).arrayBuffer())\n}\n\n/** 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. */\nexport async function shrunk(file: File): Promise<File> {\n try {\n const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' } as any)\n const long = Math.max(bitmap.width, bitmap.height)\n if (long <= LONG_SIDE && file.size <= SMALL_ENOUGH) {\n bitmap.close()\n return file\n }\n const scale = Math.min(1, LONG_SIDE / long)\n const canvas = document.createElement('canvas')\n canvas.width = Math.round(bitmap.width * scale)\n canvas.height = Math.round(bitmap.height * scale)\n canvas.getContext('2d')!.drawImage(bitmap, 0, 0, canvas.width, canvas.height)\n bitmap.close()\n const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.88))\n if (!blob) return file\n return new File([blob], file.name.replace(/\\.[^.]+$/, '') + '.jpg', {\n type: 'image/jpeg',\n lastModified: file.lastModified\n })\n } catch {\n return file\n }\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function registerGalleryPhotos(): void;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerGalleryPhotos = registerGalleryPhotos;
4
+ /*
5
+ * Who may open a gallery photo by its path - whoever sees the post (ADR-0165 decision 7). attachment-base has already
6
+ * checked the file is of the request's domain.
7
+ */
8
+ const attachment_base_1 = require("@things-factory/attachment-base");
9
+ const gallery_service_1 = require("./service/gallery/gallery-service");
10
+ function registerGalleryPhotos() {
11
+ (0, attachment_base_1.registerAttachmentReader)(gallery_service_1.GALLERY_REF_TYPE, async (attachment, context) => !!attachment.refBy && !!(await (0, gallery_service_1.visiblePost)(attachment.refBy, context)));
12
+ }
13
+ //# sourceMappingURL=attachment-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attachment-reader.js","sourceRoot":"","sources":["../server/attachment-reader.ts"],"names":[],"mappings":";;AAQA,sDAKC;AAbD;;;GAGG;AACH,qEAA0E;AAE1E,uEAAiF;AAEjF,SAAgB,qBAAqB;IACnC,IAAA,0CAAwB,EACtB,kCAAgB,EAChB,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,IAAA,6BAAW,EAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CACtG,CAAA;AACH,CAAC","sourcesContent":["/*\n * Who may open a gallery photo by its path - whoever sees the post (ADR-0165 decision 7). attachment-base has already\n * checked the file is of the request's domain.\n */\nimport { registerAttachmentReader } from '@things-factory/attachment-base'\n\nimport { GALLERY_REF_TYPE, visiblePost } from './service/gallery/gallery-service'\n\nexport function registerGalleryPhotos(): void {\n registerAttachmentReader(\n GALLERY_REF_TYPE,\n async (attachment, context) => !!attachment.refBy && !!(await visiblePost(attachment.refBy, context))\n )\n}\n"]}
@@ -0,0 +1 @@
1
+ export * from './service';
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./service"), exports);
5
+ const auth_base_1 = require("@things-factory/auth-base");
6
+ const attachment_reader_1 = require("./attachment-reader");
7
+ const role_tasks_1 = require("./role-tasks");
8
+ for (const task of role_tasks_1.ROLE_TASKS)
9
+ (0, auth_base_1.registerRoleTask)(task);
10
+ (0, attachment_reader_1.registerGalleryPhotos)();
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA,oDAAyB;AAEzB,yDAA4D;AAE5D,2DAA2D;AAC3D,6CAAyC;AAEzC,KAAK,MAAM,IAAI,IAAI,uBAAU;IAAE,IAAA,4BAAgB,EAAC,IAAI,CAAC,CAAA;AACrD,IAAA,yCAAqB,GAAE,CAAA","sourcesContent":["export * from './service'\n\nimport { registerRoleTask } from '@things-factory/auth-base'\n\nimport { registerGalleryPhotos } from './attachment-reader'\nimport { ROLE_TASKS } from './role-tasks'\n\nfor (const task of ROLE_TASKS) registerRoleTask(task)\nregisterGalleryPhotos()\n"]}
@@ -0,0 +1,2 @@
1
+ import type { RoleTask } from '@things-factory/auth-base';
2
+ export declare const ROLE_TASKS: RoleTask[];
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ /*
3
+ * The work this module names, for the seats applications build (ADR-0139 decision 11).
4
+ *
5
+ * Looking, posting and liking are no task: everyone of the domain does them (ADR-0165 decision 2). Hiding a post,
6
+ * tidying the tags and making an event from the gallery are `gallery.moderation`.
7
+ *
8
+ * Data only - `server/index.ts` registers it - so an application's tests read it without starting the framework.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ROLE_TASKS = void 0;
12
+ exports.ROLE_TASKS = [
13
+ { id: 'gallery.moderation', grants: [{ category: 'gallery', axes: ['mutation'] }] }
14
+ ];
15
+ //# sourceMappingURL=role-tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-tasks.js","sourceRoot":"","sources":["../server/role-tasks.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAIU,QAAA,UAAU,GAAe;IACpC,EAAE,EAAE,EAAE,oBAAoB,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE;CACpF,CAAA","sourcesContent":["/*\n * The work this module names, for the seats applications build (ADR-0139 decision 11).\n *\n * Looking, posting and liking are no task: everyone of the domain does them (ADR-0165 decision 2). Hiding a post,\n * tidying the tags and making an event from the gallery are `gallery.moderation`.\n *\n * Data only - `server/index.ts` registers it - so an application's tests read it without starting the framework.\n */\n\nimport type { RoleTask } from '@things-factory/auth-base'\n\nexport const ROLE_TASKS: RoleTask[] = [\n { id: 'gallery.moderation', grants: [{ category: 'gallery', axes: ['mutation'] }] }\n]\n"]}
@@ -0,0 +1,29 @@
1
+ import type { FileUpload } from 'graphql-upload/GraphQLUpload.js';
2
+ import { User } from '@things-factory/auth-base';
3
+ import { GalleryPost } from './gallery';
4
+ import { GalleryEventCard, GalleryEventChoice, GalleryEventFacts, GalleryEventInput, GalleryFilter, GalleryLikeState, GalleryPerson, GalleryPhoto, GalleryPostInput, GalleryPostList, GallerySummary, GalleryTagCount } from './gallery-types';
5
+ export declare class GalleryResolver {
6
+ galleryPosts(filter: GalleryFilter | undefined, skip: number | undefined, take: number | undefined, context: ResolverContext): Promise<GalleryPostList>;
7
+ galleryPost(id: string, context: ResolverContext): Promise<GalleryPost | undefined>;
8
+ galleryLikers(id: string, context: ResolverContext): Promise<GalleryPerson[]>;
9
+ galleryTags(text: string | undefined, context: ResolverContext): Promise<GalleryTagCount[]>;
10
+ galleryEvents(context: ResolverContext): Promise<GalleryEventCard[]>;
11
+ galleryEventChoices(takenAt: Date | undefined, context: ResolverContext): Promise<GalleryEventChoice[]>;
12
+ gallerySummary(context: ResolverContext): Promise<GallerySummary>;
13
+ createGalleryPost(input: GalleryPostInput, photos: FileUpload[], context: ResolverContext): Promise<GalleryPost>;
14
+ updateGalleryPost(id: string, input: GalleryPostInput, attach: FileUpload[] | undefined, remove: string[] | undefined, context: ResolverContext): Promise<GalleryPost>;
15
+ deleteGalleryPost(id: string, context: ResolverContext): Promise<boolean>;
16
+ likeGalleryPost(id: string, like: boolean, context: ResolverContext): Promise<GalleryLikeState>;
17
+ visitGallery(context: ResolverContext): Promise<boolean>;
18
+ hideGalleryPost(id: string, reason: string, context: ResolverContext): Promise<GalleryPost>;
19
+ unhideGalleryPost(id: string, context: ResolverContext): Promise<GalleryPost>;
20
+ renameGalleryTag(from: string, to: string, context: ResolverContext): Promise<number>;
21
+ createGalleryEvent(input: GalleryEventInput, context: ResolverContext): Promise<GalleryEventCard>;
22
+ photos(post: GalleryPost, context: ResolverContext): Promise<GalleryPhoto[]>;
23
+ creator(post: GalleryPost): Promise<User | undefined>;
24
+ event(post: GalleryPost): Promise<GalleryEventFacts | undefined>;
25
+ liked(post: GalleryPost, context: ResolverContext): Promise<boolean>;
26
+ editable(post: GalleryPost, context: ResolverContext): boolean;
27
+ moderatable(context: ResolverContext): Promise<boolean>;
28
+ hiddenReason(post: GalleryPost, context: ResolverContext): Promise<string | undefined>;
29
+ }