@file-viewer/core 2.0.11 → 2.1.1
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/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/config/options.d.ts +1 -1
- package/dist/config/options.js +1 -1
- package/dist/contracts/types.d.ts +76 -1
- package/dist/headless.d.ts +3 -3
- package/dist/headless.js +2 -2
- package/dist/index.d.ts +10 -7
- package/dist/index.js +106 -49
- package/dist/lifecycle/operations.d.ts +1 -0
- package/dist/lifecycle/operations.js +65 -6
- package/dist/platform/assets.d.ts +3 -1
- package/dist/platform/assets.js +43 -6
- package/dist/registry/capabilities.d.ts +2 -2
- package/dist/registry/capabilities.js +2 -1
- package/dist/registry/formats.d.ts +20 -7
- package/dist/registry/formats.js +14 -5
- package/dist/registry/registry.d.ts +8 -1
- package/dist/registry/registry.js +29 -0
- package/dist/renderers/image.js +1 -10
- package/dist/renderers/index.d.ts +320 -2
- package/dist/renderers/index.js +27 -157
- package/dist/viewer/createViewer.js +86 -3
- package/package.json +17 -44
- package/dist/renderers/archive.d.ts +0 -2
- package/dist/renderers/archive.js +0 -547
- package/dist/renderers/archiveCache.d.ts +0 -10
- package/dist/renderers/archiveCache.js +0 -96
- package/dist/renderers/archiveFallback.d.ts +0 -7
- package/dist/renderers/archiveFallback.js +0 -166
- package/dist/renderers/archiveShared.d.ts +0 -23
- package/dist/renderers/archiveShared.js +0 -71
- package/dist/renderers/audio.d.ts +0 -8
- package/dist/renderers/audio.js +0 -219
- package/dist/renderers/cad.d.ts +0 -2
- package/dist/renderers/cad.js +0 -446
- package/dist/renderers/code.d.ts +0 -11
- package/dist/renderers/code.js +0 -233
- package/dist/renderers/data.d.ts +0 -7
- package/dist/renderers/data.js +0 -370
- package/dist/renderers/drawing.d.ts +0 -10
- package/dist/renderers/drawing.js +0 -882
- package/dist/renderers/eda.d.ts +0 -2
- package/dist/renderers/eda.js +0 -434
- package/dist/renderers/edaParser.d.ts +0 -77
- package/dist/renderers/edaParser.js +0 -569
- package/dist/renderers/email.d.ts +0 -2
- package/dist/renderers/email.js +0 -463
- package/dist/renderers/epub.d.ts +0 -2
- package/dist/renderers/epub.js +0 -331
- package/dist/renderers/geo.d.ts +0 -2
- package/dist/renderers/geo.js +0 -284
- package/dist/renderers/markdown.d.ts +0 -2
- package/dist/renderers/markdown.js +0 -83
- package/dist/renderers/model.d.ts +0 -2
- package/dist/renderers/model.js +0 -567
- package/dist/renderers/ofd.d.ts +0 -2
- package/dist/renderers/ofd.js +0 -256
- package/dist/renderers/openDocument.d.ts +0 -2
- package/dist/renderers/openDocument.js +0 -122
- package/dist/renderers/pdf.d.ts +0 -3
- package/dist/renderers/pdf.js +0 -1001
- package/dist/renderers/pdfStyles.d.ts +0 -1
- package/dist/renderers/pdfStyles.js +0 -1
- package/dist/renderers/pptx.d.ts +0 -2
- package/dist/renderers/pptx.js +0 -217
- package/dist/renderers/spreadsheet/state.d.ts +0 -80
- package/dist/renderers/spreadsheet/state.js +0 -96
- package/dist/renderers/spreadsheet/view.d.ts +0 -25
- package/dist/renderers/spreadsheet/view.js +0 -833
- package/dist/renderers/spreadsheet/worker/index.d.ts +0 -2
- package/dist/renderers/spreadsheet/worker/index.js +0 -1
- package/dist/renderers/spreadsheet/worker/sheetjs/SheetJsModel.d.ts +0 -73
- package/dist/renderers/spreadsheet/worker/sheetjs/SheetJsModel.js +0 -623
- package/dist/renderers/spreadsheet/worker/sheetjs/color.d.ts +0 -2
- package/dist/renderers/spreadsheet/worker/sheetjs/color.js +0 -73
- package/dist/renderers/spreadsheet/worker/sheetjs/index.d.ts +0 -1
- package/dist/renderers/spreadsheet/worker/sheetjs/index.js +0 -1
- package/dist/renderers/spreadsheet/worker/sheetjs/parser.d.ts +0 -18
- package/dist/renderers/spreadsheet/worker/sheetjs/parser.js +0 -106
- package/dist/renderers/spreadsheet/worker/sheetjs/sheet.worker.d.ts +0 -1
- package/dist/renderers/spreadsheet/worker/sheetjs/sheet.worker.js +0 -11
- package/dist/renderers/spreadsheet/worker/type.d.ts +0 -57
- package/dist/renderers/spreadsheet/worker/type.js +0 -1
- package/dist/renderers/spreadsheet.d.ts +0 -3
- package/dist/renderers/spreadsheet.js +0 -929
- package/dist/renderers/typst.d.ts +0 -8
- package/dist/renderers/typst.js +0 -547
- package/dist/renderers/umd/parser.d.ts +0 -30
- package/dist/renderers/umd/parser.js +0 -408
- package/dist/renderers/umd.d.ts +0 -2
- package/dist/renderers/umd.js +0 -297
- package/dist/renderers/video.d.ts +0 -8
- package/dist/renderers/video.js +0 -108
- package/dist/renderers/wordDoc.d.ts +0 -5
- package/dist/renderers/wordDoc.js +0 -284
- package/dist/renderers/wordDocx.d.ts +0 -5
- package/dist/renderers/wordDocx.js +0 -985
- package/dist/renderers/wordDocx.worker.d.ts +0 -1
- package/dist/renderers/wordDocx.worker.js +0 -96
- package/vendor/ofd/dltech/jbig2/arithmetic_decoder.js +0 -183
- package/vendor/ofd/dltech/jbig2/ccitt.js +0 -1070
- package/vendor/ofd/dltech/jbig2/compatibility.js +0 -12
- package/vendor/ofd/dltech/jbig2/core_utils.js +0 -180
- package/vendor/ofd/dltech/jbig2/is_node.js +0 -27
- package/vendor/ofd/dltech/jbig2/jbig2.js +0 -2589
- package/vendor/ofd/dltech/jbig2/jbig2_stream.js +0 -81
- package/vendor/ofd/dltech/jbig2/primitives.js +0 -387
- package/vendor/ofd/dltech/jbig2/stream.js +0 -1348
- package/vendor/ofd/dltech/jbig2/util.js +0 -972
- package/vendor/ofd/dltech/ofd/ofd.d.ts +0 -11
- package/vendor/ofd/dltech/ofd/ofd.js +0 -100
- package/vendor/ofd/dltech/ofd/ofd_parser.js +0 -395
- package/vendor/ofd/dltech/ofd/ofd_render.js +0 -473
- package/vendor/ofd/dltech/ofd/ofd_util.js +0 -350
- package/vendor/ofd/dltech/ofd/pipeline.js +0 -26
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
const DB_NAME = 'flyfish-file-viewer-cache';
|
|
2
|
-
const STORE_NAME = 'archiveEntries';
|
|
3
|
-
const DB_VERSION = 1;
|
|
4
|
-
const MAX_CACHE_ENTRY_BYTES = 24 * 1024 * 1024;
|
|
5
|
-
const MAX_CACHE_TOTAL_BYTES = 96 * 1024 * 1024;
|
|
6
|
-
let dbPromise = null;
|
|
7
|
-
const canUseIndexedDB = () => typeof indexedDB !== 'undefined';
|
|
8
|
-
const openCacheDb = () => {
|
|
9
|
-
if (!canUseIndexedDB()) {
|
|
10
|
-
return Promise.reject(new Error('IndexedDB 不可用'));
|
|
11
|
-
}
|
|
12
|
-
if (dbPromise) {
|
|
13
|
-
return dbPromise;
|
|
14
|
-
}
|
|
15
|
-
dbPromise = new Promise((resolve, reject) => {
|
|
16
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
17
|
-
request.onupgradeneeded = () => {
|
|
18
|
-
const db = request.result;
|
|
19
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
20
|
-
const store = db.createObjectStore(STORE_NAME, { keyPath: 'key' });
|
|
21
|
-
store.createIndex('updatedAt', 'updatedAt');
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
request.onsuccess = () => resolve(request.result);
|
|
25
|
-
request.onerror = () => reject(request.error);
|
|
26
|
-
});
|
|
27
|
-
return dbPromise;
|
|
28
|
-
};
|
|
29
|
-
const runStore = async (mode, runner) => {
|
|
30
|
-
const db = await openCacheDb();
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
const transaction = db.transaction(STORE_NAME, mode);
|
|
33
|
-
const request = runner(transaction.objectStore(STORE_NAME));
|
|
34
|
-
request.onsuccess = () => resolve(request.result);
|
|
35
|
-
request.onerror = () => reject(request.error);
|
|
36
|
-
transaction.onerror = () => reject(transaction.error);
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
export const readArchiveCache = async (key) => {
|
|
40
|
-
try {
|
|
41
|
-
const cached = await runStore('readonly', store => store.get(key));
|
|
42
|
-
return cached || null;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
const pruneArchiveCache = async () => {
|
|
49
|
-
try {
|
|
50
|
-
const db = await openCacheDb();
|
|
51
|
-
await new Promise((resolve, reject) => {
|
|
52
|
-
const transaction = db.transaction(STORE_NAME, 'readwrite');
|
|
53
|
-
const store = transaction.objectStore(STORE_NAME);
|
|
54
|
-
const index = store.index('updatedAt');
|
|
55
|
-
const entries = [];
|
|
56
|
-
let total = 0;
|
|
57
|
-
index.openCursor().onsuccess = event => {
|
|
58
|
-
const cursor = event.target.result;
|
|
59
|
-
if (!cursor) {
|
|
60
|
-
while (total > MAX_CACHE_TOTAL_BYTES && entries.length) {
|
|
61
|
-
const entry = entries.shift();
|
|
62
|
-
if (entry) {
|
|
63
|
-
total -= entry.size;
|
|
64
|
-
store.delete(entry.key);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const value = cursor.value;
|
|
70
|
-
total += value.size || 0;
|
|
71
|
-
entries.push({ key: value.key, size: value.size || 0 });
|
|
72
|
-
cursor.continue();
|
|
73
|
-
};
|
|
74
|
-
transaction.oncomplete = () => resolve();
|
|
75
|
-
transaction.onerror = () => reject(transaction.error);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Cache pruning is a best-effort optimization and must not block preview.
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
export const writeArchiveCache = async (entry) => {
|
|
83
|
-
if (entry.size > MAX_CACHE_ENTRY_BYTES) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
try {
|
|
87
|
-
await runStore('readwrite', store => store.put({
|
|
88
|
-
...entry,
|
|
89
|
-
updatedAt: Date.now(),
|
|
90
|
-
}));
|
|
91
|
-
await pruneArchiveCache();
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Quota, private-mode, or browser policy errors simply disable cache writes.
|
|
95
|
-
}
|
|
96
|
-
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type ArchiveEntryView } from './archiveShared';
|
|
2
|
-
/**
|
|
3
|
-
* Worker fallback for constrained browsers, temporary local servers, and
|
|
4
|
-
* mobile WebViews. The main libarchive path still covers broader formats;
|
|
5
|
-
* this covers common ZIP/TAR/GZIP archives without an extra static Worker.
|
|
6
|
-
*/
|
|
7
|
-
export declare const loadArchiveEntriesWithoutWorker: (data: ArrayBuffer, filename: string) => Promise<ArchiveEntryView[] | null>;
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { getArchiveEntryExtension, isPreviewableArchiveEntry, } from './archiveShared.js';
|
|
2
|
-
const ZIP_LIKE_EXTENSIONS = new Set(['zip', 'zipx', 'jar', 'war', 'ear', 'apk', 'cbz']);
|
|
3
|
-
const TAR_LIKE_EXTENSIONS = new Set(['tar', 'tgz', 'gz', 'gzip']);
|
|
4
|
-
const TAR_BLOCK_SIZE = 512;
|
|
5
|
-
const toArrayBuffer = (bytes) => {
|
|
6
|
-
const output = new Uint8Array(bytes.byteLength);
|
|
7
|
-
output.set(bytes);
|
|
8
|
-
return output.buffer;
|
|
9
|
-
};
|
|
10
|
-
const decompressBytes = async (bytes, format) => {
|
|
11
|
-
if (typeof DecompressionStream === 'undefined') {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
const stream = new Blob([toArrayBuffer(bytes)])
|
|
15
|
-
.stream()
|
|
16
|
-
.pipeThrough(new DecompressionStream(format));
|
|
17
|
-
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
18
|
-
};
|
|
19
|
-
const normalizeArchivePath = (path) => {
|
|
20
|
-
return path.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
21
|
-
};
|
|
22
|
-
const getPathName = (path) => {
|
|
23
|
-
const parts = normalizeArchivePath(path).split('/');
|
|
24
|
-
return parts[parts.length - 1] || path;
|
|
25
|
-
};
|
|
26
|
-
const getPathDepth = (path) => {
|
|
27
|
-
return Math.max(0, normalizeArchivePath(path).split('/').length - 1);
|
|
28
|
-
};
|
|
29
|
-
const createEntryView = (source) => {
|
|
30
|
-
const path = normalizeArchivePath(source.path);
|
|
31
|
-
const name = getPathName(path);
|
|
32
|
-
return {
|
|
33
|
-
id: path,
|
|
34
|
-
path,
|
|
35
|
-
name,
|
|
36
|
-
extension: getArchiveEntryExtension(name),
|
|
37
|
-
size: source.size,
|
|
38
|
-
lastModified: source.lastModified,
|
|
39
|
-
depth: getPathDepth(path),
|
|
40
|
-
previewable: isPreviewableArchiveEntry(name),
|
|
41
|
-
compressedFile: {
|
|
42
|
-
name,
|
|
43
|
-
size: source.size,
|
|
44
|
-
lastModified: source.lastModified,
|
|
45
|
-
async extract() {
|
|
46
|
-
const buffer = await source.load();
|
|
47
|
-
return new File([buffer], name, {
|
|
48
|
-
type: 'application/octet-stream',
|
|
49
|
-
lastModified: source.lastModified || Date.now(),
|
|
50
|
-
});
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
};
|
|
55
|
-
const parseOctal = (bytes, start, length) => {
|
|
56
|
-
const text = new TextDecoder('ascii')
|
|
57
|
-
.decode(bytes.slice(start, start + length))
|
|
58
|
-
.replace(/\0.*$/, '')
|
|
59
|
-
.trim();
|
|
60
|
-
return text ? Number.parseInt(text, 8) || 0 : 0;
|
|
61
|
-
};
|
|
62
|
-
const readTarName = (bytes, offset) => {
|
|
63
|
-
const decoder = new TextDecoder('utf-8');
|
|
64
|
-
const name = decoder.decode(bytes.slice(offset, offset + 100)).replace(/\0.*$/, '');
|
|
65
|
-
const prefix = decoder.decode(bytes.slice(offset + 345, offset + 500)).replace(/\0.*$/, '');
|
|
66
|
-
return normalizeArchivePath(prefix ? `${prefix}/${name}` : name);
|
|
67
|
-
};
|
|
68
|
-
const parseTarEntries = (bytes) => {
|
|
69
|
-
const entries = [];
|
|
70
|
-
let offset = 0;
|
|
71
|
-
while (offset + TAR_BLOCK_SIZE <= bytes.length) {
|
|
72
|
-
const header = bytes.slice(offset, offset + TAR_BLOCK_SIZE);
|
|
73
|
-
if (header.every(value => value === 0)) {
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
const path = readTarName(bytes, offset);
|
|
77
|
-
const size = parseOctal(bytes, offset + 124, 12);
|
|
78
|
-
const typeFlag = String.fromCharCode(bytes[offset + 156] || 0);
|
|
79
|
-
const dataOffset = offset + TAR_BLOCK_SIZE;
|
|
80
|
-
const nextOffset = dataOffset + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE;
|
|
81
|
-
if (path && typeFlag !== '5') {
|
|
82
|
-
const fileBytes = bytes.slice(dataOffset, dataOffset + size);
|
|
83
|
-
entries.push(createEntryView({
|
|
84
|
-
path,
|
|
85
|
-
size,
|
|
86
|
-
load: async () => toArrayBuffer(fileBytes),
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
offset = nextOffset;
|
|
90
|
-
}
|
|
91
|
-
return entries;
|
|
92
|
-
};
|
|
93
|
-
const getArchiveExtension = (filename) => {
|
|
94
|
-
const lower = filename.toLowerCase();
|
|
95
|
-
if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) {
|
|
96
|
-
return 'tgz';
|
|
97
|
-
}
|
|
98
|
-
return getArchiveEntryExtension(filename);
|
|
99
|
-
};
|
|
100
|
-
const getGzipEntryName = (filename) => {
|
|
101
|
-
const lower = filename.toLowerCase();
|
|
102
|
-
if (lower.endsWith('.gzip')) {
|
|
103
|
-
return filename.slice(0, -5) || 'archive';
|
|
104
|
-
}
|
|
105
|
-
if (lower.endsWith('.gz')) {
|
|
106
|
-
return filename.slice(0, -3) || 'archive';
|
|
107
|
-
}
|
|
108
|
-
return `${filename || 'archive'}.bin`;
|
|
109
|
-
};
|
|
110
|
-
const loadZipEntries = async (data) => {
|
|
111
|
-
const { default: JSZip } = await import('jszip');
|
|
112
|
-
const zip = await JSZip.loadAsync(data);
|
|
113
|
-
const entries = [];
|
|
114
|
-
zip.forEach((relativePath, file) => {
|
|
115
|
-
var _a, _b;
|
|
116
|
-
if (file.dir) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const metadata = file;
|
|
120
|
-
const normalizedPath = normalizeArchivePath(relativePath);
|
|
121
|
-
entries.push(createEntryView({
|
|
122
|
-
path: normalizedPath,
|
|
123
|
-
size: ((_a = metadata._data) === null || _a === void 0 ? void 0 : _a.uncompressedSize) || 0,
|
|
124
|
-
lastModified: (_b = file.date) === null || _b === void 0 ? void 0 : _b.getTime(),
|
|
125
|
-
load: async () => file.async('arraybuffer'),
|
|
126
|
-
}));
|
|
127
|
-
});
|
|
128
|
-
return entries;
|
|
129
|
-
};
|
|
130
|
-
const loadTarEntries = async (data, filename, extension) => {
|
|
131
|
-
const source = new Uint8Array(data);
|
|
132
|
-
const bytes = extension === 'tar'
|
|
133
|
-
? source
|
|
134
|
-
: await decompressBytes(source, 'gzip');
|
|
135
|
-
if (!bytes) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (extension === 'gz' || extension === 'gzip') {
|
|
139
|
-
const lower = filename.toLowerCase();
|
|
140
|
-
const isTarGz = lower.endsWith('.tar.gz') || lower.endsWith('.tgz');
|
|
141
|
-
if (!isTarGz) {
|
|
142
|
-
const name = getGzipEntryName(filename);
|
|
143
|
-
return [createEntryView({
|
|
144
|
-
path: name,
|
|
145
|
-
size: bytes.byteLength,
|
|
146
|
-
load: async () => toArrayBuffer(bytes),
|
|
147
|
-
})];
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return parseTarEntries(bytes);
|
|
151
|
-
};
|
|
152
|
-
/**
|
|
153
|
-
* Worker fallback for constrained browsers, temporary local servers, and
|
|
154
|
-
* mobile WebViews. The main libarchive path still covers broader formats;
|
|
155
|
-
* this covers common ZIP/TAR/GZIP archives without an extra static Worker.
|
|
156
|
-
*/
|
|
157
|
-
export const loadArchiveEntriesWithoutWorker = async (data, filename) => {
|
|
158
|
-
const extension = getArchiveExtension(filename);
|
|
159
|
-
if (ZIP_LIKE_EXTENSIONS.has(extension)) {
|
|
160
|
-
return loadZipEntries(data);
|
|
161
|
-
}
|
|
162
|
-
if (TAR_LIKE_EXTENSIONS.has(extension)) {
|
|
163
|
-
return loadTarEntries(data, filename, extension);
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export declare const ARCHIVE_PREVIEWABLE_EXTENSIONS: readonly string[];
|
|
2
|
-
export interface ArchiveEntryView {
|
|
3
|
-
id: string;
|
|
4
|
-
path: string;
|
|
5
|
-
name: string;
|
|
6
|
-
extension: string;
|
|
7
|
-
size: number;
|
|
8
|
-
lastModified?: number;
|
|
9
|
-
depth: number;
|
|
10
|
-
previewable: boolean;
|
|
11
|
-
compressedFile: {
|
|
12
|
-
name: string;
|
|
13
|
-
size: number;
|
|
14
|
-
lastModified?: number;
|
|
15
|
-
extract(): Promise<File>;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
export declare const getArchiveEntryExtension: (name: string) => string;
|
|
19
|
-
export declare const isArchiveExtension: (extension: string) => boolean;
|
|
20
|
-
export declare const isPreviewableArchiveEntry: (name: string) => boolean;
|
|
21
|
-
export declare const formatArchiveBytes: (value: number) => string;
|
|
22
|
-
export declare const flattenArchiveObject: (input: Record<string, unknown>, prefix?: string) => ArchiveEntryView[];
|
|
23
|
-
export declare const createArchiveCacheKey: (archiveName: string, archiveSize: number, entry: ArchiveEntryView) => string;
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { ARCHIVE_EXTENSIONS, DEFAULT_SUPPORTED_EXTENSIONS, } from '../registry/formats.js';
|
|
2
|
-
export const ARCHIVE_PREVIEWABLE_EXTENSIONS = DEFAULT_SUPPORTED_EXTENSIONS;
|
|
3
|
-
export const getArchiveEntryExtension = (name) => {
|
|
4
|
-
const clean = name.split(/[?#]/)[0] || name;
|
|
5
|
-
const dot = clean.lastIndexOf('.');
|
|
6
|
-
return dot === -1 ? '' : clean.slice(dot + 1).toLowerCase();
|
|
7
|
-
};
|
|
8
|
-
export const isArchiveExtension = (extension) => (ARCHIVE_EXTENSIONS.includes(extension.toLowerCase()));
|
|
9
|
-
export const isPreviewableArchiveEntry = (name) => {
|
|
10
|
-
const extension = getArchiveEntryExtension(name);
|
|
11
|
-
return ARCHIVE_PREVIEWABLE_EXTENSIONS.includes(extension);
|
|
12
|
-
};
|
|
13
|
-
export const formatArchiveBytes = (value) => {
|
|
14
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
15
|
-
return '-';
|
|
16
|
-
}
|
|
17
|
-
if (value < 1024) {
|
|
18
|
-
return `${value} B`;
|
|
19
|
-
}
|
|
20
|
-
const units = ['KB', 'MB', 'GB'];
|
|
21
|
-
let next = value / 1024;
|
|
22
|
-
for (const unit of units) {
|
|
23
|
-
if (next < 1024 || unit === units[units.length - 1]) {
|
|
24
|
-
return `${next.toFixed(next < 10 ? 1 : 0)} ${unit}`;
|
|
25
|
-
}
|
|
26
|
-
next /= 1024;
|
|
27
|
-
}
|
|
28
|
-
return `${value} B`;
|
|
29
|
-
};
|
|
30
|
-
const isCompressedFile = (value) => {
|
|
31
|
-
return typeof value === 'object' &&
|
|
32
|
-
value !== null &&
|
|
33
|
-
'extract' in value &&
|
|
34
|
-
typeof value.extract === 'function';
|
|
35
|
-
};
|
|
36
|
-
export const flattenArchiveObject = (input, prefix = '') => {
|
|
37
|
-
const entries = [];
|
|
38
|
-
Object.entries(input).forEach(([key, value]) => {
|
|
39
|
-
const path = prefix ? `${prefix}/${key}` : key;
|
|
40
|
-
if (isCompressedFile(value)) {
|
|
41
|
-
const name = value.name || key;
|
|
42
|
-
const extension = getArchiveEntryExtension(name);
|
|
43
|
-
entries.push({
|
|
44
|
-
id: path,
|
|
45
|
-
path,
|
|
46
|
-
name,
|
|
47
|
-
extension,
|
|
48
|
-
size: value.size || 0,
|
|
49
|
-
lastModified: value.lastModified,
|
|
50
|
-
depth: path.split('/').length - 1,
|
|
51
|
-
previewable: isPreviewableArchiveEntry(name),
|
|
52
|
-
compressedFile: value,
|
|
53
|
-
});
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (value && typeof value === 'object') {
|
|
57
|
-
entries.push(...flattenArchiveObject(value, path));
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
return entries;
|
|
61
|
-
};
|
|
62
|
-
export const createArchiveCacheKey = (archiveName, archiveSize, entry) => {
|
|
63
|
-
return [
|
|
64
|
-
'archive-entry',
|
|
65
|
-
archiveName || 'archive',
|
|
66
|
-
archiveSize,
|
|
67
|
-
entry.path,
|
|
68
|
-
entry.size,
|
|
69
|
-
entry.lastModified || 0,
|
|
70
|
-
].join(':');
|
|
71
|
-
};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { FileViewerRenderedInstance } from '../contracts/types';
|
|
2
|
-
/**
|
|
3
|
-
* Pure TypeScript audio renderer.
|
|
4
|
-
*
|
|
5
|
-
* Regular audio files use the browser's native `<audio>` element. MIDI stays in
|
|
6
|
-
* the same async chunk and lazily imports `@tonejs/midi` only for `.mid/.midi`.
|
|
7
|
-
*/
|
|
8
|
-
export default function renderAudio(buffer: ArrayBuffer, target: HTMLDivElement, type?: string): Promise<FileViewerRenderedInstance>;
|
package/dist/renderers/audio.js
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
const AUDIO_MIME_MAP = {
|
|
2
|
-
aac: 'audio/aac',
|
|
3
|
-
flac: 'audio/flac',
|
|
4
|
-
m4a: 'audio/mp4',
|
|
5
|
-
mp3: 'audio/mpeg',
|
|
6
|
-
mpeg: 'audio/mpeg',
|
|
7
|
-
oga: 'audio/ogg',
|
|
8
|
-
ogg: 'audio/ogg',
|
|
9
|
-
opus: 'audio/ogg; codecs=opus',
|
|
10
|
-
wav: 'audio/wav',
|
|
11
|
-
weba: 'audio/webm'
|
|
12
|
-
};
|
|
13
|
-
const audioStyle = `
|
|
14
|
-
.fv-audio-viewer{width:100%;min-height:100%;display:flex;align-items:center;justify-content:center;padding:28px;background:linear-gradient(135deg,rgba(14,116,144,.1),transparent 34%),linear-gradient(180deg,#f5f8fb 0%,#edf2f7 100%);box-sizing:border-box}
|
|
15
|
-
.fv-audio-card{width:min(100%,640px);display:grid;grid-template-columns:86px minmax(0,1fr);gap:18px;align-items:center;padding:24px;border-radius:8px;border:1px solid rgba(15,23,42,.08);background:rgba(255,255,255,.92);box-shadow:0 20px 52px rgba(15,23,42,.13);box-sizing:border-box}
|
|
16
|
-
.fv-audio-art{position:relative;width:86px;height:86px;border-radius:8px;background:linear-gradient(135deg,#0f766e,#2dd4bf);box-shadow:inset 0 0 0 1px rgba(255,255,255,.24)}
|
|
17
|
-
.fv-audio-art span{position:absolute;inset:18px;border-radius:999px;border:8px solid rgba(255,255,255,.88)}
|
|
18
|
-
.fv-audio-art i{position:absolute;right:18px;bottom:20px;width:18px;height:36px;border-radius:10px 10px 4px 4px;background:rgba(255,255,255,.9)}
|
|
19
|
-
.fv-audio-copy{min-width:0}
|
|
20
|
-
.fv-audio-kicker{color:#0f766e;font-size:12px;font-weight:800;letter-spacing:0}
|
|
21
|
-
.fv-audio-copy strong{display:block;margin-top:5px;color:#132235;font-size:23px;line-height:1.15}
|
|
22
|
-
.fv-audio-copy p{margin:8px 0 0;color:#64748b;font-size:13px;line-height:1.7}
|
|
23
|
-
.fv-audio-meter{grid-column:1/-1;display:grid;grid-template-columns:48px minmax(0,1fr) 48px;align-items:center;gap:10px;color:#64748b;font-size:12px;font-variant-numeric:tabular-nums}
|
|
24
|
-
.fv-audio-progress{height:6px;overflow:hidden;border-radius:999px;background:rgba(15,118,110,.12)}
|
|
25
|
-
.fv-audio-progress i{display:block;height:100%;border-radius:inherit;background:linear-gradient(90deg,#0f766e,#2dd4bf);transition:width .18s ease}
|
|
26
|
-
.fv-audio-control{grid-column:1/-1;width:100%;height:42px}
|
|
27
|
-
.fv-midi-viewer{min-height:100%;padding:28px;background:#eef1f4;box-sizing:border-box}
|
|
28
|
-
.fv-midi-card{max-width:960px;margin:0 auto;border-radius:8px;border:1px solid rgba(15,23,42,.08);background:#fff;box-shadow:0 18px 48px rgba(15,23,42,.12);overflow:hidden}
|
|
29
|
-
.fv-midi-card header{padding:18px 22px;border-bottom:1px solid rgba(15,23,42,.08)}
|
|
30
|
-
.fv-midi-card header span{display:block;color:#0f766e;font-size:12px;font-weight:800}
|
|
31
|
-
.fv-midi-card header strong{display:block;margin-top:6px;color:#132235;font-size:22px}
|
|
32
|
-
.fv-midi-state{padding:28px 22px;color:#64748b}
|
|
33
|
-
.fv-midi-error{color:#b42318}
|
|
34
|
-
.fv-midi-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:rgba(15,23,42,.08)}
|
|
35
|
-
.fv-midi-stats div{padding:16px;background:#f8fafc}
|
|
36
|
-
.fv-midi-stats span{display:block;color:#64748b;font-size:12px}
|
|
37
|
-
.fv-midi-stats strong{display:block;margin-top:4px;color:#132235;font-size:20px}
|
|
38
|
-
.fv-midi-table-wrap{overflow:auto}
|
|
39
|
-
.fv-midi-table{width:100%;border-collapse:collapse;color:#132235;font-size:14px}
|
|
40
|
-
.fv-midi-table th,.fv-midi-table td{padding:12px 16px;border-top:1px solid rgba(15,23,42,.08);text-align:left}
|
|
41
|
-
.fv-midi-table th{color:#64748b;background:#f8fafc;font-weight:700}
|
|
42
|
-
@media (max-width:700px){.fv-midi-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
43
|
-
`;
|
|
44
|
-
const createElement = (tagName, className, text) => {
|
|
45
|
-
const element = document.createElement(tagName);
|
|
46
|
-
if (className) {
|
|
47
|
-
element.className = className;
|
|
48
|
-
}
|
|
49
|
-
if (typeof text === 'string') {
|
|
50
|
-
element.textContent = text;
|
|
51
|
-
}
|
|
52
|
-
return element;
|
|
53
|
-
};
|
|
54
|
-
const formatDuration = (seconds) => {
|
|
55
|
-
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
56
|
-
return '00:00';
|
|
57
|
-
}
|
|
58
|
-
const minutes = Math.floor(seconds / 60);
|
|
59
|
-
const rest = Math.round(seconds % 60);
|
|
60
|
-
return `${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`;
|
|
61
|
-
};
|
|
62
|
-
const createStyle = () => {
|
|
63
|
-
const style = document.createElement('style');
|
|
64
|
-
style.textContent = audioStyle;
|
|
65
|
-
return style;
|
|
66
|
-
};
|
|
67
|
-
const clearTarget = (target) => {
|
|
68
|
-
target.replaceChildren();
|
|
69
|
-
};
|
|
70
|
-
const createRenderedInstance = (target, cleanup) => ({
|
|
71
|
-
$el: target,
|
|
72
|
-
unmount() {
|
|
73
|
-
cleanup();
|
|
74
|
-
clearTarget(target);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
const renderAudioElement = (buffer, target, type) => {
|
|
78
|
-
const normalizedType = type.trim().toLowerCase() || 'mp3';
|
|
79
|
-
const mimeType = AUDIO_MIME_MAP[normalizedType] || 'audio/*';
|
|
80
|
-
const sourceUrl = URL.createObjectURL(new Blob([buffer], { type: mimeType }));
|
|
81
|
-
const root = createElement('div', 'fv-audio-viewer');
|
|
82
|
-
const card = createElement('section', 'fv-audio-card');
|
|
83
|
-
const art = createElement('div', 'fv-audio-art');
|
|
84
|
-
art.append(createElement('span'), createElement('i'));
|
|
85
|
-
const copy = createElement('div', 'fv-audio-copy');
|
|
86
|
-
copy.append(createElement('span', 'fv-audio-kicker', normalizedType.toUpperCase() || 'AUDIO'), createElement('strong', '', '音频预览'), createElement('p', '', '使用浏览器原生播放器打开,兼容性取决于当前浏览器支持的音频编码。'));
|
|
87
|
-
const currentTimeText = createElement('span', '', '00:00');
|
|
88
|
-
const durationText = createElement('span', '', '--:--');
|
|
89
|
-
const progressFill = createElement('i');
|
|
90
|
-
const progress = createElement('div', 'fv-audio-progress');
|
|
91
|
-
progress.setAttribute('aria-hidden', 'true');
|
|
92
|
-
progress.append(progressFill);
|
|
93
|
-
const meter = createElement('div', 'fv-audio-meter');
|
|
94
|
-
meter.append(currentTimeText, progress, durationText);
|
|
95
|
-
const audio = createElement('audio', 'fv-audio-control');
|
|
96
|
-
audio.src = sourceUrl;
|
|
97
|
-
audio.controls = true;
|
|
98
|
-
audio.preload = 'metadata';
|
|
99
|
-
audio.textContent = '当前浏览器不支持音频播放。';
|
|
100
|
-
const handleLoadedMetadata = () => {
|
|
101
|
-
durationText.textContent = Number.isFinite(audio.duration) && audio.duration > 0
|
|
102
|
-
? formatDuration(audio.duration)
|
|
103
|
-
: '--:--';
|
|
104
|
-
};
|
|
105
|
-
const handleTimeUpdate = () => {
|
|
106
|
-
currentTimeText.textContent = formatDuration(audio.currentTime);
|
|
107
|
-
const percent = Number.isFinite(audio.duration) && audio.duration > 0
|
|
108
|
-
? Math.min(100, Math.max(0, audio.currentTime / audio.duration * 100))
|
|
109
|
-
: 0;
|
|
110
|
-
progressFill.style.width = `${percent}%`;
|
|
111
|
-
};
|
|
112
|
-
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
|
113
|
-
audio.addEventListener('timeupdate', handleTimeUpdate);
|
|
114
|
-
card.append(art, copy, meter, audio);
|
|
115
|
-
root.append(card);
|
|
116
|
-
target.replaceChildren(createStyle(), root);
|
|
117
|
-
return createRenderedInstance(target, () => {
|
|
118
|
-
audio.pause();
|
|
119
|
-
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
|
120
|
-
audio.removeEventListener('timeupdate', handleTimeUpdate);
|
|
121
|
-
URL.revokeObjectURL(sourceUrl);
|
|
122
|
-
});
|
|
123
|
-
};
|
|
124
|
-
const renderMidiElement = (buffer, target) => {
|
|
125
|
-
let disposed = false;
|
|
126
|
-
const root = createElement('div', 'fv-midi-viewer');
|
|
127
|
-
const card = createElement('section', 'fv-midi-card');
|
|
128
|
-
const header = document.createElement('header');
|
|
129
|
-
const title = createElement('strong', '', 'MIDI 文件');
|
|
130
|
-
header.append(createElement('span', '', 'MIDI'), title);
|
|
131
|
-
const body = createElement('div', 'fv-midi-state', '正在解析 MIDI 轨道...');
|
|
132
|
-
card.append(header, body);
|
|
133
|
-
root.append(card);
|
|
134
|
-
target.replaceChildren(createStyle(), root);
|
|
135
|
-
const renderError = (message) => {
|
|
136
|
-
body.className = 'fv-midi-state fv-midi-error';
|
|
137
|
-
body.textContent = message;
|
|
138
|
-
};
|
|
139
|
-
const renderTrackTable = (tracks) => {
|
|
140
|
-
const wrap = createElement('div', 'fv-midi-table-wrap');
|
|
141
|
-
const table = createElement('table', 'fv-midi-table');
|
|
142
|
-
const thead = document.createElement('thead');
|
|
143
|
-
const headerRow = document.createElement('tr');
|
|
144
|
-
for (const label of ['轨道', '乐器', '通道', '音符数', '时长']) {
|
|
145
|
-
headerRow.append(createElement('th', '', label));
|
|
146
|
-
}
|
|
147
|
-
thead.append(headerRow);
|
|
148
|
-
const tbody = document.createElement('tbody');
|
|
149
|
-
for (const track of tracks) {
|
|
150
|
-
const row = document.createElement('tr');
|
|
151
|
-
row.append(createElement('td', '', track.name), createElement('td', '', track.instrument), createElement('td', '', String(track.channel + 1)), createElement('td', '', String(track.notes)), createElement('td', '', formatDuration(track.duration)));
|
|
152
|
-
tbody.append(row);
|
|
153
|
-
}
|
|
154
|
-
table.append(thead, tbody);
|
|
155
|
-
wrap.append(table);
|
|
156
|
-
return wrap;
|
|
157
|
-
};
|
|
158
|
-
const renderSummary = (input) => {
|
|
159
|
-
title.textContent = input.name;
|
|
160
|
-
const totalNotes = input.tracks.reduce((sum, track) => sum + track.notes, 0);
|
|
161
|
-
const stats = createElement('div', 'fv-midi-stats');
|
|
162
|
-
for (const [label, value] of [
|
|
163
|
-
['时长', formatDuration(input.duration)],
|
|
164
|
-
['PPQ', String(input.ppq)],
|
|
165
|
-
['轨道', String(input.tracks.length)],
|
|
166
|
-
['音符', String(totalNotes)]
|
|
167
|
-
]) {
|
|
168
|
-
const stat = document.createElement('div');
|
|
169
|
-
stat.append(createElement('span', '', label), createElement('strong', '', value));
|
|
170
|
-
stats.append(stat);
|
|
171
|
-
}
|
|
172
|
-
body.replaceWith(stats, renderTrackTable(input.tracks));
|
|
173
|
-
};
|
|
174
|
-
void (async () => {
|
|
175
|
-
try {
|
|
176
|
-
const { Midi } = await import('@tonejs/midi');
|
|
177
|
-
const midi = new Midi(buffer);
|
|
178
|
-
if (disposed) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
renderSummary({
|
|
182
|
-
name: midi.name || 'MIDI 文件',
|
|
183
|
-
duration: midi.duration,
|
|
184
|
-
ppq: midi.header.ppq,
|
|
185
|
-
tracks: midi.tracks.map((track, index) => {
|
|
186
|
-
var _a, _b;
|
|
187
|
-
return ({
|
|
188
|
-
name: track.name || `Track ${index + 1}`,
|
|
189
|
-
instrument: ((_a = track.instrument) === null || _a === void 0 ? void 0 : _a.name) || ((_b = track.instrument) === null || _b === void 0 ? void 0 : _b.family) || 'Unknown',
|
|
190
|
-
channel: track.channel,
|
|
191
|
-
notes: track.notes.length,
|
|
192
|
-
duration: track.duration
|
|
193
|
-
});
|
|
194
|
-
})
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
if (!disposed) {
|
|
199
|
-
renderError(error instanceof Error ? error.message : 'MIDI 解析失败');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
})();
|
|
203
|
-
return createRenderedInstance(target, () => {
|
|
204
|
-
disposed = true;
|
|
205
|
-
});
|
|
206
|
-
};
|
|
207
|
-
/**
|
|
208
|
-
* Pure TypeScript audio renderer.
|
|
209
|
-
*
|
|
210
|
-
* Regular audio files use the browser's native `<audio>` element. MIDI stays in
|
|
211
|
-
* the same async chunk and lazily imports `@tonejs/midi` only for `.mid/.midi`.
|
|
212
|
-
*/
|
|
213
|
-
export default async function renderAudio(buffer, target, type) {
|
|
214
|
-
const normalizedType = (type || 'mp3').toLowerCase();
|
|
215
|
-
if (normalizedType === 'midi' || normalizedType === 'mid') {
|
|
216
|
-
return renderMidiElement(buffer, target);
|
|
217
|
-
}
|
|
218
|
-
return renderAudioElement(buffer, target, normalizedType);
|
|
219
|
-
}
|
package/dist/renderers/cad.d.ts
DELETED