@brftech/filex-core 0.32.0 → 0.33.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/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
- package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
- package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
- package/dist/DrawioViewer-BNALOB04.js.map +1 -0
- package/dist/filex-core.js +5932 -5841
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +43 -43
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +49 -8
- package/package.json +1 -1
- package/src/FileExplorer.vue +267 -235
- package/src/components/CommandPalette.vue +3 -3
- package/src/components/ConnectionsPanel.vue +2 -1
- package/src/components/GalleryView.vue +3 -3
- package/src/components/GridView.vue +4 -4
- package/src/components/ListView.vue +4 -4
- package/src/components/NFSExportsPanel.vue +2 -1
- package/src/components/PendingOpsTray.vue +1 -1
- package/src/components/PresenceBar.vue +3 -2
- package/src/components/S3KeysPanel.vue +2 -1
- package/src/components/SSHKeysPanel.vue +2 -1
- package/src/components/SecondaryPane.vue +13 -12
- package/src/components/StarButton.vue +2 -1
- package/src/components/TabBar.vue +2 -2
- package/src/components/ThemeGallery.vue +1 -1
- package/src/components/TokensPanel.vue +2 -1
- package/src/components/Toolbar.vue +9 -9
- package/src/composables/useFileApi.ts +4 -2
- package/src/composables/useLocale.ts +1 -1
- package/src/composables/useOperations.ts +3 -3
- package/src/composables/usePendingOps.ts +1 -1
- package/src/composables/useTabs.ts +1 -1
- package/src/composables/useUploadChunked.ts +39 -7
- package/src/index.ts +2 -2
- package/src/lib/themes.ts +7 -7
- package/src/lib/transfer.ts +6 -6
- package/src/locales/en.ts +32 -0
- package/src/locales/index.ts +1 -0
- package/src/locales/resolve.ts +66 -0
- package/src/locales/tr.ts +33 -0
- package/src/modals/ConvertModal.vue +14 -12
- package/src/modals/NewFolderModal.vue +1 -1
- package/src/modals/PermissionsModal.vue +12 -11
- package/src/modals/PreviewModal.vue +9 -9
- package/src/styles/base.css +29 -26
- package/src/styles/variables.css +1 -1
- package/src/types/ExplorerConfig.ts +1 -1
- package/src/types/FileNode.ts +1 -1
- package/src/viewers/CsvViewer.vue +4 -4
- package/src/viewers/DrawioViewer.vue +2 -2
- package/dist/CsvViewer-DxujFEM3.js.map +0 -1
- package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
package/src/lib/themes.ts
CHANGED
|
@@ -105,7 +105,7 @@ export const THEMES: ThemeDef[] = [
|
|
|
105
105
|
},
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
|
-
//
|
|
108
|
+
// Night Blue — deep indigo/navy.
|
|
109
109
|
id: 'night',
|
|
110
110
|
nameKey: 'theme.name.night',
|
|
111
111
|
light: {
|
|
@@ -140,7 +140,7 @@ export const THEMES: ThemeDef[] = [
|
|
|
140
140
|
},
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
|
-
//
|
|
143
|
+
// Forest — calm greens.
|
|
144
144
|
id: 'forest',
|
|
145
145
|
nameKey: 'theme.name.forest',
|
|
146
146
|
light: {
|
|
@@ -175,7 +175,7 @@ export const THEMES: ThemeDef[] = [
|
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
|
-
//
|
|
178
|
+
// Amber — warm amber/bronze.
|
|
179
179
|
id: 'amber',
|
|
180
180
|
nameKey: 'theme.name.amber',
|
|
181
181
|
light: {
|
|
@@ -210,7 +210,7 @@ export const THEMES: ThemeDef[] = [
|
|
|
210
210
|
},
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
|
-
//
|
|
213
|
+
// Lilac — soft purple.
|
|
214
214
|
id: 'lilac',
|
|
215
215
|
nameKey: 'theme.name.lilac',
|
|
216
216
|
light: {
|
|
@@ -245,7 +245,7 @@ export const THEMES: ThemeDef[] = [
|
|
|
245
245
|
},
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
|
-
//
|
|
248
|
+
// High Contrast — black/white with strong borders, saturated accents.
|
|
249
249
|
id: 'contrast',
|
|
250
250
|
nameKey: 'theme.name.contrast',
|
|
251
251
|
light: {
|
|
@@ -284,7 +284,7 @@ export const THEMES: ThemeDef[] = [
|
|
|
284
284
|
},
|
|
285
285
|
},
|
|
286
286
|
{
|
|
287
|
-
//
|
|
287
|
+
// Soft Gray — desaturated, quiet neutral.
|
|
288
288
|
id: 'gray',
|
|
289
289
|
nameKey: 'theme.name.gray',
|
|
290
290
|
light: {
|
|
@@ -319,7 +319,7 @@ export const THEMES: ThemeDef[] = [
|
|
|
319
319
|
},
|
|
320
320
|
},
|
|
321
321
|
{
|
|
322
|
-
// Terminal
|
|
322
|
+
// Terminal Green — phosphor green, monospace face for the full CRT vibe.
|
|
323
323
|
id: 'terminal',
|
|
324
324
|
nameKey: 'theme.name.terminal',
|
|
325
325
|
light: {
|
package/src/lib/transfer.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Three gestures land on the same wire call — drag a row onto a folder, paste
|
|
5
5
|
* after Ctrl+X, paste after Ctrl+C — and they do NOT mean the same thing once
|
|
6
|
-
* the two ends live in different
|
|
6
|
+
* the two ends live in different storages:
|
|
7
7
|
*
|
|
8
8
|
* • Ctrl+C → paste is a copy, wherever it lands.
|
|
9
|
-
* • Ctrl+X → paste is a move, wherever it lands. Across
|
|
9
|
+
* • Ctrl+X → paste is a move, wherever it lands. Across storages the server
|
|
10
10
|
* streams the bytes over and then deletes the original; before v0.27.0 the
|
|
11
11
|
* explorer quietly downgraded this to a copy and the user was left with the
|
|
12
12
|
* file in both places, believing they had moved it.
|
|
13
|
-
* • Dragging is a move inside one
|
|
13
|
+
* • Dragging is a move inside one storage and a COPY across two — the rule
|
|
14
14
|
* Explorer and Finder have taught everyone: a drag between drives copies.
|
|
15
15
|
*
|
|
16
16
|
* Kept as a pure function so both the pane path and the clipboard path ask the
|
|
@@ -23,13 +23,13 @@ export function wireAdapterOf(p: string): string {
|
|
|
23
23
|
return i === -1 ? '' : p.slice(0, i);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** What the caller asked for. `auto` is a drag: let the
|
|
26
|
+
/** What the caller asked for. `auto` is a drag: let the storages decide. */
|
|
27
27
|
export type TransferIntent = 'auto' | 'copy' | 'move';
|
|
28
28
|
|
|
29
29
|
export interface TransferPlan {
|
|
30
30
|
/** What to actually ask the server for. */
|
|
31
31
|
kind: 'copy' | 'move';
|
|
32
|
-
/** True when at least one source lives in another
|
|
32
|
+
/** True when at least one source lives in another storage than the target. */
|
|
33
33
|
cross: boolean;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -40,7 +40,7 @@ export function resolveTransfer(
|
|
|
40
40
|
): TransferPlan {
|
|
41
41
|
const target = wireAdapterOf(targetWire);
|
|
42
42
|
// A source with no prefix is a legacy embedder's bare path: it can only mean
|
|
43
|
-
// "the same
|
|
43
|
+
// "the same storage I am looking at", so it never counts as crossing.
|
|
44
44
|
const cross = sources.some((p) => {
|
|
45
45
|
const a = wireAdapterOf(p);
|
|
46
46
|
return a !== '' && target !== '' && a !== target;
|
package/src/locales/en.ts
CHANGED
|
@@ -208,6 +208,25 @@ export const en: Record<string, string> = {
|
|
|
208
208
|
'toast.renamed': 'Renamed',
|
|
209
209
|
'toast.moved': 'Moved',
|
|
210
210
|
'toast.trashed': 'Moved to trash',
|
|
211
|
+
/* i18n:hardcoded — toast copy that used to be Turkish string literals in
|
|
212
|
+
FileExplorer.vue, unreachable by any locale. */
|
|
213
|
+
'toast.failed': 'Operation failed',
|
|
214
|
+
'toast.copied': 'Copied',
|
|
215
|
+
'toast.deleted': 'Deleted',
|
|
216
|
+
'toast.cut': 'Cut',
|
|
217
|
+
'toast.cut_ready': 'Cut — ready to paste',
|
|
218
|
+
'toast.copy_ready': 'Copied — ready to paste',
|
|
219
|
+
'toast.same_folder_cut': 'Cannot cut into the same folder',
|
|
220
|
+
'toast.restored': '{n} item(s) restored',
|
|
221
|
+
'toast.delete_queued': 'Delete queued',
|
|
222
|
+
'toast.trash_retention': 'Items in the trash are removed automatically once the retention window ends. Permanent deletion is done from the admin panel.',
|
|
223
|
+
'dropzone.hint': 'Drop files here',
|
|
224
|
+
'modal.newfolder.invalid': 'Invalid character',
|
|
225
|
+
'viewer.office_unconfigured': 'OnlyOffice is not configured',
|
|
226
|
+
'viewer.pdf_inline_failed': 'Your browser could not display this PDF inline.',
|
|
227
|
+
'viewer.too_large': 'File is too large (>1 MB).',
|
|
228
|
+
'viewer.csv_rows': '{n} rows',
|
|
229
|
+
'presence.others': '{n} people',
|
|
211
230
|
'conn.offline': 'No live connection — changes may be delayed',
|
|
212
231
|
'conn.tooltip': 'Live connection to the server is unavailable; the list refreshes periodically. This notice disappears once the connection is back.',
|
|
213
232
|
|
|
@@ -383,6 +402,7 @@ export const en: Record<string, string> = {
|
|
|
383
402
|
'split.error': 'Could not load the listing',
|
|
384
403
|
'split.retry': 'Retry',
|
|
385
404
|
'split.copy_queued': 'Copy queued',
|
|
405
|
+
'split.move_queued': 'Move queued',
|
|
386
406
|
'split.cross_copy': 'Different storages — copy queued instead',
|
|
387
407
|
'dragout.downloading': 'Downloading into the folder you dropped on…',
|
|
388
408
|
'dragout.not_found': 'Could not find where it was dropped — a drop onto an application (rather than a folder) cannot be filled in',
|
|
@@ -1026,4 +1046,16 @@ export const en: Record<string, string> = {
|
|
|
1026
1046
|
'inspector.activity.comment': 'Comment by {who}',
|
|
1027
1047
|
'inspector.activity.select': 'Select a file or folder to see its activity.',
|
|
1028
1048
|
/* === /surucu:d1 === */
|
|
1049
|
+
/* i18n:hardcoded — the convert modal shipped with no keys at all: every
|
|
1050
|
+
label was a Turkish literal behind an optional `t` prop nobody passed. */
|
|
1051
|
+
'convert.title': 'Convert',
|
|
1052
|
+
'convert.loading': 'Loading the converter…',
|
|
1053
|
+
'convert.done': 'Converted and uploaded to this folder.',
|
|
1054
|
+
'convert.close': 'Close',
|
|
1055
|
+
'convert.from': 'Source',
|
|
1056
|
+
'convert.search_format': 'Search a target format (pdf, mp4, png…)',
|
|
1057
|
+
'convert.no_format': 'No matching format.',
|
|
1058
|
+
'convert.unsupported_input': 'This file type has no source format the converter can read.',
|
|
1059
|
+
'convert.converting': 'Converting…',
|
|
1060
|
+
'convert.convert': 'Convert',
|
|
1029
1061
|
};
|
package/src/locales/index.ts
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
2
|
+
|
|
3
|
+
/** The locales this component actually has a catalogue for. */
|
|
4
|
+
const SUPPORTED: readonly LocaleCode[] = ['en', 'tr'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The language filex falls back to when the host does not say.
|
|
8
|
+
*
|
|
9
|
+
* ⚠ It used to be `tr`, in eleven places. filex is an English-first
|
|
10
|
+
* open-source product, so `<filex-explorer api-base="…">` with no `locale`
|
|
11
|
+
* rendered a Turkish UI to whoever installed it — and the web component
|
|
12
|
+
* defaults `locale` to `''`, so "no locale" is the normal case for an embed.
|
|
13
|
+
* The admin SPA never showed it because it always passes one.
|
|
14
|
+
*/
|
|
15
|
+
const FALLBACK: LocaleCode = 'en';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the locale to render in.
|
|
19
|
+
*
|
|
20
|
+
* Order, and the reasoning for it:
|
|
21
|
+
*
|
|
22
|
+
* 1. **What the host asked for.** An explicit `locale` always wins. A host
|
|
23
|
+
* that names a language has made a decision, and guessing over it would
|
|
24
|
+
* make the prop advisory.
|
|
25
|
+
* 2. **What the browser asks for**, via `navigator.languages` /
|
|
26
|
+
* `navigator.language` — the same signal `Accept-Language` carries, and
|
|
27
|
+
* the reason a Turkish reader gets Turkish without the embedder doing
|
|
28
|
+
* anything.
|
|
29
|
+
* 3. **`en`.**
|
|
30
|
+
*
|
|
31
|
+
* ⚠ Step 2 makes the same embed render differently for two people. That is
|
|
32
|
+
* deliberate — it is what "follow the browser" means — but it also means a
|
|
33
|
+
* screenshot, a test or a bug report about this component must state its
|
|
34
|
+
* locale, because the component alone no longer determines it. Pass an
|
|
35
|
+
* explicit `locale` anywhere the answer has to be stable.
|
|
36
|
+
*
|
|
37
|
+
* Matching is on the primary subtag, so `tr-TR` and `en-GB` resolve; anything
|
|
38
|
+
* with no catalogue falls through rather than half-rendering.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveLocale(explicit?: LocaleCode | '' | null): LocaleCode {
|
|
41
|
+
if (explicit && SUPPORTED.includes(explicit)) return explicit;
|
|
42
|
+
return detectLocale();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The browser's preference, or `en`.
|
|
47
|
+
*
|
|
48
|
+
* ⚠ Guarded for a non-browser runtime: this package is imported by SSR and by
|
|
49
|
+
* unit tests under Node, where `navigator` is absent — and an exception here
|
|
50
|
+
* would take down the whole explorer over a language choice.
|
|
51
|
+
*/
|
|
52
|
+
export function detectLocale(): LocaleCode {
|
|
53
|
+
try {
|
|
54
|
+
const nav = typeof navigator === 'undefined' ? undefined : navigator;
|
|
55
|
+
if (!nav) return FALLBACK;
|
|
56
|
+
const wanted = nav.languages && nav.languages.length ? nav.languages : [nav.language];
|
|
57
|
+
for (const tag of wanted) {
|
|
58
|
+
if (!tag) continue;
|
|
59
|
+
const primary = String(tag).toLowerCase().split('-')[0] as LocaleCode;
|
|
60
|
+
if (SUPPORTED.includes(primary)) return primary;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
/* a hostile or exotic runtime is not a reason to fail to render */
|
|
64
|
+
}
|
|
65
|
+
return FALLBACK;
|
|
66
|
+
}
|
package/src/locales/tr.ts
CHANGED
|
@@ -208,6 +208,25 @@ export const tr: Record<string, string> = {
|
|
|
208
208
|
'toast.renamed': 'Yeniden adlandırıldı',
|
|
209
209
|
'toast.moved': 'Taşındı',
|
|
210
210
|
'toast.trashed': 'Çöpe taşındı',
|
|
211
|
+
/* i18n:hardcoded — FileExplorer.vue'da düz string olarak duran, hiçbir dilin
|
|
212
|
+
değiştiremediği bildirim metinleri. */
|
|
213
|
+
'toast.failed': 'İşlem başarısız',
|
|
214
|
+
'toast.copied': 'Kopyalandı',
|
|
215
|
+
'toast.deleted': 'Silindi',
|
|
216
|
+
'toast.cut': 'Kesildi',
|
|
217
|
+
'toast.cut_ready': 'Kes → Yapıştır hazır',
|
|
218
|
+
'toast.copy_ready': 'Kopyala → Yapıştır hazır',
|
|
219
|
+
'toast.same_folder_cut': 'Aynı klasöre kesilemez',
|
|
220
|
+
'toast.restored': '{n} öğe geri getirildi',
|
|
221
|
+
'toast.delete_queued': 'Silme kuyruğa alındı',
|
|
222
|
+
'toast.trash_retention': 'Çöpteki öğeler saklama süresi sonunda otomatik silinir. Kalıcı silme yönetici panelinden yapılır.',
|
|
223
|
+
'dropzone.hint': 'Dosyaları buraya bırak',
|
|
224
|
+
'modal.newfolder.invalid': 'Geçersiz karakter',
|
|
225
|
+
'viewer.office_unconfigured': 'OnlyOffice yapılandırması yok',
|
|
226
|
+
'viewer.pdf_inline_failed': 'Tarayıcı PDF\'i inline açamadı.',
|
|
227
|
+
'viewer.too_large': 'Dosya çok büyük (>1 MB).',
|
|
228
|
+
'viewer.csv_rows': '{n} satır',
|
|
229
|
+
'presence.others': '{n} kişi',
|
|
211
230
|
'conn.offline': 'Canlı bağlantı yok — değişiklikler gecikebilir',
|
|
212
231
|
'conn.tooltip': 'Sunucuyla canlı bağlantı kurulamadı; liste belirli aralıklarla otomatik yenilenir. Bağlantı geri gelince bu uyarı kaybolur.',
|
|
213
232
|
|
|
@@ -383,6 +402,7 @@ export const tr: Record<string, string> = {
|
|
|
383
402
|
'split.error': 'Liste yüklenemedi',
|
|
384
403
|
'split.retry': 'Yeniden dene',
|
|
385
404
|
'split.copy_queued': 'Kopyalama kuyruğa alındı',
|
|
405
|
+
'split.move_queued': 'Taşıma kuyruğa alındı',
|
|
386
406
|
'split.cross_copy': 'Depolar farklı — kopyalama kuyruğa alındı',
|
|
387
407
|
'dragout.downloading': 'Bırakılan klasöre indiriliyor…',
|
|
388
408
|
'dragout.not_found': 'Bırakılan yer bulunamadı — dosya bir klasöre değil bir uygulamaya bırakıldıysa indirme yapılamaz',
|
|
@@ -1029,4 +1049,17 @@ export const tr: Record<string, string> = {
|
|
|
1029
1049
|
'inspector.activity.comment': '{who} yorum yaptı',
|
|
1030
1050
|
'inspector.activity.select': 'Etkinliğini görmek için bir dosya ya da klasör seç.',
|
|
1031
1051
|
/* === /surucu:d1 === */
|
|
1052
|
+
/* i18n:hardcoded — dönüştürme kutusunda hiç anahtar yoktu: her etiket,
|
|
1053
|
+
kimsenin geçirmediği isteğe bağlı bir `t` prop'unun arkasındaki düz
|
|
1054
|
+
Türkçe metindi. */
|
|
1055
|
+
'convert.title': 'Dönüştür',
|
|
1056
|
+
'convert.loading': 'Dönüştürücü yükleniyor…',
|
|
1057
|
+
'convert.done': 'Dönüştürüldü ve klasöre yüklendi.',
|
|
1058
|
+
'convert.close': 'Kapat',
|
|
1059
|
+
'convert.from': 'Kaynak',
|
|
1060
|
+
'convert.search_format': 'Hedef format ara (pdf, mp4, png…)',
|
|
1061
|
+
'convert.no_format': 'Eşleşen format yok.',
|
|
1062
|
+
'convert.unsupported_input': 'Bu dosya tipi için kaynak format bulunamadı.',
|
|
1063
|
+
'convert.converting': 'Dönüştürülüyor…',
|
|
1064
|
+
'convert.convert': 'Dönüştür',
|
|
1032
1065
|
};
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* 5. we wrap the bytes in a File and upload it to the current folder.
|
|
16
16
|
*/
|
|
17
17
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
18
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
19
|
+
import { useLocale } from '../composables/useLocale';
|
|
18
20
|
|
|
19
21
|
const props = defineProps<{
|
|
20
22
|
/** Converter base, e.g. https://fm.example.com/convert */
|
|
@@ -25,9 +27,11 @@ const props = defineProps<{
|
|
|
25
27
|
fetchBytes: () => Promise<ArrayBuffer>;
|
|
26
28
|
/** Upload the produced File into the current folder (api.uploadMultipart bound to dir). */
|
|
27
29
|
upload: (file: File) => Promise<void>;
|
|
28
|
-
|
|
30
|
+
locale: LocaleCode;
|
|
29
31
|
}>();
|
|
30
32
|
|
|
33
|
+
const { t } = useLocale(() => props.locale);
|
|
34
|
+
|
|
31
35
|
const emit = defineEmits<{ (e: 'close'): void; (e: 'done', name: string): void }>();
|
|
32
36
|
|
|
33
37
|
interface Fmt {
|
|
@@ -107,7 +111,7 @@ async function loadFormats() {
|
|
|
107
111
|
formats.value = res.formats || [];
|
|
108
112
|
status.value = 'ready';
|
|
109
113
|
if (!fromFmt.value) {
|
|
110
|
-
error.value =
|
|
114
|
+
error.value = t('convert.unsupported_input');
|
|
111
115
|
}
|
|
112
116
|
} catch (e) {
|
|
113
117
|
error.value = (e as Error).message;
|
|
@@ -139,8 +143,6 @@ async function doConvert() {
|
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
function tt(k: string, fb: string) { return props.t ? props.t(k, fb) : fb; }
|
|
143
|
-
|
|
144
146
|
onMounted(() => window.addEventListener('message', onMessage));
|
|
145
147
|
onBeforeUnmount(() => window.removeEventListener('message', onMessage));
|
|
146
148
|
</script>
|
|
@@ -149,27 +151,27 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
|
|
|
149
151
|
<div class="filex-cv__bg" @click.self="emit('close')">
|
|
150
152
|
<div class="filex-cv">
|
|
151
153
|
<header class="filex-cv__head">
|
|
152
|
-
<h3>{{
|
|
154
|
+
<h3>{{ t('convert.title') }} — {{ fileName }}</h3>
|
|
153
155
|
<button class="filex-cv__x" @click="emit('close')">✕</button>
|
|
154
156
|
</header>
|
|
155
157
|
|
|
156
158
|
<div v-if="status === 'loading'" class="filex-cv__msg">
|
|
157
|
-
{{
|
|
159
|
+
{{ t('convert.loading') }}
|
|
158
160
|
</div>
|
|
159
161
|
|
|
160
162
|
<div v-else-if="status === 'done'" class="filex-cv__msg filex-cv__ok">
|
|
161
|
-
<p>✓ {{
|
|
162
|
-
<button class="filex-cv__convert" @click="emit('close')">{{
|
|
163
|
+
<p>✓ {{ t('convert.done') }}</p>
|
|
164
|
+
<button class="filex-cv__convert" @click="emit('close')">{{ t('convert.close') }}</button>
|
|
163
165
|
</div>
|
|
164
166
|
|
|
165
167
|
<template v-else>
|
|
166
168
|
<p class="filex-cv__src">
|
|
167
|
-
{{
|
|
169
|
+
{{ t('convert.from') }}: <b>{{ srcExt || '?' }}</b>
|
|
168
170
|
</p>
|
|
169
171
|
<input
|
|
170
172
|
v-model="search"
|
|
171
173
|
class="filex-cv__search"
|
|
172
|
-
:placeholder="
|
|
174
|
+
:placeholder="t('convert.search_format')"
|
|
173
175
|
/>
|
|
174
176
|
<div class="filex-cv__list">
|
|
175
177
|
<button
|
|
@@ -182,7 +184,7 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
|
|
|
182
184
|
<small>{{ f.name }}</small>
|
|
183
185
|
</button>
|
|
184
186
|
<div v-if="toList.length === 0" class="filex-cv__empty">
|
|
185
|
-
{{
|
|
187
|
+
{{ t('convert.no_format') }}
|
|
186
188
|
</div>
|
|
187
189
|
</div>
|
|
188
190
|
<div v-if="error" class="filex-cv__err">{{ error }}</div>
|
|
@@ -192,7 +194,7 @@ onBeforeUnmount(() => window.removeEventListener('message', onMessage));
|
|
|
192
194
|
:disabled="!selectedTo || !fromFmt || status === 'converting'"
|
|
193
195
|
@click="doConvert"
|
|
194
196
|
>
|
|
195
|
-
{{ status === 'converting' ?
|
|
197
|
+
{{ status === 'converting' ? t('convert.converting') : t('convert.convert') }}
|
|
196
198
|
</button>
|
|
197
199
|
</footer>
|
|
198
200
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Access modal — one popup combining "
|
|
3
|
-
// owner-only) and "
|
|
4
|
-
// from the explorer's unified "
|
|
2
|
+
// Access modal — one popup combining "Permissions" (per-user RBAC grants,
|
|
3
|
+
// owner-only) and "Share by link" (public share link, editor+). Opened
|
|
4
|
+
// from the explorer's unified "Share / Permissions" action. The layout is a fixed
|
|
5
5
|
// header/tabs with a single scrollable body so the popup never grows into one
|
|
6
6
|
// long scroll. Styling uses the SFC's --fe-* theme variables (light/dark).
|
|
7
7
|
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
|
@@ -9,6 +9,7 @@ import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
|
|
|
9
9
|
import type { ShareInfo } from '../types/FileNode';
|
|
10
10
|
import { shareCliCommand } from '../lib/shareCli';
|
|
11
11
|
import { STOCK_EXPIRY_DAYS, clampExpiryOptions, defaultExpiryDays, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
|
|
12
|
+
import { resolveLocale } from '../locales/resolve';
|
|
12
13
|
|
|
13
14
|
const props = defineProps<{
|
|
14
15
|
api: FileApi;
|
|
@@ -36,7 +37,7 @@ const props = defineProps<{
|
|
|
36
37
|
}>();
|
|
37
38
|
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
38
39
|
|
|
39
|
-
const tr = computed(() => (props.locale
|
|
40
|
+
const tr = computed(() => (resolveLocale(props.locale)) !== 'en');
|
|
40
41
|
function L(t: string, e: string): string {
|
|
41
42
|
return tr.value ? t : e;
|
|
42
43
|
}
|
|
@@ -300,7 +301,7 @@ async function inviteCreateUser() {
|
|
|
300
301
|
const r = await props.api.invitePermission({
|
|
301
302
|
path: props.path, email: email.value.trim().toLowerCase(),
|
|
302
303
|
level: level.value, create_user: true, role: createRole.value, is_dir: !!props.isDir,
|
|
303
|
-
locale: props.locale
|
|
304
|
+
locale: resolveLocale(props.locale),
|
|
304
305
|
});
|
|
305
306
|
inviteResult.value = { tempPassword: r.temp_password };
|
|
306
307
|
notice.value = r.emailed
|
|
@@ -314,7 +315,7 @@ async function inviteCreateUser() {
|
|
|
314
315
|
busy.value = false;
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
|
-
// "
|
|
318
|
+
// "Just send a share link" → jump to the share tab with the address prefilled so the
|
|
318
319
|
// owner creates a link (with their chosen expiry/PIN) and mails it there.
|
|
319
320
|
function gotoShareWithMail() {
|
|
320
321
|
shareMailTo.value = email.value.trim().toLowerCase();
|
|
@@ -388,7 +389,7 @@ async function sendShareMail() {
|
|
|
388
389
|
path: props.path, emails: list, url: shareResult.value.url,
|
|
389
390
|
pin: shareResult.value.pin ?? undefined,
|
|
390
391
|
expires_days: shareExpiry.value || undefined,
|
|
391
|
-
locale: props.locale
|
|
392
|
+
locale: resolveLocale(props.locale),
|
|
392
393
|
is_dir: !!props.isDir,
|
|
393
394
|
size: props.size,
|
|
394
395
|
});
|
|
@@ -456,7 +457,7 @@ async function sendDropMail() {
|
|
|
456
457
|
path: props.path, emails: list, url: dropResult.value.url,
|
|
457
458
|
pin: dropResult.value.pin ?? undefined,
|
|
458
459
|
expires_days: dropExpiry.value || undefined,
|
|
459
|
-
locale: props.locale
|
|
460
|
+
locale: resolveLocale(props.locale),
|
|
460
461
|
is_dir: true,
|
|
461
462
|
mode: 'drop',
|
|
462
463
|
});
|
|
@@ -756,7 +757,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
756
757
|
</div>
|
|
757
758
|
<div v-if="shareMailNotice" class="fx-perm-notice">{{ shareMailNotice }}</div>
|
|
758
759
|
|
|
759
|
-
<!-- native share (OS share sheet) — same as the fishapp
|
|
760
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
760
761
|
<button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(shareBody())">
|
|
761
762
|
<span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
|
|
762
763
|
</button>
|
|
@@ -855,7 +856,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
855
856
|
</div>
|
|
856
857
|
<div v-if="dropMailNotice" class="fx-perm-notice">{{ dropMailNotice }}</div>
|
|
857
858
|
|
|
858
|
-
<!-- native share (OS share sheet) — same as the fishapp
|
|
859
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
859
860
|
<button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(dropBody())">
|
|
860
861
|
<span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
|
|
861
862
|
</button>
|
|
@@ -1026,7 +1027,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
1026
1027
|
.fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
|
|
1027
1028
|
.fx-perm-ttlhint { margin: 4px 0 8px; }
|
|
1028
1029
|
.fx-perm-validuntil { margin: 4px 0 6px; }
|
|
1029
|
-
/* native "
|
|
1030
|
+
/* native "Share" button — sits under the mail row, full width */
|
|
1030
1031
|
.fx-perm-sharebtn { display: flex; width: 100%; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; }
|
|
1031
1032
|
|
|
1032
1033
|
/* file-drop tab */
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* office (docx/xlsx/pptx) → OnlyOffice iframe (config.onlyOfficeBase)
|
|
13
13
|
* plain text (txt/log/conf) → CodeMirror when saveText is set,
|
|
14
14
|
* `<pre>` otherwise
|
|
15
|
-
* anything else → "
|
|
15
|
+
* anything else → "Download" fallback
|
|
16
16
|
*
|
|
17
17
|
* Monaco is dynamic-imported at FileExplorer onMounted; by the time the
|
|
18
18
|
* user clicks an editable code file the chunk is usually already cached.
|
|
@@ -46,7 +46,7 @@ const props = defineProps<{
|
|
|
46
46
|
pdfSaveUrl?: string | null;
|
|
47
47
|
/** Standalone full-screen viewer route — `?path=…&storage=…&type=…`. */
|
|
48
48
|
viewerBaseUrl?: string | null;
|
|
49
|
-
/* wiring:e2 — false hides the "
|
|
49
|
+
/* wiring:e2 — false hides the "Edit" / "Open in New Tab" buttons.
|
|
50
50
|
* The standalone route fetches RAW server bytes, which inside an
|
|
51
51
|
* E2E-encrypted folder is ciphertext (and its save path could clobber
|
|
52
52
|
* the encrypted blob) — the host disables the escape hatches there. */
|
|
@@ -258,7 +258,7 @@ function buildAndOpenStandalone(mode: 'view' | 'edit'): void {
|
|
|
258
258
|
|
|
259
259
|
/**
|
|
260
260
|
* Extensions that have a meaningful "edit" surface. Read-only kinds
|
|
261
|
-
* (image/video/audio/3D/archive) don't surface
|
|
261
|
+
* (image/video/audio/3D/archive) don't surface an "Edit" button.
|
|
262
262
|
*/
|
|
263
263
|
const EDITABLE_EXTS = new Set([
|
|
264
264
|
// OnlyOffice — open the in-page office editor (or new tab) with
|
|
@@ -311,7 +311,7 @@ function onMdInput() {
|
|
|
311
311
|
mdReRenderTimer = setTimeout(() => {
|
|
312
312
|
renderMarkdown(rawText.value);
|
|
313
313
|
}, 250);
|
|
314
|
-
// Autosave 1.5s after last keystroke — manual
|
|
314
|
+
// Autosave 1.5s after last keystroke — the manual Save button + Ctrl+S
|
|
315
315
|
// still work; saveMarkdown() guards against overlap.
|
|
316
316
|
if (mdAutosaveTimer) clearTimeout(mdAutosaveTimer);
|
|
317
317
|
mdAutosaveTimer = setTimeout(() => {
|
|
@@ -785,7 +785,7 @@ async function mountOnlyOfficeEditor(): Promise<void> {
|
|
|
785
785
|
officeError.value = null;
|
|
786
786
|
if (!props.file || kind.value !== 'office') return;
|
|
787
787
|
if (!props.onlyOfficeConfigEndpoint) {
|
|
788
|
-
officeError.value = '
|
|
788
|
+
officeError.value = t('viewer.office_unconfigured');
|
|
789
789
|
return;
|
|
790
790
|
}
|
|
791
791
|
if (!officeEl.value) return;
|
|
@@ -916,8 +916,8 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
|
|
|
916
916
|
<object :data="src" type="application/pdf" class="fe-preview__iframe">
|
|
917
917
|
<div class="fe-preview__fallback">
|
|
918
918
|
<span class="fe-preview__fallback-icon">📕</span>
|
|
919
|
-
<p>
|
|
920
|
-
<a :href="download" class="fe-btn fe-btn--primary" target="_blank" rel="noopener">
|
|
919
|
+
<p>{{ t('viewer.pdf_inline_failed') }}</p>
|
|
920
|
+
<a :href="download" class="fe-btn fe-btn--primary" target="_blank" rel="noopener">{{ t('viewer.open_in_new_tab') }}</a>
|
|
921
921
|
</div>
|
|
922
922
|
</object>
|
|
923
923
|
</template>
|
|
@@ -925,7 +925,7 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
|
|
|
925
925
|
<template v-else-if="kind === 'markdown'">
|
|
926
926
|
<div v-if="loading" class="fe-preview__fallback">{{ t('viewer.loading') }}</div>
|
|
927
927
|
<div v-else-if="tooLarge" class="fe-preview__fallback">
|
|
928
|
-
|
|
928
|
+
{{ t('viewer.too_large') }} <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
|
|
929
929
|
</div>
|
|
930
930
|
<div
|
|
931
931
|
v-else-if="openMode === 'edit' && saveTextEndpoint"
|
|
@@ -995,7 +995,7 @@ function loadOnlyOfficeScript(base: string): Promise<void> {
|
|
|
995
995
|
<template v-else-if="kind === 'code' || kind === 'text'">
|
|
996
996
|
<div v-if="loading" class="fe-preview__fallback">{{ t('viewer.loading') }}</div>
|
|
997
997
|
<div v-else-if="tooLarge" class="fe-preview__fallback">
|
|
998
|
-
|
|
998
|
+
{{ t('viewer.too_large') }} <a :href="download" class="fe-btn">{{ t('viewer.download') }}</a>
|
|
999
999
|
</div>
|
|
1000
1000
|
<div v-else-if="fetchError" class="fe-preview__fallback">
|
|
1001
1001
|
<p>{{ fetchError }}</p>
|