@brftech/filex-core 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +26 -2
  2. package/dist/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
  3. package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
  4. package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
  5. package/dist/DrawioViewer-BNALOB04.js.map +1 -0
  6. package/dist/filex-core.js +9346 -8069
  7. package/dist/filex-core.js.map +1 -1
  8. package/dist/filex-core.umd.cjs +49 -49
  9. package/dist/filex-core.umd.cjs.map +1 -1
  10. package/dist/index.d.ts +235 -16
  11. package/dist/style.css +1 -1
  12. package/package.json +1 -1
  13. package/src/FileExplorer.vue +776 -246
  14. package/src/components/CommandPalette.vue +21 -5
  15. package/src/components/ConnectionsPanel.vue +2 -1
  16. package/src/components/E2eRecoveryUnlockModal.vue +29 -4
  17. package/src/components/FilterBar.vue +244 -0
  18. package/src/components/GalleryView.vue +12 -1
  19. package/src/components/GridView.vue +48 -5
  20. package/src/components/InspectorPanel.vue +231 -8
  21. package/src/components/ListView.vue +12 -3
  22. package/src/components/NFSExportsPanel.vue +2 -1
  23. package/src/components/PendingOpsTray.vue +1 -1
  24. package/src/components/PresenceBar.vue +3 -2
  25. package/src/components/S3KeysPanel.vue +2 -1
  26. package/src/components/SSHKeysPanel.vue +2 -1
  27. package/src/components/SecondaryPane.vue +28 -13
  28. package/src/components/SideNav.vue +161 -1
  29. package/src/components/StarButton.vue +2 -1
  30. package/src/components/TabBar.vue +2 -2
  31. package/src/components/ThemeGallery.vue +1 -1
  32. package/src/components/TokensPanel.vue +2 -1
  33. package/src/components/Toolbar.vue +244 -58
  34. package/src/components/ViewSwitcher.vue +81 -0
  35. package/src/composables/useFileApi.ts +49 -2
  36. package/src/composables/useLocale.ts +1 -1
  37. package/src/composables/useOperations.ts +3 -3
  38. package/src/composables/usePendingOps.ts +1 -1
  39. package/src/composables/useTabs.ts +1 -1
  40. package/src/composables/useUploadChunked.ts +39 -7
  41. package/src/index.ts +8 -2
  42. package/src/lib/e2ecrypto.ts +191 -3
  43. package/src/lib/fileFilters.ts +143 -0
  44. package/src/lib/listing.ts +47 -0
  45. package/src/lib/themes.ts +7 -7
  46. package/src/lib/transfer.ts +6 -6
  47. package/src/locales/en.ts +115 -0
  48. package/src/locales/index.ts +1 -0
  49. package/src/locales/resolve.ts +66 -0
  50. package/src/locales/tr.ts +116 -0
  51. package/src/modals/ConvertModal.vue +14 -12
  52. package/src/modals/NewFolderModal.vue +1 -1
  53. package/src/modals/PermissionsModal.vue +30 -13
  54. package/src/modals/PreviewModal.vue +9 -9
  55. package/src/styles/base.css +520 -26
  56. package/src/styles/variables.css +1 -1
  57. package/src/types/ExplorerConfig.ts +18 -2
  58. package/src/types/FileNode.ts +1 -1
  59. package/src/viewers/CsvViewer.vue +4 -4
  60. package/src/viewers/DrawioViewer.vue +2 -2
  61. package/dist/CsvViewer-DxujFEM3.js.map +0 -1
  62. package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
@@ -46,6 +46,14 @@ import RecentlyOpened from './components/RecentlyOpened.vue';
46
46
  import Breadcrumb from './components/Breadcrumb.vue';
47
47
  import ListView from './components/ListView.vue';
48
48
  import GridView from './components/GridView.vue';
49
+ import FilterBar from './components/FilterBar.vue' /* surucu:d1 */;
50
+ import ViewSwitcher from './components/ViewSwitcher.vue' /* surucu:d1 */;
51
+ import {
52
+ EMPTY_FILTERS,
53
+ applyFilters,
54
+ filtersActive,
55
+ type DriveFilters,
56
+ } from './lib/fileFilters' /* surucu:d1 */;
49
57
  import GalleryView from './components/GalleryView.vue'; /* wiring:d2 */
50
58
  import ContextMenu, { type ContextAction } from './components/ContextMenu.vue';
51
59
  import UploadProgress from './components/UploadProgress.vue';
@@ -79,12 +87,12 @@ import { useOperations } from './composables/useOperations';
79
87
  /* wiring:c4 */
80
88
  import OnboardingTour from './components/OnboardingTour.vue';
81
89
  /* /wiring:c4 */
82
- /* wiring:d1 — sekmeler + tab başına split */
90
+ /* wiring:d1 — tabs + per-tab split */
83
91
  import TabBar from './components/TabBar.vue';
84
92
  import SecondaryPane from './components/SecondaryPane.vue';
85
93
  import { useTabs, type TabState } from './composables/useTabs';
86
94
  /* /wiring:d1 */
87
- /* wiring:e2 — uçtan uca şifreli klasörler (docs/E2E-ENCRYPTION.md) */
95
+ /* wiring:e2 — end-to-end encrypted folders (docs/E2E-ENCRYPTION.md) */
88
96
  import EncryptedFolderModal from './components/EncryptedFolderModal.vue';
89
97
  import RecoveryKeyModal from './components/RecoveryKeyModal.vue';
90
98
  import E2eRecoveryUnlockModal from './components/E2eRecoveryUnlockModal.vue';
