@brftech/filex-core 0.43.0 → 0.44.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/dist/ArchiveViewer-D_tJxJhJ.js +236 -0
- package/dist/ArchiveViewer-D_tJxJhJ.js.map +1 -0
- package/dist/{CsvViewer-ByHVK5ga.js → CsvViewer-B95fArq0.js} +2 -2
- package/dist/{CsvViewer-ByHVK5ga.js.map → CsvViewer-B95fArq0.js.map} +1 -1
- package/dist/{DrawioViewer-Bgrj8-zG.js → DrawioViewer-BAaPMTge.js} +2 -2
- package/dist/{DrawioViewer-Bgrj8-zG.js.map → DrawioViewer-BAaPMTge.js.map} +1 -1
- package/dist/{EpubViewer-DJwUKlHW.js → EpubViewer-B-B7bRW7.js} +2 -2
- package/dist/{EpubViewer-DJwUKlHW.js.map → EpubViewer-B-B7bRW7.js.map} +1 -1
- package/dist/{IpynbViewer-Bz0eJ4NS.js → IpynbViewer-COOTyOcu.js} +2 -2
- package/dist/{IpynbViewer-Bz0eJ4NS.js.map → IpynbViewer-COOTyOcu.js.map} +1 -1
- package/dist/{MermaidViewer-CEDTD_pp.js → MermaidViewer-CjVCmR3s.js} +2 -2
- package/dist/{MermaidViewer-CEDTD_pp.js.map → MermaidViewer-CjVCmR3s.js.map} +1 -1
- package/dist/{PsdViewer-CZcxUpZO.js → PsdViewer-xsIYL9Gu.js} +2 -2
- package/dist/{PsdViewer-CZcxUpZO.js.map → PsdViewer-xsIYL9Gu.js.map} +1 -1
- package/dist/{TiffViewer-BwI62CbQ.js → TiffViewer-CtwnTxqL.js} +2 -2
- package/dist/{TiffViewer-BwI62CbQ.js.map → TiffViewer-CtwnTxqL.js.map} +1 -1
- package/dist/{Viewer3D-DoeZcuMm.js → Viewer3D-xYMwndEy.js} +2 -2
- package/dist/{Viewer3D-DoeZcuMm.js.map → Viewer3D-xYMwndEy.js.map} +1 -1
- package/dist/filex-core.js +372 -366
- package/dist/filex-core.umd.cjs +62 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-DuWnGuv5.js → index-BBToAjwL.js} +16770 -15869
- package/dist/index-BBToAjwL.js.map +1 -0
- package/dist/index.d.ts +185 -10
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +363 -23
- package/src/components/ContextMenu.vue +17 -0
- package/src/components/GalleryView.vue +24 -30
- package/src/components/GridView.vue +33 -36
- package/src/components/HomeView.vue +14 -4
- package/src/components/InspectorPanel.vue +9 -3
- package/src/components/ListView.vue +21 -14
- package/src/components/OperationsCenter.vue +6 -0
- package/src/components/PendingOpsTray.vue +5 -3
- package/src/components/ThumbTile.vue +58 -0
- package/src/composables/useFileApi.ts +36 -6
- package/src/composables/useLocale.ts +26 -0
- package/src/composables/useOperations.ts +7 -1
- package/src/composables/usePendingOps.ts +11 -7
- package/src/composables/useThumbs.ts +53 -21
- package/src/index.ts +15 -0
- package/src/lib/actionIcons.ts +6 -0
- package/src/lib/archiveFormats.ts +12 -0
- package/src/lib/archivePreviewCache.ts +61 -0
- package/src/lib/catalogCoverage.ts +137 -0
- package/src/lib/nearViewport.ts +77 -0
- package/src/locales/en.ts +67 -0
- package/src/locales/tr.ts +65 -0
- package/src/modals/ArchiveCreateModal.vue +192 -0
- package/src/modals/ArchiveExtractModal.vue +57 -0
- package/src/modals/ArchivePasswordModal.vue +62 -0
- package/src/modals/PreviewModal.vue +15 -1
- package/src/styles/base.css +47 -2
- package/src/types/ExplorerConfig.ts +9 -0
- package/src/types/FileNode.ts +19 -0
- package/src/viewers/ArchiveViewer.vue +145 -40
- package/dist/ArchiveViewer-BqU761Wk.js +0 -149
- package/dist/ArchiveViewer-BqU761Wk.js.map +0 -1
- package/dist/index-DuWnGuv5.js.map +0 -1
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* ArchiveViewer —
|
|
3
|
+
* ArchiveViewer — read-only preview of an archive's contents, browsed like a
|
|
4
|
+
* folder (a breadcrumb, folders first), for every format the server reads.
|
|
4
5
|
*
|
|
5
6
|
* Hits the configured archive-list endpoint (`archiveListUrl`, default
|
|
6
|
-
* `POST /api/files/archive/list`) with the adapter-qualified path
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* `POST /api/files/archive/list`) with the adapter-qualified path. An
|
|
8
|
+
* encrypted archive answers PASSWORD_REQUIRED / BAD_PASSWORD and the shared
|
|
9
|
+
* password dialog asks; the listing it unlocks is remembered for the session
|
|
10
|
+
* (`archivePreviewCache`). Extraction is exposed elsewhere (context menu /
|
|
11
|
+
* actions panel): the viewer's job is just "what's inside?".
|
|
11
12
|
*/
|
|
12
13
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
13
14
|
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
14
15
|
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
15
16
|
import DataTable, { type DataColumn } from '../components/DataTable.vue';
|
|
16
17
|
import { useLocale } from '../composables/useLocale';
|
|
18
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
19
|
+
import type { ArchivePreviewCache } from '../lib/archivePreviewCache';
|
|
20
|
+
import ArchivePasswordModal from '../modals/ArchivePasswordModal.vue';
|
|
17
21
|
|
|
18
22
|
interface ArchiveEntry {
|
|
19
23
|
name: string;
|
|
@@ -22,14 +26,19 @@ interface ArchiveEntry {
|
|
|
22
26
|
is_dir?: boolean;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
|
|
29
|
+
interface ArchiveRow extends ArchiveEntry {
|
|
30
|
+
displayName: string;
|
|
31
|
+
fullPath: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<{
|
|
26
35
|
url: string;
|
|
27
36
|
filePath?: string;
|
|
28
37
|
ext: string;
|
|
29
|
-
t?: (key: string, vars?: Record<string, string | number>) => string;
|
|
30
38
|
/** The interface language — the size column is written the way it writes
|
|
31
39
|
* numbers ("1,96 KB"), like every other size in the product. */
|
|
32
|
-
locale?:
|
|
40
|
+
locale?: LocaleCode;
|
|
41
|
+
t?: (key: string, vars?: Record<string, string | number>) => string;
|
|
33
42
|
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
34
43
|
authCredentials?: RequestCredentials;
|
|
35
44
|
/** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',
|
|
@@ -38,11 +47,19 @@ const props = defineProps<{
|
|
|
38
47
|
* every other call in the package honoured the configured base. The default
|
|
39
48
|
* keeps a same-origin install working unchanged. */
|
|
40
49
|
archiveListUrl?: string;
|
|
41
|
-
|
|
50
|
+
archivePreviewCache?: ArchivePreviewCache;
|
|
51
|
+
}>(), {
|
|
52
|
+
locale: 'en',
|
|
53
|
+
});
|
|
42
54
|
|
|
55
|
+
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
43
56
|
const entries = ref<ArchiveEntry[]>([]);
|
|
57
|
+
const currentDir = ref('');
|
|
44
58
|
const loading = ref(true);
|
|
45
59
|
const error = ref<string | null>(null);
|
|
60
|
+
const password = ref('');
|
|
61
|
+
const passwordNeeded = ref(false);
|
|
62
|
+
const passwordError = ref('');
|
|
46
63
|
|
|
47
64
|
/** ⚠ `vars` go THROUGH `t()`, not into a `.replace()` afterwards: `t()` is what
|
|
48
65
|
* picks the singular (`viewer.archive.entries_one`) from the count, and it can
|
|
@@ -63,15 +80,22 @@ function fmtSize(n: number): string {
|
|
|
63
80
|
async function load(): Promise<void> {
|
|
64
81
|
loading.value = true;
|
|
65
82
|
error.value = null;
|
|
83
|
+
passwordError.value = '';
|
|
66
84
|
entries.value = [];
|
|
67
|
-
// The viewer is mounted with the resolved file path. Hit the API
|
|
68
|
-
// endpoint with the same adapter-qualified path the preview URL
|
|
69
|
-
// points at — the backend resolves storage + relative path itself.
|
|
70
85
|
if (!props.filePath) {
|
|
71
86
|
error.value = tt('viewer.archive.error', 'Could not read archive contents.');
|
|
72
87
|
loading.value = false;
|
|
73
88
|
return;
|
|
74
89
|
}
|
|
90
|
+
if (!password.value) {
|
|
91
|
+
const cached = props.archivePreviewCache?.recall(props.filePath);
|
|
92
|
+
if (cached) {
|
|
93
|
+
entries.value = cached;
|
|
94
|
+
passwordNeeded.value = false;
|
|
95
|
+
loading.value = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
75
99
|
try {
|
|
76
100
|
const res = await fetch(props.archiveListUrl || '/api/files/archive/list', {
|
|
77
101
|
method: 'POST',
|
|
@@ -80,7 +104,7 @@ async function load(): Promise<void> {
|
|
|
80
104
|
'Content-Type': 'application/json',
|
|
81
105
|
...(props.authHeaders ? await props.authHeaders() : {}),
|
|
82
106
|
},
|
|
83
|
-
body: JSON.stringify({ path: props.filePath }),
|
|
107
|
+
body: JSON.stringify({ path: props.filePath, password: password.value || undefined }),
|
|
84
108
|
});
|
|
85
109
|
if (!res.ok) {
|
|
86
110
|
/* ⚠ Never the raw status. This printed "503 Service Unavailable" (or a
|
|
@@ -89,10 +113,21 @@ async function load(): Promise<void> {
|
|
|
89
113
|
* the HTTP layer instead of what happened. The server's reason, when it
|
|
90
114
|
* sent one we know, becomes a sentence; anything else is the generic
|
|
91
115
|
* "could not read the archive". */
|
|
92
|
-
|
|
116
|
+
const body = await res.json().catch(() => ({})) as { code?: string; error?: string };
|
|
117
|
+
if (body.code === 'PASSWORD_REQUIRED' || body.code === 'BAD_PASSWORD') {
|
|
118
|
+
props.archivePreviewCache?.forget(props.filePath);
|
|
119
|
+
passwordNeeded.value = true;
|
|
120
|
+
passwordError.value = body.code === 'BAD_PASSWORD'
|
|
121
|
+
? tt('archive.bad_password', 'The archive password is incorrect.')
|
|
122
|
+
: '';
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
throw new ArchiveError(archiveErrorText(res.status, body.code ?? body.error ?? ''));
|
|
93
126
|
}
|
|
94
127
|
const body = (await res.json()) as { entries?: ArchiveEntry[] };
|
|
95
128
|
entries.value = body.entries ?? [];
|
|
129
|
+
if (password.value) props.archivePreviewCache?.remember(props.filePath, entries.value);
|
|
130
|
+
passwordNeeded.value = false;
|
|
96
131
|
} catch (err) {
|
|
97
132
|
error.value =
|
|
98
133
|
err instanceof ArchiveError ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');
|
|
@@ -105,25 +140,16 @@ async function load(): Promise<void> {
|
|
|
105
140
|
* network, a JSON parse) reads as the generic sentence. */
|
|
106
141
|
class ArchiveError extends Error {}
|
|
107
142
|
|
|
108
|
-
|
|
109
|
-
let code = '';
|
|
110
|
-
try {
|
|
111
|
-
const body = (await res.json()) as { error?: unknown; code?: unknown };
|
|
112
|
-
code = String(body?.code ?? body?.error ?? '');
|
|
113
|
-
} catch {
|
|
114
|
-
/* not JSON — the generic sentence below */
|
|
115
|
-
}
|
|
143
|
+
function archiveErrorText(status: number, code: string): string {
|
|
116
144
|
if (/not a zip|unsupported|not supported|format/i.test(code)) {
|
|
117
145
|
return tt('viewer.archive.unsupported', 'This kind of archive cannot be listed here. Download it to see what is inside.');
|
|
118
146
|
}
|
|
119
|
-
if (
|
|
147
|
+
if (status === 413 || /too large|too big/i.test(code)) {
|
|
120
148
|
return tt('viewer.archive.too_large', 'This archive is too large to list here. Download it to see what is inside.');
|
|
121
149
|
}
|
|
122
150
|
return tt('viewer.archive.error', 'Could not read archive contents.');
|
|
123
151
|
}
|
|
124
152
|
|
|
125
|
-
onMounted(load);
|
|
126
|
-
watch(() => props.filePath, load);
|
|
127
153
|
|
|
128
154
|
/**
|
|
129
155
|
* The entries, in THE table (DataTable — the explorer's own). ⚠ It used to be
|
|
@@ -134,7 +160,7 @@ watch(() => props.filePath, load);
|
|
|
134
160
|
* to be sorted by size and have its Name column widened exactly the way the
|
|
135
161
|
* folder it came from does.
|
|
136
162
|
*/
|
|
137
|
-
const columns = computed<DataColumn<
|
|
163
|
+
const columns = computed<DataColumn<ArchiveRow>[]>(() => [
|
|
138
164
|
{
|
|
139
165
|
id: 'name',
|
|
140
166
|
label: tt('viewer.name', 'Name'),
|
|
@@ -155,8 +181,6 @@ const columns = computed<DataColumn<ArchiveEntry>[]>(() => [
|
|
|
155
181
|
},
|
|
156
182
|
]);
|
|
157
183
|
|
|
158
|
-
const totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));
|
|
159
|
-
const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
|
|
160
184
|
|
|
161
185
|
/* === ikon:emoji — the fallback screen's mark ==========================
|
|
162
186
|
* Every viewer opened its "cannot show this" / "still loading" screen with a
|
|
@@ -165,14 +189,66 @@ const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
|
|
|
165
189
|
* the person just clicked is wearing, so the fallback is recognisably about
|
|
166
190
|
* that file — and "loading" is the stroked ring, spun by CSS, because no
|
|
167
191
|
* still picture can say "still going". */
|
|
192
|
+
function submitPassword(value: string) {
|
|
193
|
+
password.value = value;
|
|
194
|
+
void load();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onMounted(load);
|
|
198
|
+
watch(() => props.filePath, () => {
|
|
199
|
+
currentDir.value = '';
|
|
200
|
+
password.value = '';
|
|
201
|
+
passwordNeeded.value = false;
|
|
202
|
+
void load();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const totalSize = computed(() => entries.value.reduce((sum, entry) => sum + (entry.size || 0), 0));
|
|
206
|
+
const fileCount = computed(() => entries.value.filter((entry) => !entry.is_dir).length);
|
|
207
|
+
|
|
208
|
+
function cleanPath(value: string): string {
|
|
209
|
+
return value.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const visibleEntries = computed<ArchiveRow[]>(() => {
|
|
213
|
+
const rows = new Map<string, ArchiveRow>();
|
|
214
|
+
for (const entry of entries.value) {
|
|
215
|
+
const fullPath = cleanPath(entry.name);
|
|
216
|
+
if (!fullPath || (currentDir.value && !fullPath.startsWith(currentDir.value))) continue;
|
|
217
|
+
const rest = currentDir.value ? fullPath.slice(currentDir.value.length) : fullPath;
|
|
218
|
+
if (!rest) continue;
|
|
219
|
+
const slash = rest.indexOf('/');
|
|
220
|
+
if (slash >= 0) {
|
|
221
|
+
const displayName = rest.slice(0, slash);
|
|
222
|
+
const dirPath = `${currentDir.value}${displayName}`;
|
|
223
|
+
if (!rows.has(dirPath)) {
|
|
224
|
+
rows.set(dirPath, { name: dirPath, fullPath: dirPath, displayName, size: 0, is_dir: true });
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
rows.set(fullPath, { ...entry, name: fullPath, fullPath, displayName: rest });
|
|
229
|
+
}
|
|
230
|
+
return [...rows.values()].sort((a, b) => {
|
|
231
|
+
if (!!a.is_dir !== !!b.is_dir) return a.is_dir ? -1 : 1;
|
|
232
|
+
return a.displayName.localeCompare(b.displayName, undefined, { numeric: true, sensitivity: 'base' });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const breadcrumbs = computed(() => {
|
|
237
|
+
const parts = cleanPath(currentDir.value).split('/').filter(Boolean);
|
|
238
|
+
return parts.map((name, index) => ({ name, path: `${parts.slice(0, index + 1).join('/')}/` }));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
function openRow(row: ArchiveRow) {
|
|
242
|
+
if (row.is_dir) currentDir.value = `${cleanPath(row.fullPath)}/`;
|
|
243
|
+
|
|
244
|
+
}
|
|
168
245
|
const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
|
|
169
246
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const name = e.name || '';
|
|
247
|
+
function entryTile(entry: ArchiveEntry): string {
|
|
248
|
+
const name = entry.name || '';
|
|
173
249
|
const dot = name.lastIndexOf('.');
|
|
174
250
|
return fileIconTile({
|
|
175
|
-
type:
|
|
251
|
+
type: entry.is_dir ? 'dir' : 'file',
|
|
176
252
|
extension: dot > 0 ? name.slice(dot + 1) : '',
|
|
177
253
|
basename: name,
|
|
178
254
|
});
|
|
@@ -181,7 +257,12 @@ function entryTile(e: ArchiveEntry): string {
|
|
|
181
257
|
|
|
182
258
|
<template>
|
|
183
259
|
<div class="filex-viewer-archive">
|
|
184
|
-
<div v-if="
|
|
260
|
+
<div v-if="passwordNeeded" class="filex-viewer-fallback">
|
|
261
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
262
|
+
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="actionIconSvg('lock')"></span>
|
|
263
|
+
<p>{{ tt('archive.password_required', 'This archive requires a password.') }}</p>
|
|
264
|
+
</div>
|
|
265
|
+
<div v-else-if="error" class="filex-viewer-fallback">
|
|
185
266
|
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
|
|
186
267
|
<span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
|
|
187
268
|
<p>{{ error }}</p>
|
|
@@ -201,6 +282,13 @@ function entryTile(e: ArchiveEntry): string {
|
|
|
201
282
|
<p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>
|
|
202
283
|
</div>
|
|
203
284
|
<div v-else class="filex-viewer-archive__pane">
|
|
285
|
+
<nav class="filex-viewer-archive__crumbs" :aria-label="tt('viewer.archive.location', 'Archive location')">
|
|
286
|
+
<button type="button" @click="currentDir = ''">{{ tt('viewer.archive.root', 'Archive') }}</button>
|
|
287
|
+
<template v-for="crumb in breadcrumbs" :key="crumb.path">
|
|
288
|
+
<span aria-hidden="true">›</span>
|
|
289
|
+
<button type="button" @click="currentDir = crumb.path">{{ crumb.name }}</button>
|
|
290
|
+
</template>
|
|
291
|
+
</nav>
|
|
204
292
|
<div class="filex-viewer-archive__summary">
|
|
205
293
|
<!-- ⚠ The count is IN the message ("{n} files"). A `{{ fileCount }}`
|
|
206
294
|
printed in front of it read "3 3 files". -->
|
|
@@ -210,20 +298,31 @@ function entryTile(e: ArchiveEntry): string {
|
|
|
210
298
|
<DataTable
|
|
211
299
|
table-id="viewer.archive"
|
|
212
300
|
:columns="columns"
|
|
213
|
-
:rows="
|
|
214
|
-
:row-key="(e:
|
|
215
|
-
:row-attrs="(e:
|
|
301
|
+
:rows="visibleEntries"
|
|
302
|
+
:row-key="(e: ArchiveRow) => e.fullPath"
|
|
303
|
+
:row-attrs="(e: ArchiveRow) => ({ 'data-dir': e.is_dir ? '1' : '0', onDblclick: () => openRow(e) })"
|
|
216
304
|
>
|
|
217
305
|
<template #cell-name="{ row }">
|
|
218
306
|
<!-- ikon:emoji — the same tile the listing draws for the same kind
|
|
219
307
|
of file, so an archive's contents and the folder it came from
|
|
220
308
|
read as one product. -->
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
309
|
+
<button type="button" class="filex-viewer-archive__entry" :disabled="!row.is_dir" @click="openRow(row)">
|
|
310
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
311
|
+
<span class="filex-viewer-archive__icon" aria-hidden="true" v-html="entryTile(row)"></span>
|
|
312
|
+
<span class="fe-list__cell-text">{{ row.displayName }}</span>
|
|
313
|
+
</button>
|
|
224
314
|
</template>
|
|
225
315
|
</DataTable>
|
|
226
316
|
</div>
|
|
317
|
+
<ArchivePasswordModal
|
|
318
|
+
:open="passwordNeeded"
|
|
319
|
+
:locale="props.locale"
|
|
320
|
+
:archive-name="(props.filePath || '').split('/').pop() || ''"
|
|
321
|
+
:busy="loading"
|
|
322
|
+
:error="passwordError"
|
|
323
|
+
@close="emit('close')"
|
|
324
|
+
@submit="submitPassword"
|
|
325
|
+
/>
|
|
227
326
|
</div>
|
|
228
327
|
</template>
|
|
229
328
|
|
|
@@ -281,4 +380,10 @@ function entryTile(e: ArchiveEntry): string {
|
|
|
281
380
|
display: block;
|
|
282
381
|
margin-bottom: 12px;
|
|
283
382
|
}
|
|
383
|
+
.filex-viewer-archive__crumbs { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; color: var(--fe-text-muted, #5a6475); }
|
|
384
|
+
.filex-viewer-archive__crumbs button { border: 0; background: transparent; color: inherit; cursor: pointer; padding: 2px 4px; border-radius: 4px; }
|
|
385
|
+
.filex-viewer-archive__crumbs button:hover { background: var(--fe-bg-elev, #f7f8fa); color: var(--fe-text, #1a1e27); }
|
|
386
|
+
.filex-viewer-archive__entry { display: inline-flex; align-items: center; max-width: 100%; border: 0; background: transparent; color: inherit; font: inherit; padding: 0; text-align: start; }
|
|
387
|
+
.filex-viewer-archive__entry:not(:disabled) { cursor: pointer; }
|
|
388
|
+
.filex-viewer-archive__entry:disabled { opacity: 1; }
|
|
284
389
|
</style>
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { defineComponent as z, ref as h, onMounted as H, watch as C, computed as d, openBlock as c, createElementBlock as u, createElementVNode as n, toDisplayString as l, unref as M, createVNode as S, withCtx as E } from "vue";
|
|
2
|
-
import { u as P, f as x, a as N, _ as V } from "./index-DuWnGuv5.js";
|
|
3
|
-
const j = { class: "filex-viewer-archive" }, A = {
|
|
4
|
-
key: 0,
|
|
5
|
-
class: "filex-viewer-fallback"
|
|
6
|
-
}, D = ["innerHTML"], I = {
|
|
7
|
-
key: 1,
|
|
8
|
-
class: "filex-viewer-fallback"
|
|
9
|
-
}, O = ["innerHTML"], B = {
|
|
10
|
-
key: 2,
|
|
11
|
-
class: "filex-viewer-fallback"
|
|
12
|
-
}, F = ["innerHTML"], U = {
|
|
13
|
-
key: 3,
|
|
14
|
-
class: "filex-viewer-archive__pane"
|
|
15
|
-
}, $ = { class: "filex-viewer-archive__summary" }, J = ["innerHTML"], q = { class: "fe-list__cell-text" }, Q = /* @__PURE__ */ z({
|
|
16
|
-
__name: "ArchiveViewer",
|
|
17
|
-
props: {
|
|
18
|
-
url: {},
|
|
19
|
-
filePath: {},
|
|
20
|
-
ext: {},
|
|
21
|
-
t: { type: Function },
|
|
22
|
-
locale: {},
|
|
23
|
-
authHeaders: { type: Function },
|
|
24
|
-
authCredentials: {},
|
|
25
|
-
archiveListUrl: {}
|
|
26
|
-
},
|
|
27
|
-
setup(y) {
|
|
28
|
-
const a = y, s = h([]), v = h(!0), o = h(null);
|
|
29
|
-
function r(e, t, i) {
|
|
30
|
-
return a.t ? a.t(e, i) : t;
|
|
31
|
-
}
|
|
32
|
-
const { formatSize: g } = P(() => a.locale ?? "en");
|
|
33
|
-
function f(e) {
|
|
34
|
-
return g(e);
|
|
35
|
-
}
|
|
36
|
-
async function _() {
|
|
37
|
-
if (v.value = !0, o.value = null, s.value = [], !a.filePath) {
|
|
38
|
-
o.value = r("viewer.archive.error", "Could not read archive contents."), v.value = !1;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
const e = await fetch(a.archiveListUrl || "/api/files/archive/list", {
|
|
43
|
-
method: "POST",
|
|
44
|
-
credentials: a.authCredentials || "same-origin",
|
|
45
|
-
headers: {
|
|
46
|
-
"Content-Type": "application/json",
|
|
47
|
-
...a.authHeaders ? await a.authHeaders() : {}
|
|
48
|
-
},
|
|
49
|
-
body: JSON.stringify({ path: a.filePath })
|
|
50
|
-
});
|
|
51
|
-
if (!e.ok)
|
|
52
|
-
throw new w(await k(e));
|
|
53
|
-
const t = await e.json();
|
|
54
|
-
s.value = t.entries ?? [];
|
|
55
|
-
} catch (e) {
|
|
56
|
-
o.value = e instanceof w ? e.message : r("viewer.archive.error", "Could not read archive contents.");
|
|
57
|
-
} finally {
|
|
58
|
-
v.value = !1;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
class w extends Error {
|
|
62
|
-
}
|
|
63
|
-
async function k(e) {
|
|
64
|
-
let t = "";
|
|
65
|
-
try {
|
|
66
|
-
const i = await e.json();
|
|
67
|
-
t = String((i == null ? void 0 : i.code) ?? (i == null ? void 0 : i.error) ?? "");
|
|
68
|
-
} catch {
|
|
69
|
-
}
|
|
70
|
-
return /not a zip|unsupported|not supported|format/i.test(t) ? r("viewer.archive.unsupported", "This kind of archive cannot be listed here. Download it to see what is inside.") : e.status === 413 || /too large|too big/i.test(t) ? r("viewer.archive.too_large", "This archive is too large to list here. Download it to see what is inside.") : r("viewer.archive.error", "Could not read archive contents.");
|
|
71
|
-
}
|
|
72
|
-
H(_), C(() => a.filePath, _);
|
|
73
|
-
const T = d(() => [
|
|
74
|
-
{
|
|
75
|
-
id: "name",
|
|
76
|
-
label: r("viewer.name", "Name"),
|
|
77
|
-
sortable: !0,
|
|
78
|
-
width: 360,
|
|
79
|
-
class: "filex-viewer-archive__name",
|
|
80
|
-
title: (e) => e.name
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
id: "size",
|
|
84
|
-
label: r("viewer.size", "Size"),
|
|
85
|
-
sortable: !0,
|
|
86
|
-
width: 110,
|
|
87
|
-
align: "right",
|
|
88
|
-
class: "filex-viewer-archive__size",
|
|
89
|
-
format: (e) => e.is_dir ? "" : f(e.size),
|
|
90
|
-
sortValue: (e) => e.is_dir ? -1 : e.size
|
|
91
|
-
}
|
|
92
|
-
]), b = d(() => s.value.reduce((e, t) => e + (t.size || 0), 0)), p = d(() => s.value.filter((e) => !e.is_dir).length), m = d(() => x({ type: "file", extension: a.ext }));
|
|
93
|
-
function L(e) {
|
|
94
|
-
const t = e.name || "", i = t.lastIndexOf(".");
|
|
95
|
-
return x({
|
|
96
|
-
type: e.is_dir ? "dir" : "file",
|
|
97
|
-
extension: i > 0 ? t.slice(i + 1) : "",
|
|
98
|
-
basename: t
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return (e, t) => (c(), u("div", j, [
|
|
102
|
-
o.value ? (c(), u("div", A, [
|
|
103
|
-
n("span", {
|
|
104
|
-
class: "filex-viewer-fallback__icon",
|
|
105
|
-
"aria-hidden": "true",
|
|
106
|
-
innerHTML: m.value
|
|
107
|
-
}, null, 8, D),
|
|
108
|
-
n("p", null, l(o.value), 1)
|
|
109
|
-
])) : v.value ? (c(), u("div", I, [
|
|
110
|
-
n("span", {
|
|
111
|
-
class: "filex-viewer-fallback__icon filex-viewer-fallback__icon--spin",
|
|
112
|
-
"aria-hidden": "true",
|
|
113
|
-
innerHTML: M(N)("progress")
|
|
114
|
-
}, null, 8, O),
|
|
115
|
-
n("p", null, l(r("viewer.loading", "Loading…")), 1)
|
|
116
|
-
])) : s.value.length === 0 ? (c(), u("div", B, [
|
|
117
|
-
n("span", {
|
|
118
|
-
class: "filex-viewer-fallback__icon",
|
|
119
|
-
"aria-hidden": "true",
|
|
120
|
-
innerHTML: m.value
|
|
121
|
-
}, null, 8, F),
|
|
122
|
-
n("p", null, l(r("viewer.archive.empty", "Archive is empty.")), 1)
|
|
123
|
-
])) : (c(), u("div", U, [
|
|
124
|
-
n("div", $, l(r("viewer.archive.entries", `${p.value} files`, { n: p.value })) + " · " + l(f(b.value)), 1),
|
|
125
|
-
S(V, {
|
|
126
|
-
"table-id": "viewer.archive",
|
|
127
|
-
columns: T.value,
|
|
128
|
-
rows: s.value,
|
|
129
|
-
"row-key": (i) => i.name,
|
|
130
|
-
"row-attrs": (i) => ({ "data-dir": i.is_dir ? "1" : "0" })
|
|
131
|
-
}, {
|
|
132
|
-
"cell-name": E(({ row: i }) => [
|
|
133
|
-
n("span", {
|
|
134
|
-
class: "filex-viewer-archive__icon",
|
|
135
|
-
"aria-hidden": "true",
|
|
136
|
-
innerHTML: L(i)
|
|
137
|
-
}, null, 8, J),
|
|
138
|
-
n("span", q, l(i.name), 1)
|
|
139
|
-
]),
|
|
140
|
-
_: 1
|
|
141
|
-
}, 8, ["columns", "rows", "row-key", "row-attrs"])
|
|
142
|
-
]))
|
|
143
|
-
]));
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
export {
|
|
147
|
-
Q as default
|
|
148
|
-
};
|
|
149
|
-
//# sourceMappingURL=ArchiveViewer-BqU761Wk.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ArchiveViewer-BqU761Wk.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';\nimport { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport DataTable, { type DataColumn } from '../components/DataTable.vue';\nimport { useLocale } from '../composables/useLocale';\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, vars?: Record<string, string | number>) => string;\n /** The interface language — the size column is written the way it writes\n * numbers (\"1,96 KB\"), like every other size in the product. */\n locale?: 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\n/** ⚠ `vars` go THROUGH `t()`, not into a `.replace()` afterwards: `t()` is what\n * picks the singular (`viewer.archive.entries_one`) from the count, and it can\n * only do that if it is told the count. Same shape CsvViewer's helper has. */\nfunction tt(key: string, fallback: string, vars?: Record<string, string | number>): string {\n return props.t ? props.t(key, vars) : fallback;\n}\n\n/* ⚠ The product's one byte formatter (useLocale.formatSize). This file had\n its own — base 1024, a dot, English units — so a zip's member read\n \"1.9 KB\" beside the explorer's \"1,96 KB\" for the same bytes (QA,\n 2026-09-21). */\nconst { formatSize } = useLocale(() => props.locale ?? 'en');\nfunction fmtSize(n: number): string {\n return formatSize(n);\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 /* ⚠ Never the raw status. This printed \"503 Service Unavailable\" (or a\n * JSON body) straight into the preview — the same failure the owner\n * reported for ONLYOFFICE (\"Config fetch 503: {…}\"): a person is shown\n * the HTTP layer instead of what happened. The server's reason, when it\n * sent one we know, becomes a sentence; anything else is the generic\n * \"could not read the archive\". */\n throw new ArchiveError(await archiveErrorText(res));\n }\n const body = (await res.json()) as { entries?: ArchiveEntry[] };\n entries.value = body.entries ?? [];\n } catch (err) {\n error.value =\n err instanceof ArchiveError ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');\n } finally {\n loading.value = false;\n }\n}\n\n/** A failure already worded for a person. Anything else that throws (the\n * network, a JSON parse) reads as the generic sentence. */\nclass ArchiveError extends Error {}\n\nasync function archiveErrorText(res: Response): Promise<string> {\n let code = '';\n try {\n const body = (await res.json()) as { error?: unknown; code?: unknown };\n code = String(body?.code ?? body?.error ?? '');\n } catch {\n /* not JSON — the generic sentence below */\n }\n if (/not a zip|unsupported|not supported|format/i.test(code)) {\n return tt('viewer.archive.unsupported', 'This kind of archive cannot be listed here. Download it to see what is inside.');\n }\n if (res.status === 413 || /too large|too big/i.test(code)) {\n return tt('viewer.archive.too_large', 'This archive is too large to list here. Download it to see what is inside.');\n }\n return tt('viewer.archive.error', 'Could not read archive contents.');\n}\n\nonMounted(load);\nwatch(() => props.filePath, load);\n\n/**\n * The entries, in THE table (DataTable — the explorer's own). ⚠ It used to be\n * the one table in the package allowed to be its own, with the reasoning that\n * a file's contents answer to the viewer's layout. The owner's rule since\n * 2026-09-21 has no such exception — \"bir yere tablo gerekiyorsa bu tabloyu\n * koymak zorundayız\" — and an archive's listing is a file listing: it wants\n * to be sorted by size and have its Name column widened exactly the way the\n * folder it came from does.\n */\nconst columns = computed<DataColumn<ArchiveEntry>[]>(() => [\n {\n id: 'name',\n label: tt('viewer.name', 'Name'),\n sortable: true,\n width: 360,\n class: 'filex-viewer-archive__name',\n title: (e) => e.name,\n },\n {\n id: 'size',\n label: tt('viewer.size', 'Size'),\n sortable: true,\n width: 110,\n align: 'right',\n class: 'filex-viewer-archive__size',\n format: (e) => (e.is_dir ? '' : fmtSize(e.size)),\n sortValue: (e) => (e.is_dir ? -1 : e.size),\n },\n]);\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\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n\n/** A row inside the archive, drawn the way the listing draws the same kind. */\nfunction entryTile(e: ArchiveEntry): string {\n const name = e.name || '';\n const dot = name.lastIndexOf('.');\n return fileIconTile({\n type: e.is_dir ? 'dir' : 'file',\n extension: dot > 0 ? name.slice(dot + 1) : '',\n basename: name,\n });\n}\n</script>\n\n<template>\n <div class=\"filex-viewer-archive\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span\n class=\"filex-viewer-fallback__icon filex-viewer-fallback__icon--spin\"\n aria-hidden=\"true\"\n v-html=\"actionIconSvg('progress')\"\n ></span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <div v-else-if=\"entries.length === 0\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></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 <!-- ⚠ The count is IN the message (\"{n} files\"). A `{{ fileCount }}`\n printed in front of it read \"3 3 files\". -->\n {{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}\n · {{ fmtSize(totalSize) }}\n </div>\n <DataTable\n table-id=\"viewer.archive\"\n :columns=\"columns\"\n :rows=\"entries\"\n :row-key=\"(e: ArchiveEntry) => e.name\"\n :row-attrs=\"(e: ArchiveEntry) => ({ 'data-dir': e.is_dir ? '1' : '0' })\"\n >\n <template #cell-name=\"{ row }\">\n <!-- ikon:emoji — the same tile the listing draws for the same kind\n of file, so an archive's contents and the folder it came from\n read as one product. -->\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->\n <span class=\"filex-viewer-archive__icon\" aria-hidden=\"true\" v-html=\"entryTile(row)\"></span>\n <span class=\"fe-list__cell-text\">{{ row.name }}</span>\n </template>\n </DataTable>\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/* The table itself is DataTable's (styles/base.css); only the viewer's own\n touches stay here — tabular figures and muted sizes, a folder row read as\n quieter than a file. */\n.filex-viewer-archive__size {\n text-align: end;\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-inline-end: 6px;\n}\n.filex-viewer-archive .fe-list__row[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","vars","formatSize","useLocale","fmtSize","n","load","res","ArchiveError","archiveErrorText","body","err","code","onMounted","watch","columns","computed","totalSize","sum","e","fileCount","typeTile","fileIconTile","entryTile","name","dot","_openBlock","_createElementBlock","_hoisted_1","_hoisted_2","_createElementVNode","_hoisted_4","_unref","actionIconSvg","_hoisted_6","_hoisted_8","_hoisted_9","_toDisplayString","_createVNode","DataTable","_withCtx","row","_hoisted_11"],"mappings":"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"}
|