From f67e1e3a8575588e7802789ef70d8c512ba80fa2 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:13:09 +0900 Subject: [PATCH] =?UTF-8?q?F7=20=EB=82=AE=EC=9D=80=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8F=84=20[=EC=9D=B4=EB=8F=99]=20=EC=9D=84?= =?UTF-8?q?=20=EC=B9=A9=20=EC=A4=84=20=EC=95=84=EB=9E=98=EB=A1=9C=20=C2=B7?= =?UTF-8?q?=20F8=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EC=B9=B8=EC=9D=84=20=EC=A2=81=EA=B3=A0=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=ED=81=B4=20=EB=95=8C=EB=A7=8C=20=EC=A0=9C?= =?UTF-8?q?=EB=AA=A9=20=EC=95=84=EB=9E=98=20=EC=A4=84=EB=A1=9C=20=C2=B7=20?= =?UTF-8?q?214=20c=20=EC=8B=9C=ED=97=98=EC=9D=84=20=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=8B=9C=EC=9E=91=20=EB=92=A4=EB=A1=9C=20(?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=8A=B9=EC=9D=B8=2010/21,=2015?= =?UTF-8?q?=20=EC=B0=BE=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - F7: 폰 가로 (900×412) 에서 떠 있는 [이동] 이 [자막 파일 추가] 를 반쯤 가렸다 (잘못 누를 수 있음) → 높이 < 500 × 글자 배율 이면 206 의 배치 (칩 줄 아래) 를 씀. 처음에 높이 < 500 만 봤더니 960×600 · 글자 1.6 (ja) 에서도 가려 글자 배율을 곱함. 높이가 넉넉하면 그대로 - F8: 다운로드 화면 ja 412 글자 1.6 에서 제목이 넓어 주소 칸이 「動画...」 만큼으로 눌렸다 → 주소 칸이 200 보다 좁아질 때만 주소 칸 · [추가] 를 제목 아래 한 줄로 (글 폭은 지금 글꼴 · 배율로 잼). 이를 위해 AppTopBar 의 좁은 배치 아래 줄을 "높이 48 고정" 에서 "48 이상" 으로 (한 줄이면 예전처럼 48 가운데, 폭은 그대로 다 씀 - app_top_bar_test 201). 한 줄 막대 (넓은 화면) 에서는 늘 한 줄 - 214 c (move_settled_test): 전체 시험에서 한 번 실패 - 250ms 안에 원본 목록을 아직 읽지 않으면 새 파일까지 옮겨져 거짓 실패였다. 보내기가 실제로 시작된 뒤 새 파일을 만들고, "새 파일을 잃지 않음 (원본 또는 받는 쪽)" 을 따로 확인 - 시험: windows_mkvdetail_shot_test 에 900×412 (1.0 · 1.6) · 1280×480 과 [자막 파일 추가] 겹침 확인을 더해 18개 통과, windows_bigtext_shot_test 60개 통과 (release\test\shots_f1), 전체 단위 1150개 통과, 214 c 혼자 5번 통과 Co-Authored-By: Claude Opus 5.5 --- .../windows_mkvdetail_shot_test.dart | 10 +++-- lib/ui/app_actions.dart | 16 +++++--- lib/ui/downloads_page.dart | 39 +++++++++++++------ lib/ui/home_page.dart | 4 +- test/move_settled_test.dart | 9 ++++- 5 files changed, 57 insertions(+), 21 deletions(-) diff --git a/integration_test/windows_mkvdetail_shot_test.dart b/integration_test/windows_mkvdetail_shot_test.dart index f88de29..7139ee4 100644 --- a/integration_test/windows_mkvdetail_shot_test.dart +++ b/integration_test/windows_mkvdetail_shot_test.dart @@ -42,13 +42,17 @@ class _Tool implements MediaTool { /// F5 · 206 (관리자 지시 10/21): MKV 자세히 칸 - 오른쪽 아래 떠 있는 [이동] 단추가 마지막 줄 ("출력: …") 이나 칩을 가리지 않는다. /// 넓은 화면은 끝까지 내리면 마지막 줄이 단추 위로 (목록 끝 여백), 좁은 화면 (412) 은 단추가 칩 줄 아래에 놓인다. -/// 412 · 960×600 · 1280×720 × 글자 1.0 · 1.6 × ko · ja. 단추와 출력 줄 · 칩이 겹치는지 자리로 잰다 +/// 412 · 960×600 · 1280×720 × 글자 1.0 · 1.6 · 낮은 화면 900×412 · 1280×480 (F7) × ko · ja. [자막 파일 추가] 도 단추에 가리지 않는다. 단추와 출력 줄 · 칩이 겹치는지 자리로 잰다 void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final out = Platform.environment['JJ_SHOT_DIR'] ?? ''; for (final lang in ['ko', 'ja']) { - for (final (w, h, scale) in [(412, 900, 1.0), (412, 900, 1.6), (960, 600, 1.0), (960, 600, 1.6), (1280, 720, 1.0), (1280, 720, 1.6)]) { + for (final (w, h, scale) in [ + (412, 900, 1.0), (412, 900, 1.6), (960, 600, 1.0), (960, 600, 1.6), (1280, 720, 1.0), (1280, 720, 1.6), + // F7 (10/21): 높이가 낮은 화면 (폰 가로 · 낮은 창) - [이동] 이 [자막 파일 추가] 를 가렸다 + (900, 412, 1.0), (900, 412, 1.6), (1280, 480, 1.0), + ]) { testWidgets('MKV 자세히 칸 이동 단추 $lang ${w}x$h 글자 $scale', (t) async { t.view.physicalSize = Size(w.toDouble(), h.toDouble()); t.view.devicePixelRatio = 1; @@ -105,7 +109,7 @@ void main() { // 206: 처음 자리에서 칩 (음성 n개 등) 이 [이동] 단추에 가리지 않음 (좁은 화면은 단추가 칩 줄 아래에 놓임) if (fab.evaluate().isNotEmpty) { final b = t.getRect(fab.first); - for (final text in [trf('음성 {0}개', [2]), trf('영상 {0}', ['h264']), '1920×1080']) { + for (final text in [trf('음성 {0}개', [2]), trf('영상 {0}', ['h264']), '1920×1080', tr('자막 파일 추가')]) { if (find.text(text).evaluate().isEmpty) continue; final r = t.getRect(find.text(text)); expect(r.overlaps(b), isFalse, reason: '칩 "$text" 이 단추에 가리지 않음 ($r · 단추 $b)'); diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 866463d..a752feb 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -103,11 +103,17 @@ class _AppTopBarState extends State { actions, ]), ), - SizedBox( - height: 48, - child: Padding( - padding: const EdgeInsets.only(left: 6), - child: widget.trailing == null ? middle : Row(children: [Expanded(child: middle), widget.trailing!]))), + // F8 (10/21): 아래 줄은 48 이상 - middle 이 좁아 두 줄로 바꾸면 (다운로드 화면의 주소 칸) 그만큼 늘어난다. 한 줄이면 예전처럼 48 가운데 + ConstrainedBox( + constraints: const BoxConstraints(minHeight: 48), + child: Align( + alignment: Alignment.centerLeft, + heightFactor: 1, + child: SizedBox( + width: double.infinity, // 아래 줄은 가운데가 폭을 다 쓴다 (201) + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: widget.trailing == null ? middle : Row(children: [Expanded(child: middle), widget.trailing!]))))), ]), ); } diff --git a/lib/ui/downloads_page.dart b/lib/ui/downloads_page.dart index b25bd4e..834c185 100644 --- a/lib/ui/downloads_page.dart +++ b/lib/ui/downloads_page.dart @@ -93,12 +93,10 @@ class _DownloadsPageState extends State { minMiddleWidth: 420, nav: const AppNavButtons(onDownloadsPage: true), actions: const AppActions(), - middle: Row(children: [ - Text(trf('다운로드 ({0})', [d.tasks.length]), - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), - SizedBox(width: isCompact(context) ? 8 : 20), - Expanded( - child: TextField( + middle: LayoutBuilder(builder: (context, box) { + final title = Text(trf('다운로드 ({0})', [d.tasks.length]), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)); + final field = TextField( controller: _url, style: const TextStyle(fontSize: 13), decoration: InputDecoration( @@ -110,11 +108,30 @@ class _DownloadsPageState extends State { border: OutlineInputBorder(), ), onSubmitted: (_) => _add(), - ), - ), - const SizedBox(width: 8), - FilledButton(onPressed: _add, child: Text(tr('추가'))), - ]), + ); + final add = FilledButton(onPressed: _add, child: Text(tr('추가'))); + // F8 (관리자 승인 10/21, 15 찾음): 좁고 글자가 크면 제목이 넓어 주소 칸이 「動画...」 만큼으로 눌렸다 - + // 주소 칸이 200 보다 좁아질 때만 주소 칸 · [추가] 를 제목 아래 한 줄로 (넓으면 예전 한 줄 그대로) + final scaler = MediaQuery.textScalerOf(context); + double textW(String t, double size, [FontWeight? w]) => (TextPainter( + text: TextSpan(text: t, style: DefaultTextStyle.of(context).style.copyWith(fontSize: size, fontWeight: w)), + textDirection: TextDirection.ltr, + textScaler: scaler, + maxLines: 1) + ..layout()) + .width; + final gap = isCompact(context) ? 8.0 : 20.0; + final need = textW(trf('다운로드 ({0})', [d.tasks.length]), 16, FontWeight.w600) + gap + 200 + 8 + textW(tr('추가'), 14) + 48; + // 한 줄 막대 (높이가 정해짐) 에서는 늘 한 줄 - 모자라면 막대가 스스로 두 줄 배치로 바뀐다 (AppTopBar) + if (box.maxWidth >= need || box.maxHeight.isFinite) { + return Row(children: [title, SizedBox(width: gap), Expanded(child: field), const SizedBox(width: 8), add]); + } + return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + title, + const SizedBox(height: 6), + Row(children: [Expanded(child: field), const SizedBox(width: 8), add]), + ]); + }), ), Container( color: JjColors.bg, diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index ae7504a..f311f51 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -1103,7 +1103,9 @@ class _VideoDetail extends StatelessWidget { ), ); // 206: 좁은 화면 (폰 세로) 은 자세히 칸이 낮아 떠 있는 버튼이 칩 (음성 n개 등) 을 가렸다 - 버튼을 칩 줄 아래에 놓는다 - if (isCompact(context)) { + // F7 (관리자 승인 10/21, 15 찾음): 높이가 낮은 화면 (폰 가로 등) 도 - 떠 있는 버튼이 [자막 파일 추가] 를 반쯤 가렸다. + // 높이는 글자 배율로 나눠 본다 (글자가 크면 같은 높이에 들어가는 줄이 적다 - 960×600 · 글자 1.6 에서도 가렸다) + if (isCompact(context) || MediaQuery.sizeOf(context).height < 500 * MediaQuery.textScalerOf(context).scale(1)) { final row = Wrap(alignment: WrapAlignment.end, spacing: 10, runSpacing: 10, children: [ if (buttons.isEmpty) button(null), for (final t in buttons) button(t), diff --git a/test/move_settled_test.dart b/test/move_settled_test.dart index 80d635c..334231a 100644 --- a/test/move_settled_test.dart +++ b/test/move_settled_test.dart @@ -55,12 +55,19 @@ void main() { File(p.join(src, 'big.bin')).writeAsBytesSync(List.filled(300 * 1024, 7)); final job = TransferJob(sources: [src], dest: dst, move: true, contents: true, bandwidthKBps: 400); final running = job.run(); - await Future.delayed(const Duration(milliseconds: 250)); + // 정해진 시간 (예전 250ms) 이 아니라 보내기가 실제로 시작된 뒤 (받는 쪽에 무엇이 생긴 뒤) 새 파일을 만든다 - + // 전체 시험 부하에서는 250ms 안에 원본 목록을 아직 읽지 않아 새 파일까지 옮겨져 거짓 실패 (10/21, 15) + for (var i = 0; i < 200; i++) { + if (Directory(dst).existsSync() && Directory(dst).listSync(recursive: true).isNotEmpty) break; + await Future.delayed(const Duration(milliseconds: 25)); + } File(p.join(src, 'arrived.txt')).writeAsStringSync('new'); await running; expect(job.error, isNull); expect(File(p.join(dst, 'big.bin')).lengthSync(), 300 * 1024); expect(File(p.join(src, 'big.bin')).existsSync(), isFalse); + // 데이터: 새 파일은 어디에도 없어지면 안 된다 (원본에 남았거나 옮겨졌거나) + expect(File(p.join(src, 'arrived.txt')).existsSync() || File(p.join(dst, 'arrived.txt')).existsSync(), isTrue, reason: '새 파일을 잃지 않음'); expect(File(p.join(src, 'arrived.txt')).existsSync(), isTrue, reason: '복사하지 않은 것은 남김'); });