@@ -92,13 +100,16 @@ import {
92
100
  createKeyRing,
93
101
  createEncryptedFolder,
94
102
  upgradeMarkerV1,
103
+ addEscrowSlot,
104
+ declineEscrowSlot,
105
+ escrowOfferState,
95
106
  parseMarker,
96
107
  unlockWithPassword,
97
108
  unlockWithRecoveryKey,
98
109
  unlockWithEscrowKey,
99
110
  importEscrowPrivateKey,
100
111
  markerHasRecovery,
101
- markerHasEscrow,
112
+ escrowAvailability,
102
113
  bytesToB64,
103
114
  b64ToBytes,
104
115
  encryptFile,
@@ -147,6 +158,7 @@ import ShareModal from './modals/ShareModal.vue';
147
158
  import PreviewModal from './modals/PreviewModal.vue';
148
159
  import ConvertModal from './modals/ConvertModal.vue';
149
160
  import PermissionsModal from './modals/PermissionsModal.vue';
161
+ import { resolveLocale } from './locales/resolve';
150
162
 
151
163
  const props = defineProps<{
152
164
  config: ExplorerConfig;
@@ -176,7 +188,7 @@ const api = useFileApi(props.config);
176
188
  // Locale up-front: the pendingOps onSettled callback below (and the undo-toast
177
189
  // helpers) need `t()` at runtime, so the catalogue must be constructed before
178
190
  // they are wired. Depends only on props — safe this early.
179
- const locale = computed(() => props.config.locale || 'tr');
191
+ const locale = computed(() => resolveLocale(props.config.locale));
180
192
  const { t } = useLocale(locale);
181
193
 
182
194
  // Live collaboration (WebSocket file-change events + presence), bundled into the
@@ -228,20 +240,20 @@ const pendingOps = usePendingOps(props.config, api, {
228
240
  const undo = opUndo.get(op.id);
229
241
  opUndo.delete(op.id);
230
242
  if (op.status === 'error') {
231
- flashToast(op.error_message || 'İşlem başarısız');
243
+ flashToast(op.error_message || t('toast.failed'));
232
244
  } else if (undo) {
233
245
  undoToast(`${undo.message} (${op.progress_total})`, undo.fn);
234
246
  } else {
235
247
  const verb =
236
248
  op.op_type === 'copy'
237
- ? 'Kopyalandı'
249
+ ? t('toast.copied')
238
250
  : op.op_type === 'move'
239
- ? 'Taşındı'
240
- : 'Silindi';
251
+ ? t('toast.moved')
252
+ : t('toast.deleted');
241
253
  flashToast(`${verb} (${op.progress_total})`);
242
254
  }
243
255
  void load();
244
- void splitPaneRef.value?.reload(); /* wiring:d1 — ikincil panel de tazelenir */
256
+ void splitPaneRef.value?.reload(); /* wiring:d1 — refresh the secondary pane too */
245
257
  },
246
258
  });
247
259
 
@@ -496,8 +508,16 @@ function onStarChange(n: FileNode, value: boolean) {
496
508
  * this calls it. A menu cannot render a component, but it must not grow its
497
509
  * own fetch either — that is the second path that drifts.
498
510
  */
499
- /** Which of `targets` can carry a star: files the server knows by id. */
511
+ /** Which of `targets` can carry a star: files the server knows by id.
512
+ *
513
+ * Empty when the identity surfaces are suppressed, which is the one
514
+ * chokepoint the context menu, the toolbar and the keyboard action all go
515
+ * through — so the affordance disappears everywhere at once rather than in
516
+ * the two places somebody remembered. Offering to star a file while the
517
+ * Starred view is hidden would write into a list the person cannot open,
518
+ * and under a shared app token that list belongs to everyone at once. */
500
519
  function starableNodes(targets: FileNode[]): FileNode[] {
520
+ if (!identitySurfaces.value) return [];
501
521
  return targets.filter((n) => typeof n.id === 'number' && n.type === 'file');
502
522
  }
503
523
 
@@ -642,8 +662,9 @@ const showDelete = ref(false);
642
662
  const showShare = ref(false);
643
663
  const showPreview = ref(false);
644
664
  const renameTarget = ref<FileNode | null>(null);
645
- /* ui-fix — açık olan rename/delete/new-folder modal yan panele mi ait?
646
- * (menü ana panelle aynı; mutasyonlar doğru panele yönlensin diye.) */
665
+ /* ui-fix — does the open rename/delete/new-folder modal belong to the side
666
+ * pane? (the menu is identical to the main pane's; this routes the mutation
667
+ * to the right one.) */
647
668
  const mutationInPane = ref(false);
648
669
  const shareTarget = ref<FileNode | null>(null);
649
670
  const activeShare = ref<(ShareInfo & { url: string; filename?: string }) | null>(null);
@@ -700,7 +721,17 @@ function closeInspector() {
700
721
  * the "one behaviour on one surface" split this shared package exists to
701
722
  * prevent. */
702
723
  const uiProfile = computed(() => props.config.uiProfile ?? 'standard');
703
- const simpleUi = computed(() => uiProfile.value === 'simple');
724
+ /**
725
+ * ⚠ `drive` answers TRUE here. It is a superset of `simple`, so every question
726
+ * `simple` already answers ("one pane?", "no tab strip?", "list and grid
727
+ * only?") must keep the same answer under it — asking `=== 'simple'` in those
728
+ * places is how the drive profile would silently grow a split pane the day
729
+ * somebody adds a fourth condition. `driveShell` is only for what `drive` adds
730
+ * on TOP.
731
+ */
732
+ const simpleUi = computed(() => uiProfile.value === 'simple' || uiProfile.value === 'drive');
733
+ /* === surucu:d1 — the Drive shell (GitHub #14, the reporter's mockups) ===== */
734
+ const driveShell = computed(() => uiProfile.value === 'drive');
704
735
 
705
736
  const SIDENAV_LS_KEY = 'filex.sidenav';
706
737
  const sideNavExpanded = ref<boolean>(
@@ -740,6 +771,31 @@ const sideNavEnabled = computed(() => props.config.sideNav ?? !rootPathProp);
740
771
  const navVisible = computed(
741
772
  () => sideNavEnabled.value && (isNarrow.value ? navDrawerOpen.value : true),
742
773
  );
774
+ /**
775
+ * Is the navigation panel already offering Trash as a destination?
776
+ *
777
+ * ⚠ Keyed to `sideNavEnabled`, NOT to `navVisible`. Three cases decided here,
778
+ * and each one is a judgement about whether the person has a way to the bin
779
+ * that is not this row:
780
+ *
781
+ * - collapsed to the icon rail → OFFERING. The entry is still rendered, still
782
+ * one click, still labelled for a screen reader; only its text is gone.
783
+ * - the drawer under 560px, closed → OFFERING. The toolbar's panel toggle is
784
+ * on screen at every width, so the destination is one tap away. Keying this
785
+ * to `navVisible` instead would add and remove a row from the LISTING every
786
+ * time somebody opened or closed the drawer — the folder changing under
787
+ * them for a reason that has nothing to do with the folder.
788
+ * - no panel at all (`sideNav: false`, or the default under `rootPath`) → NOT
789
+ * offering, and the row stays. That is the case it was invented for: a
790
+ * listing with no other door to the bin.
791
+ *
792
+ * `trashVisible: false` is handled inside the helper and outranks all of it —
793
+ * it means no Trash anywhere, panel entry included.
794
+ */
795
+ const navOffersTrash = computed(
796
+ () => sideNavEnabled.value && props.config.trashVisible !== false,
797
+ );
798
+
743
799
  /** What the toolbar toggle reports as pressed. */
744
800
  const navToggleOn = computed(() =>
745
801
  isNarrow.value ? navDrawerOpen.value : sideNavExpanded.value,
@@ -840,6 +896,130 @@ watchEffect(() => {
840
896
  if (simpleUi.value && viewMode.value === 'gallery') viewMode.value = 'grid';
841
897
  });
842
898
 
899
+ /* === surucu:d1 — the filter row =========================================
900
+ * Three chips over the listing in hand: Type · Modified · Size.
901
+ *
902
+ * ⚠ CLIENT-SIDE, and that is the honest place for them, not a shortcut. The
903
+ * listing endpoint reads exactly six parameters (`action`, `path`, `filter`,
904
+ * `storage`, `parent`, `cache`) and has no `limit`/`offset` either — so the
905
+ * rows in hand ARE the folder, and filtering them here answers the whole
906
+ * question rather than "the first page of it". Wiring a chip to a `min_size`
907
+ * the server never reads would look identical and change nothing.
908
+ *
909
+ * ⚠ Reset on navigation. A filter that survives a folder change makes the next
910
+ * folder look empty, and the reason is off-screen the moment you scroll.
911
+ */
912
+ const driveFilters = ref<DriveFilters>({ ...EMPTY_FILTERS });
913
+ const filtersOn = computed(() => driveShell.value && filtersActive(driveFilters.value));
914
+ /** What the views render. Identical reference to `files` when nothing is set. */
915
+ const displayFiles = computed<FileNode[]>(() =>
916
+ driveShell.value ? applyFilters(files.value, driveFilters.value) : files.value,
917
+ );
918
+ function clearDriveFilters() {
919
+ setDriveFilters({ ...EMPTY_FILTERS });
920
+ }
921
+ function setDriveFilters(v: DriveFilters) {
922
+ driveFilters.value = v;
923
+ // A selection the filter just hid would still be what Delete acts on, with
924
+ // nothing on screen to say so.
925
+ selection.clear();
926
+ }
927
+ watch(
928
+ () => `${currentPath.value}|${navView.value}`,
929
+ () => {
930
+ if (filtersActive(driveFilters.value)) driveFilters.value = { ...EMPTY_FILTERS };
931
+ },
932
+ );
933
+
934
+ /**
935
+ * surucu:d1 — what the header field says it will search: the folder you are
936
+ * standing in, by name. At a storage root that is the storage; in a panel view
937
+ * ("Recent", a tag) it is that view's own name, because searching from there
938
+ * searches what is on screen.
939
+ */
940
+ const driveScopeLabel = computed(() => {
941
+ if (navView.value === 'tag') return navTag.value;
942
+ if (navView.value) return t(`sidenav.${navView.value}`);
943
+ const rel = currentPath.value.replace(/\/+$/, '');
944
+ const last = rel.split('/').filter(Boolean).pop();
945
+ return last || adapter.value || '';
946
+ });
947
+
948
+ /**
949
+ * surucu:d1 — the ⌘K escalation. The header field searches THIS folder (it
950
+ * sets `searchQuery`, which the loader answers with `action=search`); this
951
+ * hands the same words to the command palette, which is where "everywhere",
952
+ * the saved searches and the commands live. One box, one shortcut, and the
953
+ * hint printed on the box does what it says.
954
+ */
955
+ const paletteSeed = ref('');
956
+ function openPaletteWith(q: string) {
957
+ paletteSeed.value = q;
958
+ showPalette.value = true;
959
+ }
960
+
961
+ /**
962
+ * surucu:d1 — "Request files" from the New menu: the access modal on the
963
+ * CURRENT folder, opened on its file-drop tab. The modal already owns that
964
+ * surface; this only gives it a target, since the folder you are standing in
965
+ * is not a selected row and has no FileNode of its own.
966
+ */
967
+ const permInitialTab = ref<'perms' | 'share' | 'drop' | undefined>(undefined);
968
+ function openFileRequest() {
969
+ const path = qualify(currentPath.value);
970
+ if (!path) return;
971
+ const segs = currentPath.value.split('/').filter(Boolean);
972
+ permTarget.value = {
973
+ path,
974
+ basename: segs.length ? segs[segs.length - 1] : adapter.value,
975
+ type: 'dir',
976
+ };
977
+ permInitialTab.value = 'drop';
978
+ showPerm.value = true;
979
+ }
980
+
981
+ /** surucu:d1 — a link minted from the details panel; same event the share
982
+ * dialog emits, so a host listening for `share-created` hears both. */
983
+ function onInspectorShareCreated(payload: { path: string; url: string }) {
984
+ emit('share-created', { path: payload.path, url: payload.url, pin: null });
985
+ flashToast(t('inspector.copied'));
986
+ }
987
+
988
+ /* === surucu:d1 — the storage line under the navigation ==================
989
+ * Fetched once per mount, and ONLY in the drive shell: no other profile draws
990
+ * it, and an explorer that has drawn this panel for a year should not start
991
+ * making a request it has no use for. `quotaMe()` answers null for a server
992
+ * without the route or a caller without a person behind it, and null renders
993
+ * nothing at all.
994
+ */
995
+ const quotaSnapshot = ref<{ used: number; total: number; unlimited: boolean } | null>(null);
996
+ async function loadQuota() {
997
+ if (!driveShell.value || !identitySurfaces.value) {
998
+ quotaSnapshot.value = null;
999
+ return;
1000
+ }
1001
+ const q = await api.quotaMe();
1002
+ if (!q) {
1003
+ quotaSnapshot.value = null;
1004
+ return;
1005
+ }
1006
+ const unlimited = !!q.unlimited || q.quota_bytes <= 0;
1007
+ // ⚠ No ceiling AND nothing counted = nothing to say, so say nothing.
1008
+ //
1009
+ // Measured, not guessed: `used_bytes` is `SUM(nodes.size) WHERE owner_id = me`
1010
+ // and `nodes.owner_id` is set by the UPLOAD path only — it is nil for every
1011
+ // file a storage sync discovered (handlers/shared.go). On an install whose
1012
+ // drives were mounted rather than uploaded into, the honest figure is
1013
+ // therefore "0 B", and a line reading "0 B used" under a drive visibly full
1014
+ // of files reads as a broken widget, not as a fact about quota. With a quota
1015
+ // set the line still earns its place — the ceiling is real and uploads count
1016
+ // against it — so only the no-quota-no-usage case is dropped.
1017
+ quotaSnapshot.value =
1018
+ unlimited && q.used_bytes <= 0
1019
+ ? null
1020
+ : { used: q.used_bytes, total: q.quota_bytes, unlimited };
1021
+ }
1022
+
843
1023
  /**
844
1024
  * nodeRowToFileNode — the starred / recently-opened endpoints answer with raw
845
1025
  * node rows (relative `path`, numeric `storage_id`), not the listing shape.
@@ -1328,7 +1508,7 @@ async function load(path?: string) {
1328
1508
  currentPath.value = '';
1329
1509
  adapter.value = '';
1330
1510
  dirname.value = '';
1331
- e2eRoot.value = ''; /* wiring:e2 — sanal kökte kilit ekranı olmaz */
1511
+ e2eRoot.value = ''; /* wiring:e2 — no lock screen at the virtual root */
1332
1512
  files.value = virtualStorageRows();
1333
1513
  return;
1334
1514
  }
@@ -1350,7 +1530,16 @@ async function load(path?: string) {
1350
1530
  files.value = filterListing(resp.files);
1351
1531
  // Inject virtual `.trash` entry at root only — shared helper so the
1352
1532
  // split-view secondary pane shows the exact same row (no row-offset).
1353
- if (injectTrashRow(files.value, resp.adapter, resp.dirname, props.config.trashVisible !== false)) {
1533
+ // …and NOT into a search result. The search response's `dirname` is the
1534
+ // scope it searched, so it is the storage root exactly as an `index` of
1535
+ // that folder would be; only this call site knows which of the two it just
1536
+ // asked for.
1537
+ if (
1538
+ injectTrashRow(files.value, resp.adapter, resp.dirname, props.config.trashVisible !== false, {
1539
+ isSearchResult: !!searchQuery.value,
1540
+ navOffersTrash: navOffersTrash.value,
1541
+ })
1542
+ ) {
1354
1543
  void hydrateTrashRowShared(files.value, resp.adapter, api);
1355
1544
  }
1356
1545
  // currentPath is the user-facing form: `s3-test/example` in
@@ -1366,7 +1555,7 @@ async function load(path?: string) {
1366
1555
  // show the dedicated not-found state instead of a toast over a stale
1367
1556
  // listing that reads as "this folder is empty".
1368
1557
  notFoundPath.value = String(requested);
1369
- e2eRoot.value = ''; /* wiring:e2 — ölü linkte kilit ekranı kalmasın */
1558
+ e2eRoot.value = ''; /* wiring:e2 — no lock screen left over on a dead link */
1370
1559
  files.value = [];
1371
1560
  emit('error', { message: e, context: { path } });
1372
1561
  return;
@@ -1412,7 +1601,7 @@ async function loadTrash() {
1412
1601
  loading.value = true;
1413
1602
  trashOrigin.value = adapter.value || '';
1414
1603
  trashMode.value = true;
1415
- e2eRoot.value = ''; /* wiring:e2 — çöp görünümü şifreli bağlamın dışındadır */
1604
+ e2eRoot.value = ''; /* wiring:e2 — the trash view is outside the encrypted context */
1416
1605
  selection.clear();
1417
1606
  try {
1418
1607
  const { entries } = await api.listTrash();
@@ -1491,10 +1680,10 @@ function wireParent(p: string): string {
1491
1680
  return slash === -1 ? prefix : prefix + rel.slice(0, slash);
1492
1681
  }
1493
1682
 
1494
- /* ui-fix — iki wire yolun AYNI dizini gösterip göstermediği (trailing-slash
1495
- * ve kök `adapter://` güvenli). Bir öğeyi ZATEN bulunduğu klasöre bırakma
1496
- * (source parent === target) no-op olmalı; aksi halde backend "kendine
1497
- * kopyala" 400'ü döner. */
1683
+ /* ui-fix — do two wire paths point at the SAME directory (safe against a
1684
+ * trailing slash and against the bare `adapter://` root)? Dropping an item
1685
+ * into the folder it is ALREADY in (source parent === target) must be a
1686
+ * no-op; otherwise the backend answers with a "copy onto itself" 400. */
1498
1687
  function sameDir(a: string, b: string): boolean {
1499
1688
  const norm = (s: string) => {
1500
1689
  const i = s.indexOf('://');
@@ -1655,6 +1844,9 @@ onMounted(async () => {
1655
1844
  module-level cache in lib/tags.ts means several explorers on one page
1656
1845
  still cost a single query. */
1657
1846
  void loadNavTags();
1847
+ /* surucu:d1 — the storage line. After the listing, like the tag list: it is
1848
+ a status, and the folder somebody asked for is the critical path. */
1849
+ void loadQuota();
1658
1850
  /* ⚠ The panel is not always on screen at mount. Below 560px it is a DRAWER
1659
1851
  that starts closed, so `navVisible` is false and the call above returns
1660
1852
  without asking for anything — measured at 390px: the drawer opened with
@@ -1741,7 +1933,7 @@ async function openSearchHit(hit: GlobalSearchHit) {
1741
1933
 
1742
1934
  useKeyboardShortcuts(rootEl, {
1743
1935
  onDelete: () => {
1744
- /* ui-fix — kısayol da aktif panele gider (menüyle tutarlı). */
1936
+ /* ui-fix — the shortcut goes to the active pane too (consistent with the menu). */
1745
1937
  if (paneIsActive.value) {
1746
1938
  const psel = splitPaneRef.value?.selectedNodes() ?? [];
1747
1939
  if (psel.length) {
@@ -1806,6 +1998,11 @@ useKeyboardShortcuts(rootEl, {
1806
1998
  onGoUp: () => (paneIsActive.value ? splitPaneRef.value?.goUp() : goUp()) /* wiring:d1 pane-route */,
1807
1999
  /* cila:c wiring */
1808
2000
  onPathJump: () => {
2001
+ /* surucu:d1 — the shortcut from anywhere else opens a BLANK palette. The
2002
+ seed belongs to the drive header's field; leaving the last one in place
2003
+ would reopen the palette pre-filled with a query the user has since
2004
+ moved on from. */
2005
+ paletteSeed.value = '';
1809
2006
  showPalette.value = true;
1810
2007
  },
1811
2008
  onShowHelp: () => {
@@ -1816,7 +2013,7 @@ useKeyboardShortcuts(rootEl, {
1816
2013
  onToggleHidden: () => toggleHiddenFiles(),
1817
2014
  onStar: () => void toggleStar(selection.nodes.value) /* yildiz:s1 */,
1818
2015
  onToggleInspector: () => toggleInspector() /* koru:k1 */,
1819
- /* wiring:d1 — sekme aksiyonları (registry: tab-new/close/next/prev) */
2016
+ /* wiring:d1 — tab actions (registry: tab-new/close/next/prev) */
1820
2017
  onTabNew: () => newTabHere(),
1821
2018
  onTabClose: () => closeTabById(tabsActiveId.value),
1822
2019
  onTabNext: () => nextTab(),
@@ -1865,9 +2062,10 @@ function openNode(n: FileNode) {
1865
2062
  void load(target);
1866
2063
  return;
1867
2064
  }
1868
- /* wiring:e2 — şifreli klasörde dosya açma: kilit açıksa çöz + salt-okunur
1869
- önizleme (blob URL mevcut viewer'lara gider); kilitliyken hiçbir şey
1870
- açılmaz (kilit ekranı zaten listeyi kapatır). */
2065
+ /* wiring:e2 — opening a file inside an encrypted folder: while unlocked,
2066
+ decrypt + show a read-only preview (the blob URL feeds the existing
2067
+ viewers); while locked nothing opens at all (the lock screen already
2068
+ hides the listing). */
1871
2069
  if (e2eUnlocked.value && n.type === 'file') {
1872
2070
  void e2eOpenPreview(n);
1873
2071
  return;
@@ -1931,7 +2129,7 @@ async function restoreSelection(targets?: FileNode[]) {
1931
2129
  .map((n) => (n as { id?: number }).id)
1932
2130
  .filter((x): x is number => typeof x === 'number');
1933
2131
  const { restored } = await api.restoreIds(ids);
1934
- flashToast(`${restored} öğe geri getirildi`);
2132
+ flashToast(t('toast.restored', { n: restored }));
1935
2133
  selection.clear();
1936
2134
  await loadTrash();
1937
2135
  return;
@@ -1940,7 +2138,7 @@ async function restoreSelection(targets?: FileNode[]) {
1940
2138
  if (!api.endpoints.restore) return;
1941
2139
  const items = nodes.map((n) => n.path); // qualified
1942
2140
  const { restored } = await api.restore(items);
1943
- flashToast(`${restored} öğe geri getirildi`);
2141
+ flashToast(t('toast.restored', { n: restored }));
1944
2142
  selection.clear();
1945
2143
  await load();
1946
2144
  } catch (err) {
@@ -1949,7 +2147,7 @@ async function restoreSelection(targets?: FileNode[]) {
1949
2147
  }
1950
2148
 
1951
2149
  function previewNode(n: FileNode) {
1952
- /* wiring:e2 — önizleme de çözülmüş blob'dan beslenir */
2150
+ /* wiring:e2 — the preview is fed from the decrypted blob as well */
1953
2151
  if (e2eUnlocked.value && n.type === 'file') {
1954
2152
  void e2eOpenPreview(n);
1955
2153
  return;
@@ -1976,8 +2174,9 @@ function openNodeInNewTab(n: FileNode) {
1976
2174
  void load(target);
1977
2175
  return;
1978
2176
  }
1979
- /* wiring:e2 — standalone editör rotası sunucudan HAM (şifreli) baytı
1980
- çeker; şifreli klasörde her "Aç" in-page çözülmüş önizlemeye iner. */
2177
+ /* wiring:e2 — the standalone editor route pulls the RAW (encrypted) bytes
2178
+ from the server, so inside an encrypted folder every "Aç" falls back to
2179
+ the in-page decrypted preview. */
1981
2180
  if (e2eActive.value) {
1982
2181
  if (e2eUnlocked.value) void e2eOpenPreview(n);
1983
2182
  return;
@@ -2012,10 +2211,10 @@ function openNodeInNewTab(n: FileNode) {
2012
2211
  void markRecent(n);
2013
2212
  }
2014
2213
 
2015
- type ContextMode = 'selection' | 'breadcrumb' | 'pane' /* ui-fix — yan panel sağ-tık */;
2214
+ type ContextMode = 'selection' | 'breadcrumb' | 'pane' /* ui-fix — side-pane right-click */;
2016
2215
  const ctxMode = ref<ContextMode>('selection');
2017
2216
  const breadcrumbCtxPath = ref<string>('');
2018
- /* ui-fix — yan panel sağ-tık menüsünün hedef node'ları (pane selection). */
2217
+ /* ui-fix — target nodes of the side pane's right-click menu (pane selection). */
2019
2218
  const paneCtxTargets = ref<FileNode[]>([]);
2020
2219
  const breadcrumbCtxLabel = ref<string>('');
2021
2220
 
@@ -2220,15 +2419,17 @@ function onCrumbContext(payload: { x: number; y: number; adapterPath: string; la
2220
2419
  ctxRef.value?.show({ clientX: payload.x, clientY: payload.y }, []);
2221
2420
  }
2222
2421
 
2223
- /* ui-fix — yan (ikincil) panelde sağ-tık: pane'i aktifleştir + menüyü aç.
2224
- * Menü ana panelle BİREBİR aynı (selectionActionList tek kaynak); aksiyonlar
2225
- * dispatchItemAction'a gider ve ctxMode==='pane' iken pane-route'lanır. */
2422
+ /* ui-fix — right-click in the side (secondary) pane: activate the pane, then
2423
+ * open the menu. The menu is EXACTLY the main pane's (selectionActionList is
2424
+ * the single source); actions go to dispatchItemAction and are pane-routed
2425
+ * while ctxMode==='pane'. */
2226
2426
  function onPaneContext(node: FileNode | null, ev: MouseEvent) {
2227
2427
  activePane.value = 'split';
2228
2428
  void refreshKept();
2229
2429
  const sel = splitPaneRef.value?.selectedNodes() ?? [];
2230
- // node=null (boş alana sağ-tık): seçimsiz menü (Yeni Klasör + Yapıştır).
2231
- // Aksi halde pane seçimi (yoksa tıklanan node) hedeftir.
2430
+ // node=null (right-click on empty space): the selection-less menu
2431
+ // ("Yeni Klasör" + "Yapıştır"). Otherwise the pane selection is the target
2432
+ // (falling back to the clicked node).
2232
2433
  paneCtxTargets.value = node ? (sel.length > 0 ? sel : [node]) : [];
2233
2434
  ctxMode.value = 'pane';
2234
2435
  ctxRef.value?.show({ clientX: ev.clientX, clientY: ev.clientY }, paneCtxTargets.value);
@@ -2241,10 +2442,10 @@ const contextActions = computed<ContextAction[]>(() => {
2241
2442
  { key: 'copy-path', label: t('breadcrumb.copy_path'), icon: '📋' },
2242
2443
  ];
2243
2444
  }
2244
- if (ctxMode.value === 'pane' /* ui-fix — yan panel menüsü ana panelle BİREBİR aynı */) {
2445
+ if (ctxMode.value === 'pane' /* ui-fix — side-pane menu is EXACTLY the main pane's */) {
2245
2446
  const psel = paneCtxTargets.value;
2246
2447
  if (psel.length === 0) {
2247
- // Boş alana sağ-tık: ana paneldeki canvas menüsüyle aynı.
2448
+ // Right-click on empty space: same as the main pane's canvas menu.
2248
2449
  return [
2249
2450
  { key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' },
2250
2451
  { key: 'paste', label: t('ctx.paste'), icon: '📋', disabled: !clipboard.value.mode },
@@ -2275,7 +2476,7 @@ const contextActions = computed<ContextAction[]>(() => {
2275
2476
  // PRIOR BUG: this used `currentPath === '/'` but the load() branch
2276
2477
  // for the virtual root sets currentPath to EMPTY string, not '/'.
2277
2478
  // So the guard never fired and every mutation action leaked into
2278
- // the menu at the depo listing — including new-folder + paste,
2479
+ // the menu at the storage listing — including new-folder + paste,
2279
2480
  // which Ada called out in the most direct possible terms. Use
2280
2481
  // the same empty-after-trim test as `atVirtualRoot` above.
2281
2482
  const trimmedPath = (currentPath.value ?? '').replace(/^\/+|\/+$/g, '');
@@ -2317,9 +2518,10 @@ const contextActions = computed<ContextAction[]>(() => {
2317
2518
 
2318
2519
  // selectionActionList — the SINGLE source of truth for the actions offered on a
2319
2520
  // selection. BOTH the right-click context menu AND the top toolbar render this
2320
- // exact list so they can never drift apart (Ada: "sağ klik menüyle üst menü
2321
- // tutmuyor"). The toolbar filters out dividers/hidden; the context menu shows
2322
- // them. Action handling is unified in dispatchItemAction().
2521
+ // exact list so they can never drift apart (Ada, translated from Turkish: "the
2522
+ // right-click menu and the top menu don't match"). The toolbar filters out
2523
+ // dividers/hidden; the context menu shows them. Action handling is unified in
2524
+ // dispatchItemAction().
2323
2525
  function selectionActionList(sel: FileNode[]): ContextAction[] {
2324
2526
  const any = sel.length > 0;
2325
2527
  const single = sel.length === 1;
@@ -2342,11 +2544,11 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
2342
2544
  const accessLabel = locale.value === 'en' ? 'Share / Permissions' : 'Paylaş / İzinler';
2343
2545
  return [
2344
2546
  { key: 'open', label: t('ctx.open'), icon: '↗', hidden: !single },
2345
- { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — klasörü yeni sekmede */,
2547
+ { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — open the folder in a new tab */,
2346
2548
  { key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
2347
2549
  { key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
2348
- { key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w || e2eActive.value /* wiring:e2 — convert ciphertext'e anlamsız */, disabled: !isFile },
2349
- { key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w || e2eActive.value /* wiring:e2 — paylaşım MVP'de kapalı (link ciphertext verir) */ },
2550
+ { key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w || e2eActive.value /* wiring:e2 — convert is meaningless on ciphertext */, disabled: !isFile },
2551
+ { key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w || e2eActive.value /* wiring:e2 — sharing is off in the MVP (the link would serve ciphertext) */ },
2350
2552
  { key: 'details', label: t('ctx.details'), icon: 'ℹ', hidden: !any } /* koru:k1 */,
2351
2553
  { key: 'copy-id', label: copyIdLabel, icon: '🆔', hidden: !singleHasId, disabled: !singleHasId },
2352
2554
  { divider: true, key: 'sep1', label: '', hidden: !w },
@@ -2401,7 +2603,7 @@ async function onContextAction(action: ContextAction, targets: FileNode[]) {
2401
2603
  }
2402
2604
  return;
2403
2605
  }
2404
- if (ctxMode.value === 'pane' /* ui-fix — yan panel: aynı dispatch, pane-route */) {
2606
+ if (ctxMode.value === 'pane' /* ui-fix — side pane: same dispatch, pane-routed */) {
2405
2607
  await dispatchItemAction(action.key, paneCtxTargets.value);
2406
2608
  return;
2407
2609
  }
@@ -2497,23 +2699,23 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
2497
2699
  }
2498
2700
  break;
2499
2701
  case 'cut':
2500
- /* ui-fix — pane bağlamında pano kaynağı pane'in dizini olmalı. */
2702
+ /* ui-fix — in a pane context the clipboard source must be the pane's dir. */
2501
2703
  if (ctxMode.value === 'pane') paneCut();
2502
2704
  else {
2503
2705
  clipboard.value = { mode: 'cut', items: targets, sourcePath: currentPath.value };
2504
- flashToast('Kes → Yapıştır hazır');
2706
+ flashToast(t('toast.cut_ready'));
2505
2707
  }
2506
2708
  break;
2507
2709
  case 'copy':
2508
2710
  if (ctxMode.value === 'pane') paneCopy();
2509
2711
  else {
2510
2712
  clipboard.value = { mode: 'copy', items: targets, sourcePath: currentPath.value };
2511
- flashToast('Kopyala → Yapıştır hazır');
2713
+ flashToast(t('toast.copy_ready'));
2512
2714
  }
2513
2715
  break;
2514
2716
  case 'paste':
2515
- /* ui-fix — sağ-klik menüsünden yapıştırma da aktif panele gider
2516
- * (klavye kısayolu zaten pane-route'luydu; menü değildi). */
2717
+ /* ui-fix — pasting from the right-click menu goes to the active pane too
2718
+ * (the keyboard shortcut was already pane-routed; the menu was not). */
2517
2719
  if (ctxMode.value === 'pane' || paneIsActive.value) await panePaste();
2518
2720
  else await paste();
2519
2721
  break;
@@ -2534,7 +2736,7 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
2534
2736
  case 'duplicate':
2535
2737
  if (targets[0]) await duplicate(targets[0]);
2536
2738
  break;
2537
- /* wiring:d1 — sağ-tık "Yeni sekmede aç" */
2739
+ /* wiring:d1 — right-click "Yeni sekmede aç" (open in new tab) */
2538
2740
  case 'open-tab':
2539
2741
  if (targets[0]) openNodeInTab(targets[0]);
2540
2742
  break;
@@ -2545,13 +2747,13 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
2545
2747
  function cut() {
2546
2748
  if (selection.isEmpty.value) return;
2547
2749
  clipboard.value = { mode: 'cut', items: selection.nodes.value, sourcePath: currentPath.value };
2548
- flashToast('Kesildi');
2750
+ flashToast(t('toast.cut'));
2549
2751
  }
2550
2752
 
2551
2753
  function copyToClipboard() {
2552
2754
  if (selection.isEmpty.value) return;
2553
2755
  clipboard.value = { mode: 'copy', items: selection.nodes.value, sourcePath: currentPath.value };
2554
- flashToast('Kopyalandı');
2756
+ flashToast(t('toast.copied'));
2555
2757
  }
2556
2758
 
2557
2759
  async function paste() {
@@ -2562,25 +2764,26 @@ async function paste() {
2562
2764
  const sourceDir = cb.sourcePath || '';
2563
2765
  const sameDir = cb.mode === 'cut' && sourceDir === currentPath.value;
2564
2766
  if (sameDir) {
2565
- flashToast('Aynı klasöre kesilemez');
2767
+ flashToast(t('toast.same_folder_cut'));
2566
2768
  return;
2567
2769
  }
2568
2770
 
2569
2771
  const targetWire = qualify(currentPath.value);
2570
- // Depo farkı yalnız mesajı değiştirir: kes KESTİR, kopyala KOPYALAR
2571
- // hedef başka depo olsa da. Aktarımı sunucu yapar (ops kuyruğu hem
2572
- // kaynak hem hedef depoyu taşır).
2772
+ // A different storage only changes the message: cut still CUTS
2773
+ // and copy still COPIES, even when the target lives on another storage.
2774
+ // The server does the transfer (the ops queue carries both the source
2775
+ // and the target storage).
2573
2776
  const plan = resolveTransfer(items, targetWire, cb.mode === 'cut' ? 'move' : 'copy');
2574
2777
  if (cb.mode === 'cut') {
2575
2778
  const originWire = qualify(sourceDir) || undefined;
2576
2779
  const { op } = await api.moveAsync(items, targetWire, originWire);
2577
2780
  registerMoveUndo(op.id, items, targetWire, originWire);
2578
2781
  pendingOps.register(op);
2579
- flashToast(plan.cross ? t('split.cross_move') : 'Taşıma kuyruğa alındı');
2782
+ flashToast(plan.cross ? t('split.cross_move') : t('split.move_queued'));
2580
2783
  } else {
2581
2784
  const { op } = await api.copy(items, targetWire);
2582
2785
  pendingOps.register(op);
2583
- flashToast(plan.cross ? t('split.cross_copy') : 'Kopyalama kuyruğa alındı');
2786
+ flashToast(plan.cross ? t('split.cross_copy') : t('split.copy_queued'));
2584
2787
  }
2585
2788
  clipboard.value = { mode: null, items: [], sourcePath: null };
2586
2789
  } catch (err) {
@@ -2598,8 +2801,9 @@ async function duplicate(n: FileNode) {
2598
2801
  }
2599
2802
 
2600
2803
  function downloadFile(n: FileNode) {
2601
- /* wiring:e2 — şifreli klasörde indirme: baytları çek, çöz, orijinal adla
2602
- kaydet (ham ciphertext'i kullanıcıya vermek anlamsız). */
2804
+ /* wiring:e2 — downloading inside an encrypted folder: fetch the bytes,
2805
+ decrypt them, save under the original name (handing the user raw
2806
+ ciphertext would be pointless). */
2603
2807
  if (e2eUnlocked.value) {
2604
2808
  void e2eDownload(n);
2605
2809
  return;
@@ -2615,7 +2819,7 @@ function downloadFile(n: FileNode) {
2615
2819
  // ------- Modals -------
2616
2820
 
2617
2821
  async function submitNewFolder(name: string) {
2618
- const inPane = mutationInPane.value; /* ui-fix — yan panelde yeni klasör */
2822
+ const inPane = mutationInPane.value; /* ui-fix — new folder in the side pane */
2619
2823
  try {
2620
2824
  const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
2621
2825
  await api.newFolder(dirWire, name);
@@ -2630,7 +2834,7 @@ async function submitNewFolder(name: string) {
2630
2834
  async function submitRename(name: string) {
2631
2835
  const target = renameTarget.value;
2632
2836
  if (!target) return;
2633
- const inPane = mutationInPane.value; /* ui-fix — yan panelden yeniden adlandırma */
2837
+ const inPane = mutationInPane.value; /* ui-fix — rename from the side pane */
2634
2838
  try {
2635
2839
  const dirWire = inPane ? qualify(splitPaneRef.value?.getPath() ?? '') : qualify(currentPath.value);
2636
2840
  const oldPath = target.path; // qualified
@@ -2658,7 +2862,7 @@ async function confirmDelete() {
2658
2862
  // offer Restore here rather than a delete that would just re-trash a path.
2659
2863
  if (trashMode.value) {
2660
2864
  showDelete.value = false;
2661
- flashToast('Çöpteki öğeler saklama süresi sonunda otomatik silinir. Kalıcı silme yönetici panelinden yapılır.');
2865
+ flashToast(t('toast.trash_retention'));
2662
2866
  return;
2663
2867
  }
2664
2868
  /* ui-fix — yan panelden silme: hedefler + dizin + tazeleme pane'e ait. */
@@ -2690,7 +2894,7 @@ async function confirmDelete() {
2690
2894
  opUndo.set(op.id, { message: t('toast.trashed'), fn: restoreUndo });
2691
2895
  }
2692
2896
  pendingOps.register(op);
2693
- flashToast('Silme kuyruğa alındı');
2897
+ flashToast(t('toast.delete_queued'));
2694
2898
  } else {
2695
2899
  await api.deleteItems(dirWire, items);
2696
2900
  if (inPane) await splitPaneRef.value?.reload();
@@ -2767,9 +2971,9 @@ function onFilePicked(ev: Event) {
2767
2971
 
2768
2972
  async function uploadFiles(list: File[]) {
2769
2973
  if (list.length === 0) return;
2770
- /* wiring:e2 — şifreli klasörde upload şeffaf şifrelenir. Kilitliyken
2771
- yükleme yok (düz metin sızdırma kapısı olurdu); 200MB üstü MVP
2772
- tek-shot sınırına takılır ve uyarıyla atlanır. */
2974
+ /* wiring:e2 — uploads into an encrypted folder are encrypted transparently.
2975
+ No upload while locked (that would be a plaintext-leak door); anything
2976
+ over 200MB hits the MVP single-shot limit and is skipped with a warning. */
2773
2977
  if (e2eLocked.value) {
2774
2978
  flashToast(t('e2e.upload.locked'));
2775
2979
  return;
@@ -2940,9 +3144,10 @@ function isExternalFileDrag(ev: DragEvent): boolean {
2940
3144
  const dt = ev.dataTransfer;
2941
3145
  if (!dt) return false;
2942
3146
  if (dt.types && dt.types.includes(FE_DND_MIME)) return false;
2943
- /* wiring:f1 — kendi başlattığımız işletim sistemi sürüklemesi 'Files' taşır
2944
- ama YÜKLEME değildir: aynı baytları sunucudan indirip geri yüklemek
2945
- (taşıma yerine kopya, üstelik iki kat trafik) olurdu. */
3147
+ /* wiring:f1 — an OS drag we started ourselves also carries 'Files', but it
3148
+ is NOT an upload: it would mean downloading the same bytes from the
3149
+ server and posting them straight back (a copy instead of a move, at
3150
+ twice the traffic). */
2946
3151
  if (activeNativeDrag()) return false;
2947
3152
  // Some browsers expose `items` early in the drag, others only on
2948
3153
  // drop. When `items` is available we use it as the authoritative
@@ -2973,9 +3178,10 @@ function onDragLeave() {
2973
3178
  if (dragCounter.value === 0) dragOver.value = false;
2974
3179
  }
2975
3180
  function onDragOver(ev: DragEvent) {
2976
- /* wiring:d1 — sürüklemeler kök gövdeye de bırakılabilir olmalı ki split
2977
- panelinden ana panelin BOŞLUĞUNA bırakmak çalışsın (origin dragover'da
2978
- okunamazkarar drop anında verilir; aynı-klasör drop'u no-op kalır). */
3181
+ /* wiring:d1 — internal drags must be droppable on the root body too, so that
3182
+ dropping from the split pane onto the main pane's EMPTY SPACE works (the
3183
+ origin can't be read during dragover the decision is made at drop time;
3184
+ a same-folder drop stays a no-op). */
2979
3185
  if (hasInternalDrag(ev)) {
2980
3186
  ev.preventDefault();
2981
3187
  return;
@@ -2986,8 +3192,9 @@ function onDragOver(ev: DragEvent) {
2986
3192
  }
2987
3193
  }
2988
3194
  function onDropUpload(ev: DragEvent) {
2989
- /* wiring:d1 — split panelinden ana panelin boşluğuna bırakma = geçerli
2990
- klasöre aktar (aynı klasörden gelenler no-op, eski davranış korunur). */
3195
+ /* wiring:d1 — dropping from the split pane onto the main pane's empty space
3196
+ = transfer into the current folder (items from the same folder stay a
3197
+ no-op, so the old behaviour is preserved). */
2991
3198
  if (hasInternalDrag(ev)) {
2992
3199
  const d1Origin = internalDragOrigin(ev) || '';
2993
3200
  const d1Here = qualify(currentPath.value);
@@ -3048,13 +3255,14 @@ onMounted(() => {
3048
3255
  window.addEventListener('drop', onWindowDrop);
3049
3256
  window.addEventListener('pointerup', onGlobalPointerUp);
3050
3257
  window.addEventListener('blur', onGlobalPointerUp);
3051
- /* wiring:f1 — kabuk hazırlarken tek bir "hazırlanıyor" der; her dosyada
3052
- toast atmak ilerlemeyi değil gürültüyü gösterirdi. Bitişi 'hazır'
3053
- toast'ı (prepareDragOut) duyurur. */
3258
+ /* wiring:f1 — while the shell prepares, say "preparing" exactly once; a
3259
+ toast per file would show noise rather than progress. The finish is
3260
+ announced by the 'ready' toast (prepareDragOut). */
3054
3261
  dragOut.value?.onProgress?.((p) => {
3055
- // Bırakma SONRASI (yer tutucu yolu) her zaman duyurulur kullanıcı
3056
- // dosyayı bir klasöre bıraktı, orada ne olduğunu bilmeye hakkı var.
3057
- // Sessizlik yalnız kimsenin istemediği ön-hazırlık için geçerli.
3262
+ // Work that happens AFTER the drop (the placeholder path) is always
3263
+ // announced the user dropped a file into a folder and has a right to
3264
+ // know what happened there. Silence only applies to the pre-preparation
3265
+ // nobody asked for.
3058
3266
  const afterDrop = !!p?.dropped;
3059
3267
  if (p?.error === 'drop_not_found') {
3060
3268
  flashToast(t('dragout.not_found'));
@@ -3072,18 +3280,18 @@ onMounted(() => {
3072
3280
  if (!p?.finished && p?.done === 0) flashToast(t('dragout.preparing'));
3073
3281
  });
3074
3282
  });
3075
- /* wiring:f1 — işletim sistemi sürüklemesi bizde 'dragend' üretmez (HTML5
3076
- sürüklemesi hiç başlamadı). Fare bırakıldığında kaydı düşürüyoruz; aksi
3077
- halde sonraki normal sürükleme bir önceki seçimi taşıdığını sanırdı. */
3283
+ /* wiring:f1 — an OS drag never fires 'dragend' for us (the HTML5 drag never
3284
+ started). We drop the record when the mouse is released; otherwise the next
3285
+ ordinary drag would think it was carrying the previous selection. */
3078
3286
  function onGlobalPointerUp() {
3079
3287
  if (activeNativeDrag()) {
3080
3288
  endNativeDrag();
3081
- // Bırakma bizim penceremizde OLMAYABİLİR de; kabuğun izlemesini yalnız
3082
- // kendi bırakma yollarımız iptal eder (onDropUpload / onItemDropInto).
3289
+ // The drop MAY NOT have landed in our own window; only our own drop paths
3290
+ // cancel the shell's watch (onDropUpload / onItemDropInto).
3083
3291
  }
3084
3292
  }
3085
3293
 
3086
- /** Sürükleme uygulama içinde bitti: kabuk artık bir bırakma beklemesin. */
3294
+ /** The drag ended inside the app: the shell should stop waiting for a drop. */
3087
3295
  function cancelShellDrag() {
3088
3296
  if (dragOut.value?.cancel) void Promise.resolve(dragOut.value.cancel()).catch(() => undefined);
3089
3297
  }
@@ -3121,14 +3329,15 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
3121
3329
  .filter((n) => n.basename !== '.trash')
3122
3330
  .map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
3123
3331
 
3124
- /* wiring:f1 — dışarı sürükleme (masaüstü / başka uygulama).
3125
- Kabuk varsa sürükleme HER ZAMAN işletim sistemi sürüklemesidir: klasörler
3126
- ve çoklu seçim ayrı ayrı GERÇEK dosya olarak düşer, BOYUT SINIRI YOK.
3127
- Baytlar hazırsa gerçek dosyalar verilir; değilse kabuk boş "yer tutucu"
3128
- verir, nereye bırakıldığını bulur ve indirmeyi ORAYA yapar (bkz.
3129
- desktop/src/dropwatch.ts). Uygulama İÇİNDE bırakılırsa sürükleme yine
3130
- sunucu tarafı taşımadır payload bizde dururve kabuğa "vazgeç" denir
3131
- ki sürücüleri boşuna dinlemesin. */
3332
+ /* wiring:f1 — drag-out (to the desktop / another application).
3333
+ When a shell is present the drag is ALWAYS an OS drag: folders and
3334
+ multi-selections land as REAL files, one by one, with NO SIZE LIMIT.
3335
+ If the bytes are ready, real files are handed over; if not, the shell
3336
+ hands over an empty "placeholder", finds out where it was dropped and
3337
+ downloads THERE (see desktop/src/dropwatch.ts). If the drop lands INSIDE
3338
+ the app the drag is still a server-side move the payload stays with us —
3339
+ and the shell is told to "give up" so it doesn't watch the drives for
3340
+ nothing. */
3132
3341
  if (dragOut.value && items.length > 0) {
3133
3342
  ev.preventDefault();
3134
3343
  beginNativeDrag(items, qualify(currentPath.value));
@@ -3141,14 +3350,15 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
3141
3350
  }
3142
3351
 
3143
3352
  ev.dataTransfer.setData(FE_DND_MIME, JSON.stringify(items));
3144
- ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — paneller arası origin damgası */
3353
+ ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — cross-pane origin stamp */
3145
3354
  ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
3146
3355
  ev.dataTransfer.effectAllowed = 'move';
3147
3356
 
3148
- /* Tek dosya + çerezli oturum: tarayıcının kendi indirme yolu (DownloadURL)
3149
- bırakma anında dosyayı masaüstüne indirir; hiçbir hazırlık gerekmez.
3150
- Bearer token'lı kurulumda (masaüstü uygulaması) bu yol kimliksiz gider,
3151
- o yüzden orada üstteki yerel yol devrededir bkz. lib/dragOut.ts. */
3357
+ /* Single file + a cookie session: the browser's own download path
3358
+ (DownloadURL) fetches the file onto the desktop at drop time; no
3359
+ preparation is needed at all. In a bearer-token setup (the desktop app)
3360
+ that path goes out unauthenticated, which is why the local path above
3361
+ takes over there — see lib/dragOut.ts. */
3152
3362
  if (items.length === 1 && items[0] && canDownloadUrlDrag(props.config.auth)) {
3153
3363
  const payload = downloadUrlPayload(items[0], api.downloadUrl(items[0].path), node.mime_type);
3154
3364
  if (payload) ev.dataTransfer.setData('DownloadURL', payload);
@@ -3157,13 +3367,13 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
3157
3367
  if (dragOut.value && items.length > 0) void prepareDragOut(items);
3158
3368
  }
3159
3369
 
3160
- /* === wiring:f1 — dışarı sürükleme hazırlığı ===
3370
+ /* === wiring:f1 — drag-out preparation ===
3161
3371
  *
3162
- * Baytlar sürükleme BAŞLAMADAN diskte olmak zorunda (işletim sistemi bırakma
3163
- * anında yoldan kopyalar), o yüzden hazırlık ayrı bir adımdır. Bitince
3164
- * kullanıcıya "hazır" denir; ikinci sürükleme artık işletim sistemi
3165
- * sürüklemesidir ve anında başlar. Bu bilgisayarda tutulan (senkron) dosyalar
3166
- * için hazırlık ilk seferde de anında biter kopya zaten yerelde.
3372
+ * The bytes have to be on disk BEFORE the drag starts (the OS copies from the
3373
+ * path at drop time), so preparation is a separate step. When it finishes the
3374
+ * user is told "ready"; the second drag is now an OS drag and starts
3375
+ * instantly. For files kept on this computer (synced) preparation finishes
3376
+ * instantly on the first go toothe copy is already local.
3167
3377
  */
3168
3378
  const dragOut = computed(() => props.config.dragOut ?? null);
3169
3379
  const dragOutReadyKey = ref('');
@@ -3171,11 +3381,12 @@ const dragOutBusy = ref(false);
3171
3381
  /** True while a preparation nobody asked for is running. */
3172
3382
  let dragOutQuiet = false;
3173
3383
 
3174
- /* Küçük seçimler SEÇİLDİĞİ anda hazırlanır, çünkü hazırlık bittikten sonraki
3175
- sürükleme işletim sistemi sürüklemesidir: bir belgeyi seçip masaüstüne
3176
- sürüklemek böylece İLK denemede çalışır. Tavan bilerek düşük tıklayarak
3177
- gezinen biri her satırda film indirmemeli; sınırın üstündekiler ilk
3178
- sürüklemede hazırlanır, ikinci sürükleme anında başlar. */
3384
+ /* Small selections are prepared the moment they are SELECTED, because once
3385
+ preparation is done the next drag is an OS drag: picking a document and
3386
+ dragging it to the desktop therefore works on the FIRST try. The ceiling is
3387
+ deliberately low someone click-browsing shouldn't download a movie on
3388
+ every row; anything above the limit is prepared on the first drag and the
3389
+ second drag starts instantly. */
3179
3390
  const DRAGOUT_PREFETCH_MAX_BYTES = 8 * 1024 * 1024;
3180
3391
  const DRAGOUT_PREFETCH_MAX_ITEMS = 10;
3181
3392
  let dragOutPrefetchTimer: ReturnType<typeof setTimeout> | undefined;
@@ -3187,8 +3398,8 @@ watch(
3187
3398
  clearTimeout(dragOutPrefetchTimer);
3188
3399
  const nodes = selection.nodes.value;
3189
3400
  if (nodes.length === 0 || nodes.length > DRAGOUT_PREFETCH_MAX_ITEMS) return;
3190
- // Klasörün boyutu listelemede bilinmez; onu tahmin etmek yerine ilk
3191
- // sürüklemeye bırakıyoruz.
3401
+ // A folder's size isn't known from the listing; rather than guess it, we
3402
+ // leave it to the first drag.
3192
3403
  if (nodes.some((n) => n.type !== 'file')) return;
3193
3404
  const total = nodes.reduce((sum, n) => sum + (n.size ?? 0), 0);
3194
3405
  if (total > DRAGOUT_PREFETCH_MAX_BYTES) return;
@@ -3209,8 +3420,9 @@ async function prepareDragOut(items: DragItem[], quiet = false): Promise<void> {
3209
3420
  const res = await hook.prepare(items);
3210
3421
  if (res?.ready) {
3211
3422
  dragOutReadyKey.value = key;
3212
- // Sessiz tur seçimle tetiklenir; kullanıcı bir şey İSTEMEDİ, o yüzden
3213
- // ona bir şey söylemek de gerekmez. Sürüklemeyle başlayan tur söyler.
3423
+ // The quiet round is triggered by a selection; the user ASKED for
3424
+ // nothing, so there's nothing to tell them either. A round that starts
3425
+ // from an actual drag does speak up.
3214
3426
  if (!quiet) flashToast(t('dragout.ready'));
3215
3427
  } else if (res?.error && !quiet) {
3216
3428
  flashToast(res.error);
@@ -3225,12 +3437,12 @@ async function prepareDragOut(items: DragItem[], quiet = false): Promise<void> {
3225
3437
 
3226
3438
  async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string, originOverride?: string): Promise<void> {
3227
3439
  try {
3228
- const originWire = originOverride ?? qualify(currentPath.value); /* wiring:d1 — split panelinden gelen sürüklemede gerçek kaynak klasör */
3440
+ const originWire = originOverride ?? qualify(currentPath.value); /* wiring:d1 — the real source folder for a drag coming from the split pane */
3229
3441
  if (api.endpoints.moveAsync) {
3230
3442
  const { op } = await api.moveAsync(sources, targetDir, originWire);
3231
3443
  registerMoveUndo(op.id, sources, targetDir, originWire);
3232
3444
  pendingOps.register(op);
3233
- flashToast('Taşıma kuyruğa alındı');
3445
+ flashToast(t('split.move_queued'));
3234
3446
  } else {
3235
3447
  await api.move(originWire, sources, targetDir);
3236
3448
  await load();
@@ -3256,11 +3468,12 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
3256
3468
  const targetDir = target.path; // qualified
3257
3469
  const sources = items
3258
3470
  .map((i) => i.path)
3259
- // ui-fix — öğe zaten targetDir'in içindeyse (parent===target) atla: yerinde
3260
- // bırakma no-op, backend "kendine kopyala" 400'ü tetiklemez.
3471
+ // ui-fix — skip items already inside targetDir (parent===target): an
3472
+ // in-place drop is a no-op and must not trigger the backend's "copy onto
3473
+ // itself" 400.
3261
3474
  .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/') && !sameDir(wireParent(p), targetDir));
3262
- if (sources.length === 0) return; // sessiz no-op (yerinde bırakma)
3263
- await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
3475
+ if (sources.length === 0) return; // silent no-op (in-place drop)
3476
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — storage-aware transfer */
3264
3477
  }
3265
3478
 
3266
3479
  async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
@@ -3272,10 +3485,10 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
3272
3485
  const targetDir = adapterPath; // already qualified by breadcrumb
3273
3486
  const sources = items
3274
3487
  .map((i) => i.path)
3275
- // ui-fix — kırıntıya (aynı klasöre) yerinde bırakma no-op.
3488
+ // ui-fix — an in-place drop onto the breadcrumb (the same folder) is a no-op.
3276
3489
  .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/') && !sameDir(wireParent(p), targetDir));
3277
3490
  if (sources.length === 0) return;
3278
- await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
3491
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — storage-aware transfer */
3279
3492
  }
3280
3493
 
3281
3494
  function onCancelUpload(job: UploadJob) {
@@ -3418,7 +3631,7 @@ function quickLookToggle() {
3418
3631
  const sel = selection.nodes.value;
3419
3632
  const n = sel.length === 1 ? sel[0] : null;
3420
3633
  if (!n || n.type !== 'file' || n.basename === '.trash') return;
3421
- /* wiring:e2 — Space peek şifreli klasörde önce çözer, sonra açar */
3634
+ /* wiring:e2 — in an encrypted folder the Space peek decrypts first, then opens */
3422
3635
  if (e2eActive.value) {
3423
3636
  if (!e2eUnlocked.value) return;
3424
3637
  void (async () => {
@@ -3462,14 +3675,14 @@ watch(
3462
3675
  (nodes) => {
3463
3676
  if (!quickLookOpen.value) return;
3464
3677
  const n = nodes.length === 1 && nodes[0].type === 'file' ? nodes[0] : null;
3465
- /* wiring:e2 — ok tuşlarıyla gezerken de hedef atanmadan ÖNCE çöz,
3466
- yoksa viewer bir anlığına ham ciphertext URL'i alır. */
3678
+ /* wiring:e2 — when arrowing through files, decrypt BEFORE assigning the
3679
+ target as well, otherwise the viewer briefly gets the raw ciphertext URL. */
3467
3680
  if (n && n.path !== quickLookTarget.value?.path && e2eUnlocked.value) {
3468
3681
  void (async () => {
3469
3682
  try {
3470
3683
  await e2eFetchDecrypted(n);
3471
3684
  } catch {
3472
- /* çözülemedihedefi yine de değiştir, viewer hata gösterir */
3685
+ /* decryption failed switch the target anyway, the viewer shows the error */
3473
3686
  }
3474
3687
  quickLookTarget.value = n;
3475
3688
  void markRecent(n);
@@ -3564,16 +3777,17 @@ onBeforeUnmount(() => {
3564
3777
  });
3565
3778
  /* === /wiring:c4 === */
3566
3779
 
3567
- /* === wiring:d1 — sekmeler (tab şeridi) + tab başına split ===
3780
+ /* === wiring:d1 — tabs (tab strip) + per-tab split ===
3568
3781
  *
3569
- * useTabs, mevcut konum state'inin (currentPath/viewMode) ÜSTÜNDE bir
3570
- * katmandır: aktif tab gezinmeleri watch ile dinleyip snapshot'ını
3571
- * günceller; tab geçişi mevcut load(path) yolunu çağırır yeni fetch
3572
- * mantığı yok. Şerit tek sekmede hiç render edilmez (embed pixel-aynı).
3782
+ * useTabs is a layer ON TOP of the existing location state
3783
+ * (currentPath/viewMode): the active tab watches navigations and updates its
3784
+ * snapshot; switching tabs calls the existing load(path) pathno new fetch
3785
+ * logic. The strip is not rendered at all on a single tab (embeds stay
3786
+ * pixel-identical).
3573
3787
  *
3574
- * Persist: `filex.tabs` — pathPersist scope mantığı izlenir: mode 'none'
3575
- * ise persist kapalı; rootPath confine anahtara eklenir ki farklı
3576
- * confine'lı embed'ler birbirinin sekmelerini ezmesin.
3788
+ * Persist: `filex.tabs` — it follows the pathPersist scope logic: with mode
3789
+ * 'none' persistence is off; the rootPath confine is added to the key so that
3790
+ * embeds with different confines don't overwrite each other's tabs.
3577
3791
  */
3578
3792
  const FE_DND_SRC_MIME = 'application/x-brf-files-src';
3579
3793
 
@@ -3588,7 +3802,7 @@ const tabsActiveId = tabsApi.activeId;
3588
3802
 
3589
3803
  const activeSplit = computed(() => tabsApi.activeTab.value?.split ?? null);
3590
3804
 
3591
- // Sekme adı OTOMATİK = güncel klasör adı (kök = depo adı / kök etiketi).
3805
+ // Tab name is AUTOMATIC = the current folder name (root = storage name / root label).
3592
3806
  function tabLabel(path: string): string {
3593
3807
  const p = (path || '').replace(/^\/+|\/+$/g, '');
3594
3808
  // gezinti:g1 — the virtual views park a sentinel in the path. Translate via
@@ -3620,12 +3834,13 @@ const tabsVisible = computed(
3620
3834
  (props.config.tabStrip !== 'auto' && tabItems.value.length > 0)),
3621
3835
  );
3622
3836
 
3623
- // Aktif tab kullanıcıyı izler: gezinme + görünüm değişimi snapshot'a yazılır.
3837
+ // The active tab follows the user: navigation + view changes go into the snapshot.
3624
3838
  watch(currentPath, (p) => tabsApi.syncActive({ path: p }));
3625
3839
  watch(viewMode, (v) => tabsApi.syncActive({ viewMode: v }));
3626
3840
 
3627
- // İlk sekme, ilk konum belli olur olmaz tohumlanır (restore varsa dokunma
3628
- // aktif snapshot ilk load sonrası currentPath watcher'ıyla zaten senkronlanır).
3841
+ // The first tab is seeded as soon as the first location is known (don't touch
3842
+ // it if a restore happened the active snapshot is already synced by the
3843
+ // currentPath watcher after the first load).
3629
3844
  onMounted(() => {
3630
3845
  if (!tabsRestored) tabsApi.seed(currentPath.value ?? '', viewMode.value);
3631
3846
  });
@@ -3643,7 +3858,7 @@ function activateTab(id: string) {
3643
3858
  if (tb) applyTabLocation(tb);
3644
3859
  }
3645
3860
  function newTabHere() {
3646
- // Mevcut konumu klonlar; görünüm zaten oradadır, load gerekmez.
3861
+ // Clones the current location; the view is already there, so no load needed.
3647
3862
  tabsApi.openTab(currentPath.value ?? '', { viewMode: viewMode.value, background: false });
3648
3863
  }
3649
3864
  function closeTabById(id: string) {
@@ -3659,16 +3874,17 @@ function prevTab() {
3659
3874
  if (tb) applyTabLocation(tb);
3660
3875
  }
3661
3876
 
3662
- /** Bir klasörü ARKA PLANDA yeni sekmede (orta-tık / sağ-tık / palet). */
3877
+ /** Open a folder in a new tab IN THE BACKGROUND (middle-click / right-click / palette). */
3663
3878
  function openNodeInTab(n: FileNode) {
3664
3879
  if (n.type !== 'dir' || n.basename === '.trash') return;
3665
3880
  const target = multiStorageRoot.value ? wireToVirtual(n.path) : stripAdapter(n.path);
3666
3881
  tabsApi.openTab(target, { viewMode: viewMode.value, background: true });
3667
3882
  }
3668
3883
 
3669
- // Orta-tık delegasyonu: ListView/GridView satırları data-fe-path taşır; kendi
3670
- // keydown/emit zinciri eklemek yerine kökte tek auxclick dinleyicisi yeter.
3671
- // (SecondaryPane kendi satırlarında stopPropagation ile kendisi halleder.)
3884
+ // Middle-click delegation: ListView/GridView rows carry data-fe-path, so a
3885
+ // single auxclick listener at the root is enough instead of adding a
3886
+ // keydown/emit chain of our own. (SecondaryPane handles its own rows via
3887
+ // stopPropagation.)
3672
3888
  function onListAuxClick(ev: MouseEvent) {
3673
3889
  if (ev.button !== 1) return;
3674
3890
  const host = ev.target as HTMLElement | null;
@@ -3680,9 +3896,9 @@ function onListAuxClick(ev: MouseEvent) {
3680
3896
  ev.preventDefault();
3681
3897
  openNodeInTab(node);
3682
3898
  }
3683
- // Orta-tuş mousedown satırlar üzerinde iptal: scroll'lu gövdede Chromium'un
3684
- // autoscroll'u devreye girer ve auxclick HİÇ üretilmez (canlı teşhis) —
3685
- // preventDefault autoscroll'u bastırır, auxclick yeniden akar.
3899
+ // Cancel the middle-button mousedown over rows: in a scrollable body Chromium's
3900
+ // autoscroll kicks in and auxclick is NEVER produced (diagnosed live) —
3901
+ // preventDefault suppresses autoscroll and auxclick flows again.
3686
3902
  function onListMiddleDown(ev: MouseEvent) {
3687
3903
  if (ev.button !== 1) return;
3688
3904
  const host = ev.target as HTMLElement | null;
@@ -3699,10 +3915,10 @@ onBeforeUnmount(() => {
3699
3915
  rootEl.value?.removeEventListener('mousedown', onListMiddleDown);
3700
3916
  });
3701
3917
 
3702
- // ---- split (tab başına ikincil panel) ------------------------------
3918
+ // ---- split (per-tab secondary pane) --------------------------------
3703
3919
 
3704
3920
  const splitPaneRef = ref<InstanceType<typeof SecondaryPane> | null>(null);
3705
- // Dar modda split devre dışı (state korunur, genişleyince geri gelir).
3921
+ // Split is disabled in narrow mode (the state is kept and comes back on widen).
3706
3922
  const splitVisible = computed(
3707
3923
  () => !!activeSplit.value && !isNarrow.value && !simpleUi.value /* gezinti:g1 */,
3708
3924
  );
@@ -3723,16 +3939,18 @@ function closeSplit() {
3723
3939
  function onPaneNavigate(p: string) {
3724
3940
  tabsApi.setSplit({ ...(activeSplit.value ?? {}), path: p });
3725
3941
  }
3726
- /* ui-fix — çöp kutusu satırı yan panelde açılınca: trash görünümü (geri
3727
- * yükleme aksiyonlarıyla) ana panele aittir ana paneli aktif edip aç. */
3942
+ /* ui-fix — when the trash row is opened from the side pane: the trash view
3943
+ * (with its restore actions) belongs to the main pane activate the main
3944
+ * pane and open it there. */
3728
3945
  function onPaneOpenTrash() {
3729
3946
  activePane.value = 'main';
3730
3947
  void loadTrash();
3731
3948
  }
3732
- /* ui-fix — pane'in KENDİ görünüm modu: split açılırken ana panelinkini
3733
- * devralır, sonrasında bağımsız. Toolbar'ın görünüm değiştiricisi ve palet
3734
- * toggle AKTİF panele yazar (Ada: "B tıklıyken ikon değiştir dersem
3735
- * B'nin değişmesi lazım"). */
3949
+ /* ui-fix — the pane's OWN view mode: it inherits the main pane's when the
3950
+ * split opens, and is independent afterwards. The toolbar's view switcher and
3951
+ * the palette toggle write to the ACTIVE pane (Ada, translated from Turkish:
3952
+ * "if I say change the icon while B is focused, B is the one that has to
3953
+ * change"). */
3736
3954
  const paneViewMode = computed<ViewMode>(() => activeSplit.value?.viewMode ?? viewMode.value);
3737
3955
  function setPaneViewMode(v: ViewMode) {
3738
3956
  if (!activeSplit.value) return;
@@ -3746,7 +3964,7 @@ function setDisplayedViewMode(v: ViewMode) {
3746
3964
  else viewMode.value = v;
3747
3965
  }
3748
3966
 
3749
- // Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
3967
+ // Active pane: shortcuts go to the active pane; a pane is activated by clicking it.
3750
3968
  const activePane = ref<'main' | 'split'>('main');
3751
3969
  function setPaneMain() {
3752
3970
  activePane.value = 'main';
@@ -3757,7 +3975,7 @@ watch(splitVisible, (v) => {
3757
3975
  const paneIsActive = computed(() => activePane.value === 'split' && splitVisible.value);
3758
3976
  const mainPaneFocus = computed(() => splitVisible.value && activePane.value === 'main');
3759
3977
 
3760
- // Pane yardımcılarıhep ana panelin mevcut dönüştürücülerini sarar.
3978
+ // Pane helpersalways wrap the main pane's existing converters.
3761
3979
  function paneToUser(wire: string): string {
3762
3980
  return multiStorageRoot.value ? wireToVirtual(wire) : stripAdapter(wire);
3763
3981
  }
@@ -3768,20 +3986,20 @@ function paneClamp(p: string): string {
3768
3986
  return clean;
3769
3987
  }
3770
3988
 
3771
- // Pano (clipboard) aktif panele göre: kes/kopyala pane seçiminden beslenir,
3772
- // yapıştır pane klasörüne iner. State ana panelinkiyle ORTAK panolar arası
3773
- // kes-yapıştır bedavaya çalışır.
3989
+ // The clipboard follows the active pane: cut/copy is fed from the pane
3990
+ // selection and paste lands in the pane's folder. The state is SHARED with the
3991
+ // main pane's — so cut-and-paste between panes works for free.
3774
3992
  function paneCut() {
3775
3993
  const nodes = splitPaneRef.value?.selectedNodes() ?? [];
3776
3994
  if (nodes.length === 0) return;
3777
3995
  clipboard.value = { mode: 'cut', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
3778
- flashToast('Kesildi');
3996
+ flashToast(t('toast.cut'));
3779
3997
  }
3780
3998
  function paneCopy() {
3781
3999
  const nodes = splitPaneRef.value?.selectedNodes() ?? [];
3782
4000
  if (nodes.length === 0) return;
3783
4001
  clipboard.value = { mode: 'copy', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
3784
- flashToast('Kopyalandı');
4002
+ flashToast(t('toast.copied'));
3785
4003
  }
3786
4004
  async function panePaste() {
3787
4005
  const cb = clipboard.value;
@@ -3790,28 +4008,29 @@ async function panePaste() {
3790
4008
  const targetWire = qualify(pane.getPath() ?? '');
3791
4009
  const originWire = qualify(cb.sourcePath || '') || undefined;
3792
4010
  if (cb.mode === 'cut' && originWire === targetWire) {
3793
- flashToast('Aynı klasöre kesilemez');
4011
+ flashToast(t('toast.same_folder_cut'));
3794
4012
  return;
3795
4013
  }
3796
4014
  await transferItems(cb.items.map((n) => n.path), targetWire, originWire, cb.mode === 'copy' ? 'copy' : 'move');
3797
4015
  clipboard.value = { mode: null, items: [], sourcePath: null };
3798
4016
  }
3799
4017
 
3800
- // ---- paneller arası aktarım ----------------------------------------
4018
+ // ---- cross-pane transfer -------------------------------------------
3801
4019
  function dndOrigin(ev: DragEvent): string | undefined {
3802
4020
  return internalDragOrigin(ev);
3803
4021
  }
3804
4022
 
3805
4023
  /**
3806
- * transferItems — panel-arası / pano aktarımının tek kapısı.
4024
+ * transferItems — the single gate for cross-pane / clipboard transfers.
3807
4025
  *
3808
- * Ne yapılacağını `resolveTransfer` söyler (lib/transfer.ts): sürükleme aynı
3809
- * depoda TAŞI, depolar arasında KOPYALA; panodan gelen kes/kopyala ise ne
3810
- * dendiyse odur kes, hedef başka depo olsa da TAŞIR (sunucu baytları
3811
- * aktarıp kaynağı siler). Eskiden depolar arası her aktarım sessizce
3812
- * kopyaya düşüyordu: kullanıcı "kes" deyip dosyayı iki yerde buluyordu.
3813
- * Bitince ikincil panel de tazelenir (ana panel moveSourcesAsync /
3814
- * pendingOps onSettled üzerinden zaten tazelenir).
4026
+ * `resolveTransfer` decides what to do (lib/transfer.ts): a drag MOVES within
4027
+ * the same storage and COPIES across storages; a cut/copy coming from
4028
+ * the clipboard does exactly what it says cut MOVES even when the target is
4029
+ * another storage (the server transfers the bytes and deletes the source).
4030
+ * Cross-storage transfers used to silently fall back to a copy: the user
4031
+ * said "cut" and found the file in two places. When it's done the secondary
4032
+ * pane is refreshed too (the main pane is already refreshed via
4033
+ * moveSourcesAsync / pendingOps onSettled).
3815
4034
  */
3816
4035
  async function transferItems(
3817
4036
  sources: string[],
@@ -3819,8 +4038,8 @@ async function transferItems(
3819
4038
  originWire?: string,
3820
4039
  intent: TransferIntent = 'auto',
3821
4040
  ): Promise<void> {
3822
- // ui-fix — yerinde bırakma (source parent === target) no-op: backend
3823
- // "kendine kopyala" 400 engellenir (paneller arası + pano yolu).
4041
+ // ui-fix — an in-place drop (source parent === target) is a no-op: this
4042
+ // avoids the backend's "copy onto itself" 400 (cross-pane + clipboard paths).
3824
4043
  const list = sources.filter(
3825
4044
  (p) => p && p !== targetWire && !targetWire.startsWith(p + '/') && !sameDir(wireParent(p), targetWire),
3826
4045
  );
@@ -3832,9 +4051,10 @@ async function transferItems(
3832
4051
  pendingOps.register(op);
3833
4052
  flashToast(plan.cross ? t('split.cross_copy') : t('split.copy_queued'));
3834
4053
  } catch (err) {
3835
- // ⚠ Sunucunun kendi mesajını göster. Burada sabit bir "depolar arası
3836
- // desteklenmiyor" metni vardı; artık DESTEKLENİYOR, yani o metin
3837
- // gerçek sebebi (izin, salt-okunur depo, dolu kota) örterdi.
4054
+ // ⚠ Show the server's own message. There used to be a hard-coded
4055
+ // "cross-storage is not supported" text here; it IS supported now, so
4056
+ // that text would mask the real cause (permissions, a read-only storage,
4057
+ // a full quota).
3838
4058
  emit('error', { message: (err as Error).message, context: { op: 'transfer', targetWire } });
3839
4059
  flashToast((err as Error).message);
3840
4060
  return;
@@ -3851,21 +4071,23 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
3851
4071
  }
3852
4072
  /* === /wiring:d1 === */
3853
4073
 
3854
- /* === wiring:e2 — uçtan uca şifreli klasörler ===
4074
+ /* === wiring:e2 — end-to-end encrypted folders ===
3855
4075
  *
3856
- * Kripto şeması + tehdit modeli: docs/E2E-ENCRYPTION.md ve lib/e2ecrypto.ts.
3857
- * Burada yalnız orkestrasyon var: backend listing yanıtındaki `e2e_root`
3858
- * kilit ekranını sürer; parola marker'a karşı TARAYICIDA doğrulanır (sunucuya
3859
- * hiçbir şey gitmez); türetilen klasör anahtarı (KEK) YALNIZ bellekte yaşar
3860
- * (`e2eRing`) — localStorage/sessionStorage'a asla yazılmaz. Upload şeffaf
3861
- * şifrelenir, önizleme/indirme şeffaf çözülür (blob URL mevcut viewer'lara).
4076
+ * Crypto scheme + threat model: docs/E2E-ENCRYPTION.md and lib/e2ecrypto.ts.
4077
+ * Only the orchestration lives here: `e2e_root` in the backend listing
4078
+ * response drives the lock screen; the password is verified against the
4079
+ * marker IN THE BROWSER (nothing goes to the server); the derived folder key
4080
+ * (KEK) lives ONLY in memory (`e2eRing`) — it is never written to
4081
+ * localStorage/sessionStorage. Uploads are encrypted transparently and
4082
+ * previews/downloads decrypted transparently (a blob URL to the existing
4083
+ * viewers).
3862
4084
  */
3863
4085
  const e2eRing = createKeyRing();
3864
- // Map'ler reaktif değil sürüm sayacı computed'ları tetikler.
4086
+ // Maps aren't reactivea version counter drives the computeds.
3865
4087
  const e2eRingVer = ref(0);
3866
- // İçinde bulunulan şifreli kökün wire yolu ('' = şifreli bağlam yok).
4088
+ // Wire path of the encrypted root we are inside ('' = no encrypted context).
3867
4089
  const e2eRoot = ref('');
3868
- // path → çözülmüş blob objectURL (önizleme). Kilitleme/unmount'ta revoke.
4090
+ // path → decrypted blob objectURL (preview). Revoked on lock/unmount.
3869
4091
  const e2eUrls = new Map<string, string>();
3870
4092
 
3871
4093
  const e2eActive = computed(() => !!e2eRoot.value && !trashMode.value);
@@ -3878,15 +4100,15 @@ const e2eLocked = computed(() => {
3878
4100
  return e2eActive.value && !e2eRing.has(e2eRoot.value);
3879
4101
  });
3880
4102
 
3881
- // Kilit ekranı formu.
4103
+ // Lock screen form.
3882
4104
  const e2ePw = ref('');
3883
4105
  const e2eUnlockBusy = ref(false);
3884
4106
  const e2eUnlockErr = ref('');
3885
- // Şifreli klasör oluşturma modalı.
4107
+ // Encrypted-folder creation modal.
3886
4108
  const showEncFolder = ref(false);
3887
4109
  const e2eCreateBusy = ref(false);
3888
4110
 
3889
- /* wiring:e2 recovery — kurtarma anahtarı + escrow.
4111
+ /* wiring:e2 recovery — recovery key + escrow.
3890
4112
  *
3891
4113
  * The marker of the folder we are looking at is cached here while the lock
3892
4114
  * screen is up: the recovery dialog needs to know which doors this folder
@@ -3906,11 +4128,42 @@ const e2eUpgradeOffer = ref(false);
3906
4128
  const e2eUpgradePw = ref('');
3907
4129
  const e2eUpgradeBusy = ref(false);
3908
4130
 
4131
+ /* wiring:e2 escrow-offer — offering an EXISTING folder an escrow slot.
4132
+ *
4133
+ * Adoption covers folders created after it, which on an installation that
4134
+ * has been running for a while is nobody's folders: the ones that matter
4135
+ * already exist. The server cannot reach them — adding a slot needs the
4136
+ * folder master key. Its owner can, from inside, with the password, and the
4137
+ * one moment that password exists in the browser is an unlock. So this
4138
+ * lives exactly where the v1 recovery offer lives, in the same strip, with
4139
+ * the same shape.
4140
+ *
4141
+ * mode distinguishes the two ways in:
4142
+ * 'unlock' right after a successful password unlock — we still hold the
4143
+ * password, so Accept needs no typing, and Not now RECORDS a
4144
+ * refusal so the question is not asked again.
4145
+ * 'manual' the way back, from the unlocked strip, for somebody who said
4146
+ * no earlier. The password is gone by then, so it is typed, and
4147
+ * Cancel records nothing — they already answered once. */
4148
+ const e2eEscrowOffer = ref(false);
4149
+ const e2eEscrowOfferMode = ref<'unlock' | 'manual'>('unlock');
4150
+ const e2eEscrowPw = ref('');
4151
+ const e2eEscrowBusy = ref(false);
4152
+ const e2eEscrowErr = ref('');
4153
+
3909
4154
  /** The installation's escrow public key, or null when escrow is off.
3910
4155
  * Published in /api/capabilities on purpose — see docs/E2E-ENCRYPTION.md. */
3911
4156
  const e2eEscrowPub = computed<string | null>(
3912
4157
  () => capabilitiesData.value?.e2e_escrow?.public_key || null,
3913
4158
  );
4159
+ /** Where the honest version of "what escrow can and cannot do" lives.
4160
+ * ⚠ Linked from the offer on purpose: the notice says what accepting means,
4161
+ * and the page says what the use-notification can and cannot promise — that
4162
+ * an operator holding the private key can decrypt offline with no request,
4163
+ * no notification and no audit row. Somebody being asked to hand over a key
4164
+ * is entitled to read that before answering, not after. */
4165
+ const e2eEscrowDocsUrl = 'https://docs.filex.sh/E2E-ENCRYPTION#what-escrow-can-and-cannot-do';
4166
+
3914
4167
  const e2eEscrowKid = computed<string | null>(
3915
4168
  () => capabilitiesData.value?.e2e_escrow?.kid || null,
3916
4169
  );
@@ -3925,7 +4178,7 @@ function e2eRevokeAll() {
3925
4178
  }
3926
4179
  onBeforeUnmount(e2eRevokeAll);
3927
4180
 
3928
- /** Kilidi aç: marker kökten çek, parolayı YERELDE doğrula, KEK'i belleğe koy. */
4181
+ /** Unlock: fetch the marker from the root, verify the password LOCALLY, put the KEK in memory. */
3929
4182
  async function e2eUnlock() {
3930
4183
  if (!e2ePw.value || e2eUnlockBusy.value || !e2eRoot.value) return;
3931
4184
  e2eUnlockBusy.value = true;
@@ -3933,7 +4186,7 @@ async function e2eUnlock() {
3933
4186
  try {
3934
4187
  let markerText = '';
3935
4188
  try {
3936
- const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME));
4189
+ const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME), { fresh: true });
3937
4190
  URL.revokeObjectURL(url);
3938
4191
  markerText = await blob.text();
3939
4192
  } catch {
@@ -3962,6 +4215,18 @@ async function e2eUnlock() {
3962
4215
  if (marker.v === 1) {
3963
4216
  e2eUpgradePw.value = e2ePw.value;
3964
4217
  e2eUpgradeOffer.value = true;
4218
+ } else if (escrowOfferState(marker, e2eEscrowKid.value) === 'offer') {
4219
+ /* wiring:e2 escrow-offer — a v2 folder that predates escrow here.
4220
+ * ⚠ Only after the unlock SUCCEEDED, and only by password: accepting
4221
+ * gives the operator a key to this folder, so the person asked has to
4222
+ * be the person who can already open it. A v1 folder is handled by the
4223
+ * branch above, which seals an escrow slot as part of the upgrade and
4224
+ * already discloses that — two offers on one unlock would be two
4225
+ * chances to get the disclosure wrong. */
4226
+ e2eEscrowPw.value = e2ePw.value;
4227
+ e2eEscrowOfferMode.value = 'unlock';
4228
+ e2eEscrowErr.value = '';
4229
+ e2eEscrowOffer.value = true;
3965
4230
  }
3966
4231
  e2ePw.value = '';
3967
4232
  } finally {
@@ -3969,7 +4234,7 @@ async function e2eUnlock() {
3969
4234
  }
3970
4235
  }
3971
4236
 
3972
- /** "Kilitle": bellekteki anahtarı ve çözülmüş blob'ları at. */
4237
+ /** "Kilitle" (Lock): drop the in-memory key and the decrypted blobs. */
3973
4238
  function e2eLock() {
3974
4239
  if (!e2eRoot.value) return;
3975
4240
  e2eRing.lock(e2eRoot.value);
@@ -3978,9 +4243,9 @@ function e2eLock() {
3978
4243
  flashToast(t('e2e.locked_toast'));
3979
4244
  }
3980
4245
 
3981
- // Uzantıönizleme MIME'ı: çözülmüş blob'un <img>/<video>/<object>
3982
- // etiketlerinde doğru render'ı için (sunucu şifreli dosyayı octet-stream
3983
- // bilir, oradan gelen tip işe yaramaz).
4246
+ // Extensionpreview MIME: so the decrypted blob renders correctly in
4247
+ // <img>/<video>/<object> tags (the server knows the encrypted file as
4248
+ // octet-stream, so the type coming from there is useless).
3984
4249
  const E2E_MIME: Record<string, string> = {
3985
4250
  txt: 'text/plain', md: 'text/markdown', log: 'text/plain', csv: 'text/csv',
3986
4251
  json: 'application/json', xml: 'application/xml', html: 'text/html',
@@ -3997,9 +4262,10 @@ function e2eMimeFor(n: FileNode): string {
3997
4262
  }
3998
4263
 
3999
4264
  /**
4000
- * Dosyayı çek + çöz + objectURL'ini cache'le. Magic'siz (ör. DAV'la düz
4001
- * yazılmış) dosyada null döner çağıran normal ham akışa düşer. Yanlış
4002
- * anahtar/bozuk veri E2eDecryptError fırlatır (çağıran toast'lar).
4265
+ * Fetch the file + decrypt it + cache its objectURL. Returns null for a file
4266
+ * with no magic (e.g. written in the clear over DAV) — the caller then falls
4267
+ * back to the normal raw flow. A wrong key / corrupt data throws
4268
+ * E2eDecryptError (the caller toasts it).
4003
4269
  */
4004
4270
  async function e2eFetchDecrypted(n: FileNode): Promise<string | null> {
4005
4271
  const cached = e2eUrls.get(n.path);
@@ -4014,12 +4280,12 @@ async function e2eFetchDecrypted(n: FileNode): Promise<string | null> {
4014
4280
  return url;
4015
4281
  }
4016
4282
 
4017
- /** PreviewModal/QuickLook'a giden URL sağlayıcı: çözülmüş blob > ham URL. */
4283
+ /** URL provider handed to PreviewModal/QuickLook: decrypted blob > raw URL. */
4018
4284
  function e2ePreviewSrc(p: string): string {
4019
4285
  return e2eUrls.get(p) ?? api.previewUrl(p);
4020
4286
  }
4021
4287
 
4022
- /** Çöz + salt-okunur in-page önizleme (openNode/previewNode buraya iner). */
4288
+ /** Decrypt + read-only in-page preview (openNode/previewNode land here). */
4023
4289
  async function e2eOpenPreview(n: FileNode) {
4024
4290
  try {
4025
4291
  await e2eFetchDecrypted(n);
@@ -4034,7 +4300,7 @@ async function e2eOpenPreview(n: FileNode) {
4034
4300
  void markRecent(n);
4035
4301
  }
4036
4302
 
4037
- /** Çöz + orijinal adla indir. Magic'siz dosya olduğu gibi iner. */
4303
+ /** Decrypt + download under the original name. A file with no magic comes down as-is. */
4038
4304
  async function e2eDownload(n: FileNode) {
4039
4305
  try {
4040
4306
  const buf = await api.fetchArrayBuffer(n.path);
@@ -4057,7 +4323,7 @@ async function e2eDownload(n: FileNode) {
4057
4323
  }
4058
4324
  }
4059
4325
 
4060
- /** Upload listesişifreli File listesi (200MB üstü + marker adı atlanır). */
4326
+ /** Upload listencrypted File list (anything over 200MB + the marker name is skipped). */
4061
4327
  async function e2eEncryptUploads(list: File[]): Promise<File[]> {
4062
4328
  const kek = e2eKek();
4063
4329
  if (!kek) return [];
@@ -4078,10 +4344,10 @@ async function e2eEncryptUploads(list: File[]): Promise<File[]> {
4078
4344
  return out;
4079
4345
  }
4080
4346
 
4081
- /** EncryptedFolderModal submit'i: klasörü + marker yükle + kilidi açık bırak. */
4347
+ /** EncryptedFolderModal submit: create the folder + upload the marker + leave it unlocked. */
4082
4348
  async function submitEncryptedFolder(payload: { name: string; password: string }) {
4083
4349
  if (e2eActive.value) {
4084
- // İç içe şifreli klasör kök tespitini bulanıklaştırır — MVP'de yok.
4350
+ // Nested encrypted folders blur root detectionnot in the MVP.
4085
4351
  flashToast(t('e2e.create.nested'));
4086
4352
  return;
4087
4353
  }
@@ -4101,7 +4367,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
4101
4367
  });
4102
4368
  const newDirWire = wireJoin(dirWire, payload.name);
4103
4369
  await api.uploadMultipart(newDirWire, [markerFile]);
4104
- // Oluşturan oturumda kilit açık başlar (parolayı az önce kendisi girdi).
4370
+ // It starts unlocked in the creating session (they just typed the password).
4105
4371
  e2eRing.set(newDirWire, fmk);
4106
4372
  e2eRingVer.value++;
4107
4373
  showEncFolder.value = false;
@@ -4204,7 +4470,7 @@ async function e2eRecoverUnlock(payload: { mode: 'recovery' | 'escrow'; value: s
4204
4470
  async function openRecoveryUnlock() {
4205
4471
  if (!e2eMarker.value && e2eRoot.value) {
4206
4472
  try {
4207
- const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME));
4473
+ const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME), { fresh: true });
4208
4474
  URL.revokeObjectURL(url);
4209
4475
  e2eMarker.value = parseMarker(await blob.text());
4210
4476
  } catch {
@@ -4243,6 +4509,101 @@ async function e2eDoUpgrade() {
4243
4509
  }
4244
4510
  }
4245
4511
 
4512
+ /* wiring:e2 escrow-offer ------------------------------------------- */
4513
+
4514
+ /** Whether this folder could be given an escrow slot, and whether its owner
4515
+ * has already answered. Drives the way-back button in the unlocked strip. */
4516
+ const e2eEscrowOfferState = computed(() => escrowOfferState(e2eMarker.value, e2eEscrowKid.value));
4517
+
4518
+ /** The way back. Somebody who said no last month can say yes today without
4519
+ * deleting and re-creating the folder — but the password is long gone from
4520
+ * memory, so they type it. That is not friction to be smoothed away: it is
4521
+ * the same proof of ownership the offer at unlock had, and handing the
4522
+ * operator a key deserves it. */
4523
+ function e2eOpenEscrowOffer() {
4524
+ e2eEscrowOfferMode.value = 'manual';
4525
+ e2eEscrowPw.value = '';
4526
+ e2eEscrowErr.value = '';
4527
+ e2eEscrowOffer.value = true;
4528
+ }
4529
+
4530
+ /** Write a marker back to the folder and adopt it as the one we are holding.
4531
+ * Both escrow-offer answers change only `.filex-e2e.json`. */
4532
+ async function e2eWriteMarker(next: E2eMarker) {
4533
+ const file = new File([JSON.stringify(next)], E2E_MARKER_NAME, { type: 'application/json' });
4534
+ await api.uploadMultipart(e2eRoot.value, [file]);
4535
+ e2eMarker.value = next;
4536
+ }
4537
+
4538
+ /** Accept: seal this folder's master key to the installation's escrow key.
4539
+ * No file is re-encrypted or moved — only the marker gains a slot. */
4540
+ async function e2eAcceptEscrow() {
4541
+ const marker = e2eMarker.value;
4542
+ const pub = e2eEscrowPub.value;
4543
+ if (!marker || !pub || !e2eRoot.value || e2eEscrowBusy.value) return;
4544
+ if (!e2eEscrowPw.value) {
4545
+ e2eEscrowErr.value = t('e2e.escrowoffer.password_required');
4546
+ return;
4547
+ }
4548
+ e2eEscrowBusy.value = true;
4549
+ e2eEscrowErr.value = '';
4550
+ try {
4551
+ // ⚠ The kid comes from the installation's CURRENT key, via the marker
4552
+ // addEscrowSlot writes — never from anything the UI is displaying. The
4553
+ // sibling bug went the other way (the dialog labelled a folder's slot
4554
+ // with the installation's kid); writing the installation's kid into a
4555
+ // slot sealed to some other key would be the same lie in reverse.
4556
+ const next = await addEscrowSlot(marker, e2eEscrowPw.value, pub);
4557
+ await e2eWriteMarker(next);
4558
+ e2eEscrowOffer.value = false;
4559
+ e2eEscrowPw.value = '';
4560
+ flashToast(t('e2e.escrowoffer.done'));
4561
+ } catch (err) {
4562
+ // A wrong password is the ordinary case here and reads as a typo, not
4563
+ // as a broken folder; anything else is worth reporting.
4564
+ if ((err as Error)?.name === 'E2eDecryptError') {
4565
+ e2eEscrowErr.value = t('e2e.escrowoffer.wrong_password');
4566
+ } else {
4567
+ e2eEscrowErr.value = t('e2e.escrowoffer.failed');
4568
+ emit('error', { message: (err as Error).message, context: { op: 'e2e-escrow-offer' } });
4569
+ }
4570
+ } finally {
4571
+ e2eEscrowBusy.value = false;
4572
+ }
4573
+ }
4574
+
4575
+ /** Not now. Recorded IN THE FOLDER, so the same person on another device is
4576
+ * not asked again and the answer survives a cleared browser. A question
4577
+ * that returns every single unlock is how people learn to click past
4578
+ * security dialogs without reading them. */
4579
+ async function e2eDeclineEscrow() {
4580
+ const marker = e2eMarker.value;
4581
+ if (!marker || !e2eRoot.value || e2eEscrowBusy.value) return;
4582
+ e2eEscrowBusy.value = true;
4583
+ try {
4584
+ await e2eWriteMarker(declineEscrowSlot(marker, new Date().toISOString()));
4585
+ e2eEscrowOffer.value = false;
4586
+ e2eEscrowPw.value = '';
4587
+ flashToast(t('e2e.escrowoffer.declined_toast'));
4588
+ } catch (err) {
4589
+ // ⚠ Fail towards asking again rather than towards silence: if the
4590
+ // refusal could not be written, the honest state is "not answered yet".
4591
+ e2eEscrowErr.value = t('e2e.escrowoffer.decline_failed');
4592
+ emit('error', { message: (err as Error).message, context: { op: 'e2e-escrow-decline' } });
4593
+ } finally {
4594
+ e2eEscrowBusy.value = false;
4595
+ }
4596
+ }
4597
+
4598
+ /** Close the way-back panel without answering anything. Distinct from
4599
+ * declining: they answered once already, and re-recording the same refusal
4600
+ * would overwrite the date it was actually made. */
4601
+ function e2eCloseEscrowOffer() {
4602
+ e2eEscrowOffer.value = false;
4603
+ e2eEscrowPw.value = '';
4604
+ e2eEscrowErr.value = '';
4605
+ }
4606
+
4246
4607
  /** Decline the offer. The folder keeps working exactly as it did, and the
4247
4608
  * prompt returns on the next unlock because the risk has not changed. */
4248
4609
  function e2eDeclineUpgrade() {
@@ -4277,7 +4638,7 @@ function closeRecoveryKey() {
4277
4638
  @drop="onDropUpload"
4278
4639
  @contextmenu="onContextCanvas"
4279
4640
  >
4280
- <!-- wiring:d1 — sekme şeridi: TEK sekmede hiç render edilmez (embed pixel-aynı) -->
4641
+ <!-- wiring:d1 — tab strip: not rendered at all on a SINGLE tab (embeds stay pixel-identical) -->
4281
4642
  <TabBar
4282
4643
  v-if="tabsVisible"
4283
4644
  :tabs="tabItems"
@@ -4294,7 +4655,7 @@ function closeRecoveryKey() {
4294
4655
  <!-- /wiring:d1 -->
4295
4656
  <Toolbar
4296
4657
  ref="toolbarRef"
4297
- :view-mode="displayedViewMode /* ui-fix — aktif panelin modu */"
4658
+ :view-mode="displayedViewMode /* ui-fix — the active pane's mode */"
4298
4659
  :search-query="searchQuery"
4299
4660
  :trash-active="trashActive"
4300
4661
  :actions="toolbarActions"
@@ -4311,10 +4672,13 @@ function closeRecoveryKey() {
4311
4672
  :nav-open="navToggleOn /* gezinti:g1 */"
4312
4673
  :nav-enabled="sideNavEnabled /* gezinti:g1 */"
4313
4674
  :view-modes="allowedViewModes /* gezinti:g1 */"
4675
+ :shell="driveShell ? 'drive' : 'classic' /* surucu:d1 */"
4676
+ :scope-label="driveScopeLabel /* surucu:d1 */"
4677
+ @open-palette="openPaletteWith /* surucu:d1 */"
4314
4678
  @toggle-inspector="toggleInspector /* koru:k1 */"
4315
4679
  @toggle-nav="toggleSideNav /* gezinti:g1 */"
4316
4680
  @open-theme="showThemeGallery = true /* wiring:c1 */"
4317
- @update:view-mode="setDisplayedViewMode($event) /* ui-fix — aktif panele */"
4681
+ @update:view-mode="setDisplayedViewMode($event) /* ui-fix — to the active pane */"
4318
4682
  @update:search-query="searchQuery = $event"
4319
4683
  @update:density="density = $event"
4320
4684
  @open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
@@ -4352,6 +4716,11 @@ function closeRecoveryKey() {
4352
4716
  :show-identity-surfaces="identitySurfaces"
4353
4717
  :can-write="canWriteHere && !atVirtualRoot && !trashActive"
4354
4718
  :locale="locale"
4719
+ :new-menu="driveShell /* surucu:d1 */"
4720
+ :can-request-files="canWriteHere && !atVirtualRoot && !trashActive && !navView /* surucu:d1 */"
4721
+ :quota="quotaSnapshot /* surucu:d1 */"
4722
+ :theme="themeMode /* surucu:d1 — the teleported New menu leaves .fe */"
4723
+ @request-files="openFileRequest /* surucu:d1 */"
4355
4724
  @toggle="toggleSideNav"
4356
4725
  @close="closeNavDrawer"
4357
4726
  @open-view="loadNavView"
@@ -4372,14 +4741,23 @@ function closeRecoveryKey() {
4372
4741
  :aria-label="t('sidenav.close')"
4373
4742
  @click="closeNavDrawer"
4374
4743
  ></button>
4375
- <!-- ui-fix — sol panelin başlığı (breadcrumb + durum şeritleri + body)
4376
- tek bir sarmalda: split modunda bu sarmal sol yarıya sığar, böylece
4377
- breadcrumb tüm sayfayı değil kendi panelini kaplar (SecondaryPane'in
4378
- kendi kırıntısıyla simetrik). Aktif-panel vurgusu da bu sarmalda. -->
4744
+ <!-- ui-fix — the left pane's header (breadcrumb + status strips + body)
4745
+ in one wrapper: in split mode this wrapper fits the left half, so the
4746
+ breadcrumb spans its own pane rather than the whole page (symmetric
4747
+ with SecondaryPane's own crumbs). The active-pane accent lives on
4748
+ this wrapper too. -->
4379
4749
  <div
4380
4750
  class="fe__primary"
4381
- :class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
4751
+ :class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — active-pane accent */"
4382
4752
  >
4753
+ <!-- surucu:d1 — the breadcrumb row. In the drive shell it also carries the
4754
+ two controls that belong to a LISTING rather than to the app (the view
4755
+ switcher and the details toggle), which is where the mockups put them
4756
+ and where the header then has room for one wide search field.
4757
+ Everywhere else the wrapper is `display: contents`, so the breadcrumb
4758
+ is the same flex child of `.fe__primary` it has always been — one
4759
+ Breadcrumb, not a second copy that can drift. -->
4760
+ <div :class="driveShell ? 'fe-subhead' : 'fe-subhead--plain'">
4383
4761
  <Breadcrumb
4384
4762
  :dirname="dirname"
4385
4763
  :adapter="adapter"
@@ -4392,6 +4770,49 @@ function closeRecoveryKey() {
4392
4770
  @crumb-context="onCrumbContext"
4393
4771
  @crumb-drop="onCrumbDropInto"
4394
4772
  />
4773
+ <div v-if="driveShell" class="fe-subhead__actions">
4774
+ <ViewSwitcher
4775
+ :view-mode="displayedViewMode"
4776
+ :locale="locale"
4777
+ :modes="allowedViewModes"
4778
+ @update:view-mode="setDisplayedViewMode($event)"
4779
+ />
4780
+ <button
4781
+ type="button"
4782
+ class="fe-btn fe-btn--icon-only fe-toolbar__inspector"
4783
+ :class="{ 'is-active': showInspector }"
4784
+ :aria-pressed="showInspector"
4785
+ :title="t('toolbar.inspector')"
4786
+ :aria-label="t('toolbar.inspector')"
4787
+ data-testid="subhead-inspector"
4788
+ @click="toggleInspector"
4789
+ >
4790
+ <svg
4791
+ class="fe-ficon"
4792
+ viewBox="0 0 24 24"
4793
+ fill="none"
4794
+ stroke="currentColor"
4795
+ stroke-width="1.8"
4796
+ stroke-linecap="round"
4797
+ aria-hidden="true"
4798
+ focusable="false"
4799
+ >
4800
+ <circle cx="12" cy="12" r="9" />
4801
+ <path d="M12 11v5" />
4802
+ <circle cx="12" cy="7.6" r="1" fill="currentColor" stroke="none" />
4803
+ </svg>
4804
+ </button>
4805
+ </div>
4806
+ </div>
4807
+ <FilterBar
4808
+ v-if="driveShell && !atVirtualRoot"
4809
+ :value="driveFilters"
4810
+ :locale="locale"
4811
+ :theme="themeMode"
4812
+ :shown="displayFiles.length"
4813
+ :total="files.length"
4814
+ @update:value="setDriveFilters"
4815
+ />
4395
4816
 
4396
4817
  <!-- Live presence: who else is viewing this folder (empty → nothing shown).
4397
4818
  When the live socket is unavailable the same strip carries a small
@@ -4410,8 +4831,9 @@ function closeRecoveryKey() {
4410
4831
  </span>
4411
4832
  </div>
4412
4833
 
4413
- <!-- wiring:e2 — kilit açık şeridi: şifreli klasörde anahtar bellekteyken
4414
- görünür; "Kilitle" anahtarı ve çözülmüş blob'ları atar. -->
4834
+ <!-- wiring:e2 — unlocked strip: visible in an encrypted folder while the
4835
+ key is in memory; "Kilitle" (Lock) drops the key and the decrypted
4836
+ blobs. -->
4415
4837
  <!-- wiring:e2 recovery — a v1 folder just opened by password. Offer it
4416
4838
  recovery HERE, visibly, rather than doing anything silently: this is
4417
4839
  the only moment filex holds the password, and (when the install has
@@ -4438,9 +4860,76 @@ function closeRecoveryKey() {
4438
4860
  </button>
4439
4861
  </div>
4440
4862
  </div>
4863
+ <!-- wiring:e2 escrow-offer — an existing folder that predates escrow on
4864
+ this installation. Shown only after an unlock SUCCEEDED, because
4865
+ accepting hands the operator a permanent second key to this folder
4866
+ and the only person entitled to do that is the one who can already
4867
+ open it. Doing nothing leaves the folder exactly as it is. -->
4868
+ <div v-if="e2eEscrowOffer" class="fe-e2e-upgrade fe-e2e-upgrade--escrow" role="alert">
4869
+ <div class="fe-e2e-upgrade__text">
4870
+ <strong>{{ t('e2e.escrowoffer.title') }}</strong>
4871
+ <p>{{ t('e2e.escrowoffer.body') }}</p>
4872
+ <p class="fe-e2e-upgrade__escrow">
4873
+ {{ t('e2e.escrowoffer.consequence') }}
4874
+ <a :href="e2eEscrowDocsUrl" target="_blank" rel="noopener noreferrer">
4875
+ {{ t('e2e.escrowoffer.learn_more') }}
4876
+ </a>
4877
+ </p>
4878
+ <p v-if="e2eEscrowKid" class="fe-e2e-upgrade__escrow">
4879
+ {{ t('e2e.recover.escrow_kid') }}: <code>{{ e2eEscrowKid }}</code>
4880
+ </p>
4881
+ <!-- The way-back path has no password in memory any more, so it is
4882
+ typed. The same proof of ownership the unlock path already had. -->
4883
+ <label v-if="e2eEscrowOfferMode === 'manual'" class="fe-e2e-upgrade__pw">
4884
+ <span>{{ t('e2e.escrowoffer.password_label') }}</span>
4885
+ <input
4886
+ v-model="e2eEscrowPw"
4887
+ type="password"
4888
+ class="fe-input"
4889
+ autocomplete="current-password"
4890
+ :disabled="e2eEscrowBusy"
4891
+ @keyup.enter="e2eAcceptEscrow"
4892
+ />
4893
+ </label>
4894
+ <p v-if="e2eEscrowErr" class="fe-form__error" role="alert">{{ e2eEscrowErr }}</p>
4895
+ </div>
4896
+ <div class="fe-e2e-upgrade__actions">
4897
+ <button
4898
+ type="button"
4899
+ class="fe-btn"
4900
+ :disabled="e2eEscrowBusy"
4901
+ @click="e2eEscrowOfferMode === 'manual' ? e2eCloseEscrowOffer() : e2eDeclineEscrow()"
4902
+ >
4903
+ {{
4904
+ e2eEscrowOfferMode === 'manual'
4905
+ ? t('e2e.escrowoffer.cancel')
4906
+ : t('e2e.escrowoffer.decline')
4907
+ }}
4908
+ </button>
4909
+ <button
4910
+ type="button"
4911
+ class="fe-btn fe-btn--primary"
4912
+ :disabled="e2eEscrowBusy"
4913
+ @click="e2eAcceptEscrow"
4914
+ >
4915
+ {{ e2eEscrowBusy ? t('e2e.escrowoffer.busy') : t('e2e.escrowoffer.accept') }}
4916
+ </button>
4917
+ </div>
4918
+ </div>
4441
4919
  <div v-if="e2eUnlocked" class="fe-e2e-strip" role="status">
4442
4920
  <span class="fe-e2e-strip__icon" aria-hidden="true">🔒</span>
4443
4921
  <span class="fe-e2e-strip__label">{{ t('e2e.strip.label') }}</span>
4922
+ <!-- The way back for somebody who declined. Quiet, but present: a
4923
+ refusal that could not be reversed without deleting the folder
4924
+ would not be a decision, it would be a trap. -->
4925
+ <button
4926
+ v-if="e2eEscrowOfferState !== 'n/a' && !e2eEscrowOffer"
4927
+ type="button"
4928
+ class="fe-btn fe-e2e-strip__btn"
4929
+ @click="e2eOpenEscrowOffer"
4930
+ >
4931
+ {{ t('e2e.escrowoffer.strip_action') }}
4932
+ </button>
4444
4933
  <button type="button" class="fe-btn fe-e2e-strip__btn" @click="e2eLock">
4445
4934
  {{ t('e2e.strip.lock') }}
4446
4935
  </button>
@@ -4457,7 +4946,7 @@ function closeRecoveryKey() {
4457
4946
  keeps the current list, exactly as before. -->
4458
4947
  <div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
4459
4948
  <span class="fe-sr-only">{{ t('loading') }}</span>
4460
- <div v-if="viewMode !== 'list' /* wiring:d2 — galeri de grid iskeletini kullanır */" class="fe-skel-grid" aria-hidden="true">
4949
+ <div v-if="viewMode !== 'list' /* wiring:d2 — the gallery uses the grid skeleton too */" class="fe-skel-grid" aria-hidden="true">
4461
4950
  <div v-for="i in 8" :key="i" class="fe-skel-card">
4462
4951
  <div class="fe-skel fe-skel--thumb"></div>
4463
4952
  <div class="fe-skel fe-skel--label"></div>
@@ -4535,9 +5024,10 @@ function closeRecoveryKey() {
4535
5024
  </details>
4536
5025
  <!-- /wiring:c4 -->
4537
5026
  </div>
4538
- <!-- wiring:e2 — şifreli klasör kilit ekranı: parola doğru girilene dek
4539
- listeleme render edilmez. Parola tarayıcıda marker'a karşı
4540
- doğrulanır; sunucuya gitmez. -->
5027
+ <!-- wiring:e2 — encrypted-folder lock screen: the listing is not
5028
+ rendered until the correct password is entered. The password is
5029
+ verified against the marker in the browser; it never reaches the
5030
+ server. -->
4541
5031
  <div v-else-if="e2eLocked" class="fe-state fe-e2e-lock">
4542
5032
  <svg
4543
5033
  class="fe-state__art"
@@ -4675,6 +5165,36 @@ function closeRecoveryKey() {
4675
5165
  </svg>
4676
5166
  <p class="fe-state__title">{{ t('empty.trash.title') }}</p>
4677
5167
  </div>
5168
+ <!-- surucu:d1 — the folder HAS rows and the filters hid all of them.
5169
+ "This folder is empty" would be false, and the way back is the chip
5170
+ row just above, so the message names it and offers the button. -->
5171
+ <div
5172
+ v-else-if="!loading && filtersOn && displayFiles.length === 0 && files.length > 0"
5173
+ class="fe-state"
5174
+ data-testid="empty-filtered"
5175
+ >
5176
+ <svg
5177
+ class="fe-state__art"
5178
+ viewBox="0 0 120 100"
5179
+ width="110"
5180
+ height="92"
5181
+ fill="none"
5182
+ stroke="currentColor"
5183
+ stroke-width="2"
5184
+ stroke-linecap="round"
5185
+ stroke-linejoin="round"
5186
+ aria-hidden="true"
5187
+ >
5188
+ <path d="M26 28h68L68 58v24l-16 8V58z" />
5189
+ </svg>
5190
+ <p class="fe-state__title">{{ t('filter.empty.title') }}</p>
5191
+ <p class="fe-state__hint">{{ t('filter.empty.hint') }}</p>
5192
+ <div class="fe-state__actions">
5193
+ <button type="button" class="fe-btn" @click="clearDriveFilters">
5194
+ {{ t('filter.clear') }}
5195
+ </button>
5196
+ </div>
5197
+ </div>
4678
5198
  <!-- Loaded, zero files, no search: the real empty-folder state. The
4679
5199
  upload affordances follow write permission (RBAC viewers only get
4680
5200
  the title). -->
@@ -4707,7 +5227,7 @@ function closeRecoveryKey() {
4707
5227
  </div>
4708
5228
  <ListView
4709
5229
  v-else-if="viewMode === 'list'"
4710
- :files="files"
5230
+ :files="displayFiles /* surucu:d1 */"
4711
5231
  :selected="selection.selected.value"
4712
5232
  :clipped="clippedPaths"
4713
5233
  :show-parent-path="!!searchQuery"
@@ -4715,6 +5235,7 @@ function closeRecoveryKey() {
4715
5235
  :loading="loading"
4716
5236
  :keep-badge-for="desktopSync ? keepBadgeFor : undefined"
4717
5237
  :starred-ids="starredIds"
5238
+ :star-enabled="identitySurfaces"
4718
5239
  :api-base="props.config.apiBase ?? ''"
4719
5240
  :auth-headers="() => buildAuthHeaders()"
4720
5241
  :auth-credentials="api.credentialsMode()"
@@ -4727,7 +5248,8 @@ function closeRecoveryKey() {
4727
5248
  />
4728
5249
  <GridView
4729
5250
  v-else-if="viewMode === 'grid' /* wiring:d2 — v-else → v-else-if (3. mod eklendi) */"
4730
- :files="files"
5251
+ :files="displayFiles /* surucu:d1 */"
5252
+ :sections="driveShell /* surucu:d1 */"
4731
5253
  :selected="selection.selected.value"
4732
5254
  :clipped="clippedPaths"
4733
5255
  :show-parent-path="!!searchQuery"
@@ -4736,6 +5258,7 @@ function closeRecoveryKey() {
4736
5258
  :keep-badge-for="desktopSync ? keepBadgeFor : undefined"
4737
5259
  :thumb-src="thumbs.src"
4738
5260
  :starred-ids="starredIds"
5261
+ :star-enabled="identitySurfaces"
4739
5262
  :api-base="props.config.apiBase ?? ''"
4740
5263
  :auth-headers="() => buildAuthHeaders()"
4741
5264
  :auth-credentials="api.credentialsMode()"
@@ -4746,10 +5269,10 @@ function closeRecoveryKey() {
4746
5269
  @item-drop-into="onItemDropInto"
4747
5270
  @star-change="onStarChange"
4748
5271
  />
4749
- <!-- wiring:d2 — galeri görünümü (GridView ile aynı event sözleşmesi) -->
5272
+ <!-- wiring:d2 — gallery view (same event contract as GridView) -->
4750
5273
  <GalleryView
4751
5274
  v-else
4752
- :files="files"
5275
+ :files="displayFiles /* surucu:d1 */"
4753
5276
  :selected="selection.selected.value"
4754
5277
  :clipped="clippedPaths"
4755
5278
  :show-parent-path="!!searchQuery"
@@ -4757,6 +5280,7 @@ function closeRecoveryKey() {
4757
5280
  :loading="loading"
4758
5281
  :thumb-src="thumbs.src"
4759
5282
  :starred-ids="starredIds"
5283
+ :star-enabled="identitySurfaces"
4760
5284
  :api-base="props.config.apiBase ?? ''"
4761
5285
  :auth-headers="() => buildAuthHeaders()"
4762
5286
  :auth-credentials="api.credentialsMode()"
@@ -4771,9 +5295,9 @@ function closeRecoveryKey() {
4771
5295
  </div>
4772
5296
  </div><!-- /fe__primary ui-fix -->
4773
5297
 
4774
- <!-- wiring:d1 — tab başına split: sağ ikincil panel (dar modda kapalı).
4775
- :key tab kimliğine bağlı tab geçişinde pane kendi konumuyla temiz
4776
- remount olur. -->
5298
+ <!-- wiring:d1 — per-tab split: the secondary pane on the right (off in
5299
+ narrow mode). :key is bound to the tab id on a tab switch the pane
5300
+ remounts cleanly with its own location. -->
4777
5301
  <SecondaryPane
4778
5302
  :keep-badge-for="desktopSync ? keepBadgeFor : undefined"
4779
5303
  v-if="splitVisible && activeSplit"
@@ -4792,14 +5316,15 @@ function closeRecoveryKey() {
4792
5316
  :active="paneIsActive"
4793
5317
  :view-mode="paneViewMode /* ui-fix */"
4794
5318
  :thumb-src="thumbs.src /* ui-fix */"
4795
- :trash-visible="config.trashVisible !== false /* ui-fix — çöp satırı simetrisi */"
5319
+ :trash-visible="config.trashVisible !== false /* ui-fix — trash row symmetry */"
5320
+ :nav-offers-trash="navOffersTrash /* surucu:d1 — the same door is not opened twice */"
4796
5321
  @navigate="onPaneNavigate"
4797
5322
  @activate="activePane = 'split'"
4798
5323
  @close="closeSplit"
4799
5324
  @open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
4800
5325
  @transfer="onPaneTransfer"
4801
- @context="onPaneContext /* ui-fix — yan panel sağ-tık menüsü */"
4802
- @open-trash="onPaneOpenTrash /* ui-fix — çöp kutusu ana panelde açılır */"
5326
+ @context="onPaneContext /* ui-fix — side-pane right-click menu */"
5327
+ @open-trash="onPaneOpenTrash /* ui-fix — the trash opens in the main pane */"
4803
5328
  />
4804
5329
  <!-- /wiring:d1 -->
4805
5330
 
@@ -4815,7 +5340,9 @@ function closeRecoveryKey() {
4815
5340
  :locale="locale"
4816
5341
  :narrow="isNarrow"
4817
5342
  :thumb-src="thumbs.src"
5343
+ :tabs="driveShell /* surucu:d1 */"
4818
5344
  @close="closeInspector"
5345
+ @share-created="onInspectorShareCreated /* surucu:d1 */"
4819
5346
  @manage-permissions="onInspectorManage"
4820
5347
  @toast="flashToast"
4821
5348
  @changed="() => load()"
@@ -4868,7 +5395,7 @@ function closeRecoveryKey() {
4868
5395
  <div v-if="dragOver" class="fe__dragover">
4869
5396
  <div class="fe__dragover-card">
4870
5397
  <span class="fe-icon">⬆</span>
4871
- <p>Dosyaları buraya bırak</p>
5398
+ <p>{{ t('dropzone.hint') }}</p>
4872
5399
  </div>
4873
5400
  </div>
4874
5401
 
@@ -4945,12 +5472,12 @@ function closeRecoveryKey() {
4945
5472
  <NewFolderModal
4946
5473
  :open="showNewFolder"
4947
5474
  :locale="locale"
4948
- :encrypted-option="!e2eActive /* wiring:e2 — içe şifreli klasör yok */"
5475
+ :encrypted-option="!e2eActive /* wiring:e2 — no nested encrypted folders */"
4949
5476
  @close="showNewFolder = false"
4950
5477
  @submit="submitNewFolder"
4951
5478
  @encrypted="showNewFolder = false; showEncFolder = true /* wiring:e2 */"
4952
5479
  />
4953
- <!-- wiring:e2 — şifreli klasör oluşturma modalı -->
5480
+ <!-- wiring:e2 — encrypted-folder creation modal -->
4954
5481
  <EncryptedFolderModal
4955
5482
  :open="showEncFolder"
4956
5483
  :locale="locale"
@@ -4974,8 +5501,8 @@ function closeRecoveryKey() {
4974
5501
  :open="showRecoveryUnlock"
4975
5502
  :locale="locale"
4976
5503
  :has-recovery="markerHasRecovery(e2eMarker)"
4977
- :has-escrow="markerHasEscrow(e2eMarker) && !!e2eEscrowKid"
4978
- :escrow-kid="e2eEscrowKid"
5504
+ :escrow-state="escrowAvailability(e2eMarker, e2eEscrowKid)"
5505
+ :escrow-kid="e2eMarker?.esc?.kid || e2eEscrowKid"
4979
5506
  :busy="e2eRecoverBusy"
4980
5507
  :error="e2eRecoverErr"
4981
5508
  @close="showRecoveryUnlock = false"
@@ -5010,12 +5537,12 @@ function closeRecoveryKey() {
5010
5537
  :locale="locale"
5011
5538
  :file="previewTarget"
5012
5539
  :theme="themeMode"
5013
- :preview-url="(p) => e2ePreviewSrc(p) /* wiring:e2 — çözülmüş blob > ham URL */"
5540
+ :preview-url="(p) => e2ePreviewSrc(p) /* wiring:e2 — decrypted blob > raw URL */"
5014
5541
  :download-url="(p) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
5015
- :only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 — OO ciphertext açamaz */"
5542
+ :only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 — OO cannot open ciphertext */"
5016
5543
  :only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
5017
- :new-tab-enabled="!e2eActive /* wiring:e2 — standalone rota ham baytı çeker */"
5018
- :save-text-endpoint="e2eActive ? null : api.endpoints.saveText || null /* wiring:e2 — düz metin kaydı sızıntı olur */"
5544
+ :new-tab-enabled="!e2eActive /* wiring:e2 — the standalone route pulls raw bytes */"
5545
+ :save-text-endpoint="e2eActive ? null : api.endpoints.saveText || null /* wiring:e2 — a plaintext save would be a leak */"
5019
5546
  :open-mode="previewMode"
5020
5547
  :auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
5021
5548
  :auth-credentials="api.credentialsMode()"
@@ -5031,6 +5558,7 @@ function closeRecoveryKey() {
5031
5558
  :file-name="convertTarget?.basename || convertTarget?.path || ''"
5032
5559
  :fetch-bytes="() => api.fetchArrayBuffer(convertTarget?.path ?? '')"
5033
5560
  :upload="(f) => api.uploadMultipart(qualify(currentPath), [f]).then(() => {})"
5561
+ :locale="locale"
5034
5562
  @close="showConvert = false"
5035
5563
  @done="onConvertDone"
5036
5564
  />
@@ -5042,7 +5570,8 @@ function closeRecoveryKey() {
5042
5570
  :size="typeof permTarget.size === 'number' ? permTarget.size : undefined"
5043
5571
  :locale="locale === 'en' ? 'en' : 'tr'"
5044
5572
  :share-max-ttl-days="shareMaxTtlDays"
5045
- @close="showPerm = false"
5573
+ :initial-tab="permInitialTab /* surucu:d1 */"
5574
+ @close="showPerm = false; permInitialTab = undefined"
5046
5575
  />
5047
5576
 
5048
5577
  <!-- Recently-opened tray. Anchored to the toolbar trigger via fixed
@@ -5108,6 +5637,7 @@ function closeRecoveryKey() {
5108
5637
 
5109
5638
  <!-- cila:c wiring — command palette (Ctrl/Cmd+K) + shortcuts help (?) -->
5110
5639
  <CommandPalette
5640
+ :initial-query="paletteSeed /* surucu:d1 */"
5111
5641
  :open="showPalette"
5112
5642
  :locale="locale"
5113
5643
  :files="files"
@@ -5121,7 +5651,7 @@ function closeRecoveryKey() {
5121
5651
  @navigate="(p: string) => load(p)"
5122
5652
  @new-folder="showNewFolder = true"
5123
5653
  @upload="triggerUpload"
5124
- @toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3 mod döngüsü, aktif panele */"
5654
+ @toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3-mode cycle, to the active pane */"
5125
5655
  @open-trash="loadTrash"
5126
5656
  @refresh="() => load()"
5127
5657
  @go-up="goUp"