@brftech/filex-core 0.6.0 → 0.7.1

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 (47) hide show
  1. package/dist/{ArchiveViewer-EGgNqLPE.js → ArchiveViewer-Qi4L2jj9.js} +2 -2
  2. package/dist/{ArchiveViewer-EGgNqLPE.js.map → ArchiveViewer-Qi4L2jj9.js.map} +1 -1
  3. package/dist/{CsvViewer-BvriJ42M.js → CsvViewer-IOIrJDsJ.js} +2 -2
  4. package/dist/{CsvViewer-BvriJ42M.js.map → CsvViewer-IOIrJDsJ.js.map} +1 -1
  5. package/dist/{DrawioViewer-B_bSPsWX.js → DrawioViewer-D0KtUQBO.js} +2 -2
  6. package/dist/{DrawioViewer-B_bSPsWX.js.map → DrawioViewer-D0KtUQBO.js.map} +1 -1
  7. package/dist/{EpubViewer-hl68h4gG.js → EpubViewer-DJOXiOi4.js} +2 -2
  8. package/dist/{EpubViewer-hl68h4gG.js.map → EpubViewer-DJOXiOi4.js.map} +1 -1
  9. package/dist/{IpynbViewer-lhwOjXK7.js → IpynbViewer-D16jXVJc.js} +2 -2
  10. package/dist/{IpynbViewer-lhwOjXK7.js.map → IpynbViewer-D16jXVJc.js.map} +1 -1
  11. package/dist/{MermaidViewer-H2Z6AYXg.js → MermaidViewer-BnSaVg1L.js} +2 -2
  12. package/dist/{MermaidViewer-H2Z6AYXg.js.map → MermaidViewer-BnSaVg1L.js.map} +1 -1
  13. package/dist/{PsdViewer-Bsk-11yH.js → PsdViewer-BUerXaQA.js} +2 -2
  14. package/dist/{PsdViewer-Bsk-11yH.js.map → PsdViewer-BUerXaQA.js.map} +1 -1
  15. package/dist/{TiffViewer-D-j9vycG.js → TiffViewer-D0Qn3IV-.js} +2 -2
  16. package/dist/{TiffViewer-D-j9vycG.js.map → TiffViewer-D0Qn3IV-.js.map} +1 -1
  17. package/dist/{Viewer3D-Bcr70rTe.js → Viewer3D-BtRK-k1B.js} +2 -2
  18. package/dist/{Viewer3D-Bcr70rTe.js.map → Viewer3D-BtRK-k1B.js.map} +1 -1
  19. package/dist/filex-core.js +66 -50
  20. package/dist/filex-core.umd.cjs +35 -35
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-CGNE1bSv.js +11802 -0
  23. package/dist/index-CGNE1bSv.js.map +1 -0
  24. package/dist/index.d.ts +184 -0
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +416 -15
  28. package/src/components/EncryptedFolderModal.vue +125 -0
  29. package/src/components/GalleryView.vue +1 -0
  30. package/src/components/GridView.vue +1 -0
  31. package/src/components/ListView.vue +1 -0
  32. package/src/components/SecondaryPane.vue +72 -78
  33. package/src/components/Toolbar.vue +112 -3
  34. package/src/composables/useFileApi.ts +7 -0
  35. package/src/composables/useKeyboardShortcuts.ts +6 -0
  36. package/src/composables/useTabs.ts +11 -2
  37. package/src/index.ts +21 -0
  38. package/src/lib/e2ecrypto.ts +299 -0
  39. package/src/locales/en.ts +34 -0
  40. package/src/locales/tr.ts +34 -0
  41. package/src/modals/NewFolderModal.vue +18 -0
  42. package/src/modals/PreviewModal.vue +7 -2
  43. package/src/styles/base.css +126 -1
  44. package/src/styles/variables.css +2 -2
  45. package/src/types/FileNode.ts +3 -0
  46. package/dist/index-C_ZSnk_I.js +0 -11109
  47. package/dist/index-C_ZSnk_I.js.map +0 -1
@@ -71,6 +71,20 @@ import TabBar from './components/TabBar.vue';
71
71
  import SecondaryPane from './components/SecondaryPane.vue';
72
72
  import { useTabs, type TabState } from './composables/useTabs';
73
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 */
74
88
 
75
89
  import NewFolderModal from './modals/NewFolderModal.vue';
76
90
  import RenameModal from './modals/RenameModal.vue';
