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.
@@ -1,135 +1,135 @@
1
- import { pdfAssetsPath } from '../document-types.js';
2
- import { PreviewUnavailable } from './preview-unavailable.js';
3
- import { usePreviewZoom } from './preview-zoom.js';
4
-
5
- const { createElement: h, useEffect, useRef, useState } = react;
6
-
7
- function asset(file) {
8
- return new URL(pdfAssetsPath + '?file=' + file, location.href).href;
9
- }
10
-
11
- const pageBatch = 24;
12
-
13
- const PdfPage = react.memo(function PdfPage({ pdf, number, initialSize, width, visible, renderZoom }) {
14
- const [size, setSize] = useState(initialSize);
15
- const [failed, setFailed] = useState(false);
16
- const canvas = useRef(null);
17
- useEffect(() => {
18
- if (!visible) return;
19
- const surface = canvas.current;
20
- let canceled = false;
21
- let page;
22
- let task;
23
- async function render() {
24
- try {
25
- page = await pdf.getPage(number);
26
- if (canceled) return;
27
- const natural = page.getViewport({ scale: 1 });
28
- setSize({ width: natural.width, height: natural.height });
29
- const pageViewport = page.getViewport({ scale: Math.min(width / natural.width, 2) * renderZoom });
30
- const pixels = Math.min(window.devicePixelRatio || 1, 4096 / Math.max(pageViewport.width, pageViewport.height));
31
- surface.width = Math.max(1, Math.floor(pageViewport.width * pixels));
32
- surface.height = Math.max(1, Math.floor(pageViewport.height * pixels));
33
- task = page.render({
34
- canvas: surface, viewport: pageViewport, transform: [pixels, 0, 0, pixels, 0, 0],
35
- });
36
- await task.promise;
37
- if (!canceled) setFailed(false);
38
- } catch {
39
- if (!canceled) setFailed(true);
40
- }
41
- }
42
- void render();
43
- return () => {
44
- canceled = true;
45
- task?.cancel();
46
- page?.cleanup();
47
- };
48
- }, [pdf, number, visible, width, renderZoom]);
49
-
50
- const displayWidth = Math.min(width, size.width * 2);
51
- return h('div', {
52
- className: 'dwe-pdf-page', 'data-pdf-page': number, 'data-dwe-zoom-surface': '',
53
- style: { '--dwe-fit-width': displayWidth + 'px', '--dwe-fit-height': displayWidth * size.height / size.width + 'px' },
54
- }, visible && h('canvas', { ref: canvas, role: 'img', 'aria-label': '文档第 ' + number + ' 页', hidden: failed }),
55
- failed && h('p', { className: 'dwe-preview-hint' }, '第 ' + number + ' 页无法显示'));
56
- });
57
-
58
- function PdfPages({ pdf, initialSize }) {
59
- const scroll = useRef(null);
60
- const [width, setWidth] = useState(0);
61
- const [visible, setVisible] = useState(() => new Set());
62
- const [count, setCount] = useState(() => Math.min(pdf.numPages, pageBatch));
63
- const [renderZoom, setRenderZoom] = useState(1);
64
- usePreviewZoom(scroll, setRenderZoom);
65
- const measured = width > 0;
66
- useEffect(() => {
67
- const viewport = scroll.current;
68
- const observer = new ResizeObserver(() => setWidth(Math.max(0, viewport.clientWidth - 16)));
69
- observer.observe(viewport);
70
- return () => observer.disconnect();
71
- }, []);
72
-
73
- useEffect(() => {
74
- if (!measured) return;
75
- const viewport = scroll.current;
76
- const observer = new IntersectionObserver(entries => {
77
- setVisible(current => {
78
- const next = new Set(current);
79
- for (const entry of entries) {
80
- const number = Number(entry.target.dataset.pdfPage);
81
- if (!number) continue;
82
- if (entry.isIntersecting) next.add(number);
83
- else next.delete(number);
84
- }
85
- return next;
86
- });
87
- if (entries.some(entry => !entry.target.dataset.pdfPage && entry.isIntersecting)) {
88
- setCount(current => Math.min(pdf.numPages, current + pageBatch));
89
- }
90
- }, { root: viewport, rootMargin: '180px 0px' });
91
- for (const node of viewport.children) observer.observe(node);
92
- return () => observer.disconnect();
93
- }, [count, measured, pdf]);
94
-
95
- return h('div', { className: 'dwe-pdf-scroll', ref: scroll, tabIndex: 0, 'aria-label': '文档预览,共 ' + pdf.numPages + ' 页' },
96
- measured && Array.from({ length: count }, (_, index) => h(PdfPage, {
97
- key: index + 1, pdf, number: index + 1, initialSize, width, renderZoom, visible: visible.has(index + 1),
98
- })),
99
- measured && count < pdf.numPages && h('div', { key: 'more', className: 'dwe-pdf-more', 'aria-hidden': true }));
100
- }
101
-
102
- export function PdfPreview({ data }) {
103
- const [document, setDocument] = useState(null);
104
- const [failed, setFailed] = useState(false);
105
- useEffect(() => {
106
- let canceled = false;
107
- let task;
108
- async function load() {
109
- try {
110
- const { getDocument, GlobalWorkerOptions } = await import(asset('build/pdf.min.mjs'));
111
- if (canceled) return;
112
- GlobalWorkerOptions.workerSrc = asset('build/pdf.worker.min.mjs');
113
- task = getDocument({
114
- data: data.slice(), cMapPacked: true, useWorkerFetch: false,
115
- cMapUrl: asset('cmaps/'), standardFontDataUrl: asset('standard_fonts/'), wasmUrl: asset('wasm/'),
116
- enableXfa: false, isEvalSupported: false,
117
- });
118
- const pdf = await task.promise;
119
- if (canceled) return;
120
- const first = await pdf.getPage(1);
121
- if (!canceled) setDocument({ pdf, initialSize: first.getViewport({ scale: 1 }) });
122
- } catch {
123
- if (!canceled) setFailed(true);
124
- }
125
- }
126
- void load();
127
- return () => {
128
- canceled = true;
129
- void task?.destroy().catch(() => {});
130
- };
131
- }, [data]);
132
-
133
- if (failed) return h(PreviewUnavailable);
134
- return document ? h(PdfPages, document) : h('p', { className: 'dwe-preview-hint', role: 'status' }, '正在加载文档页面…');
135
- }
1
+ import { pdfAssetsPath } from '../document-types.js';
2
+ import { PreviewUnavailable } from './preview-unavailable.js';
3
+ import { usePreviewZoom } from './preview-zoom.js';
4
+
5
+ const { createElement: h, useEffect, useRef, useState } = react;
6
+
7
+ function asset(file) {
8
+ return new URL(pdfAssetsPath + '?file=' + file, location.href).href;
9
+ }
10
+
11
+ const pageBatch = 24;
12
+
13
+ const PdfPage = react.memo(function PdfPage({ pdf, number, initialSize, width, visible, renderZoom }) {
14
+ const [size, setSize] = useState(initialSize);
15
+ const [failed, setFailed] = useState(false);
16
+ const canvas = useRef(null);
17
+ useEffect(() => {
18
+ if (!visible) return;
19
+ const surface = canvas.current;
20
+ let canceled = false;
21
+ let page;
22
+ let task;
23
+ async function render() {
24
+ try {
25
+ page = await pdf.getPage(number);
26
+ if (canceled) return;
27
+ const natural = page.getViewport({ scale: 1 });
28
+ setSize({ width: natural.width, height: natural.height });
29
+ const pageViewport = page.getViewport({ scale: Math.min(width / natural.width, 2) * renderZoom });
30
+ const pixels = Math.min(window.devicePixelRatio || 1, 4096 / Math.max(pageViewport.width, pageViewport.height));
31
+ surface.width = Math.max(1, Math.floor(pageViewport.width * pixels));
32
+ surface.height = Math.max(1, Math.floor(pageViewport.height * pixels));
33
+ task = page.render({
34
+ canvas: surface, viewport: pageViewport, transform: [pixels, 0, 0, pixels, 0, 0],
35
+ });
36
+ await task.promise;
37
+ if (!canceled) setFailed(false);
38
+ } catch {
39
+ if (!canceled) setFailed(true);
40
+ }
41
+ }
42
+ void render();
43
+ return () => {
44
+ canceled = true;
45
+ task?.cancel();
46
+ page?.cleanup();
47
+ };
48
+ }, [pdf, number, visible, width, renderZoom]);
49
+
50
+ const displayWidth = Math.min(width, size.width * 2);
51
+ return h('div', {
52
+ className: 'dwe-pdf-page', 'data-pdf-page': number, 'data-dwe-zoom-surface': '',
53
+ style: { '--dwe-fit-width': displayWidth + 'px', '--dwe-fit-height': displayWidth * size.height / size.width + 'px' },
54
+ }, visible && h('canvas', { ref: canvas, role: 'img', 'aria-label': '文档第 ' + number + ' 页', hidden: failed }),
55
+ failed && h('p', { className: 'dwe-preview-hint' }, '第 ' + number + ' 页无法显示'));
56
+ });
57
+
58
+ function PdfPages({ pdf, initialSize }) {
59
+ const scroll = useRef(null);
60
+ const [width, setWidth] = useState(0);
61
+ const [visible, setVisible] = useState(() => new Set());
62
+ const [count, setCount] = useState(() => Math.min(pdf.numPages, pageBatch));
63
+ const [renderZoom, setRenderZoom] = useState(1);
64
+ usePreviewZoom(scroll, setRenderZoom);
65
+ const measured = width > 0;
66
+ useEffect(() => {
67
+ const viewport = scroll.current;
68
+ const observer = new ResizeObserver(() => setWidth(Math.max(0, viewport.clientWidth - 16)));
69
+ observer.observe(viewport);
70
+ return () => observer.disconnect();
71
+ }, []);
72
+
73
+ useEffect(() => {
74
+ if (!measured) return;
75
+ const viewport = scroll.current;
76
+ const observer = new IntersectionObserver(entries => {
77
+ setVisible(current => {
78
+ const next = new Set(current);
79
+ for (const entry of entries) {
80
+ const number = Number(entry.target.dataset.pdfPage);
81
+ if (!number) continue;
82
+ if (entry.isIntersecting) next.add(number);
83
+ else next.delete(number);
84
+ }
85
+ return next;
86
+ });
87
+ if (entries.some(entry => !entry.target.dataset.pdfPage && entry.isIntersecting)) {
88
+ setCount(current => Math.min(pdf.numPages, current + pageBatch));
89
+ }
90
+ }, { root: viewport, rootMargin: '180px 0px' });
91
+ for (const node of viewport.children) observer.observe(node);
92
+ return () => observer.disconnect();
93
+ }, [count, measured, pdf]);
94
+
95
+ return h('div', { className: 'dwe-pdf-scroll', ref: scroll, tabIndex: 0, 'aria-label': '文档预览,共 ' + pdf.numPages + ' 页' },
96
+ measured && Array.from({ length: count }, (_, index) => h(PdfPage, {
97
+ key: index + 1, pdf, number: index + 1, initialSize, width, renderZoom, visible: visible.has(index + 1),
98
+ })),
99
+ measured && count < pdf.numPages && h('div', { key: 'more', className: 'dwe-pdf-more', 'aria-hidden': true }));
100
+ }
101
+
102
+ export function PdfPreview({ data }) {
103
+ const [document, setDocument] = useState(null);
104
+ const [failed, setFailed] = useState(false);
105
+ useEffect(() => {
106
+ let canceled = false;
107
+ let task;
108
+ async function load() {
109
+ try {
110
+ const { getDocument, GlobalWorkerOptions } = await import(asset('build/pdf.min.mjs'));
111
+ if (canceled) return;
112
+ GlobalWorkerOptions.workerSrc = asset('build/pdf.worker.min.mjs');
113
+ task = getDocument({
114
+ data: data.slice(), cMapPacked: true, useWorkerFetch: false,
115
+ cMapUrl: asset('cmaps/'), standardFontDataUrl: asset('standard_fonts/'), wasmUrl: asset('wasm/'),
116
+ enableXfa: false, isEvalSupported: false,
117
+ });
118
+ const pdf = await task.promise;
119
+ if (canceled) return;
120
+ const first = await pdf.getPage(1);
121
+ if (!canceled) setDocument({ pdf, initialSize: first.getViewport({ scale: 1 }) });
122
+ } catch {
123
+ if (!canceled) setFailed(true);
124
+ }
125
+ }
126
+ void load();
127
+ return () => {
128
+ canceled = true;
129
+ void task?.destroy().catch(() => {});
130
+ };
131
+ }, [data]);
132
+
133
+ if (failed) return h(PreviewUnavailable);
134
+ return document ? h(PdfPages, document) : h('p', { className: 'dwe-preview-hint', role: 'status' }, '正在加载文档页面…');
135
+ }
@@ -1,6 +1,6 @@
1
- const h = react.createElement;
2
-
3
- export function PreviewUnavailable() {
4
- return h('p', { className: 'dwe-preview-hint dwe-preview-unavailable', role: 'status' },
5
- '双击文件直接打开文件,或使用右上角的「在 DSH 文档预览中打开」预览此文档');
6
- }
1
+ const h = react.createElement;
2
+
3
+ export function PreviewUnavailable() {
4
+ return h('p', { className: 'dwe-preview-hint dwe-preview-unavailable', role: 'status' },
5
+ '双击文件直接打开文件,或使用右上角的「在 DSH 文档预览中打开」预览此文档');
6
+ }
@@ -1,47 +1,47 @@
1
- const { useEffect } = react;
2
-
3
- export function usePreviewZoom(viewport, renderAtZoom) {
4
- useEffect(() => {
5
- const element = viewport.current;
6
- let zoom = 1;
7
- let settle;
8
-
9
- function wheel(event) {
10
- if (!event.ctrlKey || !event.deltaY) return;
11
- event.preventDefault();
12
- event.stopPropagation();
13
- const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? element.clientHeight : 1;
14
- const delta = Math.max(-100, Math.min(100, event.deltaY * unit));
15
- const next = Math.max(0.25, Math.min(6, zoom * Math.exp(-delta * 0.002)));
16
- if (next === zoom) return;
17
-
18
- const surface = event.target instanceof Element && event.target.closest('[data-dwe-zoom-surface]');
19
- const before = surface ? surface.getBoundingClientRect() : element.getBoundingClientRect();
20
- const x = event.clientX - before.left;
21
- const y = event.clientY - before.top;
22
- const ratio = next / zoom;
23
- const { scrollLeft, scrollTop } = element;
24
- zoom = next;
25
- element.style.setProperty('--dwe-preview-zoom', zoom);
26
- if (surface) {
27
- const after = surface.getBoundingClientRect();
28
- element.scrollLeft += after.left + x * ratio - event.clientX;
29
- element.scrollTop += after.top + y * ratio - event.clientY;
30
- } else {
31
- element.scrollLeft = (scrollLeft + x) * ratio - x;
32
- element.scrollTop = (scrollTop + y) * ratio - y;
33
- }
34
-
35
- if (renderAtZoom) {
36
- clearTimeout(settle);
37
- settle = setTimeout(() => renderAtZoom(zoom), 120);
38
- }
39
- }
40
-
41
- element.addEventListener('wheel', wheel, { passive: false });
42
- return () => {
43
- element.removeEventListener('wheel', wheel);
44
- clearTimeout(settle);
45
- };
46
- }, [viewport, renderAtZoom]);
47
- }
1
+ const { useEffect } = react;
2
+
3
+ export function usePreviewZoom(viewport, renderAtZoom) {
4
+ useEffect(() => {
5
+ const element = viewport.current;
6
+ let zoom = 1;
7
+ let settle;
8
+
9
+ function wheel(event) {
10
+ if (!event.ctrlKey || !event.deltaY) return;
11
+ event.preventDefault();
12
+ event.stopPropagation();
13
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? element.clientHeight : 1;
14
+ const delta = Math.max(-100, Math.min(100, event.deltaY * unit));
15
+ const next = Math.max(0.25, Math.min(6, zoom * Math.exp(-delta * 0.002)));
16
+ if (next === zoom) return;
17
+
18
+ const surface = event.target instanceof Element && event.target.closest('[data-dwe-zoom-surface]');
19
+ const before = surface ? surface.getBoundingClientRect() : element.getBoundingClientRect();
20
+ const x = event.clientX - before.left;
21
+ const y = event.clientY - before.top;
22
+ const ratio = next / zoom;
23
+ const { scrollLeft, scrollTop } = element;
24
+ zoom = next;
25
+ element.style.setProperty('--dwe-preview-zoom', zoom);
26
+ if (surface) {
27
+ const after = surface.getBoundingClientRect();
28
+ element.scrollLeft += after.left + x * ratio - event.clientX;
29
+ element.scrollTop += after.top + y * ratio - event.clientY;
30
+ } else {
31
+ element.scrollLeft = (scrollLeft + x) * ratio - x;
32
+ element.scrollTop = (scrollTop + y) * ratio - y;
33
+ }
34
+
35
+ if (renderAtZoom) {
36
+ clearTimeout(settle);
37
+ settle = setTimeout(() => renderAtZoom(zoom), 120);
38
+ }
39
+ }
40
+
41
+ element.addEventListener('wheel', wheel, { passive: false });
42
+ return () => {
43
+ element.removeEventListener('wheel', wheel);
44
+ clearTimeout(settle);
45
+ };
46
+ }, [viewport, renderAtZoom]);
47
+ }
package/src/ui/preview.js CHANGED
@@ -7,7 +7,7 @@ import { PreviewUnavailable } from './preview-unavailable.js';
7
7
  import { PdfPreview } from './pdf-preview.js';
