M2-b: Windows 휴지통 주입 (전역 recycleBinForTest 없앰)
- recycle_bin: RecycleBinBackend 약속 + WindowsRecycleBin (이 기기의 휴지통). 전역 recycleBinForTest · recycle() · restoreRecycled() 없앰 - delete_ops: deletePaths(bin:) · undoRecycle(bin:) - 처음 값 WindowsRecycleBin - ExplorerDevice.recycleBin → ExplorerFlows · ExplorerDeleteFlow(recycleBin:) - 시험: useTestRecycleBin(device) 가 시험 기기에 시험 폴더 안의 휴지통을 넣음. 영구 삭제만 하는 약속 시험 4개는 휴지통으로 보내면 실패하는 것을 넘김 (사용자 휴지통에 닿지 않게). 넘겨받은 휴지통 약속 시험 1개 더함 - 시험용 휴지통: 자기 폴더가 없어졌으면 다시 만들지 않고 그대로 알림 (진단) - 전부터 있던 실패 2개 고침 (E6 부터 Windows 처음 모양이 Total Commander 라 이름 · 확장자가 다른 칸): windows_check_shot_test · windows_b3_shot_test 가 X-plore 모양을 직접 고름 확인: analyze 0 · 단위 1472 ×2 · 통합 6/6 ×2 · base10 214장 0px · windows_check_shot ×2 · windows_b3_shot ×2 (첫 확인의 통합 1회에서 412 지우기가 '경로를 찾을 수 없음' 으로 11장 벗어남 - 다시 4회 모두 0px, 원인 미확인) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
beb8107d81
commit
512c9d8b14
|
|
@ -38,7 +38,7 @@ void main() {
|
||||||
t.view.devicePixelRatio = 1;
|
t.view.devicePixelRatio = 1;
|
||||||
addTearDown(t.view.reset);
|
addTearDown(t.view.reset);
|
||||||
await t.runAsync(() => i18n.apply('ko', save: false));
|
await t.runAsync(() => i18n.apply('ko', save: false));
|
||||||
useTestRecycleBin();
|
useTestRecycleBin(device);
|
||||||
// 캡처 비교가 흔들리지 않게 시험 폴더 이름 · 파일 시각을 고정 (관리자 지시 10/22) - 끝나면 지움
|
// 캡처 비교가 흔들리지 않게 시험 폴더 이름 · 파일 시각을 고정 (관리자 지시 10/22) - 끝나면 지움
|
||||||
final tmp = Directory(p.join(Directory.systemTemp.path, 'jj_usechk_fixed_$w'));
|
final tmp = Directory(p.join(Directory.systemTemp.path, 'jj_usechk_fixed_$w'));
|
||||||
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,9 @@ void main() {
|
||||||
..writeAsStringSync('old');
|
..writeAsStringSync('old');
|
||||||
c = AppController(PlatformServices.create());
|
c = AppController(PlatformServices.create());
|
||||||
c.settings.explorer.paths = [left, right];
|
c.settings.explorer.paths = [left, right];
|
||||||
|
c.settings.explorer.style = 'xplore'; // 이 캡처는 X-plore 모양 (E6 부터 Windows 처음 값은 Total Commander - 이름 · 확장자가 다른 칸)
|
||||||
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
||||||
useTestRecycleBin();
|
useTestRecycleBin(device);
|
||||||
});
|
});
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)');
|
expect(await recycleLeftovers(tmp.path), 0, reason: '사용자 휴지통에 시험 흔적 없음 (하위 폴더 포함)');
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import 'package:jj_mkvmaker/ui/theme.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import '../test/support/fake_recycle_bin.dart';
|
import '../test/support/fake_recycle_bin.dart';
|
||||||
|
import '../test/support/test_explorer_device.dart';
|
||||||
import 'support/temp_dir.dart';
|
import 'support/temp_dir.dart';
|
||||||
|
|
||||||
/// 기기 감독 (Windows) 확인용: 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다.
|
/// 기기 감독 (Windows) 확인용: 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다.
|
||||||
|
|
@ -82,7 +83,8 @@ void main() {
|
||||||
final tmp = tempDir('jj_shot_del_');
|
final tmp = tempDir('jj_shot_del_');
|
||||||
final started = DateTime.now();
|
final started = DateTime.now();
|
||||||
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 (끝에 시험 항목만 지움)
|
// 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 (끝에 시험 항목만 지움)
|
||||||
useTestRecycleBin();
|
final device = TestExplorerDevice();
|
||||||
|
useTestRecycleBin(device);
|
||||||
try {
|
try {
|
||||||
for (final (tag, size) in sizes) {
|
for (final (tag, size) in sizes) {
|
||||||
final dir = Directory(p.join(tmp.path, tag))..createSync();
|
final dir = Directory(p.join(tmp.path, tag))..createSync();
|
||||||
|
|
@ -90,10 +92,11 @@ void main() {
|
||||||
File(p.join(dir.path, 'keep_$tag.txt')).writeAsStringSync('x');
|
File(p.join(dir.path, 'keep_$tag.txt')).writeAsStringSync('x');
|
||||||
final c = controller();
|
final c = controller();
|
||||||
c.settings
|
c.settings
|
||||||
|
..explorer.style = 'xplore' // 이 캡처는 X-plore 모양 (E6 부터 Windows 처음 값은 Total Commander - 이름 · 확장자가 다른 칸)
|
||||||
..explorer.layout = 'single'
|
..explorer.layout = 'single'
|
||||||
..explorer.click = 'select'
|
..explorer.click = 'select'
|
||||||
..explorer.paths = [dir.path, dir.path];
|
..explorer.paths = [dir.path, dir.path];
|
||||||
await show(t, size, ExplorerPage(c: c));
|
await show(t, size, ExplorerPage(c: c, device: device));
|
||||||
await tapText(t, 'recycle_me_$tag.txt');
|
await tapText(t, 'recycle_me_$tag.txt');
|
||||||
await t.sendKeyEvent(LogicalKeyboardKey.delete);
|
await t.sendKeyEvent(LogicalKeyboardKey.delete);
|
||||||
await settle(t);
|
await settle(t);
|
||||||
|
|
@ -119,10 +122,11 @@ void main() {
|
||||||
for (final (tag, size) in sizes) {
|
for (final (tag, size) in sizes) {
|
||||||
final c = controller();
|
final c = controller();
|
||||||
c.settings
|
c.settings
|
||||||
|
..explorer.style = 'xplore' // 이 캡처는 X-plore 모양 (E6 부터 Windows 처음 값은 Total Commander - 이름 · 확장자가 다른 칸)
|
||||||
..explorer.layout = 'single'
|
..explorer.layout = 'single'
|
||||||
..explorer.click = 'select'
|
..explorer.click = 'select'
|
||||||
..explorer.paths = [share.path, share.path];
|
..explorer.paths = [share.path, share.path];
|
||||||
await show(t, size, ExplorerPage(c: c));
|
await show(t, size, ExplorerPage(c: c, device: device));
|
||||||
await settle(t, 20);
|
await settle(t, 20);
|
||||||
await tapText(t, 'on_share.txt');
|
await tapText(t, 'on_share.txt');
|
||||||
await t.sendKeyEvent(LogicalKeyboardKey.delete);
|
await t.sendKeyEvent(LogicalKeyboardKey.delete);
|
||||||
|
|
@ -142,10 +146,11 @@ void main() {
|
||||||
for (final (tag, size) in sizes) {
|
for (final (tag, size) in sizes) {
|
||||||
final c = controller();
|
final c = controller();
|
||||||
c.settings
|
c.settings
|
||||||
|
..explorer.style = 'xplore' // 이 캡처는 X-plore 모양 (E6 부터 Windows 처음 값은 Total Commander - 이름 · 확장자가 다른 칸)
|
||||||
..explorer.layout = 'single'
|
..explorer.layout = 'single'
|
||||||
..explorer.click = 'select'
|
..explorer.click = 'select'
|
||||||
..explorer.paths = [deep, deep];
|
..explorer.paths = [deep, deep];
|
||||||
await show(t, size, ExplorerPage(c: c));
|
await show(t, size, ExplorerPage(c: c, device: device));
|
||||||
await settle(t, 20);
|
await settle(t, 20);
|
||||||
final f = find.textContaining('long_name_file');
|
final f = find.textContaining('long_name_file');
|
||||||
if (f.evaluate().isEmpty) break;
|
if (f.evaluate().isEmpty) break;
|
||||||
|
|
@ -185,6 +190,7 @@ void main() {
|
||||||
for (final (tag, size) in sizes) {
|
for (final (tag, size) in sizes) {
|
||||||
final c = controller();
|
final c = controller();
|
||||||
c.settings
|
c.settings
|
||||||
|
..explorer.style = 'xplore' // 이 캡처는 X-plore 모양 (E6 부터 Windows 처음 값은 Total Commander - 이름 · 확장자가 다른 칸)
|
||||||
..explorer.layout = 'single'
|
..explorer.layout = 'single'
|
||||||
..explorer.paths = [tmp.path, tmp.path];
|
..explorer.paths = [tmp.path, tmp.path];
|
||||||
await show(t, size, ExplorerPage(c: c));
|
await show(t, size, ExplorerPage(c: c));
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import '../core/app_trash.dart';
|
||||||
import '../core/file_ops.dart' show FileOps;
|
import '../core/file_ops.dart' show FileOps;
|
||||||
import '../platform/windows/recycle_bin.dart';
|
import '../platform/windows/recycle_bin.dart';
|
||||||
|
|
||||||
/// 약속 (E5b): 지우기 · 되돌리기. 휴지통은 recycle_bin (Windows) · app_trash (Android) 를 부르기만 한다.
|
/// 약속 (E5b): 지우기 · 되돌리기. 휴지통은 [RecycleBinBackend] (Windows, 처음 값 = 이 기기의 휴지통) · app_trash (Android) 를 부르기만 한다.
|
||||||
/// - [deletePaths]: 입력 = 경로들 · 영구인지 · 앱 휴지통 (있으면 그리로), 출력 = [DeleteReport] (실제로 어떻게 됐는지 하나씩).
|
/// - [deletePaths]: 입력 = 경로들 · 영구인지 · 앱 휴지통 (있으면 그리로), 출력 = [DeleteReport] (실제로 어떻게 됐는지 하나씩).
|
||||||
/// 하나가 실패해도 나머지는 계속한다. 휴지통으로 못 보낸 것은 몰래 영구 삭제하지 않고 보고에 남긴다
|
/// 하나가 실패해도 나머지는 계속한다. 휴지통으로 못 보낸 것은 몰래 영구 삭제하지 않고 보고에 남긴다
|
||||||
/// (앱 휴지통 [DeleteReport.notTrashed] · 긴 경로 [DeleteReport.tooLong] - 영구 삭제는 부르는 쪽이 물은 뒤 [deleteForever]).
|
/// (앱 휴지통 [DeleteReport.notTrashed] · 긴 경로 [DeleteReport.tooLong] - 영구 삭제는 부르는 쪽이 물은 뒤 [deleteForever]).
|
||||||
|
|
@ -40,7 +40,8 @@ class DeleteReport {
|
||||||
/// 사람이 읽을 실패 이유 (OS 메시지가 있으면 그것)
|
/// 사람이 읽을 실패 이유 (OS 메시지가 있으면 그것)
|
||||||
String deleteFailReason(Object e) => e is FileSystemException ? (e.osError?.message ?? e.message) : '$e';
|
String deleteFailReason(Object e) => e is FileSystemException ? (e.osError?.message ?? e.message) : '$e';
|
||||||
|
|
||||||
Future<DeleteReport> deletePaths(List<String> paths, {required bool permanent, AppTrash? trash, DateTime? now}) async {
|
Future<DeleteReport> deletePaths(List<String> paths,
|
||||||
|
{required bool permanent, AppTrash? trash, RecycleBinBackend bin = const WindowsRecycleBin(), DateTime? now}) async {
|
||||||
final r = DeleteReport((now ?? DateTime.now()).subtract(const Duration(seconds: 2)));
|
final r = DeleteReport((now ?? DateTime.now()).subtract(const Duration(seconds: 2)));
|
||||||
for (final x in paths) {
|
for (final x in paths) {
|
||||||
if (!permanent && trash != null) {
|
if (!permanent && trash != null) {
|
||||||
|
|
@ -55,7 +56,7 @@ Future<DeleteReport> deletePaths(List<String> paths, {required bool permanent, A
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!permanent) {
|
if (!permanent) {
|
||||||
switch (recycle(x)) {
|
switch (bin.move(x)) {
|
||||||
case RecycleResult.recycled:
|
case RecycleResult.recycled:
|
||||||
r.recycled++;
|
r.recycled++;
|
||||||
r.sent.add(x);
|
r.sent.add(x);
|
||||||
|
|
@ -101,12 +102,13 @@ Future<List<String>> undoTrash(AppTrash trash, List<TrashItem> items) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Windows 휴지통에서 되살림 - 되살린 수 · 못 되살린 것 (경로, 이유)
|
/// Windows 휴지통에서 되살림 - 되살린 수 · 못 되살린 것 (경로, 이유)
|
||||||
({int back, List<(String, String)> failed}) undoRecycle(List<String> paths, DateTime since) {
|
({int back, List<(String, String)> failed}) undoRecycle(List<String> paths, DateTime since,
|
||||||
|
{RecycleBinBackend bin = const WindowsRecycleBin()}) {
|
||||||
final failed = <(String, String)>[];
|
final failed = <(String, String)>[];
|
||||||
var back = 0;
|
var back = 0;
|
||||||
for (final x in paths) {
|
for (final x in paths) {
|
||||||
try {
|
try {
|
||||||
restoreRecycled(x, since: since);
|
bin.restore(x, since: since);
|
||||||
back++;
|
back++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
failed.add((x, e is FileSystemException ? e.message : '$e'));
|
failed.add((x, e is FileSystemException ? e.message : '$e'));
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,16 @@ import 'dart:io';
|
||||||
|
|
||||||
import 'copy_center.dart' as copy show diskSpace;
|
import 'copy_center.dart' as copy show diskSpace;
|
||||||
import 'local_fs.dart';
|
import 'local_fs.dart';
|
||||||
|
import '../platform/windows/recycle_bin.dart';
|
||||||
|
|
||||||
/// 약속: 파일 탐색기가 기기에서 받는 것 - 폴더 목록 · 저장 장치 목록 · 드라이브를 열 수 있는지 ([fs], M2 LocalFs) ·
|
/// 약속: 파일 탐색기가 기기에서 받는 것 - 폴더 목록 · 저장 장치 목록 · 드라이브를 열 수 있는지 ([fs], M2 LocalFs) ·
|
||||||
/// 저장 장치의 남은 공간 · 전체 (E6 Total Commander 모양, 못 재면 null) · Android 에서만 하는 것 (경로 입력의 SMB 안내 · SMB 서버 메뉴, 앱 휴지통, .nomedia 메뉴).
|
/// 저장 장치의 남은 공간 · 전체 (E6 Total Commander 모양, 못 재면 null) · Android 에서만 하는 것 (경로 입력의 SMB 안내 · SMB 서버 메뉴, 앱 휴지통, .nomedia 메뉴).
|
||||||
/// 앱은 [RealExplorerDevice] (이 기기 그대로) 를 쓰고, 시험은 시험 폴더를 저장 장치로 주는 구현을 넘긴다
|
/// 앱은 [RealExplorerDevice] (이 기기 그대로) 를 쓰고, 시험은 시험 폴더를 저장 장치로 주는 구현을 넘긴다
|
||||||
abstract interface class ExplorerDevice {
|
abstract interface class ExplorerDevice {
|
||||||
LocalFs get fs;
|
LocalFs get fs;
|
||||||
|
|
||||||
|
/// Windows 휴지통 (M2)
|
||||||
|
RecycleBinBackend get recycleBin;
|
||||||
Future<(int, int)?> diskSpace(String path);
|
Future<(int, int)?> diskSpace(String path);
|
||||||
bool get androidPaths;
|
bool get androidPaths;
|
||||||
bool get androidTrash;
|
bool get androidTrash;
|
||||||
|
|
@ -21,6 +25,8 @@ class RealExplorerDevice implements ExplorerDevice {
|
||||||
@override
|
@override
|
||||||
LocalFs get fs => const RealLocalFs();
|
LocalFs get fs => const RealLocalFs();
|
||||||
@override
|
@override
|
||||||
|
RecycleBinBackend get recycleBin => const WindowsRecycleBin();
|
||||||
|
@override
|
||||||
Future<(int, int)?> diskSpace(String path) => copy.diskSpace(path);
|
Future<(int, int)?> diskSpace(String path) => copy.diskSpace(path);
|
||||||
@override
|
@override
|
||||||
bool get androidPaths => Platform.isAndroid;
|
bool get androidPaths => Platform.isAndroid;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'dart:ffi';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import '../../l10n/tr.dart';
|
import '../../l10n/tr.dart';
|
||||||
|
|
@ -19,24 +18,22 @@ enum RecycleResult {
|
||||||
cancelled,
|
cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 시험용: 실제 휴지통 대신 쓸 것. 기본 전체 시험은 가짜로 흐름만 본다 - 시험이 도중에 끊기면 사용자 휴지통에
|
/// 약속 (M2 휴지통): [move] 경로 하나를 휴지통으로 → [RecycleResult] (보냄 · Windows 가 묻고 영구 삭제 · 취소).
|
||||||
/// 흔적이 남기 때문 (실제 휴지통 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만, test/support/fake_recycle_bin.dart).
|
/// 오류: 긴 경로 [LongPathException] · 그 밖은 [FileSystemException]. [restore] 원래 자리로 (같은 이름이 있으면 덮지 않고 오류).
|
||||||
|
/// 앱은 [WindowsRecycleBin] (이 기기의 휴지통) 을 ExplorerDevice 로 넘겨받는다. 시험은 생성자 · 기기로 다른 것을 넘긴다
|
||||||
|
/// (기본 전체 시험은 시험 폴더 안의 휴지통 - 도중에 끊겨도 사용자 휴지통에 흔적이 남지 않게, test/support/fake_recycle_bin.dart)
|
||||||
abstract class RecycleBinBackend {
|
abstract class RecycleBinBackend {
|
||||||
RecycleResult move(String path);
|
RecycleResult move(String path);
|
||||||
void restore(String original, {required DateTime since});
|
void restore(String original, {required DateTime since});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// null 이면 실제 휴지통 (앱은 늘 null)
|
/// 이 기기의 Windows 휴지통
|
||||||
@visibleForTesting
|
class WindowsRecycleBin implements RecycleBinBackend {
|
||||||
RecycleBinBackend? recycleBinForTest;
|
const WindowsRecycleBin();
|
||||||
|
@override
|
||||||
/// 휴지통으로 보내기 (앱의 화면은 이것을 쓴다 - 시험에서 가짜로 바꿀 수 있게)
|
RecycleResult move(String path) => moveToRecycleBin(path);
|
||||||
RecycleResult recycle(String path) => recycleBinForTest?.move(path) ?? moveToRecycleBin(path);
|
@override
|
||||||
|
void restore(String original, {required DateTime since}) => restoreFromRecycleBin(original, since: since);
|
||||||
/// 휴지통에서 원래 자리로 (앱의 화면은 이것을 쓴다)
|
|
||||||
void restoreRecycled(String original, {required DateTime since}) {
|
|
||||||
final t = recycleBinForTest;
|
|
||||||
t == null ? restoreFromRecycleBin(original, since: since) : t.restore(original, since: since);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 이 경로가 있는 곳에 휴지통이 있는지 (94). Windows 의 휴지통은 고정 디스크에만 있다:
|
/// 이 경로가 있는 곳에 휴지통이 있는지 (94). Windows 의 휴지통은 고정 디스크에만 있다:
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import 'write_guard.dart';
|
||||||
/// 지우기 규칙은 delete_ops (E5b). 여러 개면 실패해도 나머지를 계속하고, 못 지운 것 · 휴지통에 못 넣은 것은 창으로 묻거나 알린다.
|
/// 지우기 규칙은 delete_ops (E5b). 여러 개면 실패해도 나머지를 계속하고, 못 지운 것 · 휴지통에 못 넣은 것은 창으로 묻거나 알린다.
|
||||||
/// 휴지통으로 보낸 것은 알림의 [되돌리기]. 끝나면 표시 · 펼침 · 지금 폴더를 정리하고 다시 읽는다. 읽기 전용인 곳은 [ExplorerGuard] 가 막는다
|
/// 휴지통으로 보낸 것은 알림의 [되돌리기]. 끝나면 표시 · 펼침 · 지금 폴더를 정리하고 다시 읽는다. 읽기 전용인 곳은 [ExplorerGuard] 가 막는다
|
||||||
class ExplorerDeleteFlow {
|
class ExplorerDeleteFlow {
|
||||||
ExplorerDeleteFlow(this.ctl, this.sel, this.guard, {required this.androidTrash});
|
ExplorerDeleteFlow(this.ctl, this.sel, this.guard, {required this.androidTrash, required this.recycleBin});
|
||||||
|
|
||||||
final ExplorerController ctl;
|
final ExplorerController ctl;
|
||||||
final ExplorerSelect sel;
|
final ExplorerSelect sel;
|
||||||
|
|
@ -33,6 +33,9 @@ class ExplorerDeleteFlow {
|
||||||
/// 앱 휴지통을 쓰는 기기인지 (Android)
|
/// 앱 휴지통을 쓰는 기기인지 (Android)
|
||||||
final bool Function() androidTrash;
|
final bool Function() androidTrash;
|
||||||
|
|
||||||
|
/// Windows 휴지통 (기기에서 - ExplorerDevice.recycleBin)
|
||||||
|
final RecycleBinBackend Function() recycleBin;
|
||||||
|
|
||||||
/// Shift+Delete 로 불렀는지 (휴지통을 거치지 않고 지우기를 미리 골라 둔다 - 65)
|
/// Shift+Delete 로 불렀는지 (휴지통을 거치지 않고 지우기를 미리 골라 둔다 - 65)
|
||||||
bool shiftDelete = false;
|
bool shiftDelete = false;
|
||||||
|
|
||||||
|
|
@ -125,7 +128,7 @@ class ExplorerDeleteFlow {
|
||||||
final focusedBefore = pane.focused;
|
final focusedBefore = pane.focused;
|
||||||
final shownBefore = focusedBefore == null ? const <String>[] : [for (final e in sel.shown(pane, vDirname(focusedBefore))) e.path];
|
final shownBefore = focusedBefore == null ? const <String>[] : [for (final e in sel.shown(pane, vDirname(focusedBefore))) e.path];
|
||||||
// 하나씩: 실패해도 나머지는 계속. 실제로 어떻게 됐는지 세어 그대로 알린다 (94) - 지우기 규칙은 delete_ops (E5b)
|
// 하나씩: 실패해도 나머지는 계속. 실제로 어떻게 됐는지 세어 그대로 알린다 (94) - 지우기 규칙은 delete_ops (E5b)
|
||||||
final report = await deletePaths(paths, permanent: permanent, trash: !permanent && canTrash ? trash : null);
|
final report = await deletePaths(paths, permanent: permanent, trash: !permanent && canTrash ? trash : null, bin: recycleBin());
|
||||||
final failed = report.failed, tooLong = report.tooLong, sent = report.sent, trashed = report.trashed;
|
final failed = report.failed, tooLong = report.tooLong, sent = report.sent, trashed = report.trashed;
|
||||||
final notTrashed = report.notTrashed, undoable = report.undoable, since = report.since;
|
final notTrashed = report.notTrashed, undoable = report.undoable, since = report.since;
|
||||||
final recycled = report.recycled, nuked = report.nuked, kept = report.kept;
|
final recycled = report.recycled, nuked = report.nuked, kept = report.kept;
|
||||||
|
|
@ -274,7 +277,7 @@ class ExplorerDeleteFlow {
|
||||||
|
|
||||||
/// 148: 방금 휴지통으로 보낸 것을 원래 자리로 (원래 자리에 같은 이름이 생겼으면 그것은 두고 알림)
|
/// 148: 방금 휴지통으로 보낸 것을 원래 자리로 (원래 자리에 같은 이름이 생겼으면 그것은 두고 알림)
|
||||||
Future<void> _undoRecycle(List<String> paths, DateTime since) async {
|
Future<void> _undoRecycle(List<String> paths, DateTime since) async {
|
||||||
final (:back, failed: why) = undoRecycle(paths, since);
|
final (:back, failed: why) = undoRecycle(paths, since, bin: recycleBin());
|
||||||
final failed = [for (final (x, e) in why) '${vBasename(x)}: $e'];
|
final failed = [for (final (x, e) in why) '${vBasename(x)}: $e'];
|
||||||
await ctl.refreshAll(paths.map(p.dirname));
|
await ctl.refreshAll(paths.map(p.dirname));
|
||||||
for (final x in ctl.panes) {
|
for (final x in ctl.panes) {
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,18 @@ import 'tools_flow.dart';
|
||||||
import 'transfer_flow.dart';
|
import 'transfer_flow.dart';
|
||||||
import 'view_dialogs.dart';
|
import 'view_dialogs.dart';
|
||||||
import 'write_guard.dart';
|
import 'write_guard.dart';
|
||||||
|
import '../../platform/windows/recycle_bin.dart' show RecycleBinBackend;
|
||||||
|
|
||||||
/// 약속 (E5d-6): 탐색기 하나의 흐름 모듈 묶음 - 버튼 · 줄 메뉴 · 단축키 · 끌어 놓기가 같은 것을 쓴다 (작업 목록 · 담아 둔 것이 하나).
|
/// 약속 (E5d-6): 탐색기 하나의 흐름 모듈 묶음 - 버튼 · 줄 메뉴 · 단축키 · 끌어 놓기가 같은 것을 쓴다 (작업 목록 · 담아 둔 것이 하나).
|
||||||
/// 처음 쓸 때 만든다. 화면 배치에서 오는 것 (다른 창의 폴더 · 창이 둘인지 · 앱 휴지통을 쓰는 기기인지) 은 함수로 받는다
|
/// 처음 쓸 때 만든다. 화면 배치에서 오는 것 (다른 창의 폴더 · 창이 둘인지 · 앱 휴지통을 쓰는 기기인지) 은 함수로 받는다
|
||||||
class ExplorerFlows {
|
class ExplorerFlows {
|
||||||
ExplorerFlows(this.ctl, this.sel,
|
ExplorerFlows(this.ctl, this.sel,
|
||||||
{required this.androidTrash, required this.otherDirOf, required this.otherDir, required this.twoPanes});
|
{required this.androidTrash, required this.recycleBin, required this.otherDirOf, required this.otherDir, required this.twoPanes});
|
||||||
|
|
||||||
final ExplorerController ctl;
|
final ExplorerController ctl;
|
||||||
final ExplorerSelect sel;
|
final ExplorerSelect sel;
|
||||||
final bool Function() androidTrash;
|
final bool Function() androidTrash;
|
||||||
|
final RecycleBinBackend Function() recycleBin;
|
||||||
|
|
||||||
/// [pane] 옆 창의 지금 폴더 (두 창일 때만, 압축 풀 곳)
|
/// [pane] 옆 창의 지금 폴더 (두 창일 때만, 압축 풀 곳)
|
||||||
final String? Function(ExplorerPane pane) otherDirOf;
|
final String? Function(ExplorerPane pane) otherDirOf;
|
||||||
|
|
@ -34,7 +36,7 @@ class ExplorerFlows {
|
||||||
late final jobs = ExplorerJobs(ctl);
|
late final jobs = ExplorerJobs(ctl);
|
||||||
late final transfers = ExplorerTransfers(ctl, guard, jobs);
|
late final transfers = ExplorerTransfers(ctl, guard, jobs);
|
||||||
late final rsync = ExplorerRsyncFlow(ctl, jobs, transfers);
|
late final rsync = ExplorerRsyncFlow(ctl, jobs, transfers);
|
||||||
late final deletes = ExplorerDeleteFlow(ctl, sel, guard, androidTrash: androidTrash);
|
late final deletes = ExplorerDeleteFlow(ctl, sel, guard, androidTrash: androidTrash, recycleBin: recycleBin);
|
||||||
late final opener = ExplorerOpener(ctl, sel, guard, otherDirOf: otherDirOf);
|
late final opener = ExplorerOpener(ctl, sel, guard, otherDirOf: otherDirOf);
|
||||||
late final renames = ExplorerRenames(ctl, guard);
|
late final renames = ExplorerRenames(ctl, guard);
|
||||||
late final tools = ExplorerTools(ctl, deletes, transfers);
|
late final tools = ExplorerTools(ctl, deletes, transfers);
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,7 @@ class _ExplorerPageState extends State<ExplorerPage> implements ExplorerHost, Ex
|
||||||
/// 흐름 모듈 묶음 - 막기 · 진행 작업 · 복사 · 이동 · rsync · 지우기 · 열기 · 이름 · 도구 · 보기 창 · 찾기 (E5d-5 · E5d-6)
|
/// 흐름 모듈 묶음 - 막기 · 진행 작업 · 복사 · 이동 · rsync · 지우기 · 열기 · 이름 · 도구 · 보기 창 · 찾기 (E5d-5 · E5d-6)
|
||||||
late final ExplorerFlows _flows = ExplorerFlows(_ctl, _sel,
|
late final ExplorerFlows _flows = ExplorerFlows(_ctl, _sel,
|
||||||
androidTrash: () => widget.device.androidTrash,
|
androidTrash: () => widget.device.androidTrash,
|
||||||
|
recycleBin: () => widget.device.recycleBin,
|
||||||
otherDirOf: (pane) => _layout.dual ? _ctl.panes[1 - _ctl.panes.indexOf(pane)].current : null,
|
otherDirOf: (pane) => _layout.dual ? _ctl.panes[1 - _ctl.panes.indexOf(pane)].current : null,
|
||||||
otherDir: () => _layout.other?.current,
|
otherDir: () => _layout.other?.current,
|
||||||
twoPanes: () => _layout.dual || _layout.split)
|
twoPanes: () => _layout.dual || _layout.split)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import 'package:jj_mkvmaker/app/delete_ops.dart';
|
||||||
import 'package:jj_mkvmaker/core/app_trash.dart';
|
import 'package:jj_mkvmaker/core/app_trash.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import 'support/fake_recycle_bin.dart' show realRecycleSkip;
|
import 'support/fake_recycle_bin.dart' show FakeRecycleBin, realRecycleSkip;
|
||||||
|
|
||||||
/// E5b 약속 시험: lib/app/delete_ops.dart 맨 위 약속. 시험 폴더의 실제 파일 · 실제 앱 휴지통 (가짜 없음).
|
/// E5b 약속 시험: lib/app/delete_ops.dart 맨 위 약속. 시험 폴더의 실제 파일 · 실제 앱 휴지통 (가짜 없음).
|
||||||
/// Windows 휴지통은 실제 사용자 휴지통이라 JJ_TEST_REAL_RECYCLE=1 일 때만 (끝나면 되살려 흔적 없음).
|
/// Windows 휴지통은 실제 사용자 휴지통이라 JJ_TEST_REAL_RECYCLE=1 일 때만 (끝나면 되살려 흔적 없음).
|
||||||
|
|
@ -62,6 +62,17 @@ void main() {
|
||||||
expect(deleteFailReason(const FileSystemException('x', 'y', OSError('잠김', 32))), '잠김');
|
expect(deleteFailReason(const FileSystemException('x', 'y', OSError('잠김', 32))), '잠김');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('M2: 넘겨받은 휴지통으로 보내고 되돌림 (시험 폴더 안의 휴지통)', () async {
|
||||||
|
final bin = FakeRecycleBin();
|
||||||
|
addTearDown(bin.dispose);
|
||||||
|
final r = await deletePaths([a, b], permanent: false, bin: bin);
|
||||||
|
expect([r.sent, r.recycled, bin.items.length], [[a, b], 2, 2]);
|
||||||
|
expect(File(a).existsSync(), isFalse);
|
||||||
|
final u = undoRecycle(r.sent, r.since, bin: bin);
|
||||||
|
expect([u.back, u.failed], [2, isEmpty]);
|
||||||
|
expect([File(a).existsSync(), File(b).existsSync()], [true, true]);
|
||||||
|
});
|
||||||
|
|
||||||
test('Windows 휴지통: 보내고 되돌림 (실제 휴지통)', () async {
|
test('Windows 휴지통: 보내고 되돌림 (실제 휴지통)', () async {
|
||||||
final r = await deletePaths([a, b], permanent: false);
|
final r = await deletePaths([a, b], permanent: false);
|
||||||
expect(r.sent, [a, b]);
|
expect(r.sent, [a, b]);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import 'package:jj_mkvmaker/ui/explorer/marks_bar.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer/toolbar.dart';
|
import 'package:jj_mkvmaker/ui/explorer/toolbar.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer/transfer_panel.dart';
|
import 'package:jj_mkvmaker/ui/explorer/transfer_panel.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show RecycleBinBackend, RecycleResult;
|
||||||
|
|
||||||
/// E5d-4 약속 시험: explorer_buttons · toolbar · marks_bar · transfer_panel · job_view 맨 위 약속
|
/// E5d-4 약속 시험: explorer_buttons · toolbar · marks_bar · transfer_panel · job_view 맨 위 약속
|
||||||
/// (실제 시험 폴더 · 실제 화면 · 실제 복사 작업. 화면의 일 [ExplorerCommands] 은 이 시험 화면이 받아 적는다)
|
/// (실제 시험 폴더 · 실제 화면 · 실제 복사 작업. 화면의 일 [ExplorerCommands] 은 이 시험 화면이 받아 적는다)
|
||||||
|
|
@ -48,7 +49,7 @@ void main() {
|
||||||
ctl.dispose();
|
ctl.dispose();
|
||||||
});
|
});
|
||||||
await t.runAsync(() => ctl.goTo(ctl.panes[0], show));
|
await t.runAsync(() => ctl.goTo(ctl.panes[0], show));
|
||||||
final flows = ExplorerFlows(ctl, sel, androidTrash: () => false, otherDirOf: (_) => null, otherDir: () => null, twoPanes: () => false);
|
final flows = ExplorerFlows(ctl, sel, androidTrash: () => false, recycleBin: _noRecycle, otherDirOf: (_) => null, otherDir: () => null, twoPanes: () => false);
|
||||||
addTearDown(flows.dispose);
|
addTearDown(flows.dispose);
|
||||||
final b = ExplorerButtons(ctl, sel, host, flows);
|
final b = ExplorerButtons(ctl, sel, host, flows);
|
||||||
return (b, ctl, sel, host);
|
return (b, ctl, sel, host);
|
||||||
|
|
@ -191,3 +192,14 @@ class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(body: child?.call() ?? const SizedBox());
|
Widget build(BuildContext context) => Scaffold(body: child?.call() ?? const SizedBox());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 이 시험은 휴지통으로 보내지 않는다 (영구 삭제만) - 혹시 보내면 사용자 휴지통에 닿지 않고 실패한다
|
||||||
|
RecycleBinBackend _noRecycle() => const _NoRecycle();
|
||||||
|
|
||||||
|
class _NoRecycle implements RecycleBinBackend {
|
||||||
|
const _NoRecycle();
|
||||||
|
@override
|
||||||
|
RecycleResult move(String path) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
@override
|
||||||
|
void restore(String original, {required DateTime since}) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ void main() {
|
||||||
final root = p.rootPrefix(tmp.path);
|
final root = p.rootPrefix(tmp.path);
|
||||||
device.volumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
|
device.volumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))];
|
||||||
// 휴지통: 기본은 가짜 (흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
// 휴지통: 기본은 가짜 (흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통
|
||||||
bin = useTestRecycleBin();
|
bin = useTestRecycleBin(device);
|
||||||
});
|
});
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
// 사용자 휴지통에 이 시험 폴더 아래의 흔적이 없는지 스스로 확인 (가짜일 때도 - 실제 휴지통에 닿지 않았는지)
|
// 사용자 휴지통에 이 시험 폴더 아래의 흔적이 없는지 스스로 확인 (가짜일 때도 - 실제 휴지통에 닿지 않았는지)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import 'package:jj_mkvmaker/ui/explorer/transfer_flow.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer/write_guard.dart';
|
import 'package:jj_mkvmaker/ui/explorer/write_guard.dart';
|
||||||
import 'package:jj_mkvmaker/ui/text_viewer.dart';
|
import 'package:jj_mkvmaker/ui/text_viewer.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show RecycleBinBackend, RecycleResult;
|
||||||
|
|
||||||
/// E5d-5 약속 시험: write_guard · explorer_jobs · transfer_flow · delete_flow · open_flow · item_menu · item_actions 맨 위 약속
|
/// E5d-5 약속 시험: write_guard · explorer_jobs · transfer_flow · delete_flow · open_flow · item_menu · item_actions 맨 위 약속
|
||||||
/// (실제 시험 폴더 · 실제 화면 · 실제 복사 · 실제 지우기. 다른 앱으로 여는 길은 이 PC 의 앱을 열므로 보지 않는다)
|
/// (실제 시험 폴더 · 실제 화면 · 실제 복사 · 실제 지우기. 다른 앱으로 여는 길은 이 PC 의 앱을 열므로 보지 않는다)
|
||||||
|
|
@ -58,7 +59,7 @@ void main() {
|
||||||
final host = key.currentState!;
|
final host = key.currentState!;
|
||||||
final ctl = ExplorerController(c, rsync: false, host: host)..local = [(root, 'T:')];
|
final ctl = ExplorerController(c, rsync: false, host: host)..local = [(root, 'T:')];
|
||||||
final sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
|
final sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
|
||||||
final flows = ExplorerFlows(ctl, sel, androidTrash: () => false, otherDirOf: (_) => null, otherDir: () => null, twoPanes: () => false);
|
final flows = ExplorerFlows(ctl, sel, androidTrash: () => false, recycleBin: _noRecycle, otherDirOf: (_) => null, otherDir: () => null, twoPanes: () => false);
|
||||||
addTearDown(() {
|
addTearDown(() {
|
||||||
flows.dispose();
|
flows.dispose();
|
||||||
sel.dispose();
|
sel.dispose();
|
||||||
|
|
@ -197,3 +198,14 @@ class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => const Scaffold(body: SizedBox());
|
Widget build(BuildContext context) => const Scaffold(body: SizedBox());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 이 시험은 휴지통으로 보내지 않는다 (영구 삭제만) - 혹시 보내면 사용자 휴지통에 닿지 않고 실패한다
|
||||||
|
RecycleBinBackend _noRecycle() => const _NoRecycle();
|
||||||
|
|
||||||
|
class _NoRecycle implements RecycleBinBackend {
|
||||||
|
const _NoRecycle();
|
||||||
|
@override
|
||||||
|
RecycleResult move(String path) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
@override
|
||||||
|
void restore(String original, {required DateTime since}) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import 'package:jj_mkvmaker/ui/explorer_drag.dart';
|
||||||
import 'package:jj_mkvmaker/ui/storage_page.dart';
|
import 'package:jj_mkvmaker/ui/storage_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'support/fake_recycle_bin.dart';
|
import 'support/fake_recycle_bin.dart';
|
||||||
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show recycleBinForTest;
|
|
||||||
import 'support/explorer_harness.dart';
|
import 'support/explorer_harness.dart';
|
||||||
|
|
||||||
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 단축키 (TC · Windows 탐색기) · 창 사이 끌어 놓기 · 진행 패널 · 빠른 보기 · 저장 공간 분석 · 열 머리 정렬
|
/// E5e: 예전 explorer_test.dart 에서 나눈 탐색기 화면 시험 - 단축키 (TC · Windows 탐색기) · 창 사이 끌어 놓기 · 진행 패널 · 빠른 보기 · 저장 공간 분석 · 열 머리 정렬
|
||||||
|
|
@ -47,7 +46,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('83 (28 지적): Total Commander 모양 F8 = 지우기 - 확인 창 [취소] 면 그대로, [휴지통으로] 면 휴지통으로', (tester) async {
|
testWidgets('83 (28 지적): Total Commander 모양 F8 = 지우기 - 확인 창 [취소] 면 그대로, [휴지통으로] 면 휴지통으로', (tester) async {
|
||||||
final bin = recycleBinForTest;
|
final bin = device.bin;
|
||||||
if (bin is! FakeRecycleBin) return markTestSkipped('실제 휴지통 (JJ_TEST_REAL_RECYCLE=1) 에서는 건너뜀');
|
if (bin is! FakeRecycleBin) return markTestSkipped('실제 휴지통 (JJ_TEST_REAL_RECYCLE=1) 에서는 건너뜀');
|
||||||
if (!Platform.isWindows) return markTestSkipped('휴지통은 Windows 만 (Android 는 앱 휴지통 시험)');
|
if (!Platform.isWindows) return markTestSkipped('휴지통은 Windows 만 (Android 는 앱 휴지통 시험)');
|
||||||
final victim = File(p.join(left, 'tcdel.txt'))..writeAsStringSync('x');
|
final victim = File(p.join(left, 'tcdel.txt'))..writeAsStringSync('x');
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import 'package:jj_mkvmaker/ui/explorer_drag.dart' show DragLoad;
|
||||||
import 'package:jj_mkvmaker/ui/explorer_search.dart' show SearchResultsPage;
|
import 'package:jj_mkvmaker/ui/explorer_search.dart' show SearchResultsPage;
|
||||||
import 'package:jj_mkvmaker/ui/storage_page.dart' show StorageAnalysisPage;
|
import 'package:jj_mkvmaker/ui/storage_page.dart' show StorageAnalysisPage;
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show RecycleBinBackend, RecycleResult;
|
||||||
|
|
||||||
/// E5e 약속 시험: explorer_body · search_flow · tools_flow · rsync_flow 맨 위 약속
|
/// E5e 약속 시험: explorer_body · search_flow · tools_flow · rsync_flow 맨 위 약속
|
||||||
/// (실제 시험 폴더 · 실제 화면 · 실제 설정 (저장하지 않음). rsync 실행은 rsync 실행 파일이 있어야 해 확인 창까지)
|
/// (실제 시험 폴더 · 실제 화면 · 실제 설정 (저장하지 않음). rsync 실행은 rsync 실행 파일이 있어야 해 확인 창까지)
|
||||||
|
|
@ -155,7 +156,7 @@ class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands,
|
||||||
sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
|
sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
|
||||||
layout = ExplorerLayout(ctl, context: () => context);
|
layout = ExplorerLayout(ctl, context: () => context);
|
||||||
flows = ExplorerFlows(ctl, sel,
|
flows = ExplorerFlows(ctl, sel,
|
||||||
androidTrash: () => false, otherDirOf: (_) => null, otherDir: () => layout.other?.current, twoPanes: () => layout.dual || layout.split);
|
androidTrash: () => false, recycleBin: _noRecycle, otherDirOf: (_) => null, otherDir: () => layout.other?.current, twoPanes: () => layout.dual || layout.split);
|
||||||
buttons = ExplorerButtons(ctl, sel, this, flows);
|
buttons = ExplorerButtons(ctl, sel, this, flows);
|
||||||
_made = true;
|
_made = true;
|
||||||
}
|
}
|
||||||
|
|
@ -225,3 +226,14 @@ class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands,
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) => Scaffold(
|
||||||
body: !_made ? const SizedBox() : ExplorerPanesBody(ctl: ctl, sel: sel, layout: layout, parts: this, buttons: buttons, flows: flows));
|
body: !_made ? const SizedBox() : ExplorerPanesBody(ctl: ctl, sel: sel, layout: layout, parts: this, buttons: buttons, flows: flows));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 이 시험은 휴지통으로 보내지 않는다 (영구 삭제만) - 혹시 보내면 사용자 휴지통에 닿지 않고 실패한다
|
||||||
|
RecycleBinBackend _noRecycle() => const _NoRecycle();
|
||||||
|
|
||||||
|
class _NoRecycle implements RecycleBinBackend {
|
||||||
|
const _NoRecycle();
|
||||||
|
@override
|
||||||
|
RecycleResult move(String path) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
@override
|
||||||
|
void restore(String original, {required DateTime since}) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import 'package:jj_mkvmaker/ui/explorer/key_flow.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer/pane_notices.dart';
|
import 'package:jj_mkvmaker/ui/explorer/pane_notices.dart';
|
||||||
import 'package:jj_mkvmaker/ui/explorer/server_flows.dart';
|
import 'package:jj_mkvmaker/ui/explorer/server_flows.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show RecycleBinBackend, RecycleResult;
|
||||||
|
|
||||||
/// E5d-6 약속 시험: explorer_layout · access_flow · pane_notices · view_dialogs · rename_flow · tools_flow · drop_flow · key_flow ·
|
/// E5d-6 약속 시험: explorer_layout · access_flow · pane_notices · view_dialogs · rename_flow · tools_flow · drop_flow · key_flow ·
|
||||||
/// explorer_bars 맨 위 약속 (실제 시험 폴더 · 실제 화면 · 실제 설정 (저장하지 않음) · 실제 이름 바꾸기)
|
/// explorer_bars 맨 위 약속 (실제 시험 폴더 · 실제 화면 · 실제 설정 (저장하지 않음) · 실제 이름 바꾸기)
|
||||||
|
|
@ -57,7 +58,7 @@ void main() {
|
||||||
final sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
|
final sel = ExplorerSelect(ctl, clock: DateTime.now, openFile: (_, _) async {});
|
||||||
final layout = ExplorerLayout(ctl, context: () => host.context);
|
final layout = ExplorerLayout(ctl, context: () => host.context);
|
||||||
final flows = ExplorerFlows(ctl, sel,
|
final flows = ExplorerFlows(ctl, sel,
|
||||||
androidTrash: () => false, otherDirOf: (_) => null, otherDir: () => layout.other?.current, twoPanes: () => layout.dual || layout.split);
|
androidTrash: () => false, recycleBin: _noRecycle, otherDirOf: (_) => null, otherDir: () => layout.other?.current, twoPanes: () => layout.dual || layout.split);
|
||||||
final access = ExplorerAccess(ctl, probe: (_) async => drive);
|
final access = ExplorerAccess(ctl, probe: (_) async => drive);
|
||||||
final input = ExplorerInput(ctl, sel, flows, ExplorerButtons(ctl, sel, host, flows), dual: () => layout.dual);
|
final input = ExplorerInput(ctl, sel, flows, ExplorerButtons(ctl, sel, host, flows), dual: () => layout.dual);
|
||||||
addTearDown(() {
|
addTearDown(() {
|
||||||
|
|
@ -209,3 +210,14 @@ class _HostState extends State<_Host> implements ExplorerHost, ExplorerCommands
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(body: _child?.call() ?? const SizedBox());
|
Widget build(BuildContext context) => Scaffold(body: _child?.call() ?? const SizedBox());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 이 시험은 휴지통으로 보내지 않는다 (영구 삭제만) - 혹시 보내면 사용자 휴지통에 닿지 않고 실패한다
|
||||||
|
RecycleBinBackend _noRecycle() => const _NoRecycle();
|
||||||
|
|
||||||
|
class _NoRecycle implements RecycleBinBackend {
|
||||||
|
const _NoRecycle();
|
||||||
|
@override
|
||||||
|
RecycleResult move(String path) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
@override
|
||||||
|
void restore(String original, {required DateTime since}) => throw StateError('이 시험은 휴지통으로 보내지 않음');
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ void main() {
|
||||||
late AppController c;
|
late AppController c;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
useTestRecycleBin();
|
useTestRecycleBin(device);
|
||||||
tmp = Directory.systemTemp.createTempSync('jj_steady_');
|
tmp = Directory.systemTemp.createTempSync('jj_steady_');
|
||||||
left = p.join(tmp.path, 'left');
|
left = p.join(tmp.path, 'left');
|
||||||
right = p.join(tmp.path, 'right');
|
right = p.join(tmp.path, 'right');
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,6 @@ TestExplorerDevice get device => _device!;
|
||||||
|
|
||||||
void useExplorerFixture() {
|
void useExplorerFixture() {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
// 휴지통: 기본은 가짜 (이 시험들은 영구 삭제를 고르지만 혹시라도 사용자 휴지통에 닿지 않게)
|
|
||||||
useTestRecycleBin();
|
|
||||||
_tmp = Directory.systemTemp.createTempSync('jj_explorer_');
|
_tmp = Directory.systemTemp.createTempSync('jj_explorer_');
|
||||||
_left = p.join(tmp.path, 'left');
|
_left = p.join(tmp.path, 'left');
|
||||||
_right = p.join(tmp.path, 'right');
|
_right = p.join(tmp.path, 'right');
|
||||||
|
|
@ -63,6 +61,8 @@ void useExplorerFixture() {
|
||||||
Directory(right).createSync();
|
Directory(right).createSync();
|
||||||
_shell = ExplorerShell();
|
_shell = ExplorerShell();
|
||||||
_device = TestExplorerDevice();
|
_device = TestExplorerDevice();
|
||||||
|
// 휴지통: 기본은 가짜 (이 시험들은 영구 삭제를 고르지만 혹시라도 사용자 휴지통에 닿지 않게)
|
||||||
|
useTestRecycleBin(device);
|
||||||
_c = AppController(PlatformServices(
|
_c = AppController(PlatformServices(
|
||||||
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell));
|
mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell));
|
||||||
c.settings.explorer.paths = [left, right];
|
c.settings.explorer.paths = [left, right];
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import 'package:jj_mkvmaker/l10n/tr.dart';
|
||||||
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart';
|
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'test_explorer_device.dart';
|
||||||
|
|
||||||
/// 실제 사용자 휴지통을 쓰는 시험은 이 값이 1 일 때만 (휴지통 쪽 코드를 고칠 때 · 릴리스 전에 따로 돌리고 흔적 0 확인).
|
/// 실제 사용자 휴지통을 쓰는 시험은 이 값이 1 일 때만 (휴지통 쪽 코드를 고칠 때 · 릴리스 전에 따로 돌리고 흔적 0 확인).
|
||||||
/// 기본 전체 시험은 [FakeRecycleBin] 으로 흐름만 본다 - 시험이 도중에 끊기면 사용자 휴지통에 흔적이 남기 때문.
|
/// 기본 전체 시험은 [FakeRecycleBin] 으로 흐름만 본다 - 시험이 도중에 끊기면 사용자 휴지통에 흔적이 남기 때문.
|
||||||
bool get realRecycleBin => Platform.environment['JJ_TEST_REAL_RECYCLE'] == '1';
|
bool get realRecycleBin => Platform.environment['JJ_TEST_REAL_RECYCLE'] == '1';
|
||||||
|
|
@ -16,13 +18,14 @@ Object get realRecycleSkip => !Platform.isWindows
|
||||||
? false
|
? false
|
||||||
: '실제 휴지통 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만';
|
: '실제 휴지통 시험은 JJ_TEST_REAL_RECYCLE=1 일 때만';
|
||||||
|
|
||||||
/// 시험 하나 동안 휴지통을 가짜로 (JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 그대로). 가짜 휴지통을 돌려준다 (실제면 null).
|
/// 시험 하나 동안 [device] 의 휴지통을 시험 폴더 안의 것으로 (JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 그대로).
|
||||||
FakeRecycleBin? useTestRecycleBin() {
|
/// 그 휴지통을 돌려준다 (실제면 null).
|
||||||
|
FakeRecycleBin? useTestRecycleBin(TestExplorerDevice device) {
|
||||||
if (realRecycleBin) return null;
|
if (realRecycleBin) return null;
|
||||||
final fake = FakeRecycleBin();
|
final fake = FakeRecycleBin();
|
||||||
recycleBinForTest = fake;
|
device.bin = fake;
|
||||||
addTearDown(() {
|
addTearDown(() {
|
||||||
recycleBinForTest = null;
|
device.bin = null;
|
||||||
fake.dispose();
|
fake.dispose();
|
||||||
});
|
});
|
||||||
return fake;
|
return fake;
|
||||||
|
|
@ -43,6 +46,8 @@ class FakeRecycleBin implements RecycleBinBackend {
|
||||||
final long = longPathInside(path);
|
final long = longPathInside(path);
|
||||||
if (long != null) throw LongPathException(recycleErrorText(0x7C), long);
|
if (long != null) throw LongPathException(recycleErrorText(0x7C), long);
|
||||||
final full = File(path).absolute.path;
|
final full = File(path).absolute.path;
|
||||||
|
// 진단 (10/23 한 번 '경로를 찾을 수 없음'): 시험 휴지통 폴더가 없어졌으면 다시 만들지 않고 그대로 알린다
|
||||||
|
if (!bin.existsSync()) throw FileSystemException('시험 휴지통 폴더가 없어짐', bin.path);
|
||||||
final to = p.join(bin.path, '\$R${_n++}${p.extension(full)}');
|
final to = p.join(bin.path, '\$R${_n++}${p.extension(full)}');
|
||||||
_move(full, to);
|
_move(full, to);
|
||||||
items[full] = to;
|
items[full] = to;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:jj_mkvmaker/app/explorer_device.dart';
|
import 'package:jj_mkvmaker/app/explorer_device.dart';
|
||||||
import 'package:jj_mkvmaker/app/local_fs.dart';
|
import 'package:jj_mkvmaker/app/local_fs.dart';
|
||||||
import 'package:jj_mkvmaker/core/file_ops.dart' show FileEntry;
|
import 'package:jj_mkvmaker/core/file_ops.dart' show FileEntry;
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart' show RecycleBinBackend;
|
||||||
|
|
||||||
/// 시험용 [ExplorerDevice]: 정한 것만 바꾸고 나머지는 이 기기 그대로.
|
/// 시험용 [ExplorerDevice]: 정한 것만 바꾸고 나머지는 이 기기 그대로.
|
||||||
/// [android] · [trash] · [nomediaOn] 은 Windows 에서 Android 의 갈래를 지나게 할 뿐이다 - 논리만 확인, Android 실제 확인은 노트 10+
|
/// [android] · [trash] · [nomediaOn] 은 Windows 에서 Android 의 갈래를 지나게 할 뿐이다 - 논리만 확인, Android 실제 확인은 노트 10+
|
||||||
|
|
@ -20,12 +21,17 @@ class TestExplorerDevice implements ExplorerDevice {
|
||||||
/// 남은 공간 · 전체 (null 이면 이 기기) - 캡처가 실행마다 같게 정한다
|
/// 남은 공간 · 전체 (null 이면 이 기기) - 캡처가 실행마다 같게 정한다
|
||||||
(int, int)? Function(String path)? space;
|
(int, int)? Function(String path)? space;
|
||||||
|
|
||||||
|
/// Windows 휴지통 (null 이면 이 기기의 휴지통 - 시험은 useTestRecycleBin 으로 시험 폴더 안의 것)
|
||||||
|
RecycleBinBackend? bin;
|
||||||
|
|
||||||
/// 경로 입력 SMB 안내 · 앱 휴지통 · .nomedia 메뉴 (null 이면 이 기기)
|
/// 경로 입력 SMB 안내 · 앱 휴지통 · .nomedia 메뉴 (null 이면 이 기기)
|
||||||
bool? android, trash, nomediaOn;
|
bool? android, trash, nomediaOn;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
LocalFs get fs => _TestFs(this);
|
LocalFs get fs => _TestFs(this);
|
||||||
@override
|
@override
|
||||||
|
RecycleBinBackend get recycleBin => bin ?? _real.recycleBin;
|
||||||
|
@override
|
||||||
Future<(int, int)?> diskSpace(String path) async => space != null ? space!(path) : await _real.diskSpace(path);
|
Future<(int, int)?> diskSpace(String path) async => space != null ? space!(path) : await _real.diskSpace(path);
|
||||||
@override
|
@override
|
||||||
bool get androidPaths => android ?? _real.androidPaths;
|
bool get androidPaths => android ?? _real.androidPaths;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue