From 428fe99a82527e2ee58787e9c8a79aad35591fe6 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:08:55 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=A4=EC=A0=95=20A=20=EB=B6=84=EC=95=BC=201?= =?UTF-8?q?:=20=ED=8C=8C=EC=9D=BC=20=ED=83=90=EC=83=89=EA=B8=B0=20(Explore?= =?UTF-8?q?rSettings)=20-=20=ED=82=A4=2031=20=C2=B7=20=EC=98=AE=EA=B9=80?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/app/settings/explorer_settings.dart: 탐색기 설정의 이름 · 처음 값 · 읽기 · 쓰기를 한 곳에 - AppSettings 는 explorer 분야를 갖고 분야 목록을 돌며 합치기만 (toJson · fromJson 에서 탐색기 키를 지움) - 부르는 곳: s.explorerLayout → s.explorer.layout 꼴 (옛 이름 getter 없음), 저장 키 이름은 그대로 - 고침: fromJson 이 바뀐 기본값 알림 (_migrated) 을 분야를 읽기 전에 계산하던 것 - 분야를 먼저 읽음 - JSON 약속 시험: 알림 목록 (__migrated) · 옛 판 설정 경우를 더함, 기준은 옛 판 (db45f80) 에서 다시 만듦 Co-Authored-By: Claude Opus 5.5 --- integration_test/explorer_shot_test.dart | 6 +- integration_test/explorer_text_shot_test.dart | 10 +- integration_test/rsync_page_test.dart | 2 +- integration_test/windows_b3_shot_test.dart | 2 +- integration_test/windows_b6_input_test.dart | 6 +- .../windows_bigtext_shot_test.dart | 2 +- integration_test/windows_check_shot_test.dart | 22 +- .../windows_datecol_shot_test.dart | 8 +- .../windows_move_left_shot_test.dart | 2 +- .../windows_rsync_narrow_shot_test.dart | 2 +- .../windows_xplore_shot_test.dart | 8 +- lib/app/settings.dart | 195 ++------------ lib/app/settings/explorer_settings.dart | 211 +++++++++++++++ lib/ui/explorer/delete_flow.dart | 8 +- lib/ui/explorer/drop_flow.dart | 2 +- lib/ui/explorer/explorer_bars.dart | 4 +- lib/ui/explorer/explorer_body.dart | 20 +- lib/ui/explorer/explorer_buttons.dart | 4 +- lib/ui/explorer/explorer_controller.dart | 40 +-- lib/ui/explorer/explorer_jobs.dart | 2 +- lib/ui/explorer/explorer_layout.dart | 16 +- lib/ui/explorer/explorer_select.dart | 4 +- lib/ui/explorer/item_actions.dart | 2 +- lib/ui/explorer/item_menu.dart | 10 +- lib/ui/explorer/key_flow.dart | 2 +- lib/ui/explorer/open_flow.dart | 4 +- lib/ui/explorer/rename_flow.dart | 4 +- lib/ui/explorer/row_view.dart | 2 +- lib/ui/explorer/search_flow.dart | 2 +- lib/ui/explorer/transfer_panel.dart | 6 +- lib/ui/explorer/view_dialogs.dart | 24 +- lib/ui/explorer_keys_page.dart | 14 +- lib/ui/explorer_page.dart | 14 +- lib/ui/hidden_items_dialog.dart | 6 +- lib/ui/remote_thumb_view.dart | 4 +- lib/ui/settings/files_section.dart | 88 +++--- test/a11y_names_test.dart | 2 +- test/decisions_test.dart | 4 +- test/dismiss_test.dart | 6 +- test/explorer_archive_trash_test.dart | 4 +- test/explorer_buttons_contract_test.dart | 2 +- test/explorer_conflict_test.dart | 16 +- test/explorer_controller_contract_test.dart | 2 +- test/explorer_drives_test.dart | 6 +- test/explorer_filter_rsync_test.dart | 14 +- test/explorer_hidden_find_test.dart | 50 ++-- test/explorer_keys_drag_test.dart | 46 ++-- test/explorer_keys_test.dart | 22 +- test/explorer_labels_test.dart | 2 +- test/explorer_more_flows_contract_test.dart | 6 +- test/explorer_page_modules_contract_test.dart | 10 +- test/explorer_panes_test.dart | 36 +-- test/explorer_parallel_test.dart | 48 ++-- test/explorer_select_ops_test.dart | 14 +- test/explorer_sort_kind_test.dart | 20 +- test/explorer_steady_test.dart | 16 +- test/explorer_text_scale_test.dart | 32 +-- test/explorer_webdav_test.dart | 4 +- test/fixtures/settings_golden_legacy_out.json | 254 ++++++++++++++++++ test/fixtures/settings_golden_out.json | 5 + test/large_text_test.dart | 2 +- test/master_lock_test.dart | 8 +- test/remote_thumbs_test.dart | 18 +- test/root_test.dart | 4 +- test/rsync_safety_test.dart | 2 +- test/settings_json_contract_test.dart | 16 +- test/settings_store_test.dart | 18 +- test/sftp_explorer_test.dart | 2 +- test/shizuku_test.dart | 4 +- test/support/explorer_harness.dart | 2 +- test/sync_problem_test.dart | 4 +- test/vault_page_test.dart | 4 +- 72 files changed, 894 insertions(+), 569 deletions(-) create mode 100644 lib/app/settings/explorer_settings.dart create mode 100644 test/fixtures/settings_golden_legacy_out.json diff --git a/integration_test/explorer_shot_test.dart b/integration_test/explorer_shot_test.dart index fa1eef7..4e4db1d 100644 --- a/integration_test/explorer_shot_test.dart +++ b/integration_test/explorer_shot_test.dart @@ -20,7 +20,7 @@ void main() { final env = Platform.environment; final out = env['JJ_SHOT_DIR'] ?? Directory.systemTemp.path; final c = AppController(PlatformServices.create()); - c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path]; + c.settings.explorer.paths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path]; final key = GlobalKey(); final light = env['JJ_SHOT_THEME'] == 'light'; JjColors.use(light ? Brightness.light : Brightness.dark); @@ -33,8 +33,8 @@ void main() { ('split', const Size(1600, 900), 'split', 'windows'), ]) { c.settings - ..explorerLayout = layout - ..explorerStyle = style; + ..explorer.layout = layout + ..explorer.style = style; tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; await tester.runAsync(() => tester.pumpWidget(MaterialApp( diff --git a/integration_test/explorer_text_shot_test.dart b/integration_test/explorer_text_shot_test.dart index 9dd89ee..38a9b75 100644 --- a/integration_test/explorer_text_shot_test.dart +++ b/integration_test/explorer_text_shot_test.dart @@ -29,7 +29,7 @@ void main() { Directory(p.join(tmp.path, '폴더 하나')).createSync(); Directory(p.join(tmp.path, 'Another folder with a long name')).createSync(); final c = AppController(PlatformServices.create()); - c.settings.explorerPaths = [tmp.path, tmp.path]; + c.settings.explorer.paths = [tmp.path, tmp.path]; final key = GlobalKey(); // 폰처럼 (Android 는 트리 펼치기 화살표가 44 로 넓다 - Windows 로 그리면 폰보다 넉넉해 보인다) debugDefaultTargetPlatformOverride = TargetPlatform.android; @@ -37,10 +37,10 @@ void main() { for (final style in ['xplore', 'windows', 'totalcmd']) { for (final (scale, spacing) in [(100, 'normal'), (150, 'wide'), (80, 'compact')]) { c.settings - ..explorerLayout = layout - ..explorerStyle = style - ..explorerTextScale = scale - ..explorerRowSpacing = spacing; + ..explorer.layout = layout + ..explorer.style = style + ..explorer.textScale = scale + ..explorer.rowSpacing = spacing; tester.view.physicalSize = Size(width, 900); tester.view.devicePixelRatio = 1; await tester.runAsync(() => tester.pumpWidget(MaterialApp( diff --git a/integration_test/rsync_page_test.dart b/integration_test/rsync_page_test.dart index e21611a..aa283c9 100644 --- a/integration_test/rsync_page_test.dart +++ b/integration_test/rsync_page_test.dart @@ -35,7 +35,7 @@ void main() { final c = AppController(PlatformServices.create()); c.settings ..rsyncPaths = [tmp.path, tmp.path] - ..explorerStyle = 'windows'; + ..explorer.style = 'windows'; final key = GlobalKey(); tester.view.physicalSize = const Size(1500, 900); tester.view.devicePixelRatio = 1; diff --git a/integration_test/windows_b3_shot_test.dart b/integration_test/windows_b3_shot_test.dart index 05b3830..a178540 100644 --- a/integration_test/windows_b3_shot_test.dart +++ b/integration_test/windows_b3_shot_test.dart @@ -79,7 +79,7 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync('old'); c = AppController(PlatformServices.create()); - c.settings.explorerPaths = [left, right]; + c.settings.explorer.paths = [left, right]; // 휴지통: 기본은 가짜 (화면 흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 useTestRecycleBin(); }); diff --git a/integration_test/windows_b6_input_test.dart b/integration_test/windows_b6_input_test.dart index a59b761..846e5e1 100644 --- a/integration_test/windows_b6_input_test.dart +++ b/integration_test/windows_b6_input_test.dart @@ -47,8 +47,8 @@ void main() { ..writeAsStringSync('x'); c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerPaths = [left, right] - ..explorerDropAction = 'ask'; + ..explorer.paths = [left, right] + ..explorer.dropAction = 'ask'; }); tearDown(() async { for (var i = 0;; i++) { @@ -221,7 +221,7 @@ void main() { testWidgets('75: 마우스 4 · 5 번은 마우스가 올라간 창에 · Alt+← / →', (t) async { await open(t); - String at(int i) => c.settings.explorerPaths[i]; + String at(int i) => c.settings.explorer.paths[i]; final sub = p.join(left, 'sub'), r1 = p.join(right, 'r1'); await t.tap(find.text('r1')); await until(t, () => at(1) == r1 && find.text('x.txt').evaluate().isNotEmpty); diff --git a/integration_test/windows_bigtext_shot_test.dart b/integration_test/windows_bigtext_shot_test.dart index 19c6e90..2cad2b7 100644 --- a/integration_test/windows_bigtext_shot_test.dart +++ b/integration_test/windows_bigtext_shot_test.dart @@ -59,7 +59,7 @@ void main() { Directory(p.join(tmp.path, 'sub')).createSync(); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerPaths = [tmp.path, tmp.path] + ..explorer.paths = [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'))]; diff --git a/integration_test/windows_check_shot_test.dart b/integration_test/windows_check_shot_test.dart index ffde860..1c14a29 100644 --- a/integration_test/windows_check_shot_test.dart +++ b/integration_test/windows_check_shot_test.dart @@ -90,9 +90,9 @@ void main() { File(p.join(dir.path, 'keep_$tag.txt')).writeAsStringSync('x'); final c = controller(); c.settings - ..explorerLayout = 'single' - ..explorerClick = 'select' - ..explorerPaths = [dir.path, dir.path]; + ..explorer.layout = 'single' + ..explorer.click = 'select' + ..explorer.paths = [dir.path, dir.path]; await show(t, size, ExplorerPage(c: c)); await tapText(t, 'recycle_me_$tag.txt'); await t.sendKeyEvent(LogicalKeyboardKey.delete); @@ -119,9 +119,9 @@ void main() { for (final (tag, size) in sizes) { final c = controller(); c.settings - ..explorerLayout = 'single' - ..explorerClick = 'select' - ..explorerPaths = [share.path, share.path]; + ..explorer.layout = 'single' + ..explorer.click = 'select' + ..explorer.paths = [share.path, share.path]; await show(t, size, ExplorerPage(c: c)); await settle(t, 20); await tapText(t, 'on_share.txt'); @@ -142,9 +142,9 @@ void main() { for (final (tag, size) in sizes) { final c = controller(); c.settings - ..explorerLayout = 'single' - ..explorerClick = 'select' - ..explorerPaths = [deep, deep]; + ..explorer.layout = 'single' + ..explorer.click = 'select' + ..explorer.paths = [deep, deep]; await show(t, size, ExplorerPage(c: c)); await settle(t, 20); final f = find.textContaining('long_name_file'); @@ -185,8 +185,8 @@ void main() { for (final (tag, size) in sizes) { final c = controller(); c.settings - ..explorerLayout = 'single' - ..explorerPaths = [tmp.path, tmp.path]; + ..explorer.layout = 'single' + ..explorer.paths = [tmp.path, tmp.path]; await show(t, size, ExplorerPage(c: c)); Future enter(String text) async { await t.tap(find.byTooltip('경로 입력').first); diff --git a/integration_test/windows_datecol_shot_test.dart b/integration_test/windows_datecol_shot_test.dart index 52d7d6f..df187b4 100644 --- a/integration_test/windows_datecol_shot_test.dart +++ b/integration_test/windows_datecol_shot_test.dart @@ -54,10 +54,10 @@ void main() { for (final w in widths) { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerStyle = style - ..explorerLayout = 'single' - ..explorerTextScale = list - ..explorerPaths = [dir.path, dir.path]; + ..explorer.style = style + ..explorer.layout = 'single' + ..explorer.textScale = list + ..explorer.paths = [dir.path, dir.path]; t.view.physicalSize = Size(w, 800); t.view.devicePixelRatio = 1; await t.runAsync(() => t.pumpWidget(RepaintBoundary( diff --git a/integration_test/windows_move_left_shot_test.dart b/integration_test/windows_move_left_shot_test.dart index 1444b9d..ba39dc6 100644 --- a/integration_test/windows_move_left_shot_test.dart +++ b/integration_test/windows_move_left_shot_test.dart @@ -47,7 +47,7 @@ void main() { ..writeAsStringSync('NEWER'); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerPaths = [left, right] + ..explorer.paths = [left, right] ..copyConflict = 'overwrite'; final root = p.rootPrefix(tmp.path); device.volumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))]; diff --git a/integration_test/windows_rsync_narrow_shot_test.dart b/integration_test/windows_rsync_narrow_shot_test.dart index d847124..eb737fb 100644 --- a/integration_test/windows_rsync_narrow_shot_test.dart +++ b/integration_test/windows_rsync_narrow_shot_test.dart @@ -50,7 +50,7 @@ void main() { device.volumes = () => [(tmp.path, 'T:')]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerOrientation = 'side' + ..explorer.orientation = 'side' ..rsyncPaths = [l, r]; final shotKey = GlobalKey(); await t.pumpWidget(MaterialApp( diff --git a/integration_test/windows_xplore_shot_test.dart b/integration_test/windows_xplore_shot_test.dart index 505ea7a..771df2b 100644 --- a/integration_test/windows_xplore_shot_test.dart +++ b/integration_test/windows_xplore_shot_test.dart @@ -52,10 +52,10 @@ void main() { device.volumes = () => [(a.path, 'C:'), (b.path, 'D:')]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerStyle = 'xplore' - ..explorerLayout = w < 720 ? 'single' : 'dual' - ..explorerOnePaneCopy = 'other' - ..explorerPaths = [a.path, b.path] + ..explorer.style = 'xplore' + ..explorer.layout = w < 720 ? 'single' : 'dual' + ..explorer.onePaneCopy = 'other' + ..explorer.paths = [a.path, b.path] ..folderBookmarks = [FolderBookmark(name: 'Show S01', path: p.join(a.path, 'Show S01'))]; final shotKey = GlobalKey(); await t.runAsync(() => t.pumpWidget(RepaintBoundary( diff --git a/lib/app/settings.dart b/lib/app/settings.dart index d8b9a82..1e4afb7 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -17,10 +17,12 @@ import '../services/app_shell.dart' show appIconOf; import '../core/remote_idle.dart'; import 'settings/records.dart'; import 'settings/settings_group.dart'; +import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/explorer_settings.dart'; /// 환경 설정 (settings.json 에 저장) class AppSettings { @@ -181,82 +183,27 @@ class AppSettings { bool torNoticeSeen = false; // ── 파일 탐색기 (X-plore 참고) ── - /// 창: 'dual' 두 창 (기본 - 폰 세로도, 규칙 7 · 결정 3 10/22) · 'split' 왼쪽 폴더 트리 + 오른쪽 그 폴더의 파일 목록 · - /// 'single' 한 창 · 'auto' 화면 크기 따라 (폰 세로는 한 창) - String explorerLayout = 'dual'; - - /// X-plore 비교 a: 한 창일 때 복사 · 이동 - 'clip' 담아 두었다가 붙여넣기 (지금 그대로, 처음 값) / - /// 'other' 숨은 다른 창의 폴더로 바로 (X-plore 처럼, 위에 "갈 곳" 줄). 예전 동작과 달라 처음 값은 그대로 (관리자 결정) - String explorerOnePaneCopy = 'clip'; - - /// 펼친 폴더를 다시 누르면 접기 (규칙 3 "폴더 한 번 누르기 = 펼치기 · 접기", 처음 값 켬 - E3 10/22). - /// 두 번 누르기의 두 번째는 접지 않는다 ([ClickPolicy] repeat) - bool explorerRetapCollapse = true; - - /// X-plore 비교 40 (관리자 지시 10/20): 파일 탐색기 목록만의 글자 크기 (%, 80~150, 처음 100 = 지금 그대로). - /// 앱 화면 크기 · 기기 글자 크기와 곱해진다 - int explorerTextScale = 100; - static const explorerTextScaleMin = 80, explorerTextScaleMax = 150; - - /// X-plore 비교 40: 목록 줄 간격 'compact' 좁게 · 'normal' 보통 (처음 값 = 지금 그대로) · 'wide' 넓게 - String explorerRowSpacing = 'normal'; - - /// X-plore 비교 i: 일괄 이름 바꾸기의 마지막 규칙 (다음에 창이 그 값으로 열림, 비면 처음 값) - Map batchRenameLast = {}; - - /// X-plore 비교 c: X-plore 스타일에서 저장 장치 · 서버 · 즐겨찾기를 트리의 맨 위 항목으로 (위 줄 탭 · ★ 는 그대로 함께). - /// 처음 값 켬 (X-plore 스타일에서만 보임 - 다른 스타일은 그대로) - bool explorerRootsInTree = true; - - /// X-plore 스타일에서 줄 끝 표시를 늘 보이기. 처음 값 끔 - 규칙 4 "선택 표시는 선택 모드에서만" (E3 10/22) - bool explorerAlwaysMarks = false; - /// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (설정으로만, 처음 값 끔 - 10/8 사용자 결정 "화면 가운데를 밀면 화면 이동" 을 지킴, 관리자 판단 10/18) - bool explorerSwipePanes = false; - /// X-plore 비교 e (관리자 결정): Android 앱 휴지통 보관 일 (처음 14일, 0 = 끔 - 예전처럼 바로 영구 삭제). Windows 는 시스템 휴지통 - int androidTrashDays = 14; - /// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본, 두 기기 - 규칙 2) / 'open' 한 번 = 바로 열기. - /// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴. - String explorerClick = 'select'; - /// 모양: 'xplore' (기본) · 'windows' Windows 탐색기 · 'totalcmd' Total Commander. - /// 규칙 1 의 Windows 처음 값 (Total Commander) 과 저장된 값 묻기 (결정 1) 는 E6 (TC 모양) 과 함께 (관리자 결정 10/22) - String explorerStyle = 'xplore'; - /// 두 창 배치: 'side' 좌우 (기본, 118 - 폰 세로에서도) · 'stacked' 위아래 · 'auto' 화면 모양 따라 (가로로 넓으면 좌우, 세로로 길면 위아래) - String explorerOrientation = 'side'; - /// 기능 버튼 줄: 'middle' 두 창 사이 (기본) · 'edge' 오른쪽 (위아래 배치면 아래) 끝 · 'hidden' 숨김 - String explorerToolbar = 'middle'; - /// 기능 버튼 줄에 보일 버튼과 순서 (ExplorerButton 이름). 비어 있으면 기본 구성. - List explorerButtons = []; - /// 정렬 · 숨긴 항목 - String explorerSort = 'name'; - bool explorerSortDesc = false; - /// 84: 정렬을 폴더마다 기억 (처음: 끔 - 모든 폴더가 같은 정렬). 켜면 열 머리 · 정렬 창이 지금 폴더에만 쓰고 [explorerFolderSorts] 에 둔다 - bool explorerSortPerFolder = false; - /// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름. - /// 처음 끔 (지금 정렬 그대로). 켜도 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억한다 ([explorerFolderSorts]) - bool explorerSortByKind = false; - /// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 (WebDAV · SFTP · SMB · FTP) 썸네일 - 'wifi' 데이터 요금제가 아닌 망에서만 (처음 값) · - /// 'always' 늘 · 'off' 받지 않음. X-plore 모양의 목록에서 보이는 줄의 그림 · 동영상만, 동시에 2개까지 - String explorerRemoteThumbs = 'wifi'; - /// 원격 썸네일 캐시 상한 (MB, 처음 200) - 넘으면 오래 쓰지 않은 것부터 지운다 - int explorerRemoteThumbCacheMb = 200; - static const remoteThumbCacheChoices = [50, 100, 200, 500, 1000]; - /// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 중 사용자가 바꾼 것만 (동작 → 키, '' = 그 동작은 단축키 없음). - /// 비어 있으면 (처음) 스타일대로 (83) - ui/explorer_keys.dart - Map explorerKeyOverrides = {}; + + + + + + + /// X-plore 비교 5-2 암호 보관함 (사용자 결정 10/21, 관리자 결정): 처음 끔. 보관함 자체는 설정이 아니라 그 폴더 (vault.json · *.jjv) 에 있다 - /// 설정을 처음 상태로 돌려도 보관함은 그대로 (끄기만 됨) @@ -274,17 +221,9 @@ class AppSettings { /// 앱이 뒤로 간 뒤 이만큼 (초) 지나면 다시 잠금 (처음 60) int vaultAutoLockSeconds = 60; - /// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지 - Map explorerFolderSorts = {}; - /// 80: 폴더 크기 계산 - 'sort' 크기순 정렬일 때만 (처음 값) · 'always' 늘 · 'off' 안 함 (길게 누르기 [폴더 크기 계산] 은 늘 됨) - String explorerFolderSize = 'sort'; - /// 82: 창 사이 끌어 놓기 - 'ask' 묻기 (처음 값, 관리자 판단) · 'copy' 복사 · 'move' 이동 - String explorerDropAction = 'ask'; - /// 85: 탐색기의 복사 · 이동 진행 패널을 숨김 (아래 작은 표시만). 처음 값: 보임 - bool explorerProgressHidden = false; /// 89: Wi-Fi 공유 - 저절로 끄는 시간 (분, 0 = 끄지 않음). 처음 값 30분 (관리자 지시 10/17) int wifiShareMinutes = 30; @@ -297,14 +236,8 @@ class AppSettings { /// 86: ZIP 으로 묶기의 압축 정도 (1 빠르게 · 6 보통 (처음 값) · 9 작게) - 마지막에 고른 것을 기억 int zipLevel = 6; - bool explorerShowHidden = false; - /// X-plore 숨기기 (관리자 지시 10/18): 파일 탐색기 목록에서 숨긴 파일 · 폴더의 경로 (파일 자체는 바꾸지 않는다). - /// 지우기 · 이동 대상에서는 빠지지 않는다. 목록 맨 아래 [숨긴 항목 보이기] 로 다시 본다 - List explorerHiddenItems = []; - /// X-plore 맞추기 (관리자 지시 10/20): [목록에서 숨기기] 전에 무엇이 바뀌는지 확인 창 (처음 켬, 창의 [다음부터 묻지 않기] · 환경 설정에서 끔) - bool explorerHideConfirm = true; // ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ── /// 파일 탐색기의 복사 · 이동 방법: 'builtin' 현재 방식 (기본) · 'robocopy' (Windows). 파일만 고를 때 / 폴더가 들어 있을 때. @@ -533,11 +466,7 @@ class AppSettings { } } - /// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창) - List explorerPaths = []; - /// 최근에 연 폴더 (내역, 최신이 앞) - List explorerHistory = []; /// 앱 안 브라우저의 데이터 폴더 (로그인 · 쿠키). 실행 중에 정해지며 저장하지 않음 String webViewDataDir = ''; @@ -575,8 +504,11 @@ class AppSettings { AppSettings(); + /// 파일 탐색기 (X-plore 참고) + final explorer = ExplorerSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => const []; + List get groups => [explorer]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -647,50 +579,15 @@ class AppSettings { 'torHomeUrl': torHomeUrl, 'torJavaScript': torJavaScript, 'torNoticeSeen': torNoticeSeen, - 'explorerLayout': explorerLayout, - 'explorerOnePaneCopy': explorerOnePaneCopy, - 'explorerRetapCollapse': explorerRetapCollapse, - 'explorerTextScale': explorerTextScale, - 'explorerRowSpacing': explorerRowSpacing, - 'batchRenameLast': batchRenameLast, - 'explorerRootsInTree': explorerRootsInTree, - 'explorerAlwaysMarks': explorerAlwaysMarks, - 'explorerSwipePanes': explorerSwipePanes, - 'androidTrashDays': androidTrashDays, - 'explorerClick': explorerClick, - // 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다 - 'explorerV2': true, - // E3 (10/22): 선택 규칙 (사용자 지시 모음) 의 처음 값으로 한 번 옮겼음 - 'explorerRulesV1': true, - 'explorerStyle': explorerStyle, - 'explorerOrientation': explorerOrientation, - 'explorerOrientV3': true, - 'explorerToolbar': explorerToolbar, - 'explorerButtons': explorerButtons, - 'explorerSort': explorerSort, - 'explorerSortDesc': explorerSortDesc, - 'explorerFolderSize': explorerFolderSize, - 'explorerDropAction': explorerDropAction, - 'explorerProgressHidden': explorerProgressHidden, 'zipLevel': zipLevel, 'wifiShareMinutes': wifiShareMinutes, 'wifiShareTailscale': wifiShareTailscale, 'wifiShareFolder': wifiShareFolder, - 'explorerSortPerFolder': explorerSortPerFolder, - 'explorerSortByKind': explorerSortByKind, - 'explorerRemoteThumbs': explorerRemoteThumbs, - 'explorerRemoteThumbCacheMb': explorerRemoteThumbCacheMb, - 'explorerKeyOverrides': explorerKeyOverrides, 'vaultEnabled': vaultEnabled, 'shizukuEnabled': shizukuEnabled, 'rootEnabled': rootEnabled, 'vaultMasterCopy': vaultMasterCopy, 'vaultAutoLockSeconds': vaultAutoLockSeconds, - 'explorerFolderSorts': explorerFolderSorts, - 'explorerShowHidden': explorerShowHidden, - 'explorerHiddenItems': explorerHiddenItems, - 'explorerHideConfirm': explorerHideConfirm, - 'explorerPaths': explorerPaths, 'copyMethodFile': copyMethodFile, 'copyConflict': copyConflict, 'mkvExisting': mkvExisting, @@ -779,7 +676,6 @@ class AppSettings { 'backgroundV2': true, 'liveSyncOnStart': liveSyncOnStart, 'copyTasks': [for (final x in copyTasks) x.toJson()], - 'explorerHistory': explorerHistory, 'autoCheckUpdates': autoCheckUpdates, 'lastUpdateCheck': lastUpdateCheck, 'lastUpdateCheckVersion': lastUpdateCheckVersion, @@ -896,70 +792,15 @@ class AppSettings { : defaultTorHome ..torJavaScript = j['torJavaScript'] != false ..torNoticeSeen = j['torNoticeSeen'] == true - // 'auto' = 넓은 화면은 두 창, 좁은 화면 (폰 세로) 은 한 창. 예전 기본 'dual' 은 한 번 'auto' 로 - ..explorerOnePaneCopy = j['explorerOnePaneCopy'] == 'other' ? 'other' : 'clip' - // E3: 규칙 이전 판이 저장한 false 는 예전 처음 값 - 한 번 규칙 3 (켬) 으로 - ..explorerRetapCollapse = j['explorerRulesV1'] == true ? j['explorerRetapCollapse'] != false : true - // 값이 없거나 틀리면 지금 그대로 (100% · 보통), 범위 밖은 끝으로 - ..explorerTextScale = j['explorerTextScale'] is num - ? (j['explorerTextScale'] as num).round().clamp(explorerTextScaleMin, explorerTextScaleMax) - : 100 - ..explorerRowSpacing = const {'compact', 'normal', 'wide'}.contains(j['explorerRowSpacing']) ? j['explorerRowSpacing'] as String : 'normal' - ..batchRenameLast = j['batchRenameLast'] is Map ? Map.from(j['batchRenameLast'] as Map) : {} - ..explorerRootsInTree = j['explorerRootsInTree'] != false - // E3: 규칙 이전 판이 저장한 true 는 예전 처음 값 - 한 번 규칙 4 (끔) 로 - ..explorerAlwaysMarks = j['explorerRulesV1'] == true && j['explorerAlwaysMarks'] == true - ..explorerSwipePanes = j['explorerSwipePanes'] == true - ..androidTrashDays = switch (j['androidTrashDays']) { final int d when d >= 0 && d <= 365 => d, _ => 14 } - // E3 (규칙 7 · 결정 3): 처음 값 두 창. 규칙 이전 판의 'auto' 는 예전 처음 값이라 한 번 두 창으로 - ..explorerLayout = switch (j['explorerLayout']) { - 'single' || 'split' => j['explorerLayout'] as String, - 'auto' when j['explorerRulesV1'] == true => 'auto', - _ => 'dual', - } - // 규칙 2: 두 기기 모두 한 번 = 선택. 규칙 이전 Android 의 'open' 은 예전 처음 값이라 한 번 'select' 로 - ..explorerClick = j['explorerClick'] == 'open' && (j['explorerRulesV1'] == true || !Platform.isAndroid) ? 'open' : 'select' - ..explorerStyle = const ['windows', 'totalcmd'].contains(j['explorerStyle']) ? j['explorerStyle'] as String : 'xplore' - // 118: 예전 기본 '화면 모양 따라' 는 한 번 좌우로 옮긴다 (사용자가 고른 위아래는 그대로) - ..explorerOrientation = j['explorerOrientation'] == 'stacked' - ? 'stacked' - : j['explorerOrientation'] == 'auto' && j['explorerOrientV3'] == true - ? 'auto' - : 'side' - ..explorerToolbar = const ['edge', 'hidden'].contains(j['explorerToolbar']) ? j['explorerToolbar'] as String : 'middle' - ..explorerButtons = [for (final x in (j['explorerButtons'] as List?) ?? const []) '$x'] - ..explorerSort = const ['date', 'size', 'type'].contains(j['explorerSort']) ? j['explorerSort'] as String : 'name' - ..explorerSortDesc = j['explorerSortDesc'] as bool? ?? false - ..explorerFolderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort' - ..explorerSortPerFolder = j['explorerSortPerFolder'] == true - ..explorerSortByKind = j['explorerSortByKind'] == true - ..explorerRemoteThumbs = const {'off', 'wifi', 'always'}.contains(j['explorerRemoteThumbs']) ? j['explorerRemoteThumbs'] as String : 'wifi' ..vaultEnabled = j['vaultEnabled'] == true ..shizukuEnabled = j['shizukuEnabled'] == true ..rootEnabled = j['rootEnabled'] == true ..vaultMasterCopy = j['vaultMasterCopy'] == true ..vaultAutoLockSeconds = j['vaultAutoLockSeconds'] is num ? (j['vaultAutoLockSeconds'] as num).round().clamp(0, 3600) : 60 - ..explorerKeyOverrides = j['explorerKeyOverrides'] is Map - ? {for (final e in (j['explorerKeyOverrides'] as Map).entries) if (e.key is String && e.value is String) e.key as String: e.value as String} - : {} - ..explorerRemoteThumbCacheMb = j['explorerRemoteThumbCacheMb'] is num - ? (j['explorerRemoteThumbCacheMb'] as num).round().clamp(10, 10000) - : 200 - ..explorerProgressHidden = j['explorerProgressHidden'] == true ..zipLevel = const [1, 6, 9].contains(j['zipLevel']) ? j['zipLevel'] as int : 6 ..wifiShareMinutes = const [0, 10, 30, 60, 120].contains(j['wifiShareMinutes']) ? j['wifiShareMinutes'] as int : 30 ..wifiShareTailscale = j['wifiShareTailscale'] is bool ? j['wifiShareTailscale'] as bool : true ..wifiShareFolder = j['wifiShareFolder'] is String ? j['wifiShareFolder'] as String : '' - ..explorerDropAction = const ['ask', 'copy', 'move'].contains(j['explorerDropAction']) ? j['explorerDropAction'] as String : 'ask' - ..explorerFolderSorts = { - if (j['explorerFolderSorts'] is Map) - for (final e in (j['explorerFolderSorts'] as Map).entries) - if (e.key is String && e.value is String) e.key as String: e.value as String, - } - ..explorerShowHidden = j['explorerShowHidden'] as bool? ?? false - ..explorerHiddenItems = [for (final x in (j['explorerHiddenItems'] as List?) ?? const []) if (x is String && x.isNotEmpty) x] - ..explorerHideConfirm = j['explorerHideConfirm'] as bool? ?? true - ..explorerPaths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x'] ..copyMethodFile = _method(j['copyMethodFile']) ..copyConflict = const ['rename', 'overwrite', 'skip'].contains(j['copyConflict']) ? j['copyConflict'] as String : 'ask' ..mkvExisting = const ['rename', 'overwrite', 'skip'].contains(j['mkvExisting']) ? j['mkvExisting'] as String : 'ask' @@ -1079,7 +920,6 @@ class AppSettings { for (final x in (j['copyTasks'] as List?) ?? const []) if (x is Map && x['dest'] is String) CopyTask.fromJson(x), ] - ..explorerHistory = [for (final x in (j['explorerHistory'] as List?) ?? const []) '$x'] ..autoCheckUpdates = j['autoCheckUpdates'] as bool? ?? true ..lastUpdateCheck = j['lastUpdateCheck'] as String? ?? '' ..lastUpdateCheckVersion = j['lastUpdateCheckVersion'] as String? ?? '' @@ -1103,10 +943,11 @@ class AppSettings { ..aiSource = j['aiSource'] as String? ?? 'und' ..aiTargets = (j['aiTargets'] as List?)?.cast() ?? ['ko', 'en', 'ja'] ..aiWhisper = j['aiWhisper'] as String? ?? 'whisper-base'; - s.migrated = _migrated(j, s); + // 분야를 먼저 읽는다 - 바뀐 기본값 알림 ([_migrated]) 이 분야의 값을 본다 for (final g in s.groups) { g.read(j); } + s.migrated = _migrated(j, s); s.extra = Map.of(j); return s; } @@ -1163,8 +1004,8 @@ class AppSettings { static List _migrated(Map j, AppSettings s) => [ if (j['orientationV2'] != true && j['screenOrientation'] == 'landscape' && s.screenOrientation == 'auto') 'orientation', - if (j['explorerV2'] != true && j['explorerLayout'] == 'dual' && s.explorerLayout == 'auto') 'explorerLayout', - if (j['explorerV2'] != true && j['explorerClick'] == 'select' && s.explorerClick == 'open') 'explorerClick', + if (j['explorerV2'] != true && j['explorerLayout'] == 'dual' && s.explorer.layout == 'auto') 'explorerLayout', + if (j['explorerV2'] != true && j['explorerClick'] == 'select' && s.explorer.click == 'open') 'explorerClick', if (j['explorerOrientV3'] != true && j['explorerOrientation'] == 'auto') 'explorerOrientation', if (j['aiImageV1'] != true && j['components'] is List && !(j['components'] as List).contains('aiimage')) 'aiImage', // 53 · 28: 앱 안 브라우저 쿠키를 쓰던 사람에게 넘기는 사이트가 넓어졌음을 한 번 알린다 @@ -1175,7 +1016,7 @@ class AppSettings { 'background', // 193: 그 설정이 생기기 전 판 (설정 파일은 있는데 그 항목이 없음 - 예: 10.03) 에서 올라온 사람이 바로 겪는 동작 변화. // 예전 판에는 키가 없어 위의 "예전 값 → 새 값" 조건에 걸리지 않았다 (P11: 탐색기에서 한 번 눌렀는데 바로 열려 놀람) - if (j.isNotEmpty && !j.containsKey('explorerClick') && s.explorerClick == 'open') 'explorerClickNew', + if (j.isNotEmpty && !j.containsKey('explorerClick') && s.explorer.click == 'open') 'explorerClickNew', // E3 (10/22): 선택 규칙의 처음 값으로 옮긴 것 - 규칙 이전 판에서 올라온 사람에게 한 번 if (j.isNotEmpty && j['explorerRulesV1'] != true && j['explorerAlwaysMarks'] != false) 'explorerMarksRule', if (j.isNotEmpty && j['explorerRulesV1'] != true && j['explorerRetapCollapse'] != true) 'explorerRetapRule', diff --git a/lib/app/settings/explorer_settings.dart b/lib/app/settings/explorer_settings.dart new file mode 100644 index 0000000..7208613 --- /dev/null +++ b/lib/app/settings/explorer_settings.dart @@ -0,0 +1,211 @@ +import 'dart:io'; + +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 파일 탐색기 (X-plore 참고) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class ExplorerSettings implements SettingsGroup { + /// 창: 'dual' 두 창 (기본 - 폰 세로도, 규칙 7 · 결정 3 10/22) · 'split' 왼쪽 폴더 트리 + 오른쪽 그 폴더의 파일 목록 · + /// 'single' 한 창 · 'auto' 화면 크기 따라 (폰 세로는 한 창) + String layout = 'dual'; + + /// X-plore 비교 a: 한 창일 때 복사 · 이동 - 'clip' 담아 두었다가 붙여넣기 (지금 그대로, 처음 값) / + /// 'other' 숨은 다른 창의 폴더로 바로 (X-plore 처럼, 위에 "갈 곳" 줄). 예전 동작과 달라 처음 값은 그대로 (관리자 결정) + String onePaneCopy = 'clip'; + + /// 펼친 폴더를 다시 누르면 접기 (규칙 3 "폴더 한 번 누르기 = 펼치기 · 접기", 처음 값 켬 - E3 10/22). + /// 두 번 누르기의 두 번째는 접지 않는다 ([ClickPolicy] repeat) + bool retapCollapse = true; + + /// X-plore 비교 40 (관리자 지시 10/20): 파일 탐색기 목록만의 글자 크기 (%, 80~150, 처음 100 = 지금 그대로). + /// 앱 화면 크기 · 기기 글자 크기와 곱해진다 + int textScale = 100; + static const textScaleMin = 80, textScaleMax = 150; + + /// X-plore 비교 40: 목록 줄 간격 'compact' 좁게 · 'normal' 보통 (처음 값 = 지금 그대로) · 'wide' 넓게 + String rowSpacing = 'normal'; + + /// X-plore 비교 i: 일괄 이름 바꾸기의 마지막 규칙 (다음에 창이 그 값으로 열림, 비면 처음 값) + Map batchRenameLast = {}; + + /// X-plore 비교 c: X-plore 스타일에서 저장 장치 · 서버 · 즐겨찾기를 트리의 맨 위 항목으로 (위 줄 탭 · ★ 는 그대로 함께). + /// 처음 값 켬 (X-plore 스타일에서만 보임 - 다른 스타일은 그대로) + bool rootsInTree = true; + + /// X-plore 스타일에서 줄 끝 표시를 늘 보이기. 처음 값 끔 - 규칙 4 "선택 표시는 선택 모드에서만" (E3 10/22) + bool alwaysMarks = false; + + /// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (설정으로만, 처음 값 끔 - 10/8 사용자 결정 "화면 가운데를 밀면 화면 이동" 을 지킴, 관리자 판단 10/18) + bool swipePanes = false; + + /// X-plore 비교 e (관리자 결정): Android 앱 휴지통 보관 일 (처음 14일, 0 = 끔 - 예전처럼 바로 영구 삭제). Windows 는 시스템 휴지통 + int androidTrashDays = 14; + + /// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본, 두 기기 - 규칙 2) / 'open' 한 번 = 바로 열기. + /// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴. + String click = 'select'; + + /// 모양: 'xplore' (기본) · 'windows' Windows 탐색기 · 'totalcmd' Total Commander. + /// 규칙 1 의 Windows 처음 값 (Total Commander) 과 저장된 값 묻기 (결정 1) 는 E6 (TC 모양) 과 함께 (관리자 결정 10/22) + String style = 'xplore'; + + /// 두 창 배치: 'side' 좌우 (기본, 118 - 폰 세로에서도) · 'stacked' 위아래 · 'auto' 화면 모양 따라 (가로로 넓으면 좌우, 세로로 길면 위아래) + String orientation = 'side'; + + /// 기능 버튼 줄: 'middle' 두 창 사이 (기본) · 'edge' 오른쪽 (위아래 배치면 아래) 끝 · 'hidden' 숨김 + String toolbar = 'middle'; + + /// 기능 버튼 줄에 보일 버튼과 순서 (ExplorerButton 이름). 비어 있으면 기본 구성. + List buttons = []; + + /// 정렬 · 숨긴 항목 + String sort = 'name'; + + bool sortDesc = false; + + /// 84: 정렬을 폴더마다 기억 (처음: 끔 - 모든 폴더가 같은 정렬). 켜면 열 머리 · 정렬 창이 지금 폴더에만 쓰고 [folderSorts] 에 둔다 + bool sortPerFolder = false; + + /// X-plore 비교 5-1 ① (관리자 지시 10/21): 종류별 기본 정렬 - 사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름. + /// 처음 끔 (지금 정렬 그대로). 켜도 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억한다 ([folderSorts]) + bool sortByKind = false; + + /// X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 (WebDAV · SFTP · SMB · FTP) 썸네일 - 'wifi' 데이터 요금제가 아닌 망에서만 (처음 값) · + /// 'always' 늘 · 'off' 받지 않음. X-plore 모양의 목록에서 보이는 줄의 그림 · 동영상만, 동시에 2개까지 + String remoteThumbs = 'wifi'; + + /// 원격 썸네일 캐시 상한 (MB, 처음 200) - 넘으면 오래 쓰지 않은 것부터 지운다 + int remoteThumbCacheMb = 200; + static const remoteThumbCacheChoices = [50, 100, 200, 500, 1000]; + + /// X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 파일 탐색기 단축키 중 사용자가 바꾼 것만 (동작 → 키, '' = 그 동작은 단축키 없음). + /// 비어 있으면 (처음) 스타일대로 (83) - ui/explorer_keys.dart + Map keyOverrides = {}; + + /// 84: 폴더 → "기준|desc" (예: "size|1"). 오래된 것부터 지워 300개까지 + Map folderSorts = {}; + + /// 80: 폴더 크기 계산 - 'sort' 크기순 정렬일 때만 (처음 값) · 'always' 늘 · 'off' 안 함 (길게 누르기 [폴더 크기 계산] 은 늘 됨) + String folderSize = 'sort'; + + /// 82: 창 사이 끌어 놓기 - 'ask' 묻기 (처음 값, 관리자 판단) · 'copy' 복사 · 'move' 이동 + String dropAction = 'ask'; + + /// 85: 탐색기의 복사 · 이동 진행 패널을 숨김 (아래 작은 표시만). 처음 값: 보임 + bool progressHidden = false; + + bool showHidden = false; + + /// X-plore 숨기기 (관리자 지시 10/18): 파일 탐색기 목록에서 숨긴 파일 · 폴더의 경로 (파일 자체는 바꾸지 않는다). + /// 지우기 · 이동 대상에서는 빠지지 않는다. 목록 맨 아래 [숨긴 항목 보이기] 로 다시 본다 + List hiddenItems = []; + + /// X-plore 맞추기 (관리자 지시 10/20): [목록에서 숨기기] 전에 무엇이 바뀌는지 확인 창 (처음 켬, 창의 [다음부터 묻지 않기] · 환경 설정에서 끔) + bool hideConfirm = true; + + /// 마지막으로 연 폴더 (왼쪽 · 오른쪽 창) + List paths = []; + + /// 최근에 연 폴더 (내역, 최신이 앞) + List history = []; + + @override + void write(Map j, {required bool plainSecrets}) { + j['explorerLayout'] = layout; + j['explorerOnePaneCopy'] = onePaneCopy; + j['explorerRetapCollapse'] = retapCollapse; + j['explorerTextScale'] = textScale; + j['explorerRowSpacing'] = rowSpacing; + j['batchRenameLast'] = batchRenameLast; + j['explorerRootsInTree'] = rootsInTree; + j['explorerAlwaysMarks'] = alwaysMarks; + j['explorerSwipePanes'] = swipePanes; + j['androidTrashDays'] = androidTrashDays; + j['explorerClick'] = click; + j['explorerStyle'] = style; + j['explorerOrientation'] = orientation; + j['explorerToolbar'] = toolbar; + j['explorerButtons'] = buttons; + j['explorerSort'] = sort; + j['explorerSortDesc'] = sortDesc; + j['explorerSortPerFolder'] = sortPerFolder; + j['explorerSortByKind'] = sortByKind; + j['explorerRemoteThumbs'] = remoteThumbs; + j['explorerRemoteThumbCacheMb'] = remoteThumbCacheMb; + j['explorerKeyOverrides'] = keyOverrides; + j['explorerFolderSorts'] = folderSorts; + j['explorerFolderSize'] = folderSize; + j['explorerDropAction'] = dropAction; + j['explorerProgressHidden'] = progressHidden; + j['explorerShowHidden'] = showHidden; + j['explorerHiddenItems'] = hiddenItems; + j['explorerHideConfirm'] = hideConfirm; + j['explorerPaths'] = paths; + j['explorerHistory'] = history; + // 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다 + j['explorerV2'] = true; + // E3 (10/22): 선택 규칙 (사용자 지시 모음) 의 처음 값으로 한 번 옮겼음 + j['explorerRulesV1'] = true; + j['explorerOrientV3'] = true; + } + + @override + void read(Map j) { + // 'auto' = 넓은 화면은 두 창, 좁은 화면 (폰 세로) 은 한 창. 예전 기본 'dual' 은 한 번 'auto' 로 + onePaneCopy = j['explorerOnePaneCopy'] == 'other' ? 'other' : 'clip'; + // E3: 규칙 이전 판이 저장한 false 는 예전 처음 값 - 한 번 규칙 3 (켬) 으로 + retapCollapse = j['explorerRulesV1'] == true ? j['explorerRetapCollapse'] != false : true; + // 값이 없거나 틀리면 지금 그대로 (100% · 보통), 범위 밖은 끝으로 + textScale = j['explorerTextScale'] is num + ? (j['explorerTextScale'] as num).round().clamp(textScaleMin, textScaleMax) + : 100; + rowSpacing = const {'compact', 'normal', 'wide'}.contains(j['explorerRowSpacing']) ? j['explorerRowSpacing'] as String : 'normal'; + batchRenameLast = j['batchRenameLast'] is Map ? Map.from(j['batchRenameLast'] as Map) : {}; + rootsInTree = j['explorerRootsInTree'] != false; + // E3: 규칙 이전 판이 저장한 true 는 예전 처음 값 - 한 번 규칙 4 (끔) 로 + alwaysMarks = j['explorerRulesV1'] == true && j['explorerAlwaysMarks'] == true; + swipePanes = j['explorerSwipePanes'] == true; + androidTrashDays = switch (j['androidTrashDays']) { final int d when d >= 0 && d <= 365 => d, _ => 14 }; + // E3 (규칙 7 · 결정 3): 처음 값 두 창. 규칙 이전 판의 'auto' 는 예전 처음 값이라 한 번 두 창으로 + layout = switch (j['explorerLayout']) { + 'single' || 'split' => j['explorerLayout'] as String, + 'auto' when j['explorerRulesV1'] == true => 'auto', + _ => 'dual', + }; + // 규칙 2: 두 기기 모두 한 번 = 선택. 규칙 이전 Android 의 'open' 은 예전 처음 값이라 한 번 'select' 로 + click = j['explorerClick'] == 'open' && (j['explorerRulesV1'] == true || !Platform.isAndroid) ? 'open' : 'select'; + style = const ['windows', 'totalcmd'].contains(j['explorerStyle']) ? j['explorerStyle'] as String : 'xplore'; + // 118: 예전 기본 '화면 모양 따라' 는 한 번 좌우로 옮긴다 (사용자가 고른 위아래는 그대로) + orientation = j['explorerOrientation'] == 'stacked' + ? 'stacked' + : j['explorerOrientation'] == 'auto' && j['explorerOrientV3'] == true + ? 'auto' + : 'side'; + toolbar = const ['edge', 'hidden'].contains(j['explorerToolbar']) ? j['explorerToolbar'] as String : 'middle'; + buttons = [for (final x in (j['explorerButtons'] as List?) ?? const []) '$x']; + sort = const ['date', 'size', 'type'].contains(j['explorerSort']) ? j['explorerSort'] as String : 'name'; + sortDesc = j['explorerSortDesc'] as bool? ?? false; + folderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort'; + sortPerFolder = j['explorerSortPerFolder'] == true; + sortByKind = j['explorerSortByKind'] == true; + remoteThumbs = const {'off', 'wifi', 'always'}.contains(j['explorerRemoteThumbs']) ? j['explorerRemoteThumbs'] as String : 'wifi'; + keyOverrides = j['explorerKeyOverrides'] is Map + ? {for (final e in (j['explorerKeyOverrides'] as Map).entries) if (e.key is String && e.value is String) e.key as String: e.value as String} + : {}; + remoteThumbCacheMb = j['explorerRemoteThumbCacheMb'] is num + ? (j['explorerRemoteThumbCacheMb'] as num).round().clamp(10, 10000) + : 200; + progressHidden = j['explorerProgressHidden'] == true; + dropAction = const ['ask', 'copy', 'move'].contains(j['explorerDropAction']) ? j['explorerDropAction'] as String : 'ask'; + folderSorts = { + if (j['explorerFolderSorts'] is Map) + for (final e in (j['explorerFolderSorts'] as Map).entries) + if (e.key is String && e.value is String) e.key as String: e.value as String, + }; + showHidden = j['explorerShowHidden'] as bool? ?? false; + hiddenItems = [for (final x in (j['explorerHiddenItems'] as List?) ?? const []) if (x is String && x.isNotEmpty) x]; + hideConfirm = j['explorerHideConfirm'] as bool? ?? true; + paths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x']; + history = [for (final x in (j['explorerHistory'] as List?) ?? const []) '$x']; + } +} diff --git a/lib/ui/explorer/delete_flow.dart b/lib/ui/explorer/delete_flow.dart index 7ca9f41..6e62b34 100644 --- a/lib/ui/explorer/delete_flow.dart +++ b/lib/ui/explorer/delete_flow.dart @@ -55,7 +55,7 @@ class ExplorerDeleteFlow { var permanent = !canRecycle || shift; // X-plore 숨기기: 지우는 폴더 안에 목록에서 숨긴 것이 있으면 알린다 (숨긴 것도 함께 지워진다) final hiddenInside = [ - for (final h in ctl.c.settings.explorerHiddenItems) + for (final h in ctl.c.settings.explorer.hiddenItems) if (paths.any((x) => !samePath(h, x) && isSameOrInside(h, x))) h ].length; final otherMarks = [for (final x in ctl.panes) if (!identical(x, pane)) x.sel.marked.length].fold(0, (a, b) => a + b); @@ -100,7 +100,7 @@ class ExplorerDeleteFlow { if (canTrash && !permanent) Padding( padding: const EdgeInsets.only(top: 8), - child: Text(trf('앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).', [ctl.c.settings.androidTrashDays]), + child: Text(trf('앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).', [ctl.c.settings.explorer.androidTrashDays]), style: TextStyle(fontSize: 12, color: JjColors.textDim)), ), if (canRecycle) @@ -231,7 +231,7 @@ class ExplorerDeleteFlow { } /// X-plore 비교 e: Android 앱 휴지통 (보관 일이 0 이면 null - 바로 영구 삭제) - AppTrash? get appTrash => androidTrash() && ctl.c.settings.androidTrashDays > 0 + AppTrash? get appTrash => androidTrash() && ctl.c.settings.explorer.androidTrashDays > 0 ? AppTrash([for (final (v, _) in ctl.local) v]) : null; @@ -256,7 +256,7 @@ class ExplorerDeleteFlow { return; } final back = await Navigator.of(context).push>( - MaterialPageRoute(builder: (_) => TrashPage(trash: trash, days: ctl.c.settings.androidTrashDays))); + MaterialPageRoute(builder: (_) => TrashPage(trash: trash, days: ctl.c.settings.explorer.androidTrashDays))); if (back != null && back.isNotEmpty && context.mounted) await ctl.refreshAll(back.map(p.dirname)); } diff --git a/lib/ui/explorer/drop_flow.dart b/lib/ui/explorer/drop_flow.dart index bd98213..3e21810 100644 --- a/lib/ui/explorer/drop_flow.dart +++ b/lib/ui/explorer/drop_flow.dart @@ -45,7 +45,7 @@ class ExplorerDrops { ctl ..active = to ..update(); - final mode = ctl.c.settings.explorerDropAction; + final mode = ctl.c.settings.explorer.dropAction; final move = mode == 'ask' ? await askDropAction(context, load.paths, dest) : mode == 'move'; if (move == null || !context.mounted) return; // 되돌리기에 쓸 것: 대상에 이미 있던 이름 (합친 폴더는 통째로 되돌리지 않게) diff --git a/lib/ui/explorer/explorer_bars.dart b/lib/ui/explorer/explorer_bars.dart index 029d925..6fc77e9 100644 --- a/lib/ui/explorer/explorer_bars.dart +++ b/lib/ui/explorer/explorer_bars.dart @@ -88,7 +88,7 @@ class ExplorerTopBar extends StatelessWidget { child: Row(children: [Icon(b.icon, size: 18), const SizedBox(width: 12), Text(tr(b.label))]), ), // X-plore 숨기기: 큰 폴더를 펼쳐 목록 맨 아래가 멀 때도 (관리자 결정 10/18) - if (!ctl.rsync && ctl.c.settings.explorerHiddenItems.isNotEmpty) + if (!ctl.rsync && ctl.c.settings.explorer.hiddenItems.isNotEmpty) PopupMenuItem( value: 'appHidden', child: Row(children: [ @@ -97,7 +97,7 @@ class ExplorerTopBar extends StatelessWidget { Flexible( child: Text(ctl.showAppHidden ? tr('목록에서 숨긴 항목 다시 숨기기') - : trf('목록에서 숨긴 항목 보이기 ({0}개)', [ctl.c.settings.explorerHiddenItems.length])), + : trf('목록에서 숨긴 항목 보이기 ({0}개)', [ctl.c.settings.explorer.hiddenItems.length])), ), ]), ), diff --git a/lib/ui/explorer/explorer_body.dart b/lib/ui/explorer/explorer_body.dart index bd1c2be..e61b48e 100644 --- a/lib/ui/explorer/explorer_body.dart +++ b/lib/ui/explorer/explorer_body.dart @@ -28,7 +28,7 @@ class ExplorerPanesBody extends StatelessWidget { @override Widget build(BuildContext context) { final s = ctl.c.settings; - final shape = '${s.explorerStyle}|${s.explorerLayout}|${s.explorerOrientation}|${s.explorerToolbar}'; + final shape = '${s.explorer.style}|${s.explorer.layout}|${s.explorer.orientation}|${s.explorer.toolbar}'; if (ctl.ready && shape != layout.shape) { if (layout.shape.isNotEmpty) { for (final pane in ctl.panes) { @@ -41,10 +41,10 @@ class ExplorerPanesBody extends StatelessWidget { return LayoutBuilder(builder: (context, box) { // 두 창이 다 들어가는지: 트리 (화살표 · 아이콘 · 여백 78 + 이름 40) + 목록 (아이콘 · ✔ · 여백 104 + 이름 50), 이름은 글자 배율만큼. // 1배면 360 폭에도 들어가고, 기기 글자 1.3 × 150% (약 2배) 면 412 폭에도 모자란다 - final wantSplit = !ctl.rsync && s.explorerLayout == 'split'; + final wantSplit = !ctl.rsync && s.explorer.layout == 'split'; final textScale = MediaQuery.textScalerOf(context).scale(14) / 14 * layout.listTextScale; - final barW = s.explorerToolbar == 'hidden' ? 0.0 : 72.0; - final sideNow = switch (s.explorerOrientation) { 'side' => true, 'stacked' => false, _ => box.maxWidth >= box.maxHeight }; + final barW = s.explorer.toolbar == 'hidden' ? 0.0 : 72.0; + final sideNow = switch (s.explorer.orientation) { 'side' => true, 'stacked' => false, _ => box.maxWidth >= box.maxHeight }; final tightWas = layout.splitTight; layout.splitTight = wantSplit && sideNow && box.maxWidth - barW < 182 + 90 * textScale; // 바뀌면 이 builder 밖 (버튼 이름 "두 창" 등) 도 한 번 다시 그린다 @@ -57,7 +57,7 @@ class ExplorerPanesBody extends StatelessWidget { // 폭이 바뀌면 (좁은 한 창 ↔ 두 창) 이 builder 만 다시 불린다 - 그때도 새로 붙은 창을 보이게 if (ctl.ready) layout.revealNewViews(); final side = !(dual || split) || - switch (s.explorerOrientation) { + switch (s.explorer.orientation) { 'side' => true, 'stacked' => false, _ => box.maxWidth >= box.maxHeight, @@ -66,13 +66,13 @@ class ExplorerPanesBody extends StatelessWidget { // (두 창 + 가운데 버튼 줄이면 창이 140px 남짓이라 폴더 이름이 잘렸다) final rsyncOne = ctl.rsync && side && box.maxWidth < rsyncTwoPaneMinWidth; layout.narrowSplit = split && side && box.maxWidth < ExplorerLayout.narrowSplitWidth; - final bar = s.explorerToolbar == 'hidden' && !ctl.rsync + final bar = s.explorer.toolbar == 'hidden' && !ctl.rsync ? null : ExplorerToolbar( // X-plore b: 길게 누르기 = 짝 동작 (Rsync 화면은 없음) onLongPress: ctl.rsync ? null : buttons.longPress, // Rsync 화면: 늘 같은 버튼 (→ ← ⇄ 모니터링) 을 두 창 사이에 - buttons: ctl.rsync ? ExplorerButton.rsyncButtons : ExplorerButton.fromSettings(s.explorerButtons), + buttons: ctl.rsync ? ExplorerButton.rsyncButtons : ExplorerButton.fromSettings(s.explorer.buttons), vertical: side && !rsyncOne, panesSide: side, onPressed: buttons.press, @@ -81,12 +81,12 @@ class ExplorerPanesBody extends StatelessWidget { _ => true, }, selected: (b) => - (b == ExplorerButton.hidden && s.explorerShowHidden) || (b == ExplorerButton.select && sel.selecting), + (b == ExplorerButton.hidden && s.explorer.showHidden) || (b == ExplorerButton.select && sel.selecting), twoPanes: dual || split, pinned: {ExplorerButton.orient, if (flows.transfers.clip.isNotEmpty) ExplorerButton.paste}, ); - final middle = bar != null && (ctl.rsync || s.explorerToolbar == 'middle'); - final edge = bar != null && !ctl.rsync && s.explorerToolbar == 'edge'; + final middle = bar != null && (ctl.rsync || s.explorer.toolbar == 'middle'); + final edge = bar != null && !ctl.rsync && s.explorer.toolbar == 'edge'; // 180: 창마다 같은 열쇠 - 가로 ↔ 세로 배치가 바뀌어도 (창 크기 · 작업 현황이 열림) 창을 새로 만들지 않는다 (스크롤이 맨 위로 가지 않게) Widget keep(int k, Widget w) => KeyedSubtree(key: layout.viewKeys[k], child: w); if (rsyncOne) { diff --git a/lib/ui/explorer/explorer_buttons.dart b/lib/ui/explorer/explorer_buttons.dart index 0ca9c8c..27e2f48 100644 --- a/lib/ui/explorer/explorer_buttons.dart +++ b/lib/ui/explorer/explorer_buttons.dart @@ -225,14 +225,14 @@ class ExplorerButtons { case ExplorerButton.sort: await flows.dialogs.sortDialog(cmd.context); case ExplorerButton.hidden: - await ctl.c.updateSettings((s) => s.explorerShowHidden = !s.explorerShowHidden); + await ctl.c.updateSettings((s) => s.explorer.showHidden = !s.explorer.showHidden); for (final x in ctl.panes) { x.cache.clear(); for (final d in x.expanded.toList()) { await ctl.load(x, d, force: true); } } - ctl.host.snack(ctl.c.settings.explorerShowHidden ? tr('숨은 항목을 보입니다.') : tr('숨은 항목을 감춥니다.')); + ctl.host.snack(ctl.c.settings.explorer.showHidden ? tr('숨은 항목을 보입니다.') : tr('숨은 항목을 감춥니다.')); case ExplorerButton.history: await flows.dialogs.historyDialog(cmd.context, pane); case ExplorerButton.bookmarks: diff --git a/lib/ui/explorer/explorer_controller.dart b/lib/ui/explorer/explorer_controller.dart index ac76b66..952abb8 100644 --- a/lib/ui/explorer/explorer_controller.dart +++ b/lib/ui/explorer/explorer_controller.dart @@ -147,7 +147,7 @@ class ExplorerController extends ChangeNotifier { final s = sizes.of(e.path); if (s != null) return s.label; if (sizes.busy(e.path)) return '…'; - final mode = c.settings.explorerFolderSize; + final mode = c.settings.explorer.folderSize; if ((mode == 'always' || (mode == 'sort' && sortOf(q.current).$1 == 'size')) && samePath(vDirname(e.path), q.current) && !isDav(e.path)) { @@ -165,7 +165,7 @@ class ExplorerController extends ChangeNotifier { Future load(ExplorerPane q, String dir, {bool force = false}) async { final r = await loadDir(q, dir, force: force, - showHidden: c.settings.explorerShowHidden, + showHidden: c.settings.explorer.showHidden, sort: sorted, inside: _insideArchiveList, osBlocked: (d) => isAndroidRestricted(d) ? androidRestrictedError : null, @@ -272,15 +272,15 @@ class ExplorerController extends ChangeNotifier { /// [dir] 의 정렬 (기준, 거꾸로) - 폴더마다 기억한 것 → 종류별 기본 정렬 → 전체 정렬 (String, bool) sortOf(String dir, {List? list}) { final s = c.settings; - if (s.explorerSortPerFolder || s.explorerSortByKind) { - final v = s.explorerFolderSorts[_sortKey(dir)]; + if (s.explorer.sortPerFolder || s.explorer.sortByKind) { + final v = s.explorer.folderSorts[_sortKey(dir)]; if (v != null && v.contains('|')) return (v.split('|').first, v.endsWith('|1')); } - if (s.explorerSortByKind) { + if (s.explorer.sortByKind) { final k = _kindSort(dir, list: list); if (k != null) return k; } - return (s.explorerSort, s.explorerSortDesc); + return (s.explorer.sort, s.explorer.sortDesc); } /// X-plore 비교 5-1 ①: 파일의 반 넘게가 사진 · 동영상이면 날짜 (새것 먼저), 음악이면 이름, 그 밖은 null @@ -301,19 +301,19 @@ class ExplorerController extends ChangeNotifier { /// 정렬을 바꾼다 - "폴더마다 기억" (또는 종류별 정렬이 쓰이는 폴더) 이면 [dir] 에만, 아니면 전체. 모든 창을 다시 줄 세운다 Future setSort(String dir, String by, bool desc) async { - final kindHere = c.settings.explorerSortByKind && _kindSort(dir) != null; + final kindHere = c.settings.explorer.sortByKind && _kindSort(dir) != null; await c.updateSettings((x) { - if (x.explorerSortPerFolder || kindHere) { - final m = Map.of(x.explorerFolderSorts)..remove(_sortKey(dir)); + if (x.explorer.sortPerFolder || kindHere) { + final m = Map.of(x.explorer.folderSorts)..remove(_sortKey(dir)); m[_sortKey(dir)] = '$by|${desc ? 1 : 0}'; while (m.length > 300) { m.remove(m.keys.first); } - x.explorerFolderSorts = m; + x.explorer.folderSorts = m; } else { x - ..explorerSort = by - ..explorerSortDesc = desc; + ..explorer.sort = by + ..explorer.sortDesc = desc; } }); resortAll(); @@ -409,18 +409,18 @@ class ExplorerController extends ChangeNotifier { if (rsync) { unawaited(c.updateSettings((s) => s.rsyncPaths = paths)); } else { - final hist = [dir, ...c.settings.explorerHistory.where((h) => !samePath(h, dir))].take(30).toList(); + final hist = [dir, ...c.settings.explorer.history.where((h) => !samePath(h, dir))].take(30).toList(); unawaited(c.updateSettings((s) => s - ..explorerPaths = paths - ..explorerHistory = hist)); + ..explorer.paths = paths + ..explorer.history = hist)); } if (i >= 0) active = i; } // ───────── 줄 목록 · 목록에서 숨기기 ───────── - ExplorerStyle get look => ExplorerStyle.of(c.settings.explorerStyle); - bool get rootsInTree => !rsync && look == ExplorerStyle.xplore && c.settings.explorerRootsInTree; + ExplorerStyle get look => ExplorerStyle.of(c.settings.explorer.style); + bool get rootsInTree => !rsync && look == ExplorerStyle.xplore && c.settings.explorer.rootsInTree; List rows(ExplorerPane q, {bool foldersOnly = false}) => buildTreeRows(q, foldersOnly: foldersOnly, @@ -430,12 +430,12 @@ class ExplorerController extends ChangeNotifier { showHidden: showAppHidden, browsable: arc.contains); - bool appHidden(String path) => hidden.isHidden(c.settings.explorerHiddenItems, path); + bool appHidden(String path) => hidden.isHidden(c.settings.explorer.hiddenItems, path); /// 옮기거나 이름을 바꾼 것은 숨긴 목록도 새 경로로, 지운 것은 목록에서 뺀다 (관리자 결정 10/18) Future followHidden({Iterable<(String, String)> moved = const [], Iterable gone = const []}) async { - final out = await HiddenList.follow(c.settings.explorerHiddenItems, moved: moved, gone: gone); - if (out != null) await c.updateSettings((s) => s.explorerHiddenItems = out); + final out = await HiddenList.follow(c.settings.explorer.hiddenItems, moved: moved, gone: gone); + if (out != null) await c.updateSettings((s) => s.explorer.hiddenItems = out); } void toggleAppHidden() { diff --git a/lib/ui/explorer/explorer_jobs.dart b/lib/ui/explorer/explorer_jobs.dart index fa2c082..a2228e0 100644 --- a/lib/ui/explorer/explorer_jobs.dart +++ b/lib/ui/explorer/explorer_jobs.dart @@ -40,7 +40,7 @@ class ExplorerJobs extends ChangeNotifier { /// X-plore 숨기기: 옮긴 것은 숨긴 목록도 새 자리로 (원래 자리에 없고 새 자리에 있는 것만 - 같은 이름 바꾸기 · 실패는 그대로) Future followMoved(TransferJob job) async { - if (!job.move || job.contents || ctl.c.settings.explorerHiddenItems.isEmpty) return; + if (!job.move || job.contents || ctl.c.settings.explorer.hiddenItems.isEmpty) return; final moved = <(String, String)>[]; for (final s in job.sources) { final to = vJoin(job.dest, vBasename(s)); diff --git a/lib/ui/explorer/explorer_layout.dart b/lib/ui/explorer/explorer_layout.dart index 8d7cff7..cd28b1b 100644 --- a/lib/ui/explorer/explorer_layout.dart +++ b/lib/ui/explorer/explorer_layout.dart @@ -52,11 +52,11 @@ class ExplorerLayout { }); /// X-plore 비교 40: 목록 글자 크기 (설정 %) · 줄 간격을 넣은 줄 높이 - double get listTextScale => ctl.c.settings.explorerTextScale / 100; - double rowHeight() => ctl.look.rowHeightOf(context(), textScale: listTextScale, spacing: rowSpacingFactor(ctl.c.settings.explorerRowSpacing)); + double get listTextScale => ctl.c.settings.explorer.textScale / 100; + double rowHeight() => ctl.look.rowHeightOf(context(), textScale: listTextScale, spacing: rowSpacingFactor(ctl.c.settings.explorer.rowSpacing)); /// 왼쪽 폴더 트리 + 오른쪽 파일 목록 배치 - bool get split => !ctl.rsync && ctl.c.settings.explorerLayout == 'split' && !splitTight; + bool get split => !ctl.rsync && ctl.c.settings.explorer.layout == 'split' && !splitTight; /// 관리자 지시 10/20 (28 검토): 글자가 커서 (기기 글자 × 목록 글자 크기) 좁은 화면에 트리 · 목록 두 창이 다 들어가지 않으면 /// 그동안만 한 창 (트리 + 위 경로 막대) 으로 보인다 - 고른 배치 (설정) 는 그대로, 화면이 넓어지거나 글자가 작아지면 다시 두 창. [ExplorerPanesBody] 가 정한다 @@ -70,18 +70,18 @@ class ExplorerLayout { /// 좌우 두 창 (Rsync 화면은 늘) bool get dual => ctl.rsync || - ctl.c.settings.explorerLayout == 'dual' || + ctl.c.settings.explorer.layout == 'dual' || // 자동: 넓으면 두 창, 좁은 화면 (폰 세로) 은 한 창 - 같은 목록이 반씩 잘려 두 번 보이지 않게 - (ctl.c.settings.explorerLayout == 'auto' && !isCompact(context())); + (ctl.c.settings.explorer.layout == 'auto' && !isCompact(context())); /// 트리 줄. [foldersOnly]: 폴더만 ("폴더 + 파일 목록" 배치의 왼쪽) /// X-plore 비교 c: 트리 맨 위에 저장 장치 · 서버 · 즐겨찾기 (X-plore 스타일 · 설정) /// X-plore 비교 ⚖: X-plore 스타일에서 표시 동그라미 늘 보이기 (설정) - bool get alwaysMarks => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorerAlwaysMarks; + bool get alwaysMarks => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorer.alwaysMarks; /// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 밀기 = 창 바꾸기 (설정) - bool get swipePanes => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorerSwipePanes; + bool get swipePanes => !ctl.rsync && ctl.look == ExplorerStyle.xplore && ctl.c.settings.explorer.swipePanes; /// 밀어서 창 바꾸기: 두 창이면 다른 창을 활성으로, 한 창이면 숨은 창과 바꿔 보인다 bool switchPane(int dir) { @@ -104,5 +104,5 @@ class ExplorerLayout { ExplorerPane? get other => dual || hiddenTarget ? ctl.panes[1 - ctl.active] : null; /// X-plore 비교 a: 한 창인데 "숨은 다른 창으로" 복사 · 이동 (위에 갈 곳 줄) - bool get hiddenTarget => !ctl.rsync && !dual && !split && ctl.c.settings.explorerOnePaneCopy == 'other'; + bool get hiddenTarget => !ctl.rsync && !dual && !split && ctl.c.settings.explorer.onePaneCopy == 'other'; } diff --git a/lib/ui/explorer/explorer_select.dart b/lib/ui/explorer/explorer_select.dart index 4139a63..d63132c 100644 --- a/lib/ui/explorer/explorer_select.dart +++ b/lib/ui/explorer/explorer_select.dart @@ -30,7 +30,7 @@ class ExplorerSelect extends ChangeNotifier { bool selecting = false; /// 한 번 누르면 선택 · 두 번 누르면 열기 (아니면 한 번에 바로 열기) - bool get selectMode => ctl.c.settings.explorerClick != 'open'; + bool get selectMode => ctl.c.settings.explorer.click != 'open'; /// 마지막으로 누른 폴더 · 시각 (두 번 누르기의 두 번째를 가리려고) (String, DateTime)? _lastFolderTap; @@ -83,7 +83,7 @@ class ExplorerSelect extends ChangeNotifier { list: list, repeat: repeat, tapOpens: !selectMode, - retapCollapses: ctl.c.settings.explorerRetapCollapse); + retapCollapses: ctl.c.settings.explorer.retapCollapse); await _act(pane, row, act, list: list); } diff --git a/lib/ui/explorer/item_actions.dart b/lib/ui/explorer/item_actions.dart index 00bf4c3..6930ba8 100644 --- a/lib/ui/explorer/item_actions.dart +++ b/lib/ui/explorer/item_actions.dart @@ -112,7 +112,7 @@ Future confirmAppHide(BuildContext context, ExplorerController ctl, List s.explorerHideConfirm = false); + if (dontAsk) await ctl.c.updateSettings((s) => s.explorer.hideConfirm = false); return true; } diff --git a/lib/ui/explorer/item_menu.dart b/lib/ui/explorer/item_menu.dart index 56875b1..1dc0f6d 100644 --- a/lib/ui/explorer/item_menu.dart +++ b/lib/ui/explorer/item_menu.dart @@ -233,9 +233,9 @@ class ExplorerItemMenu { pane.changed(); } case 'appHide': - if (ctl.c.settings.explorerHideConfirm && !await confirmAppHide(context, ctl, targets)) return; - await ctl.c.updateSettings((s) => s.explorerHiddenItems = [ - ...s.explorerHiddenItems, + if (ctl.c.settings.explorer.hideConfirm && !await confirmAppHide(context, ctl, targets)) return; + await ctl.c.updateSettings((s) => s.explorer.hiddenItems = [ + ...s.explorer.hiddenItems, for (final x in targets) if (!ctl.appHidden(x)) x, ]); @@ -249,8 +249,8 @@ class ExplorerItemMenu { : trf('목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.', [targets.length])); case 'appUnhide': final keys = {for (final x in targets) HiddenList.keyOf(x)}; - await ctl.c.updateSettings((s) => s.explorerHiddenItems = [ - for (final x in s.explorerHiddenItems) + await ctl.c.updateSettings((s) => s.explorer.hiddenItems = [ + for (final x in s.explorer.hiddenItems) if (!keys.contains(HiddenList.keyOf(x))) x, ]); for (final q in ctl.panes) { diff --git a/lib/ui/explorer/key_flow.dart b/lib/ui/explorer/key_flow.dart index 79abfe7..82f1f24 100644 --- a/lib/ui/explorer/key_flow.dart +++ b/lib/ui/explorer/key_flow.dart @@ -117,7 +117,7 @@ class ExplorerInput { if (const {'delete', 'deletePermanent', 'rename', 'refresh', 'filter', 'up', 'back', 'forward', 'switchPane'}.contains(e.key)) e.key: e.value, } - : effectiveExplorerKeys(tc: _tcKeys, overrides: ctl.c.settings.explorerKeyOverrides); + : effectiveExplorerKeys(tc: _tcKeys, overrides: ctl.c.settings.explorer.keyOverrides); return { for (final e in keys.entries) if (cb[e.key] != null) diff --git a/lib/ui/explorer/open_flow.dart b/lib/ui/explorer/open_flow.dart index 72e7b6b..5663b0d 100644 --- a/lib/ui/explorer/open_flow.dart +++ b/lib/ui/explorer/open_flow.dart @@ -131,7 +131,7 @@ class ExplorerOpener { // 폴더를 읽지 못하면 (E10: 이제 오류로 알림) 고른 그림 하나만 List list; try { - list = pane?.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorerShowHidden), dir); + list = pane?.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorer.showHidden), dir); } catch (_) { list = const []; } @@ -150,7 +150,7 @@ class ExplorerOpener { Future slideshow(BuildContext context, ExplorerPane pane, String dir) async { List list; try { - list = pane.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorerShowHidden), dir); + list = pane.cache[dir] ?? ctl.sorted(await listEntries(dir, showHidden: ctl.c.settings.explorer.showHidden), dir); } catch (e) { ctl.host.snack(trf('폴더를 읽을 수 없습니다: {0}', [e])); return; diff --git a/lib/ui/explorer/rename_flow.dart b/lib/ui/explorer/rename_flow.dart index 089e481..fdae3fb 100644 --- a/lib/ui/explorer/rename_flow.dart +++ b/lib/ui/explorer/rename_flow.dart @@ -51,8 +51,8 @@ class ExplorerRenames { }); final done = await showBatchRename(context, ordered, isDir: vIsDirSync, - initial: RenameRule.fromJson(ctl.c.settings.batchRenameLast), - onRun: (r) => ctl.c.updateSettings((s) => s.batchRenameLast = r.toJson())); + initial: RenameRule.fromJson(ctl.c.settings.explorer.batchRenameLast), + onRun: (r) => ctl.c.updateSettings((s) => s.explorer.batchRenameLast = r.toJson())); if (done == null || done.isEmpty || !context.mounted) return; await _after(pane, done); if (!context.mounted) return; diff --git a/lib/ui/explorer/row_view.dart b/lib/ui/explorer/row_view.dart index 833199c..e00b46c 100644 --- a/lib/ui/explorer/row_view.dart +++ b/lib/ui/explorer/row_view.dart @@ -299,7 +299,7 @@ class ExplorerRowView extends StatelessWidget { if (up) return Icon(Icons.arrow_upward, size: look.iconSize, color: JjColors.textDim); if (isRoot) return Icon(isDav(e.path) ? Icons.cloud : Icons.sd_storage, color: JjColors.accent, size: look.iconSize - 2); // X-plore 비교 5-1 ② (관리자 지시 10/21): 원격은 설정 (처음 값 Wi-Fi 에서만) 에 따라 받아서 - 받기 전 · 못 받으면 아이콘 - if (look.rich && isDav(e.path) && ctl.c.settings.explorerRemoteThumbs != 'off' && remoteThumbKind(e)) { + if (look.rich && isDav(e.path) && ctl.c.settings.explorer.remoteThumbs != 'off' && remoteThumbKind(e)) { final (icon, color) = fileIcon(look, e, open: open); return RemoteThumb( key: ValueKey('rthumb:${e.path}'), c: ctl.c, entry: e, fallback: Icon(icon, size: 28, color: color)); diff --git a/lib/ui/explorer/search_flow.dart b/lib/ui/explorer/search_flow.dart index b94540e..1d1f00e 100644 --- a/lib/ui/explorer/search_flow.dart +++ b/lib/ui/explorer/search_flow.dart @@ -85,7 +85,7 @@ class ExplorerSearch extends ChangeNotifier { builder: (_) => SearchResultsPage( root: root, cond: cond, - showHidden: ctl.c.settings.explorerShowHidden, + showHidden: ctl.c.settings.explorer.showHidden, otherDir: other, onOpen: (e) => opener.open(context, e.path, pane: pane), initial: initial, diff --git a/lib/ui/explorer/transfer_panel.dart b/lib/ui/explorer/transfer_panel.dart index 0ab5ed6..f58e8a2 100644 --- a/lib/ui/explorer/transfer_panel.dart +++ b/lib/ui/explorer/transfer_panel.dart @@ -21,7 +21,7 @@ class ExplorerTransferPanel extends StatelessWidget { @override Widget build(BuildContext context) { // 85: 숨겼으면 오른쪽 아래 작은 표시 "복사 · 이동 n개 · 45%" (누르면 다시 펼침) - if (jobs.isNotEmpty && c.settings.explorerProgressHidden) { + if (jobs.isNotEmpty && c.settings.explorer.progressHidden) { return Align( alignment: Alignment.bottomRight, child: Padding( @@ -39,7 +39,7 @@ class ExplorerTransferPanel extends StatelessWidget { running: run.length, percent: pct, failed: failed, - onTap: () => c.updateSettings((x) => x.explorerProgressHidden = false)); + onTap: () => c.updateSettings((x) => x.explorer.progressHidden = false)); }, ), ), @@ -69,7 +69,7 @@ class ExplorerTransferPanel extends StatelessWidget { height: 28, child: TextButton.icon( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), - onPressed: () => c.updateSettings((x) => x.explorerProgressHidden = true), + onPressed: () => c.updateSettings((x) => x.explorer.progressHidden = true), icon: const Icon(Icons.expand_more, size: 18), label: Text(tr('숨기기'), style: const TextStyle(fontSize: 12)), ), diff --git a/lib/ui/explorer/view_dialogs.dart b/lib/ui/explorer/view_dialogs.dart index ccde0b4..bf722b7 100644 --- a/lib/ui/explorer/view_dialogs.dart +++ b/lib/ui/explorer/view_dialogs.dart @@ -38,7 +38,7 @@ class ExplorerViewDialogs { ChoiceChip(label: Text(label), selected: by == v, onSelected: (_) => set(() => by = v)), ]), SwitchListTile(value: desc, title: Text(tr('거꾸로')), onChanged: (x) => set(() => desc = x)), - if (ctl.c.settings.explorerSortPerFolder) + if (ctl.c.settings.explorer.sortPerFolder) Text(trf('이 폴더에만: {0}', [ctl.displayPath(dir)]), style: TextStyle(fontSize: 12, color: JjColors.textDim)), ]), actions: [ @@ -74,7 +74,7 @@ class ExplorerViewDialogs { } Future historyDialog(BuildContext context, ExplorerPane pane) async { - final hist = ctl.c.settings.explorerHistory.where(reachable).toList(); + final hist = ctl.c.settings.explorer.history.where(reachable).toList(); final pick = await showDialog( context: context, builder: (ctx) => SimpleDialog( @@ -97,17 +97,17 @@ class ExplorerViewDialogs { final s = ctl.c.settings; if (!twoPanes()) { await ctl.c.updateSettings((x) => x - ..explorerLayout = 'dual' - ..explorerOrientation = 'side'); + ..explorer.layout = 'dual' + ..explorer.orientation = 'side'); } else { // 지금 보이는 배치의 반대 ('화면 모양 따라' 였으면 지금 모양에서 정함) final size = MediaQuery.sizeOf(context); - final nowSide = switch (s.explorerOrientation) { + final nowSide = switch (s.explorer.orientation) { 'side' => true, 'stacked' => false, _ => size.width >= size.height, }; - await ctl.c.updateSettings((x) => x.explorerOrientation = nowSide ? 'stacked' : 'side'); + await ctl.c.updateSettings((x) => x.explorer.orientation = nowSide ? 'stacked' : 'side'); } if (!context.mounted) return; ctl.update(); @@ -118,7 +118,7 @@ class ExplorerViewDialogs { Future layoutDialog(BuildContext context) async { final s = ctl.c.settings; - var layout = s.explorerLayout, orient = s.explorerOrientation, bar = s.explorerToolbar; + var layout = s.explorer.layout, orient = s.explorer.orientation, bar = s.explorer.toolbar; final ok = await showDialog( context: context, builder: (ctx) => StatefulBuilder( @@ -181,9 +181,9 @@ class ExplorerViewDialogs { ); if (ok != true) return; await ctl.c.updateSettings((x) => x - ..explorerLayout = layout - ..explorerOrientation = orient - ..explorerToolbar = bar); + ..explorer.layout = layout + ..explorer.orientation = orient + ..explorer.toolbar = bar); if (layout == 'single') { ctl ..active = 0 @@ -196,7 +196,7 @@ class ExplorerViewDialogs { } Future buttonsDialog(BuildContext context) async { - final current = ExplorerButton.fromSettings(ctl.c.settings.explorerButtons); + final current = ExplorerButton.fromSettings(ctl.c.settings.explorer.buttons); final order = [...current, ...ExplorerButton.explorerValues.where((b) => !current.contains(b))]; final on = {...current}; final ok = await showDialog( @@ -251,6 +251,6 @@ class ExplorerViewDialogs { ); if (ok != true) return; final names = [for (final b in order) if (on.contains(b)) b.name]; - await ctl.c.updateSettings((x) => x.explorerButtons = names.isEmpty ? [ExplorerButton.layout.name] : names); + await ctl.c.updateSettings((x) => x.explorer.buttons = names.isEmpty ? [ExplorerButton.layout.name] : names); } } diff --git a/lib/ui/explorer_keys_page.dart b/lib/ui/explorer_keys_page.dart index fbc955a..a4cef9f 100644 --- a/lib/ui/explorer_keys_page.dart +++ b/lib/ui/explorer_keys_page.dart @@ -16,7 +16,7 @@ class ExplorerKeysPage extends StatelessWidget { static Future open(BuildContext context, AppController c) => Navigator.of(context).push(MaterialPageRoute(builder: (_) => ExplorerKeysPage(c: c))); - bool get _tc => ExplorerStyle.of(c.settings.explorerStyle) == ExplorerStyle.totalcmd; + bool get _tc => ExplorerStyle.of(c.settings.explorer.style) == ExplorerStyle.totalcmd; Future _change(BuildContext context, String id) async { final picked = await showDialog( @@ -24,7 +24,7 @@ class ExplorerKeysPage extends StatelessWidget { builder: (_) => _KeyCaptureDialog(c: c, action: id, tc: _tc), ); if (picked == null) return; - await c.updateSettings((x) => x.explorerKeyOverrides = {...x.explorerKeyOverrides, id: _none(picked) ? '' : keySpec(picked)}); + await c.updateSettings((x) => x.explorer.keyOverrides = {...x.explorer.keyOverrides, id: _none(picked) ? '' : keySpec(picked)}); } static bool _none(SingleActivator a) => a.trigger == LogicalKeyboardKey.abort; @@ -33,7 +33,7 @@ class ExplorerKeysPage extends StatelessWidget { Widget build(BuildContext context) => ListenableBuilder( listenable: c, builder: (context, _) { - final ov = c.settings.explorerKeyOverrides; + final ov = c.settings.explorer.keyOverrides; final keys = effectiveExplorerKeys(tc: _tc, overrides: ov); return Scaffold( appBar: AppBar( @@ -41,7 +41,7 @@ class ExplorerKeysPage extends StatelessWidget { actions: [ if (ov.isNotEmpty) TextButton( - onPressed: () => c.updateSettings((x) => x.explorerKeyOverrides = {}), + onPressed: () => c.updateSettings((x) => x.explorer.keyOverrides = {}), child: Text(tr('모두 처음 값으로')), ), ], @@ -51,7 +51,7 @@ class ExplorerKeysPage extends StatelessWidget { padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), child: Text( trf('지금 스타일 ({0}) 의 키입니다. 바꾼 키는 모든 스타일에 쓰이고, 다른 동작과 겹치면 그 동작의 키는 없어집니다. Esc 는 바꿀 수 없습니다', - [ExplorerStyle.of(c.settings.explorerStyle).label]), + [ExplorerStyle.of(c.settings.explorer.style).label]), style: TextStyle(fontSize: 12, color: JjColors.textDim)), ), for (final id in explorerKeyActions) @@ -65,7 +65,7 @@ class ExplorerKeysPage extends StatelessWidget { trailing: Wrap(spacing: 4, children: [ if (ov.containsKey(id)) TextButton( - onPressed: () => c.updateSettings((x) => x.explorerKeyOverrides = {...x.explorerKeyOverrides}..remove(id)), + onPressed: () => c.updateSettings((x) => x.explorer.keyOverrides = {...x.explorer.keyOverrides}..remove(id)), child: Text(tr('처음 값으로')), ), OutlinedButton(onPressed: () => _change(context, id), child: Text(tr('바꾸기'))), @@ -101,7 +101,7 @@ class _KeyCaptureDialogState extends State<_KeyCaptureDialog> { @override Widget build(BuildContext context) { final k = _key; - final clash = k == null ? null : keyConflict(widget.action, k, tc: widget.tc, overrides: widget.c.settings.explorerKeyOverrides); + final clash = k == null ? null : keyConflict(widget.action, k, tc: widget.tc, overrides: widget.c.settings.explorer.keyOverrides); return AlertDialog( title: Text(trf('{0} - 단축키', [explorerKeyLabel(widget.action)])), content: Focus( diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index e490db2..b97abf6 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -148,8 +148,8 @@ class _ExplorerPageState extends State implements ExplorerHost, Ex pane.addListener(_onPane); } _resumeReload = AppLifecycleListener(onResume: () => unawaited(_ctl.reloadOpen(localOnly: true))); - _hiddenSeen = c.settings.explorerHiddenItems; - _kindSeen = c.settings.explorerSortByKind; + _hiddenSeen = c.settings.explorer.hiddenItems; + _kindSeen = c.settings.explorer.sortByKind; c.addListener(_onSettings); _init(); } @@ -182,9 +182,9 @@ class _ExplorerPageState extends State implements ExplorerHost, Ex _ctl.local = await widget.device.listVolumes(); if (_ctl.local.isEmpty) _ctl.local = [(Platform.isWindows ? r'C:\' : '/', Platform.isWindows ? 'C:' : '/')]; // X-plore 비교 e: 보관 일이 지난 휴지통 항목을 지운다 (뒤에서) - if (_flows.deletes.appTrash case final t?) unawaited(t.purgeOlderThan(c.settings.androidTrashDays).catchError((_) => 0)); + if (_flows.deletes.appTrash case final t?) unawaited(t.purgeOlderThan(c.settings.explorer.androidTrashDays).catchError((_) => 0)); if (Platform.isAndroid) unawaited(_access.checkAccess()); - final saved = widget.rsync ? c.settings.rsyncPaths : c.settings.explorerPaths; + final saved = widget.rsync ? c.settings.rsyncPaths : c.settings.explorer.paths; // 창마다 따로 읽는다: 한 창이 WebDAV 서버를 기다려도 다른 창 (이 기기 파일) 은 바로 보이게 // 165: 지난 폴더가 느린 네트워크 드라이브여도 화면이 멈추지 않게 (뒤에서 확인, 오래 걸리면 그냥 열어 본다 - 못 읽으면 그 창에 이유) final reach = await Future.wait( @@ -238,7 +238,7 @@ class _ExplorerPageState extends State implements ExplorerHost, Ex void _onSettings() { if (!mounted) return; - if (_kindSeen != null && _kindSeen != c.settings.explorerSortByKind) { + if (_kindSeen != null && _kindSeen != c.settings.explorer.sortByKind) { for (final pane in _ctl.panes) { for (final k in pane.cache.keys.toList()) { pane.cache[k] = _ctl.sorted(pane.cache[k]!, k); @@ -246,8 +246,8 @@ class _ExplorerPageState extends State implements ExplorerHost, Ex pane.changed(); } } - _kindSeen = c.settings.explorerSortByKind; - final now = c.settings.explorerHiddenItems; + _kindSeen = c.settings.explorer.sortByKind; + final now = c.settings.explorer.hiddenItems; if (identical(now, _hiddenSeen)) return; _hiddenSeen = now; setState(() {}); diff --git a/lib/ui/hidden_items_dialog.dart b/lib/ui/hidden_items_dialog.dart index a13f430..e3e1a88 100644 --- a/lib/ui/hidden_items_dialog.dart +++ b/lib/ui/hidden_items_dialog.dart @@ -12,10 +12,10 @@ import 'theme.dart'; Future showHiddenItems(BuildContext context, AppController c) => showDialog( context: context, builder: (ctx) => StatefulBuilder(builder: (ctx, set) { - final list = c.settings.explorerHiddenItems; + final list = c.settings.explorer.hiddenItems; Future unhide(List which) async { - await c.updateSettings((s) => s.explorerHiddenItems = [ - for (final x in s.explorerHiddenItems) + await c.updateSettings((s) => s.explorer.hiddenItems = [ + for (final x in s.explorer.hiddenItems) if (!which.any((w) => samePath(w, x))) x, ]); set(() {}); diff --git a/lib/ui/remote_thumb_view.dart b/lib/ui/remote_thumb_view.dart index 00f058a..7277924 100644 --- a/lib/ui/remote_thumb_view.dart +++ b/lib/ui/remote_thumb_view.dart @@ -23,7 +23,7 @@ Future remoteThumbsOf(AppController c) => _hub ??= () async { final dir = Directory(p.join(await c.services.storage.tempDirectory(), 'jj_rthumbs')); final h = RemoteThumbs( dir: dir, - maxBytes: () => c.settings.explorerRemoteThumbCacheMb * 1024 * 1024, + maxBytes: () => c.settings.explorer.remoteThumbCacheMb * 1024 * 1024, allowed: () => remoteThumbsAllowedNow(c), make: (path, out) => _make(c, path, out), ); @@ -75,7 +75,7 @@ bool _metered = true; /// 지금 받아도 되는지: 'off' 아니오 · 'always' 예 · 'wifi' (처음 값) 는 데이터 요금제가 아닌 망에서만 (10초 동안 기억) Future remoteThumbsAllowedNow(AppController c) async { - switch (c.settings.explorerRemoteThumbs) { + switch (c.settings.explorer.remoteThumbs) { case 'off': return false; case 'always': diff --git a/lib/ui/settings/files_section.dart b/lib/ui/settings/files_section.dart index a009993..5b586af 100644 --- a/lib/ui/settings/files_section.dart +++ b/lib/ui/settings/files_section.dart @@ -56,13 +56,13 @@ class _FilesSettingsSectionState extends State { subtitle: Text(tr('길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴 (복사 · 이동 · 삭제 · 이름 변경 …). ' 'Ctrl + 클릭: 여러 개 고르기')), trailing: DropdownButton( - value: s.explorerClick, + value: s.explorer.click, items: [ DropdownMenuItem( value: 'select', child: Text(desk ? tr('한 번: 선택 · 두 번: 실행 (기본)') : tr('한 번: 선택 · 두 번: 실행'))), DropdownMenuItem(value: 'open', child: Text(desk ? tr('한 번: 바로 실행') : tr('한 번: 바로 실행 (기본)'))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerClick = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.click = v!), ), ), // 82: 창 사이 끌어 놓기 (처음: 묻기) @@ -70,13 +70,13 @@ class _FilesSettingsSectionState extends State { title: Text(tr('끌어 놓기 기본')), subtitle: Text(tr('다른 창으로 끌어 놓았을 때. 어느 쪽이든 복사 · 이동 확인 창 (같은 이름 등) 은 그대로 뜹니다')), trailing: DropdownButton( - value: s.explorerDropAction, + value: s.explorer.dropAction, items: [ DropdownMenuItem(value: 'ask', child: Text(tr('묻기 (기본)'))), DropdownMenuItem(value: 'copy', child: Text(tr('복사'))), DropdownMenuItem(value: 'move', child: Text(tr('이동'))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerDropAction = v ?? 'ask'), + onChanged: (v) => c.updateSettings((x) => x.explorer.dropAction = v ?? 'ask'), ), ), // 84: 정렬을 폴더마다 기억 (처음: 끔) @@ -84,8 +84,8 @@ class _FilesSettingsSectionState extends State { title: Text(tr('정렬을 폴더마다 기억')), subtitle: Text(tr('켜면 열 머리 · 정렬 기준에서 바꾼 정렬이 그 폴더에만 쓰입니다. 끄면 모든 폴더가 같은 정렬입니다')), trailing: Switch( - value: s.explorerSortPerFolder, - onChanged: (v) => c.updateSettings((x) => x.explorerSortPerFolder = v), + value: s.explorer.sortPerFolder, + onChanged: (v) => c.updateSettings((x) => x.explorer.sortPerFolder = v), ), ), // X-plore 비교 5-2 (사용자 결정 10/21): 시스템 폴더 보기 (Shizuku, 읽기 전용) - Android, 켤 때마다 경고 @@ -133,8 +133,8 @@ class _FilesSettingsSectionState extends State { title: Text(tr('종류별 기본 정렬')), subtitle: Text(tr('사진 · 동영상이 많은 폴더는 날짜 (새것 먼저), 음악이 많은 폴더는 이름으로. 그 폴더에서 정렬을 바꾸면 그 폴더는 바꾼 정렬을 기억합니다')), trailing: Switch( - value: s.explorerSortByKind, - onChanged: (v) => c.updateSettings((x) => x.explorerSortByKind = v), + value: s.explorer.sortByKind, + onChanged: (v) => c.updateSettings((x) => x.explorer.sortByKind = v), ), ), // X-plore 비교 5-1 ② (관리자 지시 10/21): 원격 썸네일 · 캐시 @@ -142,25 +142,25 @@ class _FilesSettingsSectionState extends State { title: Text(tr('원격 썸네일')), subtitle: Text(tr('WebDAV · SFTP · SMB · FTP 의 그림 · 동영상을 X-plore 모양 목록에서 작은 그림으로. 보이는 줄만, 동시에 2개까지 받습니다')), trailing: DropdownButton( - value: s.explorerRemoteThumbs, + value: s.explorer.remoteThumbs, items: [ DropdownMenuItem(value: 'off', child: Text(tr('받지 않음'))), DropdownMenuItem(value: 'wifi', child: Text(tr('Wi-Fi 에서만 (기본)'))), DropdownMenuItem(value: 'always', child: Text(tr('늘 (모바일 데이터에서도)'))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerRemoteThumbs = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.remoteThumbs = v!), ), ), SettingTile( title: Text(tr('원격 썸네일 캐시')), subtitle: Text(tr('넘으면 오래 보지 않은 것부터 지웁니다')), trailing: DropdownButton( - value: s.explorerRemoteThumbCacheMb, + value: s.explorer.remoteThumbCacheMb, items: [ - for (final v in {...AppSettings.remoteThumbCacheChoices, s.explorerRemoteThumbCacheMb}.toList()..sort()) + for (final v in {...ExplorerSettings.remoteThumbCacheChoices, s.explorer.remoteThumbCacheMb}.toList()..sort()) DropdownMenuItem(value: v, child: Text(v == 200 ? trf('{0}MB (기본)', [v]) : '${v}MB')), ], - onChanged: (v) => c.updateSettings((x) => x.explorerRemoteThumbCacheMb = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.remoteThumbCacheMb = v!), ), ), RemoteThumbCacheTile(c: c), @@ -170,22 +170,22 @@ class _FilesSettingsSectionState extends State { subtitle: Text(tr('폴더 안 파일 크기의 합을 목록에 보입니다. 큰 폴더 · NAS 는 읽는 데 오래 걸릴 수 있습니다. ' '길게 누르기 [폴더 크기 계산] 은 늘 됩니다')), trailing: DropdownButton( - value: s.explorerFolderSize, + value: s.explorer.folderSize, items: [ DropdownMenuItem(value: 'sort', child: Text(tr('크기순 정렬일 때만 (기본)'))), DropdownMenuItem(value: 'always', child: Text(tr('늘 계산'))), DropdownMenuItem(value: 'off', child: Text(tr('계산 안 함'))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerFolderSize = v ?? 'sort'), + onChanged: (v) => c.updateSettings((x) => x.explorer.folderSize = v ?? 'sort'), ), ), // X-plore 비교 5-1 ⑥ (관리자 지시 10/21): 단축키 바꾸기 (키보드가 있는 PC 에서만 보임) if (desk) ListTile( title: Text(tr('파일 탐색기 단축키')), - subtitle: Text(s.explorerKeyOverrides.isEmpty + subtitle: Text(s.explorer.keyOverrides.isEmpty ? tr('스타일대로 (바꾼 것 없음)') - : trf('바꾼 것 {0}개', [s.explorerKeyOverrides.length])), + : trf('바꾼 것 {0}개', [s.explorer.keyOverrides.length])), trailing: const Icon(Icons.chevron_right), onTap: () => ExplorerKeysPage.open(context, c), ), @@ -200,26 +200,26 @@ class _FilesSettingsSectionState extends State { '그 밖: Ctrl+C · X · V (복사 · 잘라내기 · 붙여넣기) · Ctrl+A 모두 표시 · F5 새로 고침'), ].join('\n')), trailing: DropdownButton( - value: s.explorerStyle, + value: s.explorer.style, items: [ for (final st in ExplorerStyle.values) DropdownMenuItem(value: st.name, child: Text(tr(st.label))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerStyle = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.style = v!), ), ), SettingTile( title: Text(tr('창 배치')), subtitle: Text(tr('파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)')), trailing: DropdownButton( - value: s.explorerLayout, + value: s.explorer.layout, items: [ DropdownMenuItem(value: 'auto', child: Text(tr('화면 크기 따라'))), DropdownMenuItem(value: 'dual', child: Text(tr('두 창 (기본)'))), DropdownMenuItem(value: 'split', child: Text(tr('폴더 + 파일 목록'))), DropdownMenuItem(value: 'single', child: Text(tr('한 창'))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerLayout = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.layout = v!), ), ), // X-plore 비교 e: Android 앱 휴지통 보관 일 @@ -228,95 +228,95 @@ class _FilesSettingsSectionState extends State { title: Text(tr('휴지통 보관')), subtitle: Text(tr('지운 것을 저장 장치의 앱 휴지통에 두었다가 이 날이 지나면 지웁니다. 끄면 바로 영구 삭제')), trailing: DropdownButton( - value: const [0, 3, 7, 14, 30, 60].contains(s.androidTrashDays) ? s.androidTrashDays : 14, + value: const [0, 3, 7, 14, 30, 60].contains(s.explorer.androidTrashDays) ? s.explorer.androidTrashDays : 14, items: [ for (final d in const [0, 3, 7, 14, 30, 60]) DropdownMenuItem(value: d, child: Text(d == 0 ? tr('끔') : trf('{0}일', [d]))), ], - onChanged: (v) => c.updateSettings((x) => x.androidTrashDays = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.androidTrashDays = v!), ), ), // X-plore 비교 ⚖ (관리자 결정): X-plore 스타일의 표시 동그라미 · 밀기 · 폴더 표시 규칙 SwitchListTile( title: Text(tr('X-plore 스타일: 선택 표시 늘 보이기')), subtitle: Text(tr('끄면 (처음 값) [선택] 을 켰을 때만 보입니다. 다른 스타일은 늘 선택 모드에서만')), - value: s.explorerAlwaysMarks, - onChanged: (v) => c.updateSettings((x) => x.explorerAlwaysMarks = v), + value: s.explorer.alwaysMarks, + onChanged: (v) => c.updateSettings((x) => x.explorer.alwaysMarks = v), ), SwitchListTile( title: Text(tr('X-plore 스타일: 옆으로 밀면 창 바꾸기')), subtitle: Text(tr('탐색기 안에서 밀면 다른 창으로. 다른 화면으로 가려면 위 막대에서 밉니다')), - value: s.explorerSwipePanes, - onChanged: (v) => c.updateSettings((x) => x.explorerSwipePanes = v), + value: s.explorer.swipePanes, + onChanged: (v) => c.updateSettings((x) => x.explorer.swipePanes = v), ), // X-plore 비교 c: 저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에 (X-plore 스타일) SwitchListTile( title: Text(tr('저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에')), subtitle: Text(tr('X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)')), - value: s.explorerRootsInTree, - onChanged: (v) => c.updateSettings((x) => x.explorerRootsInTree = v), + value: s.explorer.rootsInTree, + onChanged: (v) => c.updateSettings((x) => x.explorer.rootsInTree = v), ), // X-plore 비교 d: 지금 폴더를 다시 누르면 접기 SwitchListTile( title: Text(tr('펼친 폴더를 다시 누르면 접기')), subtitle: Text(tr('처음 값 켬. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)')), - value: s.explorerRetapCollapse, - onChanged: (v) => c.updateSettings((x) => x.explorerRetapCollapse = v), + value: s.explorer.retapCollapse, + onChanged: (v) => c.updateSettings((x) => x.explorer.retapCollapse = v), ), // X-plore 비교 40 (관리자 지시 10/20): 목록만의 글자 크기 · 줄 간격 (세 스타일 모두, 앱 화면 크기 · 기기 글자 크기와 곱함) SettingTile( title: Text(tr('목록 글자 크기')), subtitle: Text(tr('파일 탐색기 목록의 이름 · 크기 글자만. 앱 화면 크기 · 기기 글자 크기와 곱해집니다')), trailing: DropdownButton( - value: s.explorerTextScale, + value: s.explorer.textScale, items: [ - for (var v = AppSettings.explorerTextScaleMin; v <= AppSettings.explorerTextScaleMax; v += 10) + for (var v = ExplorerSettings.textScaleMin; v <= ExplorerSettings.textScaleMax; v += 10) DropdownMenuItem(value: v, child: Text(v == 100 ? trf('{0}% (기본)', [v]) : '$v%')), - if (s.explorerTextScale % 10 != 0) - DropdownMenuItem(value: s.explorerTextScale, child: Text('${s.explorerTextScale}%')), + if (s.explorer.textScale % 10 != 0) + DropdownMenuItem(value: s.explorer.textScale, child: Text('${s.explorer.textScale}%')), ], - onChanged: (v) => c.updateSettings((x) => x.explorerTextScale = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.textScale = v!), ), ), SettingTile( title: Text(tr('줄 간격')), subtitle: Text(tr('파일 탐색기 목록의 줄 높이. 좁게는 손가락으로 누르기 어려울 수 있습니다')), trailing: DropdownButton( - value: s.explorerRowSpacing, + value: s.explorer.rowSpacing, items: [ DropdownMenuItem(value: 'compact', child: Text(tr('좁게'))), DropdownMenuItem(value: 'normal', child: Text(tr('보통 (기본)'))), DropdownMenuItem(value: 'wide', child: Text(tr('넓게'))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerRowSpacing = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.rowSpacing = v!), ), ), // X-plore 숨기기: 목록에서 숨긴 것 보기 · 풀기 (관리자 결정 10/18) ListTile( - title: Text(trf('목록에서 숨긴 항목 ({0}개)', [s.explorerHiddenItems.length])), + title: Text(trf('목록에서 숨긴 항목 ({0}개)', [s.explorer.hiddenItems.length])), subtitle: Text(tr('파일 탐색기의 길게 누르기 [목록에서 숨기기] 로 숨긴 것. 파일은 그대로이고 목록에서만 빠집니다')), trailing: const Icon(Icons.chevron_right), - enabled: s.explorerHiddenItems.isNotEmpty, + enabled: s.explorer.hiddenItems.isNotEmpty, onTap: () => showHiddenItems(context, c), ), // X-plore 맞추기 (관리자 지시 10/20): 숨기기 전 확인 창 SwitchListTile( title: Text(tr('목록에서 숨기기 전에 묻기')), subtitle: Text(tr('무엇이 바뀌는지 (목록에서만 빠지고 파일은 그대로) 확인 창을 띄웁니다')), - value: s.explorerHideConfirm, - onChanged: (v) => c.updateSettings((x) => x.explorerHideConfirm = v), + value: s.explorer.hideConfirm, + onChanged: (v) => c.updateSettings((x) => x.explorer.hideConfirm = v), ), // X-plore 비교 a: 한 창일 때 복사 · 이동이 갈 곳 SettingTile( title: Text(tr('한 창일 때 복사 · 이동')), subtitle: Text(tr('숨은 다른 창으로: X-plore 처럼 위에 "갈 곳" 을 늘 보이고 그 폴더로 바로 복사 · 이동합니다')), trailing: DropdownButton( - value: s.explorerOnePaneCopy, + value: s.explorer.onePaneCopy, items: [ DropdownMenuItem(value: 'clip', child: Text(tr('담아 두고 붙여넣기 (기본)'))), DropdownMenuItem(value: 'other', child: Text(tr('숨은 다른 창으로'))), ], - onChanged: (v) => c.updateSettings((x) => x.explorerOnePaneCopy = v!), + onChanged: (v) => c.updateSettings((x) => x.explorer.onePaneCopy = v!), ), ), // 복사 · 이동 방법 (현재 방식 · robocopy) · 속도 제한 diff --git a/test/a11y_names_test.dart b/test/a11y_names_test.dart index 596358b..e4e37d6 100644 --- a/test/a11y_names_test.dart +++ b/test/a11y_names_test.dart @@ -35,7 +35,7 @@ void main() { File(p.join(tmp.path, 'a.txt')).writeAsStringSync('a'); Directory(p.join(tmp.path, 'sub')).createSync(); c.settings - ..explorerPaths = [tmp.path, tmp.path] + ..explorer.paths = [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'))]; diff --git a/test/decisions_test.dart b/test/decisions_test.dart index 8e12738..3fbdcc7 100644 --- a/test/decisions_test.dart +++ b/test/decisions_test.dart @@ -37,7 +37,7 @@ void main() { expect([s.webPauseOnLeave, s.autoCheckUpdates, s.youtubeAdSkip, s.youtubeAdHide, s.runInBackground, s.swipeNav], everyElement(isTrue)); // 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22) - expect([s.liveSyncOnStart, s.liveSyncIntervalSec, s.explorerLayout, s.zipComic], ['auto', 30, 'dual', false]); + expect([s.liveSyncOnStart, s.liveSyncIntervalSec, s.explorer.layout, s.zipComic], ['auto', 30, 'dual', false]); expect([AppSettings.uiScaleMin, AppSettings.uiScaleMax], [0.5, 2.5]); // 바꾼 값은 남는다 final b = AppSettings.fromJson((AppSettings() @@ -145,6 +145,6 @@ void main() { // 강제 종료 뒤 다시 켬 = 파일을 새로 읽음 final again = await tester.runAsync(() => SettingsStore(file, MemorySecretStore()).load()); expect(again!.migrated, isEmpty); - expect(again.explorerOrientation, 'side'); + expect(again.explorer.orientation, 'side'); }); } diff --git a/test/dismiss_test.dart b/test/dismiss_test.dart index 9e1362f..8e34e81 100644 --- a/test/dismiss_test.dart +++ b/test/dismiss_test.dart @@ -126,8 +126,8 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync('d'); c.settings - ..explorerLayout = 'single' - ..explorerPaths = [dir, dir]; + ..explorer.layout = 'single' + ..explorer.paths = [dir, dir]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -366,7 +366,7 @@ void main() { final center = CopyCenter.of(c); c.settings ..rsyncOptions = '-avPog' - ..explorerLayout = 'single' + ..explorer.layout = 'single' ..rsyncPaths = [outer, tmp.path]; // 이동 (원본 파일 지우기) 작업만 --delete 를 기억 await center.remember([inner], outer, contents: true, method: 'rsync', move: true).then((t) => center.update(t.copyWith(options: '-avPog --delete'))); diff --git a/test/explorer_archive_trash_test.dart b/test/explorer_archive_trash_test.dart index 78892e2..2dbd129 100644 --- a/test/explorer_archive_trash_test.dart +++ b/test/explorer_archive_trash_test.dart @@ -113,7 +113,7 @@ void main() { device.trash = true; device.volumes = () => [(tmp.path, 'T:')]; // [휴지통] 버튼이 줄에 보이게 (처음 구성은 좁으면 [더 보기] 로 간다) - c.settings.explorerButtons = ['trash', for (final b in ExplorerButton.defaultOrder) if (b != ExplorerButton.trash) b.name]; + c.settings.explorer.buttons = ['trash', for (final b in ExplorerButton.defaultOrder) if (b != ExplorerButton.trash) b.name]; addTearDown(() { }); await open(tester); @@ -180,7 +180,7 @@ void main() { }, skip: !Platform.isWindows); testWidgets('바로 실행 모드: 폴더를 누르면 펼치고, 파일을 누르면 바로 실행', (tester) async { - c.settings.explorerClick = 'open'; + c.settings.explorer.click = 'open'; await open(tester); await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); diff --git a/test/explorer_buttons_contract_test.dart b/test/explorer_buttons_contract_test.dart index c679187..b24e1b4 100644 --- a/test/explorer_buttons_contract_test.dart +++ b/test/explorer_buttons_contract_test.dart @@ -149,7 +149,7 @@ void main() { host.show(() => ExplorerTransferPanel(c: ctl.c, jobs: [job], rsync: false), ctl.c); await t.pump(); expect(find.textContaining('완료'), findsOneWidget); - await t.runAsync(() => ctl.c.updateSettings((s) => s.explorerProgressHidden = true)); + await t.runAsync(() => ctl.c.updateSettings((s) => s.explorer.progressHidden = true)); await t.pump(); expect([find.byType(TransferProgressChip).evaluate().length, find.textContaining('완료').evaluate().length], [1, 0]); }); diff --git a/test/explorer_conflict_test.dart b/test/explorer_conflict_test.dart index a85e62f..e541bd6 100644 --- a/test/explorer_conflict_test.dart +++ b/test/explorer_conflict_test.dart @@ -40,7 +40,7 @@ void main() { File(p.join(left, 'doc.txt')).setLastModifiedSync(DateTime(2026, 10, 9, 12)); File(p.join(right, 'doc.txt')).setLastModifiedSync(DateTime(2026, 10, 1, 8)); c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.explorerPaths = [left, right]; + c.settings.explorer.paths = [left, right]; final root = p.rootPrefix(tmp.path); device.volumes = () => [(root, root.replaceAll(RegExp(r'[\\/]+$'), ''))]; // 휴지통: 기본은 가짜 (흐름만), JJ_TEST_REAL_RECYCLE=1 이면 실제 휴지통 @@ -294,7 +294,7 @@ void main() { const locked = r'C:\System Volume Information'; if (!Platform.isWindows || !Directory(locked).existsSync()) return; if (skipIfReadable(locked)) return; - c.settings.explorerPaths = [locked, right]; + c.settings.explorer.paths = [locked, right]; device.volumes = () => [(r'C:\', 'C:'), (p.rootPrefix(tmp.path), p.rootPrefix(tmp.path).substring(0, 2))]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; @@ -306,7 +306,7 @@ void main() { }); testWidgets('135: 두 창이 같은 폴더 - [이동] 은 "이미 이 폴더에 있습니다" 창 · [복사] 는 사본을 만들지 묻는다', (tester) async { - c.settings.explorerPaths = [left, left]; + c.settings.explorer.paths = [left, left]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -343,7 +343,7 @@ void main() { testWidgets('135 보충: 섞어 고른 [이동] - 이미 그 폴더에 있는 것만 건너뛰고 나머지는 옮긴다 (확인 창 · 끝난 알림에 적음)', (tester) async { File(p.join(left, 'a.txt')).writeAsStringSync('a'); File(p.join(right, 'b.txt')).writeAsStringSync('b'); - c.settings.explorerPaths = [tmp.path, right]; // 왼쪽 창은 left · right 를 함께 보는 위 폴더 + c.settings.explorer.paths = [tmp.path, right]; // 왼쪽 창은 left · right 를 함께 보는 위 폴더 tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -375,7 +375,7 @@ void main() { tester.view.physicalSize = const Size(1200, 560); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); - final before = c.settings.explorerShowHidden; + final before = c.settings.explorer.showHidden; await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(device: device, c: c)))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); expect(tester.takeException(), isNull, reason: '넘침 없음'); @@ -385,8 +385,8 @@ void main() { await calm(tester); expect(find.text('숨은 항목 표시'), findsOneWidget); await tester.runAsync(() => tester.tap(find.text('숨은 항목 표시'))); - await settle(tester, () => c.settings.explorerShowHidden != before, rounds: 20); - expect(c.settings.explorerShowHidden, !before); + await settle(tester, () => c.settings.explorer.showHidden != before, rounds: 20); + expect(c.settings.explorer.showHidden, !before); // 창 배치 버튼은 좁아도 늘 줄에 (사용자 요청) · 처음 값은 자주 쓰는 것부터 (복사 · 이동 · 삭제 …) expect(find.byTooltip('창 배치: 좌우 (누르면 위아래)'), findsOneWidget); expect(ExplorerButton.fromSettings(const []).take(9).map((b) => b.name), @@ -499,7 +499,7 @@ void main() { testWidgets('37-2: 보던 폴더가 밖에서 지워지면 새로 읽을 때 남아 있는 상위 폴더로 옮기고 알린다', (tester) async { final deep = Directory(p.join(left, 'a', 'b'))..createSync(recursive: true); - c.settings.explorerPaths = [deep.path, right]; + c.settings.explorer.paths = [deep.path, right]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); diff --git a/test/explorer_controller_contract_test.dart b/test/explorer_controller_contract_test.dart index e578fb7..de29863 100644 --- a/test/explorer_controller_contract_test.dart +++ b/test/explorer_controller_contract_test.dart @@ -48,7 +48,7 @@ void main() { expect(q.expanded, containsAll([a, show])); expect([for (final e in q.cache[show]!) p.basename(e.path)], ['ep1.mkv']); expect(host.revealed, [show], reason: 'Host 에 스크롤 부탁'); - expect(ctl.c.settings.explorerPaths.first, show, reason: '마지막 폴더 기억'); + expect(ctl.c.settings.explorer.paths.first, show, reason: '마지막 폴더 기억'); await t.runAsync(() => ctl.goTo(q, b)); expect(q.root, b, reason: '다른 장치'); await t.runAsync(() => ctl.travel(q, forward: false)); diff --git a/test/explorer_drives_test.dart b/test/explorer_drives_test.dart index 992893a..88c99c6 100644 --- a/test/explorer_drives_test.dart +++ b/test/explorer_drives_test.dart @@ -29,7 +29,7 @@ void main() { ..writeAsStringSync('x'); Directory(p.join(tmp.path, 'right')).createSync(); c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.explorerPaths = [p.join(tmp.path, 'left'), p.join(tmp.path, 'right')]; + c.settings.explorer.paths = [p.join(tmp.path, 'left'), p.join(tmp.path, 'right')]; stuck = Completer(); dead = {r'R:\'}; // T: 시험 폴더 (정상) · Q: 응답 없는 네트워크 드라이브 · R: 연결 안 된 드라이브 @@ -80,8 +80,8 @@ void main() { device.volumes = () => [...empties, (tmp.path, 'T:')]; device.probe = (path) => Future.value(!dead.contains(path)); c.settings - ..explorerLayout = 'dual' - ..explorerOrientation = 'side'; + ..explorer.layout = 'dual' + ..explorer.orientation = 'side'; tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); diff --git a/test/explorer_filter_rsync_test.dart b/test/explorer_filter_rsync_test.dart index abd9d1d..20f0851 100644 --- a/test/explorer_filter_rsync_test.dart +++ b/test/explorer_filter_rsync_test.dart @@ -173,12 +173,12 @@ void main() { await tester.pump(); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인'))); await calm(tester); - expect([c.settings.explorerLayout, c.settings.explorerToolbar], ['single', 'hidden']); + expect([c.settings.explorer.layout, c.settings.explorer.toolbar], ['single', 'hidden']); expect(find.text('상위 폴더'), findsNothing); // 버튼 줄 숨김 expect(find.text('doc.txt'), findsOneWidget); // 한 창 (왼쪽만) // ⋮ 메뉴에서 버튼 구성: "찾기" 빼기 - await c.updateSettings((s) => s.explorerToolbar = 'middle'); + await c.updateSettings((s) => s.explorer.toolbar = 'middle'); await calm(tester); await tester.tap(find.byTooltip('창 배치 · 버튼 구성')); await calm(tester); @@ -191,14 +191,14 @@ void main() { await tester.pump(); await tester.tap(find.widgetWithText(FilledButton, '확인')); await calm(tester); - expect(c.settings.explorerButtons, isNot(contains('addMkv'))); - expect(c.settings.explorerButtons, contains('copy')); + expect(c.settings.explorer.buttons, isNot(contains('addMkv'))); + expect(c.settings.explorer.buttons, contains('copy')); expect(find.text('MKV 목록에 추가'), findsNothing); // 설정 저장 · 읽기 final back = AppSettings.fromJson(c.settings.toJson()); - expect([back.explorerLayout, back.explorerToolbar, back.explorerButtons], - [c.settings.explorerLayout, c.settings.explorerToolbar, c.settings.explorerButtons]); + expect([back.explorer.layout, back.explorer.toolbar, back.explorer.buttons], + [c.settings.explorer.layout, c.settings.explorer.toolbar, c.settings.explorer.buttons]); }); testWidgets('Rsync 화면: 늘 좌우 두 창 · 폴더만 · 창마다 하나만 고르기 · → ← ⇄ · 모니터링', (tester) async { @@ -207,7 +207,7 @@ void main() { File(p.join(right, 'r.txt')).writeAsStringSync('r'); File(p.join(right, 'rsub', 'back.txt')).writeAsStringSync('b'); // ⇄ 비교에서 ← 로 건너갈 것 c.settings - ..explorerLayout = 'single' // 파일 탐색기 배치와 상관없이 두 창 + ..explorer.layout = 'single' // 파일 탐색기 배치와 상관없이 두 창 ..rsyncPaths = [left, right]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; diff --git a/test/explorer_hidden_find_test.dart b/test/explorer_hidden_find_test.dart index 95b082f..5f1db8d 100644 --- a/test/explorer_hidden_find_test.dart +++ b/test/explorer_hidden_find_test.dart @@ -17,7 +17,7 @@ void main() { testWidgets('X-plore 숨기기: 길게 누르기 [목록에서 숨기기] → 목록에서만 빠짐 (파일 그대로) · 맨 아래 [숨긴 항목 보이기] · [숨기기 풀기]', (tester) async { // 폴더 + 파일 목록 배치: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 폴더 줄이 많아 맨 아래가 멀다) - c.settings.explorerLayout = 'split'; + c.settings.explorer.layout = 'split'; await open(tester); final doc = p.join(left, 'doc.txt'); await tester.longPress(find.text('doc.txt')); @@ -30,7 +30,7 @@ void main() { await settle(tester, () => find.text('doc.txt').evaluate().isEmpty); expect(find.text('doc.txt'), findsNothing); expect(File(doc).existsSync(), isTrue, reason: '파일은 바꾸지 않는다'); - expect(c.settings.explorerHiddenItems, [doc]); + expect(c.settings.explorer.hiddenItems, [doc]); // 목록 맨 아래에 [숨긴 항목 1개 보이기] expect(find.text('숨긴 항목 1개 보이기'), findsOneWidget); await idle(tester); @@ -44,14 +44,14 @@ void main() { expect(find.text('목록에서 숨기기'), findsNothing); await act(tester, () => tester.tap(find.text('숨기기 풀기'))); await settle(tester, () => find.textContaining('숨긴 항목').evaluate().isEmpty); - expect(c.settings.explorerHiddenItems, isEmpty); + expect(c.settings.explorer.hiddenItems, isEmpty); expect(find.text('doc.txt'), findsOneWidget); expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsNothing); }); testWidgets('X-plore 숨기기 확인 창: [취소] 면 그대로 · [다음부터 묻지 않기] 를 고르면 다음엔 묻지 않고 바로 숨김 (설정 저장)', (tester) async { - c.settings.explorerLayout = 'split'; - expect(AppSettings().explorerHideConfirm, isTrue, reason: '처음은 묻기'); + c.settings.explorer.layout = 'split'; + expect(AppSettings().explorer.hideConfirm, isTrue, reason: '처음은 묻기'); File(p.join(left, 'two.txt')).writeAsStringSync('2'); await open(tester); await tester.longPress(find.text('doc.txt')); @@ -60,7 +60,7 @@ void main() { await calm(tester); await act(tester, () => tester.tap(find.text('취소'))); await calm(tester); - expect(c.settings.explorerHiddenItems, isEmpty, reason: '취소하면 숨기지 않는다'); + expect(c.settings.explorer.hiddenItems, isEmpty, reason: '취소하면 숨기지 않는다'); expect(find.text('doc.txt'), findsOneWidget); // 다음부터 묻지 않기 + 숨기기 await tester.longPress(find.text('doc.txt')); @@ -70,16 +70,16 @@ void main() { await act(tester, () => tester.tap(find.text('다음부터 묻지 않기'))); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '숨기기'))); await settle(tester, () => find.text('doc.txt').evaluate().isEmpty); - expect(c.settings.explorerHideConfirm, isFalse); - expect(AppSettings.fromJson(c.settings.toJson()).explorerHideConfirm, isFalse); - expect(AppSettings.fromJson({}).explorerHideConfirm, isTrue, reason: '예전 설정 (값 없음) 은 묻기'); + expect(c.settings.explorer.hideConfirm, isFalse); + expect(AppSettings.fromJson(c.settings.toJson()).explorer.hideConfirm, isFalse); + expect(AppSettings.fromJson({}).explorer.hideConfirm, isTrue, reason: '예전 설정 (값 없음) 은 묻기'); // 이제는 창 없이 바로 await tester.longPress(find.text('two.txt')); await calm(tester); await act(tester, () => tester.tap(find.text('목록에서 숨기기'))); await settle(tester, () => find.text('two.txt').evaluate().isEmpty); expect(find.textContaining('파일 탐색기 목록에서만 빠집니다'), findsNothing); - expect(c.settings.explorerHiddenItems, [p.join(left, 'doc.txt'), p.join(left, 'two.txt')]); + expect(c.settings.explorer.hiddenItems, [p.join(left, 'doc.txt'), p.join(left, 'two.txt')]); }); testWidgets('X-plore 맞추기: 폴더 길게 누르기 [갤러리 · 음악 앱에서 숨기기 (.nomedia)] - 확인 창 뒤 빈 .nomedia, 다시 누르면 지우기 · 안의 파일은 그대로', (tester) async { @@ -129,7 +129,7 @@ void main() { }); testWidgets('X-plore 숨기기: 숨긴 것이 든 폴더를 지울 때 확인 창에 알림 (숨긴 것도 함께 지워진다)', (tester) async { - c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')]; + c.settings.explorer.hiddenItems = [p.join(left, 'sub', 'inner.txt')]; await open(tester); await tester.longPress(find.text('sub')); await calm(tester); @@ -150,7 +150,7 @@ void main() { }); testWidgets('X-plore 숨기기: 이름을 바꾸면 숨긴 목록도 새 경로로 · 영구로 지우면 목록에서 뺀다 (관리자 결정)', (tester) async { - c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')]; + c.settings.explorer.hiddenItems = [p.join(left, 'sub', 'inner.txt')]; await open(tester); await tester.longPress(find.text('sub')); await calm(tester); @@ -159,7 +159,7 @@ void main() { await act(tester, () => tester.enterText(find.byType(TextField), 'sub2')); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인'))); await settle(tester, () => find.text('sub2').evaluate().isNotEmpty); - expect(c.settings.explorerHiddenItems, [p.join(left, 'sub2', 'inner.txt')]); + expect(c.settings.explorer.hiddenItems, [p.join(left, 'sub2', 'inner.txt')]); await idle(tester); await tester.longPress(find.text('sub2')); await calm(tester); @@ -169,29 +169,29 @@ void main() { await calm(tester); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제'))); await settle(tester, () => find.text('sub2').evaluate().isEmpty); - await settle(tester, () => c.settings.explorerHiddenItems.isEmpty); - expect(c.settings.explorerHiddenItems, isEmpty); + await settle(tester, () => c.settings.explorer.hiddenItems.isEmpty); + expect(c.settings.explorer.hiddenItems, isEmpty); }); testWidgets('X-plore 숨기기: 다른 창으로 옮기면 숨긴 목록도 새 자리로 (그 안의 것도)', (tester) async { - c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')]; + c.settings.explorer.hiddenItems = [p.join(left, 'sub', 'inner.txt')]; await open(tester); await tester.longPress(find.text('sub')); await calm(tester); await act(tester, () => tester.tap(find.byTooltip('다른 창으로 이동').last)); await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동'))); - await settle(tester, () => c.settings.explorerHiddenItems.first != p.join(left, 'sub', 'inner.txt')); + await settle(tester, () => c.settings.explorer.hiddenItems.first != p.join(left, 'sub', 'inner.txt')); expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isTrue); - expect(c.settings.explorerHiddenItems, [p.join(right, 'sub', 'inner.txt')]); + expect(c.settings.explorer.hiddenItems, [p.join(right, 'sub', 'inner.txt')]); await idle(tester); }); testWidgets('X-plore 숨기기: 위 막대 ⋮ [목록에서 숨긴 항목 보이기] · 환경 설정의 [목록에서 숨긴 항목] 에서 풀기', (tester) async { final doc = p.join(left, 'doc.txt'); c.settings - ..explorerLayout = 'split' - ..explorerHiddenItems = [doc]; + ..explorer.layout = 'split' + ..explorer.hiddenItems = [doc]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -214,7 +214,7 @@ void main() { expect(find.text('목록에서 숨긴 항목 (1개)'), findsOneWidget); await act(tester, () => tester.tap(find.byTooltip('숨기기 풀기'))); await calm(tester); - expect(c.settings.explorerHiddenItems, isEmpty); + expect(c.settings.explorer.hiddenItems, isEmpty); expect(find.text('숨긴 항목이 없습니다'), findsOneWidget); await act(tester, () => tester.tap(find.text('닫기'))); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); @@ -224,8 +224,8 @@ void main() { testWidgets('80: 크기순 정렬이면 폴더 크기를 계산해 보이고, "안 함" 이어도 길게 누르기 [폴더 크기 계산]', (tester) async { File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1)); c.settings - ..explorerSort = 'size' - ..explorerSortDesc = true; + ..explorer.sort = 'size' + ..explorer.sortDesc = true; await open(tester); // sub = inner.txt (5) + big.bin (3000) await settle(tester, () => find.text('3KB').evaluate().isNotEmpty); @@ -241,8 +241,8 @@ void main() { testWidgets('80: "계산 안 함" - 크기를 보이지 않다가 [폴더 크기 계산] 을 누르면', (tester) async { File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1)); c.settings - ..explorerSort = 'size' - ..explorerFolderSize = 'off'; + ..explorer.sort = 'size' + ..explorer.folderSize = 'off'; await open(tester); await settle(tester, () => false, rounds: 15); expect(find.text('3KB'), findsNothing); diff --git a/test/explorer_keys_drag_test.dart b/test/explorer_keys_drag_test.dart index f52d3ec..ef16ad7 100644 --- a/test/explorer_keys_drag_test.dart +++ b/test/explorer_keys_drag_test.dart @@ -20,7 +20,7 @@ void main() { testWidgets('83: Total Commander 모양 단축키 - F5 복사 · F7 새 폴더 · Space/Insert 표시 (Space 는 빠른 필터가 아님)', (tester) async { File(p.join(left, 'e.txt')).writeAsStringSync('e'); - c.settings.explorerStyle = 'totalcmd'; + c.settings.explorer.style = 'totalcmd'; await open(tester); await act(tester, () => tester.tap(find.text('doc'))); await settle(tester, () => false, rounds: 15); @@ -51,7 +51,7 @@ void main() { if (bin is! FakeRecycleBin) return markTestSkipped('실제 휴지통 (JJ_TEST_REAL_RECYCLE=1) 에서는 건너뜀'); if (!Platform.isWindows) return markTestSkipped('휴지통은 Windows 만 (Android 는 앱 휴지통 시험)'); final victim = File(p.join(left, 'tcdel.txt'))..writeAsStringSync('x'); - c.settings.explorerStyle = 'totalcmd'; + c.settings.explorer.style = 'totalcmd'; await open(tester); await settle(tester, () => find.text('tcdel').evaluate().isNotEmpty); await act(tester, () => tester.tap(find.text('tcdel'))); @@ -80,7 +80,7 @@ void main() { }); testWidgets('83: Windows 탐색기 모양 단축키 - Ctrl+A 모두 표시 · Ctrl+C 담기 · Ctrl+V 붙여넣기', (tester) async { - c.settings.explorerStyle = 'windows'; + c.settings.explorer.style = 'windows'; await open(tester); await act(tester, () => tester.tap(find.text('doc.txt'))); await settle(tester, () => false, rounds: 15); @@ -151,7 +151,7 @@ void main() { File(p.join(right, 'doc.txt')).writeAsStringSync('old'); File(p.join(left, 'e.txt')).writeAsStringSync('e'); c.settings - ..explorerDropAction = 'move' + ..explorer.dropAction = 'move' ..copyConflict = 'overwrite'; await open(tester); // 전체 시험 중 (부하) 에는 목록을 늦게 읽어 e.txt 를 못 찾았다 - 두 파일이 보일 때까지 @@ -177,7 +177,7 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync('mine'); c.settings - ..explorerDropAction = 'move' + ..explorer.dropAction = 'move' ..copyConflict = 'overwrite'; await open(tester); await act(tester, () => tester.tap(find.text('선택').first)); @@ -232,21 +232,21 @@ void main() { } await act(tester, () => tester.tap(find.text('숨기기'))); - await until(() => c.settings.explorerProgressHidden && find.text('복사 · 이동 끝').evaluate().isNotEmpty); - expect(c.settings.explorerProgressHidden, isTrue, reason: '숨김은 기억'); + await until(() => c.settings.explorer.progressHidden && find.text('복사 · 이동 끝').evaluate().isNotEmpty); + expect(c.settings.explorer.progressHidden, isTrue, reason: '숨김은 기억'); expect(find.text('숨기기'), findsNothing); expect(find.text('복사 · 이동 끝'), findsOneWidget, reason: '아래 작은 표시'); // 작은 표시를 누름 (아래 알림이 덮고 있어도 - 알림이 닫히기를 기다리면 그사이 작은 표시가 내려갈 수 있다) tester.widget(find.byType(TransferProgressChip)).onTap(); - await until(() => !c.settings.explorerProgressHidden); + await until(() => !c.settings.explorer.progressHidden); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); - expect(c.settings.explorerProgressHidden, isFalse, reason: '누르면 다시 펼침'); + expect(c.settings.explorer.progressHidden, isFalse, reason: '누르면 다시 펼침'); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await calm(tester); }); testWidgets('81: 빠른 보기 (글 파일 앞부분) - Windows 는 TC 키 F3 (규칙 1), Android 는 Space · 필터에 치는 중에는 그냥 빈칸', (tester) async { - c.settings.explorerStyle = 'windows'; + c.settings.explorer.style = 'windows'; await open(tester); await act(tester, () => tester.tap(find.text('doc.txt'))); await settle(tester, () => false, rounds: 15); @@ -270,7 +270,7 @@ void main() { await calm(tester); // TC: F3 - await c.updateSettings((s) => s.explorerStyle = 'totalcmd'); + await c.updateSettings((s) => s.explorer.style = 'totalcmd'); await calm(tester); await act(tester, () => tester.tap(find.text('doc'))); await settle(tester, () => false, rounds: 15); @@ -284,7 +284,7 @@ void main() { // P0 (15 지적 10/17): 글자 칸에 치는 중에는 파일 단축키가 키를 가로채지 않는다 (예전: 필터 칸의 Delete → 파일 지우기 창) for (final style in const ['windows', 'xplore', 'totalcmd']) { testWidgets('P0 $style: 빠른 필터 칸에서 Ctrl+A · C · V · Delete · Space · F8 · Backspace 는 파일에 쓰이지 않는다', (tester) async { - c.settings.explorerStyle = style; + c.settings.explorer.style = style; await open(tester); await act(tester, () => tester.tap(find.text(style == 'totalcmd' ? 'doc' : 'doc.txt'))); await settle(tester, () => false, rounds: 15); @@ -345,35 +345,35 @@ void main() { testWidgets('84: 열 머리를 누르면 그 기준으로 · 다시 누르면 거꾸로 (처음: 모든 폴더 같은 정렬)', (tester) async { File(p.join(left, 'a_big.txt')).writeAsStringSync('x' * 50); - c.settings.explorerStyle = 'windows'; + c.settings.explorer.style = 'windows'; await open(tester); double y(String name) => tester.getTopLeft(find.text(name).first).dy; expect(y('a_big.txt'), lessThan(y('doc.txt')), reason: '처음: 이름순'); await act(tester, () => tester.tap(find.text('크기').first)); await calm(tester); - expect((c.settings.explorerSort, c.settings.explorerSortDesc), ('size', false)); + expect((c.settings.explorer.sort, c.settings.explorer.sortDesc), ('size', false)); expect(y('doc.txt'), lessThan(y('a_big.txt')), reason: '크기 오름차순: doc.txt (5) 가 먼저'); await act(tester, () => tester.tap(find.text('크기').first)); await calm(tester); - expect(c.settings.explorerSortDesc, isTrue, reason: '같은 열을 다시 누르면 거꾸로'); + expect(c.settings.explorer.sortDesc, isTrue, reason: '같은 열을 다시 누르면 거꾸로'); // (거꾸로면 트리에 펼쳐진 위 폴더 (Temp 등) 의 차례도 바뀌어 시험 폴더가 화면 밖으로 갈 수 있어 줄 위치는 보지 않는다) expect(find.byIcon(Icons.arrow_drop_down), findsWidgets, reason: '지금 정렬 열에 ▼'); }); testWidgets('84: "정렬을 폴더마다 기억" 을 켜면 그 폴더에만 (전체 정렬 · 다른 창은 그대로)', (tester) async { c.settings - ..explorerStyle = 'totalcmd' - ..explorerSortPerFolder = true; + ..explorer.style = 'totalcmd' + ..explorer.sortPerFolder = true; await open(tester); // 왼쪽 창에서 날짜 열 await act(tester, () => tester.tap(find.text('날짜').first)); await calm(tester); - expect(c.settings.explorerSort, 'name', reason: '전체 정렬은 그대로'); - expect(c.settings.explorerFolderSorts.values, contains('date|0')); - expect(c.settings.explorerFolderSorts.length, 1); + expect(c.settings.explorer.sort, 'name', reason: '전체 정렬은 그대로'); + expect(c.settings.explorer.folderSorts.values, contains('date|0')); + expect(c.settings.explorer.folderSorts.length, 1); // 저장 · 읽기 - expect(AppSettings.fromJson(c.settings.toJson()).explorerFolderSorts, c.settings.explorerFolderSorts); - expect(AppSettings.fromJson(c.settings.toJson()).explorerSortPerFolder, isTrue); - expect(AppSettings.fromJson({'explorerFolderSorts': 'broken'}).explorerFolderSorts, isEmpty, reason: '틀린 값은 비움'); + expect(AppSettings.fromJson(c.settings.toJson()).explorer.folderSorts, c.settings.explorer.folderSorts); + expect(AppSettings.fromJson(c.settings.toJson()).explorer.sortPerFolder, isTrue); + expect(AppSettings.fromJson({'explorerFolderSorts': 'broken'}).explorer.folderSorts, isEmpty, reason: '틀린 값은 비움'); }); } diff --git a/test/explorer_keys_test.dart b/test/explorer_keys_test.dart index 9981425..a18f12b 100644 --- a/test/explorer_keys_test.dart +++ b/test/explorer_keys_test.dart @@ -53,10 +53,10 @@ void main() { expect(keyConflict('copy', const SingleActivator(LogicalKeyboardKey.escape), tc: false, overrides: {}), 'escape'); expect(keyConflict('copy', const SingleActivator(k, control: true), tc: false, overrides: {}), isNull); // 설정 저장 · 읽기 · 틀린 꼴은 비움 - final back = AppSettings.fromJson((AppSettings()..explorerKeyOverrides = {'copy': spec}).toJson()); - expect(back.explorerKeyOverrides, {'copy': spec}); - expect(AppSettings.fromJson({'explorerKeyOverrides': 'x'}).explorerKeyOverrides, isEmpty); - expect(AppSettings().explorerKeyOverrides, isEmpty); + final back = AppSettings.fromJson((AppSettings()..explorer.keyOverrides = {'copy': spec}).toJson()); + expect(back.explorer.keyOverrides, {'copy': spec}); + expect(AppSettings.fromJson({'explorerKeyOverrides': 'x'}).explorer.keyOverrides, isEmpty); + expect(AppSettings().explorer.keyOverrides, isEmpty); }); late Directory tmp; @@ -71,9 +71,9 @@ void main() { addTearDown(tester.view.reset); final c = ctl(); c.settings - ..explorerPaths = [tmp.path, tmp.path] - ..explorerStyle = 'windows' - ..explorerKeyOverrides = {'newFolder': keySpec(const SingleActivator(k, control: true, shift: true))}; + ..explorer.paths = [tmp.path, tmp.path] + ..explorer.style = 'windows' + ..explorer.keyOverrides = {'newFolder': keySpec(const SingleActivator(k, control: true, shift: true))}; await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); for (var i = 0; i < 20; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); @@ -99,7 +99,7 @@ void main() { tester.view.physicalSize = const Size(1000, 1600); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); - final c = ctl()..settings.explorerStyle = 'windows'; + final c = ctl()..settings.explorer.style = 'windows'; await tester.pumpWidget(MaterialApp(home: ExplorerKeysPage(c: c))); Future change(String label) async { await tester.tap(find.descendant(of: find.widgetWithText(ListTile, label), matching: find.text('바꾸기'))); @@ -112,18 +112,18 @@ void main() { expect(find.textContaining('이미 "새로 고침" 에 쓰는 키입니다'), findsOneWidget); await tester.tap(find.text('저장')); await tester.pumpAndSettle(); - expect(c.settings.explorerKeyOverrides['copy'], keySpec(const SingleActivator(LogicalKeyboardKey.f5))); + expect(c.settings.explorer.keyOverrides['copy'], keySpec(const SingleActivator(LogicalKeyboardKey.f5))); expect(find.descendant(of: find.widgetWithText(ListTile, '새로 고침'), matching: find.text('(없음)')), findsOneWidget, reason: '겹친 동작은 키를 잃음'); // 처음 값으로 await tester.tap(find.descendant(of: find.widgetWithText(ListTile, '다른 창으로 복사'), matching: find.text('처음 값으로'))); await tester.pumpAndSettle(); - expect(c.settings.explorerKeyOverrides, isEmpty); + expect(c.settings.explorer.keyOverrides, isEmpty); // 단축키 없음 await change('붙여넣기'); await tester.tap(find.text('단축키 없음')); await tester.pumpAndSettle(); - expect(c.settings.explorerKeyOverrides['paste'], ''); + expect(c.settings.explorer.keyOverrides['paste'], ''); // Esc 는 저장할 수 없음 await change('삭제'); await tester.sendKeyEvent(LogicalKeyboardKey.escape); diff --git a/test/explorer_labels_test.dart b/test/explorer_labels_test.dart index ee09bb4..ca82713 100644 --- a/test/explorer_labels_test.dart +++ b/test/explorer_labels_test.dart @@ -31,7 +31,7 @@ void main() { tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - c.settings.explorerPaths = [tmp.path, tmp.path]; + c.settings.explorer.paths = [tmp.path, tmp.path]; // 기기 글꼴 · 글자 크기가 시험 글꼴보다 크다 (S10 일본어에서 잘림) - 글자 크기를 키운 경우도 await tester.runAsync(() => tester.pumpWidget(MaterialApp( builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(scale)), child: child!), diff --git a/test/explorer_more_flows_contract_test.dart b/test/explorer_more_flows_contract_test.dart index ee4fda6..377bc14 100644 --- a/test/explorer_more_flows_contract_test.dart +++ b/test/explorer_more_flows_contract_test.dart @@ -70,15 +70,15 @@ void main() { testWidgets('창 배치: 두 창 = 창 둘 + 버튼 줄 · 한 창 = 창 하나 · 폴더 + 파일 목록 = 트리 + 목록 · 좁은 Rsync = [왼쪽 | 오른쪽]', (t) async { final h = await open(t); expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerToolbar).evaluate().length], [2, 1]); - h.ctl.c.settings.explorerLayout = 'single'; + h.ctl.c.settings.explorer.layout = 'single'; h.redraw(); await t.pump(); expect(find.byType(ExplorerPaneView).evaluate().length, 1); - h.ctl.c.settings.explorerLayout = 'split'; + h.ctl.c.settings.explorer.layout = 'split'; h.redraw(); await t.pump(); expect([find.byType(ExplorerPaneView).evaluate().length, find.byType(ExplorerListView).evaluate().length], [1, 1]); - h.ctl.c.settings.explorerToolbar = 'hidden'; + h.ctl.c.settings.explorer.toolbar = 'hidden'; h.redraw(); await t.pump(); expect(find.byType(ExplorerToolbar), findsNothing, reason: '버튼 줄 숨김'); diff --git a/test/explorer_page_modules_contract_test.dart b/test/explorer_page_modules_contract_test.dart index 0045157..1117d61 100644 --- a/test/explorer_page_modules_contract_test.dart +++ b/test/explorer_page_modules_contract_test.dart @@ -77,11 +77,11 @@ void main() { final s = m.ctl.c.settings; m.ctl.active = 0; expect([m.layout.dual, m.layout.split, identical(m.layout.other, m.ctl.panes[1]), m.layout.rowHeight() > 0], [true, false, true, true]); - s.explorerLayout = 'single'; + s.explorer.layout = 'single'; expect([m.layout.dual, m.layout.hiddenTarget, m.layout.other], [false, false, null], reason: '처음 값은 담아 두기'); - s.explorerOnePaneCopy = 'other'; + s.explorer.onePaneCopy = 'other'; expect([m.layout.hiddenTarget, identical(m.layout.other, m.ctl.panes[1])], [true, true]); - s.explorerLayout = 'split'; + s.explorer.layout = 'split'; expect(m.layout.split, isTrue); m.layout.splitTight = true; expect(m.layout.split, isFalse); @@ -106,7 +106,7 @@ void main() { testWidgets('보기 창: 정렬 기준 고르고 확인 = 그 정렬 · 버튼 구성 [취소] = 그대로', (t) async { final m = await open(t); - final before = [...m.ctl.c.settings.explorerButtons]; + final before = [...m.ctl.c.settings.explorer.buttons]; unawaited(m.flows.dialogs.sortDialog(m.host.context)); await t.pump(); await t.tap(find.text('크기')); @@ -118,7 +118,7 @@ void main() { await t.pump(); await t.tap(find.text('취소')); await t.pump(); - expect(m.ctl.c.settings.explorerButtons, before); + expect(m.ctl.c.settings.explorer.buttons, before); }); testWidgets('이름 바꾸기: 이름 창 → 실제로 바뀜 · 초점이 새 이름으로 / Wi-Fi 공유는 원격 폴더면 알림', (t) async { diff --git a/test/explorer_panes_test.dart b/test/explorer_panes_test.dart index 78ef20e..f6b68c7 100644 --- a/test/explorer_panes_test.dart +++ b/test/explorer_panes_test.dart @@ -78,10 +78,10 @@ void main() { }); testWidgets('X-plore a: 한 창 + "숨은 다른 창으로" - 위에 갈 곳 줄 · [복사] 는 그 폴더로 · [지금 폴더로] 로 바꾸기 (처음 값은 담아 두기)', (tester) async { - expect(AppSettings().explorerOnePaneCopy, 'clip', reason: '처음 값은 지금 그대로'); + expect(AppSettings().explorer.onePaneCopy, 'clip', reason: '처음 값은 지금 그대로'); c.settings - ..explorerLayout = 'single' - ..explorerOnePaneCopy = 'other'; + ..explorer.layout = 'single' + ..explorer.onePaneCopy = 'other'; await open(tester); expect(find.textContaining('갈 곳: '), findsOneWidget); expect(find.textContaining('갈 곳: right'), findsOneWidget, reason: '숨은 창의 폴더 이름 (전체 경로는 풍선 도움말)'); @@ -94,8 +94,8 @@ void main() { expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'hello'); // [지금 폴더로] → 갈 곳이 지금 보는 폴더 (left) await act(tester, () => tester.tap(find.text('지금 폴더로'))); - await settle(tester, () => c.settings.explorerPaths[1] == left, rounds: 50); - expect(c.settings.explorerPaths[1], left); + await settle(tester, () => c.settings.explorer.paths[1] == left, rounds: 50); + expect(c.settings.explorer.paths[1], left); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await calm(tester); await tester.pump(const Duration(seconds: 2)); @@ -195,7 +195,7 @@ void main() { }); testWidgets('규칙 3: 펼친 폴더를 다시 누르면 접힘 · 두 번 누르기의 두 번째는 접지 않음 (처음 값 켬)', (tester) async { - expect(AppSettings().explorerRetapCollapse, isTrue, reason: '규칙 3 (10/22)'); + expect(AppSettings().explorer.retapCollapse, isTrue, reason: '규칙 3 (10/22)'); await open(tester); await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); @@ -215,7 +215,7 @@ void main() { testWidgets('X-plore c: X-plore 스타일 트리 맨 위에 다른 저장 장치 · 즐겨찾기 - 누르면 그곳으로, 즐겨찾기는 펼침', (tester) async { device.volumes = () => [(left, 'L:'), (right, 'R:')]; c.settings - ..explorerStyle = 'xplore' + ..explorer.style = 'xplore' ..folderBookmarks = [FolderBookmark(name: '내 즐겨찾기', path: p.join(left, 'sub'))]; await open(tester); final otherRoot = find.byKey(ValueKey('k1:$right')); @@ -227,27 +227,27 @@ void main() { await calm(tester); expect(find.text('내 즐겨찾기'), findsOneWidget); await act(tester, () => tester.tap(find.text('내 즐겨찾기'))); - await settle(tester, () => c.settings.explorerPaths.first == p.join(left, 'sub'), rounds: 80); - expect(c.settings.explorerPaths.first, p.join(left, 'sub')); + await settle(tester, () => c.settings.explorer.paths.first == p.join(left, 'sub'), rounds: 80); + expect(c.settings.explorer.paths.first, p.join(left, 'sub')); await act(tester, () => tester.tap(find.byKey(ValueKey('k1:$right')).first)); - await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 80); - expect(c.settings.explorerPaths.first, right, reason: '다른 저장 장치 줄 = 그 저장 장치로'); + await settle(tester, () => c.settings.explorer.paths.first == right, rounds: 80); + expect(c.settings.explorer.paths.first, right, reason: '다른 저장 장치 줄 = 그 저장 장치로'); // 다른 스타일은 그대로 (맨 위 항목 없음) - await tester.runAsync(() => c.updateSettings((x) => x.explorerStyle = 'windows')); + await tester.runAsync(() => c.updateSettings((x) => x.explorer.style = 'windows')); await calm(tester); expect(find.byKey(const ValueKey('k2:\u0000favorites')), findsNothing); }); testWidgets('X-plore ⚖: 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (한 창이면 숨은 창과 바꿈)', (tester) async { - expect(AppSettings().explorerSwipePanes, isFalse, reason: '처음 값은 끔 - 화면 가운데를 밀면 화면 이동 (10/8 사용자 결정)'); - c.settings.explorerSwipePanes = true; - c.settings.explorerLayout = 'single'; + expect(AppSettings().explorer.swipePanes, isFalse, reason: '처음 값은 끔 - 화면 가운데를 밀면 화면 이동 (10/8 사용자 결정)'); + c.settings.explorer.swipePanes = true; + c.settings.explorer.layout = 'single'; await open(tester); - expect(c.settings.explorerPaths, [left, right]); + expect(c.settings.explorer.paths, [left, right]); // 화면 가운데 높이에서 (위 · 아래 가장자리 1/5 는 밀기로 보지 않는다 - SwipeNav) await tester.flingFrom(tester.getCenter(find.byType(ExplorerPage)), const Offset(-400, 0), 1500); - await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 60); - expect(c.settings.explorerPaths, [right, left], reason: '숨은 창이 보이는 창으로'); + await settle(tester, () => c.settings.explorer.paths.first == right, rounds: 60); + expect(c.settings.explorer.paths, [right, left], reason: '숨은 창이 보이는 창으로'); ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); await tester.pump(const Duration(seconds: 1)); }); diff --git a/test/explorer_parallel_test.dart b/test/explorer_parallel_test.dart index f975f6e..562e0d3 100644 --- a/test/explorer_parallel_test.dart +++ b/test/explorer_parallel_test.dart @@ -29,7 +29,7 @@ void main() { Directory(right).createSync(); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerPaths = [left, right] + ..explorer.paths = [left, right] ..copyBandwidthKBps = 512; // 큰 파일이 6초쯤 걸리게 tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; @@ -69,7 +69,7 @@ void main() { device.volumes = () => [(tmp.path, 'T:')]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerOrientation = 'side' + ..explorer.orientation = 'side' ..rsyncPaths = [l, r]; tester.view.physicalSize = const Size(400, 860); tester.view.devicePixelRatio = 1; @@ -109,7 +109,7 @@ void main() { device.volumes = () => [(tmp.path, 'T:')]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerOrientation = 'side' + ..explorer.orientation = 'side' ..rsyncPaths = [l, r]; tester.view.physicalSize = const Size(412, 900); tester.view.devicePixelRatio = 1; @@ -145,7 +145,7 @@ void main() { device.volumes = () => [(tmp.path, 'T:')]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerOrientation = 'side' + ..explorer.orientation = 'side' ..rsyncPaths = [l, r]; tester.view.physicalSize = const Size(860, 400); tester.view.devicePixelRatio = 1; @@ -167,7 +167,7 @@ void main() { device.volumes = () => [(inner, '내장 저장소'), (sd, 'SD 카드')]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerOrientation = 'side' + ..explorer.orientation = 'side' ..rsyncPaths = [p.join(inner, 'L'), p.join(sd, 'R')]; tester.view.physicalSize = const Size(412, 880); tester.view.devicePixelRatio = 1; @@ -193,7 +193,7 @@ void main() { Directory(p.join(r, 'rsub')).createSync(recursive: true); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerLayout = 'single' + ..explorer.layout = 'single' ..rsyncPaths = [l, r]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; @@ -226,8 +226,8 @@ void main() { File(p.join(l, 'top.txt')).writeAsStringSync('t'); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerLayout = 'single' - ..explorerPaths = [l, l]; + ..explorer.layout = 'single' + ..explorer.paths = [l, l]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -259,8 +259,8 @@ void main() { } final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerLayout = 'single' - ..explorerPaths = [dl, dl]; + ..explorer.layout = 'single' + ..explorer.paths = [dl, dl]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -293,9 +293,9 @@ void main() { File(p.join(l, 'zz_file.txt')).writeAsStringSync('t'); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerLayout = 'dual' - ..explorerOrientation = 'stacked' - ..explorerPaths = [l, l]; + ..explorer.layout = 'dual' + ..explorer.orientation = 'stacked' + ..explorer.paths = [l, l]; tester.view.physicalSize = const Size(420, 640); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -311,17 +311,17 @@ void main() { }); test('118: 두 창 배치 처음 값은 좌우 · 예전 "화면 모양 따라" 는 한 번 좌우로 (알림) · 고른 위아래는 그대로', () { - expect(AppSettings().explorerOrientation, 'side'); - expect(AppSettings.fromJson({}).explorerOrientation, 'side'); + expect(AppSettings().explorer.orientation, 'side'); + expect(AppSettings.fromJson({}).explorer.orientation, 'side'); final old = AppSettings.fromJson({'explorerOrientation': 'auto'}); - expect(old.explorerOrientation, 'side'); + expect(old.explorer.orientation, 'side'); expect(old.migrated, contains('explorerOrientation')); final stacked = AppSettings.fromJson({'explorerOrientation': 'stacked'}); - expect(stacked.explorerOrientation, 'stacked'); + expect(stacked.explorer.orientation, 'stacked'); expect(stacked.migrated, isNot(contains('explorerOrientation'))); // 옮긴 뒤 사용자가 다시 '화면 모양 따라' 를 고르면 그대로 - final again = AppSettings.fromJson((AppSettings()..explorerOrientation = 'auto').toJson()); - expect(again.explorerOrientation, 'auto'); + final again = AppSettings.fromJson((AppSettings()..explorer.orientation = 'auto').toJson()); + expect(again.explorer.orientation, 'auto'); expect(again.migrated, isEmpty); }); @@ -334,8 +334,8 @@ void main() { ..writeAsStringSync('a'); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerLayout = 'single' - ..explorerPaths = [l, l]; + ..explorer.layout = 'single' + ..explorer.paths = [l, l]; // 폰 세로처럼 좁고 긴 화면 tester.view.physicalSize = const Size(500, 1000); tester.view.devicePixelRatio = 1; @@ -356,15 +356,15 @@ void main() { } await press('두 창으로 보기 (좌우)', '두 창으로'); - expect((c.settings.explorerLayout, c.settings.explorerOrientation), ('dual', 'side'), reason: '폰 세로에서도 좌우'); + expect((c.settings.explorer.layout, c.settings.explorer.orientation), ('dual', 'side'), reason: '폰 세로에서도 좌우'); expect(find.text('a.txt'), findsNWidgets(2)); final left = tester.getCenter(find.text('a.txt').first), right = tester.getCenter(find.text('a.txt').last); expect(right.dx, greaterThan(left.dx)); await press('창 배치: 좌우 (누르면 위아래)', '좌우 ⇆'); - expect(c.settings.explorerOrientation, 'stacked'); + expect(c.settings.explorer.orientation, 'stacked'); final top = tester.getCenter(find.text('a.txt').first), bottom = tester.getCenter(find.text('a.txt').last); expect(bottom.dy, greaterThan(top.dy)); await press('창 배치: 위아래 (누르면 좌우)', '위아래 ⇅'); - expect(c.settings.explorerOrientation, 'side'); + expect(c.settings.explorer.orientation, 'side'); }); } diff --git a/test/explorer_select_ops_test.dart b/test/explorer_select_ops_test.dart index 4c2d6f5..fd3291a 100644 --- a/test/explorer_select_ops_test.dart +++ b/test/explorer_select_ops_test.dart @@ -17,8 +17,8 @@ void main() { testWidgets('폴더 + 파일 목록 배치 · Windows 탐색기 / Total Commander 스타일', (tester) async { c.settings - ..explorerLayout = 'split' - ..explorerStyle = 'windows'; + ..explorer.layout = 'split' + ..explorer.style = 'windows'; await open(tester); // 왼쪽 트리는 폴더만, 오른쪽 목록은 고른 폴더 (left) 의 내용 expect(find.text('sub'), findsNWidgets(2)); // 트리 + 목록 @@ -62,7 +62,7 @@ void main() { expect(find.text('inner.txt'), findsOneWidget); // 목록에 바로 보임 // Total Commander: [폴더] · · 확장자 칸 - await c.updateSettings((s) => s.explorerStyle = 'totalcmd'); + await c.updateSettings((s) => s.explorer.style = 'totalcmd'); await settle(tester, () => find.text('[sub]').evaluate().length == 2); expect(find.text('[sub]'), findsNWidgets(2)); expect(find.text(''), findsOneWidget); @@ -115,7 +115,7 @@ void main() { expect(find.byTooltip('표시 지우기'), findsNothing); expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing); // 설정 "늘 보이기" 를 켜면 X-plore 스타일은 선택 모드 밖에서도 보임 - await tester.runAsync(() => c.updateSettings((x) => x.explorerAlwaysMarks = true)); + await tester.runAsync(() => c.updateSettings((x) => x.explorer.alwaysMarks = true)); await settle(tester, () => false, rounds: 5); expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); }); @@ -333,7 +333,7 @@ void main() { testWidgets('75: 창마다 뒤로 · 앞으로 - 버튼 · Alt+← / → · 마우스 4 · 5번', (tester) async { await open(tester); final sub = p.join(left, 'sub'); - String now() => c.settings.explorerPaths.first; + String now() => c.settings.explorer.paths.first; await act(tester, () => tester.tap(find.text('sub').first)); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); // 뒤로 (버튼): 왼쪽 창은 left 로 @@ -374,7 +374,7 @@ void main() { await open(tester); final r1 = p.join(right, 'r1'); final sub = p.join(left, 'sub'); - String at(int i) => c.settings.explorerPaths[i]; + String at(int i) => c.settings.explorer.paths[i]; await act(tester, () => tester.tap(find.text('r1'))); await settle(tester, () => at(1) == r1 && find.text('x.txt').evaluate().isNotEmpty, rounds: 50); // 왼쪽 창이 활성 @@ -399,7 +399,7 @@ void main() { testWidgets('75: 지워진 폴더로 앞으로 - 그 항목은 버리고 지금 폴더 그대로 · 이유를 알림', (tester) async { await open(tester); final sub = p.join(left, 'sub'); - String now() => c.settings.explorerPaths.first; + String now() => c.settings.explorer.paths.first; await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => now() == sub, rounds: 50); await act(tester, () => tester.tap(find.byTooltip('뒤로 (Alt+←)').first)); diff --git a/test/explorer_sort_kind_test.dart b/test/explorer_sort_kind_test.dart index a770392..09b2ccf 100644 --- a/test/explorer_sort_kind_test.dart +++ b/test/explorer_sort_kind_test.dart @@ -52,11 +52,11 @@ void main() { addTearDown(() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null)); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerPaths = [dir, dir] - ..explorerLayout = 'split' - ..explorerStyle = 'windows' - ..explorerSort = sort - ..explorerSortByKind = kind; + ..explorer.paths = [dir, dir] + ..explorer.layout = 'split' + ..explorer.style = 'windows' + ..explorer.sort = sort + ..explorer.sortByKind = kind; await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c)))); for (var i = 0; i < 300 && (i < 10 || find.text(wait).evaluate().isEmpty); i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 30))); @@ -77,9 +77,9 @@ void main() { } test('설정: 처음 끔 · 저장 · 읽기 · 예전 설정 (값 없음) 은 끔', () { - expect(AppSettings().explorerSortByKind, isFalse); - expect(AppSettings.fromJson((AppSettings()..explorerSortByKind = true).toJson()).explorerSortByKind, isTrue); - expect(AppSettings.fromJson({}).explorerSortByKind, isFalse); + expect(AppSettings().explorer.sortByKind, isFalse); + expect(AppSettings.fromJson((AppSettings()..explorer.sortByKind = true).toJson()).explorer.sortByKind, isTrue); + expect(AppSettings.fromJson({}).explorer.sortByKind, isFalse); }); testWidgets('끄면 (처음) 사진 폴더도 지금 정렬 (이름) 그대로', (tester) async { @@ -98,8 +98,8 @@ void main() { await tester.pump(); } expect(order(tester, ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg']), ['a.jpg', 'b.jpg', 'c.mp4', 'd.jpg'], reason: '누른 것이 바로 보인다'); - expect(c.settings.explorerFolderSorts.values, contains('name|0')); - expect((c.settings.explorerSort, c.settings.explorerSortDesc), ('name', false), reason: '전체 정렬은 그대로'); + expect(c.settings.explorer.folderSorts.values, contains('name|0')); + expect((c.settings.explorer.sort, c.settings.explorer.sortDesc), ('name', false), reason: '전체 정렬은 그대로'); await close(tester); }); diff --git a/test/explorer_steady_test.dart b/test/explorer_steady_test.dart index 1558301..3eb0a5e 100644 --- a/test/explorer_steady_test.dart +++ b/test/explorer_steady_test.dart @@ -37,7 +37,7 @@ void main() { device.volumes = () => [(tmp.path, 'T:')]; c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: NoopShell())); c.settings - ..explorerPaths = [left, right] + ..explorer.paths = [left, right] ..copyMethodFile = 'builtin' ..copyMethodFolder = 'builtin'; }); @@ -56,7 +56,7 @@ void main() { for (final layout in ['dual', 'split']) { for (final selecting in [false, true]) { testWidgets('180: 복사해도 자리 · 스크롤 그대로 ($layout${selecting ? ' · 선택 모드' : ''})', (tester) async { - c.settings.explorerLayout = layout; + c.settings.explorer.layout = layout; tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -120,8 +120,8 @@ void main() { testWidgets('180: 창 크기가 바뀌어 두 창이 가로 ↔ 세로로 바뀌어도 스크롤 그대로', (tester) async { c.settings - ..explorerLayout = 'dual' - ..explorerOrientation = 'auto'; + ..explorer.layout = 'dual' + ..explorer.orientation = 'auto'; tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -146,8 +146,8 @@ void main() { testWidgets('183: [새로 고침] 은 두 창 모두 (같은 폴더를 보는 다른 창에 밖에서 지운 파일이 남지 않게)', (tester) async { c.settings - ..explorerLayout = 'dual' - ..explorerPaths = [left, left]; + ..explorer.layout = 'dual' + ..explorer.paths = [left, left]; tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); @@ -162,8 +162,8 @@ void main() { testWidgets('232: 앱으로 돌아오면 보이는 폴더를 다시 읽는다 (다른 앱에서 지우고 · 만든 것이 바로 보이게)', (tester) async { c.settings - ..explorerLayout = 'dual' - ..explorerPaths = [left, left]; + ..explorer.layout = 'dual' + ..explorer.paths = [left, left]; tester.view.physicalSize = const Size(1280, 800); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); diff --git a/test/explorer_text_scale_test.dart b/test/explorer_text_scale_test.dart index 6041c61..810f598 100644 --- a/test/explorer_text_scale_test.dart +++ b/test/explorer_text_scale_test.dart @@ -21,16 +21,16 @@ void main() { test('설정: 처음은 100% · 보통 (지금 그대로), 저장 · 읽기, 틀린 값은 그대로 쪽으로 · 범위 밖은 끝으로', () { final s = AppSettings(); - expect((s.explorerTextScale, s.explorerRowSpacing), (100, 'normal')); + expect((s.explorer.textScale, s.explorer.rowSpacing), (100, 'normal')); s - ..explorerTextScale = 130 - ..explorerRowSpacing = 'wide'; + ..explorer.textScale = 130 + ..explorer.rowSpacing = 'wide'; final back = AppSettings.fromJson(s.toJson()); - expect((back.explorerTextScale, back.explorerRowSpacing), (130, 'wide')); - expect(AppSettings.fromJson({}).explorerTextScale, 100); - expect(AppSettings.fromJson({'explorerTextScale': 'x', 'explorerRowSpacing': 'huge'}).explorerRowSpacing, 'normal'); - expect(AppSettings.fromJson({'explorerTextScale': 300}).explorerTextScale, 150); - expect(AppSettings.fromJson({'explorerTextScale': 10}).explorerTextScale, 80); + expect((back.explorer.textScale, back.explorer.rowSpacing), (130, 'wide')); + expect(AppSettings.fromJson({}).explorer.textScale, 100); + expect(AppSettings.fromJson({'explorerTextScale': 'x', 'explorerRowSpacing': 'huge'}).explorer.rowSpacing, 'normal'); + expect(AppSettings.fromJson({'explorerTextScale': 300}).explorer.textScale, 150); + expect(AppSettings.fromJson({'explorerTextScale': 10}).explorer.textScale, 80); }); /// [name] 글자의 실제 글자 크기 (기기 · 목록 글자 크기를 곱한 것) @@ -54,14 +54,14 @@ void main() { Directory(p.join(tmp.path, 'sub')).createSync(); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerPaths = [tmp.path, tmp.path] + ..explorer.paths = [tmp.path, tmp.path] // 폴더 + 파일 목록: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 의 다른 폴더가 많아 아래로 밀린다) - ..explorerLayout = 'split' - ..explorerStyle = style + ..explorer.layout = 'split' + ..explorer.style = style // ✔ 칸 자리를 보는 시험: 표시를 늘 보이게 (설정 - 처음 값은 규칙 4 로 선택 모드에서만) - ..explorerAlwaysMarks = true - ..explorerTextScale = scale - ..explorerRowSpacing = spacing; + ..explorer.alwaysMarks = true + ..explorer.textScale = scale + ..explorer.rowSpacing = spacing; await tester.runAsync(() => tester.pumpWidget(MaterialApp( builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(device)), child: child!), home: ExplorerPage(c: c)))); @@ -119,7 +119,7 @@ void main() { final c = await open(tester, style, sub, device: 1.3, scale: 150, spacing: 'wide'); await tester.pump(); expect(tester.takeException(), isNull); - expect(c.settings.explorerLayout, 'split', reason: '고른 배치는 바꾸지 않는다'); + expect(c.settings.explorer.layout, 'split', reason: '고른 배치는 바꾸지 않는다'); expect(find.text(sub), findsOneWidget, reason: '트리 · 목록 두 창이 아니라 한 창'); expect(find.text(a), findsOneWidget, reason: '한 창 트리에 파일도 보인다'); final para = tester.renderObject(find.text(sub)); @@ -156,7 +156,7 @@ void main() { final sub = style == 'totalcmd' ? '[sub]' : 'sub'; final c = await open(tester, style, sub, device: 1.3, scale: 150); await tester.pump(); - expect(c.settings.explorerLayout, 'split'); + expect(c.settings.explorer.layout, 'split'); expect(tester.takeException(), isNull); expect(find.text(type), findsNothing, reason: '좁아서 유형 열 숨김'); expect(find.text('크기'), findsOneWidget, reason: '크기는 남김'); diff --git a/test/explorer_webdav_test.dart b/test/explorer_webdav_test.dart index 930c175..b18bd69 100644 --- a/test/explorer_webdav_test.dart +++ b/test/explorer_webdav_test.dart @@ -72,7 +72,7 @@ void main() { } testWidgets('파일 탐색기: WebDAV 탭 → 서버 추가 (연결 확인) → 목록 · 열기 (받아서) · 로컬 → WebDAV 복사', (tester) async { - c.settings.explorerPaths = [left, left]; + c.settings.explorer.paths = [left, left]; await pump(tester, ExplorerPage(device: device, c: c)); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); // 서버가 없으면 위쪽 저장 장치 옆에 "WebDAV" 탭 (창마다) @@ -165,7 +165,7 @@ void main() { addTearDown(() => SecretGate.check = null); c.settings ..webdavServers = [s] - ..explorerPaths = [left, left] + ..explorer.paths = [left, left] ..folderBookmarks = [const FolderBookmark(name: 'NAS 원격', path: 'dav://nas/원격')]; DavRegistry.configure([s]); await pump(tester, ExplorerPage(device: device, c: c)); diff --git a/test/fixtures/settings_golden_legacy_out.json b/test/fixtures/settings_golden_legacy_out.json new file mode 100644 index 0000000..90b6e02 --- /dev/null +++ b/test/fixtures/settings_golden_legacy_out.json @@ -0,0 +1,254 @@ +{ + "__migrated": [ + "orientation", + "explorerOrientation", + "cookieScope", + "explorerMarksRule", + "explorerRetapRule", + "explorerLayoutRule", + "swipeNav", + "dnsForced", + "copySkipNewer" + ], + "addFinishedDownloads": true, + "aiAgreed": [], + "aiAutoDevice": "", + "aiAutoDeviceFor": "", + "aiCfg": 1.0, + "aiCount": 1, + "aiDevice": "auto", + "aiEngine": "local", + "aiHeight": 512, + "aiImageV1": true, + "aiNegative": "", + "aiPrompt": "", + "aiQualityButton": false, + "aiQualityStrength": "normal", + "aiRecent": [], + "aiSaveDir": "", + "aiServices": [], + "aiSource": "und", + "aiSteps": 4, + "aiStrength": 0.6, + "aiTaesd": true, + "aiTargets": [ + "ko", + "en", + "ja" + ], + "aiUpAfter": "upscaled", + "aiUpDir": "", + "aiUpFormat": "same", + "aiUpJpgQuality": 92, + "aiUpMemory": "normal", + "aiUpModel": "auto", + "aiUpScale": 2, + "aiUpSecPerMp": {}, + "aiWhisper": "whisper-base", + "aiWidth": 512, + "allFilesHint2": false, + "allowInnerToOuter": true, + "androidTrashDays": 14, + "appIcon": "yellow", + "askAiOptions": true, + "autoCheckUpdates": true, + "backgroundV2": true, + "batchRenameLast": {}, + "browserEngine": "edge", + "browserSwipeHinted": false, + "cefWhenElevated": false, + "cleanupPrechecked": [ + "work", + "download" + ], + "clipboardWatch": true, + "closeAction": "background", + "components": [ + "mkv", + "browser", + "explorer", + "rsync", + "downloads", + "aiimage", + "viewer" + ], + "cookieScopeV2": true, + "copyBandwidthKBps": 0, + "copyConflict": "ask", + "copyMethodFile": "builtin", + "copyMethodFolder": "builtin", + "copyRunMode": "each", + "copySkipNewer": true, + "copyTasks": [], + "davExternalOpen": "ask", + "dnsDohUrl": "https://dns.google/dns-query", + "dnsMode": "custom", + "dnsPrimary": "8.8.8.8", + "dnsSecondary": "8.8.4.4", + "downloadRoot": null, + "encode": { + "brightness": 0, + "codec": "copy", + "contrast": 0, + "fit": "fill", + "frame": "original", + "quality": "normal", + "resolution": "original", + "rotate": "none", + "saturation": 0, + "temperature": 0 + }, + "explorerAlwaysMarks": false, + "explorerButtons": [], + "explorerClick": "open", + "explorerDropAction": "ask", + "explorerFolderSize": "sort", + "explorerFolderSorts": {}, + "explorerHiddenItems": [], + "explorerHideConfirm": true, + "explorerHistory": [], + "explorerKeyOverrides": {}, + "explorerLayout": "dual", + "explorerOnePaneCopy": "clip", + "explorerOrientV3": true, + "explorerOrientation": "side", + "explorerPaths": [], + "explorerProgressHidden": false, + "explorerRemoteThumbCacheMb": 200, + "explorerRemoteThumbs": "wifi", + "explorerRetapCollapse": true, + "explorerRootsInTree": true, + "explorerRowSpacing": "normal", + "explorerRulesV1": true, + "explorerShowHidden": false, + "explorerSort": "name", + "explorerSortByKind": false, + "explorerSortDesc": false, + "explorerSortPerFolder": false, + "explorerStyle": "xplore", + "explorerSwipePanes": false, + "explorerTextScale": 100, + "explorerToolbar": "middle", + "explorerV2": true, + "externalBrowser": "system", + "externalPlayers": {}, + "folderBookmarks": [], + "ftpServers": [], + "homeUrl": "https://www.youtube.com/", + "imageExts": [ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "bmp" + ], + "lastRunVersion": "", + "lastUpdateCheck": "", + "lastUpdateCheckVersion": "", + "liveSyncIntervalSec": 30, + "liveSyncOnStart": "auto", + "liveSyncPairs": [], + "loginCookieSites": [ + "youtube.com", + "google.com", + "instagram.com", + "x.com", + "twitter.com", + "naver.com" + ], + "maxParallelDownloads": 3, + "maxParallelJobs": 5, + "migrationNotice": true, + "minimizeToTray": true, + "mkvCompactList": "auto", + "mkvExisting": "ask", + "mkvOutputRoot": null, + "moveTargets": [], + "navOrder": [ + "mkv", + "browser", + "tor", + "explorer", + "rsync", + "downloads", + "aiimage" + ], + "openFileAction": "play", + "openFileWindow": "same", + "openSubtitlesSaved": { + "key": false, + "password": false, + "user": false + }, + "orientationV2": true, + "playlistMode": "series", + "preferredSubtitleLanguage": "", + "readerAutoSeconds": 3, + "readerBrightness": 0.0, + "readerFit": "page", + "readerGallery": false, + "readerRtl": false, + "readerSplit": "auto", + "readerSystemBars": true, + "readerVolumeKeys": false, + "recycleOnDelete": true, + "rememberPasswords": true, + "remoteIdleSeconds": 60, + "robocopyLinksV1": true, + "robocopyOptions": "/E /COPY:DAT /DCOPY:T /R:2 /W:2", + "rollbackKeepModels": "", + "rootEnabled": false, + "rsyncOptions": "-avPog", + "rsyncPath": "", + "rsyncPaths": [], + "rsyncSource": "download", + "runInBackground": true, + "screenOrientation": "auto", + "sftpServers": [], + "shizukuEnabled": false, + "showHotkey": "Ctrl+Shift+X", + "showLog": true, + "skippedVersion": "", + "smbRequireSecure": false, + "smbServers": [], + "startMenuShortcut": true, + "startScreen": "home", + "subtitleScale": 1.0, + "swipeNav": true, + "swipeWrap": true, + "syncStopToast": true, + "torHomeUrl": "https://check.torproject.org/", + "torIdleMinutes": 2, + "torJavaScript": true, + "torKeepData": false, + "torNoticeSeen": false, + "torrentStallSeconds": 60, + "tripleTapAction": "move", + "uiBrightness": "dark", + "uiLanguage": "ko", + "uiLanguagesAdded": [], + "uiScale": 1.0, + "uiScaleDefault": 1.0, + "vaultAutoLockSeconds": 60, + "vaultEnabled": false, + "vaultMasterCopy": false, + "webJavaScript": true, + "webPauseOnLeave": true, + "webTranslate": false, + "webdavServers": [], + "wifiShareFolder": "", + "wifiShareMinutes": 30, + "wifiShareTailscale": true, + "youtubeAdHide": true, + "youtubeAdSkip": true, + "ytContainer": "mp4", + "ytCookiesBrowser": "*internal", + "ytCookiesFile": "", + "ytExpandPlaylists": true, + "ytPreferH264": false, + "ytQuality": "best", + "zipComic": false, + "zipComicV2": true, + "zipLevel": 6 +} \ No newline at end of file diff --git a/test/fixtures/settings_golden_out.json b/test/fixtures/settings_golden_out.json index b2fa662..a81bd14 100644 --- a/test/fixtures/settings_golden_out.json +++ b/test/fixtures/settings_golden_out.json @@ -1,4 +1,9 @@ { + "__migrated": [ + "zipComic", + "explorerMarksRule", + "explorerRetapRule" + ], "addFinishedDownloads": false, "aiAgreed": [], "aiAutoDevice": "x_", diff --git a/test/large_text_test.dart b/test/large_text_test.dart index 5f44b3a..b1f1301 100644 --- a/test/large_text_test.dart +++ b/test/large_text_test.dart @@ -52,7 +52,7 @@ void main() { Directory(p.join(tmp.path, 'sub')).createSync(); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..explorerPaths = [tmp.path, tmp.path] + ..explorer.paths = [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'))]; diff --git a/test/master_lock_test.dart b/test/master_lock_test.dart index 7137f98..b43a028 100644 --- a/test/master_lock_test.dart +++ b/test/master_lock_test.dart @@ -311,14 +311,14 @@ void main() { settingsStore: store); await c.init(); // 보통 저장: 모르는 키 · 다른 판의 열쇠 남음 (40) - await c.updateSettings((s) => s.explorerLayout = 'dual'); + await c.updateSettings((s) => s.explorer.layout = 'dual'); Map saved() => jsonDecode(file.readAsStringSync()) as Map; expect(saved()['zz_new_version'], {'keep': 1}); expect(secrets.values['nfs.pw.n1'], 'new-version-pw'); // 앱 설정 전체: 모르는 키 없음 · 다른 판의 열쇠는 아직 남음 (비밀번호 체크를 안 함) await resetAllSettings(c); expect(saved().containsKey('zz_new_version'), isFalse); - expect(saved()['explorerLayout'], AppSettings().explorerLayout); + expect(saved()['explorerLayout'], AppSettings().explorer.layout); expect(secrets.values['nfs.pw.n1'], 'new-version-pw', reason: '[저장된 실제 비밀번호] 를 고르지 않으면 남김'); // 저장된 실제 비밀번호 · API 키: 다른 판의 열쇠도 지움 · 마스터 (sec.) 는 남김 await clearStoredSecrets(c); @@ -327,9 +327,9 @@ void main() { }); test('최상으로 "앱 설정 전체" 를 지우면 처음 상태', () async { - final c = controller()..settings.explorerLayout = 'single'; + final c = controller()..settings.explorer.layout = 'single'; await resetAllSettings(c); - expect(c.settings.explorerLayout, AppSettings().explorerLayout); + expect(c.settings.explorer.layout, AppSettings().explorer.layout); expect(c.settings.webdavServers, isEmpty); }); }); diff --git a/test/remote_thumbs_test.dart b/test/remote_thumbs_test.dart index 8b15055..c1fc268 100644 --- a/test/remote_thumbs_test.dart +++ b/test/remote_thumbs_test.dart @@ -44,13 +44,13 @@ void main() { test('설정: 처음 값 Wi-Fi 에서만 · 캐시 200MB, 틀린 값은 처음 값으로', () { final s = AppSettings(); - expect((s.explorerRemoteThumbs, s.explorerRemoteThumbCacheMb), ('wifi', 200)); - expect(AppSettings.fromJson({'explorerRemoteThumbs': 'x', 'explorerRemoteThumbCacheMb': 'y'}).explorerRemoteThumbs, 'wifi'); + expect((s.explorer.remoteThumbs, s.explorer.remoteThumbCacheMb), ('wifi', 200)); + expect(AppSettings.fromJson({'explorerRemoteThumbs': 'x', 'explorerRemoteThumbCacheMb': 'y'}).explorer.remoteThumbs, 'wifi'); final back = AppSettings.fromJson((AppSettings() - ..explorerRemoteThumbs = 'off' - ..explorerRemoteThumbCacheMb = 500) + ..explorer.remoteThumbs = 'off' + ..explorer.remoteThumbCacheMb = 500) .toJson()); - expect((back.explorerRemoteThumbs, back.explorerRemoteThumbCacheMb), ('off', 500)); + expect((back.explorer.remoteThumbs, back.explorer.remoteThumbCacheMb), ('off', 500)); }); /// 파일 읽기 · 쓰기 (실제 IO) 를 기다린다 - [ok] 가 될 때까지 (5초까지) @@ -140,9 +140,9 @@ void main() { activeMeteredOf = () async => false; resetRemoteThumbNetForTest(); expect(await remoteThumbsAllowedNow(c), isTrue, reason: 'Wi-Fi (집 Wi-Fi 위의 Tailscale 포함)'); - c.settings.explorerRemoteThumbs = 'off'; + c.settings.explorer.remoteThumbs = 'off'; expect(await remoteThumbsAllowedNow(c), isFalse); - c.settings.explorerRemoteThumbs = 'always'; + c.settings.explorer.remoteThumbs = 'always'; activeMeteredOf = () async => true; resetRemoteThumbNetForTest(); expect(await remoteThumbsAllowedNow(c), isTrue); @@ -196,7 +196,7 @@ void main() { expect(await (DavPath.parse('dav://nas/v.bin').client as DavClient).rangeSupported('/v.bin'), isFalse); final calls = >[]; final c = AppController(PlatformServices(mediaTool: _Ffmpeg(calls), storage: DesktopStorageService())); - c.settings.explorerRemoteThumbs = 'always'; + c.settings.explorer.remoteThumbs = 'always'; resetRemoteThumbsForTest(); addTearDown(resetRemoteThumbsForTest); final h = await remoteThumbsOf(c); @@ -247,7 +247,7 @@ void main() { await tool.runFfmpeg(['-y', '-f', 'lavfi', '-i', 'testsrc=size=320x240:rate=10', '-t', '7', '-pix_fmt', 'yuv420p', mp4]); server.ranges = true; final c = AppController(PlatformServices(mediaTool: tool, storage: DesktopStorageService())); - c.settings.explorerRemoteThumbs = 'always'; + c.settings.explorer.remoteThumbs = 'always'; resetRemoteThumbsForTest(); addTearDown(resetRemoteThumbsForTest); final h = await remoteThumbsOf(c); diff --git a/test/root_test.dart b/test/root_test.dart index cd32f65..8442fcf 100644 --- a/test/root_test.dart +++ b/test/root_test.dart @@ -130,8 +130,8 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..rootEnabled = on - ..explorerPaths = [tmp.path, tmp.path] - ..explorerLayout = 'single'; + ..explorer.paths = [tmp.path, tmp.path] + ..explorer.layout = 'single'; return c; } diff --git a/test/rsync_safety_test.dart b/test/rsync_safety_test.dart index f0ae515..a5b2e21 100644 --- a/test/rsync_safety_test.dart +++ b/test/rsync_safety_test.dart @@ -38,7 +38,7 @@ void main() { c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..webdavServers = [s] - ..explorerLayout = 'single' + ..explorer.layout = 'single' ..rsyncPaths = [left, 'dav://nas/']; DavRegistry.configure([s]); }); diff --git a/test/settings_json_contract_test.dart b/test/settings_json_contract_test.dart index 967beaa..9aefc85 100644 --- a/test/settings_json_contract_test.dart +++ b/test/settings_json_contract_test.dart @@ -24,7 +24,11 @@ void main() { return const JsonEncoder.withIndent(' ').convert(sort(v)); } - String roundTrip(String text) => canonical(AppSettings.fromJson(jsonDecode(text) as Map).toJson()); + /// 읽고 다시 쓴 것 + 읽을 때 정한 "바뀐 기본값 알림" 목록 (파일에는 없지만 읽기의 결과) + String roundTrip(String text) { + final s = AppSettings.fromJson(jsonDecode(text) as Map); + return canonical({...s.toJson(), '__migrated': s.migrated}); + } /// 모든 키의 값을 바꾼 입력 (bool 은 반대, 수는 +7, 글은 앞에 x_, 안쪽 묶음도) - 읽기의 검사 · 고침도 함께 지나게 Object? change(Object? v) => switch (v) { @@ -54,6 +58,16 @@ void main() { expect(canonical(AppSettings().toJson()), read(defFile)); }); + test('옛 판 설정 (옮김 표시 없음 · 예전 처음 값): 읽은 값과 바뀐 기본값 알림이 나누기 전 판과 같다', () { + const legacy = {'explorerLayout': 'auto', 'explorerClick': 'open', 'explorerOrientation': 'auto', 'screenOrientation': 'landscape'}; + final outFile = File(p.join(fixtures, 'settings_golden_legacy_out.json')); + if (write) { + outFile.writeAsStringSync(roundTrip(jsonEncode(legacy))); + return; + } + expect(roundTrip(jsonEncode(legacy)), outFile.readAsStringSync().replaceAll('\r\n', '\n')); + }); + final expectPath = Platform.environment['JJ_USER_SETTINGS_EXPECT']; test('사용자 설정 파일 (복사본): 읽고 쓰면 나누기 전 판과 같다', () { final user = File(p.join(Platform.environment['APPDATA'] ?? '', 'com.jj', 'jj_mkvmaker', 'settings.json')); diff --git a/test/settings_store_test.dart b/test/settings_store_test.dart index 98d29d7..5b1c6e2 100644 --- a/test/settings_store_test.dart +++ b/test/settings_store_test.dart @@ -142,16 +142,16 @@ void main() { }); expect(old.screenOrientation, 'auto'); // 규칙 7 · 결정 3 (10/22): 처음 배치가 두 창이 되어 예전 'dual' 은 그대로 (옮겼다는 알림 없음) - expect(old.explorerLayout, 'dual'); + expect(old.explorer.layout, 'dual'); expect(old.zipComic, isFalse); expect(old.migrated, containsAll(['orientation', 'zipComic'])); expect(old.migrated, isNot(contains('explorerLayout'))); // 다시 저장 · 읽기: 이제 사용자가 고른 값으로 보고 그대로 · 알림 없음 final again = AppSettings.fromJson((old - ..explorerLayout = 'dual' + ..explorer.layout = 'dual' ..screenOrientation = 'landscape') .toJson()); - expect(again.explorerLayout, 'dual'); + expect(again.explorer.layout, 'dual'); expect(again.screenOrientation, 'landscape'); expect(again.migrated, isEmpty); // 처음 설치 (빈 설정) 는 알릴 것 없음 @@ -166,16 +166,16 @@ void main() { 'explorerRetapCollapse': false, 'explorerClick': 'open', }); - expect([old.explorerLayout, old.explorerAlwaysMarks, old.explorerRetapCollapse], ['dual', false, true]); - expect(old.explorerClick, Platform.isAndroid ? 'select' : 'open'); // Windows 의 'open' 은 사용자가 고른 값 + expect([old.explorer.layout, old.explorer.alwaysMarks, old.explorer.retapCollapse], ['dual', false, true]); + expect(old.explorer.click, Platform.isAndroid ? 'select' : 'open'); // Windows 의 'open' 은 사용자가 고른 값 expect(old.migrated, containsAll(['explorerLayoutRule', 'explorerMarksRule', 'explorerRetapRule'])); // 저장한 뒤 사용자가 다시 고른 값은 그대로 · 다시 묻지 않음 final again = AppSettings.fromJson((old - ..explorerLayout = 'auto' - ..explorerAlwaysMarks = true - ..explorerRetapCollapse = false) + ..explorer.layout = 'auto' + ..explorer.alwaysMarks = true + ..explorer.retapCollapse = false) .toJson()); - expect([again.explorerLayout, again.explorerAlwaysMarks, again.explorerRetapCollapse], ['auto', true, false]); + expect([again.explorer.layout, again.explorer.alwaysMarks, again.explorer.retapCollapse], ['auto', true, false]); expect(again.migrated, isEmpty); }); diff --git a/test/sftp_explorer_test.dart b/test/sftp_explorer_test.dart index 82a6dee..ca782a2 100644 --- a/test/sftp_explorer_test.dart +++ b/test/sftp_explorer_test.dart @@ -52,7 +52,7 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); await tester.runAsync(() => c.updateSettings((x) => x ..sftpServers = [SftpServer(id: 's1', name: 'NAS', host: '127.0.0.1', port: s!.port, user: 'jj', password: 'pw')] - ..explorerPaths = ['sftp://s1/', local.path])); + ..explorer.paths = ['sftp://s1/', local.path])); tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); diff --git a/test/shizuku_test.dart b/test/shizuku_test.dart index 2cbc713..e768470 100644 --- a/test/shizuku_test.dart +++ b/test/shizuku_test.dart @@ -113,8 +113,8 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..shizukuEnabled = on - ..explorerPaths = [tmp.path, tmp.path] - ..explorerLayout = 'single'; + ..explorer.paths = [tmp.path, tmp.path] + ..explorer.layout = 'single'; return c; } diff --git a/test/support/explorer_harness.dart b/test/support/explorer_harness.dart index 7d3675a..b35c7da 100644 --- a/test/support/explorer_harness.dart +++ b/test/support/explorer_harness.dart @@ -65,7 +65,7 @@ void useExplorerFixture() { _device = TestExplorerDevice(); _c = AppController(PlatformServices( mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), shell: shell)); - c.settings.explorerPaths = [left, right]; + c.settings.explorer.paths = [left, right]; }); // Windows: 방금 닫은 파일 (65 의 잠금 시험) 이나 색인 · 백신이 잠깐 잡고 있으면 지우기가 실패한다 - 될 때까지 기다린다 tearDown(() async { diff --git a/test/sync_problem_test.dart b/test/sync_problem_test.dart index 0c7c976..ab93940 100644 --- a/test/sync_problem_test.dart +++ b/test/sync_problem_test.dart @@ -187,7 +187,7 @@ void main() { ..createSync(recursive: true) ..writeAsStringSync('r'); c.settings - ..explorerLayout = 'single' + ..explorer.layout = 'single' ..rsyncOptions = '-avPog --delete' ..rsyncPaths = [l, r]; tester.view.physicalSize = const Size(1600, 900); @@ -221,7 +221,7 @@ void main() { old.setLastModifiedSync(DateTime.now().subtract(const Duration(hours: 1))); final writing = File(p.join(dir, 'now.mkv.jjpart'))..writeAsStringSync('half'); // 지금 쓰는 중 (방금 바뀜) expect(isPartialFile('a.jjsync'), isTrue); - c.settings.explorerPaths = [dir, dir]; + c.settings.explorer.paths = [dir, dir]; tester.view.physicalSize = const Size(1600, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); diff --git a/test/vault_page_test.dart b/test/vault_page_test.dart index a199731..5512840 100644 --- a/test/vault_page_test.dart +++ b/test/vault_page_test.dart @@ -215,8 +215,8 @@ void main() { for (final on in [false, true]) { final c = ctl() ..settings.vaultEnabled = on - ..settings.explorerPaths = [dir.path, dir.path] - ..settings.explorerLayout = 'split'; + ..settings.explorer.paths = [dir.path, dir.path] + ..settings.explorer.layout = 'split'; await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: c)))); await until(t, () => find.text('a.txt').evaluate().isNotEmpty); await t.longPress(find.text('a.txt').last);