From 2d2f7055baadb0aff7297913810a72dd6c414d47 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 06:54:25 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20253=20=EA=B8=80=EC=9E=90=20=ED=81=AC?= =?UTF-8?q?=EA=B2=8C=20(1.6=EB=B0=B0)=20=ED=95=B4=EB=8F=84=20=EB=84=98?= =?UTF-8?q?=EC=B9=98=EC=A7=80=20=EC=95=8A=EA=B2=8C:=20=ED=83=90=EC=83=89?= =?UTF-8?q?=EA=B8=B0=20=EC=A4=84=20=EB=86=92=EC=9D=B4=20=C2=B7=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=83=81=EC=9E=90=20?= =?UTF-8?q?=C2=B7=20=EC=9C=84=20=EB=A7=89=EB=8C=80=20=C2=B7=20=EC=9D=B8?= =?UTF-8?q?=EC=BD=94=EB=94=A9=20=C2=B7=20=EB=AA=A8=EB=8B=88=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=C2=B7=20=EC=96=B8=EC=96=B4=20=C2=B7=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A0=84=EC=B2=B4=20=EC=8B=9C=ED=97=98=20(?= =?UTF-8?q?=EA=B2=80=ED=86=A0=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/app_actions.dart | 14 +++++-- lib/ui/explorer_look.dart | 6 +++ lib/ui/explorer_page.dart | 10 ++--- lib/ui/home_page.dart | 12 +++++- lib/ui/language_settings.dart | 10 ++++- lib/ui/monitor_page.dart | 4 +- lib/ui/setting_tile.dart | 7 +++- test/large_text_test.dart | 69 +++++++++++++++++++++++++++++++++++ 8 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 test/large_text_test.dart diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 83aa61e..866463d 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -360,10 +360,16 @@ class AppNavButtons extends StatelessWidget { child: icon, ), const SizedBox(height: 2), - Text(label, - maxLines: 1, - overflow: TextOverflow.clip, - style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)), + // 253: 글자가 크면 칸 (높이 46) 아래로 넘쳤다 - 남는 높이에 맞게 줄인다 + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text(label, + maxLines: 1, + overflow: TextOverflow.clip, + style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)), + ), + ), ]), ), ), diff --git a/lib/ui/explorer_look.dart b/lib/ui/explorer_look.dart index e1919ae..17da758 100644 --- a/lib/ui/explorer_look.dart +++ b/lib/ui/explorer_look.dart @@ -21,6 +21,12 @@ enum ExplorerStyle { static ExplorerStyle of(String name) => values.firstWhere((s) => s.name == name, orElse: () => xplore); double get rowHeight => switch (this) { xplore => 52, windows => 34, totalcmd => 26 }; + + /// 253: 줄 높이 - 기기 글자 크기를 키우면 그만큼 (고정 높이라 1.6배에서 이름 · 크기 두 줄이 넘쳤다) + double rowHeightOf(BuildContext context) { + final s = MediaQuery.textScalerOf(context).scale(fontSize) / fontSize; + return s <= 1 ? rowHeight : (rowHeight * s).roundToDouble(); + } double get iconSize => switch (this) { xplore => 30, windows => 20, totalcmd => 16 }; double get indent => switch (this) { xplore => 16, windows => 14, totalcmd => 12 }; double get fontSize => switch (this) { xplore => 14, windows => 13, totalcmd => 12.5 }; diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index fc1a415..ba30631 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -782,7 +782,7 @@ class _ExplorerPageState extends State { final pos = pane.scroll.position; // 180: 끝 (maxScrollExtent) 으로 자르지 않는다 - 줄이 막 늘어난 때는 끝이 아직 앞의 것이라 보던 폴더로 못 간다. // 넘치면 다음 배치 때 실제 끝으로 맞춰진다 - pane.scroll.jumpTo((i * _look.rowHeight - pos.viewportDimension / 4).clamp(0.0, double.infinity)); + pane.scroll.jumpTo((i * _look.rowHeightOf(context) - pos.viewportDimension / 4).clamp(0.0, double.infinity)); }); } @@ -3094,7 +3094,7 @@ class _ExplorerPageState extends State { ListView.builder( controller: pane.scroll, itemCount: rows.length, - itemExtent: look.rowHeight, + itemExtent: look.rowHeightOf(context), itemBuilder: (_, k) => _rowView(pane, rows[k], compact: foldersOnly), ), // 읽는 중 (WebDAV 서버를 기다릴 때 등). 180: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다) @@ -3135,7 +3135,7 @@ class _ExplorerPageState extends State { : ListView.builder( controller: pane.listScroll, itemCount: rows.length, - itemExtent: look.rowHeight, + itemExtent: look.rowHeightOf(context), itemBuilder: (_, k) => _rowView(pane, rows[k], list: true), ), ), @@ -3383,7 +3383,7 @@ class _ExplorerPageState extends State { // 손가락으로 누르는 기기 (Android) 는 폭도 넓게 Widget chevron() => SizedBox( width: chevronWidth, - height: look.rowHeight, + height: look.rowHeightOf(context), child: e.isDir && !list && !row.isUp ? (loading ? const Center( @@ -3414,7 +3414,7 @@ class _ExplorerPageState extends State { if (!list) chevron(), SizedBox( width: look.rich ? 56 : look.iconSize + 6, - height: look.rowHeight - 8, + height: look.rowHeightOf(context) - 8, child: Center(child: _icon(e, row.isRoot, open, up: row.isUp)), ), SizedBox(width: look.rich ? 8 : 6), diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 7da53d8..911f218 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -572,11 +572,17 @@ class _EncodeBar extends StatelessWidget { padding: const EdgeInsets.only(right: 6), child: Text(t, style: const TextStyle(fontSize: 12, color: JjColors.textDim)), ); + // 253: 글자가 크면 고르기 상자가 화면보다 넓어져 넘쳤다 - 제 폭 (가장 긴 항목) 을 쓰되 화면 폭을 넘지 않게 (글은 …) + final maxDrop = (MediaQuery.sizeOf(context).width - 40).clamp(120.0, double.infinity); Widget drop(T value, List items, String Function(T) text, ValueChanged onChanged, {bool Function(T)? enabled}) => - DropdownButton( + ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxDrop), + child: IntrinsicWidth( + child: DropdownButton( value: value, isDense: true, + isExpanded: true, underline: const SizedBox(), style: const TextStyle(fontSize: 13, color: JjColors.text), items: [ @@ -586,12 +592,16 @@ class _EncodeBar extends StatelessWidget { enabled: enabled?.call(i) ?? true, child: Text( text(i) + ((enabled?.call(i) ?? true) ? '' : tr(' (사용 불가)')), + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( color: (enabled?.call(i) ?? true) ? null : JjColors.textDim), ), ), ], onChanged: locked ? null : (v) => onChanged(v as T), + ), + ), ); final compact = isCompact(context); diff --git a/lib/ui/language_settings.dart b/lib/ui/language_settings.dart index d9f3769..c251ee1 100644 --- a/lib/ui/language_settings.dart +++ b/lib/ui/language_settings.dart @@ -28,7 +28,13 @@ class LanguageSettings extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(72, 0, 16, 8), child: Wrap(spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ - DropdownButton( + // 253: 글자가 크면 (긴 "시스템 언어 따르기 (…)") 화면보다 넓어져 넘쳤다 - 남는 폭에 맞게 줄인다 + ConstrainedBox( + constraints: BoxConstraints(maxWidth: (MediaQuery.sizeOf(context).width - 100).clamp(120.0, double.infinity)), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: DropdownButton( value: i18n.chosen == I18nController.system || i18n.available.contains(i18n.chosen) ? i18n.chosen : 'ko', @@ -42,6 +48,8 @@ class LanguageSettings extends StatelessWidget { DropdownMenuItem(value: code, child: Text(I18nController.nativeName(code))), ], onChanged: (v) => i18n.apply(v!), + ), + ), ), OutlinedButton.icon( onPressed: i18n.canTranslate ? () => addLanguageDialog(context) : null, diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index c24f764..1448e6f 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), - // 257: 폰 세로에서 버튼이 화면 밖으로 넘치지 않게 줄을 바꾼다 (오른쪽 정렬은 그대로) - Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 2, runSpacing: 2, children: [ + // 257 · 253: 폰 세로 · 글자가 크면 버튼이 화면 밖으로 넘치지 않게 줄을 바꾼다 (오른쪽 정렬은 그대로) + Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 6, runSpacing: 4, children: [ if (running) TextButton.icon(onPressed: () => center.cancel(t.id), icon: const Icon(Icons.stop, size: 18), label: Text(tr('취소'))) else diff --git a/lib/ui/setting_tile.dart b/lib/ui/setting_tile.dart index d6c85f4..6e32680 100644 --- a/lib/ui/setting_tile.dart +++ b/lib/ui/setting_tile.dart @@ -41,7 +41,9 @@ class SettingTile extends StatelessWidget { Widget _tile(BuildContext context) { final t = trailing; - if (t == null || _small(t) || !isCompact(context)) { + // 253: 글자를 크게 (1.2배 넘게) 하면 넓은 화면에서도 고르기 상자를 설명 아래로 (오른쪽에 두면 넘쳤다) + final large = MediaQuery.textScalerOf(context).scale(10) > 12; + if (t == null || _small(t) || (!isCompact(context) && !large)) { return ListTile( leading: leading, title: title, @@ -60,7 +62,8 @@ class SettingTile extends StatelessWidget { subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ?subtitle, const SizedBox(height: 6), - Align(alignment: Alignment.centerLeft, child: t), + // 253: 그래도 화면보다 넓으면 (긴 항목 · 큰 글자) 줄여서 다 보이게 + Align(alignment: Alignment.centerLeft, child: FittedBox(fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: t)), ]), enabled: enabled, dense: dense, diff --git a/test/large_text_test.dart b/test/large_text_test.dart new file mode 100644 index 0000000..2f42c37 --- /dev/null +++ b/test/large_text_test.dart @@ -0,0 +1,69 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/app/app_controller.dart'; +import 'package:jj_mkvmaker/app/download_manager.dart'; +import 'package:jj_mkvmaker/app/settings.dart'; +import 'package:jj_mkvmaker/core/models.dart'; +import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; +import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; +import 'package:jj_mkvmaker/services/platform_services.dart'; +import 'package:jj_mkvmaker/ui/downloads_page.dart'; +import 'package:jj_mkvmaker/ui/explorer_page.dart'; +import 'package:jj_mkvmaker/ui/home_page.dart'; +import 'package:jj_mkvmaker/ui/monitor_page.dart'; +import 'package:jj_mkvmaker/ui/settings_page.dart'; +import 'package:path/path.dart' as p; + +/// 253: 글자를 크게 (기기 설정 글꼴 크기 · 접근성) 해도 화면이 넘치지 않는다 - 줄 높이가 고정이던 목록이 넘쳤다 +void main() { + late Directory tmp; + setUp(() => tmp = Directory.systemTemp.createTempSync('jj_big_')); + tearDown(() => tmp.deleteSync(recursive: true)); + + final pages = { + 'MKV': (c, d) => HomePage(c: c), + '파일 탐색기': (c, d) => ExplorerPage(c: c), + 'Rsync': (c, d) => ExplorerPage(c: c, rsync: true), + '모니터링': (c, d) => MonitorPage(c: c), + '다운로드': (c, d) => DownloadsPage(d: d), + '환경 설정': (c, d) => SettingsPage(c: c), + }; + for (final MapEntry(key: name, value: page) in pages.entries) { + for (final size in [const Size(412, 900), const Size(900, 412), const Size(1280, 800)]) { + testWidgets('253: $name (${size.width.round()}×${size.height.round()}, 글자 1.6배) 넘치지 않는다', (tester) async { + tester.view.physicalSize = size; + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + const pp = MethodChannel('plugins.flutter.io/path_provider'); + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, (_) async => tmp.path); + addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null)); + File(p.join(tmp.path, '아주 긴 이름의 동영상 파일 - 시험용 2026.mp4')).writeAsStringSync('v'); + Directory(p.join(tmp.path, 'sub')).createSync(); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + c.settings + ..explorerPaths = [tmp.path, tmp.path] + ..rsyncPaths = [tmp.path, tmp.path] + ..copyTasks = [CopyTask(id: '1', sources: [tmp.path], dest: p.join(tmp.path, 'sub'), method: 'rsync', options: '-av')] + ..liveSyncPairs = [LiveSyncPair(tmp.path, p.join(tmp.path, 'x'))]; + c.videos.add(VideoItem(p.join(tmp.path, '아주 긴 이름의 동영상 파일 - 시험용 2026.mp4'))); + c.selected = c.videos.first; + final d = DownloadManager(backends: const [], settings: () => c.settings, readClipboard: () async => null); + addTearDown(d.dispose); + await tester.runAsync(() => tester.pumpWidget(MaterialApp( + builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: const TextScaler.linear(1.6)), child: child!), + home: page(c, d)))); + for (var i = 0; i < 15; i++) { + await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); + await tester.pump(); + } + expect(tester.takeException(), isNull); + // 화면을 내리고 남은 타이머 (탐색기 목록 읽기 등) 가 끝나게 + await tester.pumpWidget(const SizedBox()); + await tester.pump(const Duration(seconds: 1)); + }); + } + } +}