From 0bd14662120c71196645835a41e5f60b1538c154 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:50:49 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=ED=99=94=EB=A9=B4=20(=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=A7=80=EC=8B=9C=2010/20,=2028=20=EC=A7=80?= =?UTF-8?q?=EC=A0=81):=20=EA=B2=BD=EA=B3=A0=20=EA=B8=80=20=EC=95=88?= =?UTF-8?q?=EC=9D=98=20=EC=98=B5=EC=85=98=20(--filter=3DH=20a.txt)=20?= =?UTF-8?q?=EC=9D=84=20=EC=A4=84=EB=B0=94=EA=BF=88=20=EC=97=86=EC=9D=B4=20?= =?UTF-8?q?=EB=AC=B6=EC=9D=8C=20=C2=B7=20=EC=A2=81=EC=9D=80=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=EB=8A=94=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20"rsync=20=C2=B7=20=EB=B3=B5=EC=82=AC=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4=20=C2=B7=20=EC=A7=80=EC=9A=B0=EA=B8=B0=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8"=20=EC=B9=A9=EC=9D=84=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=95=84=EB=9E=98=20=ED=95=9C=20=EC=A4=84=EB=A1=9C=20(?= =?UTF-8?q?=EC=9E=98=EB=A6=AC=EB=8D=98=20=EA=B2=83)=20(=EA=B2=80=ED=86=A0?= =?UTF-8?q?=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- lib/ui/monitor_page.dart | 29 ++++++++++++++++------------- lib/ui/sync_preview_view.dart | 6 +++++- test/unbreakable_option_test.dart | 23 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 test/unbreakable_option_test.dart diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index 1ef0b3d..5e00aa8 100644 --- a/lib/ui/monitor_page.dart +++ b/lib/ui/monitor_page.dart @@ -21,7 +21,7 @@ import 'path_label.dart'; import 'rsync_setup.dart'; import 'schedule_editor.dart'; import '../core/sync_preview.dart' show optionsRemoveSource; -import 'sync_preview_view.dart' show FilterLimitNote, confirmDeletingRun, effectiveRunText; +import 'sync_preview_view.dart' show FilterLimitNote, confirmDeletingRun, effectiveRunText, unbreakableOption; import 'theme.dart'; /// 모니터링 (파일 탐색기 > 모니터링): 복사 · rsync 와 lsync (실시간 동기화) 를 따로 보여 준다. @@ -969,6 +969,18 @@ class _LiveTabState extends State<_LiveTab> { final active = LiveSync.activeNow(x); final running = live?.isRunning(x) ?? false; final held = live?.isPaused(x) ?? false; + // 257: 칩은 길게 누르기 · 마우스로 전체를 본다. 관리자 지시 (10/20, 28 지적): 좁은 화면 (폰 세로) 에서는 경로 줄에 끼면 + // "rsync · 복…" 으로 잘려 경로 아래 따로 한 줄로 (넓으면 예전처럼 경로 옆) + final narrow = MediaQuery.sizeOf(context).width < 600; + final chipText = [tr(CopyMethod.of(x.method).label), tr('복사로 맞춤'), if (x.delete) tr('지우기 포함')].join(' · '); + final chip = Tooltip( + message: chipText, + child: Chip( + visualDensity: VisualDensity.compact, + // 243: 실시간 동기화는 늘 복사로 맞춘다 (원본은 지우지 않음 - 원본을 비우려면 [최종 정리]) + label: Text(chipText, maxLines: 1, overflow: TextOverflow.ellipsis), + ), + ); return Card( color: JjColors.panel, margin: const EdgeInsets.only(bottom: 10), @@ -1006,17 +1018,7 @@ class _LiveTabState extends State<_LiveTab> { ), ), // 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 (!narrow) Flexible(child: chip), // 이번 실행 동안 멈춤 / 시작 (설정의 켜기 · 끄기는 오른쪽 스위치) if (live != null && x.enabled) IconButton( @@ -1026,6 +1028,7 @@ class _LiveTabState extends State<_LiveTab> { ), Switch(value: x.enabled, onChanged: (v) => _replace(x, x.copyWith(enabled: v))), ]), + if (narrow) Padding(padding: const EdgeInsets.only(left: 32, top: 2), child: chip), if (held) Padding( padding: const EdgeInsets.only(left: 32, top: 2), @@ -1054,7 +1057,7 @@ class _LiveTabState extends State<_LiveTab> { const Icon(Icons.block, size: 16, color: Colors.redAccent), Text(tr('이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다'), style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), - Text('(${live.noAutoDelete[k]!.join(' · ')})', style: const TextStyle(fontSize: 12, color: Colors.redAccent)), + Text('(${live.noAutoDelete[k]!.map(unbreakableOption).join(' · ')})', style: const TextStyle(fontSize: 12, color: Colors.redAccent)), OutlinedButton( onPressed: () => confirmLiveDeleteNow(context, live, x), child: Text(tr('지금 지우기 (확인)')), diff --git a/lib/ui/sync_preview_view.dart b/lib/ui/sync_preview_view.dart index a2ca922..3235a1c 100644 --- a/lib/ui/sync_preview_view.dart +++ b/lib/ui/sync_preview_view.dart @@ -307,6 +307,10 @@ Future confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM /// 15 검토 F2 (177): rsync 옵션에 미리 볼 수 없는 거르기 규칙 (고치개 · H · P · merge · -F · -C · 읽지 못한 파일 등) 이 있으면 /// 지울 목록이 실제보다 적게 보일 수 있다 - 빨갛게 알리고, 지우기 (--delete) 와 함께면 체크해야 [실행] 이 켜진다 (72 · 104 와 같게) +/// 옵션 글 (예: --filter=H a.txt) 이 좁은 창에서 "--" 와 "filter=…" 로 갈리지 않게 줄바꿈 없이 묶는다 (관리자 지시 10/20, 28 지적): +/// 빈칸은 줄바꿈 없는 빈칸, - · / 뒤에는 줄바꿈을 막는 글자 (U+2060, 보이지 않음) +String unbreakableOption(String s) => s.replaceAll(' ', ' ').replaceAllMapped(RegExp(r'[-/]'), (m) => '${m[0]}⁠'); + class FilterLimitNote extends StatelessWidget { final List unsupported; final bool deletes; @@ -316,7 +320,7 @@ class FilterLimitNote extends StatelessWidget { @override Widget build(BuildContext context) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(trf('이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}', [unsupported.join(' · ')]), + Text(trf('이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}', [unsupported.map(unbreakableOption).join(' · ')]), style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), if (deletes) CheckboxListTile( diff --git a/test/unbreakable_option_test.dart b/test/unbreakable_option_test.dart new file mode 100644 index 0000000..d2678ee --- /dev/null +++ b/test/unbreakable_option_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/ui/sync_preview_view.dart'; + +/// 관리자 지시 (10/20, 28 지적): 경고 글 안의 옵션 (--filter=H a.txt) 이 좁은 창에서 "--" 와 "filter=…" 로 갈리던 것 +void main() { + double widest(String s, {bool min = true}) { + final tp = TextPainter(text: TextSpan(text: s, style: const TextStyle(fontSize: 12)), textDirection: TextDirection.ltr)..layout(); + final w = min ? tp.minIntrinsicWidth : tp.maxIntrinsicWidth; + tp.dispose(); + return w; + } + + test('옵션 글은 줄바꿈 자리가 없다 (가장 좁게 접어도 한 줄 폭)', () { + for (final o in ['--filter=H a.txt', '-f"- *.log"', '--files-from=list/a b.txt', '-C']) { + final u = unbreakableOption(o); + expect(widest(u), closeTo(widest(u, min: false), 0.01), reason: o); + expect(u.replaceAll('⁠', '').replaceAll(' ', ' '), o, reason: '보이는 글은 그대로'); + } + // 묶지 않은 글은 줄이 갈릴 수 있다 (시험이 실제로 무언가를 보는지) + expect(widest('--filter=H a.txt'), lessThan(widest('--filter=H a.txt', min: false))); + }); +}