@internetarchive/collection-browser 4.8.1-webdev-9227.4 → 4.9.1-webdev-9670.0

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 (105) hide show
  1. package/README.md +11 -33
  2. package/dist/src/app-root.d.ts +11 -0
  3. package/dist/src/app-root.js +95 -1
  4. package/dist/src/app-root.js.map +1 -1
  5. package/dist/src/collection-browser.d.ts +43 -2
  6. package/dist/src/collection-browser.js +221 -15
  7. package/dist/src/collection-browser.js.map +1 -1
  8. package/dist/src/collection-facets/facet-row-template.d.ts +1 -1
  9. package/dist/src/collection-facets/facet-row-template.js +7 -14
  10. package/dist/src/collection-facets/facet-row-template.js.map +1 -1
  11. package/dist/src/collection-facets/facet-row.js +1 -3
  12. package/dist/src/collection-facets/facet-row.js.map +1 -1
  13. package/dist/src/collection-facets/more-facets-content.js +5 -6
  14. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  15. package/dist/src/collection-facets/more-facets-scroller.js +2 -3
  16. package/dist/src/collection-facets/more-facets-scroller.js.map +1 -1
  17. package/dist/src/collection-facets/smart-facets/heuristics/wikidata/wikidata-entity-map.js +4 -13
  18. package/dist/src/collection-facets/smart-facets/heuristics/wikidata/wikidata-entity-map.js.map +1 -1
  19. package/dist/src/collection-facets/smart-facets/smart-facet-bar.js +2 -6
  20. package/dist/src/collection-facets/smart-facets/smart-facet-bar.js.map +1 -1
  21. package/dist/src/collection-facets.js +5 -6
  22. package/dist/src/collection-facets.js.map +1 -1
  23. package/dist/src/empty-placeholder.d.ts +7 -7
  24. package/dist/src/empty-placeholder.js +15 -29
  25. package/dist/src/empty-placeholder.js.map +1 -1
  26. package/dist/src/expanded-date-picker.js +2 -3
  27. package/dist/src/expanded-date-picker.js.map +1 -1
  28. package/dist/src/manage/manage-bar.d.ts +2 -3
  29. package/dist/src/manage/manage-bar.js +16 -15
  30. package/dist/src/manage/manage-bar.js.map +1 -1
  31. package/dist/src/manage/remove-items-modal-content.js +2 -3
  32. package/dist/src/manage/remove-items-modal-content.js.map +1 -1
  33. package/dist/src/mediatype/mediatype-config.js +18 -55
  34. package/dist/src/mediatype/mediatype-config.js.map +1 -1
  35. package/dist/src/models.js +40 -120
  36. package/dist/src/models.js.map +1 -1
  37. package/dist/src/sort-filter-bar/alpha-bar-tooltip.js +2 -8
  38. package/dist/src/sort-filter-bar/alpha-bar-tooltip.js.map +1 -1
  39. package/dist/src/sort-filter-bar/alpha-bar.js +2 -7
  40. package/dist/src/sort-filter-bar/alpha-bar.js.map +1 -1
  41. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +12 -0
  42. package/dist/src/sort-filter-bar/sort-filter-bar.js +109 -28
  43. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  44. package/dist/src/tiles/grid/account-tile.js +2 -3
  45. package/dist/src/tiles/grid/account-tile.js.map +1 -1
  46. package/dist/src/tiles/grid/collection-tile.js +11 -12
  47. package/dist/src/tiles/grid/collection-tile.js.map +1 -1
  48. package/dist/src/tiles/grid/item-tile.d.ts +4 -0
  49. package/dist/src/tiles/grid/item-tile.js +23 -8
  50. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  51. package/dist/src/tiles/grid/tile-stats.js +3 -6
  52. package/dist/src/tiles/grid/tile-stats.js.map +1 -1
  53. package/dist/src/tiles/hover/hover-pane-controller.d.ts +2 -1
  54. package/dist/src/tiles/hover/hover-pane-controller.js +1 -1
  55. package/dist/src/tiles/hover/hover-pane-controller.js.map +1 -1
  56. package/dist/src/tiles/image-block.d.ts +5 -0
  57. package/dist/src/tiles/image-block.js +29 -0
  58. package/dist/src/tiles/image-block.js.map +1 -1
  59. package/dist/src/tiles/list/tile-list-compact-header.js +2 -3
  60. package/dist/src/tiles/list/tile-list-compact-header.js.map +1 -1
  61. package/dist/src/tiles/list/tile-list-compact.js +1 -3
  62. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  63. package/dist/src/tiles/list/tile-list.js +5 -15
  64. package/dist/src/tiles/list/tile-list.js.map +1 -1
  65. package/dist/src/tiles/models.d.ts +2 -1
  66. package/dist/src/tiles/models.js.map +1 -1
  67. package/dist/src/tiles/overlay/text-overlay.js +1 -3
  68. package/dist/src/tiles/overlay/text-overlay.js.map +1 -1
  69. package/dist/src/tiles/preview/tile-preview-controller.d.ts +61 -0
  70. package/dist/src/tiles/preview/tile-preview-controller.js +216 -0
  71. package/dist/src/tiles/preview/tile-preview-controller.js.map +1 -0
  72. package/dist/src/tiles/preview/tile-preview-media.d.ts +44 -0
  73. package/dist/src/tiles/preview/tile-preview-media.js +151 -0
  74. package/dist/src/tiles/preview/tile-preview-media.js.map +1 -0
  75. package/dist/src/tiles/preview/tile-preview.d.ts +66 -0
  76. package/dist/src/tiles/preview/tile-preview.js +494 -0
  77. package/dist/src/tiles/preview/tile-preview.js.map +1 -0
  78. package/dist/src/tiles/review-block.js +2 -3
  79. package/dist/src/tiles/review-block.js.map +1 -1
  80. package/dist/src/tiles/tile-dispatcher.d.ts +8 -2
  81. package/dist/src/tiles/tile-dispatcher.js +36 -6
  82. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  83. package/dist/src/tiles/tile-mediatype-icon.js +1 -3
  84. package/dist/src/tiles/tile-mediatype-icon.js.map +1 -1
  85. package/dist/src/utils/format-count.d.ts +1 -3
  86. package/dist/src/utils/format-count.js +59 -19
  87. package/dist/src/utils/format-count.js.map +1 -1
  88. package/dist/src/utils/format-unit-size.d.ts +1 -1
  89. package/dist/src/utils/format-unit-size.js +18 -21
  90. package/dist/src/utils/format-unit-size.js.map +1 -1
  91. package/dist/src/utils/grid-zoom-controller.d.ts +69 -0
  92. package/dist/src/utils/grid-zoom-controller.js +261 -0
  93. package/dist/src/utils/grid-zoom-controller.js.map +1 -0
  94. package/dist/src/utils/grid-zoom.d.ts +31 -0
  95. package/dist/src/utils/grid-zoom.js +109 -0
  96. package/dist/src/utils/grid-zoom.js.map +1 -0
  97. package/package.json +2 -6
  98. package/dist/src/locales/es.d.ts +0 -200
  99. package/dist/src/locales/es.js +0 -215
  100. package/dist/src/locales/es.js.map +0 -1
  101. package/dist/src/utils/get-locale.d.ts +0 -14
  102. package/dist/src/utils/get-locale.js +0 -20
  103. package/dist/src/utils/get-locale.js.map +0 -1
  104. package/locales/es.d.ts +0 -1
  105. package/locales/es.js +0 -1
