@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.
Files changed (60) 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-ByHVK5ga.js → CsvViewer-B95fArq0.js} +2 -2
  4. package/dist/{CsvViewer-ByHVK5ga.js.map → CsvViewer-B95fArq0.js.map} +1 -1
  5. package/dist/{DrawioViewer-Bgrj8-zG.js → DrawioViewer-BAaPMTge.js} +2 -2
  6. package/dist/{DrawioViewer-Bgrj8-zG.js.map → DrawioViewer-BAaPMTge.js.map} +1 -1
  7. package/dist/{EpubViewer-DJwUKlHW.js → EpubViewer-B-B7bRW7.js} +2 -2
  8. package/dist/{EpubViewer-DJwUKlHW.js.map → EpubViewer-B-B7bRW7.js.map} +1 -1
  9. package/dist/{IpynbViewer-Bz0eJ4NS.js → IpynbViewer-COOTyOcu.js} +2 -2
  10. package/dist/{IpynbViewer-Bz0eJ4NS.js.map → IpynbViewer-COOTyOcu.js.map} +1 -1
  11. package/dist/{MermaidViewer-CEDTD_pp.js → MermaidViewer-CjVCmR3s.js} +2 -2
  12. package/dist/{MermaidViewer-CEDTD_pp.js.map → MermaidViewer-CjVCmR3s.js.map} +1 -1
  13. package/dist/{PsdViewer-CZcxUpZO.js → PsdViewer-xsIYL9Gu.js} +2 -2
  14. package/dist/{PsdViewer-CZcxUpZO.js.map → PsdViewer-xsIYL9Gu.js.map} +1 -1
  15. package/dist/{TiffViewer-BwI62CbQ.js → TiffViewer-CtwnTxqL.js} +2 -2
  16. package/dist/{TiffViewer-BwI62CbQ.js.map → TiffViewer-CtwnTxqL.js.map} +1 -1
  17. package/dist/{Viewer3D-DoeZcuMm.js → Viewer3D-xYMwndEy.js} +2 -2
  18. package/dist/{Viewer3D-DoeZcuMm.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-DuWnGuv5.js → index-BBToAjwL.js} +16770 -15869
  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/GalleryView.vue +24 -30
  30. package/src/components/GridView.vue +33 -36
  31. package/src/components/HomeView.vue +14 -4
  32. package/src/components/InspectorPanel.vue +9 -3
  33. package/src/components/ListView.vue +21 -14
  34. package/src/components/OperationsCenter.vue +6 -0
  35. package/src/components/PendingOpsTray.vue +5 -3
  36. package/src/components/ThumbTile.vue +58 -0
  37. package/src/composables/useFileApi.ts +36 -6
  38. package/src/composables/useLocale.ts +26 -0
  39. package/src/composables/useOperations.ts +7 -1
  40. package/src/composables/usePendingOps.ts +11 -7
  41. package/src/composables/useThumbs.ts +53 -21
  42. package/src/index.ts +15 -0
  43. package/src/lib/actionIcons.ts +6 -0
  44. package/src/lib/archiveFormats.ts +12 -0
  45. package/src/lib/archivePreviewCache.ts +61 -0
  46. package/src/lib/catalogCoverage.ts +137 -0
  47. package/src/lib/nearViewport.ts +77 -0
  48. package/src/locales/en.ts +67 -0
  49. package/src/locales/tr.ts +65 -0
  50. package/src/modals/ArchiveCreateModal.vue +192 -0
  51. package/src/modals/ArchiveExtractModal.vue +57 -0
  52. package/src/modals/ArchivePasswordModal.vue +62 -0
  53. package/src/modals/PreviewModal.vue +15 -1
  54. package/src/styles/base.css +47 -2
  55. package/src/types/ExplorerConfig.ts +9 -0
  56. package/src/types/FileNode.ts +19 -0
  57. package/src/viewers/ArchiveViewer.vue +145 -40
  58. package/dist/ArchiveViewer-BqU761Wk.js +0 -149
  59. package/dist/ArchiveViewer-BqU761Wk.js.map +0 -1
  60. package/dist/index-DuWnGuv5.js.map +0 -1
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
 
@@ -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