설정 글 칸이 입력 중에 끊기지 않게 · 키보드가 열리면 위 줄 접기 (213) · 좁은 창 위 줄 기준 480 (221)
- 213 (관리자 지시, 입력이 안 됨): 설정의 rsync · robocopy 옵션 칸이 값이 기본값과 같아지거나 달라지는 순간 새로 만들어져 포커스를 잃었다. 칸마다 컨트롤러를 두고 고정 key, ↺ 는 칸의 글을 바꾼다. - 213: 폰 가로에서 키보드가 열려 남는 높이가 360 미만이면 설정 화면의 위 줄 둘 (막대 · 묶음 바로가기) 을 접는다. - 221 (관리자 지시): 폰 세로 (창 약 400dp) 에서 창 위 줄이 넓은 배치라 탭 칸이 130dp 남짓이어서 두 번째 저장소 (SD 카드) 가 끝에서 잘려 보였다. 넓은 배치 기준을 320 → 480 으로 올려, 그보다 좁으면 끝 버튼을 [⋮] 하나로 모으고 탭 칸을 넓힌다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>fix_mkv
parent
954b86b84c
commit
3ffaf23955
|
|
@ -62,15 +62,29 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
|
||||
String _valid(String v) => v == 'robocopy' && Platform.isWindows ? v : 'builtin';
|
||||
|
||||
/// 옵션 입력칸 (바꾸면 바로 저장, ↺ 로 기본값)
|
||||
Widget _options(String title, String hint, String value, String def, void Function(AppSettings, String) set) => SettingTile(
|
||||
/// 옵션 입력칸의 글 (칸마다 하나 - 213: 칸을 새로 만들지 않고 글만 바꾼다)
|
||||
final _optionText = <String, TextEditingController>{};
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final x in _optionText.values) {
|
||||
x.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 옵션 입력칸 (바꾸면 바로 저장, ↺ 로 기본값).
|
||||
/// 213: 예전에는 값이 기본값과 같아지거나 달라지는 순간 칸을 새로 만들어 (↺ 로 되돌릴 때 글을 바꾸려고) 포커스를 잃고 입력이 끊겼다
|
||||
Widget _options(String title, String hint, String value, String def, void Function(AppSettings, String) set) {
|
||||
final ctl = _optionText.putIfAbsent(title, () => TextEditingController(text: value));
|
||||
return SettingTile(
|
||||
title: Text(title),
|
||||
subtitle: Text(hint),
|
||||
trailing: SizedBox(
|
||||
width: 380,
|
||||
child: TextFormField(
|
||||
key: ValueKey('$title|$value' == '$title|$def' ? '$title-def' : title),
|
||||
initialValue: value,
|
||||
controller: ctl,
|
||||
scrollPadding: const EdgeInsets.all(80),
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
|
|
@ -79,8 +93,8 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
tooltip: trf('기본값으로 ({0})', [def]),
|
||||
icon: const Icon(Icons.restart_alt, size: 18),
|
||||
onPressed: () async {
|
||||
ctl.text = def;
|
||||
await c.updateSettings((x) => set(x, def));
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -89,6 +103,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.rsync ? _rsyncPart() : _explorerPart();
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ import 'explorer_bookmarks.dart';
|
|||
///
|
||||
/// [rsync] 면 Rsync 화면: 같은 모양이지만 늘 좌우 두 창 · 폴더만 · 창마다 폴더 하나만 고른다.
|
||||
/// 가운데 → (왼쪽 → 오른쪽) · ← (오른쪽 → 왼쪽) · ⇄ (둘 다 함께) 로 rsync, [모니터링] 에 실행한 것이 남는다.
|
||||
/// 202 · 221: 창 위 줄을 [← → ★] · 탭 · [WebDAV · 경로 입력] 으로 펴는 가장 작은 창 폭. 이보다 좁으면 끝 버튼은 [⋮] 하나 -
|
||||
/// 폰 세로 (약 400) 에서 탭 칸이 130px 남짓이라 두 번째 저장소 (SD 카드) 가 잘려 보였다
|
||||
const double paneHeaderWideWidth = 480;
|
||||
|
||||
/// 191: Rsync 화면이 좌우 두 창 + 가운데 버튼 줄로 보이는 가장 작은 폭 (이보다 좁으면 창 하나씩)
|
||||
const double rsyncTwoPaneMinWidth = 560;
|
||||
|
||||
|
|
@ -3091,13 +3095,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
/// 창 위쪽: 저장 장치 고르기
|
||||
/// 창 위 줄: [뒤로 · 앞으로 · 즐겨찾기] · 저장 장치 탭 (넘치면 옆으로 밀기) · [WebDAV · 경로 입력].
|
||||
/// 202: 드라이브가 많아 넘쳐도 끝의 버튼은 늘 보이게 (예전에는 줄 전체가 한 목록이라 화면 밖으로 밀려 누를 수 없었다)
|
||||
/// 좁은 창 (위아래 두 창 · 폰) 은 앞 버튼도 탭과 함께 밀고, 끝 버튼은 [⋮] 하나로 (늘 보임)
|
||||
/// 좁은 창 (위아래 두 창 · 폰, [paneHeaderWideWidth] 미만) 은 앞 버튼도 탭과 함께 밀고, 끝 버튼은 [⋮] 하나로 (늘 보임)
|
||||
Widget _paneHeader(_Pane pane) => SizedBox(
|
||||
height: 40,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, box) => box.maxWidth >= 320
|
||||
builder: (context, box) => box.maxWidth >= paneHeaderWideWidth
|
||||
? Row(children: [
|
||||
..._headerStart(pane),
|
||||
Expanded(child: _driveTabs(pane)),
|
||||
|
|
|
|||
|
|
@ -224,6 +224,9 @@ class _ContextMenuTileState extends State<_ContextMenuTile> {
|
|||
}
|
||||
|
||||
/// 환경 설정
|
||||
/// 213: 키보드가 열린 뒤 남는 높이가 이보다 작으면 설정 화면의 위 줄을 접는다
|
||||
const double settingsCrampedHeight = 360;
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
final AppController c;
|
||||
const SettingsPage({super.key, required this.c});
|
||||
|
|
@ -300,8 +303,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
final s = c.settings;
|
||||
// Android: 창 · 트레이 · 탐색기 · 다운로드 (yt-dlp · aria2) · 내장 Chrome 설정은 없음
|
||||
final desk = !Platform.isAndroid;
|
||||
// 213: 폰 가로에서 키보드가 열리면 남는 높이가 작아 위 줄 둘 (막대 · 묶음 바로가기) 이 화면을 다 차지해
|
||||
// 넣는 칸이 보이지 않았다 - 그동안은 두 줄을 접는다 (키보드를 닫으면 다시 보임)
|
||||
final mq = MediaQuery.of(context);
|
||||
final cramped = mq.viewInsets.bottom > 0 && mq.size.height - mq.viewInsets.bottom < settingsCrampedHeight;
|
||||
return Scaffold(
|
||||
body: Column(children: [
|
||||
if (!cramped)
|
||||
AppTopBar(
|
||||
nav: const AppNavButtons(),
|
||||
actions: AppActions(c: c, onSettingsPage: true),
|
||||
|
|
@ -316,6 +324,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
]),
|
||||
),
|
||||
// 묶음 바로가기 (누르면 그 묶음으로). 스크롤해도 위에 그대로
|
||||
if (!cramped)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(isCompact(context) ? 8 : 24, 12, isCompact(context) ? 8 : 24, 0),
|
||||
child: _groupBar(desk),
|
||||
|
|
|
|||
|
|
@ -117,6 +117,34 @@ void main() {
|
|||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('221: 폰 세로 Rsync (한 창) 의 저장소 탭은 글이 다 보인다 (SD 카드가 잘리지 않음)', (tester) async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_rsync_tabs_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final inner = p.join(tmp.path, 'inner'), sd = p.join(tmp.path, 'sd');
|
||||
Directory(p.join(inner, 'L')).createSync(recursive: true);
|
||||
Directory(p.join(sd, 'R')).createSync(recursive: true);
|
||||
ExplorerPage.debugVolumes = () => [(inner, '내장 저장소'), (sd, 'SD 카드')];
|
||||
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerOrientation = 'side'
|
||||
..rsyncPaths = [p.join(inner, 'L'), p.join(sd, 'R')];
|
||||
tester.view.physicalSize = const Size(412, 880);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c, rsync: true))));
|
||||
await settle(tester, () => find.text('SD 카드').evaluate().isNotEmpty);
|
||||
// 탭 글은 눌리지 않는다 (글 폭 그대로) - 넘치면 옆으로 밀어서 다 보인다 (202 와 같은 방식)
|
||||
final plain = TextPainter(text: const TextSpan(text: 'SD 카드'), textDirection: TextDirection.ltr)..layout();
|
||||
expect(tester.getSize(find.text('SD 카드').first).width, greaterThanOrEqualTo(plain.width * 0.9), reason: '"(" 처럼 눌리지 않음');
|
||||
final tabs = find.ancestor(of: find.text('SD 카드').first, matching: find.byType(ListView)).first;
|
||||
await tester.drag(tabs, const Offset(-400, 0));
|
||||
await settle(tester, () => false, rounds: 10);
|
||||
expect(tester.getRect(find.text('SD 카드').first).right, lessThanOrEqualTo(tester.getRect(tabs).right + 1), reason: '밀면 끝까지 보임');
|
||||
expect(find.byTooltip('경로 입력 · WebDAV'), findsOneWidget, reason: '끝 버튼은 [⋮] 하나');
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('25: Rsync 화면 폴더도 길게 누르면 메뉴 (고르기 · 열기 · 이름 변경 · 삭제 · 정보)', (tester) async {
|
||||
final tmp = Directory.systemTemp.createTempSync('jj_rsync_menu_');
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/core/sync_tools.dart' show defaultRsyncOptions;
|
||||
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/copy_sync_settings.dart';
|
||||
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
||||
|
||||
/// 213: 설정의 rsync 옵션 칸 - 값이 기본값과 같아지거나 달라지는 순간에도 포커스를 잃지 않고 이어서 쓸 수 있다
|
||||
void main() {
|
||||
testWidgets('213: 기본값 ("-avPog") 을 지나며 쳐도 칸이 그대로 (포커스 · 입력 이어짐) · ↺ 는 칸 안의 글도 되돌린다', (tester) async {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings.rsyncOptions = '-avPo';
|
||||
tester.view.physicalSize = const Size(1400, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
// 설정 화면처럼 설정이 바뀌면 다시 그린다
|
||||
child: ListenableBuilder(listenable: c, builder: (_, _) => CopySyncSettings(c: c, rsync: true)),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
final field = find.byType(TextFormField).first;
|
||||
await tester.tap(field);
|
||||
await tester.pump();
|
||||
final first = tester.state(field);
|
||||
// 한 글자씩: "-avPo" → "-avPog" (기본값과 같아짐) → "-avPog " → "-avPog -"
|
||||
for (final t in ['-avPog', '-avPog ', '-avPog -']) {
|
||||
await tester.enterText(field, t);
|
||||
await tester.pump();
|
||||
expect(tester.state(find.byType(TextFormField).first), same(first), reason: '칸을 새로 만들지 않음 ($t)');
|
||||
expect(tester.testTextInput.hasAnyClients, isTrue, reason: '포커스 그대로 ($t)');
|
||||
}
|
||||
expect(c.settings.rsyncOptions, '-avPog -');
|
||||
// ↺: 칸 안의 글도 기본값으로
|
||||
await tester.tap(find.byTooltip('기본값으로 ($defaultRsyncOptions)').first);
|
||||
await tester.pump();
|
||||
expect(c.settings.rsyncOptions, defaultRsyncOptions);
|
||||
expect(find.text(defaultRsyncOptions), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('213: 폰 가로에서 키보드가 열리면 설정 화면의 위 줄 (막대 · 묶음 바로가기) 을 접고, 닫으면 다시', (tester) async {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
tester.view.physicalSize = const Size(900, 420);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(MaterialApp(home: SettingsPage(c: c)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(ActionChip, '일반'), findsOneWidget);
|
||||
expect(find.text('바꾸면 바로 저장됩니다'), findsOneWidget);
|
||||
// 키보드 (남는 높이 420 - 280 = 140)
|
||||
tester.view.viewInsets = const FakeViewPadding(bottom: 280);
|
||||
addTearDown(tester.view.resetViewInsets);
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(ActionChip, '일반'), findsNothing);
|
||||
expect(find.text('바꾸면 바로 저장됩니다'), findsNothing);
|
||||
tester.view.resetViewInsets();
|
||||
await tester.pump();
|
||||
expect(find.widgetWithText(ActionChip, '일반'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue