@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
package/src/locales/en.ts CHANGED
@@ -12,6 +12,8 @@ export const en: Record<string, string> = {
12
12
  'ctx.open': 'Open',
13
13
  'ctx.preview': 'Preview',
14
14
  'ctx.download': 'Download',
15
+ 'ctx.archive_create': 'Create archive…',
16
+ 'ctx.archive_extract': 'Extract archive…',
15
17
  'ctx.convert': 'Convert',
16
18
  'ctx.rename': 'Rename',
17
19
  'ctx.delete': 'Delete',
@@ -57,6 +59,51 @@ export const en: Record<string, string> = {
57
59
  'modal.newfolder.cancel': 'Cancel',
58
60
 
59
61
  'modal.share.title': 'Share link',
62
+ 'archive.create_title': 'Create archive',
63
+ 'archive.extract_title': 'Extract archive',
64
+ 'archive.create_count': '{count} selected items',
65
+ 'archive.create_count_one': '{count} selected item',
66
+ 'archive.filename': 'Archive name',
67
+ 'archive.format': 'Format',
68
+ 'archive.password_optional': 'Password (optional)',
69
+ 'archive.password_confirm': 'Confirm password',
70
+ 'archive.password_mismatch': 'Passwords do not match.',
71
+ 'archive.encrypt_names': 'Encrypt file names',
72
+ 'archive.compression': 'Compression',
73
+ 'archive.solid': 'Create solid archive',
74
+ 'archive.dictionary_size': 'Dictionary size',
75
+ 'archive.dictionary_size_value': '{n} MiB',
76
+ 'archive.dictionary_hint': 'Larger dictionaries can improve compression but use more memory while creating and extracting.',
77
+ 'archive.destination_folder': 'Extract to folder',
78
+ 'archive.create': 'Create',
79
+ 'archive.creating': 'Creating…',
80
+ 'archive.extract': 'Extract',
81
+ 'archive.extract_here': 'Extract here',
82
+ 'archive.extract_to_folder': 'Extract to folder',
83
+ 'archive.extracting': 'Extracting…',
84
+ 'archive.cancel': 'Cancel',
85
+ 'archive.invalid_name': 'Enter a file name without slashes.',
86
+ 'archive.invalid_extension': 'Archive name must use one of the available format extensions.',
87
+ 'archive.invalid_folder': 'Enter a folder name without slashes.',
88
+ 'archive.password_required': 'This archive requires a password.',
89
+ 'archive.password_title': 'Archive password',
90
+ 'archive.continue': 'Continue',
91
+ 'archive.opening': 'Opening…',
92
+ 'archive.bad_password': 'The archive password is incorrect.',
93
+ 'archive.target_exists': 'An item with that name already exists. Choose another name or remove it first.',
94
+ 'archive.password_charset': 'A ZIP password can use only ASCII characters: unaccented letters, digits and ordinary punctuation. Choose 7z to use any characters.',
95
+ 'archive.error_unsupported': 'This archive cannot be extracted: this server cannot read its format, or it holds something filex does not extract (a link, a special file, or a file whose size it does not state).',
96
+ 'archive.error_limit': 'This archive is larger than the server allows to extract.',
97
+ 'archive.error_unavailable': 'This server cannot open this kind of archive right now.',
98
+ 'archive.created': 'Archive created',
99
+ 'archive.queued': 'Archive creation is running in the background',
100
+ 'archive.extracted': 'Extracted {count} files',
101
+ 'archive.extracted_one': 'Extracted {count} file',
102
+ 'archive.extraction_queued': 'Archive extraction is running in the background',
103
+ 'archive.extraction_completed': 'Extraction completed',
104
+ 'archive.extraction_cancelled': 'Extraction cancelled after {count} files',
105
+ 'archive.extraction_cancelled_one': 'Extraction cancelled after {count} file',
106
+
60
107
  'modal.share.pin': 'PIN protected',
61
108
  'modal.share.expires': 'Expires at',
62
109
  'modal.share.max_downloads': 'Download limit',
@@ -169,6 +216,9 @@ export const en: Record<string, string> = {
169
216
  'viewer.archive.error': 'Could not read archive contents.',
170
217
  'viewer.archive.unsupported': 'This kind of archive cannot be listed here. Download it to see what is inside.',
171
218
  'viewer.archive.too_large': 'This archive is too large to list here. Download it to see what is inside.',
219
+ 'viewer.archive.open': 'Open archive',
220
+ 'viewer.archive.root': 'Archive',
221
+ 'viewer.archive.location': 'Archive location',
172
222
  'viewer.name': 'Name',
173
223
  'viewer.size': 'Size',
174
224
 
@@ -314,6 +364,19 @@ export const en: Record<string, string> = {
314
364
  'search.in_content': 'In content',
315
365
  'search.truncated': 'More results than shown — narrow your search',
316
366
 
367
+ /* Catalog coverage (docs/LAZY-CATALOGUE.md, lib/catalogCoverage). One
368
+ sentence per reason; the banner sits over a listing and over search
369
+ results, and says what is left out and why. */
370
+ 'coverage.first_scan': 'This storage’s first sync has not finished. Until it does, search, folder sizes and usage leave part of it out.',
371
+ 'coverage.lazy_filling': 'filex is still cataloging this storage ({pct}% of the folders found so far). Until it finishes, search, folder sizes and usage leave part of it out.',
372
+ 'coverage.lazy_on_open': 'Only the folders people open on this storage are cataloged. Search, folder sizes and usage cover those folders only.',
373
+ 'coverage.search_some': 'Not everything on these storages is cataloged yet: {names}. This search may miss files there.',
374
+ 'coverage.catalog_all': 'Catalog everything',
375
+ 'coverage.catalog_started': 'Sync started — the whole storage is being cataloged.',
376
+ 'size.at_least': '≥ {size}',
377
+ 'size.partial_hint': 'Part of this folder is not cataloged yet, so its size is at least this.',
378
+ 'size.unknown_hint': 'This folder is not cataloged yet, so its size is not known.',
379
+
317
380
  /* === bag:b4 === */
318
381
  'toolbar.more': 'More actions',
319
382
  'toolbar.search.close': 'Close search',
@@ -461,6 +524,7 @@ export const en: Record<string, string> = {
461
524
  'opc.retry': 'Retry',
462
525
  'opc.dismiss': 'Dismiss',
463
526
  'opc.queued': 'Queued',
527
+ 'opc.cancelling': 'Cancel requested…',
464
528
  'opc.percent': '{n}%',
465
529
  'opc.status.running': 'In progress',
466
530
  'opc.status.done': 'Done',
@@ -677,6 +741,8 @@ export const en: Record<string, string> = {
677
741
  'plugin.pdf.date_order': 'Date order',
678
742
  'plugin.pdf.date_separator': 'Separator',
679
743
  'plugin.pdf.date_example': 'Example',
744
+ 'opc.kind.archive-create': 'Creating archive',
745
+ 'opc.kind.archive-extract': 'Extracting archive',
680
746
  /* === wiring:c4 — onboarding tour + accessibility + error card === */
681
747
  'tour.aria': 'Onboarding tour',
682
748
  'tour.restart': 'Restart the tour',
@@ -1392,6 +1458,7 @@ export const en: Record<string, string> = {
1392
1458
  'drive.search.hint_title': 'Search everywhere and run commands ({combo})',
1393
1459
  'drive.storage.used': '{used} of {total} used',
1394
1460
  'drive.storage.used_unlimited': '{used} used',
1461
+ 'drive.storage.used_partial': 'at least {used} used',
1395
1462
  'drive.storage.label': 'Storage',
1396
1463
  /* === gorunum:v3-shell — the Home view (the overview, inside the shell) ==
1397
1464
  * The section headings are `sidenav.storages` / `sidenav.recent` /
package/src/locales/tr.ts CHANGED
@@ -12,6 +12,8 @@ export const tr: Record<string, string> = {
12
12
  'ctx.open': 'Aç',
13
13
  'ctx.preview': 'Önizle',
14
14
  'ctx.download': 'İndir',
15
+ 'ctx.archive_create': 'Arşiv oluştur…',
16
+ 'ctx.archive_extract': 'Arşivi çıkar…',
15
17
  'ctx.convert': 'Dönüştür',
16
18
  'ctx.rename': 'Yeniden adlandır',
17
19
  'ctx.delete': 'Sil',
@@ -57,6 +59,51 @@ export const tr: Record<string, string> = {
57
59
  'modal.newfolder.cancel': 'Vazgeç',
58
60
 
59
61
  'modal.share.title': 'Paylaşım bağlantısı',
62
+ 'archive.create_title': 'Arşiv oluştur',
63
+ 'archive.extract_title': 'Arşivi çıkar',
64
+ 'archive.create_count': '{count} öğe seçildi',
65
+ 'archive.create_count_one': '{count} öğe seçildi',
66
+ 'archive.filename': 'Arşiv adı',
67
+ 'archive.format': 'Biçim',
68
+ 'archive.password_optional': 'Parola (isteğe bağlı)',
69
+ 'archive.password_confirm': 'Parolayı doğrula',
70
+ 'archive.password_mismatch': 'Parolalar birbiriyle eşleşmiyor.',
71
+ 'archive.encrypt_names': 'Dosya adlarını şifrele',
72
+ 'archive.compression': 'Sıkıştırma',
73
+ 'archive.solid': 'Katı arşiv oluştur',
74
+ 'archive.dictionary_size': 'Sözlük boyutu',
75
+ 'archive.dictionary_size_value': '{n} MiB',
76
+ 'archive.dictionary_hint': 'Daha büyük sözlükler sıkıştırmayı iyileştirebilir ancak oluşturma ve çıkarma sırasında daha fazla bellek kullanır.',
77
+ 'archive.destination_folder': 'Çıkarılacak klasör',
78
+ 'archive.create': 'Oluştur',
79
+ 'archive.creating': 'Oluşturuluyor…',
80
+ 'archive.extract': 'Çıkar',
81
+ 'archive.extract_here': 'Buraya çıkar',
82
+ 'archive.extract_to_folder': 'Klasöre çıkar',
83
+ 'archive.extracting': 'Çıkarılıyor…',
84
+ 'archive.cancel': 'Vazgeç',
85
+ 'archive.invalid_name': 'Eğik çizgi içermeyen bir dosya adı girin.',
86
+ 'archive.invalid_extension': 'Arşiv adı kullanılabilir biçim uzantılarından biriyle bitmelidir.',
87
+ 'archive.invalid_folder': 'Eğik çizgi içermeyen bir klasör adı girin.',
88
+ 'archive.password_required': 'Bu arşiv için parola gerekiyor.',
89
+ 'archive.password_title': 'Arşiv parolası',
90
+ 'archive.continue': 'Devam et',
91
+ 'archive.opening': 'Açılıyor…',
92
+ 'archive.bad_password': 'Arşiv parolası yanlış.',
93
+ 'archive.target_exists': 'Bu ada sahip bir öğe zaten var. Başka bir ad seçin veya önce mevcut öğeyi kaldırın.',
94
+ 'archive.password_charset': 'ZIP parolasında yalnızca ASCII karakterler kullanılabilir; ç, ğ, ı, İ, ö, ş, ü gibi harfler kullanılamaz. Her karakteri kullanmak için 7z biçimini seçin.',
95
+ 'archive.error_unsupported': 'Bu arşiv çıkarılamıyor: bu sunucu biçimini okuyamıyor ya da içinde filex’in çıkarmadığı bir şey var (bağlantı, özel dosya ya da boyutunu belirtmeyen bir dosya).',
96
+ 'archive.error_limit': 'Bu arşiv, sunucunun çıkarmaya izin verdiği boyutu aşıyor.',
97
+ 'archive.error_unavailable': 'Bu sunucu şu anda bu tür bir arşivi açamıyor.',
98
+ 'archive.created': 'Arşiv oluşturuldu',
99
+ 'archive.queued': 'Arşiv oluşturma arka planda çalışıyor',
100
+ 'archive.extracted': '{count} dosya çıkarıldı',
101
+ 'archive.extracted_one': '{count} dosya çıkarıldı',
102
+ 'archive.extraction_queued': 'Arşiv çıkarma arka planda çalışıyor',
103
+ 'archive.extraction_completed': 'Çıkarma tamamlandı',
104
+ 'archive.extraction_cancelled': 'Çıkarma {count} dosyadan sonra iptal edildi',
105
+ 'archive.extraction_cancelled_one': 'Çıkarma {count} dosyadan sonra iptal edildi',
106
+
60
107
  'modal.share.pin': 'PIN koruması',
61
108
  'modal.share.expires': 'Son kullanma',
62
109
  'modal.share.max_downloads': 'İndirme limiti',
@@ -165,6 +212,9 @@ export const tr: Record<string, string> = {
165
212
  'viewer.archive.error': 'Arşiv okunamadı.',
166
213
  'viewer.archive.unsupported': 'Bu tür bir arşiv burada listelenemiyor. İçinde ne olduğunu görmek için indirin.',
167
214
  'viewer.archive.too_large': 'Bu arşiv burada listelenemeyecek kadar büyük. İçinde ne olduğunu görmek için indirin.',
215
+ 'viewer.archive.open': 'Arşivi aç',
216
+ 'viewer.archive.root': 'Arşiv',
217
+ 'viewer.archive.location': 'Arşiv konumu',
168
218
  'viewer.name': 'Ad',
169
219
  'viewer.size': 'Boyut',
170
220
 
@@ -306,6 +356,17 @@ export const tr: Record<string, string> = {
306
356
  'search.in_content': 'İçerikte',
307
357
  'search.truncated': 'Gösterilenden daha fazla sonuç var — aramanızı daraltın',
308
358
 
359
+ /* Katalog kapsamı (docs/LAZY-CATALOGUE.md, lib/catalogCoverage). */
360
+ 'coverage.first_scan': 'Bu deponun ilk senkronu henüz bitmedi. Bitene kadar arama, klasör boyutları ve kullanım deponun bir kısmını kapsamaz.',
361
+ 'coverage.lazy_filling': 'filex bu depoyu hâlâ kataloglıyor (şimdiye kadar bulunan klasörlerin %{pct} kadarı tamam). Bitene kadar arama, klasör boyutları ve kullanım deponun bir kısmını kapsamaz.',
362
+ 'coverage.lazy_on_open': 'Bu depoda yalnız açılan klasörler kataloglanır. Arama, klasör boyutları ve kullanım yalnız bu klasörleri kapsar.',
363
+ 'coverage.search_some': 'Şu depoların tamamı henüz kataloglanmadı: {names}. Bu arama oradaki bazı dosyaları bulamayabilir.',
364
+ 'coverage.catalog_all': 'Tümünü katalogla',
365
+ 'coverage.catalog_started': 'Senkron başladı — deponun tamamı kataloglanıyor.',
366
+ 'size.at_least': '≥ {size}',
367
+ 'size.partial_hint': 'Bu klasörün bir kısmı henüz kataloglanmadı; boyutu en az bu kadar.',
368
+ 'size.unknown_hint': 'Bu klasör henüz kataloglanmadı; boyutu bilinmiyor.',
369
+
309
370
  /* === bag:b4 === */
310
371
  'toolbar.more': 'Diğer işlemler',
311
372
  'toolbar.search.close': 'Aramayı kapat',
@@ -453,6 +514,7 @@ export const tr: Record<string, string> = {
453
514
  'opc.retry': 'Yeniden dene',
454
515
  'opc.dismiss': 'Kapat',
455
516
  'opc.queued': 'Sırada',
517
+ 'opc.cancelling': 'İptal isteniyor…',
456
518
  'opc.percent': '%{n}',
457
519
  'opc.status.running': 'Sürüyor',
458
520
  'opc.status.done': 'Bitti',
@@ -667,6 +729,8 @@ export const tr: Record<string, string> = {
667
729
  'plugin.pdf.date_order': 'Tarih sırası',
668
730
  'plugin.pdf.date_separator': 'Ayraç',
669
731
  'plugin.pdf.date_example': 'Örnek',
732
+ 'opc.kind.archive-create': 'Arşiv oluşturuluyor',
733
+ 'opc.kind.archive-extract': 'Arşiv çıkarılıyor',
670
734
  /* === wiring:c4 — onboarding turu + erişilebilirlik + hata kartı === */
671
735
  'tour.aria': 'Tanıtım turu',
672
736
  'tour.restart': 'Turu tekrar başlat',
@@ -1374,6 +1438,7 @@ export const tr: Record<string, string> = {
1374
1438
  'drive.search.hint_title': 'Her yerde ara ve komut çalıştır ({combo})',
1375
1439
  'drive.storage.used': '{total} alanın {used} kadarı dolu',
1376
1440
  'drive.storage.used_unlimited': '{used} kullanılıyor',
1441
+ 'drive.storage.used_partial': 'en az {used} kullanılıyor',
1377
1442
  'drive.storage.label': 'Depolama',
1378
1443
  /* === gorunum:v3-shell — Ana sayfa görünümü (kabuğun içindeki genel bakış)
1379
1444
  * Bölüm başlıkları `sidenav.storages` / `sidenav.recent` / `sidenav.starred`,
@@ -0,0 +1,192 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue';
3
+ import type { LocaleCode } from '../types/ExplorerConfig';
4
+ import type { ArchiveCreateFormat } from '../types/FileNode';
5
+ import { useLocale } from '../composables/useLocale';
6
+ import Modal from './Modal.vue';
7
+ import { archiveFormatLabel } from '../lib/archiveFormats';
8
+
9
+ const props = withDefaults(defineProps<{
10
+ open: boolean;
11
+ locale: LocaleCode;
12
+ count: number;
13
+ suggestedName: string;
14
+ defaultFormat?: ArchiveCreateFormat;
15
+ allowedFormats?: ArchiveCreateFormat[];
16
+ busy?: boolean;
17
+ requestError?: string;
18
+ /** Can the server set a password (it needs 7-Zip)? Default yes. ⚠ Stated
19
+ * in withDefaults: Vue casts an absent optional boolean prop to false. */
20
+ encryption?: boolean;
21
+ }>(), { encryption: true });
22
+
23
+ const emit = defineEmits<{
24
+ (e: 'close'): void;
25
+ (e: 'submit', value: { name: string; format: ArchiveCreateFormat; password?: string; encrypt_filenames: boolean; compression: number; solid?: boolean; dictionary_size_mb?: number }): void;
26
+ }>();
27
+
28
+ const { t } = useLocale(() => props.locale);
29
+ const name = ref('archive.zip');
30
+ const format = ref<ArchiveCreateFormat>('7z');
31
+ const password = ref('');
32
+ const confirmPassword = ref('');
33
+ const encryptNames = ref(false);
34
+ const compression = ref(5);
35
+ const solid = ref(true);
36
+ const dictionarySizeMB = ref(64);
37
+ const error = ref('');
38
+
39
+ const allFormats: ArchiveCreateFormat[] = ['zip', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz'];
40
+ const availableFormats = computed<ArchiveCreateFormat[]>(() => {
41
+ const formats = (props.allowedFormats ?? allFormats).filter(
42
+ (value, index, all) => allFormats.includes(value) && all.indexOf(value) === index,
43
+ );
44
+ return formats.length ? formats : allFormats;
45
+ });
46
+ const supportsEncryption = computed(() => props.encryption !== false && (format.value === 'zip' || format.value === '7z'));
47
+
48
+ function formatFromName(value: string): ArchiveCreateFormat | null {
49
+ const lower = value.trim().toLowerCase();
50
+ return allFormats.find((candidate) => lower.endsWith(`.${candidate}`)) ?? null;
51
+ }
52
+
53
+ function stripArchiveExtension(value: string): string {
54
+ const detected = formatFromName(value);
55
+ return detected ? value.slice(0, -(detected.length + 1)) : value;
56
+ }
57
+
58
+ watch(() => props.open, (open) => {
59
+ if (!open) return;
60
+ const preferred = props.defaultFormat && availableFormats.value.includes(props.defaultFormat)
61
+ ? props.defaultFormat
62
+ : availableFormats.value.includes('7z') ? '7z' : availableFormats.value[0];
63
+ format.value = preferred;
64
+ name.value = `${stripArchiveExtension(props.suggestedName || 'archive')}.${preferred}`;
65
+ password.value = '';
66
+ confirmPassword.value = '';
67
+ encryptNames.value = false;
68
+ compression.value = 5;
69
+ solid.value = true;
70
+ dictionarySizeMB.value = 64;
71
+ error.value = '';
72
+ });
73
+
74
+ watch(format, (next) => {
75
+ if (!props.open) return;
76
+ const base = stripArchiveExtension(name.value.trim()) || 'archive';
77
+ name.value = `${base}.${next}`;
78
+ if (next !== 'zip' && next !== '7z') {
79
+ password.value = '';
80
+ confirmPassword.value = '';
81
+ encryptNames.value = false;
82
+ }
83
+ });
84
+
85
+ watch(name, (next) => {
86
+ const detected = formatFromName(next);
87
+ if (detected && availableFormats.value.includes(detected)) format.value = detected;
88
+ });
89
+
90
+ function submit() {
91
+ if (!name.value.trim() || /[\\/]/.test(name.value)) {
92
+ error.value = t('archive.invalid_name');
93
+ return;
94
+ }
95
+ const extension = formatFromName(name.value);
96
+ if (!extension || !availableFormats.value.includes(extension)) {
97
+ error.value = t('archive.invalid_extension');
98
+ return;
99
+ }
100
+ if (supportsEncryption.value && password.value !== confirmPassword.value) {
101
+ error.value = t('archive.password_mismatch');
102
+ return;
103
+ }
104
+ /* 7-Zip encrypts a ZIP with printable ASCII only; the server refuses the
105
+ * rest too (PASSWORD_CHARSET), this only says so before anything is sent. */
106
+ if (extension === 'zip' && password.value && !/^[\x20-\x7e]*$/.test(password.value)) {
107
+ error.value = t('archive.password_charset');
108
+ return;
109
+ }
110
+ emit('submit', {
111
+ name: name.value.trim(),
112
+ format: extension,
113
+ password: supportsEncryption.value ? password.value || undefined : undefined,
114
+ encrypt_filenames: !!password.value && extension === '7z' && encryptNames.value,
115
+ compression: compression.value,
116
+ solid: extension === '7z' ? solid.value : undefined,
117
+ dictionary_size_mb: extension === '7z' ? dictionarySizeMB.value : undefined,
118
+ });
119
+ }
120
+ </script>
121
+
122
+ <template>
123
+ <Modal :open="open" :title="t('archive.create_title')" size="sm" @close="emit('close')">
124
+ <form class="fe-form" @submit.prevent="submit">
125
+ <p class="fe-field__hint">{{ t('archive.create_count', { count }) }}</p>
126
+ <label class="fe-field">
127
+ <span class="fe-field__label">{{ t('archive.filename') }}</span>
128
+ <input v-model="name" class="fe-input" autocomplete="off" />
129
+ </label>
130
+ <label class="fe-field">
131
+ <span class="fe-field__label">{{ t('archive.format') }}</span>
132
+ <select v-model="format" class="fe-input">
133
+ <option v-for="option in availableFormats" :key="option" :value="option">
134
+ {{ archiveFormatLabel(option) }}
135
+ </option>
136
+ </select>
137
+ </label>
138
+ <label v-if="supportsEncryption" class="fe-field">
139
+ <span class="fe-field__label">{{ t('archive.password_optional') }}</span>
140
+ <input
141
+ v-model="password"
142
+ class="fe-input"
143
+ type="password"
144
+ autocomplete="off"
145
+ data-1p-ignore
146
+ data-bwignore="true"
147
+ data-lpignore="true"
148
+ />
149
+ </label>
150
+ <label v-if="supportsEncryption" class="fe-field">
151
+ <span class="fe-field__label">{{ t('archive.password_confirm') }}</span>
152
+ <input
153
+ v-model="confirmPassword"
154
+ class="fe-input"
155
+ type="password"
156
+ autocomplete="off"
157
+ data-1p-ignore
158
+ data-bwignore="true"
159
+ data-lpignore="true"
160
+ />
161
+ </label>
162
+ <label v-if="format === '7z' && password" class="fe-checkbox">
163
+ <input v-model="encryptNames" type="checkbox" />
164
+ <span>{{ t('archive.encrypt_names') }}</span>
165
+ </label>
166
+ <label v-if="format !== 'tar'" class="fe-field">
167
+ <span class="fe-field__label">{{ t('archive.compression') }}: {{ compression }}</span>
168
+ <input v-model.number="compression" type="range" min="0" max="9" />
169
+ </label>
170
+ <template v-if="format === '7z'">
171
+ <label class="fe-checkbox">
172
+ <input v-model="solid" type="checkbox" />
173
+ <span>{{ t('archive.solid') }}</span>
174
+ </label>
175
+ <label class="fe-field">
176
+ <span class="fe-field__label">{{ t('archive.dictionary_size') }}</span>
177
+ <select v-model.number="dictionarySizeMB" class="fe-input">
178
+ <option v-for="size in [4, 8, 16, 32, 64, 128, 256]" :key="size" :value="size">{{ t('archive.dictionary_size_value', { n: size }) }}</option>
179
+ </select>
180
+ <span class="fe-field__hint">{{ t('archive.dictionary_hint') }}</span>
181
+ </label>
182
+ </template>
183
+ <p v-if="error || requestError" class="fe-form__error">{{ error || requestError }}</p>
184
+ </form>
185
+ <template #actions>
186
+ <button type="button" class="fe-btn" :disabled="busy" @click="emit('close')">{{ t('archive.cancel') }}</button>
187
+ <button type="button" class="fe-btn fe-btn--primary" :disabled="busy" @click="submit">
188
+ {{ busy ? t('archive.creating') : t('archive.create') }}
189
+ </button>
190
+ </template>
191
+ </Modal>
192
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue';
3
+ import type { LocaleCode } from '../types/ExplorerConfig';
4
+ import { useLocale } from '../composables/useLocale';
5
+ import Modal from './Modal.vue';
6
+
7
+ const props = defineProps<{
8
+ open: boolean;
9
+ locale: LocaleCode;
10
+ archiveName: string;
11
+ suggestedFolder: string;
12
+ busy?: boolean;
13
+ error?: string;
14
+ }>();
15
+
16
+ const emit = defineEmits<{
17
+ (e: 'close'): void;
18
+ (e: 'submit', value: { folder: string }): void;
19
+ }>();
20
+
21
+ const { t } = useLocale(() => props.locale);
22
+ const folder = ref('');
23
+ const localError = ref('');
24
+
25
+ watch(() => props.open, (open) => {
26
+ if (!open) return;
27
+ folder.value = props.suggestedFolder;
28
+ localError.value = '';
29
+ });
30
+
31
+ function submit() {
32
+ if (!folder.value.trim() || /[\\/]/.test(folder.value)) {
33
+ localError.value = t('archive.invalid_folder');
34
+ return;
35
+ }
36
+ emit('submit', { folder: folder.value.trim() });
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <Modal :open="open" :title="t('archive.extract_title')" size="sm" @close="emit('close')">
42
+ <form class="fe-form" @submit.prevent="submit">
43
+ <p class="fe-field__hint">{{ archiveName }}</p>
44
+ <label class="fe-field">
45
+ <span class="fe-field__label">{{ t('archive.destination_folder') }}</span>
46
+ <input v-model="folder" class="fe-input" autocomplete="off" />
47
+ </label>
48
+ <p v-if="localError || error" class="fe-form__error">{{ localError || error }}</p>
49
+ </form>
50
+ <template #actions>
51
+ <button type="button" class="fe-btn" :disabled="busy" @click="emit('close')">{{ t('archive.cancel') }}</button>
52
+ <button type="button" class="fe-btn fe-btn--primary" :disabled="busy" @click="submit">
53
+ {{ busy ? t('archive.extracting') : t('archive.extract_to_folder') }}
54
+ </button>
55
+ </template>
56
+ </Modal>
57
+ </template>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue';
3
+ import type { LocaleCode } from '../types/ExplorerConfig';
4
+ import { useLocale } from '../composables/useLocale';
5
+ import Modal from './Modal.vue';
6
+
7
+ const props = defineProps<{
8
+ open: boolean;
9
+ locale: LocaleCode;
10
+ archiveName: string;
11
+ busy?: boolean;
12
+ error?: string;
13
+ }>();
14
+
15
+ const emit = defineEmits<{
16
+ (e: 'close'): void;
17
+ (e: 'submit', password: string): void;
18
+ }>();
19
+
20
+ const { t } = useLocale(() => props.locale);
21
+ const password = ref('');
22
+
23
+ watch(() => props.open, (open) => {
24
+ if (open) password.value = '';
25
+ });
26
+
27
+ function submit() {
28
+ if (!password.value || props.busy) return;
29
+ emit('submit', password.value);
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <Modal :open="open" :title="t('archive.password_title')" size="sm" @close="emit('close')">
35
+ <form class="fe-form" @submit.prevent="submit">
36
+ <p class="fe-field__hint">{{ archiveName }}</p>
37
+ <label class="fe-field">
38
+ <span class="fe-field__label">{{ t('archive.password_required') }}</span>
39
+ <input
40
+ v-model="password"
41
+ class="fe-input"
42
+ type="password"
43
+ autocomplete="off"
44
+ data-1p-ignore
45
+ data-bwignore="true"
46
+ data-lpignore="true"
47
+ autofocus
48
+ :disabled="busy"
49
+ />
50
+ </label>
51
+ <p v-if="error" class="fe-form__error" role="alert">{{ error }}</p>
52
+ </form>
53
+ <template #actions>
54
+ <button type="button" class="fe-btn" :disabled="busy" @click="emit('close')">
55
+ {{ t('archive.cancel') }}
56
+ </button>
57
+ <button type="button" class="fe-btn fe-btn--primary" :disabled="busy || !password" @click="submit">
58
+ {{ busy ? t('archive.opening') : t('archive.continue') }}
59
+ </button>
60
+ </template>
61
+ </Modal>
62
+ </template>
@@ -33,6 +33,7 @@ import { fileIconTile } from '../lib/fileIcons';
33
33
  import { actionIconSvg } from '../lib/actionIcons';
34
34
  import { OFFICE_EXTS } from '../lib/serviceGate';
35
35
  import { requestFailure, sayFailure } from '../lib/errorWords';
36
+ import { createArchivePreviewCache } from '../lib/archivePreviewCache';
36
37
 
37
38
  const props = defineProps<{
38
39
  open: boolean;
@@ -114,6 +115,7 @@ const emit = defineEmits<{
114
115
  }>();
115
116
 
116
117
  const { t, formatSize, formatDate, nodeDisplayName } = useLocale(() => props.locale);
118
+ const archivePreviewCache = createArchivePreviewCache();
117
119
 
118
120
  function ext(f: FileNode | null): string {
119
121
  return (f?.extension || '').toLowerCase();
@@ -188,6 +190,15 @@ const VIEWER_MAP: Record<string, () => Promise<Component>> = {
188
190
  tsv: () => import('../viewers/CsvViewer.vue'),
189
191
 
190
192
  zip: () => import('../viewers/ArchiveViewer.vue'),
193
+ '7z': () => import('../viewers/ArchiveViewer.vue'),
194
+ rar: () => import('../viewers/ArchiveViewer.vue'),
195
+ tar: () => import('../viewers/ArchiveViewer.vue'),
196
+ gz: () => import('../viewers/ArchiveViewer.vue'),
197
+ tgz: () => import('../viewers/ArchiveViewer.vue'),
198
+ bz2: () => import('../viewers/ArchiveViewer.vue'),
199
+ tbz2: () => import('../viewers/ArchiveViewer.vue'),
200
+ xz: () => import('../viewers/ArchiveViewer.vue'),
201
+ txz: () => import('../viewers/ArchiveViewer.vue'),
191
202
  };
192
203
 
193
204
  type PreviewKind =
@@ -264,9 +275,10 @@ const viewerProps = computed(() => {
264
275
  // /api/files/archive/list handler falls back to storages[0] when
265
276
  // no adapter prefix is present — on multi-storage instances that
266
277
  // 500s for every non-default storage (sample.zip on fm s3-test).
267
- if (e === 'zip' || e === 'rar' || e === '7z' || e === 'tar' || e === 'gz' || e === 'tgz') {
278
+ if (['zip', 'rar', '7z', 'tar', 'gz', 'tgz', 'bz2', 'tbz2', 'xz', 'txz'].includes(e)) {
268
279
  base.filePath = props.file.path;
269
280
  if (props.archiveListEndpoint) base.archiveListUrl = props.archiveListEndpoint;
281
+ base.archivePreviewCache = archivePreviewCache;
270
282
  }
271
283
  }
272
284
  if (e === 'drawio' || e === 'dio') {
@@ -1124,6 +1136,7 @@ onBeforeUnmount(() => {
1124
1136
  document.removeEventListener('fullscreenchange', onFullscreenChange);
1125
1137
  disposeOnlyOfficeEditor();
1126
1138
  disposeMonaco();
1139
+ archivePreviewCache.clear();
1127
1140
  });
1128
1141
 
1129
1142
  /**
@@ -1375,6 +1388,7 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
1375
1388
  v-bind="viewerProps"
1376
1389
  class="fe-preview__viewer"
1377
1390
  @fallback="onPdfFallback"
1391
+ @close="emit('close')"
1378
1392
  />
1379
1393
  </template>
1380
1394