@@ -671,6 +685,7 @@ async function load(path?: string) {
671
685
  currentPath.value = '';
672
686
  adapter.value = '';
673
687
  dirname.value = '';
688
+ e2eRoot.value = ''; /* wiring:e2 — sanal kökte kilit ekranı olmaz */
674
689
  files.value = virtualStorageRows();
675
690
  return;
676
691
  }
@@ -685,11 +700,16 @@ async function load(path?: string) {
685
700
  adapter.value = resp.adapter;
686
701
  dirname.value = resp.dirname;
687
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 */
688
707
  files.value = (resp.files || []).filter((f) => {
689
708
  if (f.path.includes('.thumbs')) return false;
690
709
  if (f.path.includes('.versions') || f.basename === '.versions') return false;
691
710
  if (f.basename === '.trash') return false;
692
711
  if (f.basename === '.keepdir') return false;
712
+ if (f.basename === E2E_MARKER_NAME) return false; /* wiring:e2 — marker gizli */
693
713
  return true;
694
714
  });
695
715
  // Inject virtual `.trash` entry at root only.
@@ -725,6 +745,7 @@ async function load(path?: string) {
725
745
  // show the dedicated not-found state instead of a toast over a stale
726
746
  // listing that reads as "this folder is empty".
727
747
  notFoundPath.value = String(requested);
748
+ e2eRoot.value = ''; /* wiring:e2 — ölü linkte kilit ekranı kalmasın */
728
749
  files.value = [];
729
750
  emit('error', { message: e, context: { path } });
730
751
  return;
@@ -791,6 +812,7 @@ async function loadTrash() {
791
812
  loading.value = true;
792
813
  trashOrigin.value = adapter.value || '';
793
814
  trashMode.value = true;
815
+ e2eRoot.value = ''; /* wiring:e2 — çöp görünümü şifreli bağlamın dışındadır */
794
816
  selection.clear();
795
817
  try {
796
818
  const { entries } = await api.listTrash();
@@ -1173,6 +1195,15 @@ function openNode(n: FileNode) {
1173
1195
  void load(target);
1174
1196
  return;
1175
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 */
1176
1207
  // "Aç" / double-click contract: open in a new tab against the
1177
1208
  // standalone editor route, regardless of file type. The editor page
1178
1209
  // picks the right viewer (OnlyOffice for office, Monaco for code/
@@ -1248,6 +1279,12 @@ async function restoreSelection(targets?: FileNode[]) {
1248
1279
  }
1249
1280
 
1250
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 */
1251
1288
  previewMode.value = 'view';
1252
1289
  previewTarget.value = n;
1253
1290
  showPreview.value = true;
@@ -1269,6 +1306,13 @@ function openNodeInNewTab(n: FileNode) {
1269
1306
  void load(target);
1270
1307
  return;
1271
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 */
1272
1316
  // RBAC: a viewer (no edit on this item) can't use the editable "Aç"
1273
1317
  // surface — drop to the read-only in-page preview instead.
1274
1318
  if (!permCanEdit((n.perm as string) ?? dirPerm.value)) {
@@ -1428,8 +1472,8 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
1428
1472
  { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — klasörü yeni sekmede aç */,
1429
1473
  { key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
1430
1474
  { key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
1431
- { key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w, disabled: !isFile },
1432
- { 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) */ },
1433
1477
  { key: 'details', label: t('ctx.details'), icon: 'ℹ', hidden: !any } /* koru:k1 */,
1434
1478
  { key: 'copy-id', label: copyIdLabel, icon: '🆔', hidden: !singleHasId, disabled: !singleHasId },
1435
1479
  { divider: true, key: 'sep1', label: '', hidden: !w },
@@ -1537,7 +1581,10 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
1537
1581
  flashToast('Kopyala → Yapıştır hazır');
1538
1582
  break;
1539
1583
  case 'paste':
1540
- await paste();
1584
+ /* ui-fix — sağ-klik menüsünden yapıştırma da aktif panele gider
1585
+ * (klavye kısayolu zaten pane-route'luydu; menü değildi). */
1586
+ if (paneIsActive.value) await panePaste();
1587
+ else await paste();
1541
1588
  break;
1542
1589
  case 'delete':
1543
1590
  showDelete.value = true;
@@ -1611,6 +1658,13 @@ async function duplicate(n: FileNode) {
1611
1658
  }
1612
1659
 
1613
1660
  function downloadFile(n: FileNode) {
1661
+ /* wiring:e2 — şifreli klasörde indirme: baytları çek, çöz, orijinal adla
1662
+ kaydet (ham ciphertext'i kullanıcıya vermek anlamsız). */
1663
+ if (e2eUnlocked.value) {
1664
+ void e2eDownload(n);
1665
+ return;
1666
+ }
1667
+ /* /wiring:e2 */
1614
1668
  // Keep `<adapter>://<rel>` so backend resolves the right storage
1615
1669
  // (stripping it would default to the first storage, which 404s for
1616
1670
  // any non-default storage like S3/SFTP/WebDAV).
@@ -1763,6 +1817,18 @@ function onFilePicked(ev: Event) {
1763
1817
 
1764
1818
  async function uploadFiles(list: File[]) {
1765
1819
  if (list.length === 0) return;
1820
+ /* wiring:e2 — şifreli klasörde upload şeffaf şifrelenir. Kilitliyken
1821
+ yükleme yok (düz metin sızdırma kapısı olurdu); 200MB üstü MVP
1822
+ tek-shot sınırına takılır ve uyarıyla atlanır. */
1823
+ if (e2eLocked.value) {
1824
+ flashToast(t('e2e.upload.locked'));
1825
+ return;
1826
+ }
1827
+ if (e2eUnlocked.value) {
1828
+ list = await e2eEncryptUploads(list);
1829
+ if (list.length === 0) return;
1830
+ }
1831
+ /* /wiring:e2 */
1766
1832
  const canChunk = !!(api.endpoints.uploadInit && api.endpoints.uploadFinalize);
1767
1833
  for (const f of list) {
1768
1834
  // Chunked (S3 multipart) only when the endpoints exist AND the file is
@@ -2169,6 +2235,23 @@ function quickLookToggle() {
2169
2235
  const sel = selection.nodes.value;
2170
2236
  const n = sel.length === 1 ? sel[0] : null;
2171
2237
  if (!n || n.type !== 'file' || n.basename === '.trash') return;
2238
+ /* wiring:e2 — Space peek şifreli klasörde önce çözer, sonra açar */
2239
+ if (e2eActive.value) {
2240
+ if (!e2eUnlocked.value) return;
2241
+ void (async () => {
2242
+ try {
2243
+ await e2eFetchDecrypted(n);
2244
+ } catch {
2245
+ flashToast(t('e2e.decrypt_failed'));
2246
+ return;
2247
+ }
2248
+ quickLookTarget.value = n;
2249
+ quickLookOpen.value = true;
2250
+ void markRecent(n);
2251
+ })();
2252
+ return;
2253
+ }
2254
+ /* /wiring:e2 */
2172
2255
  quickLookTarget.value = n;
2173
2256
  quickLookOpen.value = true;
2174
2257
  void markRecent(n);
@@ -2196,6 +2279,21 @@ watch(
2196
2279
  (nodes) => {
2197
2280
  if (!quickLookOpen.value) return;
2198
2281
  const n = nodes.length === 1 && nodes[0].type === 'file' ? nodes[0] : null;
2282
+ /* wiring:e2 — ok tuşlarıyla gezerken de hedef atanmadan ÖNCE çöz,
2283
+ yoksa viewer bir anlığına ham ciphertext URL'i alır. */
2284
+ if (n && n.path !== quickLookTarget.value?.path && e2eUnlocked.value) {
2285
+ void (async () => {
2286
+ try {
2287
+ await e2eFetchDecrypted(n);
2288
+ } catch {
2289
+ /* çözülemedi — hedefi yine de değiştir, viewer hata gösterir */
2290
+ }
2291
+ quickLookTarget.value = n;
2292
+ void markRecent(n);
2293
+ })();
2294
+ return;
2295
+ }
2296
+ /* /wiring:e2 */
2199
2297
  if (n && n.path !== quickLookTarget.value?.path) {
2200
2298
  quickLookTarget.value = n;
2201
2299
  void markRecent(n);
@@ -2423,14 +2521,30 @@ function toggleSplit() {
2423
2521
  return;
2424
2522
  }
2425
2523
  if (isNarrow.value) return;
2426
- tabsApi.setSplit({ path: currentPath.value ?? '' });
2524
+ tabsApi.setSplit({ path: currentPath.value ?? '', viewMode: viewMode.value });
2427
2525
  }
2428
2526
  function closeSplit() {
2429
2527
  tabsApi.setSplit(null);
2430
2528
  activePane.value = 'main';
2431
2529
  }
2432
2530
  function onPaneNavigate(p: string) {
2433
- tabsApi.setSplit({ path: p });
2531
+ tabsApi.setSplit({ ...(activeSplit.value ?? {}), path: p });
2532
+ }
2533
+ /* ui-fix — pane'in KENDİ görünüm modu: split açılırken ana panelinkini
2534
+ * devralır, sonrasında bağımsız. Toolbar'ın görünüm değiştiricisi ve palet
2535
+ * toggle'ı AKTİF panele yazar (Burak: "B tıklıyken ikon değiştir dersem
2536
+ * B'nin değişmesi lazım"). */
2537
+ const paneViewMode = computed<ViewMode>(() => activeSplit.value?.viewMode ?? viewMode.value);
2538
+ function setPaneViewMode(v: ViewMode) {
2539
+ if (!activeSplit.value) return;
2540
+ tabsApi.setSplit({ ...activeSplit.value, viewMode: v });
2541
+ }
2542
+ const displayedViewMode = computed<ViewMode>(() =>
2543
+ paneIsActive.value ? paneViewMode.value : viewMode.value,
2544
+ );
2545
+ function setDisplayedViewMode(v: ViewMode) {
2546
+ if (paneIsActive.value) setPaneViewMode(v);
2547
+ else viewMode.value = v;
2434
2548
  }
2435
2549
 
2436
2550
  // Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
@@ -2532,6 +2646,225 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2532
2646
  void transferItems(p.sources, p.targetWire, p.originWire);
2533
2647
  }
2534
2648
  /* === /wiring:d1 === */
2649
+
2650
+ /* === wiring:e2 — uçtan uca şifreli klasörler ===
2651
+ *
2652
+ * Kripto şeması + tehdit modeli: docs/E2E-ENCRYPTION.md ve lib/e2ecrypto.ts.
2653
+ * Burada yalnız orkestrasyon var: backend listing yanıtındaki `e2e_root`
2654
+ * kilit ekranını sürer; parola marker'a karşı TARAYICIDA doğrulanır (sunucuya
2655
+ * hiçbir şey gitmez); türetilen klasör anahtarı (KEK) YALNIZ bellekte yaşar
2656
+ * (`e2eRing`) — localStorage/sessionStorage'a asla yazılmaz. Upload şeffaf
2657
+ * şifrelenir, önizleme/indirme şeffaf çözülür (blob URL mevcut viewer'lara).
2658
+ */
2659
+ const e2eRing = createKeyRing();
2660
+ // Map'ler reaktif değil — sürüm sayacı computed'ları tetikler.
2661
+ const e2eRingVer = ref(0);
2662
+ // İçinde bulunulan şifreli kökün wire yolu ('' = şifreli bağlam yok).
2663
+ const e2eRoot = ref('');
2664
+ // path → çözülmüş blob objectURL (önizleme). Kilitleme/unmount'ta revoke.
2665
+ const e2eUrls = new Map<string, string>();
2666
+
2667
+ const e2eActive = computed(() => !!e2eRoot.value && !trashMode.value);
2668
+ const e2eUnlocked = computed(() => {
2669
+ void e2eRingVer.value;
2670
+ return e2eActive.value && e2eRing.has(e2eRoot.value);
2671
+ });
2672
+ const e2eLocked = computed(() => {
2673
+ void e2eRingVer.value;
2674
+ return e2eActive.value && !e2eRing.has(e2eRoot.value);
2675
+ });
2676
+
2677
+ // Kilit ekranı formu.
2678
+ const e2ePw = ref('');
2679
+ const e2eUnlockBusy = ref(false);
2680
+ const e2eUnlockErr = ref('');
2681
+ // Şifreli klasör oluşturma modalı.
2682
+ const showEncFolder = ref(false);
2683
+ const e2eCreateBusy = ref(false);
2684
+
2685
+ function e2eKek(): CryptoKey | null {
2686
+ return e2eRing.get(e2eRoot.value) ?? null;
2687
+ }
2688
+
2689
+ function e2eRevokeAll() {
2690
+ for (const url of e2eUrls.values()) URL.revokeObjectURL(url);
2691
+ e2eUrls.clear();
2692
+ }
2693
+ onBeforeUnmount(e2eRevokeAll);
2694
+
2695
+ /** Kilidi aç: marker'ı kökten çek, parolayı YERELDE doğrula, KEK'i belleğe koy. */
2696
+ async function e2eUnlock() {
2697
+ if (!e2ePw.value || e2eUnlockBusy.value || !e2eRoot.value) return;
2698
+ e2eUnlockBusy.value = true;
2699
+ e2eUnlockErr.value = '';
2700
+ try {
2701
+ let markerText = '';
2702
+ try {
2703
+ const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME));
2704
+ URL.revokeObjectURL(url);
2705
+ markerText = await blob.text();
2706
+ } catch {
2707
+ e2eUnlockErr.value = t('e2e.unlock.marker_missing');
2708
+ return;
2709
+ }
2710
+ const marker = parseMarker(markerText);
2711
+ if (!marker) {
2712
+ e2eUnlockErr.value = t('e2e.unlock.marker_missing');
2713
+ return;
2714
+ }
2715
+ const kek = await verifyPassword(marker, e2ePw.value);
2716
+ if (!kek) {
2717
+ e2eUnlockErr.value = t('e2e.unlock.wrong');
2718
+ return;
2719
+ }
2720
+ e2eRing.set(e2eRoot.value, kek);
2721
+ e2eRingVer.value++;
2722
+ e2ePw.value = '';
2723
+ } finally {
2724
+ e2eUnlockBusy.value = false;
2725
+ }
2726
+ }
2727
+
2728
+ /** "Kilitle": bellekteki anahtarı ve çözülmüş blob'ları at. */
2729
+ function e2eLock() {
2730
+ if (!e2eRoot.value) return;
2731
+ e2eRing.lock(e2eRoot.value);
2732
+ e2eRingVer.value++;
2733
+ e2eRevokeAll();
2734
+ flashToast(t('e2e.locked_toast'));
2735
+ }
2736
+
2737
+ // Uzantı → önizleme MIME'ı: çözülmüş blob'un <img>/<video>/<object>
2738
+ // etiketlerinde doğru render'ı için (sunucu şifreli dosyayı octet-stream
2739
+ // bilir, oradan gelen tip işe yaramaz).
2740
+ const E2E_MIME: Record<string, string> = {
2741
+ txt: 'text/plain', md: 'text/markdown', log: 'text/plain', csv: 'text/csv',
2742
+ json: 'application/json', xml: 'application/xml', html: 'text/html',
2743
+ jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
2744
+ webp: 'image/webp', bmp: 'image/bmp', avif: 'image/avif', svg: 'image/svg+xml',
2745
+ pdf: 'application/pdf',
2746
+ mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', m4v: 'video/mp4',
2747
+ mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', flac: 'audio/flac',
2748
+ m4a: 'audio/mp4', aac: 'audio/aac', opus: 'audio/opus',
2749
+ };
2750
+ function e2eMimeFor(n: FileNode): string {
2751
+ const ext = (n.extension || '').toLowerCase();
2752
+ return E2E_MIME[ext] || 'application/octet-stream';
2753
+ }
2754
+
2755
+ /**
2756
+ * Dosyayı çek + çöz + objectURL'ini cache'le. Magic'siz (ör. DAV'la düz
2757
+ * yazılmış) dosyada null döner — çağıran normal ham akışa düşer. Yanlış
2758
+ * anahtar/bozuk veri E2eDecryptError fırlatır (çağıran toast'lar).
2759
+ */
2760
+ async function e2eFetchDecrypted(n: FileNode): Promise<string | null> {
2761
+ const cached = e2eUrls.get(n.path);
2762
+ if (cached) return cached;
2763
+ const kek = e2eKek();
2764
+ if (!kek) return null;
2765
+ const buf = await api.fetchArrayBuffer(n.path);
2766
+ if (!hasMagic(buf)) return null;
2767
+ const plain = await decryptFile(kek, buf);
2768
+ const url = URL.createObjectURL(new Blob([plain], { type: e2eMimeFor(n) }));
2769
+ e2eUrls.set(n.path, url);
2770
+ return url;
2771
+ }
2772
+
2773
+ /** PreviewModal/QuickLook'a giden URL sağlayıcı: çözülmüş blob > ham URL. */
2774
+ function e2ePreviewSrc(p: string): string {
2775
+ return e2eUrls.get(p) ?? api.previewUrl(p);
2776
+ }
2777
+
2778
+ /** Çöz + salt-okunur in-page önizleme (openNode/previewNode buraya iner). */
2779
+ async function e2eOpenPreview(n: FileNode) {
2780
+ try {
2781
+ await e2eFetchDecrypted(n);
2782
+ } catch {
2783
+ flashToast(t('e2e.decrypt_failed'));
2784
+ return;
2785
+ }
2786
+ previewMode.value = 'view';
2787
+ previewTarget.value = n;
2788
+ showPreview.value = true;
2789
+ emit('file-opened', { path: n.path, basename: n.basename });
2790
+ void markRecent(n);
2791
+ }
2792
+
2793
+ /** Çöz + orijinal adla indir. Magic'siz dosya olduğu gibi iner. */
2794
+ async function e2eDownload(n: FileNode) {
2795
+ try {
2796
+ const buf = await api.fetchArrayBuffer(n.path);
2797
+ const kek = e2eKek();
2798
+ let out = buf;
2799
+ if (hasMagic(buf)) {
2800
+ if (!kek) throw new Error('locked');
2801
+ out = await decryptFile(kek, buf);
2802
+ }
2803
+ const url = URL.createObjectURL(new Blob([out], { type: e2eMimeFor(n) }));
2804
+ const a = document.createElement('a');
2805
+ a.href = url;
2806
+ a.download = n.basename;
2807
+ document.body.appendChild(a);
2808
+ a.click();
2809
+ a.remove();
2810
+ setTimeout(() => URL.revokeObjectURL(url), 30_000);
2811
+ } catch {
2812
+ flashToast(t('e2e.download.failed'));
2813
+ }
2814
+ }
2815
+
2816
+ /** Upload listesi → şifreli File listesi (200MB üstü + marker adı atlanır). */
2817
+ async function e2eEncryptUploads(list: File[]): Promise<File[]> {
2818
+ const kek = e2eKek();
2819
+ if (!kek) return [];
2820
+ const out: File[] = [];
2821
+ for (const f of list) {
2822
+ if (f.name === E2E_MARKER_NAME) continue;
2823
+ if (f.size > E2E_MAX_FILE_BYTES) {
2824
+ flashToast(t('e2e.upload.too_big'));
2825
+ continue;
2826
+ }
2827
+ try {
2828
+ const ct = await encryptFile(kek, await f.arrayBuffer());
2829
+ out.push(new File([ct], f.name, { type: 'application/octet-stream' }));
2830
+ } catch (err) {
2831
+ emit('error', { message: (err as Error).message, context: { op: 'e2e-encrypt', file: f.name } });
2832
+ }
2833
+ }
2834
+ return out;
2835
+ }
2836
+
2837
+ /** EncryptedFolderModal submit'i: klasörü aç + marker'ı yükle + kilidi açık bırak. */
2838
+ async function submitEncryptedFolder(payload: { name: string; password: string }) {
2839
+ if (e2eActive.value) {
2840
+ // İç içe şifreli klasör kök tespitini bulanıklaştırır — MVP'de yok.
2841
+ flashToast(t('e2e.create.nested'));
2842
+ return;
2843
+ }
2844
+ e2eCreateBusy.value = true;
2845
+ try {
2846
+ const dirWire = qualify(currentPath.value);
2847
+ await api.newFolder(dirWire, payload.name);
2848
+ const { marker, kek } = await createMarker(payload.password);
2849
+ const markerFile = new File([JSON.stringify(marker)], E2E_MARKER_NAME, {
2850
+ type: 'application/json',
2851
+ });
2852
+ const newDirWire = wireJoin(dirWire, payload.name);
2853
+ await api.uploadMultipart(newDirWire, [markerFile]);
2854
+ // Oluşturan oturumda kilit açık başlar (parolayı az önce kendisi girdi).
2855
+ e2eRing.set(newDirWire, kek);
2856
+ e2eRingVer.value++;
2857
+ showEncFolder.value = false;
2858
+ flashToast(t('e2e.create.done'));
2859
+ await load();
2860
+ } catch (err) {
2861
+ emit('error', { message: (err as Error).message, context: { op: 'e2e-create' } });
2862
+ flashToast(t('e2e.create.failed'));
2863
+ } finally {
2864
+ e2eCreateBusy.value = false;
2865
+ }
2866
+ }
2867
+ /* === /wiring:e2 === */
2535
2868
  </script>
2536
2869
 
2537
2870
  <template>
@@ -2569,7 +2902,7 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2569
2902
  <!-- /wiring:d1 -->
2570
2903
  <Toolbar
2571
2904
  ref="toolbarRef"
2572
- :view-mode="viewMode"
2905
+ :view-mode="displayedViewMode /* ui-fix — aktif panelin modu */"
2573
2906
  :search-query="searchQuery"
2574
2907
  :trash-active="trashActive"
2575
2908
  :actions="toolbarActions"
@@ -2585,7 +2918,7 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2585
2918
  :inspector-open="showInspector /* koru:k1 */"
2586
2919
  @toggle-inspector="toggleInspector /* koru:k1 */"
2587
2920
  @open-theme="showThemeGallery = true /* wiring:c1 */"
2588
- @update:view-mode="viewMode = $event"
2921
+ @update:view-mode="setDisplayedViewMode($event) /* ui-fix — aktif panele */"
2589
2922
  @update:search-query="searchQuery = $event"
2590
2923
  @update:density="density = $event"
2591
2924
  @open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
@@ -2627,6 +2960,17 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2627
2960
  </span>
2628
2961
  </div>
2629
2962
 
2963
+ <!-- wiring:e2 — kilit açık şeridi: şifreli klasörde anahtar bellekteyken
2964
+ görünür; "Kilitle" anahtarı ve çözülmüş blob'ları atar. -->
2965
+ <div v-if="e2eUnlocked" class="fe-e2e-strip" role="status">
2966
+ <span class="fe-e2e-strip__icon" aria-hidden="true">🔒</span>
2967
+ <span class="fe-e2e-strip__label">{{ t('e2e.strip.label') }}</span>
2968
+ <button type="button" class="fe-btn fe-e2e-strip__btn" @click="e2eLock">
2969
+ {{ t('e2e.strip.lock') }}
2970
+ </button>
2971
+ </div>
2972
+ <!-- /wiring:e2 -->
2973
+
2630
2974
  <!-- koru:k1 — fe__main lays the listing body and the inspector panel out
2631
2975
  as flex siblings (row). Without the inspector open it is visually
2632
2976
  identical to the previous direct-child fe__body. -->
@@ -2720,6 +3064,49 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2720
3064
  </details>
2721
3065
  <!-- /wiring:c4 -->
2722
3066
  </div>
3067
+ <!-- wiring:e2 — şifreli klasör kilit ekranı: parola doğru girilene dek
3068
+ listeleme render edilmez. Parola tarayıcıda marker'a karşı
3069
+ doğrulanır; sunucuya gitmez. -->
3070
+ <div v-else-if="e2eLocked" class="fe-state fe-e2e-lock">
3071
+ <svg
3072
+ class="fe-state__art"
3073
+ viewBox="0 0 120 100"
3074
+ width="110"
3075
+ height="92"
3076
+ fill="none"
3077
+ stroke="currentColor"
3078
+ stroke-width="2"
3079
+ stroke-linecap="round"
3080
+ stroke-linejoin="round"
3081
+ aria-hidden="true"
3082
+ >
3083
+ <rect x="38" y="44" width="44" height="34" rx="6" />
3084
+ <path d="M46 44v-8a14 14 0 0 1 28 0v8" />
3085
+ <circle cx="60" cy="59" r="3" fill="currentColor" stroke="none" />
3086
+ <path d="M60 62v7" />
3087
+ </svg>
3088
+ <p class="fe-state__title">{{ t('e2e.locked.title') }}</p>
3089
+ <p class="fe-state__hint">{{ t('e2e.locked.hint') }}</p>
3090
+ <form class="fe-e2e-lock__form" @submit.prevent="e2eUnlock">
3091
+ <input
3092
+ v-model="e2ePw"
3093
+ type="password"
3094
+ class="fe-input fe-e2e-lock__input"
3095
+ :placeholder="t('e2e.locked.pw_placeholder')"
3096
+ autocomplete="current-password"
3097
+ :disabled="e2eUnlockBusy"
3098
+ />
3099
+ <button
3100
+ type="submit"
3101
+ class="fe-btn fe-btn--primary"
3102
+ :disabled="e2eUnlockBusy || !e2ePw"
3103
+ >
3104
+ {{ e2eUnlockBusy ? t('e2e.locked.busy') : t('e2e.locked.unlock') }}
3105
+ </button>
3106
+ </form>
3107
+ <p v-if="e2eUnlockErr" class="fe-form__error" role="alert">{{ e2eUnlockErr }}</p>
3108
+ </div>
3109
+ <!-- /wiring:e2 -->
2723
3110
  <!-- Search with zero hits — its own message, not "folder is empty". -->
2724
3111
  <div v-else-if="!loading && files.length === 0 && searchQuery" class="fe-state">
2725
3112
  <svg
@@ -2862,6 +3249,8 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2862
3249
  :multi-root="multiStorageRoot"
2863
3250
  :virtual-rows="virtualStorageRows"
2864
3251
  :active="paneIsActive"
3252
+ :view-mode="paneViewMode /* ui-fix */"
3253
+ :thumb-src="thumbs.src /* ui-fix */"
2865
3254
  @navigate="onPaneNavigate"
2866
3255
  @activate="activePane = 'split'"
2867
3256
  @close="closeSplit"
@@ -2960,9 +3349,20 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2960
3349
  <NewFolderModal
2961
3350
  :open="showNewFolder"
2962
3351
  :locale="locale"
3352
+ :encrypted-option="!e2eActive /* wiring:e2 — iç içe şifreli klasör yok */"
2963
3353
  @close="showNewFolder = false"
2964
3354
  @submit="submitNewFolder"
3355
+ @encrypted="showNewFolder = false; showEncFolder = true /* wiring:e2 */"
3356
+ />
3357
+ <!-- wiring:e2 — şifreli klasör oluşturma modalı -->
3358
+ <EncryptedFolderModal
3359
+ :open="showEncFolder"
3360
+ :locale="locale"
3361
+ :busy="e2eCreateBusy"
3362
+ @close="showEncFolder = false"
3363
+ @submit="submitEncryptedFolder"
2965
3364
  />
3365
+ <!-- /wiring:e2 -->
2966
3366
  <RenameModal
2967
3367
  :open="showRename"
2968
3368
  :locale="locale"
@@ -2990,11 +3390,12 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
2990
3390
  :locale="locale"
2991
3391
  :file="previewTarget"
2992
3392
  :theme="config.theme || 'auto'"
2993
- :preview-url="(p) => api.previewUrl(p)"
2994
- :download-url="(p) => api.downloadUrl(p)"
2995
- :only-office-base="effectiveOnlyOfficeBase"
3393
+ :preview-url="(p) => e2ePreviewSrc(p) /* wiring:e2 — çözülmüş blob > ham URL */"
3394
+ :download-url="(p) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
3395
+ :only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 — OO ciphertext açamaz */"
2996
3396
  :only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
2997
- :save-text-endpoint="api.endpoints.saveText || null"
3397
+ :new-tab-enabled="!e2eActive /* wiring:e2 — standalone rota ham baytı çeker */"
3398
+ :save-text-endpoint="e2eActive ? null : api.endpoints.saveText || null /* wiring:e2 — düz metin kaydı sızıntı olur */"
2998
3399
  :open-mode="previewMode"
2999
3400
  :auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
3000
3401
  :auth-credentials="api.credentialsMode()"
@@ -3096,7 +3497,7 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
3096
3497
  @navigate="(p: string) => load(p)"
3097
3498
  @new-folder="showNewFolder = true"
3098
3499
  @upload="triggerUpload"
3099
- @toggle-view="viewMode = viewMode === 'list' ? 'grid' : viewMode === 'grid' ? 'gallery' : 'list' /* wiring:d2 — 3 mod döngüsü */"
3500
+ @toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3 mod döngüsü, aktif panele */"
3100
3501
  @open-trash="loadTrash"
3101
3502
  @refresh="() => load()"
3102
3503
  @go-up="goUp"
@@ -3165,9 +3566,9 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
3165
3566
  :locale="locale"
3166
3567
  :file="quickLookTarget"
3167
3568
  :theme="config.theme || 'auto'"
3168
- :preview-url="(p: string) => api.previewUrl(p)"
3169
- :download-url="(p: string) => api.downloadUrl(p)"
3170
- :only-office-base="effectiveOnlyOfficeBase"
3569
+ :preview-url="(p: string) => e2ePreviewSrc(p) /* wiring:e2 */"
3570
+ :download-url="(p: string) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
3571
+ :only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 */"
3171
3572
  :only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
3172
3573
  :auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
3173
3574
  :auth-credentials="api.credentialsMode()"