@brftech/filex-core 0.43.2 → 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.
Files changed (55) hide show
  1. package/dist/ArchiveViewer-D_tJxJhJ.js +236 -0
  2. package/dist/ArchiveViewer-D_tJxJhJ.js.map +1 -0
  3. package/dist/{CsvViewer-I0AceHh8.js → CsvViewer-B95fArq0.js} +2 -2
  4. package/dist/{CsvViewer-I0AceHh8.js.map → CsvViewer-B95fArq0.js.map} +1 -1
  5. package/dist/{DrawioViewer-B2vl10hY.js → DrawioViewer-BAaPMTge.js} +2 -2
  6. package/dist/{DrawioViewer-B2vl10hY.js.map → DrawioViewer-BAaPMTge.js.map} +1 -1
  7. package/dist/{EpubViewer-L3rxrx5i.js → EpubViewer-B-B7bRW7.js} +2 -2
  8. package/dist/{EpubViewer-L3rxrx5i.js.map → EpubViewer-B-B7bRW7.js.map} +1 -1
  9. package/dist/{IpynbViewer-Do8n0RzJ.js → IpynbViewer-COOTyOcu.js} +2 -2
  10. package/dist/{IpynbViewer-Do8n0RzJ.js.map → IpynbViewer-COOTyOcu.js.map} +1 -1
  11. package/dist/{MermaidViewer-0JqH6k0f.js → MermaidViewer-CjVCmR3s.js} +2 -2
  12. package/dist/{MermaidViewer-0JqH6k0f.js.map → MermaidViewer-CjVCmR3s.js.map} +1 -1
  13. package/dist/{PsdViewer-BnI-NDr8.js → PsdViewer-xsIYL9Gu.js} +2 -2
  14. package/dist/{PsdViewer-BnI-NDr8.js.map → PsdViewer-xsIYL9Gu.js.map} +1 -1
  15. package/dist/{TiffViewer-CrLDXI_f.js → TiffViewer-CtwnTxqL.js} +2 -2
  16. package/dist/{TiffViewer-CrLDXI_f.js.map → TiffViewer-CtwnTxqL.js.map} +1 -1
  17. package/dist/{Viewer3D-CpB1K0-0.js → Viewer3D-xYMwndEy.js} +2 -2
  18. package/dist/{Viewer3D-CpB1K0-0.js.map → Viewer3D-xYMwndEy.js.map} +1 -1
  19. package/dist/filex-core.js +372 -366
  20. package/dist/filex-core.umd.cjs +62 -62
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/{index-D9_EBsqF.js → index-BBToAjwL.js} +16563 -15727
  23. package/dist/index-BBToAjwL.js.map +1 -0
  24. package/dist/index.d.ts +185 -10
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +363 -23
  28. package/src/components/ContextMenu.vue +17 -0
  29. package/src/components/HomeView.vue +14 -4
  30. package/src/components/InspectorPanel.vue +9 -3
  31. package/src/components/ListView.vue +6 -3
  32. package/src/components/OperationsCenter.vue +6 -0
  33. package/src/components/PendingOpsTray.vue +5 -3
  34. package/src/composables/useFileApi.ts +36 -6
  35. package/src/composables/useLocale.ts +26 -0
  36. package/src/composables/useOperations.ts +7 -1
  37. package/src/composables/usePendingOps.ts +11 -7
  38. package/src/index.ts +15 -0
  39. package/src/lib/actionIcons.ts +6 -0
  40. package/src/lib/archiveFormats.ts +12 -0
  41. package/src/lib/archivePreviewCache.ts +61 -0
  42. package/src/lib/catalogCoverage.ts +137 -0
  43. package/src/locales/en.ts +67 -0
  44. package/src/locales/tr.ts +65 -0
  45. package/src/modals/ArchiveCreateModal.vue +192 -0
  46. package/src/modals/ArchiveExtractModal.vue +57 -0
  47. package/src/modals/ArchivePasswordModal.vue +62 -0
  48. package/src/modals/PreviewModal.vue +15 -1
  49. package/src/styles/base.css +47 -2
  50. package/src/types/ExplorerConfig.ts +9 -0
  51. package/src/types/FileNode.ts +19 -0
  52. package/src/viewers/ArchiveViewer.vue +145 -40
  53. package/dist/ArchiveViewer-Bn-pwgX8.js +0 -149
  54. package/dist/ArchiveViewer-Bn-pwgX8.js.map +0 -1
  55. package/dist/index-D9_EBsqF.js.map +0 -1
