123 lines
5.8 KiB
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.explorerDropAction;
|
|
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);
|
|
}
|
|
}
|
|
}
|