52 복사 · 이동 진행 막대: 화면을 떠났다 돌아와도 다시 보이고, 끝나면 받는 폴더를 새로 읽음

- 화면을 열 때 CopyCenter 에서 아직 도는 작업을 찾아 진행 막대를 다시 붙이고, 끝나면 받는 · 원본 폴더를 새로 읽은 뒤 내린다
  (.jjpart 가 남아 보이지 않게)
- 시험: 속도 제한으로 오래 걸리는 복사 도중 화면을 없앴다가 다시 열기 (수정 전 코드에서는 실패)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-09 18:41:03 +09:00
parent f01fe96151
commit 1c919ce8c6
2 changed files with 49 additions and 0 deletions

View File

@ -285,6 +285,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
}
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<ExplorerPage> {
/// (Rsync 화면의 ⇄ 는 둘이 함께)
List<TransferJob> _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<void>.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<String> paths) => paths.any(isDav);

View File

@ -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('건너뛰기'));