@@ -213,6 +213,41 @@ filex-explorer {
213
213
  font-size: 0.8125rem;
214
214
  }
215
215
 
216
+ /* The catalog does not cover all of this storage (lib/catalogCoverage): the
217
+ same quiet strip as a cut search, with room for one action. */
218
+ .fe-coverage {
219
+ flex: 0 0 auto;
220
+ display: flex;
221
+ align-items: center;
222
+ flex-wrap: wrap;
223
+ gap: 0.5rem;
224
+ padding: 0.375rem 0.75rem;
225
+ border-bottom: 1px solid var(--fe-border, rgba(0, 0, 0, 0.06));
226
+ color: var(--fe-text-muted);
227
+ font-size: 0.8125rem;
228
+ }
229
+ .fe-coverage__text {
230
+ flex: 1 1 16rem;
231
+ min-width: 0;
232
+ }
233
+ .fe-coverage__action {
234
+ flex: 0 0 auto;
235
+ padding: 0.125rem 0.625rem;
236
+ border: 1px solid var(--fe-border-strong);
237
+ border-radius: var(--fe-radius-sm);
238
+ background: var(--fe-bg-elev);
239
+ color: var(--fe-text);
240
+ font: inherit;
241
+ cursor: pointer;
242
+ }
243
+ .fe-coverage__action:hover:not(:disabled) {
244
+ background: var(--fe-bg-hover);
245
+ }
246
+ .fe-coverage__action:disabled {
247
+ opacity: 0.6;
248
+ cursor: default;
249
+ }
250
+
216
251
  /* Initial-load spinner (shown until the first listing arrives). */
