@3sln/trove 0.0.13 → 0.0.16
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/package.json +1 -1
- package/packages/core/src/indexing.js +19 -0
- package/packages/web/dist/assets/{main-828yzsr7.js → main-wpfmmbfd.js} +16 -16
- package/packages/web/dist/assets/{main-828yzsr7.js.map → main-wpfmmbfd.js.map} +5 -5
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/src/bl/fileType.js +43 -0
- package/packages/web/src/ui/components/views/grid.js +15 -2
- package/packages/web/src/ui/media.js +34 -0
package/packages/web/dist/sw.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// API and FILES deliberately do NOT rotate. API is data, not code, and FILES holds the
|
|
19
19
|
// bytes of files the user pinned for offline use — naming it per build would throw away
|
|
20
20
|
// someone's offline library every time the app was redeployed.
|
|
21
|
-
const SHELL = 'trove-shell-
|
|
21
|
+
const SHELL = 'trove-shell-5c503618bc';
|
|
22
22
|
const API = 'trove-api-v1';
|
|
23
23
|
const FILES = 'trove-files-v1';
|
|
24
24
|
const KEEP = new Set([SHELL, API, FILES]);
|
|
@@ -43,6 +43,49 @@ export function iconForKind(node) {
|
|
|
43
43
|
return ICONS[kindOf(node)] || 'file';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The key an indexer writes a tile picture under, and the ONE key the grid looks for.
|
|
48
|
+
*
|
|
49
|
+
* A preset name rather than a per-plugin registration, because a thumbnail is not a
|
|
50
|
+
* contribution TYPE — it is a fact about a file that any indexer may happen to know. The
|
|
51
|
+
* audiobook plugin extracts cover art from an m4b's `covr` atom and an LPF's manifest;
|
|
52
|
+
* a PDF indexer could write its first page under the same key and the grid would draw it
|
|
53
|
+
* with no change here.
|
|
54
|
+
*
|
|
55
|
+
* @see plugins/audiobook/src/coverIndexer.js
|
|
56
|
+
*/
|
|
57
|
+
export const THUMBNAIL_KEY = 'thumbnail';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A picture to put on this node's tile, or null.
|
|
61
|
+
*
|
|
62
|
+
* Contributions are namespaced by contributor, and a view deliberately does not care WHICH
|
|
63
|
+
* one supplied it: the first that did wins, and a drive with two thumbnail indexers for one
|
|
64
|
+
* file type has a configuration problem rather than a rendering one.
|
|
65
|
+
*
|
|
66
|
+
* TWO SHAPES, because a thumbnail is either already somewhere or it is not:
|
|
67
|
+
*
|
|
68
|
+
* { range: {start, end}, contentType } the bytes live in the FILE — an m4b's cover art
|
|
69
|
+
* sits in its `covr` atom — so the contribution
|
|
70
|
+
* points at them. Eighty bytes on the wire instead
|
|
71
|
+
* of a base64 copy on every listing.
|
|
72
|
+
* { src } a self-contained URL, for the cases with nothing
|
|
73
|
+
* to point at. Small by construction; see the
|
|
74
|
+
* indexer's cap.
|
|
75
|
+
*
|
|
76
|
+
* @returns {{range?: {start: number, end: number}, src?: string, contentType?: string}|null}
|
|
77
|
+
*/
|
|
78
|
+
export function thumbnailOf(node) {
|
|
79
|
+
const contributions = node?.contributions;
|
|
80
|
+
if (!contributions) return null;
|
|
81
|
+
for (const contribution of Object.values(contributions)) {
|
|
82
|
+
const thumb = contribution?.metadata?.[THUMBNAIL_KEY];
|
|
83
|
+
if (typeof thumb?.src === 'string' && thumb.src) return thumb;
|
|
84
|
+
if (Number.isFinite(thumb?.range?.start) && Number.isFinite(thumb?.range?.end)) return thumb;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
46
89
|
/** Whether a node holds indexable text (drives offline text extraction). */
|
|
47
90
|
export function isTexty(node) {
|
|
48
91
|
return kindOf(node) === 'text';
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
import { dd } from '../../../runtime.js';
|
|
17
17
|
import { icon } from '../../icon.js';
|
|
18
|
+
import { thumbnailOf } from '../../../bl/fileType.js';
|
|
18
19
|
import { groupHeader, menuButton, openRowMenu } from './parts.js';
|
|
19
|
-
import { attachMedia } from '../../media.js';
|
|
20
|
+
import { attachMedia, attachThumbnail } from '../../media.js';
|
|
20
21
|
import { activate } from '../../activate.js';
|
|
21
22
|
|
|
22
23
|
const { div, span, img } = dd;
|
|
@@ -81,7 +82,11 @@ function columns() {
|
|
|
81
82
|
|
|
82
83
|
function tile(it, active, { hover, select }, ui) {
|
|
83
84
|
const node = it.node;
|
|
84
|
-
|
|
85
|
+
// A contributed thumbnail beats the file itself. It is already a picture of the right
|
|
86
|
+
// size, it needs no minted URL, and for anything that is not an image it is the only
|
|
87
|
+
// picture there is — an audiobook's cover art comes this way. See bl/fileType.js.
|
|
88
|
+
const thumb = thumbnailOf(node);
|
|
89
|
+
const pictorial = !thumb && (node?.contentType || '').startsWith('image/');
|
|
85
90
|
return div({ className: `grid-tile ${active ? 'active' : ''}`, title: it.detail ? `${it.title} — ${it.detail}` : it.title },
|
|
86
91
|
div({ className: 'gt-media' },
|
|
87
92
|
// The icon is drawn underneath, always. When the image loads it covers it; when
|
|
@@ -92,6 +97,14 @@ function tile(it, active, { hover, select }, ui) {
|
|
|
92
97
|
// The `src` is minted rather than built — a tile fetches without our Authorization
|
|
93
98
|
// header, so the URL has to carry its own grant. Minting is batched (see
|
|
94
99
|
// platform/mediaUrls.js), so a wall of tiles costs one request, not one each.
|
|
100
|
+
// A contributed thumbnail needs no minted URL: it is either self-contained or a
|
|
101
|
+
// range inside a file this session may already hold. See ui/media.js.
|
|
102
|
+
thumb
|
|
103
|
+
? img({ className: 'gt-img', alt: '', loading: 'lazy', decoding: 'async' }).on({
|
|
104
|
+
$attach: (dom) => { dom._detachThumb = attachThumbnail(dom, node, thumb, ui); },
|
|
105
|
+
$detach: (dom) => { dom._detachThumb?.(); dom._detachThumb = null; },
|
|
106
|
+
}).opaque()
|
|
107
|
+
: null,
|
|
95
108
|
pictorial
|
|
96
109
|
? img({ className: 'gt-img', alt: '', loading: 'lazy', decoding: 'async' }).on({
|
|
97
110
|
$attach: (dom) => {
|
|
@@ -112,3 +112,37 @@ export function attachMedia(el, node, ui, { op = 'media', onError } = {}) {
|
|
|
112
112
|
function isMedia(el) {
|
|
113
113
|
return typeof el?.play === 'function' && typeof el?.load === 'function';
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Point an `<img>` at a contributed thumbnail — see bl/fileType.js for the two shapes.
|
|
118
|
+
*
|
|
119
|
+
* A `src` needs no help. A `range` is bytes inside the file, so they are fetched through
|
|
120
|
+
* `fileChunks`, which means a pinned book draws its cover with the network off and a book
|
|
121
|
+
* being kept offline contributes the read. The blob URL is revoked on detach: a wall of
|
|
122
|
+
* tiles that minted one each and never gave them back would hold every cover in memory for
|
|
123
|
+
* as long as the tab lived.
|
|
124
|
+
*
|
|
125
|
+
* @returns {() => void} detach
|
|
126
|
+
*/
|
|
127
|
+
export function attachThumbnail(el, node, thumb, ui) {
|
|
128
|
+
if (thumb?.src) {
|
|
129
|
+
el.src = thumb.src;
|
|
130
|
+
return () => {};
|
|
131
|
+
}
|
|
132
|
+
let url = null;
|
|
133
|
+
let live = true;
|
|
134
|
+
ui.platform.fileChunks.read(node.id, thumb.range)
|
|
135
|
+
.then(({ bytes }) => {
|
|
136
|
+
if (!live || !bytes?.length) return;
|
|
137
|
+
url = URL.createObjectURL(new Blob([bytes], { type: thumb.contentType || 'image/jpeg' }));
|
|
138
|
+
el.src = url;
|
|
139
|
+
})
|
|
140
|
+
// A cover that will not load is a tile with an icon on it, which is what the list shows
|
|
141
|
+
// anyway — not an error worth putting in front of anyone.
|
|
142
|
+
.catch(() => { el.style.display = 'none'; });
|
|
143
|
+
return () => {
|
|
144
|
+
live = false;
|
|
145
|
+
if (url) URL.revokeObjectURL(url);
|
|
146
|
+
url = null;
|
|
147
|
+
};
|
|
148
|
+
}
|