@@ -0,0 +1,216 @@
1
+ /** How long the pointer must rest on a tile before its preview starts. */
2
+ export const HOVER_INTENT_MS = 300;
3
+ /** Keyboard focus waits a little longer, so tabbing through tiles stays quiet. */
4
+ export const FOCUS_INTENT_MS = 600;
5
+ export const LONG_PRESS_MS = 500;
6
+ /** How long the hover info pane waits on a tile that shows previews (a bit longer than one preview cycle). */
7
+ export const PREVIEW_PANE_DELAY_MS = 5000;
8
+ const LONG_PRESS_MOVE_TOLERANCE_PX = 10;
9
+ /** The tile whose preview is currently showing. Only one is ever active. */
10
+ let current;
11
+ /** Whether this user and connection should see animated previews at all. */
12
+ export function previewsAllowed() {
13
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {
14
+ return false;
15
+ }
16
+ const connection = navigator.connection;
17
+ if (connection?.saveData)
18
+ return false;
19
+ if (/^(slow-)?2g$/.test(connection?.effectiveType ?? ''))
20
+ return false;
21
+ return true;
22
+ }
23
+ /**
24
+ * Decides when a tile's preview is active: after a hover-intent delay for a
25
+ * mouse, on keyboard focus, and on long-press for touch. Starting one preview
26
+ * stops whichever was active before.
27
+ */
28
+ export class TilePreviewController {
29
+ constructor(host, options) {
30
+ this.host = host;
31
+ this.options = options;
32
+ this.active = false;
33
+ this.suppressClick = false;
34
+ /** Whether the press in progress is the one that started the preview. */
35
+ this.pressStartedPreview = false;
36
+ this.onAnyPointerDown = (e) => {
37
+ if (e.pointerType === 'touch' && !e.isPrimary)
38
+ this.deactivate();
39
+ };
40
+ this.onZoomGesture = () => {
41
+ this.deactivate();
42
+ };
43
+ this.onWheel = (e) => {
44
+ if (e.ctrlKey)
45
+ this.deactivate();
46
+ };
47
+ this.onPointerEnter = (e) => {
48
+ if (e.pointerType === 'touch')
49
+ return;
50
+ this.schedule('hover', HOVER_INTENT_MS);
51
+ };
52
+ this.onPointerLeave = (e) => {
53
+ if (e.pointerType === 'touch')
54
+ return;
55
+ this.deactivate();
56
+ };
57
+ this.onPointerDown = (e) => {
58
+ if (e.pointerType !== 'touch')
59
+ return;
60
+ if (!this.options.enabled() || !previewsAllowed())
61
+ return;
62
+ this.pressStartedPreview = false;
63
+ this.pressOrigin = { x: e.clientX, y: e.clientY };
64
+ this.schedule('long-press', LONG_PRESS_MS);
65
+ };
66
+ this.onPointerMove = (e) => {
67
+ if (!this.pressOrigin)
68
+ return;
69
+ const moved = Math.hypot(e.clientX - this.pressOrigin.x, e.clientY - this.pressOrigin.y);
70
+ if (moved > LONG_PRESS_MOVE_TOLERANCE_PX)
71
+ this.onPointerCancel();
72
+ };
73
+ /** Ends a press. If it started the preview, the click that follows is swallowed. */
74
+ this.onPointerUp = () => {
75
+ if (!this.pressOrigin)
76
+ return;
77
+ this.pressOrigin = undefined;
78
+ if (this.pressStartedPreview) {
79
+ this.pressStartedPreview = false;
80
+ this.suppressClick = true;
81
+ window.clearTimeout(this.suppressTimer);
82
+ this.suppressTimer = window.setTimeout(() => {
83
+ this.suppressClick = false;
84
+ }, 400);
85
+ }
86
+ else {
87
+ window.clearTimeout(this.timer);
88
+ }
89
+ };
90
+ /** A cancelled or moved press never produces a click. */
91
+ this.onPointerCancel = () => {
92
+ if (!this.pressOrigin)
93
+ return;
94
+ this.pressOrigin = undefined;
95
+ this.pressStartedPreview = false;
96
+ window.clearTimeout(this.timer);
97
+ };
98
+ this.onClick = (e) => {
99
+ if (!this.suppressClick)
100
+ return;
101
+ this.suppressClick = false;
102
+ e.preventDefault();
103
+ e.stopPropagation();
104
+ };
105
+ /** Keeps the long-press from opening the browser's link menu. */
106
+ this.onContextMenu = (e) => {
107
+ if (this.pressOrigin || this.active)
108
+ e.preventDefault();
109
+ };
110
+ this.onFocusIn = (e) => {
111
+ const target = e.composedPath()[0];
112
+ if (target?.matches?.(':focus-visible')) {
113
+ this.schedule('focus', FOCUS_INTENT_MS);
114
+ }
115
+ };
116
+ this.onFocusOut = () => {
117
+ this.deactivate();
118
+ };
119
+ this.onKeyDown = (e) => {
120
+ if (e.key === 'Escape')
121
+ this.deactivate();
122
+ };
123
+ this.onOutsidePointerDown = (e) => {
124
+ if (!e.composedPath().includes(this.host))
125
+ this.deactivate();
126
+ };
127
+ this.onScroll = () => {
128
+ this.deactivate();
129
+ };
130
+ host.addController(this);
131
+ }
132
+ hostConnected() {
133
+ const h = this.host;
134
+ h.addEventListener('pointerenter', this.onPointerEnter);
135
+ h.addEventListener('pointerleave', this.onPointerLeave);
136
+ h.addEventListener('pointerdown', this.onPointerDown);
137
+ h.addEventListener('pointermove', this.onPointerMove);
138
+ h.addEventListener('pointerup', this.onPointerUp);
139
+ h.addEventListener('pointercancel', this.onPointerCancel);
140
+ h.addEventListener('contextmenu', this.onContextMenu);
141
+ h.addEventListener('click', this.onClick, true);
142
+ h.addEventListener('focusin', this.onFocusIn);
143
+ h.addEventListener('focusout', this.onFocusOut);
144
+ h.addEventListener('keydown', this.onKeyDown);
145
+ }
146
+ hostDisconnected() {
147
+ const h = this.host;
148
+ h.removeEventListener('pointerenter', this.onPointerEnter);
149
+ h.removeEventListener('pointerleave', this.onPointerLeave);
150
+ h.removeEventListener('pointerdown', this.onPointerDown);
151
+ h.removeEventListener('pointermove', this.onPointerMove);
152
+ h.removeEventListener('pointerup', this.onPointerUp);
153
+ h.removeEventListener('pointercancel', this.onPointerCancel);
154
+ h.removeEventListener('contextmenu', this.onContextMenu);
155
+ h.removeEventListener('click', this.onClick, true);
156
+ h.removeEventListener('focusin', this.onFocusIn);
157
+ h.removeEventListener('focusout', this.onFocusOut);
158
+ h.removeEventListener('keydown', this.onKeyDown);
159
+ this.deactivate();
160
+ }
161
+ /** Stops this tile's preview and any pending start. */
162
+ deactivate() {
163
+ window.clearTimeout(this.timer);
164
+ window.clearTimeout(this.suppressTimer);
165
+ this.timer = undefined;
166
+ this.pressOrigin = undefined;
167
+ this.pressStartedPreview = false;
168
+ document.removeEventListener('pointerdown', this.onOutsidePointerDown, true);
169
+ window.removeEventListener('scroll', this.onScroll, true);
170
+ this.removeZoomGuards();
171
+ if (current?.owner === this.host)
172
+ current = undefined;
173
+ if (this.active) {
174
+ this.active = false;
175
+ this.host.requestUpdate();
176
+ }
177
+ }
178
+ schedule(how, delay) {
179
+ window.clearTimeout(this.timer);
180
+ if (!this.options.enabled() || !previewsAllowed())
181
+ return;
182
+ this.timer = window.setTimeout(() => this.activate(how), delay);
183
+ this.addZoomGuards();
184
+ }
185
+ /**
186
+ * A pinch (a second finger, a Safari gesture or a trackpad ctrl+wheel) is a
187
+ * zoom, not a request for a preview, so it cancels a pending or active one.
188
+ * The listeners only exist while a preview is pending or showing.
189
+ */
190
+ addZoomGuards() {
191
+ document.addEventListener('pointerdown', this.onAnyPointerDown, true);
192
+ window.addEventListener('gesturestart', this.onZoomGesture);
193
+ window.addEventListener('wheel', this.onWheel, { passive: true });
194
+ }
195
+ removeZoomGuards() {
196
+ document.removeEventListener('pointerdown', this.onAnyPointerDown, true);
197
+ window.removeEventListener('gesturestart', this.onZoomGesture);
198
+ window.removeEventListener('wheel', this.onWheel);
199
+ }
200
+ activate(how) {
201
+ if (!this.options.enabled() || !previewsAllowed())
202
+ return;
203
+ if (current && current.owner !== this.host)
204
+ current.stop();
205
+ current = { owner: this.host, stop: () => this.deactivate() };
206
+ this.active = true;
207
+ this.host.requestUpdate();
208
+ if (how === 'long-press') {
209
+ this.pressStartedPreview = true;
210
+ // A touch preview stays up until the next touch elsewhere or a scroll.
211
+ document.addEventListener('pointerdown', this.onOutsidePointerDown, true);
212
+ window.addEventListener('scroll', this.onScroll, true);
213
+ }
214
+ }
215
+ }
216
+ //# sourceMappingURL=tile-preview-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tile-preview-controller.js","sourceRoot":"","sources":["../../../../src/tiles/preview/tile-preview-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\n\n/** How long the pointer must rest on a tile before its preview starts. */\nexport const HOVER_INTENT_MS = 300;\n/** Keyboard focus waits a little longer, so tabbing through tiles stays quiet. */\nexport const FOCUS_INTENT_MS = 600;\nexport const LONG_PRESS_MS = 500;\n/** How long the hover info pane waits on a tile that shows previews (a bit longer than one preview cycle). */\nexport const PREVIEW_PANE_DELAY_MS = 5000;\nconst LONG_PRESS_MOVE_TOLERANCE_PX = 10;\n\ntype Activation = 'hover' | 'focus' | 'long-press';\n\n/** The tile whose preview is currently showing. Only one is ever active. */\nlet current: { owner: object; stop: () => void } | undefined;\n\n/** Whether this user and connection should see animated previews at all. */\nexport function previewsAllowed(): boolean {\n if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) {\n return false;\n }\n const connection = (\n navigator as Navigator & {\n connection?: { saveData?: boolean; effectiveType?: string };\n }\n ).connection;\n if (connection?.saveData) return false;\n if (/^(slow-)?2g$/.test(connection?.effectiveType ?? '')) return false;\n return true;\n}\n\n/**\n * Decides when a tile's preview is active: after a hover-intent delay for a\n * mouse, on keyboard focus, and on long-press for touch. Starting one preview\n * stops whichever was active before.\n */\nexport class TilePreviewController implements ReactiveController {\n active = false;\n\n private timer?: number;\n\n private pressOrigin?: { x: number; y: number };\n\n private suppressClick = false;\n\n private suppressTimer?: number;\n\n /** Whether the press in progress is the one that started the preview. */\n private pressStartedPreview = false;\n\n constructor(\n private host: ReactiveControllerHost & HTMLElement,\n private options: { enabled: () => boolean },\n ) {\n host.addController(this);\n }\n\n hostConnected(): void {\n const h = this.host;\n h.addEventListener('pointerenter', this.onPointerEnter);\n h.addEventListener('pointerleave', this.onPointerLeave);\n h.addEventListener('pointerdown', this.onPointerDown);\n h.addEventListener('pointermove', this.onPointerMove);\n h.addEventListener('pointerup', this.onPointerUp);\n h.addEventListener('pointercancel', this.onPointerCancel);\n h.addEventListener('contextmenu', this.onContextMenu);\n h.addEventListener('click', this.onClick, true);\n h.addEventListener('focusin', this.onFocusIn);\n h.addEventListener('focusout', this.onFocusOut);\n h.addEventListener('keydown', this.onKeyDown);\n }\n\n hostDisconnected(): void {\n const h = this.host;\n h.removeEventListener('pointerenter', this.onPointerEnter);\n h.removeEventListener('pointerleave', this.onPointerLeave);\n h.removeEventListener('pointerdown', this.onPointerDown);\n h.removeEventListener('pointermove', this.onPointerMove);\n h.removeEventListener('pointerup', this.onPointerUp);\n h.removeEventListener('pointercancel', this.onPointerCancel);\n h.removeEventListener('contextmenu', this.onContextMenu);\n h.removeEventListener('click', this.onClick, true);\n h.removeEventListener('focusin', this.onFocusIn);\n h.removeEventListener('focusout', this.onFocusOut);\n h.removeEventListener('keydown', this.onKeyDown);\n this.deactivate();\n }\n\n /** Stops this tile's preview and any pending start. */\n deactivate(): void {\n window.clearTimeout(this.timer);\n window.clearTimeout(this.suppressTimer);\n this.timer = undefined;\n this.pressOrigin = undefined;\n this.pressStartedPreview = false;\n document.removeEventListener(\n 'pointerdown',\n this.onOutsidePointerDown,\n true,\n );\n window.removeEventListener('scroll', this.onScroll, true);\n this.removeZoomGuards();\n if (current?.owner === this.host) current = undefined;\n if (this.active) {\n this.active = false;\n this.host.requestUpdate();\n }\n }\n\n private schedule(how: Activation, delay: number): void {\n window.clearTimeout(this.timer);\n if (!this.options.enabled() || !previewsAllowed()) return;\n this.timer = window.setTimeout(() => this.activate(how), delay);\n this.addZoomGuards();\n }\n\n /**\n * A pinch (a second finger, a Safari gesture or a trackpad ctrl+wheel) is a\n * zoom, not a request for a preview, so it cancels a pending or active one.\n * The listeners only exist while a preview is pending or showing.\n */\n private addZoomGuards(): void {\n document.addEventListener('pointerdown', this.onAnyPointerDown, true);\n window.addEventListener('gesturestart', this.onZoomGesture);\n window.addEventListener('wheel', this.onWheel, { passive: true });\n }\n\n private removeZoomGuards(): void {\n document.removeEventListener('pointerdown', this.onAnyPointerDown, true);\n window.removeEventListener('gesturestart', this.onZoomGesture);\n window.removeEventListener('wheel', this.onWheel);\n }\n\n private onAnyPointerDown = (e: PointerEvent): void => {\n if (e.pointerType === 'touch' && !e.isPrimary) this.deactivate();\n };\n\n private onZoomGesture = (): void => {\n this.deactivate();\n };\n\n private onWheel = (e: WheelEvent): void => {\n if (e.ctrlKey) this.deactivate();\n };\n\n private activate(how: Activation): void {\n if (!this.options.enabled() || !previewsAllowed()) return;\n if (current && current.owner !== this.host) current.stop();\n current = { owner: this.host, stop: () => this.deactivate() };\n this.active = true;\n this.host.requestUpdate();\n if (how === 'long-press') {\n this.pressStartedPreview = true;\n // A touch preview stays up until the next touch elsewhere or a scroll.\n document.addEventListener('pointerdown', this.onOutsidePointerDown, true);\n window.addEventListener('scroll', this.onScroll, true);\n }\n }\n\n private onPointerEnter = (e: PointerEvent): void => {\n if (e.pointerType === 'touch') return;\n this.schedule('hover', HOVER_INTENT_MS);\n };\n\n private onPointerLeave = (e: PointerEvent): void => {\n if (e.pointerType === 'touch') return;\n this.deactivate();\n };\n\n private onPointerDown = (e: PointerEvent): void => {\n if (e.pointerType !== 'touch') return;\n if (!this.options.enabled() || !previewsAllowed()) return;\n this.pressStartedPreview = false;\n this.pressOrigin = { x: e.clientX, y: e.clientY };\n this.schedule('long-press', LONG_PRESS_MS);\n };\n\n private onPointerMove = (e: PointerEvent): void => {\n if (!this.pressOrigin) return;\n const moved = Math.hypot(\n e.clientX - this.pressOrigin.x,\n e.clientY - this.pressOrigin.y,\n );\n if (moved > LONG_PRESS_MOVE_TOLERANCE_PX) this.onPointerCancel();\n };\n\n /** Ends a press. If it started the preview, the click that follows is swallowed. */\n private onPointerUp = (): void => {\n if (!this.pressOrigin) return;\n this.pressOrigin = undefined;\n if (this.pressStartedPreview) {\n this.pressStartedPreview = false;\n this.suppressClick = true;\n window.clearTimeout(this.suppressTimer);\n this.suppressTimer = window.setTimeout(() => {\n this.suppressClick = false;\n }, 400);\n } else {\n window.clearTimeout(this.timer);\n }\n };\n\n /** A cancelled or moved press never produces a click. */\n private onPointerCancel = (): void => {\n if (!this.pressOrigin) return;\n this.pressOrigin = undefined;\n this.pressStartedPreview = false;\n window.clearTimeout(this.timer);\n };\n\n private onClick = (e: MouseEvent): void => {\n if (!this.suppressClick) return;\n this.suppressClick = false;\n e.preventDefault();\n e.stopPropagation();\n };\n\n /** Keeps the long-press from opening the browser's link menu. */\n private onContextMenu = (e: Event): void => {\n if (this.pressOrigin || this.active) e.preventDefault();\n };\n\n private onFocusIn = (e: FocusEvent): void => {\n const target = e.composedPath()[0] as Element | undefined;\n if (target?.matches?.(':focus-visible')) {\n this.schedule('focus', FOCUS_INTENT_MS);\n }\n };\n\n private onFocusOut = (): void => {\n this.deactivate();\n };\n\n private onKeyDown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape') this.deactivate();\n };\n\n private onOutsidePointerDown = (e: PointerEvent): void => {\n if (!e.composedPath().includes(this.host)) this.deactivate();\n };\n\n private onScroll = (): void => {\n this.deactivate();\n };\n}\n"]}
@@ -0,0 +1,44 @@
1
+ /** What a tile preview shows, resolved from an item's file list. */
2
+ export type TilePreviewMedia = {
3
+ kind: 'video';
4
+ src: string;
5
+ start: number;
6
+ fallbackUrls: string[];
7
+ } | {
8
+ kind: 'slides';
9
+ urls: string[];
10
+ fit: 'contain' | 'cover';
11
+ } | {
12
+ kind: 'audio';
13
+ src: string;
14
+ start: number;
15
+ waveform?: string;
16
+ } | {
17
+ kind: 'zoom';
18
+ };
19
+ /** Clip and snippet length, in seconds. */
20
+ export declare const PREVIEW_CLIP_SECONDS = 4;
21
+ /** What the previews are allowed to fetch. */
22
+ export interface TilePreviewOptions {
23
+ /**
24
+ * Play a real clip of the movie, taken from its smallest playable derivative.
25
+ * A clip costs up to about 4 MB per preview (the browser buffers ahead), so by
26
+ * default movies cycle a few of their stored frame thumbnails instead (about
27
+ * 35 KB). Movies with no playable derivative always use the frames.
28
+ */
29
+ clips?: boolean;
30
+ /**
31
+ * Play video clips and audio snippets with sound. Only takes effect once the
32
+ * page has been activated by a click or key press; otherwise the browser
33
+ * refuses and the preview stays muted.
34
+ */
35
+ sound?: boolean;
36
+ }
37
+ /**
38
+ * Resolves the preview media for an item. One request at most per item per
39
+ * page load (none for texts and fallbacks), and the result, including a
40
+ * failure, is cached so re-hovering never hits the network again.
41
+ */
42
+ export declare function resolvePreviewMedia(identifier: string, mediatype: string, baseUrl?: string, options?: TilePreviewOptions): Promise<TilePreviewMedia>;
43
+ /** Test hook. */
44
+ export declare function clearPreviewMediaCache(): void;
@@ -0,0 +1,151 @@
1
+ /** Clip and snippet length, in seconds. */
2
+ export const PREVIEW_CLIP_SECONDS = 4;
3
+ const MAX_SLIDES = 4;
4
+ const MAX_FRAMES = 6;
5
+ const MIN_CLIP_SOURCE_SECONDS = 8;
6
+ const MAX_IMAGE_BYTES = 1_500_000;
7
+ const PAGE_NUMBERS = [1, 3, 5, 8];
8
+ const cache = new Map();
9
+ /**
10
+ * Resolves the preview media for an item. One request at most per item per
11
+ * page load (none for texts and fallbacks), and the result, including a
12
+ * failure, is cached so re-hovering never hits the network again.
13
+ */
14
+ export function resolvePreviewMedia(identifier, mediatype, baseUrl = 'https://archive.org', options = {}) {
15
+ const key = `${baseUrl}|${identifier}|${options.clips ? 'clip' : ''}`;
16
+ let pending = cache.get(key);
17
+ if (!pending) {
18
+ pending = resolve(identifier, mediatype, baseUrl, options).catch(() => ({
19
+ kind: 'zoom',
20
+ }));
21
+ cache.set(key, pending);
22
+ }
23
+ return pending;
24
+ }
25
+ /** Test hook. */
26
+ export function clearPreviewMediaCache() {
27
+ cache.clear();
28
+ }
29
+ async function resolve(identifier, mediatype, baseUrl, options) {
30
+ const download = (name) => `${baseUrl}/download/${identifier}/${name
31
+ .split('/')
32
+ .map(encodeURIComponent)
33
+ .join('/')}`;
34
+ // Page images come from the BookReader endpoint, so no file list is needed.
35
+ // Pages that don't exist are dropped when they fail to load.
36
+ if (mediatype === 'texts') {
37
+ return {
38
+ kind: 'slides',
39
+ fit: 'contain',
40
+ urls: PAGE_NUMBERS.map(n => download(`page/n${n}_w300.jpg`)),
41
+ };
42
+ }
43
+ const files = await fetchFiles(identifier, baseUrl);
44
+ if (mediatype === 'movies') {
45
+ // Frames are spread across the whole film, skipping the first (the cover).
46
+ const frames = spread(files
47
+ .filter(f => f.format === 'Thumbnail')
48
+ .map(f => f.name)
49
+ .sort()
50
+ .slice(1), MAX_FRAMES).map(download);
51
+ const clip = options.clips ? smallestPlayable(files) : undefined;
52
+ if (clip) {
53
+ return {
54
+ kind: 'video',
55
+ src: download(clip.name),
56
+ start: clipStart(clip.length),
57
+ fallbackUrls: frames,
58
+ };
59
+ }
60
+ return frames.length > 1
61
+ ? { kind: 'slides', fit: 'cover', urls: frames }
62
+ : { kind: 'zoom' };
63
+ }
64
+ if (mediatype === 'audio' || mediatype === 'etree') {
65
+ const mp3 = files
66
+ .filter(f => f.name.toLowerCase().endsWith('.mp3'))
67
+ .sort((a, b) => a.name.localeCompare(b.name))[0];
68
+ if (!mp3)
69
+ return { kind: 'zoom' };
70
+ const png = files.find(f => f.name === mp3.name.replace(/\.mp3$/i, '.png'));
71
+ return {
72
+ kind: 'audio',
73
+ src: download(mp3.name),
74
+ start: startAt(mp3.length),
75
+ waveform: png && download(png.name),
76
+ };
77
+ }
78
+ if (mediatype === 'image') {
79
+ const images = files
80
+ .filter(f => f.format === 'JPEG' &&
81
+ Number(f.size ?? 0) > 0 &&
82
+ Number(f.size) <= MAX_IMAGE_BYTES)
83
+ .map(f => download(f.name))
84
+ .slice(0, MAX_SLIDES);
85
+ return images.length > 1
86
+ ? { kind: 'slides', fit: 'cover', urls: images }
87
+ : { kind: 'zoom' };
88
+ }
89
+ return { kind: 'zoom' };
90
+ }
91
+ async function fetchFiles(identifier, baseUrl) {
92
+ const res = await fetch(`${baseUrl}/metadata/${identifier}/files`);
93
+ if (!res.ok)
94
+ return [];
95
+ const body = await res.json();
96
+ return Array.isArray(body.result) ? body.result : [];
97
+ }
98
+ /**
99
+ * The smallest derivative this browser can play (mp4 or webm, at least a few
100
+ * seconds long). The smallest has the lowest bitrate and the shortest header, so
101
+ * it starts soonest.
102
+ */
103
+ function smallestPlayable(files) {
104
+ const video = document.createElement('video');
105
+ return files
106
+ .filter(f => {
107
+ const ext = f.name.toLowerCase().split('.').pop();
108
+ const type = ext === 'webm'
109
+ ? 'video/webm'
110
+ : ext === 'mp4' || ext === 'm4v'
111
+ ? 'video/mp4'
112
+ : '';
113
+ const seconds = parseLength(f.length);
114
+ return (!!type &&
115
+ f.format !== 'Thumbnail' &&
116
+ Number(f.size) > 0 &&
117
+ (seconds === 0 || seconds >= MIN_CLIP_SOURCE_SECONDS) &&
118
+ !!video.canPlayType(type));
119
+ })
120
+ .sort((a, b) => Number(a.size) - Number(b.size))[0];
121
+ }
122
+ /** Seconds into a film to start at: 10% in, to skip leading black, capped at 60. */
123
+ function clipStart(length) {
124
+ const seconds = parseLength(length);
125
+ if (!seconds)
126
+ return 0;
127
+ return Math.max(0, Math.min(60, Math.floor(seconds / 10), seconds - PREVIEW_CLIP_SECONDS));
128
+ }
129
+ /** Picks `count` evenly spaced entries from `list`. */
130
+ function spread(list, count) {
131
+ if (list.length <= count)
132
+ return list;
133
+ return Array.from({ length: count }, (_, i) => list[Math.floor((i * list.length) / count)]);
134
+ }
135
+ /** Seconds into the media to start at: a third of the way in, capped at 60. */
136
+ function startAt(length) {
137
+ const seconds = parseLength(length);
138
+ if (!seconds)
139
+ return 0;
140
+ return Math.max(0, Math.min(60, Math.floor(seconds / 3), seconds - PREVIEW_CLIP_SECONDS));
141
+ }
142
+ /** Parses a file `length`, either plain seconds ("16.23") or "mm:ss" / "h:mm:ss". */
143
+ function parseLength(length) {
144
+ if (!length)
145
+ return 0;
146
+ const parts = length.split(':').map(Number);
147
+ if (parts.some(Number.isNaN))
148
+ return 0;
149
+ return parts.reduce((total, part) => total * 60 + part, 0);
150
+ }
151
+ //# sourceMappingURL=tile-preview-media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tile-preview-media.js","sourceRoot":"","sources":["../../../../src/tiles/preview/tile-preview-media.ts"],"names":[],"mappings":"AAcA,2CAA2C;AAC3C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAmBtC,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,MAAM,UAAU,GAAG,CAAC,CAAC;AACrB,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAClC,MAAM,eAAe,GAAG,SAAS,CAAC;AAClC,MAAM,YAAY,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAElC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqC,CAAC;AAE3D;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAAkB,EAClB,SAAiB,EACjB,OAAO,GAAG,qBAAqB,EAC/B,UAA8B,EAAE;IAEhC,MAAM,GAAG,GAAG,GAAG,OAAO,IAAI,UAAU,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACtE,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;YACtE,IAAI,EAAE,MAAe;SACtB,CAAC,CAAC,CAAC;QACJ,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,sBAAsB;IACpC,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,OAAO,CACpB,UAAkB,EAClB,SAAiB,EACjB,OAAe,EACf,OAA2B;IAE3B,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE,CAChC,GAAG,OAAO,aAAa,UAAU,IAAI,IAAI;SACtC,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,kBAAkB,CAAC;SACvB,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAEjB,4EAA4E;IAC5E,6DAA6D;IAC7D,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;QAC1B,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,GAAG,EAAE,SAAS;YACd,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;SAC7D,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAEpD,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC3B,2EAA2E;QAC3E,MAAM,MAAM,GAAG,MAAM,CACnB,KAAK;aACF,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC;aACrC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAChB,IAAI,EAAE;aACN,KAAK,CAAC,CAAC,CAAC,EACX,UAAU,CACX,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAEhB,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACjE,IAAI,IAAI,EAAE,CAAC;YACT,OAAO;gBACL,IAAI,EAAE,OAAO;gBACb,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;gBACxB,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC7B,YAAY,EAAE,MAAM;aACrB,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC;YACtB,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE;YAChD,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACvB,CAAC;IAED,IAAI,SAAS,KAAK,OAAO,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,KAAK;aACd,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;aAClD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QAC5E,OAAO;YACL,IAAI,EAAE,OAAO;YACb,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YACvB,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;YAC1B,QAAQ,EAAE,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;SACpC,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,KAAK;aACjB,MAAM,CACL,CAAC,CAAC,EAAE,CACF,CAAC,CAAC,MAAM,KAAK,MAAM;YACnB,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC;YACvB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,eAAe,CACpC;aACA,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;aAC1B,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QACxB,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC;YACtB,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE;YAChD,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACvB,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,UAAkB,EAClB,OAAe;IAEf,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,aAAa,UAAU,QAAQ,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,CAAC;IACvB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,KAAiB;IACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAE;QACV,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;QAClD,MAAM,IAAI,GACR,GAAG,KAAK,MAAM;YACZ,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,KAAK;gBAC9B,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,EAAE,CAAC;QACX,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACtC,OAAO,CACL,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,MAAM,KAAK,WAAW;YACxB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YAClB,CAAC,OAAO,KAAK,CAAC,IAAI,OAAO,IAAI,uBAAuB,CAAC;YACrD,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAC1B,CAAC;IACJ,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,oFAAoF;AACpF,SAAS,SAAS,CAAC,MAAe;IAChC,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC,OAAO;QAAE,OAAO,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC,GAAG,CACb,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC,CACvE,CAAC;AACJ,CAAC;AAED,uDAAuD;AACvD,SAAS,MAAM,CAAI,IAAS,EAAE,KAAa;IACzC,IAAI,IAAI,CAAC,MAAM,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,KAAK,CAAC,IAAI,CACf,EAAE,MAAM,EAAE,KAAK,EAAE,EACjB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CACtD,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,SAAS,OAAO,CAAC,MAAe;IAC9B,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,CAAC,OAAO;QAAE,OAAO,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC,GAAG,CACb,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC,CACtE,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,SAAS,WAAW,CAAC,MAAe;IAClC,IAAI,CAAC,MAAM;QAAE,OAAO,CAAC,CAAC;IACtB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;AAC7D,CAAC","sourcesContent":["/** What a tile preview shows, resolved from an item's file list. */\nexport type TilePreviewMedia =\n | { kind: 'video'; src: string; start: number; fallbackUrls: string[] }\n | { kind: 'slides'; urls: string[]; fit: 'contain' | 'cover' }\n | { kind: 'audio'; src: string; start: number; waveform?: string }\n | { kind: 'zoom' };\n\ninterface ItemFile {\n name: string;\n format?: string;\n length?: string;\n size?: string;\n}\n\n/** Clip and snippet length, in seconds. */\nexport const PREVIEW_CLIP_SECONDS = 4;\n\n/** What the previews are allowed to fetch. */\nexport interface TilePreviewOptions {\n /**\n * Play a real clip of the movie, taken from its smallest playable derivative.\n * A clip costs up to about 4 MB per preview (the browser buffers ahead), so by\n * default movies cycle a few of their stored frame thumbnails instead (about\n * 35 KB). Movies with no playable derivative always use the frames.\n */\n clips?: boolean;\n /**\n * Play video clips and audio snippets with sound. Only takes effect once the\n * page has been activated by a click or key press; otherwise the browser\n * refuses and the preview stays muted.\n */\n sound?: boolean;\n}\n\nconst MAX_SLIDES = 4;\nconst MAX_FRAMES = 6;\nconst MIN_CLIP_SOURCE_SECONDS = 8;\nconst MAX_IMAGE_BYTES = 1_500_000;\nconst PAGE_NUMBERS = [1, 3, 5, 8];\n\nconst cache = new Map<string, Promise<TilePreviewMedia>>();\n\n/**\n * Resolves the preview media for an item. One request at most per item per\n * page load (none for texts and fallbacks), and the result, including a\n * failure, is cached so re-hovering never hits the network again.\n */\nexport function resolvePreviewMedia(\n identifier: string,\n mediatype: string,\n baseUrl = 'https://archive.org',\n options: TilePreviewOptions = {},\n): Promise<TilePreviewMedia> {\n const key = `${baseUrl}|${identifier}|${options.clips ? 'clip' : ''}`;\n let pending = cache.get(key);\n if (!pending) {\n pending = resolve(identifier, mediatype, baseUrl, options).catch(() => ({\n kind: 'zoom' as const,\n }));\n cache.set(key, pending);\n }\n return pending;\n}\n\n/** Test hook. */\nexport function clearPreviewMediaCache(): void {\n cache.clear();\n}\n\nasync function resolve(\n identifier: string,\n mediatype: string,\n baseUrl: string,\n options: TilePreviewOptions,\n): Promise<TilePreviewMedia> {\n const download = (name: string) =>\n `${baseUrl}/download/${identifier}/${name\n .split('/')\n .map(encodeURIComponent)\n .join('/')}`;\n\n // Page images come from the BookReader endpoint, so no file list is needed.\n // Pages that don't exist are dropped when they fail to load.\n if (mediatype === 'texts') {\n return {\n kind: 'slides',\n fit: 'contain',\n urls: PAGE_NUMBERS.map(n => download(`page/n${n}_w300.jpg`)),\n };\n }\n\n const files = await fetchFiles(identifier, baseUrl);\n\n if (mediatype === 'movies') {\n // Frames are spread across the whole film, skipping the first (the cover).\n const frames = spread(\n files\n .filter(f => f.format === 'Thumbnail')\n .map(f => f.name)\n .sort()\n .slice(1),\n MAX_FRAMES,\n ).map(download);\n\n const clip = options.clips ? smallestPlayable(files) : undefined;\n if (clip) {\n return {\n kind: 'video',\n src: download(clip.name),\n start: clipStart(clip.length),\n fallbackUrls: frames,\n };\n }\n return frames.length > 1\n ? { kind: 'slides', fit: 'cover', urls: frames }\n : { kind: 'zoom' };\n }\n\n if (mediatype === 'audio' || mediatype === 'etree') {\n const mp3 = files\n .filter(f => f.name.toLowerCase().endsWith('.mp3'))\n .sort((a, b) => a.name.localeCompare(b.name))[0];\n if (!mp3) return { kind: 'zoom' };\n const png = files.find(f => f.name === mp3.name.replace(/\\.mp3$/i, '.png'));\n return {\n kind: 'audio',\n src: download(mp3.name),\n start: startAt(mp3.length),\n waveform: png && download(png.name),\n };\n }\n\n if (mediatype === 'image') {\n const images = files\n .filter(\n f =>\n f.format === 'JPEG' &&\n Number(f.size ?? 0) > 0 &&\n Number(f.size) <= MAX_IMAGE_BYTES,\n )\n .map(f => download(f.name))\n .slice(0, MAX_SLIDES);\n return images.length > 1\n ? { kind: 'slides', fit: 'cover', urls: images }\n : { kind: 'zoom' };\n }\n\n return { kind: 'zoom' };\n}\n\nasync function fetchFiles(\n identifier: string,\n baseUrl: string,\n): Promise<ItemFile[]> {\n const res = await fetch(`${baseUrl}/metadata/${identifier}/files`);\n if (!res.ok) return [];\n const body = await res.json();\n return Array.isArray(body.result) ? body.result : [];\n}\n\n/**\n * The smallest derivative this browser can play (mp4 or webm, at least a few\n * seconds long). The smallest has the lowest bitrate and the shortest header, so\n * it starts soonest.\n */\nfunction smallestPlayable(files: ItemFile[]): ItemFile | undefined {\n const video = document.createElement('video');\n return files\n .filter(f => {\n const ext = f.name.toLowerCase().split('.').pop();\n const type =\n ext === 'webm'\n ? 'video/webm'\n : ext === 'mp4' || ext === 'm4v'\n ? 'video/mp4'\n : '';\n const seconds = parseLength(f.length);\n return (\n !!type &&\n f.format !== 'Thumbnail' &&\n Number(f.size) > 0 &&\n (seconds === 0 || seconds >= MIN_CLIP_SOURCE_SECONDS) &&\n !!video.canPlayType(type)\n );\n })\n .sort((a, b) => Number(a.size) - Number(b.size))[0];\n}\n\n/** Seconds into a film to start at: 10% in, to skip leading black, capped at 60. */\nfunction clipStart(length?: string): number {\n const seconds = parseLength(length);\n if (!seconds) return 0;\n return Math.max(\n 0,\n Math.min(60, Math.floor(seconds / 10), seconds - PREVIEW_CLIP_SECONDS),\n );\n}\n\n/** Picks `count` evenly spaced entries from `list`. */\nfunction spread<T>(list: T[], count: number): T[] {\n if (list.length <= count) return list;\n return Array.from(\n { length: count },\n (_, i) => list[Math.floor((i * list.length) / count)],\n );\n}\n\n/** Seconds into the media to start at: a third of the way in, capped at 60. */\nfunction startAt(length?: string): number {\n const seconds = parseLength(length);\n if (!seconds) return 0;\n return Math.max(\n 0,\n Math.min(60, Math.floor(seconds / 3), seconds - PREVIEW_CLIP_SECONDS),\n );\n}\n\n/** Parses a file `length`, either plain seconds (\"16.23\") or \"mm:ss\" / \"h:mm:ss\". */\nfunction parseLength(length?: string): number {\n if (!length) return 0;\n const parts = length.split(':').map(Number);\n if (parts.some(Number.isNaN)) return 0;\n return parts.reduce((total, part) => total * 60 + part, 0);\n}\n"]}
@@ -0,0 +1,66 @@
1
+ import { LitElement, nothing, PropertyValues } from 'lit';
2
+ import { type TilePreviewOptions } from './tile-preview-media';
3
+ /**
4
+ * Fills a tile's image area with a live preview of the item while `active`:
5
+ * a muted clip for movies, a few page images for texts, a waveform for audio,
6
+ * cycling images for images, and a slow pan over the thumbnail for anything
7
+ * else. It is absolutely positioned over the existing thumbnail, so showing
8
+ * and hiding it never moves the layout.
9
+ */
10
+ export declare class TilePreview extends LitElement {
11
+ identifier?: string;
12
+ mediatype?: string;
13
+ baseUrl: string;
14
+ /** The tile's thumbnail, panned slowly when there is nothing richer to show. */
15
+ thumbnailUrl?: string;
16
+ active: boolean;
17
+ options: TilePreviewOptions;
18
+ private media?;
19
+ private slideUrls;
20
+ private slide;
21
+ private ready;
22
+ /** Whether sound is audibly playing, as opposed to muted or refused. */
23
+ private audible;
24
+ private video?;
25
+ private audio?;
26
+ private slideTimer?;
27
+ private frameTimer?;
28
+ /** Pending fade animation frame per element, so one element's fade never cancels another's. */
29
+ private fadeFrames;
30
+ /** Bumped on every activation so a slow resolve can't show a stale preview. */
31
+ private activation;
32
+ protected willUpdate(changed: PropertyValues<this>): void;
33
+ protected updated(changed: PropertyValues<this>): void;
34
+ disconnectedCallback(): void;
35
+ private start;
36
+ private stop;
37
+ /**
38
+ * Stops a media element: a short fade-out when it is audible, then pause and
39
+ * drop the source so the browser stops downloading it.
40
+ */
41
+ private releaseMedia;
42
+ private cancelFade;
43
+ /** Ramps a media element's volume to `target` over `ms`. */
44
+ private fade;
45
+ /**
46
+ * Starts a media element, with sound when the options ask for it. A browser
47
+ * that refuses audible playback gets a muted one instead.
48
+ */
49
+ private playMedia;
50
+ /** Swaps a clip that is slow or unplayable for the item's frame thumbnails. */
51
+ private useFallbackFrames;
52
+ private startSlides;
53
+ private loadedSlides;
54
+ private onSlideError;
55
+ private onSlideLoad;
56
+ private onVideoPlaying;
57
+ /** Loops the clip within its start-to-start+clip-length window. */
58
+ private onVideoTimeUpdate;
59
+ private onVideoLoaded;
60
+ private playSnippet;
61
+ render(): import("lit").TemplateResult<1> | typeof nothing;
62
+ /** A muted-speaker hint, so people know a clip or snippet could have sound. */
63
+ private get soundBadgeTemplate();
64
+ private mediaTemplate;
65
+ static styles: import("lit").CSSResult;
66
+ }