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