탐색기: 이동해도 그 폴더를 고르지 않음 · 148 휴지통으로 보낸 뒤 [되돌리기]
- 드라이브 탭 · 경로로 가기 · 상위 폴더 · .. · [열기] 로 가도 그 폴더를 복사 · 이동 대상으로 잡지 않는다 (누르거나 표시하지 않은 폴더가 대상이 되지 않게, 위치는 지금 폴더 표시 · 스크롤로) - 148: 휴지통으로 보낸 알림 (10초) 에 [되돌리기]. 이 사용자 (SID) 의 휴지통에서 보낸 시각 뒤의 $I 를 찾아 $R 을 원래 경로로. 원래 자리에 같은 이름이 있거나 원래 폴더가 그사이 지워졌으면 되돌리지 않고 알림 (폴더를 다시 만들지 않음) - 시험: 실제 휴지통으로 되돌리기 · 같은 이름 · 원래 폴더 없음 · 휴지통에 없음, 탐색기 [휴지통으로] → [되돌리기], 상위 폴더 뒤 [복사] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음3
parent
8f5d2204f0
commit
e1e3316d8d
|
|
@ -325,6 +325,7 @@
|
|||
"{0}개 항목을 {1}": "{0} items {1}",
|
||||
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} items to this folder — {1}\n{2}",
|
||||
"{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "{0} items held. Press [Paste] in the destination folder ({1}).",
|
||||
"{0}개 항목을 되돌렸습니다.": "Restored {0} item(s).",
|
||||
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "Delete {0} items? This cannot be undone.",
|
||||
"{0}개 항목을 휴지통으로 보낼까요?": "Move {0} items to the Recycle Bin?",
|
||||
"{0}개 항목을 휴지통으로 보냈습니다.": "Moved {0} items to the Recycle Bin.",
|
||||
|
|
@ -672,8 +673,10 @@
|
|||
"동영상이 있는 폴더 아래 jj_mkv (기본)": "jj_mkv under the video's folder (default)",
|
||||
"동의하고 받기": "Agree and download",
|
||||
"동작 중 · {0} 까지": "Running · until {0}",
|
||||
"되돌리기": "Undo",
|
||||
"되돌리기 계속": "Continue rollback",
|
||||
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "Rollback: copied AI models ({0}): {1}",
|
||||
"되돌리지 못함: {0}": "Could not restore: {0}",
|
||||
"되돌릴 수 없습니다.": "This can't be undone.",
|
||||
"되살리고 다시 시작": "Restore and restart",
|
||||
"되살리기": "Restore",
|
||||
|
|
@ -1206,6 +1209,8 @@
|
|||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "Too many requests. Try again in a moment.",
|
||||
"우르두어": "Urdu",
|
||||
"우크라이나어": "Ukrainian",
|
||||
"원래 자리에 같은 이름이 이미 있습니다": "An item with the same name already exists in the original place",
|
||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "The original folder no longer exists, so it was not restored (restore it from the Recycle Bin yourself)",
|
||||
"원문 보기 (번역 끄기)": "Show original (turn off translation)",
|
||||
"원본": "Source",
|
||||
"원본 비율": "Original aspect",
|
||||
|
|
@ -1865,9 +1870,11 @@
|
|||
"회전 없음": "No rotation",
|
||||
"회차": "Episode",
|
||||
"휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 풀지 못해 끊기거나 배터리를 많이 쓰는 일이 있습니다. YouTube 의 H.264 는 1080p 까지라 더 큰 화질을 고르면 크기가 먼저입니다.": "Phones and tablets often can't decode AV1/VP9 in hardware, causing stutter or heavy battery use. YouTube's H.264 goes up to 1080p, so for higher qualities size comes first.",
|
||||
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "Not found in the Recycle Bin (already emptied or restored)",
|
||||
"휴지통으로": "To Recycle Bin",
|
||||
"휴지통으로 보내기": "Move to Recycle Bin",
|
||||
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "Delete permanently without the Recycle Bin (Shift+Delete)",
|
||||
"휴지통이 없는 곳입니다": "This location has no Recycle Bin",
|
||||
"휴지통이 없습니다": "No Recycle Bin",
|
||||
"히브리어": "Hebrew",
|
||||
"힌디어": "Hindi"
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@
|
|||
"{0}개 항목을 {1}": "{0} 件の項目を{1}",
|
||||
"{0}개 항목을 다음 폴더로 {1}\n{2}": "{0} 件の項目を次のフォルダーへ{1}\n{2}",
|
||||
"{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "{0} 件を保持しました。入れるフォルダーで [貼り付け] を押してください ({1})。",
|
||||
"{0}개 항목을 되돌렸습니다.": "{0} 件を元に戻しました。",
|
||||
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "{0} 件の項目を削除しますか? 元に戻せません。",
|
||||
"{0}개 항목을 휴지통으로 보낼까요?": "{0} 個の項目をごみ箱に移動しますか?",
|
||||
"{0}개 항목을 휴지통으로 보냈습니다.": "{0} 個の項目をごみ箱に移動しました。",
|
||||
|
|
@ -672,8 +673,10 @@
|
|||
"동영상이 있는 폴더 아래 jj_mkv (기본)": "動画があるフォルダーの下の jj_mkv(既定)",
|
||||
"동의하고 받기": "同意してダウンロード",
|
||||
"동작 중 · {0} 까지": "動作中 · {0} まで",
|
||||
"되돌리기": "元に戻す",
|
||||
"되돌리기 계속": "戻す操作を続ける",
|
||||
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "戻す: AI モデル {0} をコピーしました: {1}",
|
||||
"되돌리지 못함: {0}": "元に戻せません: {0}",
|
||||
"되돌릴 수 없습니다.": "元に戻せません。",
|
||||
"되살리고 다시 시작": "復元して再起動",
|
||||
"되살리기": "元に戻す",
|
||||
|
|
@ -1206,6 +1209,8 @@
|
|||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "リクエストが多すぎます。しばらくしてからもう一度お試しください。",
|
||||
"우르두어": "ウルドゥー語",
|
||||
"우크라이나어": "ウクライナ語",
|
||||
"원래 자리에 같은 이름이 이미 있습니다": "元の場所に同じ名前が既にあります",
|
||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "元のフォルダーがないため元に戻せませんでした (ごみ箱から直接復元してください)",
|
||||
"원문 보기 (번역 끄기)": "原文を表示 (翻訳をオフ)",
|
||||
"원본": "元",
|
||||
"원본 비율": "元の比率",
|
||||
|
|
@ -1865,9 +1870,11 @@
|
|||
"회전 없음": "回転なし",
|
||||
"회차": "話数",
|
||||
"휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 풀지 못해 끊기거나 배터리를 많이 쓰는 일이 있습니다. YouTube 의 H.264 는 1080p 까지라 더 큰 화질을 고르면 크기가 먼저입니다.": "スマホ・タブレットは AV1・VP9 をハードウェアで再生できず、途切れたりバッテリーを多く使うことがあります。YouTube の H.264 は 1080p までなので、それより高い画質を選ぶとサイズが優先です。",
|
||||
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "ごみ箱に見つかりません (空にしたか元に戻した)",
|
||||
"휴지통으로": "ごみ箱へ",
|
||||
"휴지통으로 보내기": "ごみ箱に移動",
|
||||
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "ごみ箱を使わずに完全に削除 (Shift+Delete)",
|
||||
"휴지통이 없는 곳입니다": "ごみ箱がない場所です",
|
||||
"휴지통이 없습니다": "ごみ箱がありません",
|
||||
"히브리어": "ヘブライ語",
|
||||
"힌디어": "ヒンディー語"
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@
|
|||
"{0}개 항목을 {1}": "{0} 个项目{1}",
|
||||
"{0}개 항목을 다음 폴더로 {1}\n{2}": "将 {0} 个项目到以下文件夹 — {1}\n{2}",
|
||||
"{0}개 항목을 담았습니다. 넣을 폴더에서 [붙여넣기] 를 누르세요 ({1}).": "已暂存 {0} 个项目。请在目标文件夹中按 [粘贴] ({1})。",
|
||||
"{0}개 항목을 되돌렸습니다.": "已恢复 {0} 个项目。",
|
||||
"{0}개 항목을 지울까요? 되돌릴 수 없습니다.": "删除 {0} 个项目吗? 此操作无法撤销。",
|
||||
"{0}개 항목을 휴지통으로 보낼까요?": "将 {0} 个项目移到回收站吗?",
|
||||
"{0}개 항목을 휴지통으로 보냈습니다.": "已将 {0} 个项目移到回收站。",
|
||||
|
|
@ -672,8 +673,10 @@
|
|||
"동영상이 있는 폴더 아래 jj_mkv (기본)": "视频所在文件夹下的 jj_mkv(默认)",
|
||||
"동의하고 받기": "同意并下载",
|
||||
"동작 중 · {0} 까지": "运行中 · 直到 {0}",
|
||||
"되돌리기": "撤销",
|
||||
"되돌리기 계속": "继续回退",
|
||||
"되돌리기: AI 모델 {0} 을 복사해 두었습니다: {1}": "回退: 已复制 AI 模型 {0}: {1}",
|
||||
"되돌리지 못함: {0}": "无法恢复: {0}",
|
||||
"되돌릴 수 없습니다.": "无法撤销。",
|
||||
"되살리고 다시 시작": "恢复并重新启动",
|
||||
"되살리기": "恢复",
|
||||
|
|
@ -1206,6 +1209,8 @@
|
|||
"요청이 너무 많습니다. 잠시 후 다시 시도하세요.": "请求过多。请稍后再试。",
|
||||
"우르두어": "乌尔都语",
|
||||
"우크라이나어": "乌克兰语",
|
||||
"원래 자리에 같은 이름이 이미 있습니다": "原位置已存在同名项目",
|
||||
"원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)": "原文件夹已不存在,未能恢复 (请在回收站中手动还原)",
|
||||
"원문 보기 (번역 끄기)": "显示原文 (关闭翻译)",
|
||||
"원본": "源",
|
||||
"원본 비율": "原始比例",
|
||||
|
|
@ -1865,9 +1870,11 @@
|
|||
"회전 없음": "不旋转",
|
||||
"회차": "集数",
|
||||
"휴대폰 · 태블릿은 AV1 · VP9 을 하드웨어로 풀지 못해 끊기거나 배터리를 많이 쓰는 일이 있습니다. YouTube 의 H.264 는 1080p 까지라 더 큰 화질을 고르면 크기가 먼저입니다.": "手机 · 平板常无法硬件解码 AV1 · VP9,会卡顿或耗电。YouTube 的 H.264 最高 1080p,选择更高画质时以尺寸优先。",
|
||||
"휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)": "在回收站中找不到 (已清空或已恢复)",
|
||||
"휴지통으로": "移到回收站",
|
||||
"휴지통으로 보내기": "移到回收站",
|
||||
"휴지통을 거치지 않고 영구 삭제 (Shift+Delete)": "不经过回收站永久删除 (Shift+Delete)",
|
||||
"휴지통이 없는 곳입니다": "此位置没有回收站",
|
||||
"휴지통이 없습니다": "没有回收站",
|
||||
"히브리어": "希伯来语",
|
||||
"힌디어": "印地语"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:ffi';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../../l10n/tr.dart';
|
||||
|
||||
|
|
@ -112,6 +113,76 @@ bool? recycledInfoExists(String original, {required DateTime since}) {
|
|||
return readable ? false : null;
|
||||
}
|
||||
|
||||
/// 지금 사용자의 SID (예: S-1-5-21-…, 휴지통 폴더 이름). 모르면 null
|
||||
String? currentUserSid() => _sid ??= () {
|
||||
try {
|
||||
// 전체 경로로 (PATH 에 Git 등의 다른 whoami 가 먼저 있을 수 있다)
|
||||
final exe = '${Platform.environment['SystemRoot'] ?? r'C:\Windows'}\\System32\\whoami.exe';
|
||||
final r = Process.runSync(exe, ['/user', '/fo', 'csv', '/nh']);
|
||||
return RegExp(r'"(S-1-[0-9-]+)"').firstMatch('${r.stdout}')?.group(1);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}();
|
||||
String? _sid;
|
||||
|
||||
/// 148: 방금 휴지통으로 보낸 [original] 을 원래 자리로 되돌린다 ([since] 뒤에 생긴 것 중 가장 새것).
|
||||
/// 휴지통의 $R… (내용) 을 원래 경로로 옮기고 $I… (정보) 를 지운다. 원래 자리에 이미 무엇이 있으면 덮지 않고 실패.
|
||||
/// 실패하면 [FileSystemException] (이유는 읽을 수 있는 말).
|
||||
void restoreFromRecycleBin(String original, {required DateTime since}) {
|
||||
final m = RegExp(r'^([a-zA-Z]):').firstMatch(original);
|
||||
if (m == null) throw FileSystemException(tr('휴지통이 없는 곳입니다'), original);
|
||||
if (FileSystemEntity.typeSync(original, followLinks: false) != FileSystemEntityType.notFound) {
|
||||
throw FileSystemException(tr('원래 자리에 같은 이름이 이미 있습니다'), original);
|
||||
}
|
||||
// 원래 폴더가 그사이 지워졌으면 다시 만들지 않는다 (사용자가 지운 폴더를 몰래 되살리지 않게)
|
||||
if (!Directory(p.dirname(original)).existsSync()) {
|
||||
throw FileSystemException(tr('원래 폴더가 없어 되돌리지 못했습니다 (휴지통에서 직접 복원하세요)'), original);
|
||||
}
|
||||
// 이 사용자 (SID) 의 휴지통 폴더에서만 찾는다 (관리자 권한으로 다른 사용자의 휴지통을 읽을 수 있어도)
|
||||
final sid = currentUserSid();
|
||||
if (sid == null) throw FileSystemException(tr('휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)'), original);
|
||||
File? info;
|
||||
DateTime? newest;
|
||||
final bin = Directory('${m[1]}:\\\$Recycle.Bin');
|
||||
try {
|
||||
for (final user in [Directory('${bin.path}\\$sid')]) {
|
||||
List<FileSystemEntity> items;
|
||||
try {
|
||||
items = user.listSync(followLinks: false);
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
for (final f in items.whereType<File>()) {
|
||||
if (!f.uri.pathSegments.last.startsWith(r'$I')) continue;
|
||||
try {
|
||||
final t = f.lastModifiedSync();
|
||||
if (t.isBefore(since) || (newest != null && t.isBefore(newest))) continue;
|
||||
final was = recycledInfoPath(f.readAsBytesSync());
|
||||
if (was != null && was.toLowerCase() == original.toLowerCase()) {
|
||||
info = f;
|
||||
newest = t;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
final i = info;
|
||||
if (i == null) throw FileSystemException(tr('휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)'), original);
|
||||
final dir = i.parent.path;
|
||||
final content = '$dir\\\$R${i.uri.pathSegments.last.substring(2)}';
|
||||
final type = FileSystemEntity.typeSync(content, followLinks: false);
|
||||
if (type == FileSystemEntityType.notFound) throw FileSystemException(tr('휴지통에서 찾지 못했습니다 (이미 비웠거나 되돌림)'), original);
|
||||
if (type == FileSystemEntityType.directory) {
|
||||
Directory(content).renameSync(original);
|
||||
} else {
|
||||
File(content).renameSync(original);
|
||||
}
|
||||
try {
|
||||
i.deleteSync();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// 휴지통이 받을 수 있는 경로 길이 (MAX_PATH - 끝의 NUL)
|
||||
const recycleMaxPath = 259;
|
||||
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
for (var i = 0; i < 2; i++) {
|
||||
final pane = _panes[i];
|
||||
unawaited(_goTo(pane, wants[i], remember: false, focus: false).then((_) => _reveal(pane, pane.current)));
|
||||
unawaited(_goTo(pane, wants[i], remember: false).then((_) => _reveal(pane, pane.current)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -594,7 +594,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
/// 그 폴더로: 저장 장치부터 그 폴더까지 펼치고 지금 폴더로 정한다
|
||||
/// [fresh]: 경로 입력 · 찾기 · 내역으로 갈 때 - 그 폴더를 새로 읽는다 (앱 밖에서 바뀐 것까지 보이게, 122)
|
||||
Future<void> _goTo(_Pane pane, String dir, {bool remember = true, bool fresh = false, bool focus = true}) async {
|
||||
/// 폴더로 간다. 그 폴더를 "고른 항목" 으로 잡지 않는다 (48 보충 · 관리자: 누르거나 표시하지 않은 폴더가 복사 · 이동 대상이 되지 않게).
|
||||
/// 위치는 지금 폴더 표시 · 스크롤로 보인다. [focus] 는 그 폴더를 일부러 고를 때만
|
||||
Future<void> _goTo(_Pane pane, String dir, {bool remember = true, bool fresh = false, bool focus = false}) async {
|
||||
// 124 · 128: 사용자가 WebDAV 로 간 것이면 마스터 창을 한 번 취소했어도 다시 묻는다 (켤 때 되살리는 것은 [remember] 없음)
|
||||
if (remember && isDav(dir)) await SecretGate.pass(force: true);
|
||||
if (!mounted) return;
|
||||
|
|
@ -1331,12 +1333,16 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 하나씩: 실패해도 나머지는 계속. 실제로 어떻게 됐는지 세어 그대로 알린다 (94)
|
||||
final failed = <(String, String)>[];
|
||||
var recycled = 0, nuked = 0, kept = 0;
|
||||
// 148: 휴지통으로 보낸 것 (되돌리기용) - 정보 파일 시각은 초 단위라 조금 앞에서부터
|
||||
final since = DateTime.now().subtract(const Duration(seconds: 2));
|
||||
final sent = <String>[];
|
||||
for (final x in paths) {
|
||||
try {
|
||||
if (!permanent) {
|
||||
switch (moveToRecycleBin(x)) {
|
||||
case RecycleResult.recycled:
|
||||
recycled++;
|
||||
sent.add(x);
|
||||
case RecycleResult.deletedPermanently:
|
||||
nuked++;
|
||||
case RecycleResult.cancelled:
|
||||
|
|
@ -1370,13 +1376,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
actions: [FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('확인')))],
|
||||
),
|
||||
);
|
||||
} else if (!permanent) {
|
||||
_snack([
|
||||
} else if (!permanent && mounted) {
|
||||
final text = [
|
||||
if (recycled > 0) trf('{0}개 항목을 휴지통으로 보냈습니다.', [recycled]),
|
||||
// 휴지통보다 커서 Windows 가 물었고 사용자가 영구 삭제를 골랐다
|
||||
if (nuked > 0) trf('{0}개는 휴지통에 들어가지 않아 영구 삭제했습니다 (Windows 가 묻고 고른 대로).', [nuked]),
|
||||
if (kept > 0) trf('{0}개는 지우지 않았습니다 (취소).', [kept]),
|
||||
].join(' '));
|
||||
].join(' ');
|
||||
// 148: 휴지통으로 보낸 것은 바로 [되돌리기]
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
duration: const Duration(seconds: 10),
|
||||
content: Text(text),
|
||||
action: sent.isEmpty ? null : SnackBarAction(label: tr('되돌리기'), onPressed: () => _undoRecycle(sent, since)),
|
||||
));
|
||||
}
|
||||
for (final x in _panes) {
|
||||
x.marked.removeAll(paths);
|
||||
|
|
@ -1387,6 +1399,28 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
await _refreshAll(paths.map(p.dirname));
|
||||
}
|
||||
|
||||
/// 148: 방금 휴지통으로 보낸 것을 원래 자리로 (원래 자리에 같은 이름이 생겼으면 그것은 두고 알림)
|
||||
Future<void> _undoRecycle(List<String> paths, DateTime since) async {
|
||||
final failed = <String>[];
|
||||
var back = 0;
|
||||
for (final x in paths) {
|
||||
try {
|
||||
restoreFromRecycleBin(x, since: since);
|
||||
back++;
|
||||
} catch (e) {
|
||||
failed.add('${vBasename(x)}: ${e is FileSystemException ? e.message : e}');
|
||||
}
|
||||
}
|
||||
await _refreshAll(paths.map(p.dirname));
|
||||
for (final x in _panes) {
|
||||
x.changed();
|
||||
}
|
||||
_snack([
|
||||
if (back > 0) trf('{0}개 항목을 되돌렸습니다.', [back]),
|
||||
if (failed.isNotEmpty) trf('되돌리지 못함: {0}', [failed.take(3).join(' · ')]),
|
||||
].join(' '));
|
||||
}
|
||||
|
||||
/// 다른 창의 지금 폴더로 복사 · 이동 (진행 창 · 취소)
|
||||
/// 복사 · 이동 버튼: 두 창이면 다른 창의 지금 폴더로, 그 밖의 배치는 담아 두기 (붙여넣기로)
|
||||
Future<void> _transfer(_Pane pane, List<String> sources, {required bool move}) async {
|
||||
|
|
|
|||
|
|
@ -103,6 +103,15 @@ void main() {
|
|||
expect(find.widgetWithText(FilledButton, '복사'), findsNothing, reason: '확인 창이 뜨지 않음');
|
||||
expect(find.textContaining('복사할 항목을 고르세요'), findsOneWidget);
|
||||
expect(Directory(p.join(right, 'left')).existsSync(), isFalse);
|
||||
// 상위 폴더로 가도 (사용자 이동) 그 폴더를 대상으로 잡지 않는다
|
||||
await tester.runAsync(() => tester.tap(find.text('상위 폴더').first));
|
||||
await settle(tester, () => false, rounds: 15);
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pump();
|
||||
await tester.runAsync(() => tester.tap(find.text('복사').first));
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(FilledButton, '복사'), findsNothing);
|
||||
expect(find.textContaining('복사할 항목을 고르세요'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('52: 복사 중에 화면을 떠났다 돌아와도 진행 막대가 다시 보이고, 끝나면 받는 폴더를 새로 읽는다 (.jjpart 가 남아 보이지 않음)', (tester) async {
|
||||
|
|
@ -132,6 +141,31 @@ void main() {
|
|||
expect(find.textContaining('.jjpart'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('148: 휴지통으로 보낸 뒤 알림의 [되돌리기] 를 누르면 원래 자리로', (tester) async {
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
|
||||
await settle(tester, () => find.text('doc.txt').evaluate().length >= 2);
|
||||
await tester.runAsync(() => tester.tap(find.text('doc.txt').first));
|
||||
await settle(tester, () => false, rounds: 10);
|
||||
await tester.runAsync(() => tester.tap(find.text('삭제').first));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '휴지통으로')));
|
||||
await settle(tester, () => find.text('되돌리기').evaluate().isNotEmpty);
|
||||
expect(File(p.join(left, 'doc.txt')).existsSync(), isFalse);
|
||||
expect(find.textContaining('1개 항목을 휴지통으로 보냈습니다.'), findsOneWidget);
|
||||
// 알림의 버튼은 시험의 가짜 시간 안에서 누른다 (알림 닫기 애니메이션이 시험 밖에서 돌지 않게)
|
||||
await tester.tap(find.text('되돌리기'));
|
||||
await tester.pump();
|
||||
await settle(tester, () => find.textContaining('되돌렸습니다').evaluate().isNotEmpty);
|
||||
expect(File(p.join(left, 'doc.txt')).readAsStringSync(), 'new');
|
||||
expect(find.textContaining('1개 항목을 되돌렸습니다.'), findsOneWidget);
|
||||
// 알림 (10초) 이 시험이 끝난 뒤에 닫히지 않게 정리
|
||||
ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars();
|
||||
await tester.pumpAndSettle();
|
||||
}, skip: !Platform.isWindows);
|
||||
|
||||
testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async {
|
||||
await startCopy(tester);
|
||||
await tester.tap(find.text('건너뛰기'));
|
||||
|
|
|
|||
|
|
@ -53,6 +53,54 @@ void main() {
|
|||
}
|
||||
}, skip: !Platform.isWindows);
|
||||
|
||||
test('148: 휴지통으로 보낸 파일 · 폴더를 원래 자리로 되돌린다 (휴지통에 남지 않음) · 원래 자리에 같은 이름이 있으면 덮지 않음', () async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_undo_');
|
||||
addTearDown(() async {
|
||||
await _purgeFromRecycleBin(tmp.absolute.path); // 실패해 남은 것이 있으면 사용자 휴지통에서 지운다
|
||||
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
||||
});
|
||||
final since = DateTime.now().subtract(const Duration(seconds: 2));
|
||||
final f = File(p.join(tmp.absolute.path, 'undo.txt'))..writeAsStringSync('keep me');
|
||||
final d = Directory(p.join(tmp.absolute.path, 'undo_dir'))..createSync();
|
||||
File(p.join(d.path, 'a.txt')).writeAsStringSync('a');
|
||||
expect(moveToRecycleBin(f.path), RecycleResult.recycled);
|
||||
expect(moveToRecycleBin(d.path), RecycleResult.recycled);
|
||||
expect(f.existsSync() || d.existsSync(), isFalse);
|
||||
restoreFromRecycleBin(f.path, since: since);
|
||||
restoreFromRecycleBin(d.path, since: since);
|
||||
expect(f.readAsStringSync(), 'keep me');
|
||||
expect(File(p.join(d.path, 'a.txt')).readAsStringSync(), 'a');
|
||||
expect(recycledInfoExists(f.path, since: since), isFalse, reason: '휴지통에 남지 않음');
|
||||
// 원래 자리에 같은 이름이 생겼으면 덮지 않고 실패 (휴지통의 것은 그대로)
|
||||
expect(moveToRecycleBin(f.path), RecycleResult.recycled);
|
||||
f.writeAsStringSync('new one');
|
||||
expect(() => restoreFromRecycleBin(f.path, since: since), throwsA(isA<FileSystemException>()));
|
||||
expect(f.readAsStringSync(), 'new one');
|
||||
expect(recycledInfoExists(f.path, since: since), isTrue);
|
||||
// 휴지통에 없으면 실패
|
||||
expect(() => restoreFromRecycleBin(p.join(tmp.absolute.path, 'never.txt'), since: since), throwsA(isA<FileSystemException>()));
|
||||
}, skip: !Platform.isWindows);
|
||||
|
||||
test('148: 원래 폴더가 그사이 지워졌으면 다시 만들지 않고 알린다 · 이 사용자 (SID) 의 휴지통에서만 찾는다', () async {
|
||||
expect(currentUserSid(), matches(RegExp(r'^S-1-5-')));
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_undo_');
|
||||
addTearDown(() async {
|
||||
await _purgeFromRecycleBin(tmp.absolute.path);
|
||||
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
|
||||
});
|
||||
final since = DateTime.now().subtract(const Duration(seconds: 2));
|
||||
final folder = Directory(p.join(tmp.absolute.path, 'gone'))..createSync();
|
||||
final f = File(p.join(folder.path, 'x.txt'))..writeAsStringSync('x');
|
||||
expect(moveToRecycleBin(f.path), RecycleResult.recycled);
|
||||
folder.deleteSync(); // 사용자가 그 폴더를 지움
|
||||
expect(
|
||||
() => restoreFromRecycleBin(f.path, since: since),
|
||||
throwsA(isA<FileSystemException>()
|
||||
.having((e) => e.message, 'message', contains('원래 폴더가 없어 되돌리지 못했습니다'))));
|
||||
expect(folder.existsSync(), isFalse, reason: '지운 폴더를 몰래 다시 만들지 않음');
|
||||
expect(recycledInfoExists(f.path, since: since), isTrue, reason: '휴지통의 것은 그대로');
|
||||
}, skip: !Platform.isWindows);
|
||||
|
||||
test('긴 경로 (260자 넘음) 는 휴지통으로 보내지 않고 이유를 알린다 - 묻지 않고 영구 삭제되던 것 (폴더 안에 있어도)', () {
|
||||
if (!Platform.isWindows) return;
|
||||
// 긴 경로를 만들고 지우려면 앞에 \\?\ 를 붙인다
|
||||
|
|
|
|||
Loading…
Reference in New Issue