WIP 253 글자 크게 (1.6배) 해도 넘치지 않게: 탐색기 줄 높이 · 설정 고르기 상자 · 위 막대 · 인코딩 · 모니터링 · 언어 · 화면 전체 시험 (검토 대기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음6
RyuJongJin 2026-10-12 06:54:25 +09:00
parent 6d322b1726
commit 2d2f7055ba
8 changed files with 117 additions and 15 deletions

View File

@ -360,10 +360,16 @@ class AppNavButtons extends StatelessWidget {
child: icon, child: icon,
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text(label, // 253: 글자가 크면 칸 (높이 46) 아래로 넘쳤다 - 남는 높이에 맞게 줄인다
maxLines: 1, Flexible(
overflow: TextOverflow.clip, child: FittedBox(
style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)), fit: BoxFit.scaleDown,
child: Text(label,
maxLines: 1,
overflow: TextOverflow.clip,
style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)),
),
),
]), ]),
), ),
), ),

View File

@ -21,6 +21,12 @@ enum ExplorerStyle {
static ExplorerStyle of(String name) => values.firstWhere((s) => s.name == name, orElse: () => xplore); static ExplorerStyle of(String name) => values.firstWhere((s) => s.name == name, orElse: () => xplore);
double get rowHeight => switch (this) { xplore => 52, windows => 34, totalcmd => 26 }; 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 iconSize => switch (this) { xplore => 30, windows => 20, totalcmd => 16 };
double get indent => switch (this) { xplore => 16, windows => 14, totalcmd => 12 }; double get indent => switch (this) { xplore => 16, windows => 14, totalcmd => 12 };
double get fontSize => switch (this) { xplore => 14, windows => 13, totalcmd => 12.5 }; double get fontSize => switch (this) { xplore => 14, windows => 13, totalcmd => 12.5 };

View File

@ -782,7 +782,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
final pos = pane.scroll.position; final pos = pane.scroll.position;
// 180: 끝 (maxScrollExtent) 으로 자르지 않는다 - 줄이 막 늘어난 때는 끝이 아직 앞의 것이라 보던 폴더로 못 간다. // 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<ExplorerPage> {
ListView.builder( ListView.builder(
controller: pane.scroll, controller: pane.scroll,
itemCount: rows.length, itemCount: rows.length,
itemExtent: look.rowHeight, itemExtent: look.rowHeightOf(context),
itemBuilder: (_, k) => _rowView(pane, rows[k], compact: foldersOnly), itemBuilder: (_, k) => _rowView(pane, rows[k], compact: foldersOnly),
), ),
// 읽는 중 (WebDAV 서버를 기다릴 때 등). 180: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다) // 읽는 중 (WebDAV 서버를 기다릴 때 등). 180: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다)
@ -3135,7 +3135,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
: ListView.builder( : ListView.builder(
controller: pane.listScroll, controller: pane.listScroll,
itemCount: rows.length, itemCount: rows.length,
itemExtent: look.rowHeight, itemExtent: look.rowHeightOf(context),
itemBuilder: (_, k) => _rowView(pane, rows[k], list: true), itemBuilder: (_, k) => _rowView(pane, rows[k], list: true),
), ),
), ),
@ -3383,7 +3383,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
// 손가락으로 누르는 기기 (Android) 는 폭도 넓게 // 손가락으로 누르는 기기 (Android) 는 폭도 넓게
Widget chevron() => SizedBox( Widget chevron() => SizedBox(
width: chevronWidth, width: chevronWidth,
height: look.rowHeight, height: look.rowHeightOf(context),
child: e.isDir && !list && !row.isUp child: e.isDir && !list && !row.isUp
? (loading ? (loading
? const Center( ? const Center(
@ -3414,7 +3414,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (!list) chevron(), if (!list) chevron(),
SizedBox( SizedBox(
width: look.rich ? 56 : look.iconSize + 6, 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)), child: Center(child: _icon(e, row.isRoot, open, up: row.isUp)),
), ),
SizedBox(width: look.rich ? 8 : 6), SizedBox(width: look.rich ? 8 : 6),

View File

@ -572,11 +572,17 @@ class _EncodeBar extends StatelessWidget {
padding: const EdgeInsets.only(right: 6), padding: const EdgeInsets.only(right: 6),
child: Text(t, style: const TextStyle(fontSize: 12, color: JjColors.textDim)), 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>(T value, List<T> items, String Function(T) text, Widget drop<T>(T value, List<T> items, String Function(T) text,
ValueChanged<T> onChanged, {bool Function(T)? enabled}) => ValueChanged<T> onChanged, {bool Function(T)? enabled}) =>
DropdownButton<T>( ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxDrop),
child: IntrinsicWidth(
child: DropdownButton<T>(
value: value, value: value,
isDense: true, isDense: true,
isExpanded: true,
underline: const SizedBox(), underline: const SizedBox(),
style: const TextStyle(fontSize: 13, color: JjColors.text), style: const TextStyle(fontSize: 13, color: JjColors.text),
items: [ items: [
@ -586,12 +592,16 @@ class _EncodeBar extends StatelessWidget {
enabled: enabled?.call(i) ?? true, enabled: enabled?.call(i) ?? true,
child: Text( child: Text(
text(i) + ((enabled?.call(i) ?? true) ? '' : tr(' (사용 불가)')), text(i) + ((enabled?.call(i) ?? true) ? '' : tr(' (사용 불가)')),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: (enabled?.call(i) ?? true) ? null : JjColors.textDim), color: (enabled?.call(i) ?? true) ? null : JjColors.textDim),
), ),
), ),
], ],
onChanged: locked ? null : (v) => onChanged(v as T), onChanged: locked ? null : (v) => onChanged(v as T),
),
),
); );
final compact = isCompact(context); final compact = isCompact(context);

