JJ_MKVMaker/lib/ui/explorer/drop_flow.dart

123 lines
5.8 KiB
Dart

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<String> dragPaths(ExplorerPane pane, FileEntry e) =>
sel.isMarked(pane, e.path) && !pane.sel.isEmpty ? outermostPaths(pane.sel.marked) : [e.path];
/// 82: 놓음 - 설정 "끌어 놓기 기본" (묻기 · 복사 · 이동) 뒤 지금의 복사 · 이동 (확인 창 · 같은 이름 · 진행 패널).
/// 이동이 끝나면 아래 알림에 [되돌리기] (10초)
Future<void> 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.explorer.dropAction;
final move = mode == 'ask' ? await askDropAction(context, load.paths, dest) : mode == 'move';
if (move == null || !context.mounted) return;
// 되돌리기에 쓸 것: 대상에 이미 있던 이름 (합친 폴더는 통째로 되돌리지 않게)
final existed = <String>{};
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<String> existed) {
// (원래 폴더 → 지금 경로들)
final back = <String, List<String>>{};
// 되돌리지 않는 것: 합친 폴더 (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<void> _undoMove(BuildContext context, ExplorerPane pane, Map<String, List<String>> 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);
}
}
}