@brftech/filex-core 0.31.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/README.md +26 -2
- 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 +9346 -8069
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +49 -49
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +235 -16
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +776 -246
- package/src/components/CommandPalette.vue +21 -5
- package/src/components/ConnectionsPanel.vue +2 -1
- package/src/components/E2eRecoveryUnlockModal.vue +29 -4
- package/src/components/FilterBar.vue +244 -0
- package/src/components/GalleryView.vue +12 -1
- package/src/components/GridView.vue +48 -5
- package/src/components/InspectorPanel.vue +231 -8
- package/src/components/ListView.vue +12 -3
- 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 +28 -13
- package/src/components/SideNav.vue +161 -1
- 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 +244 -58
- package/src/components/ViewSwitcher.vue +81 -0
- package/src/composables/useFileApi.ts +49 -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 +8 -2
- package/src/lib/e2ecrypto.ts +191 -3
- package/src/lib/fileFilters.ts +143 -0
- package/src/lib/listing.ts +47 -0
- package/src/lib/themes.ts +7 -7
- package/src/lib/transfer.ts +6 -6
- package/src/locales/en.ts +115 -0
- package/src/locales/index.ts +1 -0
- package/src/locales/resolve.ts +66 -0
- package/src/locales/tr.ts +116 -0
- package/src/modals/ConvertModal.vue +14 -12
- package/src/modals/NewFolderModal.vue +1 -1
- package/src/modals/PermissionsModal.vue +30 -13
- package/src/modals/PreviewModal.vue +9 -9
- package/src/styles/base.css +520 -26
- package/src/styles/variables.css +1 -1
- package/src/types/ExplorerConfig.ts +18 -2
- 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/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',
|
|
@@ -467,6 +487,10 @@ export const en: Record<string, string> = {
|
|
|
467
487
|
'e2e.recover.escrow_warn_body':
|
|
468
488
|
'Opening a folder with the escrow key notifies its owner. Use it only when you are entitled to.',
|
|
469
489
|
'e2e.recover.escrow_kid': 'Escrow key',
|
|
490
|
+
'e2e.recover.escrow_predates':
|
|
491
|
+
'This installation has an escrow key, but this folder does not. It was created before escrow was turned on here, so its master key was never wrapped to the escrow identity. The escrow key will not open it. Nothing the operator can do changes that — adding a slot needs this folder\u2019s password, which the server has never had. The folder\u2019s owner can grant it: unlock with the password, then choose to give the operator a key. Until they do, use the password or the recovery key.',
|
|
492
|
+
'e2e.recover.escrow_other_key':
|
|
493
|
+
'This folder carries an escrow key from a DIFFERENT installation. It was created elsewhere and restored here, so this installation\u2019s escrow key does not open it. Only the private key belonging to the escrow key it was sealed to can.',
|
|
470
494
|
'e2e.recover.escrow_placeholder': 'Paste the escrow private key (PKCS#8, base64 or PEM)',
|
|
471
495
|
'e2e.recover.bad_format':
|
|
472
496
|
'That does not look like a recovery key. It is 32 characters in eight groups of four.',
|
|
@@ -481,6 +505,25 @@ export const en: Record<string, string> = {
|
|
|
481
505
|
'e2e.recover.escrow_done': 'Unlocked with the escrow key — the owner has been notified',
|
|
482
506
|
'e2e.recover.unlock': 'Unlock',
|
|
483
507
|
'e2e.recover.busy': 'Unlocking…',
|
|
508
|
+
'e2e.escrowoffer.title': 'Give the operator a key to this folder?',
|
|
509
|
+
'e2e.escrowoffer.body':
|
|
510
|
+
'This folder was created before key escrow was turned on here, so the operator of this installation cannot open it. Right now — while your password is in memory — filex can seal this folder to the escrow key. Your files are not re-encrypted or moved; only the key file changes.',
|
|
511
|
+
'e2e.escrowoffer.consequence':
|
|
512
|
+
'What this means: the operator gains a second, permanent way into this folder, without your password. You are notified when that key is used, but that notification is an announcement rather than a control.',
|
|
513
|
+
'e2e.escrowoffer.learn_more': 'What escrow can and cannot do',
|
|
514
|
+
'e2e.escrowoffer.password_label': 'Folder password',
|
|
515
|
+
'e2e.escrowoffer.password_required': 'Enter the folder password.',
|
|
516
|
+
'e2e.escrowoffer.wrong_password': 'That is not this folder\u2019s password.',
|
|
517
|
+
'e2e.escrowoffer.accept': 'Give the operator a key',
|
|
518
|
+
'e2e.escrowoffer.decline': 'No, keep it to myself',
|
|
519
|
+
'e2e.escrowoffer.cancel': 'Cancel',
|
|
520
|
+
'e2e.escrowoffer.busy': 'Sealing…',
|
|
521
|
+
'e2e.escrowoffer.done': 'The operator can now open this folder with the escrow key',
|
|
522
|
+
'e2e.escrowoffer.declined_toast': 'Kept to yourself. filex will not ask again for this folder.',
|
|
523
|
+
'e2e.escrowoffer.decline_failed':
|
|
524
|
+
'Could not record your answer, so filex will ask again next time. Nothing about the folder changed.',
|
|
525
|
+
'e2e.escrowoffer.failed': 'Could not give the operator a key to this folder',
|
|
526
|
+
'e2e.escrowoffer.strip_action': 'Escrow key…',
|
|
484
527
|
'e2e.upgrade.title': 'This folder has no recovery key',
|
|
485
528
|
'e2e.upgrade.body':
|
|
486
529
|
'It was created before recovery keys existed, so its password is the only way in. Right now — and only right now, while the password is in memory — filex can add one. Your files are not re-encrypted or moved.',
|
|
@@ -943,4 +986,76 @@ export const en: Record<string, string> = {
|
|
|
943
986
|
'conn.tokens.expiryNever': 'never',
|
|
944
987
|
'conn.tokens.capNote': 'Your account and its permissions are the ceiling — asking for more than you have is refused, not granted.',
|
|
945
988
|
/* === /gezinti:g1 === */
|
|
989
|
+
|
|
990
|
+
/* === surucu:d1 — the Drive shell (uiProfile: 'drive', GitHub #14) ===== */
|
|
991
|
+
'drive.new': 'New',
|
|
992
|
+
'drive.new.upload': 'Upload files',
|
|
993
|
+
'drive.new.folder': 'New folder',
|
|
994
|
+
'drive.new.request': 'Request files',
|
|
995
|
+
'drive.search.placeholder': 'Search in {scope}',
|
|
996
|
+
'drive.search.placeholder_all': 'Search your files',
|
|
997
|
+
'drive.search.hint_title': 'Search everywhere and run commands ({combo})',
|
|
998
|
+
'drive.storage.used': '{used} of {total} used',
|
|
999
|
+
'drive.storage.used_unlimited': '{used} used',
|
|
1000
|
+
'drive.storage.label': 'Storage',
|
|
1001
|
+
'drive.section.folders': 'Folders',
|
|
1002
|
+
'drive.section.files': 'Files',
|
|
1003
|
+
'filter.aria': 'Filters',
|
|
1004
|
+
'filter.type': 'Type',
|
|
1005
|
+
'filter.type.any': 'Any type',
|
|
1006
|
+
'filter.type.folder': 'Folders',
|
|
1007
|
+
'filter.type.document': 'Documents',
|
|
1008
|
+
'filter.type.spreadsheet': 'Spreadsheets',
|
|
1009
|
+
'filter.type.presentation': 'Presentations',
|
|
1010
|
+
'filter.type.pdf': 'PDFs',
|
|
1011
|
+
'filter.type.image': 'Images',
|
|
1012
|
+
'filter.type.video': 'Videos',
|
|
1013
|
+
'filter.type.audio': 'Audio',
|
|
1014
|
+
'filter.type.archive': 'Archives',
|
|
1015
|
+
'filter.type.code': 'Code',
|
|
1016
|
+
'filter.modified': 'Modified',
|
|
1017
|
+
'filter.modified.any': 'Any time',
|
|
1018
|
+
'filter.modified.today': 'Today',
|
|
1019
|
+
'filter.modified.7d': 'Last 7 days',
|
|
1020
|
+
'filter.modified.30d': 'Last 30 days',
|
|
1021
|
+
'filter.modified.year': 'This year',
|
|
1022
|
+
'filter.size': 'Size',
|
|
1023
|
+
'filter.size.any': 'Any size',
|
|
1024
|
+
'filter.size.lt1': 'Under 1 MB',
|
|
1025
|
+
'filter.size.1to10': '1 - 10 MB',
|
|
1026
|
+
'filter.size.10to100': '10 - 100 MB',
|
|
1027
|
+
'filter.size.gt100': 'Over 100 MB',
|
|
1028
|
+
'filter.clear': 'Clear filters',
|
|
1029
|
+
'filter.empty.title': 'Nothing here matches those filters',
|
|
1030
|
+
'filter.empty.hint': 'Clear a filter, or search everywhere with the field above.',
|
|
1031
|
+
'filter.count': '{shown} of {total}',
|
|
1032
|
+
'inspector.tab.details': 'Details',
|
|
1033
|
+
'inspector.tab.activity': 'Activity',
|
|
1034
|
+
'inspector.people': 'People with access',
|
|
1035
|
+
'inspector.people.you': 'You',
|
|
1036
|
+
'inspector.people.inherited': 'from a parent folder',
|
|
1037
|
+
'inspector.people.empty': 'Only you, and anyone with a link below.',
|
|
1038
|
+
'inspector.people.manage': 'Manage access',
|
|
1039
|
+
'inspector.link': 'Shared link',
|
|
1040
|
+
'inspector.link.none': 'Not shared',
|
|
1041
|
+
'inspector.link.create': 'Create link',
|
|
1042
|
+
'inspector.link.copy': 'Copy link',
|
|
1043
|
+
'inspector.activity.empty': 'No activity recorded for this item yet.',
|
|
1044
|
+
'inspector.activity.hint': 'Versions are written when a file changes; comments are written by people.',
|
|
1045
|
+
'inspector.activity.version': 'Version {n} saved',
|
|
1046
|
+
'inspector.activity.comment': 'Comment by {who}',
|
|
1047
|
+
'inspector.activity.select': 'Select a file or folder to see its activity.',
|
|
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',
|
|
946
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',
|
|
@@ -467,6 +487,10 @@ export const tr: Record<string, string> = {
|
|
|
467
487
|
'e2e.recover.escrow_warn_body':
|
|
468
488
|
'Emanet anahtarıyla açılan klasörün sahibine bildirim gönderilir. Yalnız yetkiniz olduğunda kullanın.',
|
|
469
489
|
'e2e.recover.escrow_kid': 'Emanet anahtarı',
|
|
490
|
+
'e2e.recover.escrow_predates':
|
|
491
|
+
'Bu kurulumun emanet anahtarı var ama bu klasörün yok. Klasör, emanet burada açılmadan önce oluşturulmuş; ana anahtarı emanet kimliğine hiç sarmalanmadı. Emanet anahtarı bu klasörü açmaz. İşletmecinin yapabileceği hiçbir şey bunu değiştirmez — sarmalama için klasörün parolası gerekir ve sunucu onu hiçbir zaman görmedi. Klasörün sahibi verebilir: parolayla kilidi açıp işletmeciye anahtar vermeyi seçebilir. O yapılana kadar parolayı ya da kurtarma anahtarını kullanın.',
|
|
492
|
+
'e2e.recover.escrow_other_key':
|
|
493
|
+
'Bu klasör BAŞKA bir kurulumun emanet anahtarını taşıyor. Başka yerde oluşturulup buraya geri yüklenmiş, bu yüzden bu kurulumun emanet anahtarı onu açmaz. Yalnızca sarmalandığı emanet anahtarının özel yarısı açabilir.',
|
|
470
494
|
'e2e.recover.escrow_placeholder': 'Emanet özel anahtarını yapıştırın (PKCS#8, base64 ya da PEM)',
|
|
471
495
|
'e2e.recover.bad_format':
|
|
472
496
|
'Bu bir kurtarma anahtarına benzemiyor. Dörtlü sekiz grup, toplam 32 karakter olmalı.',
|
|
@@ -481,6 +505,25 @@ export const tr: Record<string, string> = {
|
|
|
481
505
|
'e2e.recover.escrow_done': 'Emanet anahtarıyla açıldı — klasör sahibine bildirildi',
|
|
482
506
|
'e2e.recover.unlock': 'Kilidi aç',
|
|
483
507
|
'e2e.recover.busy': 'Açılıyor…',
|
|
508
|
+
'e2e.escrowoffer.title': 'İşletmeciye bu klasörün anahtarı verilsin mi?',
|
|
509
|
+
'e2e.escrowoffer.body':
|
|
510
|
+
'Bu klasör, burada anahtar emaneti açılmadan önce oluşturulmuş; bu yüzden kurulumun işletmecisi onu açamıyor. Şu anda — parolanız bellekteyken — filex bu klasörü emanet anahtarına sarmalayabilir. Dosyalarınız yeniden şifrelenmez, hiçbir yere taşınmaz; yalnızca anahtar dosyası değişir.',
|
|
511
|
+
'e2e.escrowoffer.consequence':
|
|
512
|
+
'Bunun anlamı: işletmeci, parolanız olmadan bu klasöre giren ikinci ve kalıcı bir yol kazanır. O anahtar kullanıldığında size bildirim gelir, ama bu bildirim bir denetim değil yalnızca bir duyurudur.',
|
|
513
|
+
'e2e.escrowoffer.learn_more': 'Emanet anahtarı neyi yapar, neyi yapamaz',
|
|
514
|
+
'e2e.escrowoffer.password_label': 'Klasör parolası',
|
|
515
|
+
'e2e.escrowoffer.password_required': 'Klasör parolasını girin.',
|
|
516
|
+
'e2e.escrowoffer.wrong_password': 'Bu, bu klasörün parolası değil.',
|
|
517
|
+
'e2e.escrowoffer.accept': 'İşletmeciye anahtar ver',
|
|
518
|
+
'e2e.escrowoffer.decline': 'Hayır, bende kalsın',
|
|
519
|
+
'e2e.escrowoffer.cancel': 'Vazgeç',
|
|
520
|
+
'e2e.escrowoffer.busy': 'Sarmalanıyor…',
|
|
521
|
+
'e2e.escrowoffer.done': 'İşletmeci artık bu klasörü emanet anahtarıyla açabilir',
|
|
522
|
+
'e2e.escrowoffer.declined_toast': 'Sizde kaldı. filex bu klasör için bir daha sormayacak.',
|
|
523
|
+
'e2e.escrowoffer.decline_failed':
|
|
524
|
+
'Cevabınız kaydedilemedi, bu yüzden filex bir dahakine yine soracak. Klasörde hiçbir şey değişmedi.',
|
|
525
|
+
'e2e.escrowoffer.failed': 'İşletmeciye bu klasörün anahtarı verilemedi',
|
|
526
|
+
'e2e.escrowoffer.strip_action': 'Emanet anahtarı…',
|
|
484
527
|
'e2e.upgrade.title': 'Bu klasörün kurtarma anahtarı yok',
|
|
485
528
|
'e2e.upgrade.body':
|
|
486
529
|
'Kurtarma anahtarları eklenmeden önce oluşturulmuş, bu yüzden tek giriş yolu parolası. Şu anda — ve yalnız şu anda, parola bellekteyken — filex bir tane ekleyebilir. Dosyalarınız yeniden şifrelenmez, hiçbir yere taşınmaz.',
|
|
@@ -946,4 +989,77 @@ export const tr: Record<string, string> = {
|
|
|
946
989
|
'conn.tokens.expiryNever': 'süresiz',
|
|
947
990
|
'conn.tokens.capNote': 'Tavan hesabın ve yetkilerin — sahip olduğundan fazlasını istemek reddedilir, verilmez.',
|
|
948
991
|
/* === /gezinti:g1 === */
|
|
992
|
+
|
|
993
|
+
/* === surucu:d1 — Sürücü kabuğu (uiProfile: 'drive', GitHub #14) ======== */
|
|
994
|
+
'drive.new': 'Yeni',
|
|
995
|
+
'drive.new.upload': 'Dosya yükle',
|
|
996
|
+
'drive.new.folder': 'Yeni klasör',
|
|
997
|
+
'drive.new.request': 'Dosya iste',
|
|
998
|
+
'drive.search.placeholder': '{scope} içinde ara',
|
|
999
|
+
'drive.search.placeholder_all': 'Dosyalarında ara',
|
|
1000
|
+
'drive.search.hint_title': 'Her yerde ara ve komut çalıştır ({combo})',
|
|
1001
|
+
'drive.storage.used': '{total} alanın {used} kadarı dolu',
|
|
1002
|
+
'drive.storage.used_unlimited': '{used} kullanılıyor',
|
|
1003
|
+
'drive.storage.label': 'Depolama',
|
|
1004
|
+
'drive.section.folders': 'Klasörler',
|
|
1005
|
+
'drive.section.files': 'Dosyalar',
|
|
1006
|
+
'filter.aria': 'Süzgeçler',
|
|
1007
|
+
'filter.type': 'Tür',
|
|
1008
|
+
'filter.type.any': 'Her tür',
|
|
1009
|
+
'filter.type.folder': 'Klasörler',
|
|
1010
|
+
'filter.type.document': 'Belgeler',
|
|
1011
|
+
'filter.type.spreadsheet': 'Hesap tabloları',
|
|
1012
|
+
'filter.type.presentation': 'Sunumlar',
|
|
1013
|
+
'filter.type.pdf': 'PDF’ler',
|
|
1014
|
+
'filter.type.image': 'Görseller',
|
|
1015
|
+
'filter.type.video': 'Videolar',
|
|
1016
|
+
'filter.type.audio': 'Ses dosyaları',
|
|
1017
|
+
'filter.type.archive': 'Arşivler',
|
|
1018
|
+
'filter.type.code': 'Kod',
|
|
1019
|
+
'filter.modified': 'Değiştirilme',
|
|
1020
|
+
'filter.modified.any': 'Her zaman',
|
|
1021
|
+
'filter.modified.today': 'Bugün',
|
|
1022
|
+
'filter.modified.7d': 'Son 7 gün',
|
|
1023
|
+
'filter.modified.30d': 'Son 30 gün',
|
|
1024
|
+
'filter.modified.year': 'Bu yıl',
|
|
1025
|
+
'filter.size': 'Boyut',
|
|
1026
|
+
'filter.size.any': 'Her boyut',
|
|
1027
|
+
'filter.size.lt1': '1 MB altı',
|
|
1028
|
+
'filter.size.1to10': '1 - 10 MB',
|
|
1029
|
+
'filter.size.10to100': '10 - 100 MB',
|
|
1030
|
+
'filter.size.gt100': '100 MB üstü',
|
|
1031
|
+
'filter.clear': 'Süzgeçleri temizle',
|
|
1032
|
+
'filter.empty.title': 'Bu süzgeçlere uyan bir şey yok',
|
|
1033
|
+
'filter.empty.hint': 'Bir süzgeci kaldır ya da yukarıdaki alandan her yerde ara.',
|
|
1034
|
+
'filter.count': '{total} ögeden {shown} tanesi',
|
|
1035
|
+
'inspector.tab.details': 'Ayrıntılar',
|
|
1036
|
+
'inspector.tab.activity': 'Etkinlik',
|
|
1037
|
+
'inspector.people': 'Erişimi olan kişiler',
|
|
1038
|
+
'inspector.people.you': 'Sen',
|
|
1039
|
+
'inspector.people.inherited': 'üst klasörden',
|
|
1040
|
+
'inspector.people.empty': 'Yalnızca sen ve aşağıdaki bağlantıya sahip olanlar.',
|
|
1041
|
+
'inspector.people.manage': 'Erişimi yönet',
|
|
1042
|
+
'inspector.link': 'Paylaşım bağlantısı',
|
|
1043
|
+
'inspector.link.none': 'Paylaşılmadı',
|
|
1044
|
+
'inspector.link.create': 'Bağlantı oluştur',
|
|
1045
|
+
'inspector.link.copy': 'Bağlantıyı kopyala',
|
|
1046
|
+
'inspector.activity.empty': 'Bu öge için henüz etkinlik kaydı yok.',
|
|
1047
|
+
'inspector.activity.hint': 'Sürümler dosya değiştikçe, yorumlar ise insanlar yazdıkça birikir.',
|
|
1048
|
+
'inspector.activity.version': '{n}. sürüm kaydedildi',
|
|
1049
|
+
'inspector.activity.comment': '{who} yorum yaptı',
|
|
1050
|
+
'inspector.activity.select': 'Etkinliğini görmek için bir dosya ya da klasör seç.',
|
|
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',
|
|
949
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;
|
|
@@ -19,10 +20,24 @@ const props = defineProps<{
|
|
|
19
20
|
/** Server ceiling on a new link's life in days (capabilities.share_max_ttl_days).
|
|
20
21
|
* undefined/0 = no ceiling. The expiry choices are derived from it. */
|
|
21
22
|
shareMaxTtlDays?: number;
|
|
23
|
+
/**
|
|
24
|
+
* surucu:d1 — which tab this modal opens on. Absent = 'perms', the behaviour
|
|
25
|
+
* every existing caller has.
|
|
26
|
+
*
|
|
27
|
+
* The drive shell's "Request files" needs 'drop': the file-drop link already
|
|
28
|
+
* lives in this modal, and a menu entry that opens it on the permissions tab
|
|
29
|
+
* and leaves the user to find the third tab is a menu entry that lied about
|
|
30
|
+
* what it does.
|
|
31
|
+
*
|
|
32
|
+
* ⚠ A hint, not a lock: `load()` still falls back to 'share' when the caller
|
|
33
|
+
* turns out not to be an owner, because the permissions tab is not theirs to
|
|
34
|
+
* see.
|
|
35
|
+
*/
|
|
36
|
+
initialTab?: 'perms' | 'share' | 'drop';
|
|
22
37
|
}>();
|
|
23
38
|
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
24
39
|
|
|
25
|
-
const tr = computed(() => (props.locale
|
|
40
|
+
const tr = computed(() => (resolveLocale(props.locale)) !== 'en');
|
|
26
41
|
function L(t: string, e: string): string {
|
|
27
42
|
return tr.value ? t : e;
|
|
28
43
|
}
|
|
@@ -37,7 +52,7 @@ const pathParts = computed(() => {
|
|
|
37
52
|
});
|
|
38
53
|
|
|
39
54
|
type Tab = 'perms' | 'share' | 'drop';
|
|
40
|
-
const tab = ref<Tab>('perms');
|
|
55
|
+
const tab = ref<Tab>(props.initialTab ?? 'perms');
|
|
41
56
|
const canManage = ref(false); // owner/admin → can see the permissions tab
|
|
42
57
|
|
|
43
58
|
// ── permissions state ──
|
|
@@ -185,7 +200,9 @@ async function reload() {
|
|
|
185
200
|
if (st === 403) {
|
|
186
201
|
canManage.value = false;
|
|
187
202
|
// Editor (not owner): no permissions tab → fall back to the link tab.
|
|
188
|
-
|
|
203
|
+
// ⚠ Unless the caller asked for a specific one: "Request files" opens
|
|
204
|
+
// 'drop', which an editor may perfectly well use.
|
|
205
|
+
if (!props.initialTab || props.initialTab === 'perms') tab.value = 'share';
|
|
189
206
|
} else {
|
|
190
207
|
err.value = e instanceof Error ? e.message : String(e);
|
|
191
208
|
}
|
|
@@ -284,7 +301,7 @@ async function inviteCreateUser() {
|
|
|
284
301
|
const r = await props.api.invitePermission({
|
|
285
302
|
path: props.path, email: email.value.trim().toLowerCase(),
|
|
286
303
|
level: level.value, create_user: true, role: createRole.value, is_dir: !!props.isDir,
|
|
287
|
-
locale: props.locale
|
|
304
|
+
locale: resolveLocale(props.locale),
|
|
288
305
|
});
|
|
289
306
|
inviteResult.value = { tempPassword: r.temp_password };
|
|
290
307
|
notice.value = r.emailed
|
|
@@ -298,7 +315,7 @@ async function inviteCreateUser() {
|
|
|
298
315
|
busy.value = false;
|
|
299
316
|
}
|
|
300
317
|
}
|
|
301
|
-
// "
|
|
318
|
+
// "Just send a share link" → jump to the share tab with the address prefilled so the
|
|
302
319
|
// owner creates a link (with their chosen expiry/PIN) and mails it there.
|
|
303
320
|
function gotoShareWithMail() {
|
|
304
321
|
shareMailTo.value = email.value.trim().toLowerCase();
|
|
@@ -372,7 +389,7 @@ async function sendShareMail() {
|
|
|
372
389
|
path: props.path, emails: list, url: shareResult.value.url,
|
|
373
390
|
pin: shareResult.value.pin ?? undefined,
|
|
374
391
|
expires_days: shareExpiry.value || undefined,
|
|
375
|
-
locale: props.locale
|
|
392
|
+
locale: resolveLocale(props.locale),
|
|
376
393
|
is_dir: !!props.isDir,
|
|
377
394
|
size: props.size,
|
|
378
395
|
});
|
|
@@ -440,7 +457,7 @@ async function sendDropMail() {
|
|
|
440
457
|
path: props.path, emails: list, url: dropResult.value.url,
|
|
441
458
|
pin: dropResult.value.pin ?? undefined,
|
|
442
459
|
expires_days: dropExpiry.value || undefined,
|
|
443
|
-
locale: props.locale
|
|
460
|
+
locale: resolveLocale(props.locale),
|
|
444
461
|
is_dir: true,
|
|
445
462
|
mode: 'drop',
|
|
446
463
|
});
|
|
@@ -740,7 +757,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
740
757
|
</div>
|
|
741
758
|
<div v-if="shareMailNotice" class="fx-perm-notice">{{ shareMailNotice }}</div>
|
|
742
759
|
|
|
743
|
-
<!-- native share (OS share sheet) — same as the fishapp
|
|
760
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
744
761
|
<button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(shareBody())">
|
|
745
762
|
<span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
|
|
746
763
|
</button>
|
|
@@ -839,7 +856,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
839
856
|
</div>
|
|
840
857
|
<div v-if="dropMailNotice" class="fx-perm-notice">{{ dropMailNotice }}</div>
|
|
841
858
|
|
|
842
|
-
<!-- native share (OS share sheet) — same as the fishapp
|
|
859
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
843
860
|
<button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(dropBody())">
|
|
844
861
|
<span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
|
|
845
862
|
</button>
|
|
@@ -1010,7 +1027,7 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
1010
1027
|
.fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
|
|
1011
1028
|
.fx-perm-ttlhint { margin: 4px 0 8px; }
|
|
1012
1029
|
.fx-perm-validuntil { margin: 4px 0 6px; }
|
|
1013
|
-
/* native "
|
|
1030
|
+
/* native "Share" button — sits under the mail row, full width */
|
|
1014
1031
|
.fx-perm-sharebtn { display: flex; width: 100%; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; }
|
|
1015
1032
|
|
|
1016
1033
|
/* file-drop tab */
|