파일 탐색기 같은 이름 창: 두 파일의 크기 · 바뀐 날 · 어느 쪽이 새것인지 (172)
"doc.txt: 보낼 것 3B · 2026-10-09 12:00 / 있는 것 3B · 2026-10-01 08:00 (보낼 것이 더 새것)" (3개까지, 폴더는 "폴더"). _findClashes 가 vStat 으로 두 쪽을 함께 읽는다 (WebDAV 포함). explorer_conflict_test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음4
parent
f805403641
commit
a66f3a8823
|
|
@ -6,8 +6,11 @@
|
||||||
" (결과물 {0}개 함께)": " ({0} results moved too)",
|
" (결과물 {0}개 함께)": " ({0} results moved too)",
|
||||||
" (대기열)": " (queued)",
|
" (대기열)": " (queued)",
|
||||||
" (동영상 {0}개)": " ({0} videos)",
|
" (동영상 {0}개)": " ({0} videos)",
|
||||||
|
" (바뀐 날 같음)": " (same modified time)",
|
||||||
|
" (보낼 것이 더 새것)": " (the one being sent is newer)",
|
||||||
" (보통)": " (normal)",
|
" (보통)": " (normal)",
|
||||||
" (사용 불가)": " (unavailable)",
|
" (사용 불가)": " (unavailable)",
|
||||||
|
" (있는 것이 더 새것)": " (the existing one is newer)",
|
||||||
" (자막 {0}개 함께)": " ({0} subtitles moved too)",
|
" (자막 {0}개 함께)": " ({0} subtitles moved too)",
|
||||||
" (지금: 내림차순 · 다시 누르면 오름차순)": " (now: descending · press again for ascending)",
|
" (지금: 내림차순 · 다시 누르면 오름차순)": " (now: descending · press again for ascending)",
|
||||||
" (지금: 오름차순 · 다시 누르면 내림차순)": " (now: ascending · press again for descending)",
|
" (지금: 오름차순 · 다시 누르면 내림차순)": " (now: ascending · press again for descending)",
|
||||||
|
|
@ -932,6 +935,7 @@
|
||||||
"보기": "View",
|
"보기": "View",
|
||||||
"보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom",
|
"보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom",
|
||||||
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "How often [Slideshow] in the viewer turns to the next page",
|
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "How often [Slideshow] in the viewer turns to the next page",
|
||||||
|
"보낼 것 {0} / 있는 것 {1}": "sending {0} / existing {1}",
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "The folder you were viewing is gone, so moved to its parent folder",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "The folder you were viewing is gone, so moved to its parent folder",
|
||||||
"보안": "Security",
|
"보안": "Security",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@
|
||||||
" (결과물 {0}개 함께)": "(成果物 {0}個も一緒に)",
|
" (결과물 {0}개 함께)": "(成果物 {0}個も一緒に)",
|
||||||
" (대기열)": "(キュー)",
|
" (대기열)": "(キュー)",
|
||||||
" (동영상 {0}개)": "(動画 {0}個)",
|
" (동영상 {0}개)": "(動画 {0}個)",
|
||||||
|
" (바뀐 날 같음)": " (更新日時が同じ)",
|
||||||
|
" (보낼 것이 더 새것)": " (送る方が新しい)",
|
||||||
" (보통)": "(標準)",
|
" (보통)": "(標準)",
|
||||||
" (사용 불가)": "(使用不可)",
|
" (사용 불가)": "(使用不可)",
|
||||||
|
" (있는 것이 더 새것)": " (既存の方が新しい)",
|
||||||
" (자막 {0}개 함께)": "(字幕 {0}個も一緒に)",
|
" (자막 {0}개 함께)": "(字幕 {0}個も一緒に)",
|
||||||
" (지금: 내림차순 · 다시 누르면 오름차순)": "(現在: 降順 · もう一度押すと昇順)",
|
" (지금: 내림차순 · 다시 누르면 오름차순)": "(現在: 降順 · もう一度押すと昇順)",
|
||||||
" (지금: 오름차순 · 다시 누르면 내림차순)": "(現在: 昇順 · もう一度押すと降順)",
|
" (지금: 오름차순 · 다시 누르면 내림차순)": "(現在: 昇順 · もう一度押すと降順)",
|
||||||
|
|
@ -932,6 +935,7 @@
|
||||||
"보기": "表示",
|
"보기": "表示",
|
||||||
"보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小",
|
"보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小",
|
||||||
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "表示画面の [連続表示] が次のページへ進む間隔",
|
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "表示画面の [連続表示] が次のページへ進む間隔",
|
||||||
|
"보낼 것 {0} / 있는 것 {1}": "送る方 {0} / 既存 {1}",
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました",
|
||||||
"보안": "セキュリティ",
|
"보안": "セキュリティ",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@
|
||||||
" (결과물 {0}개 함께)": "(成品 {0} 个一并移动)",
|
" (결과물 {0}개 함께)": "(成品 {0} 个一并移动)",
|
||||||
" (대기열)": "(队列)",
|
" (대기열)": "(队列)",
|
||||||
" (동영상 {0}개)": "({0} 个视频)",
|
" (동영상 {0}개)": "({0} 个视频)",
|
||||||
|
" (바뀐 날 같음)": "(修改时间相同)",
|
||||||
|
" (보낼 것이 더 새것)": "(要发送的较新)",
|
||||||
" (보통)": "(标准)",
|
" (보통)": "(标准)",
|
||||||
" (사용 불가)": "(不可用)",
|
" (사용 불가)": "(不可用)",
|
||||||
|
" (있는 것이 더 새것)": "(已有的较新)",
|
||||||
" (자막 {0}개 함께)": "(字幕 {0} 个一并移动)",
|
" (자막 {0}개 함께)": "(字幕 {0} 个一并移动)",
|
||||||
" (지금: 내림차순 · 다시 누르면 오름차순)": "(当前: 降序 · 再按一次改为升序)",
|
" (지금: 내림차순 · 다시 누르면 오름차순)": "(当前: 降序 · 再按一次改为升序)",
|
||||||
" (지금: 오름차순 · 다시 누르면 내림차순)": "(当前: 升序 · 再按一次改为降序)",
|
" (지금: 오름차순 · 다시 누르면 내림차순)": "(当前: 升序 · 再按一次改为降序)",
|
||||||
|
|
@ -932,6 +935,7 @@
|
||||||
"보기": "查看",
|
"보기": "查看",
|
||||||
"보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放",
|
"보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放",
|
||||||
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "浏览画面中 [连续浏览] 翻到下一页的间隔",
|
"보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "浏览画面中 [连续浏览] 翻到下一页的间隔",
|
||||||
|
"보낼 것 {0} / 있는 것 {1}": "要发送 {0} / 已有 {1}",
|
||||||
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹",
|
"보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹",
|
||||||
"보안": "安全",
|
"보안": "安全",
|
||||||
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。",
|
"보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。",
|
||||||
|
|
|
||||||
|
|
@ -1568,12 +1568,16 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
||||||
/// 154: 항목 이름 (앞의 3개 + "외 n개")
|
/// 154: 항목 이름 (앞의 3개 + "외 n개")
|
||||||
/// 48: 받는 폴더에 같은 이름이 있는 원본 (같은 폴더로 복사는 늘 새 이름이라 빼고)
|
/// 48: 받는 폴더에 같은 이름이 있는 원본 (같은 폴더로 복사는 늘 새 이름이라 빼고)
|
||||||
static Future<List<String>> _findClashes(List<String> sources, String dest) async {
|
/// 48 · 172: 대상 폴더에 같은 이름이 있는 원본 - 두 파일의 크기 · 바뀐 날을 함께 (어느 쪽이 새것인지 보고 고르게)
|
||||||
final out = <String>[];
|
static Future<List<NameClash>> _findClashes(List<String> sources, String dest) async {
|
||||||
|
final out = <NameClash>[];
|
||||||
for (final s in sources) {
|
for (final s in sources) {
|
||||||
if (samePath(vDirname(s), dest)) continue;
|
if (samePath(vDirname(s), dest)) continue;
|
||||||
try {
|
try {
|
||||||
if (await vExists(vJoin(dest, vBasename(s)))) out.add(s);
|
final there = await vStat(vJoin(dest, vBasename(s)));
|
||||||
|
if (there == null) continue;
|
||||||
|
final here = await vStat(s);
|
||||||
|
out.add((src: s, here: here, there: there));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|
@ -1711,7 +1715,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
final builtin = method == CopyMethod.builtin || _viaDav([...sources, dest]);
|
final builtin = method == CopyMethod.builtin || _viaDav([...sources, dest]);
|
||||||
// 확인 창은 바로 띄우고, 같은 이름은 창 안에서 뒤에서 찾는다 (느린 네트워크 · WebDAV 폴더라도 창이 늦게 뜨지 않게).
|
// 확인 창은 바로 띄우고, 같은 이름은 창 안에서 뒤에서 찾는다 (느린 네트워크 · WebDAV 폴더라도 창이 늦게 뜨지 않게).
|
||||||
// 다 찾기 전에 누르면 이름 바꾸기 (원래 파일을 건드리지 않는 쪽)
|
// 다 찾기 전에 누르면 이름 바꾸기 (원래 파일을 건드리지 않는 쪽)
|
||||||
final clashesFuture = builtin ? _findClashes(sources, dest) : Future.value(const <String>[]);
|
final clashesFuture = builtin ? _findClashes(sources, dest) : Future.value(const <NameClash>[]);
|
||||||
// 48: 환경 설정 "같은 이름이 있을 때" 가 늘 하는 것이면 묻지 않고 그대로 (확인 창에 알림만)
|
// 48: 환경 설정 "같은 이름이 있을 때" 가 늘 하는 것이면 묻지 않고 그대로 (확인 창에 알림만)
|
||||||
final always = switch (c.settings.copyConflict) {
|
final always = switch (c.settings.copyConflict) {
|
||||||
'rename' => NameConflict.rename,
|
'rename' => NameConflict.rename,
|
||||||
|
|
@ -1734,7 +1738,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
'\n\n${trf('방법: {0}', [tr(method.label)])}'
|
'\n\n${trf('방법: {0}', [tr(method.label)])}'
|
||||||
'${method == CopyMethod.builtin ? '' : ' · ${task.options}'}'
|
'${method == CopyMethod.builtin ? '' : ' · ${task.options}'}'
|
||||||
'${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'),
|
'${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'),
|
||||||
FutureBuilder<List<String>>(
|
FutureBuilder<List<NameClash>>(
|
||||||
future: clashesFuture,
|
future: clashesFuture,
|
||||||
builder: (ctx, snap) {
|
builder: (ctx, snap) {
|
||||||
if (snap.connectionState != ConnectionState.done) {
|
if (snap.connectionState != ConnectionState.done) {
|
||||||
|
|
@ -1743,12 +1747,18 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
child: Text(tr('같은 이름이 있는지 확인하는 중 …'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
child: Text(tr('같은 이름이 있는지 확인하는 중 …'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final clashes = snap.data ?? const <String>[];
|
final clashes = snap.data ?? const <NameClash>[];
|
||||||
if (clashes.isEmpty) return const SizedBox.shrink();
|
if (clashes.isEmpty) return const SizedBox.shrink();
|
||||||
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(trf('같은 이름이 이미 있습니다: {0}', [_namesOf(clashes)]),
|
Text(trf('같은 이름이 이미 있습니다: {0}', [_namesOf([for (final x in clashes) x.src])]),
|
||||||
style: const TextStyle(color: Colors.orangeAccent)),
|
style: const TextStyle(color: Colors.orangeAccent)),
|
||||||
|
// 172: 두 파일의 크기 · 바뀐 날 (3개까지)
|
||||||
|
for (final x in clashes.take(3))
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4, left: 8),
|
||||||
|
child: Text(clashCompareText(x), style: const TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
if (always != null)
|
if (always != null)
|
||||||
Text(
|
Text(
|
||||||
trf('환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)', [
|
trf('환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)', [
|
||||||
|
|
@ -3036,6 +3046,24 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 172: 같은 이름 - 보낼 것 (복사 · 이동, [here]) 과 대상 폴더에 이미 있는 것 ([there])
|
||||||
|
typedef NameClash = ({String src, ({bool isDir, int size, DateTime modified})? here, ({bool isDir, int size, DateTime modified}) there});
|
||||||
|
|
||||||
|
/// 172: "이름: 보낼 것 1.2 MB · 2026-10-09 14:00 / 있는 것 900 KB · 2026-10-01 09:00 (보낼 것이 더 새것)"
|
||||||
|
String clashCompareText(NameClash x) {
|
||||||
|
String one(({bool isDir, int size, DateTime modified}) s) =>
|
||||||
|
[if (!s.isDir) formatSize(s.size) else tr('폴더'), if (s.modified.year > 1) _date(s.modified)].join(' · ');
|
||||||
|
final h = x.here, t = x.there;
|
||||||
|
final newer = h == null || h.modified.year <= 1 || t.modified.year <= 1
|
||||||
|
? ''
|
||||||
|
: h.modified.isAfter(t.modified.add(const Duration(seconds: 2)))
|
||||||
|
? tr(' (보낼 것이 더 새것)')
|
||||||
|
: t.modified.isAfter(h.modified.add(const Duration(seconds: 2)))
|
||||||
|
? tr(' (있는 것이 더 새것)')
|
||||||
|
: tr(' (바뀐 날 같음)');
|
||||||
|
return '${vBasename(x.src)}: ${trf('보낼 것 {0} / 있는 것 {1}', [h == null ? '?' : one(h), one(t)])}$newer';
|
||||||
|
}
|
||||||
|
|
||||||
String _date(DateTime d) =>
|
String _date(DateTime d) =>
|
||||||
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')} '
|
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')} '
|
||||||
'${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}';
|
'${d.hour.toString().padLeft(2, '0')}:${d.minute.toString().padLeft(2, '0')}';
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ void main() {
|
||||||
File(p.join(right, 'doc.txt'))
|
File(p.join(right, 'doc.txt'))
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsStringSync('old');
|
..writeAsStringSync('old');
|
||||||
|
// 172: 바뀐 날 (왼쪽이 더 새것)
|
||||||
|
File(p.join(left, 'doc.txt')).setLastModifiedSync(DateTime(2026, 10, 9, 12));
|
||||||
|
File(p.join(right, 'doc.txt')).setLastModifiedSync(DateTime(2026, 10, 1, 8));
|
||||||
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
c.settings.explorerPaths = [left, right];
|
c.settings.explorerPaths = [left, right];
|
||||||
final root = p.rootPrefix(tmp.path);
|
final root = p.rootPrefix(tmp.path);
|
||||||
|
|
@ -67,6 +70,8 @@ void main() {
|
||||||
expect(find.widgetWithText(FilledButton, '복사'), findsOneWidget);
|
expect(find.widgetWithText(FilledButton, '복사'), findsOneWidget);
|
||||||
await settle(tester, () => find.textContaining('같은 이름이 이미 있습니다').evaluate().isNotEmpty);
|
await settle(tester, () => find.textContaining('같은 이름이 이미 있습니다').evaluate().isNotEmpty);
|
||||||
expect(find.textContaining('같은 이름이 이미 있습니다: doc.txt'), findsOneWidget);
|
expect(find.textContaining('같은 이름이 이미 있습니다: doc.txt'), findsOneWidget);
|
||||||
|
// 172: 두 파일의 크기 · 바뀐 날 (어느 쪽이 새것인지)
|
||||||
|
expect(find.textContaining('doc.txt: 보낼 것 3B · 2026-10-09 12:00 / 있는 것 3B · 2026-10-01 08:00 (보낼 것이 더 새것)'), findsOneWidget);
|
||||||
// 154 · 155: 확인 창에 항목 이름
|
// 154 · 155: 확인 창에 항목 이름
|
||||||
expect(find.textContaining('\ndoc.txt'), findsOneWidget);
|
expect(find.textContaining('\ndoc.txt'), findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue