diff --git a/lib/app/live_sync.dart b/lib/app/live_sync.dart index daef84c..4e14cf8 100644 --- a/lib/app/live_sync.dart +++ b/lib/app/live_sync.dart @@ -322,6 +322,9 @@ class LiveSync extends ChangeNotifier { if (sig == _sig) return; _sig = sig; _stopAll(); + // 258: 지우거나 끈 · 멈춘 쌍의 "원본을 읽지 못해 멈춤" 은 지운다 (작업 알림 "⚠ 동기화 멈춤 N개" 가 앱을 다시 켤 때까지 남았다) + final keys = pairs.map(keyOf).toSet(); + problems.removeWhere((k, _) => !keys.contains(k)); for (final x in pairs) { final k = keyOf(x); if (!isDav(x.source) && !Directory(x.source).existsSync()) { diff --git a/lib/ui/copy_sync_settings.dart b/lib/ui/copy_sync_settings.dart index 42b3d48..2533172 100644 --- a/lib/ui/copy_sync_settings.dart +++ b/lib/ui/copy_sync_settings.dart @@ -517,9 +517,9 @@ class LiveSyncRunOptions extends StatelessWidget { ]), ), ), - Row(mainAxisSize: MainAxisSize.min, children: [ + // 257: 폰 세로에서 넘쳤다 - 모자라면 고르기 상자를 아래 줄로 + Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text(tr('다시 켤 때 동기화'), style: const TextStyle(fontSize: 13, color: JjColors.textDim)), - const SizedBox(width: 8), onStart, ]), ]); diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index 373caff..c24f764 100644 --- a/lib/ui/monitor_page.dart +++ b/lib/ui/monitor_page.dart @@ -642,8 +642,8 @@ class _CopyCardState extends State<_CopyCard> { ]), ), const SizedBox(height: 6), - Row(children: [ - const Spacer(), + // 257: 폰 세로에서 버튼이 화면 밖으로 넘치지 않게 줄을 바꾼다 (오른쪽 정렬은 그대로) + Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 2, runSpacing: 2, children: [ if (running) TextButton.icon(onPressed: () => center.cancel(t.id), icon: const Icon(Icons.stop, size: 18), label: Text(tr('취소'))) else @@ -652,7 +652,6 @@ class _CopyCardState extends State<_CopyCard> { icon: const Icon(Icons.play_arrow, size: 18), label: Text(t.lastResult == 'cancelled' || t.lastResult == 'failed' ? tr('재개') : tr('실행')), ), - const SizedBox(width: 6), TextButton.icon( onPressed: () => _toLive(t), icon: const Icon(Icons.sync_alt, size: 18), @@ -959,10 +958,17 @@ class _LiveTabState extends State<_LiveTab> { ), ), ), - Chip( - visualDensity: VisualDensity.compact, - // 243: 실시간 동기화는 늘 복사로 맞춘다 (원본은 지우지 않음 - 원본을 비우려면 [최종 정리]) - label: Text([tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · ')), + // 257: 폰 세로에서 이 줄이 넘쳤다 - 칩은 남는 폭만큼 (넘치면 … 로 줄임, 전체는 길게 누르기 · 마우스) + Flexible( + child: Tooltip( + message: [tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · '), + child: Chip( + visualDensity: VisualDensity.compact, + // 243: 실시간 동기화는 늘 복사로 맞춘다 (원본은 지우지 않음 - 원본을 비우려면 [최종 정리]) + label: Text([tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · '), + maxLines: 1, overflow: TextOverflow.ellipsis), + ), + ), ), // 이번 실행 동안 멈춤 / 시작 (설정의 켜기 · 끄기는 오른쪽 스위치) if (live != null && x.enabled) @@ -1008,12 +1014,15 @@ class _LiveTabState extends State<_LiveTab> { child: Row(mainAxisSize: MainAxisSize.min, children: [ WeekGridPreview(lines: x.schedule, width: 120), const SizedBox(width: 8), - Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : Colors.orangeAccent)), - if (x.schedule.isNotEmpty) - Text(x.schedule.join(' | '), style: const TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)), - Text(tr('누르면 일정 바꾸기'), style: const TextStyle(fontSize: 10, color: JjColors.textDim)), - ]), + // 257: 폰 세로에서 글이 줄을 바꿀 수 있게 (넘쳤다) + Flexible( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : Colors.orangeAccent)), + if (x.schedule.isNotEmpty) + Text(x.schedule.join(' | '), style: const TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)), + Text(tr('누르면 일정 바꾸기'), style: const TextStyle(fontSize: 10, color: JjColors.textDim)), + ]), + ), ]), ), ), @@ -1060,8 +1069,8 @@ class _LiveTabState extends State<_LiveTab> { ]), ), const SizedBox(height: 6), - Row(children: [ - const Spacer(), + // 257: 폰 세로에서 [고치기] · [지우기] 가 화면 밖으로 밀려 누를 수 없었다 - 줄을 바꾼다 (오른쪽 정렬은 그대로) + Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 2, runSpacing: 2, children: [ TextButton.icon( onPressed: live == null ? null : () => live.refreshPending(x), icon: const Icon(Icons.refresh, size: 18), @@ -1072,7 +1081,6 @@ class _LiveTabState extends State<_LiveTab> { icon: const Icon(Icons.sync, size: 18), label: Text(tr('지금 맞추기')), ), - const SizedBox(width: 6), TextButton.icon( onPressed: running ? null : () => _finalize(x), icon: const Icon(Icons.cleaning_services_outlined, size: 18), diff --git a/test/sync_problem_test.dart b/test/sync_problem_test.dart index 6cee69e..6f3f4a1 100644 --- a/test/sync_problem_test.dart +++ b/test/sync_problem_test.dart @@ -61,6 +61,18 @@ void main() { live.dispose(); }); + test('258: 멈춘 쌍을 지우면 "동기화 멈춤" 도 바로 사라진다 (앱을 다시 켜지 않아도)', () async { + final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true); + c.settings.liveSyncPairs = [pair]; + final live = LiveSync(c)..start(); + final k = LiveSync.keyOf(pair); + await live.syncNow(pair); + expect(live.problems[k], isNotNull); + await c.updateSettings((s) => s.liveSyncPairs = []); + expect(live.problems, isEmpty); + live.dispose(); + }); + test('70: 원본이 정말 비었으면 멈추고 지울 목록 → [그래도 맞추기] 로 그 한 번만 지운다', () async { Directory(src).createSync(); final pair = LiveSyncPair(src, dst, delete: true).copyWith(deleteConfirmed: true); @@ -138,6 +150,33 @@ void main() { expect(find.text('원본 폴더가 비어 있어 멈췄습니다'), findsNothing); }); + testWidgets('257: 폰 세로 (411 폭) 에서 lsync 카드의 [고치기] · [지우기] 가 화면 안에 있어 누를 수 있다', (tester) async { + Directory(src).createSync(); + File(p.join(src, 'x.txt')).writeAsStringSync('x'); + final pair = LiveSyncPair(src, dst); + c.settings.liveSyncPairs = [pair]; + final live = LiveSync(c); + LiveSync.instance = live; + addTearDown(() { + LiveSync.instance = null; + live.dispose(); + }); + tester.view.physicalSize = const Size(411, 800); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget(MaterialApp(home: MonitorPage(c: c, initialTab: 1))); + await tester.pumpAndSettle(); + expect(tester.takeException(), isNull, reason: '넘치지 않음'); + for (final tip in ['다시 세기', '지금 맞추기', '최종 정리', '복사 · rsync 로 이동']) { + final r = tester.getRect(find.text(tip)); + expect(r.right <= 411 && r.left >= 0, isTrue, reason: '$tip $r'); + } + for (final tip in ['고치기', '지우기']) { + final r = tester.getRect(find.byTooltip(tip).first); + expect(r.right <= 411 && r.left >= 0, isTrue, reason: '$tip $r 이 화면 밖'); + } + }); + testWidgets('93: Rsync --delete 실행은 비교가 끝나 지울 목록이 보인 뒤에만 [실행] 이 켜진다', (tester) async { final l = p.join(tmp.path, 'L'), r = p.join(tmp.path, 'R'); File(p.join(l, 'sub', 'a.txt'))