diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 60602e7..33e7fba 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -285,6 +285,13 @@ class _ExplorerPageState extends State { } if (!mounted) return; setState(() => _ready = true); + // 52: 다른 화면에 다녀와도 아직 도는 복사 · 이동의 진행 막대를 다시 보이고, 끝나면 그 폴더들을 새로 읽는다 + final center = CopyCenter.peekOf(c); + if (center != null) { + for (final job in center.jobs.values.where((j) => !j.finished)) { + _adoptJob(job); + } + } // 165: 드라이브 종류 · 연결은 드라이브마다 따로 뒤에서 (느린 드라이브가 있어도 다른 드라이브 · 창은 바로) if (Platform.isWindows) { for (final (path, _) in _local) { @@ -1446,6 +1453,21 @@ class _ExplorerPageState extends State { /// (Rsync 화면의 ⇄ 는 둘이 함께) List _jobs = const []; + /// 52: 이 화면을 떠난 사이에 시작된 (또는 앞의 화면이 시작한) 작업을 다시 보이고, 끝나면 새로 읽고 막대를 내린다 + void _adoptJob(TransferJob job) { + if (_jobs.contains(job)) return; + setState(() => _jobs = [..._jobs, job]); + unawaited(job.done.then((_) async { + if (!mounted) return; + await _refreshAll([job.dest, ...job.sources.map(vDirname)]); + for (final x in _panes) { + x.changed(); + } + await Future.delayed(const Duration(milliseconds: 1200)); + if (mounted && _jobs.contains(job)) setState(() => _jobs = [for (final j in _jobs) if (j != job) j]); + })); + } + /// 한쪽이라도 WebDAV 면 rsync 대신 앱이 직접 맞춘다 (rsync 는 WebDAV 를 모름) bool _viaDav(Iterable paths) => paths.any(isDav); diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index c1e90d8..44cbbee 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -105,6 +105,33 @@ void main() { expect(Directory(p.join(right, 'left')).existsSync(), isFalse); }); + testWidgets('52: 복사 중에 화면을 떠났다 돌아와도 진행 막대가 다시 보이고, 끝나면 받는 폴더를 새로 읽는다 (.jjpart 가 남아 보이지 않음)', (tester) async { + File(p.join(left, 'big.bin')).writeAsBytesSync(List.filled(150 * 1024, 3)); + c.settings.copyBandwidthKBps = 60; // 약 2.5초 걸리게 + 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('big.bin').evaluate().isNotEmpty); + await tester.runAsync(() => tester.tap(find.text('big.bin'))); + await settle(tester, () => false, rounds: 10); + await tester.runAsync(() => tester.tap(find.text('복사').first)); + await tester.pump(); + await tester.runAsync(() => tester.tap(find.widgetWithText(FilledButton, '복사'))); + await settle(tester, () => find.byType(LinearProgressIndicator).evaluate().isNotEmpty); + // 다른 화면으로 (탐색기 화면이 없어짐) → 돌아옴 + await tester.runAsync(() => tester.pumpWidget(const MaterialApp(home: Scaffold(body: Text('다른 화면'))))); + await tester.pump(); + await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); + await settle(tester, () => find.byType(LinearProgressIndicator).evaluate().isNotEmpty, rounds: 30); + expect(find.byType(LinearProgressIndicator), findsWidgets, reason: '돌아와도 진행 막대'); + // 끝나면 오른쪽 창에 big.bin 이 보이고 .jjpart 는 없다 + await settle(tester, () => File(p.join(right, 'big.bin')).existsSync() && find.text('big.bin').evaluate().length >= 2, + rounds: 300); + expect(find.text('big.bin'), findsNWidgets(2)); + expect(find.textContaining('.jjpart'), findsNothing); + }); + testWidgets('48: 건너뛰기를 고르면 그대로 두고 건너뛴 것을 알린다', (tester) async { await startCopy(tester); await tester.tap(find.text('건너뛰기'));