WIP 253 글자 크게 (1.6배) 해도 넘치지 않게: 탐색기 줄 높이 · 설정 고르기 상자 · 위 막대 · 인코딩 · 모니터링 · 언어 · 화면 전체 시험 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>묶음6
parent
6d322b1726
commit
2d2f7055ba
|
|
@ -360,10 +360,16 @@ class AppNavButtons extends StatelessWidget {
|
||||||
child: icon,
|
child: icon,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(label,
|
// 253: 글자가 크면 칸 (높이 46) 아래로 넘쳤다 - 남는 높이에 맞게 줄인다
|
||||||
|
Flexible(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Text(label,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)),
|
style: TextStyle(fontSize: 10, color: here ? JjColors.accent : JjColors.textDim)),
|
||||||
|
),
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue