From d83b0b5aef1fd6348657d37af416b88c661284e3 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:15:58 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=A4=EC=A0=95=20A:=20=EC=9D=BC=EB=B0=98=20?= =?UTF-8?q?=EB=B6=84=EC=95=BC=20(GeneralSettings)=20=C2=B7=20=EB=82=A8?= =?UTF-8?q?=EC=9D=80=20=ED=82=A4=20=EC=A0=95=EB=A6=AC=20-=20toJson=20?= =?UTF-8?q?=C2=B7=20fromJson=20=EC=9D=80=20=EB=B6=84=EC=95=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EB=A7=8C=20=EB=8F=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 일반: 즐겨찾기 · 저장 공간 정리 · 바뀐 기본값 알림 · 안쪽 → 바깥 · 비밀번호 기억 · 휴지통 · 모든 파일 안내 (allFilesHint2) - imageExts 는 보기 화면 분야 (ReaderSettings) 로 - 표시 cookieScopeV2 → 다운로드, robocopyLinksV1 → 복사 분야로 - AppSettings 에 남은 것: 분야 목록 · 모르는 항목 보존 · 비밀 값 빼기 · 바뀐 기본값 알림 · 실행 중 값 (webViewDataDir) Co-Authored-By: Claude Opus 5.5 --- .../windows_xplore_shot_test.dart | 2 +- lib/app/live_sync.dart | 4 +- lib/app/settings.dart | 225 +----------------- lib/app/settings/copy_settings.dart | 2 + lib/app/settings/download_settings.dart | 2 + lib/app/settings/general_settings.dart | 52 ++++ lib/app/settings/reader_settings.dart | 6 + lib/app/settings/settings_store.dart | 2 +- lib/app/settings/tor_settings.dart | 2 +- lib/ui/ai_image_page.dart | 2 +- lib/ui/all_files_guide.dart | 6 +- lib/ui/cleanup_dialog.dart | 2 +- lib/ui/component_settings.dart | 6 +- lib/ui/copy_sync_settings.dart | 4 +- lib/ui/explorer/delete_flow.dart | 2 +- lib/ui/explorer/explorer_controller.dart | 4 +- lib/ui/explorer/item_menu.dart | 8 +- lib/ui/explorer/open_flow.dart | 10 +- lib/ui/explorer/pane_header.dart | 2 +- lib/ui/explorer/pane_view.dart | 2 +- lib/ui/explorer/rsync_flow.dart | 2 +- lib/ui/explorer_bookmarks.dart | 10 +- lib/ui/migration_notice.dart | 2 +- lib/ui/reader/reader_dialogs.dart | 2 +- lib/ui/settings/app_sections.dart | 6 +- lib/ui/settings/files_section.dart | 4 +- lib/ui/settings/general_sections.dart | 8 +- lib/ui/settings/transfer_sections.dart | 4 +- lib/ui/vault/vault_actions.dart | 2 +- lib/ui/zip_page.dart | 6 +- test/decisions_test.dart | 28 +-- test/explorer_filter_rsync_test.dart | 4 +- test/explorer_panes_test.dart | 2 +- test/explorer_select_ops_test.dart | 12 +- test/explorer_webdav_test.dart | 2 +- test/reader_test.dart | 6 +- test/restore_access_test.dart | 4 +- test/settings_sections_contract_test.dart | 4 +- test/small_items_test.dart | 4 +- 39 files changed, 157 insertions(+), 300 deletions(-) create mode 100644 lib/app/settings/general_settings.dart diff --git a/integration_test/windows_xplore_shot_test.dart b/integration_test/windows_xplore_shot_test.dart index 771df2b..f612652 100644 --- a/integration_test/windows_xplore_shot_test.dart +++ b/integration_test/windows_xplore_shot_test.dart @@ -56,7 +56,7 @@ void main() { ..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'))]; + ..general.folderBookmarks = [FolderBookmark(name: 'Show S01', path: p.join(a.path, 'Show S01'))]; final shotKey = GlobalKey(); await t.runAsync(() => t.pumpWidget(RepaintBoundary( key: shotKey, diff --git a/lib/app/live_sync.dart b/lib/app/live_sync.dart index e90ea95..b49de28 100644 --- a/lib/app/live_sync.dart +++ b/lib/app/live_sync.dart @@ -463,7 +463,7 @@ class LiveSync extends ChangeNotifier { continue; } // 대상이 원본 안이면 맞출 때마다 원본이 바뀌어 끝없이 돈다. 34: 원본이 대상 안 (안쪽 → 바깥) 은 지우기가 없을 때만 - if (nestingProblem(x, allowInnerToOuter: s.allowInnerToOuter) case final nest?) { + if (nestingProblem(x, allowInnerToOuter: s.general.allowInnerToOuter) case final nest?) { status[k] = (DateTime.now(), nest); continue; } @@ -519,7 +519,7 @@ class LiveSync extends ChangeNotifier { final bad = unreadableOf(x); final nest = bad.isNotEmpty ? optionProblemMessage(bad) - : nestingProblem(x, allowInnerToOuter: c.settings.allowInnerToOuter); + : nestingProblem(x, allowInnerToOuter: c.settings.general.allowInnerToOuter); if (nest != null) { status[k] = (DateTime.now(), nest); notifyListeners(); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 846af0b..e6bea5f 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -2,10 +2,9 @@ import 'dart:io'; import 'package:path/path.dart' as p; -import '../core/reader_sources.dart' show defaultImageExtensions; import '../core/download_detect.dart'; -import 'settings/records.dart'; import 'settings/settings_group.dart'; +import 'settings/general_settings.dart'; import 'settings/playback_settings.dart'; import 'settings/mkv_settings.dart'; import 'settings/display_settings.dart'; @@ -25,6 +24,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/general_settings.dart'; export 'settings/playback_settings.dart'; export 'settings/mkv_settings.dart'; export 'settings/display_settings.dart'; @@ -43,192 +43,9 @@ export 'settings/explorer_settings.dart'; /// 환경 설정 (settings.json 에 저장) class AppSettings { - - - /// 73: 파일 탐색기 · Rsync 즐겨찾기 (폴더, Rsync 는 짝) - List folderBookmarks = []; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ── 네트워크 · DNS (사용자 요청 10/10: "DNS 주소를 강제로 내가 설정한 주소로", 기본 8.8.8.8 · 보조 8.8.4.4 - 관리자 판단) ── - - - // ── Tor 브라우저 (사용자 요청 10/10) ── - - - - - - // ── 파일 탐색기 (X-plore 참고) ── - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ── - - - - - - - - - - - - - - /// 그림 보기 (만화 보기) 대상 확장자 (파일 탐색기에서 두 번 누르면 보기로 연다) - List imageExts = [...defaultImageExtensions]; - - - - - - - - - - - - // ── 102: 사용자에게 물어 정한 것 · 관리자 1차 판단 (기본값 = 그 답) ── - - - - /// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드) - List cleanupPrechecked = ['work', 'download']; - - /// 19: 업데이트로 바뀐 기본값을 켤 때 한 번 알림 - bool migrationNotice = true; - - /// 34: Rsync · 동기화에서 원본이 대상 안 (안쪽 → 바깥) 을 허용 (지우기가 있을 때만 막음). 끄면 늘 막음 - bool allowInnerToOuter = true; - - /// 40 · 54: 비밀번호를 이 기기의 안전 저장소에 기억 (끄면 저장하지 않고 앱을 켤 때마다 다시 넣음) - bool rememberPasswords = true; - - - /// 65 (Windows): 탐색기에서 지우면 휴지통으로 (끄면 늘 영구 삭제로 묻는다) - bool recycleOnDelete = true; - - - - - /// 39: 켤 때 "모든 파일 접근" 안내를 한 번 보였는지 (그 뒤로는 파일 탐색기 안의 안내로) - bool allFilesHintShown = false; - - - - - - - - - - // ───────── 123 · 121: AI 그림 · 해상도 올리기 ───────── - - - - - - - - - - // 121: 해상도 올리기 - - - - - - - - - - - - - - - /// 앱 안 브라우저의 데이터 폴더 (로그인 · 쿠키). 실행 중에 정해지며 저장하지 않음 String webViewDataDir = ''; - - - - - - - - - AppSettings(); /// 파일 탐색기 (X-plore 참고) @@ -237,7 +54,7 @@ class AppSettings { /// 네트워크 · DNS (사용자 요청 10/10) final network = NetworkSettings(); - /// Tor 釉뚮씪?곗? (?ъ슜???붿껌 10/10) + /// Tor 브라우저 (사용자 요청 10/10) final tor = TorBrowserSettings(); /// 복사 · 이동 (파일 탐색기) · rsync · 실시간 동기화 (Rsync 화면) @@ -276,26 +93,16 @@ class AppSettings { /// 재생 (동영상 열기 · 재생 목록 · 외부 플레이어 · 자막 크기 · 기본 자막 언어) final playback = PlaybackSettings(); + /// 일반 (즐겨찾기 · 저장 공간 정리 · 바뀐 기본값 알림 · 안쪽 → 바깥 동기화 · 비밀번호 기억 · 휴지통 · 모든 파일 안내) + final general = GeneralSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote, display, mkv, playback]; + List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools, remote, display, mkv, playback, general]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { - final j = { - // 이 판이 모르는 항목 (새 판이 쓴 것 등) 은 지우지 않고 그대로 (40) - ...stripSecrets(extra), - 'folderBookmarks': [for (final b in folderBookmarks) b.toJson()], - 'cookieScopeV2': true, - 'robocopyLinksV1': true, - 'imageExts': imageExts, - 'cleanupPrechecked': cleanupPrechecked, - 'migrationNotice': migrationNotice, - 'allowInnerToOuter': allowInnerToOuter, - 'rememberPasswords': rememberPasswords, - 'recycleOnDelete': recycleOnDelete, - // 116: 예전 판의 표시 ('allFilesHintShown') 는 권한이 없어진 것을 몰라 다시 띄우지 못했다 → 새 이름으로 한 번 더 - 'allFilesHint2': allFilesHintShown, - }; + // 이 판이 모르는 항목 (새 판이 쓴 것 등) 은 지우지 않고 그대로 (40) + final j = {...stripSecrets(extra)}; for (final g in groups) { g.write(j, plainSecrets: plainSecrets); } @@ -303,19 +110,7 @@ class AppSettings { } factory AppSettings.fromJson(Map j) { - final s = AppSettings() - ..folderBookmarks = [for (final x in (j['folderBookmarks'] as List?) ?? const []) ?FolderBookmark.fromJson(x)] - ..imageExts = j['imageExts'] is List - ? [for (final x in j['imageExts'] as List) '$x'.toLowerCase()] - : [...defaultImageExtensions] - ..cleanupPrechecked = j['cleanupPrechecked'] is List - ? [for (final x in j['cleanupPrechecked'] as List) '$x'] - : ['work', 'download'] - ..migrationNotice = j['migrationNotice'] != false - ..allowInnerToOuter = j['allowInnerToOuter'] != false - ..rememberPasswords = j['rememberPasswords'] != false - ..recycleOnDelete = j['recycleOnDelete'] != false - ..allFilesHintShown = j['allFilesHint2'] == true; + final s = AppSettings(); // 분야를 먼저 읽는다 - 바뀐 기본값 알림 ([_migrated]) 이 분야의 값을 본다 for (final g in s.groups) { g.read(j); diff --git a/lib/app/settings/copy_settings.dart b/lib/app/settings/copy_settings.dart index c359328..ef9edfd 100644 --- a/lib/app/settings/copy_settings.dart +++ b/lib/app/settings/copy_settings.dart @@ -72,6 +72,8 @@ class CopySettings implements SettingsGroup { j['copyTasks'] = [for (final x in tasks) x.toJson()]; j['rsyncPaths'] = rsyncPaths; j['syncStopToast'] = syncStopToast; + // 193: robocopy 가 링크 · junction 을 따라가지 않게 (/XJ) 바뀐 것을 알린 판 (바뀐 기본값 알림 'robocopyLinks') + j['robocopyLinksV1'] = true; } @override diff --git a/lib/app/settings/download_settings.dart b/lib/app/settings/download_settings.dart index 75454fb..48a16e8 100644 --- a/lib/app/settings/download_settings.dart +++ b/lib/app/settings/download_settings.dart @@ -54,6 +54,8 @@ class DownloadSettings implements SettingsGroup { j['loginCookieSites'] = loginCookieSites; j['ytCookiesFile'] = ytCookiesFile; j['maxParallelDownloads'] = maxParallel; + // 53 · 28: 쿠키를 넘기는 사이트가 넓어진 것을 알린 판 (바뀐 기본값 알림 'cookieScope') + j['cookieScopeV2'] = true; } @override diff --git a/lib/app/settings/general_settings.dart b/lib/app/settings/general_settings.dart new file mode 100644 index 0000000..b5f88e1 --- /dev/null +++ b/lib/app/settings/general_settings.dart @@ -0,0 +1,52 @@ +import 'records.dart'; +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 일반 (즐겨찾기 · 저장 공간 정리 · 바뀐 기본값 알림 · 안쪽 → 바깥 동기화 · 비밀번호 기억 · 휴지통 · 모든 파일 안내) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class GeneralSettings implements SettingsGroup { + /// 73: 파일 탐색기 · Rsync 즐겨찾기 (폴더, Rsync 는 짝) + List folderBookmarks = []; + + /// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드) + List cleanupPrechecked = ['work', 'download']; + + /// 19: 업데이트로 바뀐 기본값을 켤 때 한 번 알림 + bool migrationNotice = true; + + /// 34: Rsync · 동기화에서 원본이 대상 안 (안쪽 → 바깥) 을 허용 (지우기가 있을 때만 막음). 끄면 늘 막음 + bool allowInnerToOuter = true; + + /// 40 · 54: 비밀번호를 이 기기의 안전 저장소에 기억 (끄면 저장하지 않고 앱을 켤 때마다 다시 넣음) + bool rememberPasswords = true; + + /// 65 (Windows): 탐색기에서 지우면 휴지통으로 (끄면 늘 영구 삭제로 묻는다) + bool recycleOnDelete = true; + + /// 39: 켤 때 "모든 파일 접근" 안내를 한 번 보였는지 (그 뒤로는 파일 탐색기 안의 안내로) + bool allFilesHintShown = false; + + @override + void write(Map j, {required bool plainSecrets}) { + j['folderBookmarks'] = [for (final b in folderBookmarks) b.toJson()]; + j['cleanupPrechecked'] = cleanupPrechecked; + j['migrationNotice'] = migrationNotice; + j['allowInnerToOuter'] = allowInnerToOuter; + j['rememberPasswords'] = rememberPasswords; + j['recycleOnDelete'] = recycleOnDelete; + // 116: 예전 판의 표시 ('allFilesHintShown') 는 권한이 없어진 것을 몰라 다시 띄우지 못했다 → 새 이름으로 한 번 더 + j['allFilesHint2'] = allFilesHintShown; + } + + @override + void read(Map j) { + folderBookmarks = [for (final x in (j['folderBookmarks'] as List?) ?? const []) ?FolderBookmark.fromJson(x)]; + cleanupPrechecked = j['cleanupPrechecked'] is List + ? [for (final x in j['cleanupPrechecked'] as List) '$x'] + : ['work', 'download']; + migrationNotice = j['migrationNotice'] != false; + allowInnerToOuter = j['allowInnerToOuter'] != false; + rememberPasswords = j['rememberPasswords'] != false; + recycleOnDelete = j['recycleOnDelete'] != false; + allFilesHintShown = j['allFilesHint2'] == true; + } +} diff --git a/lib/app/settings/reader_settings.dart b/lib/app/settings/reader_settings.dart index a8d1648..f451686 100644 --- a/lib/app/settings/reader_settings.dart +++ b/lib/app/settings/reader_settings.dart @@ -1,4 +1,5 @@ +import '../../core/reader_sources.dart' show defaultImageExtensions; import 'settings_group.dart'; /// 약속 (설정 A): 환경 설정 > 보기 화면 (그림 · 만화 · PDF · 문서) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. @@ -32,6 +33,9 @@ class ReaderSettings implements SettingsGroup { /// ZIP · CBZ 를 두 번 누르면 안의 그림을 만화처럼 보기 (끄면 목록). 기본은 목록 (사용자 지시) bool zipComic = false; + /// 그림 보기 (만화 보기) 대상 확장자 (파일 탐색기에서 두 번 누르면 보기로 연다) + List imageExts = [...defaultImageExtensions]; + @override void write(Map j, {required bool plainSecrets}) { j['readerFit'] = fit; @@ -43,6 +47,7 @@ class ReaderSettings implements SettingsGroup { j['readerVolumeKeys'] = volumeKeys; j['readerSplit'] = split; j['zipComic'] = zipComic; + j['imageExts'] = imageExts; // 예전 기본값 (만화 보기 켜짐) 은 지시와 반대였으므로 한 번 꺼진 상태 (목록) 로 j['zipComicV2'] = true; } @@ -58,5 +63,6 @@ class ReaderSettings implements SettingsGroup { volumeKeys = j['readerVolumeKeys'] as bool? ?? false; split = const ['auto', 'on', 'off'].contains(j['readerSplit']) ? j['readerSplit'] as String : 'auto'; zipComic = j['zipComic'] == true && j['zipComicV2'] == true; + imageExts = j['imageExts'] is List ? [for (final x in j['imageExts'] as List) '$x'.toLowerCase()] : [...defaultImageExtensions]; } } diff --git a/lib/app/settings/settings_store.dart b/lib/app/settings/settings_store.dart index 17d8521..5a1cde0 100644 --- a/lib/app/settings/settings_store.dart +++ b/lib/app/settings/settings_store.dart @@ -62,7 +62,7 @@ class SettingsStore { /// 지금 설정의 비밀 값 (빈 값은 넣지 않음 = 저장소에서 지움) /// 규칙 (관리자 10/21): 새 종류의 비밀 값은 지금 꼴 ([secretForms]) 과 앞부분이 겹치지 않는 새 머리글 (dav2. 등) 로 만든다 - /// 겹치면 그 꼴을 아는 예전 판이 저장할 때 "없어진 항목" 으로 보고 지운다. 새 꼴은 [secretForms] 에도 더한다 - static Map _secretsOf(AppSettings s) => !s.rememberPasswords ? const {} : { + static Map _secretsOf(AppSettings s) => !s.general.rememberPasswords ? const {} : { for (final e in _osKeys.entries) if (_os(s, e.key).isNotEmpty) e.value: _os(s, e.key), for (final x in s.remote.webdavServers) diff --git a/lib/app/settings/tor_settings.dart b/lib/app/settings/tor_settings.dart index 3040cbe..94d8059 100644 --- a/lib/app/settings/tor_settings.dart +++ b/lib/app/settings/tor_settings.dart @@ -1,7 +1,7 @@ import 'settings_group.dart'; -/// 약속 (설정 A): 환경 설정 > Tor 釉뚮씪?곗? (?ъ슜???붿껌 10/10) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 약속 (설정 A): 환경 설정 > Tor 브라우저 (사용자 요청 10/10) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. /// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) class TorBrowserSettings implements SettingsGroup { /// Tor 화면의 쿠키 · 기록 · 캐시를 닫은 뒤에도 남기기 (기본: 닫으면 지움) diff --git a/lib/ui/ai_image_page.dart b/lib/ui/ai_image_page.dart index da225c0..59852ff 100644 --- a/lib/ui/ai_image_page.dart +++ b/lib/ui/ai_image_page.dart @@ -184,7 +184,7 @@ class _AiImagePageState extends State with RouteAware { } Future _pickInit() async { - final files = await c.services.storage.pickFiles(title: tr('바탕 그림 고르기'), extensions: c.settings.imageExts); + final files = await c.services.storage.pickFiles(title: tr('바탕 그림 고르기'), extensions: c.settings.reader.imageExts); if (files.isNotEmpty && mounted) setState(() => _init = files.first); } diff --git a/lib/ui/all_files_guide.dart b/lib/ui/all_files_guide.dart index cca3468..b2d82c3 100644 --- a/lib/ui/all_files_guide.dart +++ b/lib/ui/all_files_guide.dart @@ -9,11 +9,11 @@ import '../l10n/tr.dart'; /// 권한이 있으면 표시를 지워 두어 권한이 없어진 뒤 (사용자가 끔 · 재설치) 다음에 켤 때 한 번 더 띄운다. Future allFilesGuideDue(AppController c, {required bool hasAccess}) async { if (hasAccess) { - if (c.settings.allFilesHintShown) await c.updateSettings((s) => s.allFilesHintShown = false); + if (c.settings.general.allFilesHintShown) await c.updateSettings((s) => s.general.allFilesHintShown = false); return false; } - if (c.settings.allFilesHintShown) return false; - await c.updateSettings((s) => s.allFilesHintShown = true); + if (c.settings.general.allFilesHintShown) return false; + await c.updateSettings((s) => s.general.allFilesHintShown = true); return true; } diff --git a/lib/ui/cleanup_dialog.dart b/lib/ui/cleanup_dialog.dart index e5889af..0ffa384 100644 --- a/lib/ui/cleanup_dialog.dart +++ b/lib/ui/cleanup_dialog.dart @@ -43,7 +43,7 @@ class _CleanupDialogState extends State<_CleanupDialog> { _groups = g; // 처음에는 작업 임시 파일 · 받다 만 다운로드만 고른 상태로 (사용자 결정 10/8). // 받다 만 AI 모델 · 지난 작업 기록 · 업데이트 남은 파일은 직접 체크해야 지운다 - final pre = widget.cleaner.c.settings.cleanupPrechecked.toSet(); // 102: 환경 설정 (기본: 작업 임시 파일 · 받다 만 다운로드) + final pre = widget.cleaner.c.settings.general.cleanupPrechecked.toSet(); // 102: 환경 설정 (기본: 작업 임시 파일 · 받다 만 다운로드) _picked.addAll([for (final x in g) if (x.skipped == null && x.items.isNotEmpty && pre.contains(x.id)) x.id]); }); }); diff --git a/lib/ui/component_settings.dart b/lib/ui/component_settings.dart index 35d3c19..699aa19 100644 --- a/lib/ui/component_settings.dart +++ b/lib/ui/component_settings.dart @@ -33,9 +33,9 @@ class ViewerSettings extends StatelessWidget { for (final e in _choices) FilterChip( label: Text(e), - selected: s.imageExts.contains(e), - onSelected: (on) => c.updateSettings((x) => x.imageExts = [ - for (final y in x.imageExts) if (y != e) y, + selected: s.reader.imageExts.contains(e), + onSelected: (on) => c.updateSettings((x) => x.reader.imageExts = [ + for (final y in x.reader.imageExts) if (y != e) y, if (on) e, ]), ), diff --git a/lib/ui/copy_sync_settings.dart b/lib/ui/copy_sync_settings.dart index 76ada1c..89954e4 100644 --- a/lib/ui/copy_sync_settings.dart +++ b/lib/ui/copy_sync_settings.dart @@ -325,7 +325,7 @@ class _LiveSyncTile extends StatelessWidget { pathRow(ctx, tr('원본'), src!, (v) => set(() => src = v)), pathRow(ctx, tr('대상'), dst!, (v) => set(() => dst = v)), // 34: 안쪽 → 바깥은 지우기 없이만, 바깥 → 안쪽은 안 됨 (끝없이 돎) - if (LiveSync.nestingProblem(LiveSyncPair(src!, dst!, delete: delete), allowInnerToOuter: c.settings.allowInnerToOuter) + if (LiveSync.nestingProblem(LiveSyncPair(src!, dst!, delete: delete), allowInnerToOuter: c.settings.general.allowInnerToOuter) case final nest?) Text(nest, style: TextStyle(fontSize: 12, color: JjColors.alert)) else if (isSameOrInside(src!, dst!)) @@ -361,7 +361,7 @@ class _LiveSyncTile extends StatelessWidget { actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))), FilledButton( - onPressed: LiveSync.nestingProblem(LiveSyncPair(src!, dst!, delete: delete), allowInnerToOuter: c.settings.allowInnerToOuter) != + onPressed: LiveSync.nestingProblem(LiveSyncPair(src!, dst!, delete: delete), allowInnerToOuter: c.settings.general.allowInnerToOuter) != null ? null : () => Navigator.pop(ctx, true), diff --git a/lib/ui/explorer/delete_flow.dart b/lib/ui/explorer/delete_flow.dart index 6e62b34..84fe8d1 100644 --- a/lib/ui/explorer/delete_flow.dart +++ b/lib/ui/explorer/delete_flow.dart @@ -51,7 +51,7 @@ class ExplorerDeleteFlow { // X-plore 비교 e: Android 는 앱 휴지통 (저장 장치의 .jj_trash, 보관 일이 0 이면 끔) final trash = appTrash; final canTrash = trash != null && paths.every((x) => !isDav(x) && trash.trashDirFor(x) != null && !trash.isInTrash(x)); - final canRecycle = (local && ctl.c.settings.recycleOnDelete && paths.every(hasRecycleBin)) || canTrash; + final canRecycle = (local && ctl.c.settings.general.recycleOnDelete && paths.every(hasRecycleBin)) || canTrash; var permanent = !canRecycle || shift; // X-plore 숨기기: 지우는 폴더 안에 목록에서 숨긴 것이 있으면 알린다 (숨긴 것도 함께 지워진다) final hiddenInside = [ diff --git a/lib/ui/explorer/explorer_controller.dart b/lib/ui/explorer/explorer_controller.dart index 38f7783..8396945 100644 --- a/lib/ui/explorer/explorer_controller.dart +++ b/lib/ui/explorer/explorer_controller.dart @@ -293,7 +293,7 @@ class ExplorerController extends ChangeNotifier { } final files = [for (final e in list ?? const []) if (!e.isDir) e]; if (files.isEmpty) return null; - final media = files.where((e) => c.settings.imageExts.contains(e.ext) || isVideoFile(e.path)).length; + final media = files.where((e) => c.settings.reader.imageExts.contains(e.ext) || isVideoFile(e.path)).length; if (media * 2 > files.length) return ('date', true); if (files.where((e) => isAudioFile(e.path)).length * 2 > files.length) return ('name', false); return null; @@ -425,7 +425,7 @@ class ExplorerController extends ChangeNotifier { List rows(ExplorerPane q, {bool foldersOnly = false}) => buildTreeRows(q, foldersOnly: foldersOnly, roots: rootsInTree ? [for (final (path, _) in volumes) path] : const [], - favorites: rootsInTree ? [for (final b in c.settings.folderBookmarks) b.path] : const [], + favorites: rootsInTree ? [for (final b in c.settings.general.folderBookmarks) b.path] : const [], isHidden: appHidden, showHidden: showAppHidden, browsable: arc.contains); diff --git a/lib/ui/explorer/item_menu.dart b/lib/ui/explorer/item_menu.dart index a4c5531..a546474 100644 --- a/lib/ui/explorer/item_menu.dart +++ b/lib/ui/explorer/item_menu.dart @@ -52,10 +52,10 @@ class ExplorerItemMenu { // 그림 → PDF: 표시한 것 중 그림 (누른 것이 표시에 없으면 누른 것만) final viewer = ctl.c.settings.display.components.contains('viewer'); final picked = pane.sel.has(e.path) ? pane.sel.marked : [e.path]; - final images = [for (final x in picked) if (ctl.c.settings.imageExts.contains(extOf(x))) x]; + final images = [for (final x in picked) if (ctl.c.settings.reader.imageExts.contains(extOf(x))) x]; final ext = extOf(e.path); final viewable = viewer && !e.isDir && - (ctl.c.settings.imageExts.contains(ext) || ext == 'pdf' || zipExtensions.contains(ext)); + (ctl.c.settings.reader.imageExts.contains(ext) || ext == 'pdf' || zipExtensions.contains(ext)); final dual = cmd.dual; // 77: 누른 것이 표시한 것 중 하나면 표시한 것 모두에 (복사 · 이동 · 삭제 · 공유 · 경로 복사) final targets = sel.isMarked(pane, e.path) && pane.sel.marked.length > 1 ? pane.sel.marked : [e.path]; @@ -130,7 +130,7 @@ class ExplorerItemMenu { if (flows.transfers.clip.isNotEmpty) ('paste', Icons.content_paste, trf('{0} 에 붙여넣기 ({1}개)', [vBasename(e.isDir ? e.path : vDirname(e.path)), flows.transfers.clip.items.length])), ('copyPath', Icons.link, many ? trf('경로 복사 ({0}개)', [targets.length]) : tr('경로 복사')), - if (e.isDir && !ctl.c.settings.folderBookmarks.any((b) => !b.isPair && samePath(b.path, e.path))) + if (e.isDir && !ctl.c.settings.general.folderBookmarks.any((b) => !b.isPair && samePath(b.path, e.path))) ('bookmark', Icons.star_outline, tr('즐겨찾기에 추가')), if (!dav && (!Platform.isAndroid || e.isDir)) ('reveal', Icons.folder_outlined, tr('파일 관리자에서 보기')), ('info', Icons.info_outline, tr('정보')), @@ -306,7 +306,7 @@ class ExplorerItemMenu { ctl.host.snack(many ? trf('경로 {0}개를 복사했습니다', [paths.length]) : trf('경로를 복사했습니다: {0}', [paths.single])); case 'bookmark': final name = vBasename(e.path).isEmpty ? e.path : vBasename(e.path); - await ctl.c.updateSettings((s) => s.folderBookmarks = [...s.folderBookmarks, FolderBookmark(name: name, path: e.path)]); + await ctl.c.updateSettings((s) => s.general.folderBookmarks = [...s.general.folderBookmarks, FolderBookmark(name: name, path: e.path)]); ctl.host.snack(trf('즐겨찾기에 넣었습니다: {0}', [name])); ctl.update(); case 'view': diff --git a/lib/ui/explorer/open_flow.dart b/lib/ui/explorer/open_flow.dart index d1db6b3..aad63ed 100644 --- a/lib/ui/explorer/open_flow.dart +++ b/lib/ui/explorer/open_flow.dart @@ -58,7 +58,7 @@ class ExplorerOpener { // 여는 길은 file_opener (E5c) 가 고른다 switch (openRouteFor(path, components: ctl.c.settings.display.components, - imageExts: ctl.c.settings.imageExts, + imageExts: ctl.c.settings.reader.imageExts, android: Platform.isAndroid, windows: Platform.isWindows, hwpEngine: RhwpEngine.available)) { @@ -105,7 +105,7 @@ class ExplorerOpener { builder: (_) => ExplorerGridPage( title: vBasename(dir).isEmpty ? dir : vBasename(dir), entries: list, - imageExts: ctl.c.settings.imageExts, + imageExts: ctl.c.settings.reader.imageExts, videoThumb: (e) => BigVideoThumbs.of(ctl.c, e), onOpen: (e) => open(context, e.path, pane: pane), ), @@ -137,7 +137,7 @@ class ExplorerOpener { } final images = [ for (final e in list) - if (!e.isDir && ctl.c.settings.imageExts.contains(extOf(e.path))) e.path, + if (!e.isDir && ctl.c.settings.reader.imageExts.contains(extOf(e.path))) e.path, ]; if (!images.any((x) => samePath(x, path))) images.insert(0, path); final start = images.indexWhere((x) => samePath(x, path)); @@ -157,7 +157,7 @@ class ExplorerOpener { } final images = [ for (final e in list) - if (!e.isDir && ctl.c.settings.imageExts.contains(extOf(e.path)) && (ctl.showAppHidden || !ctl.appHidden(e.path))) e.path, + if (!e.isDir && ctl.c.settings.reader.imageExts.contains(extOf(e.path)) && (ctl.showAppHidden || !ctl.appHidden(e.path))) e.path, ]; if (images.isEmpty) { ctl.host.snack(tr('이 폴더에는 그림이 없습니다')); @@ -361,7 +361,7 @@ class ExplorerOpener { index: i, probe: ctl.c.services.mediaTool.probe, thumbnail: (e) => BigVideoThumbs.of(ctl.c, e), - imageExts: ctl.c.settings.imageExts, + imageExts: ctl.c.settings.reader.imageExts, onOpen: (e) => e.isDir ? unawaited(ctl.goTo(pane, e.path)) : unawaited(open(context, e.path, pane: pane)), ), ); diff --git a/lib/ui/explorer/pane_header.dart b/lib/ui/explorer/pane_header.dart index 6e1b024..b40cc00 100644 --- a/lib/ui/explorer/pane_header.dart +++ b/lib/ui/explorer/pane_header.dart @@ -123,7 +123,7 @@ class ExplorerPaneHeader extends StatelessWidget { tooltip: tr('즐겨찾기'), visualDensity: VisualDensity.compact, icon: Icon( - ctl.c.settings.folderBookmarks.any((b) => !b.isPair && samePath(b.path, pane.current)) ? Icons.star : Icons.star_outline, + ctl.c.settings.general.folderBookmarks.any((b) => !b.isPair && samePath(b.path, pane.current)) ? Icons.star : Icons.star_outline, size: 16, color: JjColors.amber), onPressed: () => onBookmarks(), diff --git a/lib/ui/explorer/pane_view.dart b/lib/ui/explorer/pane_view.dart index 5507ebc..3c23d09 100644 --- a/lib/ui/explorer/pane_view.dart +++ b/lib/ui/explorer/pane_view.dart @@ -320,7 +320,7 @@ class ExplorerExtraRow extends StatelessWidget { final path = row.entry.path; final volume = ctl.volumes.firstWhere((v) => samePath(v.$1, path), orElse: () => (path, vBasename(path))).$2; final fav = row.kind == TreeRow.favorite - ? ctl.c.settings.folderBookmarks.firstWhere((b) => b.path == path, orElse: () => FolderBookmark(name: vBasename(path), path: path)) + ? ctl.c.settings.general.folderBookmarks.firstWhere((b) => b.path == path, orElse: () => FolderBookmark(name: vBasename(path), path: path)) : null; final (IconData icon, Color color, String label, bool? open) = switch (row.kind) { TreeRow.otherRoot => ( diff --git a/lib/ui/explorer/rsync_flow.dart b/lib/ui/explorer/rsync_flow.dart index 0ca2ced..d38ad74 100644 --- a/lib/ui/explorer/rsync_flow.dart +++ b/lib/ui/explorer/rsync_flow.dart @@ -44,7 +44,7 @@ class ExplorerRsyncFlow { r: r, right: right, left: left, - allowInnerToOuter: ctl.c.settings.allowInnerToOuter, + allowInnerToOuter: ctl.c.settings.general.allowInnerToOuter, jobs: this.jobs.list, viaDav: l != null && r != null && ExplorerTransfers.viaDav([l, r]), copySkipNewer: ctl.c.settings.copy.skipNewer); diff --git a/lib/ui/explorer_bookmarks.dart b/lib/ui/explorer_bookmarks.dart index 9a13b84..fef0f0b 100644 --- a/lib/ui/explorer_bookmarks.dart +++ b/lib/ui/explorer_bookmarks.dart @@ -41,7 +41,7 @@ class _BookmarksDialogState extends State<_BookmarksDialog> { bool get _rsync => widget.right != null && widget.goPair != null; Future _save(List list) async { - await widget.c.updateSettings((x) => x.folderBookmarks = list); + await widget.c.updateSettings((x) => x.general.folderBookmarks = list); if (mounted) setState(() {}); } @@ -57,7 +57,7 @@ class _BookmarksDialogState extends State<_BookmarksDialog> { : FolderBookmark(name: vBasename(widget.current).isEmpty ? widget.current : vBasename(widget.current), path: widget.current); final name = await _askName(b.name, pair ? tr('지금 짝을 즐겨찾기에') : tr('지금 폴더를 즐겨찾기에')); if (name == null) return; - await _save([...s.folderBookmarks, b.copyWith(name: name)]); + await _save([...s.general.folderBookmarks, b.copyWith(name: name)]); } void _open(FolderBookmark b) { @@ -71,7 +71,7 @@ class _BookmarksDialogState extends State<_BookmarksDialog> { @override Widget build(BuildContext context) { - final mine = [for (final b in s.folderBookmarks) if (_rsync || !b.isPair) b]; + final mine = [for (final b in s.general.folderBookmarks) if (_rsync || !b.isPair) b]; // 바로 가기: 다운로드 폴더 · MKV 결과 폴더 (넣지 않아도) final shortcuts = [ (tr('다운로드'), s.resolvedDownloadRoot()), @@ -105,14 +105,14 @@ class _BookmarksDialogState extends State<_BookmarksDialog> { icon: const Icon(Icons.edit_outlined, size: 18), onPressed: () async { final name = await _askName(b.name, tr('이름 바꾸기')); - if (name != null) await _save([for (final x in s.folderBookmarks) identical(x, b) ? x.copyWith(name: name) : x]); + if (name != null) await _save([for (final x in s.general.folderBookmarks) identical(x, b) ? x.copyWith(name: name) : x]); }, ), IconButton( tooltip: tr('즐겨찾기에서 빼기 (폴더는 그대로)'), visualDensity: VisualDensity.compact, icon: const Icon(Icons.close, size: 18), - onPressed: () => _save([for (final x in s.folderBookmarks) if (!identical(x, b)) x]), + onPressed: () => _save([for (final x in s.general.folderBookmarks) if (!identical(x, b)) x]), ), ]), ), diff --git a/lib/ui/migration_notice.dart b/lib/ui/migration_notice.dart index 5c3a978..815f8bd 100644 --- a/lib/ui/migration_notice.dart +++ b/lib/ui/migration_notice.dart @@ -7,7 +7,7 @@ import 'settings_page.dart'; /// 업데이트하면서 예전 기본값을 새 기본값으로 한 번 옮겼으면, 켤 때 무엇이 바뀌었는지와 /// 환경 설정에서 되돌릴 수 있다는 것을 한 번 알린다 ([AppSettings.migrated]). Future showMigrationNotice(BuildContext context, AppController c) async { - if (!c.settings.migrationNotice) return; // 102: 환경 설정에서 끌 수 있음 + if (!c.settings.general.migrationNotice) return; // 102: 환경 설정에서 끌 수 있음 final items = c.settings.migrated; if (items.isEmpty) return; String line(String id) => switch (id) { diff --git a/lib/ui/reader/reader_dialogs.dart b/lib/ui/reader/reader_dialogs.dart index 249060b..b92a22f 100644 --- a/lib/ui/reader/reader_dialogs.dart +++ b/lib/ui/reader/reader_dialogs.dart @@ -143,4 +143,4 @@ Future askSaveChanges(BuildContext context) => showDialog( /// PDF 에 넣을 그림을 고른다 (설정의 그림 확장자). 고르지 않으면 빈 목록 Future> pickPdfImages(AppController c) => - c.services.storage.pickFiles(title: tr('PDF 에 넣을 그림 선택'), extensions: c.settings.imageExts); + c.services.storage.pickFiles(title: tr('PDF 에 넣을 그림 선택'), extensions: c.settings.reader.imageExts); diff --git a/lib/ui/settings/app_sections.dart b/lib/ui/settings/app_sections.dart index 48a6e8c..268ed61 100644 --- a/lib/ui/settings/app_sections.dart +++ b/lib/ui/settings/app_sections.dart @@ -153,9 +153,9 @@ class _CleanupSettingsSectionState extends State { ]) FilterChip( label: Text(label), - selected: s.cleanupPrechecked.contains(id), - onSelected: (on) => c.updateSettings((x) => x.cleanupPrechecked = [ - for (final y in x.cleanupPrechecked) if (y != id) y, + selected: s.general.cleanupPrechecked.contains(id), + onSelected: (on) => c.updateSettings((x) => x.general.cleanupPrechecked = [ + for (final y in x.general.cleanupPrechecked) if (y != id) y, if (on) id, ]), ), diff --git a/lib/ui/settings/files_section.dart b/lib/ui/settings/files_section.dart index 8c49a03..9743126 100644 --- a/lib/ui/settings/files_section.dart +++ b/lib/ui/settings/files_section.dart @@ -45,8 +45,8 @@ class _FilesSettingsSectionState extends State { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ if (desk) SwitchListTile( - value: s.recycleOnDelete, - onChanged: (v) => c.updateSettings((x) => x.recycleOnDelete = v), + value: s.general.recycleOnDelete, + onChanged: (v) => c.updateSettings((x) => x.general.recycleOnDelete = v), title: Text(tr('지우면 휴지통으로')), subtitle: Text(tr('켜면 휴지통으로 보내 되살릴 수 있습니다 (Shift+Delete 는 영구 삭제). 끄면 늘 영구 삭제로 묻습니다. ' '네트워크 드라이브 · USB 메모리는 휴지통이 없어 늘 영구 삭제입니다')), diff --git a/lib/ui/settings/general_sections.dart b/lib/ui/settings/general_sections.dart index 0af2da1..337dffe 100644 --- a/lib/ui/settings/general_sections.dart +++ b/lib/ui/settings/general_sections.dart @@ -36,14 +36,14 @@ class _GeneralSettingsSectionState extends State { const LanguageSettings(), // 102: 물어 정한 것 · 1차 판단은 모두 여기서 바꿀 수 있다 (기본값 = 그 답) SwitchListTile( - value: s.migrationNotice, - onChanged: (v) => c.updateSettings((x) => x.migrationNotice = v), + value: s.general.migrationNotice, + onChanged: (v) => c.updateSettings((x) => x.general.migrationNotice = v), title: Text(tr('업데이트로 바뀐 기본값 알림')), subtitle: Text(tr('새 버전이 예전 기본값을 바꿨으면 켤 때 한 번 무엇이 바뀌었는지 알려 줍니다')), ), SwitchListTile( - value: s.rememberPasswords, - onChanged: (v) => c.updateSettings((x) => x.rememberPasswords = v), + value: s.general.rememberPasswords, + onChanged: (v) => c.updateSettings((x) => x.general.rememberPasswords = v), title: Text(tr('비밀번호 기억 (안전 저장소)')), subtitle: Text(tr('WebDAV · OpenSubtitles 비밀번호를 이 기기의 안전 저장소에 둡니다. 끄면 저장하지 않고 (있던 것도 지움) 앱을 켤 때마다 다시 넣습니다')), ), diff --git a/lib/ui/settings/transfer_sections.dart b/lib/ui/settings/transfer_sections.dart index 6769543..c42e80e 100644 --- a/lib/ui/settings/transfer_sections.dart +++ b/lib/ui/settings/transfer_sections.dart @@ -35,8 +35,8 @@ class _RsyncSettingsSectionState extends State { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ CopySyncSettings(c: c, rsync: true), SwitchListTile( - value: s.allowInnerToOuter, - onChanged: (v) => c.updateSettings((x) => x.allowInnerToOuter = v), + value: s.general.allowInnerToOuter, + onChanged: (v) => c.updateSettings((x) => x.general.allowInnerToOuter = v), title: Text(tr('원본이 대상 안에 있어도 맞추기 (안쪽 → 바깥)')), subtitle: Text(tr('확인 창에서 알린 뒤 허용합니다. 지우기 (--delete) 가 있으면 늘 막습니다. 끄면 늘 막습니다')), ), diff --git a/lib/ui/vault/vault_actions.dart b/lib/ui/vault/vault_actions.dart index ac9eb45..802f5b1 100644 --- a/lib/ui/vault/vault_actions.dart +++ b/lib/ui/vault/vault_actions.dart @@ -33,7 +33,7 @@ const vaultImageMax = 256 * 1024 * 1024; /// 보기: 그림은 메모리에서 바로 (파일로 풀지 않음 - 관리자 지시 10/21), 동영상 · 음악은 [session] 폴더에 잠깐 풀고 본 뒤 지운다 Future vaultViewEntry(BuildContext context, AppController c, VaultUiState s, VaultViewSession session, VaultEntry e) async { final ext = p.extension(e.name).replaceFirst('.', '').toLowerCase(); - if (c.settings.imageExts.contains(ext) && e.size <= vaultImageMax) { + if (c.settings.reader.imageExts.contains(ext) && e.size <= vaultImageMax) { try { s.setBusy(true); final bytes = await s.vault!.readBytes(e, maxBytes: vaultImageMax); diff --git a/lib/ui/zip_page.dart b/lib/ui/zip_page.dart index 4c2b366..f7a9e9d 100644 --- a/lib/ui/zip_page.dart +++ b/lib/ui/zip_page.dart @@ -18,7 +18,7 @@ Future openZip(BuildContext context, AppController c, String path, {Str /// ZIP 안 그림을 만화처럼 보기 (그림이 없으면 false) Future openZipComic(BuildContext context, AppController c, String path) async { final temp = await c.services.storage.tempDirectory(); - final src = await ZipImagesSource.open(path, tempDir: temp, imageExts: c.settings.imageExts); + final src = await ZipImagesSource.open(path, tempDir: temp, imageExts: c.settings.reader.imageExts); if (src.length == 0) { await src.dispose(); return false; @@ -61,7 +61,7 @@ class _ZipPageState extends State { } } - bool get _hasImages => _entries?.any((f) => c.settings.imageExts.contains(extOf(f.name))) ?? false; + bool get _hasImages => _entries?.any((f) => c.settings.reader.imageExts.contains(extOf(f.name))) ?? false; /// 86: 풀기는 압축 풀기 흐름 (목록 검사 - 밖으로 나가는 경로 · 링크 · 크기 · 비밀번호, 같은 이름 · 진행 · 취소) 으로 Future _extract({required bool all}) async { @@ -114,7 +114,7 @@ class _ZipPageState extends State { value: on, onChanged: (v) => setState(() => v == true ? _marked.add(f.name) : _marked.remove(f.name)), secondary: Icon( - c.settings.imageExts.contains(extOf(f.name)) + c.settings.reader.imageExts.contains(extOf(f.name)) ? Icons.image_outlined : Icons.insert_drive_file_outlined, color: JjColors.textDim, diff --git a/test/decisions_test.dart b/test/decisions_test.dart index 38de083..3a5affd 100644 --- a/test/decisions_test.dart +++ b/test/decisions_test.dart @@ -27,10 +27,10 @@ void main() { test('기본값 = 정한 답 · 저장 · 읽기', () { final s = AppSettings(); expect([ - s.run.startMenuShortcut, s.copy.syncStopToast, s.display.swipeWrap, s.migrationNotice, s.allowInnerToOuter, - s.rememberPasswords, s.recycleOnDelete, s.reader.systemBars, + s.run.startMenuShortcut, s.copy.syncStopToast, s.display.swipeWrap, s.general.migrationNotice, s.general.allowInnerToOuter, + s.general.rememberPasswords, s.general.recycleOnDelete, s.reader.systemBars, ], everyElement(isTrue)); - expect(s.cleanupPrechecked, ['work', 'download']); + expect(s.general.cleanupPrechecked, ['work', 'download']); expect(s.mkv.tripleTapAction, 'move'); expect(s.remote.davExternalOpen, 'ask'); // 이미 있던 설정의 기본값도 결정대로 @@ -44,17 +44,17 @@ void main() { ..run.startMenuShortcut = false ..copy.syncStopToast = false ..display.swipeWrap = false - ..migrationNotice = false - ..allowInnerToOuter = false - ..rememberPasswords = false - ..recycleOnDelete = false - ..cleanupPrechecked = ['logs'] + ..general.migrationNotice = false + ..general.allowInnerToOuter = false + ..general.rememberPasswords = false + ..general.recycleOnDelete = false + ..general.cleanupPrechecked = ['logs'] ..mkv.tripleTapAction = 'none' ..remote.davExternalOpen = 'url') .toJson()); - expect([b.run.startMenuShortcut, b.copy.syncStopToast, b.display.swipeWrap, b.migrationNotice, b.allowInnerToOuter, b.rememberPasswords, - b.recycleOnDelete], everyElement(isFalse)); - expect([b.cleanupPrechecked, b.mkv.tripleTapAction, b.remote.davExternalOpen], [['logs'], 'none', 'url']); + expect([b.run.startMenuShortcut, b.copy.syncStopToast, b.display.swipeWrap, b.general.migrationNotice, b.general.allowInnerToOuter, b.general.rememberPasswords, + b.general.recycleOnDelete], everyElement(isFalse)); + expect([b.general.cleanupPrechecked, b.mkv.tripleTapAction, b.remote.davExternalOpen], [['logs'], 'none', 'url']); }); test('안쪽 → 바깥 허용을 끄면 늘 막는다', () { @@ -72,7 +72,7 @@ void main() { s.remote.webdavServers = [const DavServer(id: 'd', name: 'n', url: 'https://n', user: 'u', password: 'pw')]; await st.save(s); expect(secrets.values['dav.pw.d'], 'pw'); - s.rememberPasswords = false; + s.general.rememberPasswords = false; await st.save(s); expect(secrets.values, isEmpty); expect(File(p.join(dir.path, 'settings.json')).readAsStringSync(), isNot(contains('"pw"'))); @@ -108,8 +108,8 @@ void main() { } test('116: 예전 판의 "권한 안내 보임" 표시는 읽지 않는다 (권한이 없으면 이 판에서 한 번 더 안내)', () { - expect(AppSettings.fromJson({'allFilesHintShown': true}).allFilesHintShown, isFalse); - expect(AppSettings.fromJson((AppSettings()..allFilesHintShown = true).toJson()).allFilesHintShown, isTrue); + expect(AppSettings.fromJson({'allFilesHintShown': true}).general.allFilesHintShown, isFalse); + expect(AppSettings.fromJson((AppSettings()..general.allFilesHintShown = true).toJson()).general.allFilesHintShown, isTrue); }); testWidgets('126: "바뀐 기본 설정" 창을 닫으면 바로 저장 - 강제 종료 뒤 켜도 다시 뜨지 않음', (tester) async { diff --git a/test/explorer_filter_rsync_test.dart b/test/explorer_filter_rsync_test.dart index f192ad1..2861f21 100644 --- a/test/explorer_filter_rsync_test.dart +++ b/test/explorer_filter_rsync_test.dart @@ -116,8 +116,8 @@ void main() { test('73: 즐겨찾기 저장 · 읽기 (Rsync 짝 포함)', () { final s = AppSettings() - ..folderBookmarks = const [FolderBookmark(name: 'NAS/영화', path: r'\\nas\movies'), FolderBookmark(name: '백업', path: r'D:\a', right: r'E:\b')]; - final b = AppSettings.fromJson(s.toJson()).folderBookmarks; + ..general.folderBookmarks = const [FolderBookmark(name: 'NAS/영화', path: r'\\nas\movies'), FolderBookmark(name: '백업', path: r'D:\a', right: r'E:\b')]; + final b = AppSettings.fromJson(s.toJson()).general.folderBookmarks; expect([for (final x in b) (x.name, x.path, x.right, x.isPair)], [('NAS/영화', r'\\nas\movies', null, false), ('백업', r'D:\a', r'E:\b', true)]); }); diff --git a/test/explorer_panes_test.dart b/test/explorer_panes_test.dart index f6b68c7..6e9d10f 100644 --- a/test/explorer_panes_test.dart +++ b/test/explorer_panes_test.dart @@ -216,7 +216,7 @@ void main() { device.volumes = () => [(left, 'L:'), (right, 'R:')]; c.settings ..explorer.style = 'xplore' - ..folderBookmarks = [FolderBookmark(name: '내 즐겨찾기', path: p.join(left, 'sub'))]; + ..general.folderBookmarks = [FolderBookmark(name: '내 즐겨찾기', path: p.join(left, 'sub'))]; await open(tester); final otherRoot = find.byKey(ValueKey('k1:$right')); expect(otherRoot, findsWidgets, reason: '왼쪽 창 트리에 R: (다른 저장 장치) 줄'); diff --git a/test/explorer_select_ops_test.dart b/test/explorer_select_ops_test.dart index 00f6544..0d94e64 100644 --- a/test/explorer_select_ops_test.dart +++ b/test/explorer_select_ops_test.dart @@ -156,14 +156,14 @@ void main() { await tester.enterText(find.byType(TextField).last, '왼쪽 폴더'); await tester.tap(find.widgetWithText(FilledButton, '확인')); await calm(tester); - expect([for (final b in c.settings.folderBookmarks) (b.name, b.path)], [('왼쪽 폴더', left)]); + expect([for (final b in c.settings.general.folderBookmarks) (b.name, b.path)], [('왼쪽 폴더', left)]); expect(find.text('왼쪽 폴더'), findsOneWidget); await tester.tap(find.text('닫기')); await calm(tester); expect(find.byIcon(Icons.star), findsOneWidget, reason: '즐겨찾기한 폴더를 보는 창의 ★ 는 채움'); // 오른쪽 폴더를 즐겨찾기에 두고 왼쪽 창에서 한 번에 가기 - await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: 'R', path: right)]); + await c.updateSettings((x) => x.general.folderBookmarks = [...x.general.folderBookmarks, FolderBookmark(name: 'R', path: right)]); await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first)); await calm(tester); await act(tester, () => tester.tap(find.text('R'))); @@ -175,7 +175,7 @@ void main() { await calm(tester); await tester.tap(find.byTooltip('즐겨찾기에서 빼기 (폴더는 그대로)').first); await calm(tester); - expect([for (final b in c.settings.folderBookmarks) b.name], ['R']); + expect([for (final b in c.settings.general.folderBookmarks) b.name], ['R']); expect(Directory(left).existsSync(), isTrue); await tester.tap(find.text('닫기')); await calm(tester); @@ -194,12 +194,12 @@ void main() { await calm(tester); await tester.tap(find.widgetWithText(FilledButton, '확인')); await calm(tester); - final pair = c.settings.folderBookmarks.single; + final pair = c.settings.general.folderBookmarks.single; expect((pair.path, pair.right, pair.name), (left, right, 'left ⇄ right')); await tester.tap(find.text('닫기')); await calm(tester); // 반대 짝을 넣어 두고 열면 두 창이 함께 바뀐다 - await c.updateSettings((x) => x.folderBookmarks = [...x.folderBookmarks, FolderBookmark(name: '거꾸로', path: right, right: left)]); + await c.updateSettings((x) => x.general.folderBookmarks = [...x.general.folderBookmarks, FolderBookmark(name: '거꾸로', path: right, right: left)]); await act(tester, () => tester.tap(find.byTooltip('즐겨찾기').first)); await calm(tester); await act(tester, () => tester.tap(find.text('거꾸로'))); @@ -322,7 +322,7 @@ void main() { await calm(tester); await tester.tap(find.text('즐겨찾기에 추가')); await calm(tester); - expect(c.settings.folderBookmarks.map((x) => x.path), contains(p.join(left, 'sub'))); + expect(c.settings.general.folderBookmarks.map((x) => x.path), contains(p.join(left, 'sub'))); await tester.longPress(find.text('sub').first); await calm(tester); expect(find.text('즐겨찾기에 추가'), findsNothing); diff --git a/test/explorer_webdav_test.dart b/test/explorer_webdav_test.dart index 99fcf93..ab523be 100644 --- a/test/explorer_webdav_test.dart +++ b/test/explorer_webdav_test.dart @@ -166,7 +166,7 @@ void main() { c.settings ..remote.webdavServers = [s] ..explorer.paths = [left, left] - ..folderBookmarks = [const FolderBookmark(name: 'NAS 원격', path: 'dav://nas/원격')]; + ..general.folderBookmarks = [const FolderBookmark(name: 'NAS 원격', path: 'dav://nas/원격')]; DavRegistry.configure([s]); await pump(tester, ExplorerPage(device: device, c: c)); await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); diff --git a/test/reader_test.dart b/test/reader_test.dart index a8fed6f..877a862 100644 --- a/test/reader_test.dart +++ b/test/reader_test.dart @@ -57,16 +57,16 @@ void main() { test('보기 설정 저장 · 읽기', () { final s = AppSettings() - ..imageExts = ['png', 'webp'] + ..reader.imageExts = ['png', 'webp'] ..reader.fit = 'width' ..reader.rtl = true ..reader.brightness = 0.3 ..reader.zipComic = false; final b = AppSettings.fromJson(s.toJson()); - expect([b.imageExts, b.reader.fit, b.reader.rtl, b.reader.brightness, b.reader.zipComic], [ + expect([b.reader.imageExts, b.reader.fit, b.reader.rtl, b.reader.brightness, b.reader.zipComic], [ ['png', 'webp'], 'width', true, 0.3, false, ]); - expect(AppSettings.fromJson({}).imageExts, defaultImageExtensions); + expect(AppSettings.fromJson({}).reader.imageExts, defaultImageExtensions); }); testWidgets('224: 보는 동안 밖에서 지운 그림 - 앱으로 돌아오면 "파일이 없어졌습니다" 와 다음 그림, 다 없어지면 닫는다', (tester) async { diff --git a/test/restore_access_test.dart b/test/restore_access_test.dart index 046dd4a..9a32da8 100644 --- a/test/restore_access_test.dart +++ b/test/restore_access_test.dart @@ -170,11 +170,11 @@ void main() { test('안내는 겹쳐 띄우지 않되, 권한이 있다가 없어지면 다음에 켤 때 한 번 더', () async { final c = AppController(PlatformServices(mediaTool: _Tool(), storage: DesktopStorageService())); - expect(c.settings.allFilesHintShown, isFalse); + expect(c.settings.general.allFilesHintShown, isFalse); expect(await allFilesGuideDue(c, hasAccess: false), isTrue, reason: '처음 켬, 권한 없음'); expect(await allFilesGuideDue(c, hasAccess: false), isFalse, reason: '다시 켜도 권한이 계속 없으면 띄우지 않음'); expect(await allFilesGuideDue(c, hasAccess: true), isFalse, reason: '허용함'); - expect(c.settings.allFilesHintShown, isFalse); + expect(c.settings.general.allFilesHintShown, isFalse); expect(await allFilesGuideDue(c, hasAccess: true), isFalse); expect(await allFilesGuideDue(c, hasAccess: false), isTrue, reason: '사용자가 권한을 끈 뒤 켬 → 한 번 더'); expect(await allFilesGuideDue(c, hasAccess: false), isFalse); diff --git a/test/settings_sections_contract_test.dart b/test/settings_sections_contract_test.dart index ae88e10..4ed646c 100644 --- a/test/settings_sections_contract_test.dart +++ b/test/settings_sections_contract_test.dart @@ -50,11 +50,11 @@ void main() { testWidgets('일반: 스위치를 바꾸면 바로 설정에 (업데이트로 바뀐 기본값 알림)', (t) async { final c = app(); - final before = c.settings.migrationNotice; + final before = c.settings.general.migrationNotice; await show(t, GeneralSettingsSection(c: c, desk: true)); await t.tap(find.widgetWithText(SwitchListTile, '업데이트로 바뀐 기본값 알림')); await t.pump(); - expect(c.settings.migrationNotice, !before); + expect(c.settings.general.migrationNotice, !before); }); testWidgets('공용 줄: 개수 고르기 · 글 넣는 줄 저장 · 폴더 줄 [기본값] 은 고른 것이 있을 때만', (t) async { diff --git a/test/small_items_test.dart b/test/small_items_test.dart index cac5b7e..3e72c29 100644 --- a/test/small_items_test.dart +++ b/test/small_items_test.dart @@ -41,8 +41,8 @@ void main() { }); test('39: "모든 파일 접근" 시작 안내는 한 번만 (보였다는 표시가 설정에 남음)', () { - expect(AppSettings.fromJson({}).allFilesHintShown, isFalse); - expect(AppSettings.fromJson((AppSettings()..allFilesHintShown = true).toJson()).allFilesHintShown, isTrue); + expect(AppSettings.fromJson({}).general.allFilesHintShown, isFalse); + expect(AppSettings.fromJson((AppSettings()..general.allFilesHintShown = true).toJson()).general.allFilesHintShown, isTrue); }); test('103: 크기 때문에 저절로 H.264 로 바꾼 코덱은 원본 크기로 되돌리면 "원본 유지" 로 돌아온다 (직접 고른 코덱은 그대로)', () {