@brftech/filex-core 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/filex-core.js +6475 -6370
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +39 -39
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +51 -0
- package/package.json +1 -1
- package/src/FileExplorer.vue +203 -51
- package/src/components/Breadcrumb.vue +3 -2
- package/src/components/GalleryView.vue +3 -2
- package/src/components/GridView.vue +3 -2
- package/src/components/ListView.vue +3 -2
- package/src/components/SecondaryPane.vue +5 -10
- package/src/lib/dragOut.ts +0 -0
- package/src/lib/transfer.ts +51 -0
- package/src/locales/en.ts +6 -1
- package/src/locales/tr.ts +6 -1
- package/src/types/ExplorerConfig.ts +47 -0
package/dist/index.d.ts
CHANGED
|
@@ -829,6 +829,57 @@ export declare interface ExplorerConfig {
|
|
|
829
829
|
driver?: string;
|
|
830
830
|
readOnly?: boolean;
|
|
831
831
|
}>;
|
|
832
|
+
/**
|
|
833
|
+
* Desktop-shell hook — dragging rows OUT of the window onto the OS.
|
|
834
|
+
*
|
|
835
|
+
* Present only in the filex desktop app. A web page cannot hand the OS a
|
|
836
|
+
* list of files: Chromium carries one `DownloadURL` per drag, so the browser
|
|
837
|
+
* gets a single-file drag-out for free (the explorer sets it itself) and
|
|
838
|
+
* folders/multi-selections need real local paths — which is what the shell
|
|
839
|
+
* provides here.
|
|
840
|
+
*
|
|
841
|
+
* The bytes have to exist BEFORE the drag starts — the OS copies from a path
|
|
842
|
+
* at drop time — and the shell has two ways to satisfy that, which is why
|
|
843
|
+
* this is more than one call:
|
|
844
|
+
*
|
|
845
|
+
* `prepare` — fetch local copies up front. The explorer calls it for small
|
|
846
|
+
* selections as soon as they are selected, so the common drag hands over
|
|
847
|
+
* real, complete files (correct even when the drop target is an
|
|
848
|
+
* application that reads the file immediately).
|
|
849
|
+
* `start` — begin the OS drag, whatever the size. The shell may hand the
|
|
850
|
+
* OS empty placeholders and download into wherever they land afterwards,
|
|
851
|
+
* so this is never gated on `prepare` having finished.
|
|
852
|
+
* `cancel` — the drag ended INSIDE the explorer (an internal move). The
|
|
853
|
+
* shell stops waiting for a drop it will never see.
|
|
854
|
+
*
|
|
855
|
+
* `onProgress` drives the explorer's toast; `error: 'drop_not_found'` means
|
|
856
|
+
* the drop went somewhere the shell cannot write to (an application rather
|
|
857
|
+
* than a folder) and nothing was transferred.
|
|
858
|
+
*/
|
|
859
|
+
dragOut?: {
|
|
860
|
+
prepare: (items: Array<{
|
|
861
|
+
path: string;
|
|
862
|
+
basename: string;
|
|
863
|
+
type: 'file' | 'dir';
|
|
864
|
+
}>) => Promise<{
|
|
865
|
+
ready: boolean;
|
|
866
|
+
error?: string;
|
|
867
|
+
}>;
|
|
868
|
+
start: (items: Array<{
|
|
869
|
+
path: string;
|
|
870
|
+
basename: string;
|
|
871
|
+
type: 'file' | 'dir';
|
|
872
|
+
}>) => void | Promise<void>;
|
|
873
|
+
cancel?: () => void | Promise<void>;
|
|
874
|
+
onProgress?: (cb: (p: {
|
|
875
|
+
done: number;
|
|
876
|
+
total: number;
|
|
877
|
+
name?: string;
|
|
878
|
+
dropped?: string;
|
|
879
|
+
finished?: boolean;
|
|
880
|
+
error?: string;
|
|
881
|
+
}) => void) => void;
|
|
882
|
+
};
|
|
832
883
|
/**
|
|
833
884
|
* Desktop-shell hook — selective sync ("keep on this computer").
|
|
834
885
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brftech/filex-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "filex core — Vue 3 source of truth for the filex file manager (FileExplorer + ConnectionsPanel SFCs, composables, types)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/filex-core.umd.cjs",
|
package/src/FileExplorer.vue
CHANGED
|
@@ -106,6 +106,19 @@ import {
|
|
|
106
106
|
injectTrashRow,
|
|
107
107
|
hydrateTrashRow as hydrateTrashRowShared,
|
|
108
108
|
} from './lib/listing';
|
|
109
|
+
import { resolveTransfer, type TransferIntent } from './lib/transfer';
|
|
110
|
+
import {
|
|
111
|
+
activeNativeDrag,
|
|
112
|
+
beginNativeDrag,
|
|
113
|
+
canDownloadUrlDrag,
|
|
114
|
+
dragKey,
|
|
115
|
+
downloadUrlPayload,
|
|
116
|
+
endNativeDrag,
|
|
117
|
+
hasInternalDrag,
|
|
118
|
+
internalDragItems,
|
|
119
|
+
internalDragOrigin,
|
|
120
|
+
type DragItem,
|
|
121
|
+
} from './lib/dragOut';
|
|
109
122
|
|
|
110
123
|
import NewFolderModal from './modals/NewFolderModal.vue';
|
|
111
124
|
import RenameModal from './modals/RenameModal.vue';
|
|
@@ -1934,17 +1947,21 @@ async function paste() {
|
|
|
1934
1947
|
return;
|
|
1935
1948
|
}
|
|
1936
1949
|
|
|
1950
|
+
const targetWire = qualify(currentPath.value);
|
|
1951
|
+
// Depo farkı yalnız mesajı değiştirir: kes KESTİR, kopyala KOPYALAR —
|
|
1952
|
+
// hedef başka depo olsa da. Aktarımı sunucu yapar (ops kuyruğu hem
|
|
1953
|
+
// kaynak hem hedef depoyu taşır).
|
|
1954
|
+
const plan = resolveTransfer(items, targetWire, cb.mode === 'cut' ? 'move' : 'copy');
|
|
1937
1955
|
if (cb.mode === 'cut') {
|
|
1938
|
-
const targetWire = qualify(currentPath.value);
|
|
1939
1956
|
const originWire = qualify(sourceDir) || undefined;
|
|
1940
1957
|
const { op } = await api.moveAsync(items, targetWire, originWire);
|
|
1941
1958
|
registerMoveUndo(op.id, items, targetWire, originWire);
|
|
1942
1959
|
pendingOps.register(op);
|
|
1943
|
-
flashToast('Taşıma kuyruğa alındı');
|
|
1960
|
+
flashToast(plan.cross ? t('split.cross_move') : 'Taşıma kuyruğa alındı');
|
|
1944
1961
|
} else {
|
|
1945
|
-
const { op } = await api.copy(items,
|
|
1962
|
+
const { op } = await api.copy(items, targetWire);
|
|
1946
1963
|
pendingOps.register(op);
|
|
1947
|
-
flashToast('Kopyalama kuyruğa alındı');
|
|
1964
|
+
flashToast(plan.cross ? t('split.cross_copy') : 'Kopyalama kuyruğa alındı');
|
|
1948
1965
|
}
|
|
1949
1966
|
clipboard.value = { mode: null, items: [], sourcePath: null };
|
|
1950
1967
|
} catch (err) {
|
|
@@ -2304,6 +2321,10 @@ function isExternalFileDrag(ev: DragEvent): boolean {
|
|
|
2304
2321
|
const dt = ev.dataTransfer;
|
|
2305
2322
|
if (!dt) return false;
|
|
2306
2323
|
if (dt.types && dt.types.includes(FE_DND_MIME)) return false;
|
|
2324
|
+
/* wiring:f1 — kendi başlattığımız işletim sistemi sürüklemesi 'Files' taşır
|
|
2325
|
+
ama YÜKLEME değildir: aynı baytları sunucudan indirip geri yüklemek
|
|
2326
|
+
(taşıma yerine kopya, üstelik iki kat trafik) olurdu. */
|
|
2327
|
+
if (activeNativeDrag()) return false;
|
|
2307
2328
|
// Some browsers expose `items` early in the drag, others only on
|
|
2308
2329
|
// drop. When `items` is available we use it as the authoritative
|
|
2309
2330
|
// signal — `kind === 'file'` means a real OS file. When unavailable
|
|
@@ -2336,7 +2357,7 @@ function onDragOver(ev: DragEvent) {
|
|
|
2336
2357
|
/* wiring:d1 — iç sürüklemeler kök gövdeye de bırakılabilir olmalı ki split
|
|
2337
2358
|
panelinden ana panelin BOŞLUĞUNA bırakmak çalışsın (origin dragover'da
|
|
2338
2359
|
okunamaz — karar drop anında verilir; aynı-klasör drop'u no-op kalır). */
|
|
2339
|
-
if (ev
|
|
2360
|
+
if (hasInternalDrag(ev)) {
|
|
2340
2361
|
ev.preventDefault();
|
|
2341
2362
|
return;
|
|
2342
2363
|
}
|
|
@@ -2348,28 +2369,28 @@ function onDragOver(ev: DragEvent) {
|
|
|
2348
2369
|
function onDropUpload(ev: DragEvent) {
|
|
2349
2370
|
/* wiring:d1 — split panelinden ana panelin boşluğuna bırakma = geçerli
|
|
2350
2371
|
klasöre aktar (aynı klasörden gelenler no-op, eski davranış korunur). */
|
|
2351
|
-
if (ev
|
|
2352
|
-
const d1Origin = ev
|
|
2372
|
+
if (hasInternalDrag(ev)) {
|
|
2373
|
+
const d1Origin = internalDragOrigin(ev) || '';
|
|
2353
2374
|
const d1Here = qualify(currentPath.value);
|
|
2354
|
-
|
|
2375
|
+
const d1Items = internalDragItems(ev);
|
|
2376
|
+
if (d1Items && d1Origin && d1Here && d1Origin !== d1Here && !trashMode.value && canWriteHere.value) {
|
|
2355
2377
|
ev.preventDefault();
|
|
2356
2378
|
dragCounter.value = 0;
|
|
2357
2379
|
dragOver.value = false;
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
} catch {
|
|
2362
|
-
/* bozuk payload — yok say */
|
|
2363
|
-
}
|
|
2380
|
+
endNativeDrag();
|
|
2381
|
+
cancelShellDrag();
|
|
2382
|
+
void transferItems(d1Items.map((i) => i.path), d1Here, d1Origin);
|
|
2364
2383
|
return;
|
|
2365
2384
|
}
|
|
2366
2385
|
}
|
|
2367
2386
|
/* /wiring:d1 */
|
|
2368
2387
|
// Internal row drag — nothing to do here, the row drop handler
|
|
2369
2388
|
// in GridView/ListView already resolved the move.
|
|
2370
|
-
if (ev
|
|
2389
|
+
if (hasInternalDrag(ev)) {
|
|
2371
2390
|
dragCounter.value = 0;
|
|
2372
2391
|
dragOver.value = false;
|
|
2392
|
+
endNativeDrag();
|
|
2393
|
+
cancelShellDrag();
|
|
2373
2394
|
return;
|
|
2374
2395
|
}
|
|
2375
2396
|
// Browser-internal image drag without real files — bail before
|
|
@@ -2406,11 +2427,54 @@ function onWindowDrop(ev: DragEvent) {
|
|
|
2406
2427
|
onMounted(() => {
|
|
2407
2428
|
window.addEventListener('dragover', onWindowDragOver);
|
|
2408
2429
|
window.addEventListener('drop', onWindowDrop);
|
|
2430
|
+
window.addEventListener('pointerup', onGlobalPointerUp);
|
|
2431
|
+
window.addEventListener('blur', onGlobalPointerUp);
|
|
2432
|
+
/* wiring:f1 — kabuk hazırlarken tek bir "hazırlanıyor" der; her dosyada
|
|
2433
|
+
toast atmak ilerlemeyi değil gürültüyü gösterirdi. Bitişi 'hazır'
|
|
2434
|
+
toast'ı (prepareDragOut) duyurur. */
|
|
2435
|
+
dragOut.value?.onProgress?.((p) => {
|
|
2436
|
+
// Bırakma SONRASI iş (yer tutucu yolu) her zaman duyurulur — kullanıcı
|
|
2437
|
+
// dosyayı bir klasöre bıraktı, orada ne olduğunu bilmeye hakkı var.
|
|
2438
|
+
// Sessizlik yalnız kimsenin istemediği ön-hazırlık için geçerli.
|
|
2439
|
+
const afterDrop = !!p?.dropped;
|
|
2440
|
+
if (p?.error === 'drop_not_found') {
|
|
2441
|
+
flashToast(t('dragout.not_found'));
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
if (p?.error) {
|
|
2445
|
+
if (afterDrop || !dragOutQuiet) flashToast(p.error);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
if (afterDrop) {
|
|
2449
|
+
flashToast(p?.finished ? t('dragout.done') : t('dragout.downloading'));
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (dragOutQuiet) return;
|
|
2453
|
+
if (!p?.finished && p?.done === 0) flashToast(t('dragout.preparing'));
|
|
2454
|
+
});
|
|
2409
2455
|
});
|
|
2456
|
+
/* wiring:f1 — işletim sistemi sürüklemesi bizde 'dragend' üretmez (HTML5
|
|
2457
|
+
sürüklemesi hiç başlamadı). Fare bırakıldığında kaydı düşürüyoruz; aksi
|
|
2458
|
+
halde sonraki normal sürükleme bir önceki seçimi taşıdığını sanırdı. */
|
|
2459
|
+
function onGlobalPointerUp() {
|
|
2460
|
+
if (activeNativeDrag()) {
|
|
2461
|
+
endNativeDrag();
|
|
2462
|
+
// Bırakma bizim penceremizde OLMAYABİLİR de; kabuğun izlemesini yalnız
|
|
2463
|
+
// kendi bırakma yollarımız iptal eder (onDropUpload / onItemDropInto).
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/** Sürükleme uygulama içinde bitti: kabuk artık bir bırakma beklemesin. */
|
|
2468
|
+
function cancelShellDrag() {
|
|
2469
|
+
if (dragOut.value?.cancel) void Promise.resolve(dragOut.value.cancel()).catch(() => undefined);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2410
2472
|
onBeforeUnmount(() => {
|
|
2411
2473
|
window.removeEventListener('dragover', onWindowDragOver);
|
|
2412
2474
|
window.removeEventListener('drop', onWindowDrop);
|
|
2413
2475
|
window.removeEventListener('hashchange', onHashChange);
|
|
2476
|
+
window.removeEventListener('pointerup', onGlobalPointerUp);
|
|
2477
|
+
window.removeEventListener('blur', onGlobalPointerUp);
|
|
2414
2478
|
});
|
|
2415
2479
|
|
|
2416
2480
|
const clippedPaths = computed<Set<string>>(() => {
|
|
@@ -2437,10 +2501,107 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
|
|
|
2437
2501
|
.filter((n) => !clippedPaths.value.has(n.path))
|
|
2438
2502
|
.filter((n) => n.basename !== '.trash')
|
|
2439
2503
|
.map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
|
|
2504
|
+
|
|
2505
|
+
/* wiring:f1 — dışarı sürükleme (masaüstü / başka uygulama).
|
|
2506
|
+
Kabuk varsa sürükleme HER ZAMAN işletim sistemi sürüklemesidir: klasörler
|
|
2507
|
+
ve çoklu seçim ayrı ayrı GERÇEK dosya olarak düşer, BOYUT SINIRI YOK.
|
|
2508
|
+
Baytlar hazırsa gerçek dosyalar verilir; değilse kabuk boş "yer tutucu"
|
|
2509
|
+
verir, nereye bırakıldığını bulur ve indirmeyi ORAYA yapar (bkz.
|
|
2510
|
+
desktop/src/dropwatch.ts). Uygulama İÇİNDE bırakılırsa sürükleme yine
|
|
2511
|
+
sunucu tarafı taşımadır — payload bizde durur — ve kabuğa "vazgeç" denir
|
|
2512
|
+
ki sürücüleri boşuna dinlemesin. */
|
|
2513
|
+
if (dragOut.value && items.length > 0) {
|
|
2514
|
+
ev.preventDefault();
|
|
2515
|
+
beginNativeDrag(items, qualify(currentPath.value));
|
|
2516
|
+
void Promise.resolve(dragOut.value.start(items)).catch((err) => {
|
|
2517
|
+
endNativeDrag();
|
|
2518
|
+
cancelShellDrag();
|
|
2519
|
+
emit('error', { message: (err as Error).message, context: { op: 'drag-out' } });
|
|
2520
|
+
});
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2440
2524
|
ev.dataTransfer.setData(FE_DND_MIME, JSON.stringify(items));
|
|
2441
2525
|
ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — paneller arası origin damgası */
|
|
2442
2526
|
ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
|
|
2443
2527
|
ev.dataTransfer.effectAllowed = 'move';
|
|
2528
|
+
|
|
2529
|
+
/* Tek dosya + çerezli oturum: tarayıcının kendi indirme yolu (DownloadURL)
|
|
2530
|
+
bırakma anında dosyayı masaüstüne indirir; hiçbir hazırlık gerekmez.
|
|
2531
|
+
Bearer token'lı kurulumda (masaüstü uygulaması) bu yol kimliksiz gider,
|
|
2532
|
+
o yüzden orada üstteki yerel yol devrededir — bkz. lib/dragOut.ts. */
|
|
2533
|
+
if (items.length === 1 && items[0] && canDownloadUrlDrag(props.config.auth)) {
|
|
2534
|
+
const payload = downloadUrlPayload(items[0], api.downloadUrl(items[0].path), node.mime_type);
|
|
2535
|
+
if (payload) ev.dataTransfer.setData('DownloadURL', payload);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
if (dragOut.value && items.length > 0) void prepareDragOut(items);
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
/* === wiring:f1 — dışarı sürükleme hazırlığı ===
|
|
2542
|
+
*
|
|
2543
|
+
* Baytlar sürükleme BAŞLAMADAN diskte olmak zorunda (işletim sistemi bırakma
|
|
2544
|
+
* anında yoldan kopyalar), o yüzden hazırlık ayrı bir adımdır. Bitince
|
|
2545
|
+
* kullanıcıya "hazır" denir; ikinci sürükleme artık işletim sistemi
|
|
2546
|
+
* sürüklemesidir ve anında başlar. Bu bilgisayarda tutulan (senkron) dosyalar
|
|
2547
|
+
* için hazırlık ilk seferde de anında biter — kopya zaten yerelde.
|
|
2548
|
+
*/
|
|
2549
|
+
const dragOut = computed(() => props.config.dragOut ?? null);
|
|
2550
|
+
const dragOutReadyKey = ref('');
|
|
2551
|
+
const dragOutBusy = ref(false);
|
|
2552
|
+
/** True while a preparation nobody asked for is running. */
|
|
2553
|
+
let dragOutQuiet = false;
|
|
2554
|
+
|
|
2555
|
+
/* Küçük seçimler SEÇİLDİĞİ anda hazırlanır, çünkü hazırlık bittikten sonraki
|
|
2556
|
+
sürükleme işletim sistemi sürüklemesidir: bir belgeyi seçip masaüstüne
|
|
2557
|
+
sürüklemek böylece İLK denemede çalışır. Tavan bilerek düşük — tıklayarak
|
|
2558
|
+
gezinen biri her satırda film indirmemeli; sınırın üstündekiler ilk
|
|
2559
|
+
sürüklemede hazırlanır, ikinci sürükleme anında başlar. */
|
|
2560
|
+
const DRAGOUT_PREFETCH_MAX_BYTES = 8 * 1024 * 1024;
|
|
2561
|
+
const DRAGOUT_PREFETCH_MAX_ITEMS = 10;
|
|
2562
|
+
let dragOutPrefetchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
2563
|
+
|
|
2564
|
+
watch(
|
|
2565
|
+
() => selection.selected.value,
|
|
2566
|
+
() => {
|
|
2567
|
+
if (!dragOut.value) return;
|
|
2568
|
+
clearTimeout(dragOutPrefetchTimer);
|
|
2569
|
+
const nodes = selection.nodes.value;
|
|
2570
|
+
if (nodes.length === 0 || nodes.length > DRAGOUT_PREFETCH_MAX_ITEMS) return;
|
|
2571
|
+
// Klasörün boyutu listelemede bilinmez; onu tahmin etmek yerine ilk
|
|
2572
|
+
// sürüklemeye bırakıyoruz.
|
|
2573
|
+
if (nodes.some((n) => n.type !== 'file')) return;
|
|
2574
|
+
const total = nodes.reduce((sum, n) => sum + (n.size ?? 0), 0);
|
|
2575
|
+
if (total > DRAGOUT_PREFETCH_MAX_BYTES) return;
|
|
2576
|
+
const items = nodes.map((n) => ({ path: n.path, basename: n.basename, type: n.type }));
|
|
2577
|
+
dragOutPrefetchTimer = setTimeout(() => void prepareDragOut(items, true), 400);
|
|
2578
|
+
},
|
|
2579
|
+
{ deep: true },
|
|
2580
|
+
);
|
|
2581
|
+
|
|
2582
|
+
async function prepareDragOut(items: DragItem[], quiet = false): Promise<void> {
|
|
2583
|
+
const hook = dragOut.value;
|
|
2584
|
+
if (!hook || dragOutBusy.value) return;
|
|
2585
|
+
const key = dragKey(items);
|
|
2586
|
+
if (key === dragOutReadyKey.value) return;
|
|
2587
|
+
dragOutBusy.value = true;
|
|
2588
|
+
dragOutQuiet = quiet;
|
|
2589
|
+
try {
|
|
2590
|
+
const res = await hook.prepare(items);
|
|
2591
|
+
if (res?.ready) {
|
|
2592
|
+
dragOutReadyKey.value = key;
|
|
2593
|
+
// Sessiz tur seçimle tetiklenir; kullanıcı bir şey İSTEMEDİ, o yüzden
|
|
2594
|
+
// ona bir şey söylemek de gerekmez. Sürüklemeyle başlayan tur söyler.
|
|
2595
|
+
if (!quiet) flashToast(t('dragout.ready'));
|
|
2596
|
+
} else if (res?.error && !quiet) {
|
|
2597
|
+
flashToast(res.error);
|
|
2598
|
+
}
|
|
2599
|
+
} catch (err) {
|
|
2600
|
+
emit('error', { message: (err as Error).message, context: { op: 'drag-out-prepare' } });
|
|
2601
|
+
} finally {
|
|
2602
|
+
dragOutBusy.value = false;
|
|
2603
|
+
dragOutQuiet = false;
|
|
2604
|
+
}
|
|
2444
2605
|
}
|
|
2445
2606
|
|
|
2446
2607
|
async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string, originOverride?: string): Promise<void> {
|
|
@@ -2468,15 +2629,10 @@ async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: s
|
|
|
2468
2629
|
|
|
2469
2630
|
async function onItemDropInto(target: FileNode, ev: DragEvent) {
|
|
2470
2631
|
if (target.type !== 'dir') return;
|
|
2471
|
-
const
|
|
2472
|
-
if (!
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
items = JSON.parse(raw);
|
|
2476
|
-
} catch {
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
if (items.length === 0) return;
|
|
2632
|
+
const items = internalDragItems(ev);
|
|
2633
|
+
if (!items || items.length === 0) return;
|
|
2634
|
+
endNativeDrag();
|
|
2635
|
+
cancelShellDrag();
|
|
2480
2636
|
|
|
2481
2637
|
const targetDir = target.path; // qualified
|
|
2482
2638
|
const sources = items
|
|
@@ -2489,15 +2645,10 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
|
|
|
2489
2645
|
}
|
|
2490
2646
|
|
|
2491
2647
|
async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
|
|
2492
|
-
const
|
|
2493
|
-
if (!
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
items = JSON.parse(raw);
|
|
2497
|
-
} catch {
|
|
2498
|
-
return;
|
|
2499
|
-
}
|
|
2500
|
-
if (items.length === 0) return;
|
|
2648
|
+
const items = internalDragItems(ev);
|
|
2649
|
+
if (!items || items.length === 0) return;
|
|
2650
|
+
endNativeDrag();
|
|
2651
|
+
cancelShellDrag();
|
|
2501
2652
|
|
|
2502
2653
|
const targetDir = adapterPath; // already qualified by breadcrumb
|
|
2503
2654
|
const sources = items
|
|
@@ -3012,33 +3163,31 @@ async function panePaste() {
|
|
|
3012
3163
|
flashToast('Aynı klasöre kesilemez');
|
|
3013
3164
|
return;
|
|
3014
3165
|
}
|
|
3015
|
-
await transferItems(cb.items.map((n) => n.path), targetWire, originWire, cb.mode === 'copy');
|
|
3166
|
+
await transferItems(cb.items.map((n) => n.path), targetWire, originWire, cb.mode === 'copy' ? 'copy' : 'move');
|
|
3016
3167
|
clipboard.value = { mode: null, items: [], sourcePath: null };
|
|
3017
3168
|
}
|
|
3018
3169
|
|
|
3019
3170
|
// ---- paneller arası aktarım ----------------------------------------
|
|
3020
|
-
|
|
3021
|
-
function wireAdapterOf(p: string): string {
|
|
3022
|
-
const i = p.indexOf('://');
|
|
3023
|
-
return i === -1 ? '' : p.slice(0, i);
|
|
3024
|
-
}
|
|
3025
3171
|
function dndOrigin(ev: DragEvent): string | undefined {
|
|
3026
|
-
|
|
3027
|
-
return v || undefined;
|
|
3172
|
+
return internalDragOrigin(ev);
|
|
3028
3173
|
}
|
|
3029
3174
|
|
|
3030
3175
|
/**
|
|
3031
3176
|
* transferItems — panel-arası / pano aktarımının tek kapısı.
|
|
3032
|
-
*
|
|
3033
|
-
*
|
|
3034
|
-
*
|
|
3035
|
-
*
|
|
3177
|
+
*
|
|
3178
|
+
* Ne yapılacağını `resolveTransfer` söyler (lib/transfer.ts): sürükleme aynı
|
|
3179
|
+
* depoda TAŞI, depolar arasında KOPYALA; panodan gelen kes/kopyala ise ne
|
|
3180
|
+
* dendiyse odur — kes, hedef başka depo olsa da TAŞIR (sunucu baytları
|
|
3181
|
+
* aktarıp kaynağı siler). ⚠ Eskiden depolar arası her aktarım sessizce
|
|
3182
|
+
* kopyaya düşüyordu: kullanıcı "kes" deyip dosyayı iki yerde buluyordu.
|
|
3183
|
+
* Bitince ikincil panel de tazelenir (ana panel moveSourcesAsync /
|
|
3184
|
+
* pendingOps onSettled üzerinden zaten tazelenir).
|
|
3036
3185
|
*/
|
|
3037
3186
|
async function transferItems(
|
|
3038
3187
|
sources: string[],
|
|
3039
3188
|
targetWire: string,
|
|
3040
3189
|
originWire?: string,
|
|
3041
|
-
|
|
3190
|
+
intent: TransferIntent = 'auto',
|
|
3042
3191
|
): Promise<void> {
|
|
3043
3192
|
// ui-fix — yerinde bırakma (source parent === target) no-op: backend
|
|
3044
3193
|
// "kendine kopyala" 400'ü engellenir (paneller arası + pano yolu).
|
|
@@ -3046,19 +3195,22 @@ async function transferItems(
|
|
|
3046
3195
|
(p) => p && p !== targetWire && !targetWire.startsWith(p + '/') && !sameDir(wireParent(p), targetWire),
|
|
3047
3196
|
);
|
|
3048
3197
|
if (list.length === 0 || !targetWire) return;
|
|
3049
|
-
const
|
|
3050
|
-
|
|
3051
|
-
if (cross || forceCopy) {
|
|
3198
|
+
const plan = resolveTransfer(list, targetWire, intent);
|
|
3199
|
+
if (plan.kind === 'copy') {
|
|
3052
3200
|
try {
|
|
3053
3201
|
const { op } = await api.copy(list, targetWire);
|
|
3054
3202
|
pendingOps.register(op);
|
|
3055
|
-
flashToast(cross ? t('split.cross_copy') : t('split.copy_queued'));
|
|
3203
|
+
flashToast(plan.cross ? t('split.cross_copy') : t('split.copy_queued'));
|
|
3056
3204
|
} catch (err) {
|
|
3205
|
+
// ⚠ Sunucunun kendi mesajını göster. Burada sabit bir "depolar arası
|
|
3206
|
+
// desteklenmiyor" metni vardı; artık DESTEKLENİYOR, yani o metin
|
|
3207
|
+
// gerçek sebebi (izin, salt-okunur depo, dolu kota) örterdi.
|
|
3057
3208
|
emit('error', { message: (err as Error).message, context: { op: 'transfer', targetWire } });
|
|
3058
|
-
flashToast(
|
|
3209
|
+
flashToast((err as Error).message);
|
|
3059
3210
|
return;
|
|
3060
3211
|
}
|
|
3061
3212
|
} else {
|
|
3213
|
+
if (plan.cross) flashToast(t('split.cross_move'));
|
|
3062
3214
|
await moveSourcesAsync(list, targetWire, 'move-transfer', originWire);
|
|
3063
3215
|
}
|
|
3064
3216
|
void splitPaneRef.value?.reload();
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* navigates, Escape cancels.
|
|
21
21
|
*/
|
|
22
22
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|
23
|
+
import { hasInternalDrag } from '../lib/dragOut';
|
|
23
24
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
24
25
|
import { useLocale } from '../composables/useLocale';
|
|
25
26
|
|
|
@@ -199,14 +200,14 @@ function onContext(ev: MouseEvent, crumb: Crumb) {
|
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
function onCrumbDragOver(ev: DragEvent) {
|
|
202
|
-
if (!ev
|
|
203
|
+
if (!hasInternalDrag(ev)) return;
|
|
203
204
|
ev.preventDefault();
|
|
204
205
|
ev.stopPropagation();
|
|
205
206
|
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
function onCrumbDrop(ev: DragEvent, crumb: Crumb) {
|
|
209
|
-
if (!ev
|
|
210
|
+
if (!hasInternalDrag(ev)) return;
|
|
210
211
|
ev.preventDefault();
|
|
211
212
|
ev.stopPropagation();
|
|
212
213
|
emit('crumb-drop', crumb.adapterPath, ev);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* below and size+date revealed on hover/focus. GridView itself is untouched.
|
|
10
10
|
*/
|
|
11
11
|
import { ref } from 'vue';
|
|
12
|
+
import { hasInternalDrag } from '../lib/dragOut';
|
|
12
13
|
import type { FileNode } from '../types/FileNode';
|
|
13
14
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
14
15
|
import { useLocale } from '../composables/useLocale';
|
|
@@ -74,7 +75,7 @@ const dropTargetPath = ref<string | null>(null);
|
|
|
74
75
|
|
|
75
76
|
function onItemDragOver(n: FileNode, ev: DragEvent) {
|
|
76
77
|
if (n.type !== 'dir') return;
|
|
77
|
-
if (!ev
|
|
78
|
+
if (!hasInternalDrag(ev)) return;
|
|
78
79
|
ev.preventDefault();
|
|
79
80
|
ev.stopPropagation();
|
|
80
81
|
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
@@ -88,7 +89,7 @@ function onItemDragLeave(n: FileNode) {
|
|
|
88
89
|
function onItemDrop(n: FileNode, ev: DragEvent) {
|
|
89
90
|
dropTargetPath.value = null;
|
|
90
91
|
if (n.type !== 'dir') return;
|
|
91
|
-
if (!ev
|
|
92
|
+
if (!hasInternalDrag(ev)) return;
|
|
92
93
|
ev.preventDefault();
|
|
93
94
|
ev.stopPropagation();
|
|
94
95
|
emit('item-drop-into', n, ev);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { ref } from 'vue'; /* wiring:c4 */
|
|
6
6
|
import type { FileNode } from '../types/FileNode';
|
|
7
|
+
import { hasInternalDrag } from '../lib/dragOut';
|
|
7
8
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
8
9
|
import { useLocale } from '../composables/useLocale';
|
|
9
10
|
import { fileIconSvg } from '../lib/fileIcons';
|
|
@@ -75,7 +76,7 @@ const dropTargetPath = ref<string | null>(null);
|
|
|
75
76
|
|
|
76
77
|
function onItemDragOver(n: FileNode, ev: DragEvent) {
|
|
77
78
|
if (n.type !== 'dir') return;
|
|
78
|
-
if (!ev
|
|
79
|
+
if (!hasInternalDrag(ev)) return;
|
|
79
80
|
ev.preventDefault();
|
|
80
81
|
ev.stopPropagation();
|
|
81
82
|
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
@@ -90,7 +91,7 @@ function onItemDragLeave(n: FileNode) {
|
|
|
90
91
|
function onItemDrop(n: FileNode, ev: DragEvent) {
|
|
91
92
|
dropTargetPath.value = null; /* wiring:c4 */
|
|
92
93
|
if (n.type !== 'dir') return;
|
|
93
|
-
if (!ev
|
|
94
|
+
if (!hasInternalDrag(ev)) return;
|
|
94
95
|
ev.preventDefault();
|
|
95
96
|
ev.stopPropagation();
|
|
96
97
|
emit('item-drop-into', n, ev);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* across List and Grid.
|
|
8
8
|
*/
|
|
9
9
|
import { computed, ref } from 'vue';
|
|
10
|
+
import { hasInternalDrag } from '../lib/dragOut';
|
|
10
11
|
import type { FileNode } from '../types/FileNode';
|
|
11
12
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
12
13
|
import { useLocale } from '../composables/useLocale';
|
|
@@ -95,7 +96,7 @@ const dropTargetPath = ref<string | null>(null);
|
|
|
95
96
|
|
|
96
97
|
function onItemDragOver(n: FileNode, ev: DragEvent) {
|
|
97
98
|
if (n.type !== 'dir') return;
|
|
98
|
-
if (!ev
|
|
99
|
+
if (!hasInternalDrag(ev)) return;
|
|
99
100
|
ev.preventDefault();
|
|
100
101
|
ev.stopPropagation();
|
|
101
102
|
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
|
|
@@ -110,7 +111,7 @@ function onItemDragLeave(n: FileNode) {
|
|
|
110
111
|
function onItemDrop(n: FileNode, ev: DragEvent) {
|
|
111
112
|
dropTargetPath.value = null; /* wiring:c4 */
|
|
112
113
|
if (n.type !== 'dir') return;
|
|
113
|
-
if (!ev
|
|
114
|
+
if (!hasInternalDrag(ev)) return;
|
|
114
115
|
ev.preventDefault();
|
|
115
116
|
ev.stopPropagation();
|
|
116
117
|
emit('item-drop-into', n, ev);
|
|
@@ -31,6 +31,7 @@ import GalleryView from './GalleryView.vue';
|
|
|
31
31
|
// undo) correctly across panes.
|
|
32
32
|
const FE_DND_MIME = 'application/x-brf-files';
|
|
33
33
|
const FE_DND_SRC_MIME = 'application/x-brf-files-src';
|
|
34
|
+
import { hasInternalDrag, internalDragItems, internalDragOrigin } from '../lib/dragOut';
|
|
34
35
|
|
|
35
36
|
const props = defineProps<{
|
|
36
37
|
api: FileApi;
|
|
@@ -256,19 +257,13 @@ function onRowDragStart(n: FileNode, ev: DragEvent) {
|
|
|
256
257
|
const dropBg = ref(false);
|
|
257
258
|
|
|
258
259
|
function acceptDrag(ev: DragEvent): boolean {
|
|
259
|
-
return
|
|
260
|
+
return hasInternalDrag(ev);
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
function handleDropPayload(ev: DragEvent, targetWire: string) {
|
|
263
|
-
const
|
|
264
|
-
if (!
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
items = JSON.parse(raw);
|
|
268
|
-
} catch {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
const origin = ev.dataTransfer?.getData(FE_DND_SRC_MIME) || undefined;
|
|
264
|
+
const items = internalDragItems(ev);
|
|
265
|
+
if (!items || !targetWire) return;
|
|
266
|
+
const origin = internalDragOrigin(ev);
|
|
272
267
|
const sources = items
|
|
273
268
|
.map((i) => i.path)
|
|
274
269
|
.filter((p) => p && p !== targetWire && !targetWire.startsWith(p + '/'));
|
|
Binary file
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a transfer between two places actually means.
|
|
3
|
+
*
|
|
4
|
+
* Three gestures land on the same wire call — drag a row onto a folder, paste
|
|
5
|
+
* after Ctrl+X, paste after Ctrl+C — and they do NOT mean the same thing once
|
|
6
|
+
* the two ends live in different depolar:
|
|
7
|
+
*
|
|
8
|
+
* • Ctrl+C → paste is a copy, wherever it lands.
|
|
9
|
+
* • Ctrl+X → paste is a move, wherever it lands. Across depolar the server
|
|
10
|
+
* streams the bytes over and then deletes the original; before v0.27.0 the
|
|
11
|
+
* explorer quietly downgraded this to a copy and the user was left with the
|
|
12
|
+
* file in both places, believing they had moved it.
|
|
13
|
+
* • Dragging is a move inside one depo and a COPY across two — the rule
|
|
14
|
+
* Explorer and Finder have taught everyone: a drag between drives copies.
|
|
15
|
+
*
|
|
16
|
+
* Kept as a pure function so both the pane path and the clipboard path ask the
|
|
17
|
+
* same question, and so the answer is testable without a server.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** `alpha://a/b` → `alpha`; a bare path → `''`. */
|
|
21
|
+
export function wireAdapterOf(p: string): string {
|
|
22
|
+
const i = String(p ?? '').indexOf('://');
|
|
23
|
+
return i === -1 ? '' : p.slice(0, i);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What the caller asked for. `auto` is a drag: let the depolar decide. */
|
|
27
|
+
export type TransferIntent = 'auto' | 'copy' | 'move';
|
|
28
|
+
|
|
29
|
+
export interface TransferPlan {
|
|
30
|
+
/** What to actually ask the server for. */
|
|
31
|
+
kind: 'copy' | 'move';
|
|
32
|
+
/** True when at least one source lives in another depo than the target. */
|
|
33
|
+
cross: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveTransfer(
|
|
37
|
+
sources: string[],
|
|
38
|
+
targetWire: string,
|
|
39
|
+
intent: TransferIntent = 'auto',
|
|
40
|
+
): TransferPlan {
|
|
41
|
+
const target = wireAdapterOf(targetWire);
|
|
42
|
+
// A source with no prefix is a legacy embedder's bare path: it can only mean
|
|
43
|
+
// "the same depo I am looking at", so it never counts as crossing.
|
|
44
|
+
const cross = sources.some((p) => {
|
|
45
|
+
const a = wireAdapterOf(p);
|
|
46
|
+
return a !== '' && target !== '' && a !== target;
|
|
47
|
+
});
|
|
48
|
+
if (intent === 'copy') return { kind: 'copy', cross };
|
|
49
|
+
if (intent === 'move') return { kind: 'move', cross };
|
|
50
|
+
return { kind: cross ? 'copy' : 'move', cross };
|
|
51
|
+
}
|
package/src/locales/en.ts
CHANGED
|
@@ -379,7 +379,12 @@ export const en: Record<string, string> = {
|
|
|
379
379
|
'split.retry': 'Retry',
|
|
380
380
|
'split.copy_queued': 'Copy queued',
|
|
381
381
|
'split.cross_copy': 'Different storages — copy queued instead',
|
|
382
|
-
'
|
|
382
|
+
'dragout.downloading': 'Downloading into the folder you dropped on…',
|
|
383
|
+
'dragout.not_found': 'Could not find where it was dropped — a drop onto an application (rather than a folder) cannot be filled in',
|
|
384
|
+
'dragout.done': 'Download finished',
|
|
385
|
+
'dragout.ready': 'Files are ready — drag them to your desktop',
|
|
386
|
+
'dragout.preparing': 'Getting them ready to drag…',
|
|
387
|
+
'split.cross_move': 'Different storages — move queued (bytes travel, then the original is removed)',
|
|
383
388
|
/* === /wiring:d1 === */
|
|
384
389
|
/* wiring:d2 — gallery view */
|
|
385
390
|
'toolbar.view.gallery': 'Gallery',
|
package/src/locales/tr.ts
CHANGED
|
@@ -379,7 +379,12 @@ export const tr: Record<string, string> = {
|
|
|
379
379
|
'split.retry': 'Yeniden dene',
|
|
380
380
|
'split.copy_queued': 'Kopyalama kuyruğa alındı',
|
|
381
381
|
'split.cross_copy': 'Depolar farklı — kopyalama kuyruğa alındı',
|
|
382
|
-
'
|
|
382
|
+
'dragout.downloading': 'Bırakılan klasöre indiriliyor…',
|
|
383
|
+
'dragout.not_found': 'Bırakılan yer bulunamadı — dosya bir klasöre değil bir uygulamaya bırakıldıysa indirme yapılamaz',
|
|
384
|
+
'dragout.done': 'İndirme tamamlandı',
|
|
385
|
+
'dragout.ready': 'Dosyalar hazır — masaüstüne sürükleyebilirsin',
|
|
386
|
+
'dragout.preparing': 'Sürükleme için hazırlanıyor…',
|
|
387
|
+
'split.cross_move': 'Depolar farklı — taşıma kuyruğa alındı (baytlar aktarılıp kaynak silinir)',
|
|
383
388
|
/* === /wiring:d1 === */
|
|
384
389
|
/* wiring:d2 — galeri görünümü */
|
|
385
390
|
'toolbar.view.gallery': 'Galeri',
|