@brftech/filex-core 0.5.0 → 0.7.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-DKW-HyXT.js → ArchiveViewer-w6GDJJt9.js} +2 -2
- package/dist/{ArchiveViewer-DKW-HyXT.js.map → ArchiveViewer-w6GDJJt9.js.map} +1 -1
- package/dist/{CsvViewer-D2HmP2yr.js → CsvViewer-Ci5-mke8.js} +2 -2
- package/dist/{CsvViewer-D2HmP2yr.js.map → CsvViewer-Ci5-mke8.js.map} +1 -1
- package/dist/{DrawioViewer-DPNvOFCs.js → DrawioViewer-D2iMuQfD.js} +2 -2
- package/dist/{DrawioViewer-DPNvOFCs.js.map → DrawioViewer-D2iMuQfD.js.map} +1 -1
- package/dist/{EpubViewer-ByqjNuNL.js → EpubViewer-D4ydsa9X.js} +2 -2
- package/dist/{EpubViewer-ByqjNuNL.js.map → EpubViewer-D4ydsa9X.js.map} +1 -1
- package/dist/{IpynbViewer-2iONhbuJ.js → IpynbViewer-CWBwiFKX.js} +2 -2
- package/dist/{IpynbViewer-2iONhbuJ.js.map → IpynbViewer-CWBwiFKX.js.map} +1 -1
- package/dist/{MermaidViewer-CmiU7hIT.js → MermaidViewer-CDhuqQli.js} +2 -2
- package/dist/{MermaidViewer-CmiU7hIT.js.map → MermaidViewer-CDhuqQli.js.map} +1 -1
- package/dist/{PsdViewer-Inhl2yN9.js → PsdViewer-C_TkFb2H.js} +2 -2
- package/dist/{PsdViewer-Inhl2yN9.js.map → PsdViewer-C_TkFb2H.js.map} +1 -1
- package/dist/{TiffViewer-C2AFFEze.js → TiffViewer-BmWCXR7e.js} +2 -2
- package/dist/{TiffViewer-C2AFFEze.js.map → TiffViewer-BmWCXR7e.js.map} +1 -1
- package/dist/{Viewer3D-YdW14q_n.js → Viewer3D-5kPT1QsY.js} +2 -2
- package/dist/{Viewer3D-YdW14q_n.js.map → Viewer3D-5kPT1QsY.js.map} +1 -1
- package/dist/filex-core.js +66 -47
- package/dist/filex-core.umd.cjs +41 -40
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-nc0-oI_l.js +11723 -0
- package/dist/index-nc0-oI_l.js.map +1 -0
- package/dist/index.d.ts +364 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +756 -23
- package/src/components/CommandPalette.vue +12 -1
- package/src/components/EncryptedFolderModal.vue +125 -0
- package/src/components/GalleryView.vue +215 -0
- package/src/components/GridView.vue +2 -0
- package/src/components/InspectorPanel.vue +170 -0
- package/src/components/ListView.vue +2 -0
- package/src/components/SecondaryPane.vue +411 -0
- package/src/components/TabBar.vue +174 -0
- package/src/components/Toolbar.vue +23 -5
- package/src/composables/useFileApi.ts +60 -0
- package/src/composables/useKeyboardShortcuts.ts +19 -0
- package/src/composables/useTabs.ts +222 -0
- package/src/index.ts +27 -0
- package/src/lib/e2ecrypto.ts +299 -0
- package/src/locales/en.ts +67 -0
- package/src/locales/tr.ts +67 -0
- package/src/modals/NewFolderModal.vue +18 -0
- package/src/modals/PreviewModal.vue +7 -2
- package/src/styles/base.css +631 -0
- package/src/types/FileNode.ts +4 -1
- package/dist/index-eARevpz0.js +0 -9952
- package/dist/index-eARevpz0.js.map +0 -1
package/src/FileExplorer.vue
CHANGED
|
@@ -42,6 +42,7 @@ import RecentlyOpened from './components/RecentlyOpened.vue';
|
|
|
42
42
|
import Breadcrumb from './components/Breadcrumb.vue';
|
|
43
43
|
import ListView from './components/ListView.vue';
|
|
44
44
|
import GridView from './components/GridView.vue';
|
|
45
|
+
import GalleryView from './components/GalleryView.vue'; /* wiring:d2 */
|
|
45
46
|
import ContextMenu, { type ContextAction } from './components/ContextMenu.vue';
|
|
46
47
|
import UploadProgress from './components/UploadProgress.vue';
|
|
47
48
|
import PendingOpsTray from './components/PendingOpsTray.vue';
|
|
@@ -65,6 +66,25 @@ import { useOperations } from './composables/useOperations';
|
|
|
65
66
|
/* wiring:c4 */
|
|
66
67
|
import OnboardingTour from './components/OnboardingTour.vue';
|
|
67
68
|
/* /wiring:c4 */
|
|
69
|
+
/* wiring:d1 — sekmeler + tab başına split */
|
|
70
|
+
import TabBar from './components/TabBar.vue';
|
|
71
|
+
import SecondaryPane from './components/SecondaryPane.vue';
|
|
72
|
+
import { useTabs, type TabState } from './composables/useTabs';
|
|
73
|
+
/* /wiring:d1 */
|
|
74
|
+
/* wiring:e2 — uçtan uca şifreli klasörler (docs/E2E-ENCRYPTION.md) */
|
|
75
|
+
import EncryptedFolderModal from './components/EncryptedFolderModal.vue';
|
|
76
|
+
import {
|
|
77
|
+
createKeyRing,
|
|
78
|
+
createMarker,
|
|
79
|
+
parseMarker,
|
|
80
|
+
verifyPassword,
|
|
81
|
+
encryptFile,
|
|
82
|
+
decryptFile,
|
|
83
|
+
hasMagic,
|
|
84
|
+
E2E_MARKER_NAME,
|
|
85
|
+
E2E_MAX_FILE_BYTES,
|
|
86
|
+
} from './lib/e2ecrypto';
|
|
87
|
+
/* /wiring:e2 */
|
|
68
88
|
|
|
69
89
|
import NewFolderModal from './modals/NewFolderModal.vue';
|
|
70
90
|
import RenameModal from './modals/RenameModal.vue';
|
|
@@ -167,6 +187,7 @@ const pendingOps = usePendingOps(props.config, api, {
|
|
|
167
187
|
flashToast(`${verb} (${op.progress_total})`);
|
|
168
188
|
}
|
|
169
189
|
void load();
|
|
190
|
+
void splitPaneRef.value?.reload(); /* wiring:d1 — ikincil panel de tazelenir */
|
|
170
191
|
},
|
|
171
192
|
});
|
|
172
193
|
|
|
@@ -206,7 +227,7 @@ const viewMode = customRef<ViewMode>((track, trigger) => {
|
|
|
206
227
|
let value: ViewMode = (() => {
|
|
207
228
|
try {
|
|
208
229
|
const stored = localStorage.getItem(VIEW_MODE_KEY);
|
|
209
|
-
if (stored === 'list' || stored === 'grid') return stored;
|
|
230
|
+
if (stored === 'list' || stored === 'grid' || stored === 'gallery') return stored; /* wiring:d2 */
|
|
210
231
|
} catch {
|
|
211
232
|
/* private mode */
|
|
212
233
|
}
|
|
@@ -664,6 +685,7 @@ async function load(path?: string) {
|
|
|
664
685
|
currentPath.value = '';
|
|
665
686
|
adapter.value = '';
|
|
666
687
|
dirname.value = '';
|
|
688
|
+
e2eRoot.value = ''; /* wiring:e2 — sanal kökte kilit ekranı olmaz */
|
|
667
689
|
files.value = virtualStorageRows();
|
|
668
690
|
return;
|
|
669
691
|
}
|
|
@@ -678,11 +700,16 @@ async function load(path?: string) {
|
|
|
678
700
|
adapter.value = resp.adapter;
|
|
679
701
|
dirname.value = resp.dirname;
|
|
680
702
|
dirPerm.value = (resp.perm as string) || '';
|
|
703
|
+
/* wiring:e2 — backend tells us when this dir sits inside an encrypted
|
|
704
|
+
subtree; '' resets on every plain folder. Drives the lock screen. */
|
|
705
|
+
e2eRoot.value = typeof resp.e2e_root === 'string' ? resp.e2e_root : '';
|
|
706
|
+
/* /wiring:e2 */
|
|
681
707
|
files.value = (resp.files || []).filter((f) => {
|
|
682
708
|
if (f.path.includes('.thumbs')) return false;
|
|
683
709
|
if (f.path.includes('.versions') || f.basename === '.versions') return false;
|
|
684
710
|
if (f.basename === '.trash') return false;
|
|
685
711
|
if (f.basename === '.keepdir') return false;
|
|
712
|
+
if (f.basename === E2E_MARKER_NAME) return false; /* wiring:e2 — marker gizli */
|
|
686
713
|
return true;
|
|
687
714
|
});
|
|
688
715
|
// Inject virtual `.trash` entry at root only.
|
|
@@ -718,6 +745,7 @@ async function load(path?: string) {
|
|
|
718
745
|
// show the dedicated not-found state instead of a toast over a stale
|
|
719
746
|
// listing that reads as "this folder is empty".
|
|
720
747
|
notFoundPath.value = String(requested);
|
|
748
|
+
e2eRoot.value = ''; /* wiring:e2 — ölü linkte kilit ekranı kalmasın */
|
|
721
749
|
files.value = [];
|
|
722
750
|
emit('error', { message: e, context: { path } });
|
|
723
751
|
return;
|
|
@@ -784,6 +812,7 @@ async function loadTrash() {
|
|
|
784
812
|
loading.value = true;
|
|
785
813
|
trashOrigin.value = adapter.value || '';
|
|
786
814
|
trashMode.value = true;
|
|
815
|
+
e2eRoot.value = ''; /* wiring:e2 — çöp görünümü şifreli bağlamın dışındadır */
|
|
787
816
|
selection.clear();
|
|
788
817
|
try {
|
|
789
818
|
const { entries } = await api.listTrash();
|
|
@@ -1084,8 +1113,9 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1084
1113
|
showRename.value = true;
|
|
1085
1114
|
}
|
|
1086
1115
|
},
|
|
1087
|
-
onSelectAll: () => selection.selectAll()
|
|
1116
|
+
onSelectAll: () => (paneIsActive.value ? splitPaneRef.value?.selectAll() : selection.selectAll()) /* wiring:d1 pane-route */,
|
|
1088
1117
|
onOpen: () => {
|
|
1118
|
+
if (paneIsActive.value) return splitPaneRef.value?.openSelected(); /* wiring:d1 pane-route */
|
|
1089
1119
|
const n = selection.nodes.value[0];
|
|
1090
1120
|
if (n) openNode(n);
|
|
1091
1121
|
},
|
|
@@ -1102,10 +1132,10 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1102
1132
|
if (isNarrow.value) closeInspector();
|
|
1103
1133
|
},
|
|
1104
1134
|
onFocusSearch: () => toolbarRef.value?.focusSearch(),
|
|
1105
|
-
onCut: () => cut()
|
|
1106
|
-
onCopy: () => copyToClipboard()
|
|
1107
|
-
onPaste: () => paste()
|
|
1108
|
-
onGoUp: () => goUp()
|
|
1135
|
+
onCut: () => (paneIsActive.value ? paneCut() : cut()) /* wiring:d1 pane-route */,
|
|
1136
|
+
onCopy: () => (paneIsActive.value ? paneCopy() : copyToClipboard()) /* wiring:d1 pane-route */,
|
|
1137
|
+
onPaste: () => (paneIsActive.value ? void panePaste() : void paste()) /* wiring:d1 pane-route */,
|
|
1138
|
+
onGoUp: () => (paneIsActive.value ? splitPaneRef.value?.goUp() : goUp()) /* wiring:d1 pane-route */,
|
|
1109
1139
|
/* cila:c wiring */
|
|
1110
1140
|
onPathJump: () => {
|
|
1111
1141
|
showPalette.value = true;
|
|
@@ -1116,6 +1146,12 @@ useKeyboardShortcuts(rootEl, {
|
|
|
1116
1146
|
/* /cila:c wiring */
|
|
1117
1147
|
onQuickLook: () => quickLookToggle() /* wiring:c2 */,
|
|
1118
1148
|
onToggleInspector: () => toggleInspector() /* koru:k1 */,
|
|
1149
|
+
/* wiring:d1 — sekme aksiyonları (registry: tab-new/close/next/prev) */
|
|
1150
|
+
onTabNew: () => newTabHere(),
|
|
1151
|
+
onTabClose: () => closeTabById(tabsActiveId.value),
|
|
1152
|
+
onTabNext: () => nextTab(),
|
|
1153
|
+
onTabPrev: () => prevTab(),
|
|
1154
|
+
/* /wiring:d1 */
|
|
1119
1155
|
hasSelection: () => !selection.isEmpty.value,
|
|
1120
1156
|
});
|
|
1121
1157
|
|
|
@@ -1159,6 +1195,15 @@ function openNode(n: FileNode) {
|
|
|
1159
1195
|
void load(target);
|
|
1160
1196
|
return;
|
|
1161
1197
|
}
|
|
1198
|
+
/* wiring:e2 — şifreli klasörde dosya açma: kilit açıksa çöz + salt-okunur
|
|
1199
|
+
önizleme (blob URL mevcut viewer'lara gider); kilitliyken hiçbir şey
|
|
1200
|
+
açılmaz (kilit ekranı zaten listeyi kapatır). */
|
|
1201
|
+
if (e2eUnlocked.value && n.type === 'file') {
|
|
1202
|
+
void e2eOpenPreview(n);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (e2eLocked.value && n.type === 'file') return;
|
|
1206
|
+
/* /wiring:e2 */
|
|
1162
1207
|
// "Aç" / double-click contract: open in a new tab against the
|
|
1163
1208
|
// standalone editor route, regardless of file type. The editor page
|
|
1164
1209
|
// picks the right viewer (OnlyOffice for office, Monaco for code/
|
|
@@ -1234,6 +1279,12 @@ async function restoreSelection(targets?: FileNode[]) {
|
|
|
1234
1279
|
}
|
|
1235
1280
|
|
|
1236
1281
|
function previewNode(n: FileNode) {
|
|
1282
|
+
/* wiring:e2 — önizleme de çözülmüş blob'dan beslenir */
|
|
1283
|
+
if (e2eUnlocked.value && n.type === 'file') {
|
|
1284
|
+
void e2eOpenPreview(n);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
/* /wiring:e2 */
|
|
1237
1288
|
previewMode.value = 'view';
|
|
1238
1289
|
previewTarget.value = n;
|
|
1239
1290
|
showPreview.value = true;
|
|
@@ -1255,6 +1306,13 @@ function openNodeInNewTab(n: FileNode) {
|
|
|
1255
1306
|
void load(target);
|
|
1256
1307
|
return;
|
|
1257
1308
|
}
|
|
1309
|
+
/* wiring:e2 — standalone editör rotası sunucudan HAM (şifreli) baytı
|
|
1310
|
+
çeker; şifreli klasörde her "Aç" in-page çözülmüş önizlemeye iner. */
|
|
1311
|
+
if (e2eActive.value) {
|
|
1312
|
+
if (e2eUnlocked.value) void e2eOpenPreview(n);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
/* /wiring:e2 */
|
|
1258
1316
|
// RBAC: a viewer (no edit on this item) can't use the editable "Aç"
|
|
1259
1317
|
// surface — drop to the read-only in-page preview instead.
|
|
1260
1318
|
if (!permCanEdit((n.perm as string) ?? dirPerm.value)) {
|
|
@@ -1370,6 +1428,7 @@ const contextActions = computed<ContextAction[]>(() => {
|
|
|
1370
1428
|
if (!single) return [];
|
|
1371
1429
|
return [
|
|
1372
1430
|
{ key: 'open', label: t('ctx.open'), icon: '↗' },
|
|
1431
|
+
{ key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉' } /* wiring:d1 */,
|
|
1373
1432
|
];
|
|
1374
1433
|
}
|
|
1375
1434
|
|
|
@@ -1410,10 +1469,11 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
|
|
|
1410
1469
|
const accessLabel = locale.value === 'en' ? 'Share / Permissions' : 'Paylaş / İzinler';
|
|
1411
1470
|
return [
|
|
1412
1471
|
{ key: 'open', label: t('ctx.open'), icon: '↗', hidden: !single },
|
|
1472
|
+
{ key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — klasörü yeni sekmede aç */,
|
|
1413
1473
|
{ key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
|
|
1414
1474
|
{ key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
|
|
1415
|
-
{ key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w
|
|
1416
|
-
{ key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w },
|
|
1475
|
+
{ key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w || e2eActive.value /* wiring:e2 — convert ciphertext'e anlamsız */, disabled: !isFile },
|
|
1476
|
+
{ key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w || e2eActive.value /* wiring:e2 — paylaşım MVP'de kapalı (link ciphertext verir) */ },
|
|
1417
1477
|
{ key: 'details', label: t('ctx.details'), icon: 'ℹ', hidden: !any } /* koru:k1 */,
|
|
1418
1478
|
{ key: 'copy-id', label: copyIdLabel, icon: '🆔', hidden: !singleHasId, disabled: !singleHasId },
|
|
1419
1479
|
{ divider: true, key: 'sep1', label: '', hidden: !w },
|
|
@@ -1535,6 +1595,11 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
|
|
|
1535
1595
|
case 'duplicate':
|
|
1536
1596
|
if (targets[0]) await duplicate(targets[0]);
|
|
1537
1597
|
break;
|
|
1598
|
+
/* wiring:d1 — sağ-tık "Yeni sekmede aç" */
|
|
1599
|
+
case 'open-tab':
|
|
1600
|
+
if (targets[0]) openNodeInTab(targets[0]);
|
|
1601
|
+
break;
|
|
1602
|
+
/* /wiring:d1 */
|
|
1538
1603
|
}
|
|
1539
1604
|
}
|
|
1540
1605
|
|
|
@@ -1590,6 +1655,13 @@ async function duplicate(n: FileNode) {
|
|
|
1590
1655
|
}
|
|
1591
1656
|
|
|
1592
1657
|
function downloadFile(n: FileNode) {
|
|
1658
|
+
/* wiring:e2 — şifreli klasörde indirme: baytları çek, çöz, orijinal adla
|
|
1659
|
+
kaydet (ham ciphertext'i kullanıcıya vermek anlamsız). */
|
|
1660
|
+
if (e2eUnlocked.value) {
|
|
1661
|
+
void e2eDownload(n);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
/* /wiring:e2 */
|
|
1593
1665
|
// Keep `<adapter>://<rel>` so backend resolves the right storage
|
|
1594
1666
|
// (stripping it would default to the first storage, which 404s for
|
|
1595
1667
|
// any non-default storage like S3/SFTP/WebDAV).
|
|
@@ -1742,6 +1814,18 @@ function onFilePicked(ev: Event) {
|
|
|
1742
1814
|
|
|
1743
1815
|
async function uploadFiles(list: File[]) {
|
|
1744
1816
|
if (list.length === 0) return;
|
|
1817
|
+
/* wiring:e2 — şifreli klasörde upload şeffaf şifrelenir. Kilitliyken
|
|
1818
|
+
yükleme yok (düz metin sızdırma kapısı olurdu); 200MB üstü MVP
|
|
1819
|
+
tek-shot sınırına takılır ve uyarıyla atlanır. */
|
|
1820
|
+
if (e2eLocked.value) {
|
|
1821
|
+
flashToast(t('e2e.upload.locked'));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
if (e2eUnlocked.value) {
|
|
1825
|
+
list = await e2eEncryptUploads(list);
|
|
1826
|
+
if (list.length === 0) return;
|
|
1827
|
+
}
|
|
1828
|
+
/* /wiring:e2 */
|
|
1745
1829
|
const canChunk = !!(api.endpoints.uploadInit && api.endpoints.uploadFinalize);
|
|
1746
1830
|
for (const f of list) {
|
|
1747
1831
|
// Chunked (S3 multipart) only when the endpoints exist AND the file is
|
|
@@ -1881,11 +1965,38 @@ function onDragLeave() {
|
|
|
1881
1965
|
if (dragCounter.value === 0) dragOver.value = false;
|
|
1882
1966
|
}
|
|
1883
1967
|
function onDragOver(ev: DragEvent) {
|
|
1968
|
+
/* wiring:d1 — iç sürüklemeler kök gövdeye de bırakılabilir olmalı ki split
|
|
1969
|
+
panelinden ana panelin BOŞLUĞUNA bırakmak çalışsın (origin dragover'da
|
|
1970
|
+
okunamaz — karar drop anında verilir; aynı-klasör drop'u no-op kalır). */
|
|
1971
|
+
if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
|
|
1972
|
+
ev.preventDefault();
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
/* /wiring:d1 */
|
|
1884
1976
|
if (isExternalFileDrag(ev)) {
|
|
1885
1977
|
ev.preventDefault();
|
|
1886
1978
|
}
|
|
1887
1979
|
}
|
|
1888
1980
|
function onDropUpload(ev: DragEvent) {
|
|
1981
|
+
/* wiring:d1 — split panelinden ana panelin boşluğuna bırakma = geçerli
|
|
1982
|
+
klasöre aktar (aynı klasörden gelenler no-op, eski davranış korunur). */
|
|
1983
|
+
if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
|
|
1984
|
+
const d1Origin = ev.dataTransfer.getData(FE_DND_SRC_MIME) || '';
|
|
1985
|
+
const d1Here = qualify(currentPath.value);
|
|
1986
|
+
if (d1Origin && d1Here && d1Origin !== d1Here && !trashMode.value && canWriteHere.value) {
|
|
1987
|
+
ev.preventDefault();
|
|
1988
|
+
dragCounter.value = 0;
|
|
1989
|
+
dragOver.value = false;
|
|
1990
|
+
try {
|
|
1991
|
+
const d1Items = JSON.parse(ev.dataTransfer.getData(FE_DND_MIME)) as Array<{ path: string }>;
|
|
1992
|
+
void transferItems(d1Items.map((i) => i.path), d1Here, d1Origin);
|
|
1993
|
+
} catch {
|
|
1994
|
+
/* bozuk payload — yok say */
|
|
1995
|
+
}
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/* /wiring:d1 */
|
|
1889
2000
|
// Internal row drag — nothing to do here, the row drop handler
|
|
1890
2001
|
// in GridView/ListView already resolved the move.
|
|
1891
2002
|
if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
|
|
@@ -1959,13 +2070,14 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
|
|
|
1959
2070
|
.filter((n) => n.basename !== '.trash')
|
|
1960
2071
|
.map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
|
|
1961
2072
|
ev.dataTransfer.setData(FE_DND_MIME, JSON.stringify(items));
|
|
2073
|
+
ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — paneller arası origin damgası */
|
|
1962
2074
|
ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
|
|
1963
2075
|
ev.dataTransfer.effectAllowed = 'move';
|
|
1964
2076
|
}
|
|
1965
2077
|
|
|
1966
|
-
async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string): Promise<void> {
|
|
2078
|
+
async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string, originOverride?: string): Promise<void> {
|
|
1967
2079
|
try {
|
|
1968
|
-
const originWire = qualify(currentPath.value);
|
|
2080
|
+
const originWire = originOverride ?? qualify(currentPath.value); /* wiring:d1 — split panelinden gelen sürüklemede gerçek kaynak klasör */
|
|
1969
2081
|
if (api.endpoints.moveAsync) {
|
|
1970
2082
|
const { op } = await api.moveAsync(sources, targetDir, originWire);
|
|
1971
2083
|
registerMoveUndo(op.id, sources, targetDir, originWire);
|
|
@@ -2006,7 +2118,7 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
|
|
|
2006
2118
|
flashToast('Aynı klasöre taşınamaz');
|
|
2007
2119
|
return;
|
|
2008
2120
|
}
|
|
2009
|
-
await
|
|
2121
|
+
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
2010
2122
|
}
|
|
2011
2123
|
|
|
2012
2124
|
async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
@@ -2025,7 +2137,7 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
|
2025
2137
|
.map((i) => i.path)
|
|
2026
2138
|
.filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/'));
|
|
2027
2139
|
if (sources.length === 0) return;
|
|
2028
|
-
await
|
|
2140
|
+
await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
|
|
2029
2141
|
}
|
|
2030
2142
|
|
|
2031
2143
|
function onCancelUpload(job: UploadJob) {
|
|
@@ -2120,6 +2232,23 @@ function quickLookToggle() {
|
|
|
2120
2232
|
const sel = selection.nodes.value;
|
|
2121
2233
|
const n = sel.length === 1 ? sel[0] : null;
|
|
2122
2234
|
if (!n || n.type !== 'file' || n.basename === '.trash') return;
|
|
2235
|
+
/* wiring:e2 — Space peek şifreli klasörde önce çözer, sonra açar */
|
|
2236
|
+
if (e2eActive.value) {
|
|
2237
|
+
if (!e2eUnlocked.value) return;
|
|
2238
|
+
void (async () => {
|
|
2239
|
+
try {
|
|
2240
|
+
await e2eFetchDecrypted(n);
|
|
2241
|
+
} catch {
|
|
2242
|
+
flashToast(t('e2e.decrypt_failed'));
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
quickLookTarget.value = n;
|
|
2246
|
+
quickLookOpen.value = true;
|
|
2247
|
+
void markRecent(n);
|
|
2248
|
+
})();
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
/* /wiring:e2 */
|
|
2123
2252
|
quickLookTarget.value = n;
|
|
2124
2253
|
quickLookOpen.value = true;
|
|
2125
2254
|
void markRecent(n);
|
|
@@ -2147,6 +2276,21 @@ watch(
|
|
|
2147
2276
|
(nodes) => {
|
|
2148
2277
|
if (!quickLookOpen.value) return;
|
|
2149
2278
|
const n = nodes.length === 1 && nodes[0].type === 'file' ? nodes[0] : null;
|
|
2279
|
+
/* wiring:e2 — ok tuşlarıyla gezerken de hedef atanmadan ÖNCE çöz,
|
|
2280
|
+
yoksa viewer bir anlığına ham ciphertext URL'i alır. */
|
|
2281
|
+
if (n && n.path !== quickLookTarget.value?.path && e2eUnlocked.value) {
|
|
2282
|
+
void (async () => {
|
|
2283
|
+
try {
|
|
2284
|
+
await e2eFetchDecrypted(n);
|
|
2285
|
+
} catch {
|
|
2286
|
+
/* çözülemedi — hedefi yine de değiştir, viewer hata gösterir */
|
|
2287
|
+
}
|
|
2288
|
+
quickLookTarget.value = n;
|
|
2289
|
+
void markRecent(n);
|
|
2290
|
+
})();
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
/* /wiring:e2 */
|
|
2150
2294
|
if (n && n.path !== quickLookTarget.value?.path) {
|
|
2151
2295
|
quickLookTarget.value = n;
|
|
2152
2296
|
void markRecent(n);
|
|
@@ -2245,6 +2389,463 @@ onBeforeUnmount(() => {
|
|
|
2245
2389
|
rootEl.value?.removeEventListener('fe:tour-restart', onTourRestartEvent);
|
|
2246
2390
|
});
|
|
2247
2391
|
/* === /wiring:c4 === */
|
|
2392
|
+
|
|
2393
|
+
/* === wiring:d1 — sekmeler (tab şeridi) + tab başına split ===
|
|
2394
|
+
*
|
|
2395
|
+
* useTabs, mevcut konum state'inin (currentPath/viewMode) ÜSTÜNDE bir
|
|
2396
|
+
* katmandır: aktif tab gezinmeleri watch ile dinleyip snapshot'ını
|
|
2397
|
+
* günceller; tab geçişi mevcut load(path) yolunu çağırır — yeni fetch
|
|
2398
|
+
* mantığı yok. Şerit tek sekmede hiç render edilmez (embed pixel-aynı).
|
|
2399
|
+
*
|
|
2400
|
+
* Persist: `filex.tabs` — pathPersist scope mantığı izlenir: mode 'none'
|
|
2401
|
+
* ise persist kapalı; rootPath confine'ı anahtara eklenir ki farklı
|
|
2402
|
+
* confine'lı embed'ler birbirinin sekmelerini ezmesin.
|
|
2403
|
+
*/
|
|
2404
|
+
const FE_DND_SRC_MIME = 'application/x-brf-files-src';
|
|
2405
|
+
|
|
2406
|
+
const TABS_LS_BASE = 'filex.tabs';
|
|
2407
|
+
function tabsStorageKey(): string | null {
|
|
2408
|
+
if (persistMode() === 'none') return null;
|
|
2409
|
+
return rootPathProp ? `${TABS_LS_BASE}:${rootPathProp}` : TABS_LS_BASE;
|
|
2410
|
+
}
|
|
2411
|
+
const tabsApi = useTabs({ storageKey: tabsStorageKey() });
|
|
2412
|
+
const tabsRestored = tabsApi.restore();
|
|
2413
|
+
const tabsActiveId = tabsApi.activeId;
|
|
2414
|
+
|
|
2415
|
+
const tabsVisible = computed(() => tabsApi.hasMultiple.value);
|
|
2416
|
+
const activeSplit = computed(() => tabsApi.activeTab.value?.split ?? null);
|
|
2417
|
+
|
|
2418
|
+
// Sekme adı OTOMATİK = güncel klasör adı (kök = depo adı / kök etiketi).
|
|
2419
|
+
function tabLabel(path: string): string {
|
|
2420
|
+
const p = (path || '').replace(/^\/+|\/+$/g, '');
|
|
2421
|
+
if (p === '.trash') return t('node.trash');
|
|
2422
|
+
if (!p) return multiStorageRoot.value ? t('breadcrumb.root') : adapter.value || t('breadcrumb.root');
|
|
2423
|
+
return p.split('/').pop() || p;
|
|
2424
|
+
}
|
|
2425
|
+
const tabItems = computed(() =>
|
|
2426
|
+
tabsApi.tabs.value.map((tb) => ({ id: tb.id, label: tabLabel(tb.path), split: !!tb.split })),
|
|
2427
|
+
);
|
|
2428
|
+
|
|
2429
|
+
// Aktif tab kullanıcıyı izler: gezinme + görünüm değişimi snapshot'a yazılır.
|
|
2430
|
+
watch(currentPath, (p) => tabsApi.syncActive({ path: p }));
|
|
2431
|
+
watch(viewMode, (v) => tabsApi.syncActive({ viewMode: v }));
|
|
2432
|
+
|
|
2433
|
+
// İlk sekme, ilk konum belli olur olmaz tohumlanır (restore varsa dokunma —
|
|
2434
|
+
// aktif snapshot ilk load sonrası currentPath watcher'ıyla zaten senkronlanır).
|
|
2435
|
+
onMounted(() => {
|
|
2436
|
+
if (!tabsRestored) tabsApi.seed(currentPath.value ?? '', viewMode.value);
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
function applyTabLocation(tb: TabState) {
|
|
2440
|
+
if (tb.viewMode && tb.viewMode !== viewMode.value) viewMode.value = tb.viewMode;
|
|
2441
|
+
if (tb.path === '.trash') {
|
|
2442
|
+
void loadTrash();
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
void load(tb.path);
|
|
2446
|
+
}
|
|
2447
|
+
function activateTab(id: string) {
|
|
2448
|
+
const tb = tabsApi.activate(id);
|
|
2449
|
+
if (tb) applyTabLocation(tb);
|
|
2450
|
+
}
|
|
2451
|
+
function newTabHere() {
|
|
2452
|
+
// Mevcut konumu klonlar; görünüm zaten oradadır, load gerekmez.
|
|
2453
|
+
tabsApi.openTab(currentPath.value ?? '', { viewMode: viewMode.value, background: false });
|
|
2454
|
+
}
|
|
2455
|
+
function closeTabById(id: string) {
|
|
2456
|
+
const next = tabsApi.closeTab(id);
|
|
2457
|
+
if (next) applyTabLocation(next);
|
|
2458
|
+
}
|
|
2459
|
+
function nextTab() {
|
|
2460
|
+
const tb = tabsApi.step(1);
|
|
2461
|
+
if (tb) applyTabLocation(tb);
|
|
2462
|
+
}
|
|
2463
|
+
function prevTab() {
|
|
2464
|
+
const tb = tabsApi.step(-1);
|
|
2465
|
+
if (tb) applyTabLocation(tb);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/** Bir klasörü ARKA PLANDA yeni sekmede aç (orta-tık / sağ-tık / palet). */
|
|
2469
|
+
function openNodeInTab(n: FileNode) {
|
|
2470
|
+
if (n.type !== 'dir' || n.basename === '.trash') return;
|
|
2471
|
+
const target = multiStorageRoot.value ? wireToVirtual(n.path) : stripAdapter(n.path);
|
|
2472
|
+
tabsApi.openTab(target, { viewMode: viewMode.value, background: true });
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// Orta-tık delegasyonu: ListView/GridView satırları data-fe-path taşır; kendi
|
|
2476
|
+
// keydown/emit zinciri eklemek yerine kökte tek auxclick dinleyicisi yeter.
|
|
2477
|
+
// (SecondaryPane kendi satırlarında stopPropagation ile kendisi halleder.)
|
|
2478
|
+
function onListAuxClick(ev: MouseEvent) {
|
|
2479
|
+
if (ev.button !== 1) return;
|
|
2480
|
+
const host = ev.target as HTMLElement | null;
|
|
2481
|
+
const el = host && typeof host.closest === 'function' ? host.closest('[data-fe-path]') : null;
|
|
2482
|
+
const p = el?.getAttribute('data-fe-path');
|
|
2483
|
+
if (!p) return;
|
|
2484
|
+
const node = files.value.find((f) => f.path === p);
|
|
2485
|
+
if (!node || node.type !== 'dir' || node.basename === '.trash') return;
|
|
2486
|
+
ev.preventDefault();
|
|
2487
|
+
openNodeInTab(node);
|
|
2488
|
+
}
|
|
2489
|
+
// Orta-tuş mousedown'ı satırlar üzerinde iptal: scroll'lu gövdede Chromium'un
|
|
2490
|
+
// autoscroll'u devreye girer ve auxclick HİÇ üretilmez (canlı teşhis) —
|
|
2491
|
+
// preventDefault autoscroll'u bastırır, auxclick yeniden akar.
|
|
2492
|
+
function onListMiddleDown(ev: MouseEvent) {
|
|
2493
|
+
if (ev.button !== 1) return;
|
|
2494
|
+
const host = ev.target as HTMLElement | null;
|
|
2495
|
+
if (host && typeof host.closest === 'function' && host.closest('[data-fe-path]')) {
|
|
2496
|
+
ev.preventDefault();
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
onMounted(() => {
|
|
2500
|
+
rootEl.value?.addEventListener('auxclick', onListAuxClick);
|
|
2501
|
+
rootEl.value?.addEventListener('mousedown', onListMiddleDown);
|
|
2502
|
+
});
|
|
2503
|
+
onBeforeUnmount(() => {
|
|
2504
|
+
rootEl.value?.removeEventListener('auxclick', onListAuxClick);
|
|
2505
|
+
rootEl.value?.removeEventListener('mousedown', onListMiddleDown);
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
// ---- split (tab başına ikincil panel) ------------------------------
|
|
2509
|
+
|
|
2510
|
+
const splitPaneRef = ref<InstanceType<typeof SecondaryPane> | null>(null);
|
|
2511
|
+
// Dar modda split devre dışı (state korunur, genişleyince geri gelir).
|
|
2512
|
+
const splitVisible = computed(() => !!activeSplit.value && !isNarrow.value);
|
|
2513
|
+
|
|
2514
|
+
function toggleSplit() {
|
|
2515
|
+
if (activeSplit.value) {
|
|
2516
|
+
tabsApi.setSplit(null);
|
|
2517
|
+
activePane.value = 'main';
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
if (isNarrow.value) return;
|
|
2521
|
+
tabsApi.setSplit({ path: currentPath.value ?? '' });
|
|
2522
|
+
}
|
|
2523
|
+
function closeSplit() {
|
|
2524
|
+
tabsApi.setSplit(null);
|
|
2525
|
+
activePane.value = 'main';
|
|
2526
|
+
}
|
|
2527
|
+
function onPaneNavigate(p: string) {
|
|
2528
|
+
tabsApi.setSplit({ path: p });
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
// Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
|
|
2532
|
+
const activePane = ref<'main' | 'split'>('main');
|
|
2533
|
+
function setPaneMain() {
|
|
2534
|
+
activePane.value = 'main';
|
|
2535
|
+
}
|
|
2536
|
+
watch(splitVisible, (v) => {
|
|
2537
|
+
if (!v) activePane.value = 'main';
|
|
2538
|
+
});
|
|
2539
|
+
const paneIsActive = computed(() => activePane.value === 'split' && splitVisible.value);
|
|
2540
|
+
const mainPaneFocus = computed(() => splitVisible.value && activePane.value === 'main');
|
|
2541
|
+
|
|
2542
|
+
// Pane yardımcıları — hep ana panelin mevcut dönüştürücülerini sarar.
|
|
2543
|
+
function paneToUser(wire: string): string {
|
|
2544
|
+
return multiStorageRoot.value ? wireToVirtual(wire) : stripAdapter(wire);
|
|
2545
|
+
}
|
|
2546
|
+
function paneClamp(p: string): string {
|
|
2547
|
+
const clean = String(p ?? '').replace(/^\/+|\/+$/g, '');
|
|
2548
|
+
if (!rootFloor) return clean;
|
|
2549
|
+
if (!clean || !(clean === rootFloor || clean.startsWith(rootFloor + '/'))) return rootFloor;
|
|
2550
|
+
return clean;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
// Pano (clipboard) aktif panele göre: kes/kopyala pane seçiminden beslenir,
|
|
2554
|
+
// yapıştır pane klasörüne iner. State ana panelinkiyle ORTAK — panolar arası
|
|
2555
|
+
// kes-yapıştır bedavaya çalışır.
|
|
2556
|
+
function paneCut() {
|
|
2557
|
+
const nodes = splitPaneRef.value?.selectedNodes() ?? [];
|
|
2558
|
+
if (nodes.length === 0) return;
|
|
2559
|
+
clipboard.value = { mode: 'cut', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
|
|
2560
|
+
flashToast('Kesildi');
|
|
2561
|
+
}
|
|
2562
|
+
function paneCopy() {
|
|
2563
|
+
const nodes = splitPaneRef.value?.selectedNodes() ?? [];
|
|
2564
|
+
if (nodes.length === 0) return;
|
|
2565
|
+
clipboard.value = { mode: 'copy', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
|
|
2566
|
+
flashToast('Kopyalandı');
|
|
2567
|
+
}
|
|
2568
|
+
async function panePaste() {
|
|
2569
|
+
const cb = clipboard.value;
|
|
2570
|
+
const pane = splitPaneRef.value;
|
|
2571
|
+
if (!cb.mode || cb.items.length === 0 || !pane) return;
|
|
2572
|
+
const targetWire = qualify(pane.getPath() ?? '');
|
|
2573
|
+
const originWire = qualify(cb.sourcePath || '') || undefined;
|
|
2574
|
+
if (cb.mode === 'cut' && originWire === targetWire) {
|
|
2575
|
+
flashToast('Aynı klasöre kesilemez');
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
await transferItems(cb.items.map((n) => n.path), targetWire, originWire, cb.mode === 'copy');
|
|
2579
|
+
clipboard.value = { mode: null, items: [], sourcePath: null };
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// ---- paneller arası aktarım ----------------------------------------
|
|
2583
|
+
|
|
2584
|
+
function wireAdapterOf(p: string): string {
|
|
2585
|
+
const i = p.indexOf('://');
|
|
2586
|
+
return i === -1 ? '' : p.slice(0, i);
|
|
2587
|
+
}
|
|
2588
|
+
function dndOrigin(ev: DragEvent): string | undefined {
|
|
2589
|
+
const v = ev.dataTransfer?.getData(FE_DND_SRC_MIME);
|
|
2590
|
+
return v || undefined;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
/**
|
|
2594
|
+
* transferItems — panel-arası / pano aktarımının tek kapısı.
|
|
2595
|
+
* Aynı depo → TAŞI (mevcut moveSourcesAsync yolu: kuyruk + geri al).
|
|
2596
|
+
* Farklı depo → KOPYALA dene; backend cross-storage desteklemiyorsa
|
|
2597
|
+
* i18n'li hata toast'ı. Bitince ikincil panel de tazelenir (ana panel
|
|
2598
|
+
* moveSourcesAsync / pendingOps onSettled üzerinden zaten tazelenir).
|
|
2599
|
+
*/
|
|
2600
|
+
async function transferItems(
|
|
2601
|
+
sources: string[],
|
|
2602
|
+
targetWire: string,
|
|
2603
|
+
originWire?: string,
|
|
2604
|
+
forceCopy = false,
|
|
2605
|
+
): Promise<void> {
|
|
2606
|
+
const list = sources.filter((p) => p && p !== targetWire && !targetWire.startsWith(p + '/'));
|
|
2607
|
+
if (list.length === 0 || !targetWire) return;
|
|
2608
|
+
const targetAdapter = wireAdapterOf(targetWire);
|
|
2609
|
+
const cross = list.some((p) => wireAdapterOf(p) !== targetAdapter);
|
|
2610
|
+
if (cross || forceCopy) {
|
|
2611
|
+
try {
|
|
2612
|
+
const { op } = await api.copy(list, targetWire);
|
|
2613
|
+
pendingOps.register(op);
|
|
2614
|
+
flashToast(cross ? t('split.cross_copy') : t('split.copy_queued'));
|
|
2615
|
+
} catch (err) {
|
|
2616
|
+
emit('error', { message: (err as Error).message, context: { op: 'transfer', targetWire } });
|
|
2617
|
+
flashToast(cross ? t('split.cross_failed') : (err as Error).message);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
} else {
|
|
2621
|
+
await moveSourcesAsync(list, targetWire, 'move-transfer', originWire);
|
|
2622
|
+
}
|
|
2623
|
+
void splitPaneRef.value?.reload();
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?: string }) {
|
|
2627
|
+
void transferItems(p.sources, p.targetWire, p.originWire);
|
|
2628
|
+
}
|
|
2629
|
+
/* === /wiring:d1 === */
|
|
2630
|
+
|
|
2631
|
+
/* === wiring:e2 — uçtan uca şifreli klasörler ===
|
|
2632
|
+
*
|
|
2633
|
+
* Kripto şeması + tehdit modeli: docs/E2E-ENCRYPTION.md ve lib/e2ecrypto.ts.
|
|
2634
|
+
* Burada yalnız orkestrasyon var: backend listing yanıtındaki `e2e_root`
|
|
2635
|
+
* kilit ekranını sürer; parola marker'a karşı TARAYICIDA doğrulanır (sunucuya
|
|
2636
|
+
* hiçbir şey gitmez); türetilen klasör anahtarı (KEK) YALNIZ bellekte yaşar
|
|
2637
|
+
* (`e2eRing`) — localStorage/sessionStorage'a asla yazılmaz. Upload şeffaf
|
|
2638
|
+
* şifrelenir, önizleme/indirme şeffaf çözülür (blob URL mevcut viewer'lara).
|
|
2639
|
+
*/
|
|
2640
|
+
const e2eRing = createKeyRing();
|
|
2641
|
+
// Map'ler reaktif değil — sürüm sayacı computed'ları tetikler.
|
|
2642
|
+
const e2eRingVer = ref(0);
|
|
2643
|
+
// İçinde bulunulan şifreli kökün wire yolu ('' = şifreli bağlam yok).
|
|
2644
|
+
const e2eRoot = ref('');
|
|
2645
|
+
// path → çözülmüş blob objectURL (önizleme). Kilitleme/unmount'ta revoke.
|
|
2646
|
+
const e2eUrls = new Map<string, string>();
|
|
2647
|
+
|
|
2648
|
+
const e2eActive = computed(() => !!e2eRoot.value && !trashMode.value);
|
|
2649
|
+
const e2eUnlocked = computed(() => {
|
|
2650
|
+
void e2eRingVer.value;
|
|
2651
|
+
return e2eActive.value && e2eRing.has(e2eRoot.value);
|
|
2652
|
+
});
|
|
2653
|
+
const e2eLocked = computed(() => {
|
|
2654
|
+
void e2eRingVer.value;
|
|
2655
|
+
return e2eActive.value && !e2eRing.has(e2eRoot.value);
|
|
2656
|
+
});
|
|
2657
|
+
|
|
2658
|
+
// Kilit ekranı formu.
|
|
2659
|
+
const e2ePw = ref('');
|
|
2660
|
+
const e2eUnlockBusy = ref(false);
|
|
2661
|
+
const e2eUnlockErr = ref('');
|
|
2662
|
+
// Şifreli klasör oluşturma modalı.
|
|
2663
|
+
const showEncFolder = ref(false);
|
|
2664
|
+
const e2eCreateBusy = ref(false);
|
|
2665
|
+
|
|
2666
|
+
function e2eKek(): CryptoKey | null {
|
|
2667
|
+
return e2eRing.get(e2eRoot.value) ?? null;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function e2eRevokeAll() {
|
|
2671
|
+
for (const url of e2eUrls.values()) URL.revokeObjectURL(url);
|
|
2672
|
+
e2eUrls.clear();
|
|
2673
|
+
}
|
|
2674
|
+
onBeforeUnmount(e2eRevokeAll);
|
|
2675
|
+
|
|
2676
|
+
/** Kilidi aç: marker'ı kökten çek, parolayı YERELDE doğrula, KEK'i belleğe koy. */
|
|
2677
|
+
async function e2eUnlock() {
|
|
2678
|
+
if (!e2ePw.value || e2eUnlockBusy.value || !e2eRoot.value) return;
|
|
2679
|
+
e2eUnlockBusy.value = true;
|
|
2680
|
+
e2eUnlockErr.value = '';
|
|
2681
|
+
try {
|
|
2682
|
+
let markerText = '';
|
|
2683
|
+
try {
|
|
2684
|
+
const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME));
|
|
2685
|
+
URL.revokeObjectURL(url);
|
|
2686
|
+
markerText = await blob.text();
|
|
2687
|
+
} catch {
|
|
2688
|
+
e2eUnlockErr.value = t('e2e.unlock.marker_missing');
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
const marker = parseMarker(markerText);
|
|
2692
|
+
if (!marker) {
|
|
2693
|
+
e2eUnlockErr.value = t('e2e.unlock.marker_missing');
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
const kek = await verifyPassword(marker, e2ePw.value);
|
|
2697
|
+
if (!kek) {
|
|
2698
|
+
e2eUnlockErr.value = t('e2e.unlock.wrong');
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
e2eRing.set(e2eRoot.value, kek);
|
|
2702
|
+
e2eRingVer.value++;
|
|
2703
|
+
e2ePw.value = '';
|
|
2704
|
+
} finally {
|
|
2705
|
+
e2eUnlockBusy.value = false;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/** "Kilitle": bellekteki anahtarı ve çözülmüş blob'ları at. */
|
|
2710
|
+
function e2eLock() {
|
|
2711
|
+
if (!e2eRoot.value) return;
|
|
2712
|
+
e2eRing.lock(e2eRoot.value);
|
|
2713
|
+
e2eRingVer.value++;
|
|
2714
|
+
e2eRevokeAll();
|
|
2715
|
+
flashToast(t('e2e.locked_toast'));
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// Uzantı → önizleme MIME'ı: çözülmüş blob'un <img>/<video>/<object>
|
|
2719
|
+
// etiketlerinde doğru render'ı için (sunucu şifreli dosyayı octet-stream
|
|
2720
|
+
// bilir, oradan gelen tip işe yaramaz).
|
|
2721
|
+
const E2E_MIME: Record<string, string> = {
|
|
2722
|
+
txt: 'text/plain', md: 'text/markdown', log: 'text/plain', csv: 'text/csv',
|
|
2723
|
+
json: 'application/json', xml: 'application/xml', html: 'text/html',
|
|
2724
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
|
|
2725
|
+
webp: 'image/webp', bmp: 'image/bmp', avif: 'image/avif', svg: 'image/svg+xml',
|
|
2726
|
+
pdf: 'application/pdf',
|
|
2727
|
+
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', m4v: 'video/mp4',
|
|
2728
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', flac: 'audio/flac',
|
|
2729
|
+
m4a: 'audio/mp4', aac: 'audio/aac', opus: 'audio/opus',
|
|
2730
|
+
};
|
|
2731
|
+
function e2eMimeFor(n: FileNode): string {
|
|
2732
|
+
const ext = (n.extension || '').toLowerCase();
|
|
2733
|
+
return E2E_MIME[ext] || 'application/octet-stream';
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* Dosyayı çek + çöz + objectURL'ini cache'le. Magic'siz (ör. DAV'la düz
|
|
2738
|
+
* yazılmış) dosyada null döner — çağıran normal ham akışa düşer. Yanlış
|
|
2739
|
+
* anahtar/bozuk veri E2eDecryptError fırlatır (çağıran toast'lar).
|
|
2740
|
+
*/
|
|
2741
|
+
async function e2eFetchDecrypted(n: FileNode): Promise<string | null> {
|
|
2742
|
+
const cached = e2eUrls.get(n.path);
|
|
2743
|
+
if (cached) return cached;
|
|
2744
|
+
const kek = e2eKek();
|
|
2745
|
+
if (!kek) return null;
|
|
2746
|
+
const buf = await api.fetchArrayBuffer(n.path);
|
|
2747
|
+
if (!hasMagic(buf)) return null;
|
|
2748
|
+
const plain = await decryptFile(kek, buf);
|
|
2749
|
+
const url = URL.createObjectURL(new Blob([plain], { type: e2eMimeFor(n) }));
|
|
2750
|
+
e2eUrls.set(n.path, url);
|
|
2751
|
+
return url;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
/** PreviewModal/QuickLook'a giden URL sağlayıcı: çözülmüş blob > ham URL. */
|
|
2755
|
+
function e2ePreviewSrc(p: string): string {
|
|
2756
|
+
return e2eUrls.get(p) ?? api.previewUrl(p);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
/** Çöz + salt-okunur in-page önizleme (openNode/previewNode buraya iner). */
|
|
2760
|
+
async function e2eOpenPreview(n: FileNode) {
|
|
2761
|
+
try {
|
|
2762
|
+
await e2eFetchDecrypted(n);
|
|
2763
|
+
} catch {
|
|
2764
|
+
flashToast(t('e2e.decrypt_failed'));
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
previewMode.value = 'view';
|
|
2768
|
+
previewTarget.value = n;
|
|
2769
|
+
showPreview.value = true;
|
|
2770
|
+
emit('file-opened', { path: n.path, basename: n.basename });
|
|
2771
|
+
void markRecent(n);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
/** Çöz + orijinal adla indir. Magic'siz dosya olduğu gibi iner. */
|
|
2775
|
+
async function e2eDownload(n: FileNode) {
|
|
2776
|
+
try {
|
|
2777
|
+
const buf = await api.fetchArrayBuffer(n.path);
|
|
2778
|
+
const kek = e2eKek();
|
|
2779
|
+
let out = buf;
|
|
2780
|
+
if (hasMagic(buf)) {
|
|
2781
|
+
if (!kek) throw new Error('locked');
|
|
2782
|
+
out = await decryptFile(kek, buf);
|
|
2783
|
+
}
|
|
2784
|
+
const url = URL.createObjectURL(new Blob([out], { type: e2eMimeFor(n) }));
|
|
2785
|
+
const a = document.createElement('a');
|
|
2786
|
+
a.href = url;
|
|
2787
|
+
a.download = n.basename;
|
|
2788
|
+
document.body.appendChild(a);
|
|
2789
|
+
a.click();
|
|
2790
|
+
a.remove();
|
|
2791
|
+
setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
|
2792
|
+
} catch {
|
|
2793
|
+
flashToast(t('e2e.download.failed'));
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
/** Upload listesi → şifreli File listesi (200MB üstü + marker adı atlanır). */
|
|
2798
|
+
async function e2eEncryptUploads(list: File[]): Promise<File[]> {
|
|
2799
|
+
const kek = e2eKek();
|
|
2800
|
+
if (!kek) return [];
|
|
2801
|
+
const out: File[] = [];
|
|
2802
|
+
for (const f of list) {
|
|
2803
|
+
if (f.name === E2E_MARKER_NAME) continue;
|
|
2804
|
+
if (f.size > E2E_MAX_FILE_BYTES) {
|
|
2805
|
+
flashToast(t('e2e.upload.too_big'));
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
try {
|
|
2809
|
+
const ct = await encryptFile(kek, await f.arrayBuffer());
|
|
2810
|
+
out.push(new File([ct], f.name, { type: 'application/octet-stream' }));
|
|
2811
|
+
} catch (err) {
|
|
2812
|
+
emit('error', { message: (err as Error).message, context: { op: 'e2e-encrypt', file: f.name } });
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
return out;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
/** EncryptedFolderModal submit'i: klasörü aç + marker'ı yükle + kilidi açık bırak. */
|
|
2819
|
+
async function submitEncryptedFolder(payload: { name: string; password: string }) {
|
|
2820
|
+
if (e2eActive.value) {
|
|
2821
|
+
// İç içe şifreli klasör kök tespitini bulanıklaştırır — MVP'de yok.
|
|
2822
|
+
flashToast(t('e2e.create.nested'));
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
e2eCreateBusy.value = true;
|
|
2826
|
+
try {
|
|
2827
|
+
const dirWire = qualify(currentPath.value);
|
|
2828
|
+
await api.newFolder(dirWire, payload.name);
|
|
2829
|
+
const { marker, kek } = await createMarker(payload.password);
|
|
2830
|
+
const markerFile = new File([JSON.stringify(marker)], E2E_MARKER_NAME, {
|
|
2831
|
+
type: 'application/json',
|
|
2832
|
+
});
|
|
2833
|
+
const newDirWire = wireJoin(dirWire, payload.name);
|
|
2834
|
+
await api.uploadMultipart(newDirWire, [markerFile]);
|
|
2835
|
+
// Oluşturan oturumda kilit açık başlar (parolayı az önce kendisi girdi).
|
|
2836
|
+
e2eRing.set(newDirWire, kek);
|
|
2837
|
+
e2eRingVer.value++;
|
|
2838
|
+
showEncFolder.value = false;
|
|
2839
|
+
flashToast(t('e2e.create.done'));
|
|
2840
|
+
await load();
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
emit('error', { message: (err as Error).message, context: { op: 'e2e-create' } });
|
|
2843
|
+
flashToast(t('e2e.create.failed'));
|
|
2844
|
+
} finally {
|
|
2845
|
+
e2eCreateBusy.value = false;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
/* === /wiring:e2 === */
|
|
2248
2849
|
</script>
|
|
2249
2850
|
|
|
2250
2851
|
<template>
|
|
@@ -2265,6 +2866,21 @@ onBeforeUnmount(() => {
|
|
|
2265
2866
|
@drop="onDropUpload"
|
|
2266
2867
|
@contextmenu="onContextCanvas"
|
|
2267
2868
|
>
|
|
2869
|
+
<!-- wiring:d1 — sekme şeridi: TEK sekmede hiç render edilmez (embed pixel-aynı) -->
|
|
2870
|
+
<TabBar
|
|
2871
|
+
v-if="tabsVisible"
|
|
2872
|
+
:tabs="tabItems"
|
|
2873
|
+
:active-id="tabsActiveId"
|
|
2874
|
+
:locale="locale"
|
|
2875
|
+
:split-enabled="!isNarrow"
|
|
2876
|
+
:split-active="!!activeSplit"
|
|
2877
|
+
@select="activateTab"
|
|
2878
|
+
@close="closeTabById"
|
|
2879
|
+
@new="newTabHere"
|
|
2880
|
+
@reorder="(from: number, to: number) => tabsApi.move(from, to)"
|
|
2881
|
+
@toggle-split="toggleSplit"
|
|
2882
|
+
/>
|
|
2883
|
+
<!-- /wiring:d1 -->
|
|
2268
2884
|
<Toolbar
|
|
2269
2885
|
ref="toolbarRef"
|
|
2270
2886
|
:view-mode="viewMode"
|
|
@@ -2325,17 +2941,33 @@ onBeforeUnmount(() => {
|
|
|
2325
2941
|
</span>
|
|
2326
2942
|
</div>
|
|
2327
2943
|
|
|
2944
|
+
<!-- wiring:e2 — kilit açık şeridi: şifreli klasörde anahtar bellekteyken
|
|
2945
|
+
görünür; "Kilitle" anahtarı ve çözülmüş blob'ları atar. -->
|
|
2946
|
+
<div v-if="e2eUnlocked" class="fe-e2e-strip" role="status">
|
|
2947
|
+
<span class="fe-e2e-strip__icon" aria-hidden="true">🔒</span>
|
|
2948
|
+
<span class="fe-e2e-strip__label">{{ t('e2e.strip.label') }}</span>
|
|
2949
|
+
<button type="button" class="fe-btn fe-e2e-strip__btn" @click="e2eLock">
|
|
2950
|
+
{{ t('e2e.strip.lock') }}
|
|
2951
|
+
</button>
|
|
2952
|
+
</div>
|
|
2953
|
+
<!-- /wiring:e2 -->
|
|
2954
|
+
|
|
2328
2955
|
<!-- koru:k1 — fe__main lays the listing body and the inspector panel out
|
|
2329
2956
|
as flex siblings (row). Without the inspector open it is visually
|
|
2330
2957
|
identical to the previous direct-child fe__body. -->
|
|
2331
|
-
<div class="fe__main">
|
|
2332
|
-
<div
|
|
2958
|
+
<div class="fe__main" :class="{ 'fe__main--split': splitVisible } /* wiring:d1 */">
|
|
2959
|
+
<div
|
|
2960
|
+
class="fe__body"
|
|
2961
|
+
:class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
|
|
2962
|
+
@pointerdown.capture="setPaneMain() /* wiring:d1 */"
|
|
2963
|
+
@click.self="selection.clear()"
|
|
2964
|
+
>
|
|
2333
2965
|
<!-- Initial load: skeleton ghosts (view-mode aware) instead of an
|
|
2334
2966
|
empty/"no files" flash. Only when there's nothing yet — navigation
|
|
2335
2967
|
keeps the current list, exactly as before. -->
|
|
2336
2968
|
<div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
|
|
2337
2969
|
<span class="fe-sr-only">{{ t('loading') }}</span>
|
|
2338
|
-
<div v-if="viewMode
|
|
2970
|
+
<div v-if="viewMode !== 'list' /* wiring:d2 — galeri de grid iskeletini kullanır */" class="fe-skel-grid" aria-hidden="true">
|
|
2339
2971
|
<div v-for="i in 8" :key="i" class="fe-skel-card">
|
|
2340
2972
|
<div class="fe-skel fe-skel--thumb"></div>
|
|
2341
2973
|
<div class="fe-skel fe-skel--label"></div>
|
|
@@ -2413,6 +3045,49 @@ onBeforeUnmount(() => {
|
|
|
2413
3045
|
</details>
|
|
2414
3046
|
<!-- /wiring:c4 -->
|
|
2415
3047
|
</div>
|
|
3048
|
+
<!-- wiring:e2 — şifreli klasör kilit ekranı: parola doğru girilene dek
|
|
3049
|
+
listeleme render edilmez. Parola tarayıcıda marker'a karşı
|
|
3050
|
+
doğrulanır; sunucuya gitmez. -->
|
|
3051
|
+
<div v-else-if="e2eLocked" class="fe-state fe-e2e-lock">
|
|
3052
|
+
<svg
|
|
3053
|
+
class="fe-state__art"
|
|
3054
|
+
viewBox="0 0 120 100"
|
|
3055
|
+
width="110"
|
|
3056
|
+
height="92"
|
|
3057
|
+
fill="none"
|
|
3058
|
+
stroke="currentColor"
|
|
3059
|
+
stroke-width="2"
|
|
3060
|
+
stroke-linecap="round"
|
|
3061
|
+
stroke-linejoin="round"
|
|
3062
|
+
aria-hidden="true"
|
|
3063
|
+
>
|
|
3064
|
+
<rect x="38" y="44" width="44" height="34" rx="6" />
|
|
3065
|
+
<path d="M46 44v-8a14 14 0 0 1 28 0v8" />
|
|
3066
|
+
<circle cx="60" cy="59" r="3" fill="currentColor" stroke="none" />
|
|
3067
|
+
<path d="M60 62v7" />
|
|
3068
|
+
</svg>
|
|
3069
|
+
<p class="fe-state__title">{{ t('e2e.locked.title') }}</p>
|
|
3070
|
+
<p class="fe-state__hint">{{ t('e2e.locked.hint') }}</p>
|
|
3071
|
+
<form class="fe-e2e-lock__form" @submit.prevent="e2eUnlock">
|
|
3072
|
+
<input
|
|
3073
|
+
v-model="e2ePw"
|
|
3074
|
+
type="password"
|
|
3075
|
+
class="fe-input fe-e2e-lock__input"
|
|
3076
|
+
:placeholder="t('e2e.locked.pw_placeholder')"
|
|
3077
|
+
autocomplete="current-password"
|
|
3078
|
+
:disabled="e2eUnlockBusy"
|
|
3079
|
+
/>
|
|
3080
|
+
<button
|
|
3081
|
+
type="submit"
|
|
3082
|
+
class="fe-btn fe-btn--primary"
|
|
3083
|
+
:disabled="e2eUnlockBusy || !e2ePw"
|
|
3084
|
+
>
|
|
3085
|
+
{{ e2eUnlockBusy ? t('e2e.locked.busy') : t('e2e.locked.unlock') }}
|
|
3086
|
+
</button>
|
|
3087
|
+
</form>
|
|
3088
|
+
<p v-if="e2eUnlockErr" class="fe-form__error" role="alert">{{ e2eUnlockErr }}</p>
|
|
3089
|
+
</div>
|
|
3090
|
+
<!-- /wiring:e2 -->
|
|
2416
3091
|
<!-- Search with zero hits — its own message, not "folder is empty". -->
|
|
2417
3092
|
<div v-else-if="!loading && files.length === 0 && searchQuery" class="fe-state">
|
|
2418
3093
|
<svg
|
|
@@ -2504,6 +3179,22 @@ onBeforeUnmount(() => {
|
|
|
2504
3179
|
@star-change="onStarChange"
|
|
2505
3180
|
/>
|
|
2506
3181
|
<GridView
|
|
3182
|
+
v-else-if="viewMode === 'grid' /* wiring:d2 — v-else → v-else-if (3. mod eklendi) */"
|
|
3183
|
+
:files="files"
|
|
3184
|
+
:selected="selection.selected.value"
|
|
3185
|
+
:clipped="clippedPaths"
|
|
3186
|
+
:show-parent-path="!!searchQuery"
|
|
3187
|
+
:locale="locale"
|
|
3188
|
+
:loading="loading"
|
|
3189
|
+
:thumb-src="thumbs.src"
|
|
3190
|
+
@click-card="(n, m) => selection.click(n.path, m)"
|
|
3191
|
+
@dbl-card="openNode"
|
|
3192
|
+
@context-card="onContextTarget"
|
|
3193
|
+
@item-drag-start="onItemDragStart"
|
|
3194
|
+
@item-drop-into="onItemDropInto"
|
|
3195
|
+
/>
|
|
3196
|
+
<!-- wiring:d2 — galeri görünümü (GridView ile aynı event sözleşmesi) -->
|
|
3197
|
+
<GalleryView
|
|
2507
3198
|
v-else
|
|
2508
3199
|
:files="files"
|
|
2509
3200
|
:selected="selection.selected.value"
|
|
@@ -2518,8 +3209,35 @@ onBeforeUnmount(() => {
|
|
|
2518
3209
|
@item-drag-start="onItemDragStart"
|
|
2519
3210
|
@item-drop-into="onItemDropInto"
|
|
2520
3211
|
/>
|
|
3212
|
+
<!-- /wiring:d2 -->
|
|
2521
3213
|
</div>
|
|
2522
3214
|
|
|
3215
|
+
<!-- wiring:d1 — tab başına split: sağ ikincil panel (dar modda kapalı).
|
|
3216
|
+
:key tab kimliğine bağlı — tab geçişinde pane kendi konumuyla temiz
|
|
3217
|
+
remount olur. -->
|
|
3218
|
+
<SecondaryPane
|
|
3219
|
+
v-if="splitVisible && activeSplit"
|
|
3220
|
+
ref="splitPaneRef"
|
|
3221
|
+
:key="'split-' + tabsActiveId"
|
|
3222
|
+
:api="api"
|
|
3223
|
+
:initial-path="activeSplit.path"
|
|
3224
|
+
:locale="locale"
|
|
3225
|
+
:qualify="qualify"
|
|
3226
|
+
:to-user="paneToUser"
|
|
3227
|
+
:clamp="paneClamp"
|
|
3228
|
+
:root-label="multiStorageRoot ? '/' : adapter || t('breadcrumb.root')"
|
|
3229
|
+
:floor="rootFloor"
|
|
3230
|
+
:multi-root="multiStorageRoot"
|
|
3231
|
+
:virtual-rows="virtualStorageRows"
|
|
3232
|
+
:active="paneIsActive"
|
|
3233
|
+
@navigate="onPaneNavigate"
|
|
3234
|
+
@activate="activePane = 'split'"
|
|
3235
|
+
@close="closeSplit"
|
|
3236
|
+
@open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
|
|
3237
|
+
@transfer="onPaneTransfer"
|
|
3238
|
+
/>
|
|
3239
|
+
<!-- /wiring:d1 -->
|
|
3240
|
+
|
|
2523
3241
|
<!-- koru:k1 — inspector (details) panel; v-if keeps the closed state
|
|
2524
3242
|
free of any DOM. Narrow mode renders it as a full-size overlay. -->
|
|
2525
3243
|
<InspectorPanel
|
|
@@ -2610,9 +3328,20 @@ onBeforeUnmount(() => {
|
|
|
2610
3328
|
<NewFolderModal
|
|
2611
3329
|
:open="showNewFolder"
|
|
2612
3330
|
:locale="locale"
|
|
3331
|
+
:encrypted-option="!e2eActive /* wiring:e2 — iç içe şifreli klasör yok */"
|
|
2613
3332
|
@close="showNewFolder = false"
|
|
2614
3333
|
@submit="submitNewFolder"
|
|
3334
|
+
@encrypted="showNewFolder = false; showEncFolder = true /* wiring:e2 */"
|
|
3335
|
+
/>
|
|
3336
|
+
<!-- wiring:e2 — şifreli klasör oluşturma modalı -->
|
|
3337
|
+
<EncryptedFolderModal
|
|
3338
|
+
:open="showEncFolder"
|
|
3339
|
+
:locale="locale"
|
|
3340
|
+
:busy="e2eCreateBusy"
|
|
3341
|
+
@close="showEncFolder = false"
|
|
3342
|
+
@submit="submitEncryptedFolder"
|
|
2615
3343
|
/>
|
|
3344
|
+
<!-- /wiring:e2 -->
|
|
2616
3345
|
<RenameModal
|
|
2617
3346
|
:open="showRename"
|
|
2618
3347
|
:locale="locale"
|
|
@@ -2640,11 +3369,12 @@ onBeforeUnmount(() => {
|
|
|
2640
3369
|
:locale="locale"
|
|
2641
3370
|
:file="previewTarget"
|
|
2642
3371
|
:theme="config.theme || 'auto'"
|
|
2643
|
-
:preview-url="(p) =>
|
|
2644
|
-
:download-url="(p) => api.downloadUrl(p)"
|
|
2645
|
-
:only-office-base="effectiveOnlyOfficeBase"
|
|
3372
|
+
:preview-url="(p) => e2ePreviewSrc(p) /* wiring:e2 — çözülmüş blob > ham URL */"
|
|
3373
|
+
:download-url="(p) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
|
|
3374
|
+
:only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 — OO ciphertext açamaz */"
|
|
2646
3375
|
:only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
|
|
2647
|
-
:
|
|
3376
|
+
:new-tab-enabled="!e2eActive /* wiring:e2 — standalone rota ham baytı çeker */"
|
|
3377
|
+
:save-text-endpoint="e2eActive ? null : api.endpoints.saveText || null /* wiring:e2 — düz metin kaydı sızıntı olur */"
|
|
2648
3378
|
:open-mode="previewMode"
|
|
2649
3379
|
:auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
|
|
2650
3380
|
:auth-credentials="api.credentialsMode()"
|
|
@@ -2746,13 +3476,16 @@ onBeforeUnmount(() => {
|
|
|
2746
3476
|
@navigate="(p: string) => load(p)"
|
|
2747
3477
|
@new-folder="showNewFolder = true"
|
|
2748
3478
|
@upload="triggerUpload"
|
|
2749
|
-
@toggle-view="viewMode = viewMode === 'list' ? 'grid' : 'list'"
|
|
3479
|
+
@toggle-view="viewMode = viewMode === 'list' ? 'grid' : viewMode === 'grid' ? 'gallery' : 'list' /* wiring:d2 — 3 mod döngüsü */"
|
|
2750
3480
|
@open-trash="loadTrash"
|
|
2751
3481
|
@refresh="() => load()"
|
|
2752
3482
|
@go-up="goUp"
|
|
2753
3483
|
@open-theme="showThemeGallery = true /* wiring:int */"
|
|
2754
3484
|
@open-shortcut-settings="showShortcutSettings = true /* wiring:int */"
|
|
2755
3485
|
@start-tour="startTour() /* wiring:int */"
|
|
3486
|
+
:split-enabled="!isNarrow /* wiring:d1 */"
|
|
3487
|
+
@tab-new="newTabHere() /* wiring:d1 */"
|
|
3488
|
+
@split-toggle="toggleSplit() /* wiring:d1 */"
|
|
2756
3489
|
/>
|
|
2757
3490
|
<ShortcutsHelp
|
|
2758
3491
|
:open="showShortcutsHelp"
|
|
@@ -2812,9 +3545,9 @@ onBeforeUnmount(() => {
|
|
|
2812
3545
|
:locale="locale"
|
|
2813
3546
|
:file="quickLookTarget"
|
|
2814
3547
|
:theme="config.theme || 'auto'"
|
|
2815
|
-
:preview-url="(p: string) =>
|
|
2816
|
-
:download-url="(p: string) => api.downloadUrl(p)"
|
|
2817
|
-
:only-office-base="effectiveOnlyOfficeBase"
|
|
3548
|
+
:preview-url="(p: string) => e2ePreviewSrc(p) /* wiring:e2 */"
|
|
3549
|
+
:download-url="(p: string) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
|
|
3550
|
+
:only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 */"
|
|
2818
3551
|
:only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
|
|
2819
3552
|
:auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
|
|
2820
3553
|
:auth-credentials="api.credentialsMode()"
|