@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.
Files changed (49) hide show
  1. package/dist/{ArchiveViewer-DKW-HyXT.js → ArchiveViewer-w6GDJJt9.js} +2 -2
  2. package/dist/{ArchiveViewer-DKW-HyXT.js.map → ArchiveViewer-w6GDJJt9.js.map} +1 -1
  3. package/dist/{CsvViewer-D2HmP2yr.js → CsvViewer-Ci5-mke8.js} +2 -2
  4. package/dist/{CsvViewer-D2HmP2yr.js.map → CsvViewer-Ci5-mke8.js.map} +1 -1
  5. package/dist/{DrawioViewer-DPNvOFCs.js → DrawioViewer-D2iMuQfD.js} +2 -2
  6. package/dist/{DrawioViewer-DPNvOFCs.js.map → DrawioViewer-D2iMuQfD.js.map} +1 -1
  7. package/dist/{EpubViewer-ByqjNuNL.js → EpubViewer-D4ydsa9X.js} +2 -2
  8. package/dist/{EpubViewer-ByqjNuNL.js.map → EpubViewer-D4ydsa9X.js.map} +1 -1
  9. package/dist/{IpynbViewer-2iONhbuJ.js → IpynbViewer-CWBwiFKX.js} +2 -2
  10. package/dist/{IpynbViewer-2iONhbuJ.js.map → IpynbViewer-CWBwiFKX.js.map} +1 -1
  11. package/dist/{MermaidViewer-CmiU7hIT.js → MermaidViewer-CDhuqQli.js} +2 -2
  12. package/dist/{MermaidViewer-CmiU7hIT.js.map → MermaidViewer-CDhuqQli.js.map} +1 -1
  13. package/dist/{PsdViewer-Inhl2yN9.js → PsdViewer-C_TkFb2H.js} +2 -2
  14. package/dist/{PsdViewer-Inhl2yN9.js.map → PsdViewer-C_TkFb2H.js.map} +1 -1
  15. package/dist/{TiffViewer-C2AFFEze.js → TiffViewer-BmWCXR7e.js} +2 -2
  16. package/dist/{TiffViewer-C2AFFEze.js.map → TiffViewer-BmWCXR7e.js.map} +1 -1
  17. package/dist/{Viewer3D-YdW14q_n.js → Viewer3D-5kPT1QsY.js} +2 -2
  18. package/dist/{Viewer3D-YdW14q_n.js.map → Viewer3D-5kPT1QsY.js.map} +1 -1
  19. package/dist/filex-core.js +66 -47
  20. package/dist/filex-core.umd.cjs +41 -40
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-nc0-oI_l.js +11723 -0
  23. package/dist/index-nc0-oI_l.js.map +1 -0
  24. package/dist/index.d.ts +364 -1
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +756 -23
  28. package/src/components/CommandPalette.vue +12 -1
  29. package/src/components/EncryptedFolderModal.vue +125 -0
  30. package/src/components/GalleryView.vue +215 -0
  31. package/src/components/GridView.vue +2 -0
  32. package/src/components/InspectorPanel.vue +170 -0
  33. package/src/components/ListView.vue +2 -0
  34. package/src/components/SecondaryPane.vue +411 -0
  35. package/src/components/TabBar.vue +174 -0
  36. package/src/components/Toolbar.vue +23 -5
  37. package/src/composables/useFileApi.ts +60 -0
  38. package/src/composables/useKeyboardShortcuts.ts +19 -0
  39. package/src/composables/useTabs.ts +222 -0
  40. package/src/index.ts +27 -0
  41. package/src/lib/e2ecrypto.ts +299 -0
  42. package/src/locales/en.ts +67 -0
  43. package/src/locales/tr.ts +67 -0
  44. package/src/modals/NewFolderModal.vue +18 -0
  45. package/src/modals/PreviewModal.vue +7 -2
  46. package/src/styles/base.css +631 -0
  47. package/src/types/FileNode.ts +4 -1
  48. package/dist/index-eARevpz0.js +0 -9952
  49. package/dist/index-eARevpz0.js.map +0 -1
@@ -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, disabled: !isFile },
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 moveSourcesAsync(sources, targetDir, 'move-dnd');
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 moveSourcesAsync(sources, targetDir, 'move-dnd-crumb');
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 class="fe__body" @click.self="selection.clear()">
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 === 'grid'" class="fe-skel-grid" aria-hidden="true">
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) => api.previewUrl(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
- :save-text-endpoint="api.endpoints.saveText || null"
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) => api.previewUrl(p)"
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()"