@brftech/filex-core 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ArchiveViewer-D_tJxJhJ.js +236 -0
- package/dist/ArchiveViewer-D_tJxJhJ.js.map +1 -0
- package/dist/{CsvViewer-ByHVK5ga.js → CsvViewer-B95fArq0.js} +2 -2
- package/dist/{CsvViewer-ByHVK5ga.js.map → CsvViewer-B95fArq0.js.map} +1 -1
- package/dist/{DrawioViewer-Bgrj8-zG.js → DrawioViewer-BAaPMTge.js} +2 -2
- package/dist/{DrawioViewer-Bgrj8-zG.js.map → DrawioViewer-BAaPMTge.js.map} +1 -1
- package/dist/{EpubViewer-DJwUKlHW.js → EpubViewer-B-B7bRW7.js} +2 -2
- package/dist/{EpubViewer-DJwUKlHW.js.map → EpubViewer-B-B7bRW7.js.map} +1 -1
- package/dist/{IpynbViewer-Bz0eJ4NS.js → IpynbViewer-COOTyOcu.js} +2 -2
- package/dist/{IpynbViewer-Bz0eJ4NS.js.map → IpynbViewer-COOTyOcu.js.map} +1 -1
- package/dist/{MermaidViewer-CEDTD_pp.js → MermaidViewer-CjVCmR3s.js} +2 -2
- package/dist/{MermaidViewer-CEDTD_pp.js.map → MermaidViewer-CjVCmR3s.js.map} +1 -1
- package/dist/{PsdViewer-CZcxUpZO.js → PsdViewer-xsIYL9Gu.js} +2 -2
- package/dist/{PsdViewer-CZcxUpZO.js.map → PsdViewer-xsIYL9Gu.js.map} +1 -1
- package/dist/{TiffViewer-BwI62CbQ.js → TiffViewer-CtwnTxqL.js} +2 -2
- package/dist/{TiffViewer-BwI62CbQ.js.map → TiffViewer-CtwnTxqL.js.map} +1 -1
- package/dist/{Viewer3D-DoeZcuMm.js → Viewer3D-xYMwndEy.js} +2 -2
- package/dist/{Viewer3D-DoeZcuMm.js.map → Viewer3D-xYMwndEy.js.map} +1 -1
- package/dist/filex-core.js +372 -366
- package/dist/filex-core.umd.cjs +62 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-DuWnGuv5.js → index-BBToAjwL.js} +16770 -15869
- package/dist/index-BBToAjwL.js.map +1 -0
- package/dist/index.d.ts +185 -10
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +363 -23
- package/src/components/ContextMenu.vue +17 -0
- package/src/components/GalleryView.vue +24 -30
- package/src/components/GridView.vue +33 -36
- package/src/components/HomeView.vue +14 -4
- package/src/components/InspectorPanel.vue +9 -3
- package/src/components/ListView.vue +21 -14
- package/src/components/OperationsCenter.vue +6 -0
- package/src/components/PendingOpsTray.vue +5 -3
- package/src/components/ThumbTile.vue +58 -0
- package/src/composables/useFileApi.ts +36 -6
- package/src/composables/useLocale.ts +26 -0
- package/src/composables/useOperations.ts +7 -1
- package/src/composables/usePendingOps.ts +11 -7
- package/src/composables/useThumbs.ts +53 -21
- package/src/index.ts +15 -0
- package/src/lib/actionIcons.ts +6 -0
- package/src/lib/archiveFormats.ts +12 -0
- package/src/lib/archivePreviewCache.ts +61 -0
- package/src/lib/catalogCoverage.ts +137 -0
- package/src/lib/nearViewport.ts +77 -0
- package/src/locales/en.ts +67 -0
- package/src/locales/tr.ts +65 -0
- package/src/modals/ArchiveCreateModal.vue +192 -0
- package/src/modals/ArchiveExtractModal.vue +57 -0
- package/src/modals/ArchivePasswordModal.vue +62 -0
- package/src/modals/PreviewModal.vue +15 -1
- package/src/styles/base.css +47 -2
- package/src/types/ExplorerConfig.ts +9 -0
- package/src/types/FileNode.ts +19 -0
- package/src/viewers/ArchiveViewer.vue +145 -40
- package/dist/ArchiveViewer-BqU761Wk.js +0 -149
- package/dist/ArchiveViewer-BqU761Wk.js.map +0 -1
- package/dist/index-DuWnGuv5.js.map +0 -1
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 (
|
|
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
|
|
package/src/styles/base.css
CHANGED
|
@@ -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
|
/**
|
package/src/types/FileNode.ts
CHANGED
|
@@ -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
|