import 'dart:async'; import 'package:flutter/material.dart'; import '../../app/transfer_job.dart'; import '../../core/file_ops.dart'; import '../../core/vfs.dart'; import '../../l10n/tr.dart'; import '../explorer_drag.dart'; import '../notice.dart'; import 'explorer_controller.dart'; import 'explorer_pane.dart'; import 'explorer_select.dart'; import 'transfer_flow.dart'; import 'write_guard.dart'; /// 약속 (E5d-6): 끌어 놓기 (82). 끌 항목은 누른 것이 표시한 것 중 하나면 표시한 것 모두 (바깥 것만). 놓으면 설정 "끌어 놓기 기본" /// (묻기 · 복사 · 이동) 뒤 탐색기의 복사 · 이동 ([ExplorerTransfers.run]). 압축 안으로는 놓지 않고, 압축 안의 것은 꺼내기로. /// 이동이 끝나면 알림의 [되돌리기] - 새로 생긴 것은 통째로, 합친 폴더는 앱이 옮긴 파일만 (덮어쓴 것 · 모르는 것은 되돌리지 않고 알림) class ExplorerDrops { ExplorerDrops(this.ctl, this.sel, this.guard, this.transfers); final ExplorerController ctl; final ExplorerSelect sel; final ExplorerGuard guard; final ExplorerTransfers transfers; /// 82: 끌 항목 - 누른 것이 표시한 것 중 하나면 표시한 것 모두 (바깥 것만), 아니면 그것 하나 List dragPaths(ExplorerPane pane, FileEntry e) => sel.isMarked(pane, e.path) && !pane.sel.isEmpty ? outermostPaths(pane.sel.marked) : [e.path]; /// 82: 놓음 - 설정 "끌어 놓기 기본" (묻기 · 복사 · 이동) 뒤 지금의 복사 · 이동 (확인 창 · 같은 이름 · 진행 패널). /// 이동이 끝나면 아래 알림에 [되돌리기] (10초) Future drop(BuildContext context, DragLoad load, int to, String dest) async { if (!canDropInto(load, to, dest) || !context.mounted) return; // X-plore 비교 f: 압축 안으로는 놓지 않고, 압축 안의 것은 꺼내기로 if (guard.insideArchive(dest) || ctl.arc.contains(dest)) { ctl.host.snack(tr('압축 안은 읽기 전용입니다 (꺼내기만 할 수 있습니다)')); return; } if (load.paths.any(guard.insideArchive)) { await guard.extractFrom(context, load.paths, dest); return; } ctl ..active = to ..update(); final mode = ctl.c.settings.explorerDropAction; final move = mode == 'ask' ? await askDropAction(context, load.paths, dest) : mode == 'move'; if (move == null || !context.mounted) return; // 되돌리기에 쓸 것: 대상에 이미 있던 이름 (합친 폴더는 통째로 되돌리지 않게) final existed = {}; if (move) { for (final s in load.paths) { final t = vJoin(dest, vBasename(s)); if (await vExists(t)) existed.add(t); } } if (!context.mounted) return; await transfers.run(context, ctl.panes[load.from], load.paths, dest, move: move, onFinished: move ? (job) => _offerUndoMove(context, load, job, existed) : null); } /// 82: 이동 되돌리기 알림. 실제로 옮긴 것만 원래 자리로: 새로 생긴 항목은 통째로, 이미 있던 폴더에 합친 것은 앱이 옮긴 파일만 /// (robocopy 로 합친 폴더는 무엇을 옮겼는지 몰라 되돌리지 않고 알린다) void _offerUndoMove(BuildContext context, DragLoad load, TransferJob job, Set existed) { // (원래 폴더 → 지금 경로들) final back = >{}; // 되돌리지 않는 것: 합친 폴더 (robocopy - 무엇을 옮겼는지 모름) · 같은 이름을 덮어쓴 파일 (원래 파일은 이미 없음, 15 지적) var notUndoable = 0, overwritten = 0; final pairs = job.made.length == load.paths.length ? [for (var i = 0; i < load.paths.length; i++) (load.paths[i], job.made[i])] : [ for (final m in job.made) for (final s in load.paths) if (vBasename(s) == vBasename(m)) (s, m), ]; for (final (src, now) in pairs) { if (!existed.contains(now)) { back.putIfAbsent(vDirname(src), () => []).add(now); continue; } // 같은 이름을 덮어쓴 파일: 되돌리면 대상에 아무것도 남지 않으므로 그대로 둔다 if (!vIsDirSync(now)) { overwritten++; continue; } // 합친 폴더: 그 안에서 실제로 옮긴 파일만 final files = [for (final f in job.movedFiles) if (isSameOrInside(f, src) && !samePath(f, src)) f]; if (files.isEmpty) { notUndoable++; continue; } for (final f in files) { final rest = f.substring(src.length + 1); back.putIfAbsent(vDirname(f), () => []).add(vJoin(now, rest)); } } if (back.isEmpty || !context.mounted) return; final moved = back.values.fold(0, (a, l) => a + l.length); showNotice(ScaffoldMessenger.of(context)..hideCurrentSnackBar(), text: joinSentences([ trf('{0}개를 옮겼습니다.', [moved]), if (notUndoable > 0) trf('합친 폴더 {0}개는 무엇을 옮겼는지 몰라 되돌리지 않습니다.', [notUndoable]), if (overwritten > 0) trf('같은 이름을 덮어쓴 파일 {0}개는 되돌리지 않습니다.', [overwritten]), ]), action: (tr('되돌리기'), () => unawaited(_undoMove(context, ctl.panes[load.from], back)))); } /// 82: 되돌리기 - 원래 폴더마다 지금의 이동으로 (같은 이름이 생겼으면 지금의 같은 이름 창) Future _undoMove(BuildContext context, ExplorerPane pane, Map> back) async { for (final MapEntry(key: dir, value: items) in back.entries) { final there = [for (final x in items) if (await vExists(x)) x]; if (there.isEmpty || !context.mounted) continue; try { if (!await vExists(dir)) await vMkdirs(dir); } catch (_) {} if (!context.mounted) return; await transfers.run(context, pane, there, dir, move: true); } } }