diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index b5f3afb..30bd57f 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -1407,6 +1407,30 @@ class _ExplorerPageState extends State { } } + /// 265: 복사 · 이동 뒤 - 대상 · 원본의 위 폴더와, 펼쳐 둔 원본 · 대상 안쪽까지 두 창 모두 다시 읽는다. + /// (예전에는 원본의 위 폴더만 읽어, 합치다 남은 원본 폴더 아래에 옮겨진 파일이 계속 보였다) 없어진 폴더는 접는다 + Future _refreshAfterTransfer(List sources, String dest) async { + for (final x in _panes) { + final dirs = { + dest, + ...sources.map(vDirname), + for (final k in x.cache.keys) + if (isSameOrInside(k, dest) || sources.any((s) => isSameOrInside(k, s))) k, + }; + for (final d in dirs) { + if (!mounted) return; + if (!x.cache.containsKey(d)) continue; + if (!await vExists(d)) { + x.cache.remove(d); + x.expanded.remove(d); + continue; + } + // 180: 비우지 않고 새로 읽은 것으로 바꾼다 (자리 · 스크롤 그대로) + await _load(x, d, force: true); + } + } + } + /// 바뀐 폴더를 두 창 모두 다시 읽는다 Future _refreshAll(Iterable dirs) async { for (final pane in _panes) { @@ -2050,7 +2074,7 @@ class _ExplorerPageState extends State { if (!mounted) return false; // 180: 표시만 지우고 선택 모드는 그대로 (꺼지면 동그라미 칸 · 아래 막대가 사라져 자리가 바뀐다) pane.marked.clear(); - await _refreshAll([dest, ...sources.map(p.dirname)]); + await _refreshAfterTransfer(sources, dest); // 넣은 폴더가 창의 지금 폴더면 펼쳐서 새 항목이 보이게 for (final x in _panes) { if (samePath(x.current, dest)) { @@ -2065,6 +2089,9 @@ class _ExplorerPageState extends State { ? trf('끝나지 못했습니다: {0}', [error]) : [ trf('{0}개 항목을 {1}', [job.made.length, move ? tr('옮겼습니다.') : tr('복사했습니다.')]), + // 264: 작업 기록에만 있던 것도 알림에 (폴더를 합칠 때 받는 쪽이 더 새 파일 등) + if (move && job.leftInSource > 0) trf('{0}개는 원본에 남음 (건너뜀)', [job.leftInSource]), + if (!move && job.keptNewer > 0) trf('받는 쪽이 더 새것 {0}개 건너뜀', [job.keptNewer]), if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뜀: {0}', [_namesOf(job.skipped)]), if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)]), ].join(' · ')); diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index 35cbc3c..b3bae2a 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -108,6 +108,38 @@ void main() { expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'new'); }); + testWidgets('264 · 265: 폴더를 합쳐 옮긴 뒤 - 알림에 "원본에 남음", 펼쳐 둔 원본 폴더는 다시 읽어 옮겨진 파일이 보이지 않는다', (tester) async { + final d = p.join(left, 'D'); + File(p.join(d, 'same.txt')) + ..createSync(recursive: true) + ..writeAsStringSync('old') + ..setLastModifiedSync(DateTime(2026, 1, 1)); + File(p.join(d, 'add.txt')).writeAsStringSync('a'); + File(p.join(right, 'D', 'same.txt')) + ..createSync(recursive: true) + ..writeAsStringSync('NEWER'); + c.settings.copyConflict = 'overwrite'; + 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('D').evaluate().length >= 2); + // 왼쪽 D 를 눌러 펼치고 (고른 것) 안의 add.txt 가 보이게 + await tester.runAsync(() => tester.tap(find.text('D').first)); + await settle(tester, () => find.text('add.txt').evaluate().isNotEmpty); + expect(find.text('add.txt'), findsOneWidget); + await tester.runAsync(() => tester.tap(find.text('이동').first)); + await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '이동'))); + await settle(tester, () => find.byType(LinearProgressIndicator).evaluate().isEmpty && find.byType(SnackBar).evaluate().isNotEmpty); + expect(File(p.join(right, 'D', 'add.txt')).existsSync(), isTrue); + expect(File(p.join(d, 'same.txt')).existsSync(), isTrue, reason: '받는 쪽이 더 새것이라 원본에 남음'); + expect(find.textContaining('1개는 원본에 남음 (건너뜀)'), findsOneWidget, reason: '264: 알림에도'); + expect(find.text('add.txt'), findsNothing, reason: '265: 원본 D 를 다시 읽음 (오른쪽 D 는 펼치지 않음)'); + expect(find.text('same.txt'), findsOneWidget); + await calm(tester); + }); + testWidgets('48 보충: 처음 열면 아무것도 고르지 않은 상태 - 바로 [복사] 를 누르면 "복사할 항목을 고르세요" (폴더 통째로 복사하지 않음)', (tester) async { tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1;