8
8
  import { usePreviewZoom } from './preview-zoom.js';
9
9
 
10
- const { createElement: h, useEffect, useRef, useState } = react;
10
+ const { createElement: h, useEffect, useLayoutEffect, useRef, useState } = react;
11
11
  const coverActions = {
12
12
  [PREVIEW_IDS.archive]: '解压预览',
13
13
  [PREVIEW_IDS.model]: '查看模型',
@@ -21,6 +21,14 @@ const coverActions = {
21
21
  [PREVIEW_IDS.dfu]: '查看固件',
22
22
  };
23
23
 
24
+ export function formatSize(bytes) {
25
+ if (typeof bytes !== 'number') return '—';
26
+ if (bytes < 1024) return bytes + ' B';
27
+ if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB';
28
+ if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB';
29
+ return (bytes / 1024 ** 3).toFixed(1) + ' GB';
30
+ }
31
+
24
32
  export class PreviewBoundary extends react.Component {
25
33
  state = { failed: false };
26
34
 
@@ -35,6 +43,18 @@ export class PreviewBoundary extends react.Component {
35
43
 
36
44
  function PreviewCover({ file, root, videoType, label, archive, start }) {
37
45
  const [poster, setPoster] = useState(null);
46
+ const [resolvedSize, setResolvedSize] = useState(undefined);
47
+ const { absolute, name, size } = file;
48
+
49
+ useEffect(() => {
50
+ if (size !== undefined) return;
51
+ const controller = new AbortController();
52
+ void disks.locate(absolute, controller.signal, root).then(result => {
53
+ if (!controller.signal.aborted) setResolvedSize(result.size);
54
+ }, () => {});
55
+ return () => controller.abort();
56
+ }, [absolute, size, root]);
57
+
38
58
  useEffect(() => {
39
59
  if (!videoType) return;
40
60
  const controller = new AbortController();
@@ -50,16 +70,22 @@ function PreviewCover({ file, root, videoType, label, archive, start }) {
50
70
  h('rect', { x: 5, y: 3, width: 26, height: 30, rx: 5, fill: '#7462c4' }),
51
71
  h('path', { d: 'M18 5h3v4h-3v4h3v4h-3v4m-2 0h6v6h-6z', fill: 'none', stroke: '#fff', strokeWidth: 1.6, strokeLinejoin: 'round' }))
52
72
  : h(primitives.FileTypeIcon, { path: file.name, size: 36 });
53
- return h('div', { className: 'dwe-preview-cover', 'data-poster': Boolean(poster) || undefined },
73
+ return h('div', {
74
+ className: 'dwe-preview-cover', 'data-video': Boolean(videoType) || undefined,
75
+ 'data-poster': Boolean(poster) || undefined,
76
+ },
54
77
  poster && h('img', { src: poster, alt: '', draggable: false }),
55
- h('div', { className: 'dwe-cover-action' },
56
- !videoType && fileIcon,
57
- h('button', {
58
- type: 'button', className: videoType ? 'dwe-cover-play' : 'dwe-cover-open',
59
- title: label, 'aria-label': label, onClick: start,
60
- }, videoType ? h('svg', { width: 24, height: 24, viewBox: '0 0 24 24', 'aria-hidden': true },
61
- h('path', { d: 'M9 5v14l11-7z', fill: 'currentColor' })) : label)),
62
- h('div', { className: 'dwe-cover-file', title: file.name }, file.name));
78
+ h('div', { className: 'dwe-cover-content' },
79
+ h('div', { className: 'dwe-cover-action' },
80
+ !videoType && fileIcon,
81
+ h('button', {
82
+ type: 'button', className: videoType ? 'dwe-cover-play' : 'dwe-cover-open',
83
+ title: label, 'aria-label': label, onClick: start,
84
+ }, videoType ? h('svg', { width: 24, height: 24, viewBox: '0 0 24 24', 'aria-hidden': true },
85
+ h('path', { d: 'M9 5v14l11-7z', fill: 'currentColor' })) : label)),
86
+ h('div', { className: 'dwe-cover-details' },
87
+ h('div', { className: 'dwe-cover-file', title: name }, name),
88
+ h('div', { className: 'dwe-cover-size' }, formatSize(size ?? resolvedSize)))));
63
89
  }
64
90
 
65
91
  function ImagePreview({ url, name, onError }) {
@@ -87,6 +113,27 @@ function ImagePreview({ url, name, onError }) {
87
113
  }));
88
114
  }
89
115
 
116
+ function TextPreview({ text, eof, file }) {
117
+ const content = useRef(null);
118
+ useLayoutEffect(() => {
119
+ if (!file.line || !text) return;
120
+ let offset = 0;
121
+ for (let line = 1; line < file.line; line++) {
122
+ const end = text.indexOf('\n', offset);
123
+ if (end < 0) return;
124
+ offset = end + 1;
125
+ }
126
+ if (offset === text.length) return;
127
+ const range = document.createRange();
128
+ range.setStart(content.current.firstChild, offset);
129
+ range.setEnd(content.current.firstChild, offset + 1);
130
+ const scroll = content.current.parentElement;
131
+ scroll.scrollTop += range.getBoundingClientRect().top - scroll.getBoundingClientRect().top - 12;
132
+ }, [file, text]);
133
+ return h('pre', { ref: content, className: 'dwe-text' },
134
+ text || '(空文件)', eof ? '' : '\n\n… 仅显示首页,可在原生预览中继续查看');
135
+ }
136
+
90
137
  export function Preview({ file, root }) {
91
138
  const [result, setResult] = useState({ kind: 'loading' });
92
139
  const [started, setStarted] = useState(false);
@@ -155,8 +202,7 @@ export function Preview({ file, root }) {
155
202
  onError: () => setResult({ kind: 'unavailable' }),
156
203
  });
157
204
  case 'text':
158
- return h('pre', { className: 'dwe-text' },
159
- result.text || '(空文件)', result.eof ? '' : '\n\n… 仅显示首页,可在原生预览中继续查看');
205
+ return h(TextPreview, { text: result.text, eof: result.eof, file });
160
206
  case 'unavailable':
161
207
  return h(PreviewUnavailable);
162
208
  default: