dsh-workspace-explorer 1.1.0 → 1.1.2
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/CHANGELOG.md +15 -0
- package/README.md +11 -9
- package/docs/screenshots/grid-favorites.png +0 -0
- package/docs/screenshots/stl-preview.png +0 -0
- package/docs/screenshots/workspace-search.png +0 -0
- package/lib/client.js +820 -162
- package/package.json +53 -52
- package/scripts/build-client.mjs +1 -0
- package/src/client/conversation-drop.js +135 -0
- package/src/client/main.js +361 -91
- package/src/client/sidebar-motion.js +69 -0
- package/src/document-types.js +2 -2
- package/src/host/ExplorerShell.cs +183 -183
- package/src/host/files.js +25 -6
- package/src/host/pdf-assets.js +30 -30
- package/src/host/shell-files.js +81 -81
- package/src/thumbnail-types.js +18 -18
- package/src/ui/composer-drop.js +119 -119
- package/src/ui/explorer.js +42 -42
- package/src/ui/favorites.js +176 -124
- package/src/ui/file-menu.js +125 -125
- package/src/ui/horizontal-wheel.js +11 -11
- package/src/ui/navigation.js +9 -0
- package/src/ui/pdf-preview.js +135 -135
- package/src/ui/preview-unavailable.js +6 -6
- package/src/ui/preview-zoom.js +47 -47
- package/src/ui/preview.js +58 -12
- package/src/ui/styles.js +74 -15
- package/src/ui/thumbnail.js +105 -105
- package/src/vendor/ui/extra-views.js +1 -1
package/src/ui/styles.js
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
export const explorerCss = `
|
|
2
|
+
[data-dwe-sidebar-handoff] {
|
|
3
|
+
grid-template-columns: var(--dwe-handoff-columns) !important;
|
|
4
|
+
transition: none !important;
|
|
5
|
+
}
|
|
6
|
+
[data-dwe-sidebar-track-instant],
|
|
7
|
+
[data-sidebar-right-panel] :is([data-dockkit-host='dock'], [data-dockkit-empty], [data-dockkit-divider]):not([data-dwe-sidebar-toggle-motion]),
|
|
8
|
+
[data-dwe-sidebar-handoff] [data-sidebar-right-panel] :is([data-dockkit-host='dock'], [data-dockkit-empty], [data-dockkit-divider]) {
|
|
9
|
+
transition: none !important;
|
|
10
|
+
}
|
|
11
|
+
.dwe-conversation-drop {
|
|
12
|
+
position: fixed; inset: 0; z-index: 2147483647;
|
|
13
|
+
display: grid; box-sizing: border-box; padding: 0 6px 6px 3px;
|
|
14
|
+
--dwe-drop-blue: #4176E6;
|
|
15
|
+
pointer-events: none; user-select: none;
|
|
16
|
+
}
|
|
17
|
+
.dwe-conversation-drop-area {
|
|
18
|
+
position: relative; display: grid; place-items: center; padding: 24px;
|
|
19
|
+
border-radius: 10px; overflow: hidden;
|
|
20
|
+
background: color-mix(in srgb, var(--dsw-alias-bg-base, Canvas) 38%, transparent);
|
|
21
|
+
backdrop-filter: blur(7.5px); color: var(--dwe-drop-blue);
|
|
22
|
+
text-align: center; font-size: 24px;
|
|
23
|
+
}
|
|
24
|
+
.dwe-conversation-drop-area::before {
|
|
25
|
+
content: ''; position: absolute; inset: 0; border: 4px solid var(--dwe-drop-blue); border-radius: inherit;
|
|
26
|
+
}
|
|
27
|
+
.dwe-conversation-drop[data-dwe-drop-active] .dwe-conversation-drop-area::before { border-width: 8px; }
|
|
28
|
+
.dwe-conversation-drag-icon {
|
|
29
|
+
position: fixed; top: 0; left: 0; width: 40px; height: 40px;
|
|
30
|
+
display: grid; place-items: center; border-radius: 8px;
|
|
31
|
+
background: var(--dsw-alias-bg-base, Canvas); box-shadow: 0 2px 8px #0004;
|
|
32
|
+
transform: translate3d(clamp(0px, var(--dwe-drag-x), 100vw - 44px), clamp(0px, var(--dwe-drag-y), 100vh - 44px), 0);
|
|
33
|
+
}
|
|
2
34
|
.dwe-host { height: 100%; min-height: 0; min-width: 0; }
|
|
3
35
|
[data-dockkit-tab]:has([data-dwe-tab-title]) {
|
|
4
36
|
flex-shrink: 0; min-inline-size: calc(5em + 59px); max-inline-size: max(170px, calc(5em + 59px));
|
|
@@ -12,6 +44,7 @@ export const explorerCss = `
|
|
|
12
44
|
[data-dwe-tab-title] > span { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
13
45
|
.dwe {
|
|
14
46
|
--dwe-line: var(--dsw-alias-border-l3, color-mix(in srgb, currentColor 14%, transparent));
|
|
47
|
+
--dwe-rule-inset: 8px;
|
|
15
48
|
--dwe-tint: color-mix(in srgb, currentColor 5%, transparent);
|
|
16
49
|
--dwe-hover: color-mix(in srgb, currentColor 8%, transparent);
|
|
17
50
|
--dwe-blue: #458bf5;
|
|
@@ -48,11 +81,17 @@ export const explorerCss = `
|
|
|
48
81
|
.dwe-divider:focus-visible { outline: 2px solid var(--dwe-blue); outline-offset: -2px; }
|
|
49
82
|
.dwe-toolbar, .dwe-preview-bar {
|
|
50
83
|
display: flex; flex: none; align-items: center; gap: 6px;
|
|
51
|
-
padding: 3px 8px; min-width: 0; border-bottom: 1px solid
|
|
84
|
+
padding: 3px 8px; min-width: 0; border-bottom: 1px solid transparent;
|
|
85
|
+
}
|
|
86
|
+
.dwe-toolbar, .dwe-head, .dwe-status-bar, .dwe-preview-bar {
|
|
87
|
+
background-image: linear-gradient(#F5F5F5, #F5F5F5);
|
|
88
|
+
background-repeat: no-repeat; background-origin: border-box;
|
|
89
|
+
background-size: calc(100% - 2 * var(--dwe-rule-inset)) 1px; background-position: center bottom;
|
|
52
90
|
}
|
|
53
91
|
.dwe-address-bar { display: flex; min-width: 0; flex: none; align-items: center; gap: 3px; padding: 5px 8px 0; }
|
|
54
92
|
.dwe-toolbar { justify-content: space-between; gap: 4px; overflow-x: auto; scrollbar-width: none; }
|
|
55
93
|
.dwe-nav { display: flex; align-items: center; gap: 2px; flex: none; }
|
|
94
|
+
.dwe-v-divider { flex: 0 0 1px; height: 15px; margin-inline: 3px; background: var(--dwe-line); }
|
|
56
95
|
.dwe button {
|
|
57
96
|
font: inherit; color: inherit; border: 0; background: transparent; cursor: default;
|
|
58
97
|
}
|
|
@@ -66,8 +105,9 @@ export const explorerCss = `
|
|
|
66
105
|
background: color-mix(in srgb, var(--dwe-blue) 20%, transparent);
|
|
67
106
|
}
|
|
68
107
|
.dwe-tool[data-dwe-tool="star"][aria-pressed="true"] svg { color: #a36a00; fill: #ffca58; }
|
|
69
|
-
.dwe-tool[data-dwe-tool="
|
|
70
|
-
|
|
108
|
+
.dwe-tool:is(.dwe-scope-tool, .dwe-favorites-tool, [data-dwe-tool="preview"], [data-dwe-tool="list"], [data-dwe-tool="grid"]) {
|
|
109
|
+
&:is([aria-pressed="true"], [aria-expanded="true"]) { background: var(--dwe-hover); }
|
|
110
|
+
&:hover:not(:disabled) { background: color-mix(in srgb, var(--dwe-blue) 20%, transparent); }
|
|
71
111
|
}
|
|
72
112
|
.dwe-favorites {
|
|
73
113
|
position: absolute; z-index: 4; display: flex; flex-direction: column; box-sizing: border-box;
|
|
@@ -80,7 +120,12 @@ export const explorerCss = `
|
|
|
80
120
|
padding-inline-start: 6px; min-height: 25px;
|
|
81
121
|
}
|
|
82
122
|
.dwe-favorites ul { list-style: none; margin: 0; padding: 0; overflow: auto; }
|
|
83
|
-
.dwe-favorites li { display: flex; align-items: center; gap: 4px; }
|
|
123
|
+
.dwe-favorites li { position: relative; display: flex; align-items: center; gap: 4px; }
|
|
124
|
+
.dwe-favorites li[data-dwe-favorite-drop]::after {
|
|
125
|
+
content: ''; position: absolute; inset-inline: 4px; height: 2px; background: var(--dwe-blue); pointer-events: none;
|
|
126
|
+
}
|
|
127
|
+
.dwe-favorites li[data-dwe-favorite-drop='before']::after { top: 0; }
|
|
128
|
+
.dwe-favorites li[data-dwe-favorite-drop='after']::after { bottom: 0; }
|
|
84
129
|
.dwe-favorite-location {
|
|
85
130
|
display: flex; flex: 1; min-width: 0; align-items: center; gap: 7px; text-align: start;
|
|
86
131
|
min-height: 28px; padding: 2px 4px; border-radius: 5px;
|
|
@@ -132,7 +177,7 @@ export const explorerCss = `
|
|
|
132
177
|
.dwe-columns > span { padding-inline: 8px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
133
178
|
.dwe-head {
|
|
134
179
|
position: sticky; top: 0; z-index: 1; padding: 5px 6px; font-size: 12px;
|
|
135
|
-
background: var(--dsw-alias-bg-base, Canvas); border-bottom: 1px solid
|
|
180
|
+
background-color: var(--dsw-alias-bg-base, Canvas); border-bottom: 1px solid transparent;
|
|
136
181
|
}
|
|
137
182
|
.dwe-head span + span { border-inline-start: 1px solid var(--dwe-line); }
|
|
138
183
|
.dwe-columns > span:last-child { text-align: end; }
|
|
@@ -146,7 +191,7 @@ export const explorerCss = `
|
|
|
146
191
|
.dwe-tile .dwe-name { flex-direction: column; gap: 5px; }
|
|
147
192
|
.dwe-thumbnail {
|
|
148
193
|
position: relative; display: grid; place-items: center; inline-size: 100%; max-inline-size: 110px;
|
|
149
|
-
block-size: 60px; pointer-events: none;
|
|
194
|
+
grid-template: minmax(0, 1fr) / minmax(0, 1fr); block-size: 60px; pointer-events: none;
|
|
150
195
|
}
|
|
151
196
|
.dwe-thumbnail img {
|
|
152
197
|
display: block; max-inline-size: 100%; max-block-size: 100%; object-fit: contain; border-radius: 3px;
|
|
@@ -171,13 +216,14 @@ export const explorerCss = `
|
|
|
171
216
|
.dwe-meta, .dwe-status { opacity: .72; font-size: 12px; }
|
|
172
217
|
.dwe-status { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
173
218
|
.dwe-status-bar {
|
|
174
|
-
flex: none; display: flex; align-items: center;
|
|
175
|
-
padding: 2px
|
|
219
|
+
flex: none; display: flex; align-items: center; gap: 4px;
|
|
220
|
+
padding: 2px 8px; border-top: 1px solid transparent; background-position: center top;
|
|
176
221
|
}
|
|
222
|
+
.dwe-status-bar > .dwe-tool { margin-inline-start: auto; }
|
|
177
223
|
.dwe-preview {
|
|
178
224
|
min-height: 0; display: flex; flex-direction: column;
|
|
179
225
|
}
|
|
180
|
-
.dwe-preview-bar { background: var(--dsw-alias-bg-base, Canvas); }
|
|
226
|
+
.dwe-preview-bar { background-color: var(--dsw-alias-bg-base, Canvas); }
|
|
181
227
|
.dwe-preview-label { flex: none; margin-inline: 4px; line-height: 28px; }
|
|
182
228
|
.dwe-preview-name {
|
|
183
229
|
min-width: 0; height: 28px; box-sizing: border-box; padding-inline: 7px 3px;
|
|
@@ -188,13 +234,17 @@ export const explorerCss = `
|
|
|
188
234
|
.dwe-preview-body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
189
235
|
.dwe-preview-cover {
|
|
190
236
|
position: relative; display: grid; place-items: center; isolation: isolate;
|
|
191
|
-
height: 100%; min-height:
|
|
237
|
+
height: 100%; min-height: 112px; box-sizing: border-box; padding: 6px 12px;
|
|
238
|
+
overflow: hidden; background: var(--dwe-tint);
|
|
192
239
|
}
|
|
240
|
+
.dwe-preview-cover[data-video] { min-height: 128px; }
|
|
193
241
|
.dwe-preview-cover[data-poster] { background: #15171a; }
|
|
194
242
|
.dwe-preview-cover > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: -1; }
|
|
243
|
+
.dwe-cover-content { display: grid; grid-template-columns: minmax(0, 1fr); justify-items: center; gap: 8px; width: 100%; }
|
|
195
244
|
.dwe-cover-action { display: flex; align-items: center; gap: 10px; }
|
|
196
245
|
.dwe .dwe-cover-play {
|
|
197
|
-
display: grid; place-items: center; width: 52px; height: 52px;
|
|
246
|
+
display: grid; place-items: center; width: 52px; height: 52px; padding: 0; box-sizing: border-box;
|
|
247
|
+
border-radius: 50%; corner-shape: round;
|
|
198
248
|
background: #243953; color: #fff; box-shadow: 0 2px 8px #0003; cursor: pointer;
|
|
199
249
|
}
|
|
200
250
|
.dwe .dwe-cover-play:hover:not(:disabled) { background: #34577f; }
|
|
@@ -202,11 +252,17 @@ export const explorerCss = `
|
|
|
202
252
|
padding: 7px 12px; border: 1px solid var(--dwe-line); border-radius: 6px;
|
|
203
253
|
background: var(--dsw-alias-bg-base, Canvas); white-space: nowrap; cursor: pointer;
|
|
204
254
|
}
|
|
205
|
-
.dwe-cover-
|
|
206
|
-
|
|
207
|
-
max-width:
|
|
255
|
+
.dwe-cover-details {
|
|
256
|
+
display: grid; grid-template-columns: minmax(0, 1fr); justify-items: center; gap: 3px;
|
|
257
|
+
max-width: 100%; min-width: 0; box-sizing: border-box; padding: 2px 8px; border-radius: 5px;
|
|
258
|
+
}
|
|
259
|
+
.dwe-cover-file { max-width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
260
|
+
.dwe-cover-size { font-size: 12px; opacity: .72; }
|
|
261
|
+
.dwe-preview-cover[data-video] .dwe-cover-details {
|
|
262
|
+
position: absolute; inset: 0 0 auto; display: flex; align-items: center; gap: 6px;
|
|
263
|
+
padding: 8px; border-radius: 0; background: #0009; color: #fff;
|
|
208
264
|
}
|
|
209
|
-
.dwe-preview-cover[data-
|
|
265
|
+
.dwe-preview-cover[data-video] .dwe-cover-size { flex: none; white-space: nowrap; }
|
|
210
266
|
.dwe-pdf-scroll {
|
|
211
267
|
height: 100%; overflow: auto; scrollbar-gutter: stable both-edges; background: var(--dwe-tint);
|
|
212
268
|
}
|
|
@@ -274,6 +330,9 @@ export const explorerCss = `
|
|
|
274
330
|
.dwe-dialog-actions button[type="submit"] { background: color-mix(in srgb, var(--dwe-blue) 23%, transparent); }
|
|
275
331
|
.dwe-dialog-actions button[data-dwe-action="delete"] { background: #ad303d; color: #fff; }
|
|
276
332
|
.dwe-dialog-actions button[data-dwe-action="delete"]:hover:not(:disabled) { background: #942634; }
|
|
333
|
+
@container (max-width: 440px) {
|
|
334
|
+
.dwe-toolbar .dwe-tool:not(.dwe-scope-tool) { width: 26px; height: 26px; flex-basis: 26px; }
|
|
335
|
+
}
|
|
277
336
|
@container (max-width: 380px) {
|
|
278
337
|
.dwe-columns { grid-template-columns: minmax(0, 1fr) 62px; }
|
|
279
338
|
.dwe-columns > span:nth-child(2) { display: none; }
|
package/src/ui/thumbnail.js
CHANGED
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
import { thumbnailType } from '../thumbnail-types.js';
|
|
2
|
-
import { disks } from './workspace.js';
|
|
3
|
-
|
|
4
|
-
const { createElement: h, useEffect, useRef, useState } = react;
|
|
5
|
-
const pending = new Set();
|
|
6
|
-
let running = 0;
|
|
7
|
-
|
|
8
|
-
function startNext() {
|
|
9
|
-
while (running < 3 && pending.size) {
|
|
10
|
-
const next = pending.values().next().value;
|
|
11
|
-
pending.delete(next);
|
|
12
|
-
running++;
|
|
13
|
-
void next().finally(() => { running--; startNext(); });
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function waitForFrame(media, event, signal) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
const handlers = new AbortController();
|
|
20
|
-
function settle(error) {
|
|
21
|
-
handlers.abort();
|
|
22
|
-
signal.removeEventListener('abort', cancel);
|
|
23
|
-
if (error) reject(error);
|
|
24
|
-
else resolve();
|
|
25
|
-
}
|
|
26
|
-
function cancel() { settle(signal.reason); }
|
|
27
|
-
if (signal.aborted) { cancel(); return; }
|
|
28
|
-
const options = { once: true, signal: handlers.signal };
|
|
29
|
-
signal.addEventListener('abort', cancel, { once: true });
|
|
30
|
-
media.addEventListener(event, () => settle(), options);
|
|
31
|
-
media.addEventListener('error', () => settle(new Error('无法解码缩略图')), options);
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function makeThumbnail(path, type, signal, root, poster = false) {
|
|
36
|
-
const { url } = await disks.thumbnail(path, signal, root);
|
|
37
|
-
signal.throwIfAborted();
|
|
38
|
-
const video = type.startsWith('video/');
|
|
39
|
-
const media = document.createElement(video ? 'video' : 'img');
|
|
40
|
-
if (video) { media.preload = 'auto'; media.muted = true; }
|
|
41
|
-
try {
|
|
42
|
-
const firstFrame = waitForFrame(media, video ? 'loadeddata' : 'load', signal);
|
|
43
|
-
media.src = url;
|
|
44
|
-
await firstFrame;
|
|
45
|
-
if (video && !poster && Number.isFinite(media.duration) && media.duration > 0.2) {
|
|
46
|
-
const seeked = waitForFrame(media, 'seeked', signal);
|
|
47
|
-
media.currentTime = Math.min(0.7, media.duration * 0.12);
|
|
48
|
-
await seeked;
|
|
49
|
-
}
|
|
50
|
-
const width = video ? media.videoWidth : media.naturalWidth;
|
|
51
|
-
const height = video ? media.videoHeight : media.naturalHeight;
|
|
52
|
-
if (!width || !height) throw new Error('缩略图为空');
|
|
53
|
-
const [maxWidth, maxHeight] = poster ? [640, 360] : [192, 136];
|
|
54
|
-
const scale = Math.min(maxWidth / width, maxHeight / height, poster ? 1 : Infinity);
|
|
55
|
-
const canvas = document.createElement('canvas');
|
|
56
|
-
canvas.width = Math.max(1, Math.round(width * scale));
|
|
57
|
-
canvas.height = Math.max(1, Math.round(height * scale));
|
|
58
|
-
canvas.getContext('2d').drawImage(media, 0, 0, canvas.width, canvas.height);
|
|
59
|
-
return canvas.toDataURL('image/webp', 0.85);
|
|
60
|
-
} finally {
|
|
61
|
-
media.removeAttribute('src');
|
|
62
|
-
if (video) media.load();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function Thumbnail({ path, active, fallback, root }) {
|
|
67
|
-
const slot = useRef(null);
|
|
68
|
-
const [result, setResult] = useState(null);
|
|
69
|
-
const type = thumbnailType(path);
|
|
70
|
-
const image = result?.path === path && result?.root === root ? result.image : null;
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
const element = slot.current;
|
|
73
|
-
if (!element || !active || !type || image) return;
|
|
74
|
-
let job;
|
|
75
|
-
let failed = false;
|
|
76
|
-
function cancel() {
|
|
77
|
-
if (!job) return;
|
|
78
|
-
pending.delete(job.run);
|
|
79
|
-
job.controller.abort();
|
|
80
|
-
job = undefined;
|
|
81
|
-
}
|
|
82
|
-
const observer = new IntersectionObserver(([entry]) => {
|
|
83
|
-
if (!entry.isIntersecting) { cancel(); return; }
|
|
84
|
-
if (job || failed) return;
|
|
85
|
-
const controller = new AbortController();
|
|
86
|
-
const run = async () => {
|
|
87
|
-
try {
|
|
88
|
-
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(12000)]);
|
|
89
|
-
const image = await makeThumbnail(path, type, signal, root);
|
|
90
|
-
if (!signal.aborted) setResult({ path, root, image });
|
|
91
|
-
} catch {
|
|
92
|
-
if (!controller.signal.aborted) failed = true;
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
job = { controller, run };
|
|
96
|
-
pending.add(run);
|
|
97
|
-
startNext();
|
|
98
|
-
}, { root: element.closest('.dwe-list'), rootMargin: '72px 0px' });
|
|
99
|
-
observer.observe(element);
|
|
100
|
-
return () => { observer.disconnect(); cancel(); };
|
|
101
|
-
}, [active, path, type, image, root]);
|
|
102
|
-
|
|
103
|
-
return h('span', { ref: slot, className: 'dwe-thumbnail', 'aria-hidden': true },
|
|
104
|
-
image ? h('img', { src: image, alt: '', draggable: false }) : fallback,
|
|
105
|
-
image && type?.startsWith('video/') && h('span', { className: 'dwe-video-mark' }, '▶'));
|
|
1
|
+
import { thumbnailType } from '../thumbnail-types.js';
|
|
2
|
+
import { disks } from './workspace.js';
|
|
3
|
+
|
|
4
|
+
const { createElement: h, useEffect, useRef, useState } = react;
|
|
5
|
+
const pending = new Set();
|
|
6
|
+
let running = 0;
|
|
7
|
+
|
|
8
|
+
function startNext() {
|
|
9
|
+
while (running < 3 && pending.size) {
|
|
10
|
+
const next = pending.values().next().value;
|
|
11
|
+
pending.delete(next);
|
|
12
|
+
running++;
|
|
13
|
+
void next().finally(() => { running--; startNext(); });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function waitForFrame(media, event, signal) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const handlers = new AbortController();
|
|
20
|
+
function settle(error) {
|
|
21
|
+
handlers.abort();
|
|
22
|
+
signal.removeEventListener('abort', cancel);
|
|
23
|
+
if (error) reject(error);
|
|
24
|
+
else resolve();
|
|
25
|
+
}
|
|
26
|
+
function cancel() { settle(signal.reason); }
|
|
27
|
+
if (signal.aborted) { cancel(); return; }
|
|
28
|
+
const options = { once: true, signal: handlers.signal };
|
|
29
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
30
|
+
media.addEventListener(event, () => settle(), options);
|
|
31
|
+
media.addEventListener('error', () => settle(new Error('无法解码缩略图')), options);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function makeThumbnail(path, type, signal, root, poster = false) {
|
|
36
|
+
const { url } = await disks.thumbnail(path, signal, root);
|
|
37
|
+
signal.throwIfAborted();
|
|
38
|
+
const video = type.startsWith('video/');
|
|
39
|
+
const media = document.createElement(video ? 'video' : 'img');
|
|
40
|
+
if (video) { media.preload = 'auto'; media.muted = true; }
|
|
41
|
+
try {
|
|
42
|
+
const firstFrame = waitForFrame(media, video ? 'loadeddata' : 'load', signal);
|
|
43
|
+
media.src = url;
|
|
44
|
+
await firstFrame;
|
|
45
|
+
if (video && !poster && Number.isFinite(media.duration) && media.duration > 0.2) {
|
|
46
|
+
const seeked = waitForFrame(media, 'seeked', signal);
|
|
47
|
+
media.currentTime = Math.min(0.7, media.duration * 0.12);
|
|
48
|
+
await seeked;
|
|
49
|
+
}
|
|
50
|
+
const width = video ? media.videoWidth : media.naturalWidth;
|
|
51
|
+
const height = video ? media.videoHeight : media.naturalHeight;
|
|
52
|
+
if (!width || !height) throw new Error('缩略图为空');
|
|
53
|
+
const [maxWidth, maxHeight] = poster ? [640, 360] : [192, 136];
|
|
54
|
+
const scale = Math.min(maxWidth / width, maxHeight / height, poster ? 1 : Infinity);
|
|
55
|
+
const canvas = document.createElement('canvas');
|
|
56
|
+
canvas.width = Math.max(1, Math.round(width * scale));
|
|
57
|
+
canvas.height = Math.max(1, Math.round(height * scale));
|
|
58
|
+
canvas.getContext('2d').drawImage(media, 0, 0, canvas.width, canvas.height);
|
|
59
|
+
return canvas.toDataURL('image/webp', 0.85);
|
|
60
|
+
} finally {
|
|
61
|
+
media.removeAttribute('src');
|
|
62
|
+
if (video) media.load();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function Thumbnail({ path, active, fallback, root }) {
|
|
67
|
+
const slot = useRef(null);
|
|
68
|
+
const [result, setResult] = useState(null);
|
|
69
|
+
const type = thumbnailType(path);
|
|
70
|
+
const image = result?.path === path && result?.root === root ? result.image : null;
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const element = slot.current;
|
|
73
|
+
if (!element || !active || !type || image) return;
|
|
74
|
+
let job;
|
|
75
|
+
let failed = false;
|
|
76
|
+
function cancel() {
|
|
77
|
+
if (!job) return;
|
|
78
|
+
pending.delete(job.run);
|
|
79
|
+
job.controller.abort();
|
|
80
|
+
job = undefined;
|
|
81
|
+
}
|
|
82
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
83
|
+
if (!entry.isIntersecting) { cancel(); return; }
|
|
84
|
+
if (job || failed) return;
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const run = async () => {
|
|
87
|
+
try {
|
|
88
|
+
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(12000)]);
|
|
89
|
+
const image = await makeThumbnail(path, type, signal, root);
|
|
90
|
+
if (!signal.aborted) setResult({ path, root, image });
|
|
91
|
+
} catch {
|
|
92
|
+
if (!controller.signal.aborted) failed = true;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
job = { controller, run };
|
|
96
|
+
pending.add(run);
|
|
97
|
+
startNext();
|
|
98
|
+
}, { root: element.closest('.dwe-list'), rootMargin: '72px 0px' });
|
|
99
|
+
observer.observe(element);
|
|
100
|
+
return () => { observer.disconnect(); cancel(); };
|
|
101
|
+
}, [active, path, type, image, root]);
|
|
102
|
+
|
|
103
|
+
return h('span', { ref: slot, className: 'dwe-thumbnail', 'aria-hidden': true },
|
|
104
|
+
image ? h('img', { src: image, alt: '', draggable: false }) : fallback,
|
|
105
|
+
image && type?.startsWith('video/') && h('span', { className: 'dwe-video-mark' }, '▶'));
|
|
106
106
|
}
|