View File

@ -28,7 +28,13 @@ class LanguageSettings extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.fromLTRB(72, 0, 16, 8), padding: const EdgeInsets.fromLTRB(72, 0, 16, 8),
child: Wrap(spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ child: Wrap(spacing: 12, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
DropdownButton<String>( // 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<String>(
value: i18n.chosen == I18nController.system || i18n.available.contains(i18n.chosen) value: i18n.chosen == I18nController.system || i18n.available.contains(i18n.chosen)
? i18n.chosen ? i18n.chosen
: 'ko', : 'ko',
@ -42,6 +48,8 @@ class LanguageSettings extends StatelessWidget {
DropdownMenuItem(value: code, child: Text(I18nController.nativeName(code))), DropdownMenuItem(value: code, child: Text(I18nController.nativeName(code))),
], ],
onChanged: (v) => i18n.apply(v!), onChanged: (v) => i18n.apply(v!),
),
),
), ),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: i18n.canTranslate ? () => addLanguageDialog(context) : null, onPressed: i18n.canTranslate ? () => addLanguageDialog(context) : null,

View File

@ -642,8 +642,8 @@ class _CopyCardState extends State<_CopyCard> {
]), ]),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
// 257: 폰 세로에서 버튼이 화면 밖으로 넘치지 않게 줄을 바꾼다 (오른쪽 정렬은 그대로) // 257 · 253: 폰 세로 · 글자가 크면 버튼이 화면 밖으로 넘치지 않게 줄을 바꾼다 (오른쪽 정렬은 그대로)
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 2, runSpacing: 2, children: [ Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, spacing: 6, runSpacing: 4, children: [
if (running) if (running)
TextButton.icon(onPressed: () => center.cancel(t.id), icon: const Icon(Icons.stop, size: 18), label: Text(tr('취소'))) TextButton.icon(onPressed: () => center.cancel(t.id), icon: const Icon(Icons.stop, size: 18), label: Text(tr('취소')))
else else

View File

@ -41,7 +41,9 @@ class SettingTile extends StatelessWidget {
Widget _tile(BuildContext context) { Widget _tile(BuildContext context) {
final t = trailing; 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( return ListTile(
leading: leading, leading: leading,
title: title, title: title,
@ -60,7 +62,8 @@ class SettingTile extends StatelessWidget {
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
?subtitle, ?subtitle,
const SizedBox(height: 6), 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, enabled: enabled,
dense: dense, dense: dense,

69
test/large_text_test.dart Normal file
View File

@ -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 = <String, Widget Function(AppController, DownloadManager)>{
'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<void>.delayed(const Duration(milliseconds: 30)));
await tester.pump();
}
expect(tester.takeException(), isNull);
// 화면을 내리고 남은 타이머 (탐색기 목록 읽기 등) 가 끝나게
await tester.pumpWidget(const SizedBox());
await tester.pump(const Duration(seconds: 1));
});
}
}
}