@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.
Files changed (116) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/config/options.d.ts +1 -1
  4. package/dist/config/options.js +1 -1
  5. package/dist/contracts/types.d.ts +76 -1
  6. package/dist/headless.d.ts +3 -3
  7. package/dist/headless.js +2 -2
  8. package/dist/index.d.ts +10 -7
  9. package/dist/index.js +106 -49
  10. package/dist/lifecycle/operations.d.ts +1 -0
  11. package/dist/lifecycle/operations.js +65 -6
  12. package/dist/platform/assets.d.ts +3 -1
  13. package/dist/platform/assets.js +43 -6
  14. package/dist/registry/capabilities.d.ts +2 -2
  15. package/dist/registry/capabilities.js +2 -1
  16. package/dist/registry/formats.d.ts +20 -7
  17. package/dist/registry/formats.js +14 -5
  18. package/dist/registry/registry.d.ts +8 -1
  19. package/dist/registry/registry.js +29 -0
  20. package/dist/renderers/image.js +1 -10
  21. package/dist/renderers/index.d.ts +320 -2
  22. package/dist/renderers/index.js +27 -157
  23. package/dist/viewer/createViewer.js +86 -3
  24. package/package.json +17 -44
  25. package/dist/renderers/archive.d.ts +0 -2
  26. package/dist/renderers/archive.js +0 -547
  27. package/dist/renderers/archiveCache.d.ts +0 -10
  28. package/dist/renderers/archiveCache.js +0 -96
  29. package/dist/renderers/archiveFallback.d.ts +0 -7
  30. package/dist/renderers/archiveFallback.js +0 -166
  31. package/dist/renderers/archiveShared.d.ts +0 -23
  32. package/dist/renderers/archiveShared.js +0 -71
  33. package/dist/renderers/audio.d.ts +0 -8
  34. package/dist/renderers/audio.js +0 -219
  35. package/dist/renderers/cad.d.ts +0 -2
  36. package/dist/renderers/cad.js +0 -446
  37. package/dist/renderers/code.d.ts +0 -11
  38. package/dist/renderers/code.js +0 -233
  39. package/dist/renderers/data.d.ts +0 -7
  40. package/dist/renderers/data.js +0 -370
  41. package/dist/renderers/drawing.d.ts +0 -10
  42. package/dist/renderers/drawing.js +0 -882
  43. package/dist/renderers/eda.d.ts +0 -2
  44. package/dist/renderers/eda.js +0 -434
  45. package/dist/renderers/edaParser.d.ts +0 -77
  46. package/dist/renderers/edaParser.js +0 -569
  47. package/dist/renderers/email.d.ts +0 -2
  48. package/dist/renderers/email.js +0 -463
  49. package/dist/renderers/epub.d.ts +0 -2
  50. package/dist/renderers/epub.js +0 -331
  51. package/dist/renderers/geo.d.ts +0 -2
  52. package/dist/renderers/geo.js +0 -284
  53. package/dist/renderers/markdown.d.ts +0 -2
  54. package/dist/renderers/markdown.js +0 -83
  55. package/dist/renderers/model.d.ts +0 -2
  56. package/dist/renderers/model.js +0 -567
  57. package/dist/renderers/ofd.d.ts +0 -2
  58. package/dist/renderers/ofd.js +0 -256
  59. package/dist/renderers/openDocument.d.ts +0 -2
  60. package/dist/renderers/openDocument.js +0 -122
  61. package/dist/renderers/pdf.d.ts +0 -3
  62. package/dist/renderers/pdf.js +0 -1001
  63. package/dist/renderers/pdfStyles.d.ts +0 -1
  64. package/dist/renderers/pdfStyles.js +0 -1
  65. package/dist/renderers/pptx.d.ts +0 -2
  66. package/dist/renderers/pptx.js +0 -217
  67. package/dist/renderers/spreadsheet/state.d.ts +0 -80
  68. package/dist/renderers/spreadsheet/state.js +0 -96
  69. package/dist/renderers/spreadsheet/view.d.ts +0 -25
  70. package/dist/renderers/spreadsheet/view.js +0 -833
  71. package/dist/renderers/spreadsheet/worker/index.d.ts +0 -2
  72. package/dist/renderers/spreadsheet/worker/index.js +0 -1
  73. package/dist/renderers/spreadsheet/worker/sheetjs/SheetJsModel.d.ts +0 -73
  74. package/dist/renderers/spreadsheet/worker/sheetjs/SheetJsModel.js +0 -623
  75. package/dist/renderers/spreadsheet/worker/sheetjs/color.d.ts +0 -2
  76. package/dist/renderers/spreadsheet/worker/sheetjs/color.js +0 -73
  77. package/dist/renderers/spreadsheet/worker/sheetjs/index.d.ts +0 -1
  78. package/dist/renderers/spreadsheet/worker/sheetjs/index.js +0 -1
  79. package/dist/renderers/spreadsheet/worker/sheetjs/parser.d.ts +0 -18
  80. package/dist/renderers/spreadsheet/worker/sheetjs/parser.js +0 -106
  81. package/dist/renderers/spreadsheet/worker/sheetjs/sheet.worker.d.ts +0 -1
  82. package/dist/renderers/spreadsheet/worker/sheetjs/sheet.worker.js +0 -11
  83. package/dist/renderers/spreadsheet/worker/type.d.ts +0 -57
  84. package/dist/renderers/spreadsheet/worker/type.js +0 -1
  85. package/dist/renderers/spreadsheet.d.ts +0 -3
  86. package/dist/renderers/spreadsheet.js +0 -929
  87. package/dist/renderers/typst.d.ts +0 -8
  88. package/dist/renderers/typst.js +0 -547
  89. package/dist/renderers/umd/parser.d.ts +0 -30
  90. package/dist/renderers/umd/parser.js +0 -408
  91. package/dist/renderers/umd.d.ts +0 -2
  92. package/dist/renderers/umd.js +0 -297
  93. package/dist/renderers/video.d.ts +0 -8
  94. package/dist/renderers/video.js +0 -108
  95. package/dist/renderers/wordDoc.d.ts +0 -5
  96. package/dist/renderers/wordDoc.js +0 -284
  97. package/dist/renderers/wordDocx.d.ts +0 -5
  98. package/dist/renderers/wordDocx.js +0 -985
  99. package/dist/renderers/wordDocx.worker.d.ts +0 -1
  100. package/dist/renderers/wordDocx.worker.js +0 -96
  101. package/vendor/ofd/dltech/jbig2/arithmetic_decoder.js +0 -183
  102. package/vendor/ofd/dltech/jbig2/ccitt.js +0 -1070
  103. package/vendor/ofd/dltech/jbig2/compatibility.js +0 -12
  104. package/vendor/ofd/dltech/jbig2/core_utils.js +0 -180
  105. package/vendor/ofd/dltech/jbig2/is_node.js +0 -27
  106. package/vendor/ofd/dltech/jbig2/jbig2.js +0 -2589
  107. package/vendor/ofd/dltech/jbig2/jbig2_stream.js +0 -81
  108. package/vendor/ofd/dltech/jbig2/primitives.js +0 -387
  109. package/vendor/ofd/dltech/jbig2/stream.js +0 -1348
  110. package/vendor/ofd/dltech/jbig2/util.js +0 -972
  111. package/vendor/ofd/dltech/ofd/ofd.d.ts +0 -11
  112. package/vendor/ofd/dltech/ofd/ofd.js +0 -100
  113. package/vendor/ofd/dltech/ofd/ofd_parser.js +0 -395
  114. package/vendor/ofd/dltech/ofd/ofd_render.js +0 -473
  115. package/vendor/ofd/dltech/ofd/ofd_util.js +0 -350
  116. 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>;
@@ -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
- }
@@ -1,2 +0,0 @@
1
- import type { FileRenderContext, FileViewerRenderedInstance } from '../contracts/types';
2
- export default function renderCad(buffer: ArrayBuffer, target: HTMLDivElement, type?: string, context?: FileRenderContext): Promise<FileViewerRenderedInstance>;