diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 85d6bd1..2f86e6b 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -6,8 +6,11 @@ " (결과물 {0}개 함께)": " ({0} results moved too)", " (대기열)": " (queued)", " (동영상 {0}개)": " ({0} videos)", + " (바뀐 날 같음)": " (same modified time)", + " (보낼 것이 더 새것)": " (the one being sent is newer)", " (보통)": " (normal)", " (사용 불가)": " (unavailable)", + " (있는 것이 더 새것)": " (the existing one is newer)", " (자막 {0}개 함께)": " ({0} subtitles moved too)", " (지금: 내림차순 · 다시 누르면 오름차순)": " (now: descending · press again for ascending)", " (지금: 오름차순 · 다시 누르면 내림차순)": " (now: ascending · press again for descending)", @@ -932,6 +935,7 @@ "보기": "View", "보기 {0}초 · Ctrl+휠 확대/축소": "View {0} s · Ctrl+wheel to zoom", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "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", "보안": "Security", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "Choose the buttons to show and drag to reorder.", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 9263620..9929ca3 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -6,8 +6,11 @@ " (결과물 {0}개 함께)": "(成果物 {0}個も一緒に)", " (대기열)": "(キュー)", " (동영상 {0}개)": "(動画 {0}個)", + " (바뀐 날 같음)": " (更新日時が同じ)", + " (보낼 것이 더 새것)": " (送る方が新しい)", " (보통)": "(標準)", " (사용 불가)": "(使用不可)", + " (있는 것이 더 새것)": " (既存の方が新しい)", " (자막 {0}개 함께)": "(字幕 {0}個も一緒に)", " (지금: 내림차순 · 다시 누르면 오름차순)": "(現在: 降順 · もう一度押すと昇順)", " (지금: 오름차순 · 다시 누르면 내림차순)": "(現在: 昇順 · もう一度押すと降順)", @@ -932,6 +935,7 @@ "보기": "表示", "보기 {0}초 · Ctrl+휠 확대/축소": "表示 {0}秒・Ctrl+ホイールで拡大/縮小", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "表示画面の [連続表示] が次のページへ進む間隔", + "보낼 것 {0} / 있는 것 {1}": "送る方 {0} / 既存 {1}", "보던 폴더가 없어져 상위 폴더로 옮겼습니다": "見ていたフォルダーがなくなったため上のフォルダーに移動しました", "보안": "セキュリティ", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "表示するボタンを選び、ドラッグで順序を変えます。", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 379434a..133a98f 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -6,8 +6,11 @@ " (결과물 {0}개 함께)": "(成品 {0} 个一并移动)", " (대기열)": "(队列)", " (동영상 {0}개)": "({0} 个视频)", + " (바뀐 날 같음)": "(修改时间相同)", + " (보낼 것이 더 새것)": "(要发送的较新)", " (보통)": "(标准)", " (사용 불가)": "(不可用)", + " (있는 것이 더 새것)": "(已有的较新)", " (자막 {0}개 함께)": "(字幕 {0} 个一并移动)", " (지금: 내림차순 · 다시 누르면 오름차순)": "(当前: 降序 · 再按一次改为升序)", " (지금: 오름차순 · 다시 누르면 내림차순)": "(当前: 升序 · 再按一次改为降序)", @@ -932,6 +935,7 @@ "보기": "查看", "보기 {0}초 · Ctrl+휠 확대/축소": "显示 {0} 秒 · Ctrl+滚轮缩放", "보기 화면의 [계속 보기] 가 다음 장으로 넘기는 간격": "浏览画面中 [连续浏览] 翻到下一页的间隔", + "보낼 것 {0} / 있는 것 {1}": "要发送 {0} / 已有 {1}", "보던 폴더가 없어져 상위 폴더로 옮겼습니다": "正在查看的文件夹已不存在,已转到上级文件夹", "보안": "安全", "보일 버튼을 고르고, 끌어서 순서를 바꿉니다.": "选择要显示的按钮,拖动以调整顺序。", diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 8874673..529ee24 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -1568,12 +1568,16 @@ class _ExplorerPageState extends State { /// 154: 항목 이름 (앞의 3개 + "외 n개") /// 48: 받는 폴더에 같은 이름이 있는 원본 (같은 폴더로 복사는 늘 새 이름이라 빼고) - static Future> _findClashes(List sources, String dest) async { - final out = []; + /// 48 · 172: 대상 폴더에 같은 이름이 있는 원본 - 두 파일의 크기 · 바뀐 날을 함께 (어느 쪽이 새것인지 보고 고르게) + static Future> _findClashes(List sources, String dest) async { + final out = []; for (final s in sources) { if (samePath(vDirname(s), dest)) continue; 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 (_) {} } return out; @@ -1711,7 +1715,7 @@ class _ExplorerPageState extends State { final builtin = method == CopyMethod.builtin || _viaDav([...sources, dest]); // 확인 창은 바로 띄우고, 같은 이름은 창 안에서 뒤에서 찾는다 (느린 네트워크 · WebDAV 폴더라도 창이 늦게 뜨지 않게). // 다 찾기 전에 누르면 이름 바꾸기 (원래 파일을 건드리지 않는 쪽) - final clashesFuture = builtin ? _findClashes(sources, dest) : Future.value(const []); + final clashesFuture = builtin ? _findClashes(sources, dest) : Future.value(const []); // 48: 환경 설정 "같은 이름이 있을 때" 가 늘 하는 것이면 묻지 않고 그대로 (확인 창에 알림만) final always = switch (c.settings.copyConflict) { 'rename' => NameConflict.rename, @@ -1734,7 +1738,7 @@ class _ExplorerPageState extends State { '\n\n${trf('방법: {0}', [tr(method.label)])}' '${method == CopyMethod.builtin ? '' : ' · ${task.options}'}' '${task.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [task.bandwidthKBps])}' : ''}'), - FutureBuilder>( + FutureBuilder>( future: clashesFuture, builder: (ctx, snap) { if (snap.connectionState != ConnectionState.done) { @@ -1743,12 +1747,18 @@ class _ExplorerPageState extends State { child: Text(tr('같은 이름이 있는지 확인하는 중 …'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)), ); } - final clashes = snap.data ?? const []; + final clashes = snap.data ?? const []; if (clashes.isEmpty) return const SizedBox.shrink(); return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ 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)), + // 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) Text( trf('환경 설정대로 {0} (환경 설정 > 파일 탐색기 > 같은 이름이 있을 때)', [ @@ -3036,6 +3046,24 @@ class _ExplorerPageState extends State { } } +/// 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) => '${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')}'; diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index e99c24d..bbfdda4 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -30,6 +30,9 @@ void main() { File(p.join(right, 'doc.txt')) ..createSync(recursive: true) ..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.settings.explorerPaths = [left, right]; final root = p.rootPrefix(tmp.path); @@ -67,6 +70,8 @@ void main() { expect(find.widgetWithText(FilledButton, '복사'), findsOneWidget); await settle(tester, () => find.textContaining('같은 이름이 이미 있습니다').evaluate().isNotEmpty); 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: 확인 창에 항목 이름 expect(find.textContaining('\ndoc.txt'), findsOneWidget); }