217
252
  .fe__loading {
218
253
  display: flex;
@@ -1163,7 +1198,7 @@ filex-explorer {
1163
1198
  border-radius: 0;
1164
1199
  box-shadow: none;
1165
1200
  }
1166
- .fe-modal__card--chromeless .fe-modal__body {
1201
+ .fe-modal__card--chromeless > .fe-modal__body {
1167
1202
  flex: 1 1 auto;
1168
1203
  min-height: 0;
1169
1204
  padding: 0;
@@ -6568,7 +6603,7 @@ filex-explorer {
6568
6603
  border-radius: 0;
6569
6604
  box-shadow: none;
6570
6605
  }
6571
- .fe-modal__card--fullbleed .fe-modal__body {
6606
+ .fe-modal__card--fullbleed > .fe-modal__body {
6572
6607
  flex: 1 1 auto;
6573
6608
  min-height: 0;
6574
6609
  padding: 0;
@@ -11366,6 +11401,16 @@ filex-explorer {
11366
11401
  font-weight: 500;
11367
11402
  color: var(--fe-text-muted);
11368
11403
  }
11404
+ .fe-checkbox {
11405
+ display: inline-flex;
11406
+ align-items: center;
11407
+ gap: 8px;
11408
+ cursor: pointer;
11409
+ }
11410
+ .fe-checkbox input[type='checkbox'] {
11411
+ flex: 0 0 auto;
11412
+ margin: 0;
11413
+ }
11369
11414
  /* === /agent-fix2 === */
11370
11415
 
11371
11416
  /* === App plugins — a plugin's declarative screen (components/plugin/) ===
@@ -77,6 +77,7 @@ export interface EndpointMap {
77
77
  capabilities: string | null;
78
78
  archiveList: string | null;
79
79
  archiveExtract: string | null;
80
+ archiveCreate: string | null;
80
81
  archiveAdd: string | null;
81
82
  copy: string | null;
82
83
  moveAsync: string | null;
@@ -132,6 +133,7 @@ export interface ExplorerConfig {
132
133
 
133
134
  archiveList?: string;
134
135
  archiveExtract?: string;
136
+ archiveCreate?: string;
135
137
  archiveAdd?: string;
136
138
 
137
139
  /** Recursive S3-side copy with "-copy" collision suffix (async). */
@@ -707,6 +709,13 @@ export interface ExplorerConfig {
707
709
  * about the person under a label about the drive.
708
710
  */
709
711
  usedBytes?: number;
712
+ /**
713
+ * `usedBytes` counts only part of the storage: its catalog does not cover
714
+ * all of it yet (the server's `coverage` beside the figure —
715
+ * lib/catalogCoverage). The card then draws it as a lower bound. Absent:
716
+ * the explorer uses what the last listing said about the storage.
717
+ */
718
+ usedPartial?: boolean;
710
719
  }>;
711
720
 
712
721
  /**
@@ -24,6 +24,13 @@ export interface FileNode {
24
24
  extension?: string;
25
25
  /** Bytes. 0 = directory. */
26
26
  size?: number;
27
+ /**
28
+ * The folder's size leaves something out: the catalog does not cover all of
29
+ * it yet (a lazily cataloged folder, or any folder while its storage's first
30
+ * sync runs). `size` is then a lower bound, or unknown when 0. Draw it with
31
+ * useLocale `formatNodeSize`, never `formatSize` (docs/LAZY-CATALOGUE.md).
32
+ */
33
+ size_partial?: boolean;
27
34
  /** Unix ms (backend "last_modified"). */
28
35
  last_modified?: number;
29
36
  /** MIME. */
@@ -139,6 +146,8 @@ export interface NewDocType {
139
146
  requires?: 'onlyoffice' | 'drawio';
140
147
  }
141
148
 
149
+ export type ArchiveCreateFormat = 'zip' | '7z' | 'tar' | 'tar.gz' | 'tar.bz2' | 'tar.xz';
150
+
142
151
  export interface Capabilities {
143
152
  /** Document types this build can create. Absent on a server older than the
144
153
  * "New document" feature — hosts must treat that as "offer nothing". */
@@ -154,6 +163,16 @@ export interface Capabilities {
154
163
  /** Longest life a new share link may be given, in days (0 = no ceiling).
155
164
  * Read by the share dialogs so they offer only expiries the server keeps. */
156
165
  share_max_ttl_days?: number;
166
+ /** Archive creation policy. Absent on servers older than archive providers.
167
+ * `allowed_formats` is what this server can actually make (every format
168
+ * but a plain ZIP needs 7-Zip there); empty means it can make none, and
169
+ * `encryption` says whether a password can be set. */
170
+ archive?: {
171
+ enabled: boolean;
172
+ default_format: ArchiveCreateFormat | '';
173
+ allowed_formats: ArchiveCreateFormat[];
174
+ encryption?: boolean;
175
+ };
157
176
  /** Is the caller a person (`'user'` — a session OR their own API token) or an
158
177
  * integration (`'app'` — a host proxy, a bot, an MCP client)? The explorer
159
178
  * reads it to decide whether to draw the identity-bearing surfaces; see
@@ -1,19 +1,23 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * ArchiveViewer — minimal zip / archive contents preview.
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
- * and renders the member list as a flat table (name, size, mtime).
8
- * Read-only — extraction is exposed elsewhere (context menu / actions
9
- * panel). The viewer's job is just "what's inside?" so the user can
10
- * decide whether to extract or download.
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
- const props = defineProps<{
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?: string;
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
- throw new ArchiveError(await archiveErrorText(res));
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
- async function archiveErrorText(res: Response): Promise<string> {
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 (res.status === 413 || /too large|too big/i.test(code)) {
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<ArchiveEntry>[]>(() => [
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
- /** A row inside the archive, drawn the way the listing draws the same kind. */
171
- function entryTile(e: ArchiveEntry): string {
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: e.is_dir ? 'dir' : 'file',
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="error" class="filex-viewer-fallback">
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="entries"
214
- :row-key="(e: ArchiveEntry) => e.name"
215
- :row-attrs="(e: ArchiveEntry) => ({ 'data-dir': e.is_dir ? '1' : '0' })"
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
- <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
222
- <span class="filex-viewer-archive__icon" aria-hidden="true" v-html="entryTile(row)"></span>
223
- <span class="fe-list__cell-text">{{ row.name }}</span>
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-D9_EBsqF.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-Bn-pwgX8.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ArchiveViewer-Bn-pwgX8.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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"}