@brftech/filex-core 0.39.1 → 0.41.0
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.md +32 -25
- package/dist/ArchiveViewer-BNqukFg8.js +124 -0
- package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
- package/dist/CsvViewer-ii_-MgmG.js +141 -0
- package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
- package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
- package/dist/EpubViewer-p4B8iiWb.js +163 -0
- package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
- package/dist/IpynbViewer-D_qYRJhu.js +184 -0
- package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
- package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
- package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
- package/dist/PsdViewer-CuV-OPSd.js +122 -0
- package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
- package/dist/TiffViewer-BLeoF4Be.js +142 -0
- package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
- package/dist/Viewer3D-DHhhU76E.js +75 -0
- package/dist/Viewer3D-DHhhU76E.js.map +1 -0
- package/dist/filex-core.js +217 -20077
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-BraG7Cz4.js +26664 -0
- package/dist/index-BraG7Cz4.js.map +1 -0
- package/dist/index.d.ts +4276 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2170 -492
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +57 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +939 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +162 -30
- package/src/components/GridView.vue +322 -58
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1158 -167
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +34 -5
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +43 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +127 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +16 -4
- package/src/components/Toolbar.vue +965 -469
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +85 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useKeyboardShortcuts.ts +122 -0
- package/src/composables/useLocale.ts +365 -25
- package/src/index.ts +182 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +11 -1
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +152 -23
- package/src/lib/timezone.ts +456 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +432 -25
- package/src/locales/tr.ts +425 -25
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +543 -473
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5435 -8
- package/src/styles/variables.css +140 -60
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* - tab vs comma auto-detection (manual override via toolbar)
|
|
15
15
|
*/
|
|
16
16
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
17
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
18
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
17
19
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
18
20
|
|
|
19
21
|
const props = defineProps<{
|
|
@@ -147,17 +149,32 @@ watch(filter, () => {
|
|
|
147
149
|
function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
|
|
148
150
|
return props.t ? props.t(key, vars) : fallback;
|
|
149
151
|
}
|
|
152
|
+
|
|
153
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
154
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
155
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
156
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
157
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
158
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
159
|
+
* still picture can say "still going". */
|
|
160
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
150
161
|
</script>
|
|
151
162
|
|
|
152
163
|
<template>
|
|
153
164
|
<div class="filex-viewer-csv">
|
|
154
165
|
<div class="filex-viewer-csv__pane">
|
|
155
166
|
<div v-if="error" class="filex-viewer-fallback">
|
|
156
|
-
|
|
167
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
168
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
157
169
|
<p>{{ error }}</p>
|
|
158
170
|
</div>
|
|
159
171
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
160
|
-
|
|
172
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
173
|
+
<span
|
|
174
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
175
|
+
aria-hidden="true"
|
|
176
|
+
v-html="actionIconSvg('progress')"
|
|
177
|
+
></span>
|
|
161
178
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
162
179
|
</div>
|
|
163
180
|
<table v-else class="filex-viewer-csv__table">
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* (drawio uses the same shape: POST `{path, content}` body).
|
|
16
16
|
*/
|
|
17
17
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
18
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
18
19
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
19
20
|
|
|
20
21
|
const props = defineProps<{
|
|
@@ -180,12 +181,22 @@ onBeforeUnmount(() => {
|
|
|
180
181
|
function tt(key: string, fallback: string): string {
|
|
181
182
|
return props.t ? props.t(key) : fallback;
|
|
182
183
|
}
|
|
184
|
+
|
|
185
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
186
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
187
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
188
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
189
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
190
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
191
|
+
* still picture can say "still going". */
|
|
192
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
183
193
|
</script>
|
|
184
194
|
|
|
185
195
|
<template>
|
|
186
196
|
<div class="filex-viewer-drawio">
|
|
187
197
|
<div v-if="error" class="filex-viewer-fallback">
|
|
188
|
-
|
|
198
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
199
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
189
200
|
<p>{{ error }}</p>
|
|
190
201
|
</div>
|
|
191
202
|
<iframe
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* - font size +/- (Themes API)
|
|
14
14
|
* - graceful fallback if peer not installed
|
|
15
15
|
*/
|
|
16
|
-
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
16
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
17
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
17
18
|
|
|
18
19
|
const props = defineProps<{
|
|
19
20
|
url: string;
|
|
@@ -42,6 +43,29 @@ const loading = ref(true);
|
|
|
42
43
|
let book: any = null;
|
|
43
44
|
let rendition: any = null;
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* How long an epub.js step may hang before we call it a failure.
|
|
48
|
+
*
|
|
49
|
+
* Generous on purpose: a large publication on a slow disk legitimately takes
|
|
50
|
+
* seconds, and a false "cannot open" on a book that would have rendered is a
|
|
51
|
+
* worse bug than the one this guards against.
|
|
52
|
+
*/
|
|
53
|
+
const EPUB_STEP_TIMEOUT_MS = 15000;
|
|
54
|
+
|
|
55
|
+
/** Reject if `p` has not settled within `EPUB_STEP_TIMEOUT_MS`. */
|
|
56
|
+
function withTimeout<T>(p: Promise<T>, label: string): Promise<T> {
|
|
57
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
58
|
+
return Promise.race([
|
|
59
|
+
p,
|
|
60
|
+
new Promise<never>((_, reject) => {
|
|
61
|
+
timer = setTimeout(
|
|
62
|
+
() => reject(new Error(`EPUB ${label} timed out after ${EPUB_STEP_TIMEOUT_MS}ms`)),
|
|
63
|
+
EPUB_STEP_TIMEOUT_MS,
|
|
64
|
+
);
|
|
65
|
+
}),
|
|
66
|
+
]).finally(() => clearTimeout(timer)) as Promise<T>;
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
async function load(): Promise<void> {
|
|
46
70
|
loading.value = true;
|
|
47
71
|
ready.value = false;
|
|
@@ -81,14 +105,24 @@ async function load(): Promise<void> {
|
|
|
81
105
|
flow: 'paginated',
|
|
82
106
|
manager: 'default',
|
|
83
107
|
});
|
|
84
|
-
|
|
108
|
+
// ⚠⚠ Both of these have to be raced against a clock. epub.js resolves
|
|
109
|
+
// `display()` and `loaded.navigation` from its own internal promises, and
|
|
110
|
+
// when the archive is not a readable EPUB it simply NEVER settles —
|
|
111
|
+
// neither resolve nor reject — so the `catch` below can't fire and
|
|
112
|
+
// `finally` never runs. Measured 2026-09-13 on an 8-byte `handbook.epub`:
|
|
113
|
+
// the pane sat on "Yükleniyor…" forever with no error anywhere.
|
|
114
|
+
await withTimeout(rendition.display(), 'display');
|
|
85
115
|
rendition.themes.fontSize(fontSize.value + '%');
|
|
86
|
-
|
|
116
|
+
// `<any>` explicitly: `book` is `any`, so without it `T` infers as `{}`
|
|
117
|
+
// and `nav.toc` stops type-checking.
|
|
118
|
+
const nav = await withTimeout<any>(book.loaded.navigation, 'navigation');
|
|
87
119
|
toc.value = (nav?.toc ?? []) as TocNode[];
|
|
88
120
|
ready.value = true;
|
|
89
121
|
} catch (err) {
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
// The reader gets the localized sentence; epub.js's own wording (and the
|
|
123
|
+
// timeout marker) goes to the console for whoever is debugging.
|
|
124
|
+
console.warn('[filex] EPUB load failed:', err);
|
|
125
|
+
error.value = tt('viewer.failed_to_load', 'Failed to load file');
|
|
92
126
|
} finally {
|
|
93
127
|
loading.value = false;
|
|
94
128
|
}
|
|
@@ -165,12 +199,22 @@ watch(
|
|
|
165
199
|
function tt(key: string, fallback: string): string {
|
|
166
200
|
return props.t ? props.t(key) : fallback;
|
|
167
201
|
}
|
|
202
|
+
|
|
203
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
204
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
205
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
206
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
207
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
208
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
209
|
+
* still picture can say "still going". */
|
|
210
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
168
211
|
</script>
|
|
169
212
|
|
|
170
213
|
<template>
|
|
171
214
|
<div ref="root" class="filex-viewer-epub">
|
|
172
215
|
<div v-if="error" class="filex-viewer-fallback">
|
|
173
|
-
|
|
216
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
217
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
174
218
|
<p>{{ error }}</p>
|
|
175
219
|
</div>
|
|
176
220
|
<template v-else>
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* notebook doesn't surprise the user.
|
|
15
15
|
*/
|
|
16
16
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
17
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
18
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
17
19
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
18
20
|
import { ensureHighlight } from '../composables/useMonacoLoader';
|
|
19
21
|
|
|
@@ -192,17 +194,32 @@ const stats = computed(() => {
|
|
|
192
194
|
function tt(key: string, fallback: string): string {
|
|
193
195
|
return props.t ? props.t(key) : fallback;
|
|
194
196
|
}
|
|
197
|
+
|
|
198
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
199
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
200
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
201
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
202
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
203
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
204
|
+
* still picture can say "still going". */
|
|
205
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
195
206
|
</script>
|
|
196
207
|
|
|
197
208
|
<template>
|
|
198
209
|
<div class="filex-viewer-ipynb">
|
|
199
210
|
<div class="filex-viewer-ipynb__pane">
|
|
200
211
|
<div v-if="error" class="filex-viewer-fallback">
|
|
201
|
-
|
|
212
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
213
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
202
214
|
<p>{{ error }}</p>
|
|
203
215
|
</div>
|
|
204
216
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
205
|
-
|
|
217
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
218
|
+
<span
|
|
219
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
v-html="actionIconSvg('progress')"
|
|
222
|
+
></span>
|
|
206
223
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
207
224
|
</div>
|
|
208
225
|
<div v-else>
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* SVG node is inserted into a wrapper that supports pan + scale via
|
|
9
9
|
* pure CSS transforms (no extra panzoom dep).
|
|
10
10
|
*/
|
|
11
|
-
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
|
11
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
12
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
13
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
12
14
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
13
15
|
|
|
14
16
|
const props = defineProps<{
|
|
@@ -142,6 +144,15 @@ onBeforeUnmount(() => {
|
|
|
142
144
|
function tt(key: string, fallback: string): string {
|
|
143
145
|
return props.t ? props.t(key) : fallback;
|
|
144
146
|
}
|
|
147
|
+
|
|
148
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
149
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
150
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
151
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
152
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
153
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
154
|
+
* still picture can say "still going". */
|
|
155
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
145
156
|
</script>
|
|
146
157
|
|
|
147
158
|
<template>
|
|
@@ -158,14 +169,20 @@ function tt(key: string, fallback: string): string {
|
|
|
158
169
|
v-if="error"
|
|
159
170
|
class="filex-viewer-fallback"
|
|
160
171
|
>
|
|
161
|
-
|
|
172
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
173
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
162
174
|
<p>{{ error }}</p>
|
|
163
175
|
</div>
|
|
164
176
|
<div
|
|
165
177
|
v-else-if="loading"
|
|
166
178
|
class="filex-viewer-fallback"
|
|
167
179
|
>
|
|
168
|
-
|
|
180
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
181
|
+
<span
|
|
182
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
v-html="actionIconSvg('progress')"
|
|
185
|
+
></span>
|
|
169
186
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
170
187
|
</div>
|
|
171
188
|
<div
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* installed (the parent PreviewModal handles that branch).
|
|
21
21
|
*/
|
|
22
22
|
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue';
|
|
23
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
24
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
23
25
|
import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
|
|
24
26
|
|
|
25
27
|
const props = defineProps<{
|
|
@@ -366,6 +368,15 @@ watch(() => props.url, load);
|
|
|
366
368
|
function tt(key: string, fallback: string): string {
|
|
367
369
|
return props.t ? props.t(key) : fallback;
|
|
368
370
|
}
|
|
371
|
+
|
|
372
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
373
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
374
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
375
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
376
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
377
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
378
|
+
* still picture can say "still going". */
|
|
379
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
369
380
|
</script>
|
|
370
381
|
|
|
371
382
|
<template>
|
|
@@ -410,11 +421,14 @@ function tt(key: string, fallback: string): string {
|
|
|
410
421
|
:disabled="saveStatus === 'saving'"
|
|
411
422
|
@click="saveAnnotations"
|
|
412
423
|
>
|
|
424
|
+
<!-- ikon:emoji — the button said ⏳ / ✓, an emoji and a dingbat, in
|
|
425
|
+
a row of word buttons. Words are what this control has always
|
|
426
|
+
been; the two transient states get theirs. -->
|
|
413
427
|
{{
|
|
414
428
|
saveStatus === 'saving'
|
|
415
|
-
? '
|
|
429
|
+
? tt('viewer.saving', 'Saving…')
|
|
416
430
|
: saveStatus === 'saved'
|
|
417
|
-
? '
|
|
431
|
+
? tt('viewer.saved', 'Saved')
|
|
418
432
|
: tt('viewer.save_annotations', 'Save')
|
|
419
433
|
}}
|
|
420
434
|
</button>
|
|
@@ -429,11 +443,17 @@ function tt(key: string, fallback: string): string {
|
|
|
429
443
|
class="filex-viewer-pdf__embed"
|
|
430
444
|
/>
|
|
431
445
|
<div v-else-if="error" class="filex-viewer-fallback">
|
|
432
|
-
|
|
446
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
447
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
433
448
|
<p>{{ error }}</p>
|
|
434
449
|
</div>
|
|
435
450
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
436
|
-
|
|
451
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
452
|
+
<span
|
|
453
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
454
|
+
aria-hidden="true"
|
|
455
|
+
v-html="actionIconSvg('progress')"
|
|
456
|
+
></span>
|
|
437
457
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
438
458
|
</div>
|
|
439
459
|
<div ref="containerEl" v-show="!loading && !error" class="filex-viewer-pdf__pages" />
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* — left as TODO for V2 since `ag-psd` doesn't ship a composite
|
|
10
10
|
* pipeline of its own).
|
|
11
11
|
*/
|
|
12
|
-
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
12
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
13
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
14
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
13
15
|
import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
|
|
14
16
|
|
|
15
17
|
const props = defineProps<{
|
|
@@ -108,7 +110,14 @@ async function load(): Promise<void> {
|
|
|
108
110
|
flattenLayers(psd, flat);
|
|
109
111
|
layers.value = flat;
|
|
110
112
|
} catch (err) {
|
|
111
|
-
|
|
113
|
+
// ⚠ The person gets the localized sentence; the decoder's own words go to
|
|
114
|
+
// the console for whoever is debugging. `ag-psd` throws raw internals
|
|
115
|
+
// ("Offset is outside the bounds of the DataView" on a truncated file),
|
|
116
|
+
// and printing that verbatim put an untranslated English stack message in
|
|
117
|
+
// the middle of a Turkish UI — measured 2026-09-13 on a 6-byte
|
|
118
|
+
// `mockup.psd`.
|
|
119
|
+
console.warn('[filex] PSD decode failed:', err);
|
|
120
|
+
error.value = tt('viewer.failed_to_load', 'Failed to load file');
|
|
112
121
|
} finally {
|
|
113
122
|
loading.value = false;
|
|
114
123
|
}
|
|
@@ -133,17 +142,32 @@ watch(() => props.url, load);
|
|
|
133
142
|
function tt(key: string, fallback: string): string {
|
|
134
143
|
return props.t ? props.t(key) : fallback;
|
|
135
144
|
}
|
|
145
|
+
|
|
146
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
147
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
148
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
149
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
150
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
151
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
152
|
+
* still picture can say "still going". */
|
|
153
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
136
154
|
</script>
|
|
137
155
|
|
|
138
156
|
<template>
|
|
139
157
|
<div class="filex-viewer-psd">
|
|
140
158
|
<div class="filex-viewer-psd__pane">
|
|
141
159
|
<div v-if="error" class="filex-viewer-fallback">
|
|
142
|
-
|
|
160
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
161
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
143
162
|
<p>{{ error }}</p>
|
|
144
163
|
</div>
|
|
145
164
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
146
|
-
|
|
165
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
166
|
+
<span
|
|
167
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
v-html="actionIconSvg('progress')"
|
|
170
|
+
></span>
|
|
147
171
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
148
172
|
</div>
|
|
149
173
|
<canvas
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* conversion.
|
|
10
10
|
*/
|
|
11
11
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
12
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
13
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
12
14
|
import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
|
|
13
15
|
|
|
14
16
|
const props = defineProps<{
|
|
@@ -143,17 +145,32 @@ const pageLabel = computed(() => {
|
|
|
143
145
|
function tt(key: string, fallback: string): string {
|
|
144
146
|
return props.t ? props.t(key) : fallback;
|
|
145
147
|
}
|
|
148
|
+
|
|
149
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
150
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
151
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
152
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
153
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
154
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
155
|
+
* still picture can say "still going". */
|
|
156
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
146
157
|
</script>
|
|
147
158
|
|
|
148
159
|
<template>
|
|
149
160
|
<div class="filex-viewer-tiff">
|
|
150
161
|
<div class="filex-viewer-tiff__pane">
|
|
151
162
|
<div v-if="error" class="filex-viewer-fallback">
|
|
152
|
-
|
|
163
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
164
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
153
165
|
<p>{{ error }}</p>
|
|
154
166
|
</div>
|
|
155
167
|
<div v-else-if="loading" class="filex-viewer-fallback">
|
|
156
|
-
|
|
168
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
169
|
+
<span
|
|
170
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
v-html="actionIconSvg('progress')"
|
|
173
|
+
></span>
|
|
157
174
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
158
175
|
</div>
|
|
159
176
|
<canvas
|
package/src/viewers/Viewer3D.vue
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* (sweep-2026-05-09 bug 21.)
|
|
20
20
|
*/
|
|
21
21
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
22
|
+
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
23
|
+
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
22
24
|
|
|
23
25
|
const props = defineProps<{
|
|
24
26
|
url: string;
|
|
@@ -57,6 +59,22 @@ async function load(): Promise<void> {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* model-viewer failed to FETCH or PARSE the model.
|
|
64
|
+
*
|
|
65
|
+
* ⚠ `ready` only means the ~80 KB web component imported. The model itself is
|
|
66
|
+
* fetched and decoded by model-viewer AFTER it mounts, and when that throws
|
|
67
|
+
* (a truncated or non-glTF file) it renders an empty canvas and reports the
|
|
68
|
+
* failure only on this event. Without the listener the person got a silent
|
|
69
|
+
* black rectangle and nothing else — measured 2026-09-13 on an 8-byte
|
|
70
|
+
* `turbine.glb`, where the console had `RangeError: Invalid DataView length 12`
|
|
71
|
+
* and the pane had no text at all.
|
|
72
|
+
*/
|
|
73
|
+
function onModelError(): void {
|
|
74
|
+
ready.value = false;
|
|
75
|
+
error.value = props.t ? props.t('viewer.failed_to_load') : 'Failed to load file';
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
onMounted(load);
|
|
61
79
|
|
|
62
80
|
watch(() => props.url, () => {
|
|
@@ -64,6 +82,15 @@ watch(() => props.url, () => {
|
|
|
64
82
|
// fallback message).
|
|
65
83
|
load();
|
|
66
84
|
});
|
|
85
|
+
|
|
86
|
+
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
87
|
+
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
88
|
+
* 48px colour emoji, one per format, each from whatever emoji font the OS
|
|
89
|
+
* shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
|
|
90
|
+
* the person just clicked is wearing, so the fallback is recognisably about
|
|
91
|
+
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
92
|
+
* still picture can say "still going". */
|
|
93
|
+
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
67
94
|
</script>
|
|
68
95
|
|
|
69
96
|
<template>
|
|
@@ -77,13 +104,20 @@ watch(() => props.url, () => {
|
|
|
77
104
|
shadow-intensity="1"
|
|
78
105
|
:alt="ext + ' model'"
|
|
79
106
|
style="width: 100%; height: 100%; min-height: 480px; display: block"
|
|
107
|
+
@error="onModelError"
|
|
80
108
|
/>
|
|
81
109
|
<div v-else-if="error" class="filex-viewer-fallback">
|
|
82
|
-
|
|
110
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
111
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
83
112
|
<p>{{ error }}</p>
|
|
84
113
|
</div>
|
|
85
114
|
<div v-else class="filex-viewer-fallback">
|
|
86
|
-
|
|
115
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
116
|
+
<span
|
|
117
|
+
class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
v-html="actionIconSvg('progress')"
|
|
120
|
+
></span>
|
|
87
121
|
<p>{{ t ? t('viewer.loading') : 'Loading…' }}</p>
|
|
88
122
|
</div>
|
|
89
123
|
</div>
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { defineComponent as y, ref as d, onMounted as k, watch as b, computed as p, openBlock as s, createElementBlock as n, createElementVNode as i, toDisplayString as a, Fragment as g, renderList as z, createTextVNode as C } from "vue";
|
|
2
|
-
const S = { class: "filex-viewer-archive" }, B = {
|
|
3
|
-
key: 0,
|
|
4
|
-
class: "filex-viewer-fallback"
|
|
5
|
-
}, F = {
|
|
6
|
-
key: 1,
|
|
7
|
-
class: "filex-viewer-fallback"
|
|
8
|
-
}, $ = {
|
|
9
|
-
key: 2,
|
|
10
|
-
class: "filex-viewer-fallback"
|
|
11
|
-
}, P = {
|
|
12
|
-
key: 3,
|
|
13
|
-
class: "filex-viewer-archive__pane"
|
|
14
|
-
}, E = { class: "filex-viewer-archive__summary" }, L = { class: "filex-viewer-archive__table" }, N = { class: "filex-viewer-archive__size" }, T = ["data-dir"], H = { class: "filex-viewer-archive__icon" }, V = { class: "filex-viewer-archive__size" }, A = /* @__PURE__ */ y({
|
|
15
|
-
__name: "ArchiveViewer",
|
|
16
|
-
props: {
|
|
17
|
-
url: {},
|
|
18
|
-
filePath: {},
|
|
19
|
-
ext: {},
|
|
20
|
-
t: { type: Function },
|
|
21
|
-
authHeaders: { type: Function },
|
|
22
|
-
authCredentials: {},
|
|
23
|
-
archiveListUrl: {}
|
|
24
|
-
},
|
|
25
|
-
setup(w) {
|
|
26
|
-
const r = w, o = d([]), v = d(!0), c = d(null);
|
|
27
|
-
function l(e, t) {
|
|
28
|
-
return r.t ? r.t(e) : t;
|
|
29
|
-
}
|
|
30
|
-
function f(e) {
|
|
31
|
-
return e < 1024 ? `${e} B` : e < 1024 * 1024 ? `${(e / 1024).toFixed(1)} KB` : e < 1024 * 1024 * 1024 ? `${(e / (1024 * 1024)).toFixed(1)} MB` : `${(e / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
32
|
-
}
|
|
33
|
-
async function h() {
|
|
34
|
-
if (v.value = !0, c.value = null, o.value = [], !r.filePath) {
|
|
35
|
-
c.value = l("viewer.archive.error", "Could not read archive contents."), v.value = !1;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
const e = await fetch(r.archiveListUrl || "/api/files/archive/list", {
|
|
40
|
-
method: "POST",
|
|
41
|
-
credentials: r.authCredentials || "same-origin",
|
|
42
|
-
headers: {
|
|
43
|
-
"Content-Type": "application/json",
|
|
44
|
-
...r.authHeaders ? await r.authHeaders() : {}
|
|
45
|
-
},
|
|
46
|
-
body: JSON.stringify({ path: r.filePath })
|
|
47
|
-
});
|
|
48
|
-
if (!e.ok)
|
|
49
|
-
throw new Error(`${e.status} ${e.statusText}`);
|
|
50
|
-
const t = await e.json();
|
|
51
|
-
o.value = t.entries ?? [];
|
|
52
|
-
} catch (e) {
|
|
53
|
-
c.value = e instanceof Error ? e.message : l("viewer.archive.error", "Could not read archive contents.");
|
|
54
|
-
} finally {
|
|
55
|
-
v.value = !1;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
k(h), b(() => r.filePath, h);
|
|
59
|
-
const x = p(() => o.value.reduce((e, t) => e + (t.size || 0), 0)), _ = p(() => o.value.filter((e) => !e.is_dir).length);
|
|
60
|
-
return (e, t) => (s(), n("div", S, [
|
|
61
|
-
c.value ? (s(), n("div", B, [
|
|
62
|
-
t[0] || (t[0] = i("span", { class: "filex-viewer-fallback__icon" }, "🗜️", -1)),
|
|
63
|
-
i("p", null, a(c.value), 1)
|
|
64
|
-
])) : v.value ? (s(), n("div", F, [
|
|
65
|
-
t[1] || (t[1] = i("span", { class: "filex-viewer-fallback__icon" }, "⏳", -1)),
|
|
66
|
-
i("p", null, a(l("viewer.loading", "Loading…")), 1)
|
|
67
|
-
])) : o.value.length === 0 ? (s(), n("div", $, [
|
|
68
|
-
t[2] || (t[2] = i("span", { class: "filex-viewer-fallback__icon" }, "🗜️", -1)),
|
|
69
|
-
i("p", null, a(l("viewer.archive.empty", "Archive is empty.")), 1)
|
|
70
|
-
])) : (s(), n("div", P, [
|
|
71
|
-
i("div", E, a(_.value) + " " + a(l("viewer.archive.entries", "{n} files").replace("{n}", String(_.value))) + " · " + a(f(x.value)), 1),
|
|
72
|
-
i("table", L, [
|
|
73
|
-
i("thead", null, [
|
|
74
|
-
i("tr", null, [
|
|
75
|
-
i("th", null, a(l("viewer.name", "Name")), 1),
|
|
76
|
-
i("th", N, a(l("viewer.size", "Size")), 1)
|
|
77
|
-
])
|
|
78
|
-
]),
|
|
79
|
-
i("tbody", null, [
|
|
80
|
-
(s(!0), n(g, null, z(o.value, (u, m) => (s(), n("tr", {
|
|
81
|
-
key: m,
|
|
82
|
-
"data-dir": u.is_dir ? "1" : "0"
|
|
83
|
-
}, [
|
|
84
|
-
i("td", null, [
|
|
85
|
-
i("span", H, a(u.is_dir ? "📁" : "📄"), 1),
|
|
86
|
-
C(" " + a(u.name), 1)
|
|
87
|
-
]),
|
|
88
|
-
i("td", V, a(u.is_dir ? "" : f(u.size)), 1)
|
|
89
|
-
], 8, T))), 128))
|
|
90
|
-
])
|
|
91
|
-
])
|
|
92
|
-
]))
|
|
93
|
-
]));
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
export {
|
|
97
|
-
A as default
|
|
98
|
-
};
|
|
99
|
-
//# sourceMappingURL=ArchiveViewer-Cdfv9LFC.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ArchiveViewer-Cdfv9LFC.js","sources":["../src/viewers/ArchiveViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * ArchiveViewer — minimal zip / archive contents preview.\n *\n * Hits the configured archive-list endpoint (`archiveListUrl`, default\n * `POST /api/files/archive/list`) with the adapter-qualified path\n * and renders the member list as a flat table (name, size, mtime).\n * Read-only — extraction is exposed elsewhere (context menu / actions\n * panel). The viewer's job is just \"what's inside?\" so the user can\n * decide whether to extract or download.\n */\nimport { computed, onMounted, ref, watch } from 'vue';\n\ninterface ArchiveEntry {\n name: string;\n size: number;\n mtime?: string;\n is_dir?: boolean;\n}\n\nconst props = defineProps<{\n url: string;\n filePath?: string;\n ext: string;\n t?: (key: string) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n /** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',\n * which is the ONE thing an embed cannot assume: with `apiBase` pointing at\n * another origin the request went to the host page instead and 404'd, while\n * every other call in the package honoured the configured base. The default\n * keeps a same-origin install working unchanged. */\n archiveListUrl?: string;\n}>();\n\nconst entries = ref<ArchiveEntry[]>([]);\nconst loading = ref(true);\nconst error = ref<string | null>(null);\n\nfunction tt(key: string, fallback: string): string {\n return props.t ? props.t(key) : fallback;\n}\n\nfunction fmtSize(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;\n return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n error.value = null;\n entries.value = [];\n // The viewer is mounted with the resolved file path. Hit the API\n // endpoint with the same adapter-qualified path the preview URL\n // points at — the backend resolves storage + relative path itself.\n if (!props.filePath) {\n error.value = tt('viewer.archive.error', 'Could not read archive contents.');\n loading.value = false;\n return;\n }\n try {\n const res = await fetch(props.archiveListUrl || '/api/files/archive/list', {\n method: 'POST',\n credentials: props.authCredentials || 'same-origin',\n headers: {\n 'Content-Type': 'application/json',\n ...(props.authHeaders ? await props.authHeaders() : {}),\n },\n body: JSON.stringify({ path: props.filePath }),\n });\n if (!res.ok) {\n throw new Error(`${res.status} ${res.statusText}`);\n }\n const body = (await res.json()) as { entries?: ArchiveEntry[] };\n entries.value = body.entries ?? [];\n } catch (err) {\n error.value = err instanceof Error ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');\n } finally {\n loading.value = false;\n }\n}\n\nonMounted(load);\nwatch(() => props.filePath, load);\n\nconst totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));\nconst fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);\n</script>\n\n<template>\n <div class=\"filex-viewer-archive\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <span class=\"filex-viewer-fallback__icon\">🗜️</span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <span class=\"filex-viewer-fallback__icon\">⏳</span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <div v-else-if=\"entries.length === 0\" class=\"filex-viewer-fallback\">\n <span class=\"filex-viewer-fallback__icon\">🗜️</span>\n <p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>\n </div>\n <div v-else class=\"filex-viewer-archive__pane\">\n <div class=\"filex-viewer-archive__summary\">\n {{ fileCount }} {{ tt('viewer.archive.entries', '{n} files').replace('{n}', String(fileCount)) }}\n · {{ fmtSize(totalSize) }}\n </div>\n <table class=\"filex-viewer-archive__table\">\n <thead>\n <tr>\n <th>{{ tt('viewer.name', 'Name') }}</th>\n <th class=\"filex-viewer-archive__size\">{{ tt('viewer.size', 'Size') }}</th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(e, i) in entries\" :key=\"i\" :data-dir=\"e.is_dir ? '1' : '0'\">\n <td>\n <span class=\"filex-viewer-archive__icon\">{{ e.is_dir ? '📁' : '📄' }}</span>\n {{ e.name }}\n </td>\n <td class=\"filex-viewer-archive__size\">{{ e.is_dir ? '' : fmtSize(e.size) }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-archive {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n}\n.filex-viewer-archive__pane {\n flex: 1;\n overflow: auto;\n padding: 16px 20px;\n}\n.filex-viewer-archive__summary {\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n margin-bottom: 12px;\n font-variant-numeric: tabular-nums;\n}\n.filex-viewer-archive__table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n font-family: var(--fe-font-mono, monospace);\n}\n.filex-viewer-archive__table th,\n.filex-viewer-archive__table td {\n padding: 6px 12px;\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n text-align: left;\n}\n.filex-viewer-archive__table th {\n background: var(--fe-bg-elev, #f7f8fa);\n font-weight: 600;\n position: sticky;\n top: 0;\n}\n.filex-viewer-archive__size {\n text-align: right;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-archive__icon {\n margin-right: 6px;\n}\n.filex-viewer-archive__table tr[data-dir=\"1\"] {\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n</style>\n"],"names":["props","__props","entries","ref","loading","error","tt","key","fallback","fmtSize","n","load","res","body","err","onMounted","watch","totalSize","computed","sum","e","fileCount","_openBlock","_createElementBlock","_hoisted_1","_hoisted_2","_cache","_createElementVNode","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_toDisplayString","_hoisted_7","_hoisted_8","_Fragment","_renderList","i","_hoisted_10","_hoisted_11"],"mappings":"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"}
|