From 828d97881253b3b9a6989e817045da215a51ae9a Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:32:25 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=A4=EC=A0=95=20A=20=EB=B6=84=EC=95=BC=201?= =?UTF-8?q?1:=20=ED=83=90=EC=83=89=EA=B8=B0=20=EB=8F=84=EA=B5=AC=20(Explor?= =?UTF-8?q?erToolSettings)=20-=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=C2=B7=20=EB=A3=A8=ED=8A=B8=20=C2=B7=20Wi-Fi=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=C2=B7=20ZIP=20=EC=88=98=EC=A4=80,=20?= =?UTF-8?q?=ED=82=A4=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- lib/app/settings.dart | 33 ++++----------- lib/app/settings/explorer_tool_settings.dart | 44 ++++++++++++++++++++ lib/ui/archive_ui.dart | 4 +- lib/ui/explorer/explorer_controller.dart | 4 +- lib/ui/settings/files_section.dart | 8 ++-- lib/ui/wifi_share_page.dart | 14 +++---- test/archive_ui_test.dart | 2 +- test/net_security_test.dart | 6 +-- test/root_test.dart | 6 +-- test/shizuku_test.dart | 8 ++-- test/wifi_share_page_test.dart | 4 +- 11 files changed, 79 insertions(+), 54 deletions(-) create mode 100644 lib/app/settings/explorer_tool_settings.dart diff --git a/lib/app/settings.dart b/lib/app/settings.dart index f6b3059..85d6094 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -15,6 +15,7 @@ import '../services/app_shell.dart' show appIconOf; import '../core/remote_idle.dart'; import 'settings/records.dart'; import 'settings/settings_group.dart'; +import 'settings/explorer_tool_settings.dart'; import 'settings/vault_settings.dart'; import 'settings/run_settings.dart'; import 'settings/browser_settings.dart'; @@ -29,6 +30,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/explorer_tool_settings.dart'; export 'settings/vault_settings.dart'; export 'settings/run_settings.dart'; export 'settings/browser_settings.dart'; @@ -159,11 +161,6 @@ class AppSettings { - /// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용, 사용자 결정 10/21 · 관리자 결정: 처음 끔 · 켤 때마다 경고) - bool shizukuEnabled = false; - - /// X-plore 비교 5-2 루트 폴더 보기 (su, 읽기 전용, 관리자 결정 10/21: Shizuku 와 같은 틀 - 처음 끔 · 켤 때 · 처음 열 때 경고) - bool rootEnabled = false; @@ -171,17 +168,10 @@ class AppSettings { - /// 89: Wi-Fi 공유 - 저절로 끄는 시간 (분, 0 = 끄지 않음). 처음 값 30분 (관리자 지시 10/17) - int wifiShareMinutes = 30; - /// 89 · 보안 검토 3: Wi-Fi 공유를 Tailscale (100.64/10, 내 tailnet 의 기기) 주소에서도 연다 (관리자 결정 10/18: 처음 켬) - bool wifiShareTailscale = true; - /// 89: Wi-Fi 공유로 마지막에 고른 폴더 (다시 켤 때 처음 고른 것) - String wifiShareFolder = ''; - /// 86: ZIP 으로 묶기의 압축 정도 (1 빠르게 · 6 보통 (처음 값) · 9 작게) - 마지막에 고른 것을 기억 - int zipLevel = 6; + @@ -359,8 +349,11 @@ class AppSettings { /// 암호 보관함 final vault = VaultSettings(); + /// 탐색기 도구 - 시스템 폴더 (Shizuku) · 루트 · Wi-Fi 공유 · ZIP 묶기 + final tools = ExplorerToolSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault]; + List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser, run, vault, tools]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -398,12 +391,6 @@ class AppSettings { 'appIcon': appIcon, 'uiLanguagesAdded': uiLanguagesAdded, 'openFileWindow': openFileWindow, - 'zipLevel': zipLevel, - 'wifiShareMinutes': wifiShareMinutes, - 'wifiShareTailscale': wifiShareTailscale, - 'wifiShareFolder': wifiShareFolder, - 'shizukuEnabled': shizukuEnabled, - 'rootEnabled': rootEnabled, 'mkvExisting': mkvExisting, 'components': components, 'aiImageV1': true, @@ -492,12 +479,6 @@ class AppSettings { ? j['screenOrientation'] as String : 'auto' ..openFileWindow = j['openFileWindow'] == 'new' ? 'new' : 'same' - ..shizukuEnabled = j['shizukuEnabled'] == true - ..rootEnabled = j['rootEnabled'] == 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 : '' ..mkvExisting = const ['rename', 'overwrite', 'skip'].contains(j['mkvExisting']) ? j['mkvExisting'] as String : 'ask' ..components = j['components'] is List ? [ diff --git a/lib/app/settings/explorer_tool_settings.dart b/lib/app/settings/explorer_tool_settings.dart new file mode 100644 index 0000000..4d1498e --- /dev/null +++ b/lib/app/settings/explorer_tool_settings.dart @@ -0,0 +1,44 @@ + +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 탐색기 도구 - 시스템 폴더 (Shizuku) · 루트 · Wi-Fi 공유 · ZIP 묶기 - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class ExplorerToolSettings implements SettingsGroup { + /// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용, 사용자 결정 10/21 · 관리자 결정: 처음 끔 · 켤 때마다 경고) + bool shizukuEnabled = false; + + /// X-plore 비교 5-2 루트 폴더 보기 (su, 읽기 전용, 관리자 결정 10/21: Shizuku 와 같은 틀 - 처음 끔 · 켤 때 · 처음 열 때 경고) + bool rootEnabled = false; + + /// 89: Wi-Fi 공유 - 저절로 끄는 시간 (분, 0 = 끄지 않음). 처음 값 30분 (관리자 지시 10/17) + int wifiShareMinutes = 30; + + /// 89 · 보안 검토 3: Wi-Fi 공유를 Tailscale (100.64/10, 내 tailnet 의 기기) 주소에서도 연다 (관리자 결정 10/18: 처음 켬) + bool wifiShareTailscale = true; + + /// 89: Wi-Fi 공유로 마지막에 고른 폴더 (다시 켤 때 처음 고른 것) + String wifiShareFolder = ''; + + /// 86: ZIP 으로 묶기의 압축 정도 (1 빠르게 · 6 보통 (처음 값) · 9 작게) - 마지막에 고른 것을 기억 + int zipLevel = 6; + + @override + void write(Map j, {required bool plainSecrets}) { + j['shizukuEnabled'] = shizukuEnabled; + j['rootEnabled'] = rootEnabled; + j['wifiShareMinutes'] = wifiShareMinutes; + j['wifiShareTailscale'] = wifiShareTailscale; + j['wifiShareFolder'] = wifiShareFolder; + j['zipLevel'] = zipLevel; + } + + @override + void read(Map j) { + shizukuEnabled = j['shizukuEnabled'] == true; + rootEnabled = j['rootEnabled'] == 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 : ''; + } +} diff --git a/lib/ui/archive_ui.dart b/lib/ui/archive_ui.dart index c7e2e07..940faf4 100644 --- a/lib/ui/archive_ui.dart +++ b/lib/ui/archive_ui.dart @@ -314,7 +314,7 @@ Future createZipFlow(BuildContext context, AppController c, List createZipFlow(BuildContext context, AppController c, List x.zipLevel = level); + if (level != c.settings.tools.zipLevel) await c.updateSettings((x) => x.tools.zipLevel = level); if (!context.mounted) return null; // zip_aes: 비밀번호 ZIP 은 동봉 7-Zip 으로 (흘려 가며 AES-256 - 큰 동영상도 메모리에 다 올리지 않게) final exe = password.isEmpty ? null : await bundledSevenZip(); diff --git a/lib/ui/explorer/explorer_controller.dart b/lib/ui/explorer/explorer_controller.dart index ed419dd..e2b79e2 100644 --- a/lib/ui/explorer/explorer_controller.dart +++ b/lib/ui/explorer/explorer_controller.dart @@ -88,8 +88,8 @@ class ExplorerController extends ChangeNotifier { for (final s in c.settings.smbServers) ('$smbScheme${s.id}/', s.label), // l: 평문 FTP 는 이름 앞에 ⚠ for (final s in c.settings.ftpServers) ('$ftpScheme${s.id}/', s.tls ? s.label : '⚠ ${s.label}'), - if (c.settings.shizukuEnabled && shizukuSupported()) ('$shizukuScheme$shizukuServer/', tr('시스템 (읽기 전용)')), - if (c.settings.rootEnabled && rootSupported()) (rootHome, tr('루트 (읽기 전용)')), + if (c.settings.tools.shizukuEnabled && shizukuSupported()) ('$shizukuScheme$shizukuServer/', tr('시스템 (읽기 전용)')), + if (c.settings.tools.rootEnabled && rootSupported()) (rootHome, tr('루트 (읽기 전용)')), ]; String volumeOf(String path) => vol.volumeOf(path, local); diff --git a/lib/ui/settings/files_section.dart b/lib/ui/settings/files_section.dart index 5b586af..539505b 100644 --- a/lib/ui/settings/files_section.dart +++ b/lib/ui/settings/files_section.dart @@ -94,7 +94,7 @@ class _FilesSettingsSectionState extends State { title: Text(tr('시스템 폴더 보기 (Shizuku · 읽기 전용)')), subtitle: Text(tr('Shizuku 앱이 있으면 Android/data · obb 처럼 막힌 폴더를 읽기만 합니다. 탐색기 위 탭 [시스템 (읽기 전용)]')), trailing: Switch( - value: s.shizukuEnabled, + value: s.tools.shizukuEnabled, onChanged: (v) async { if (v && !await confirmReadOnlyView(context, @@ -104,7 +104,7 @@ class _FilesSettingsSectionState extends State { '대화 · 로그인 정보 같은 개인 데이터가 있을 수 있습니다.'))) { return; } - await c.updateSettings((x) => x.shizukuEnabled = v); + await c.updateSettings((x) => x.tools.shizukuEnabled = v); }, ), ), @@ -114,7 +114,7 @@ class _FilesSettingsSectionState extends State { title: Text(tr('루트 폴더 보기 (su · 읽기 전용)')), subtitle: Text(tr('루팅한 기기에서 su 로 시스템 · 다른 앱의 폴더를 읽기만 합니다. 탐색기 위 탭 [루트 (읽기 전용)]')), trailing: Switch( - value: s.rootEnabled, + value: s.tools.rootEnabled, onChanged: (v) async { if (v && !await confirmReadOnlyView(context, @@ -124,7 +124,7 @@ class _FilesSettingsSectionState extends State { '(대화 · 로그인 정보 · 비밀번호 등) 도 보일 수 있습니다.'))) { return; } - await c.updateSettings((x) => x.rootEnabled = v); + await c.updateSettings((x) => x.tools.rootEnabled = v); }, ), ), diff --git a/lib/ui/wifi_share_page.dart b/lib/ui/wifi_share_page.dart index 249b498..e674853 100644 --- a/lib/ui/wifi_share_page.dart +++ b/lib/ui/wifi_share_page.dart @@ -74,10 +74,10 @@ class _WifiSharePageState extends State { _lastReason = null; }); try { - final mins = widget.c.settings.wifiShareMinutes; + final mins = widget.c.settings.tools.wifiShareMinutes; final s = await WifiShare.start(widget.folder, - autoOff: mins > 0 ? Duration(minutes: mins) : null, tailscale: widget.c.settings.wifiShareTailscale); - await widget.c.updateSettings((x) => x.wifiShareFolder = widget.folder); + autoOff: mins > 0 ? Duration(minutes: mins) : null, tailscale: widget.c.settings.tools.wifiShareTailscale); + await widget.c.updateSettings((x) => x.tools.wifiShareFolder = widget.folder); _s?.removeListener(_changed); _s = s..addListener(_changed); // 작업 기록에는 켜고 끈 것만 (코드 · 주소 · 접속한 곳은 남기지 않는다) @@ -109,7 +109,7 @@ class _WifiSharePageState extends State { } List _offView() { - final mins = widget.c.settings.wifiShareMinutes; + final mins = widget.c.settings.tools.wifiShareMinutes; return [ Text(tr('공유할 폴더'), style: TextStyle(color: JjColors.textDim, fontSize: 12)), const SizedBox(height: 4), @@ -125,7 +125,7 @@ class _WifiSharePageState extends State { ], onChanged: (v) async { if (v == null) return; - await widget.c.updateSettings((x) => x.wifiShareMinutes = v); + await widget.c.updateSettings((x) => x.tools.wifiShareMinutes = v); if (mounted) setState(() {}); }, ), @@ -135,9 +135,9 @@ class _WifiSharePageState extends State { contentPadding: EdgeInsets.zero, title: Text(tr('Tailscale 에서도 열기')), subtitle: Text(tr('내 Tailscale 망 (tailnet) 의 기기도 이 주소로 볼 수 있습니다. 끄면 같은 Wi-Fi · 유선 망에서만')), - value: widget.c.settings.wifiShareTailscale, + value: widget.c.settings.tools.wifiShareTailscale, onChanged: (v) async { - await widget.c.updateSettings((x) => x.wifiShareTailscale = v); + await widget.c.updateSettings((x) => x.tools.wifiShareTailscale = v); if (mounted) setState(() {}); }, ), diff --git a/test/archive_ui_test.dart b/test/archive_ui_test.dart index 0d43b29..0fc4bd8 100644 --- a/test/archive_ui_test.dart +++ b/test/archive_ui_test.dart @@ -121,7 +121,7 @@ void main() { await tester.tap(find.widgetWithText(FilledButton, '묶기')); await wait(tester, () => finished); expect(made, p.join(tmp.path, 'photos.zip')); - expect(c.settings.zipLevel, 9, reason: '고른 압축 정도를 기억'); + expect(c.settings.tools.zipLevel, 9, reason: '고른 압축 정도를 기억'); expect(File(made!).lengthSync() > 0, isTrue); }); diff --git a/test/net_security_test.dart b/test/net_security_test.dart index daf62c0..b054df7 100644 --- a/test/net_security_test.dart +++ b/test/net_security_test.dart @@ -55,9 +55,9 @@ void main() { // 설정 "Tailscale 에서도 열기" 를 끄면 Tailscale 주소는 열지 않는다 (처음 값은 켬) expect(WifiShare.shareInterface('Tailscale', '100.69.29.106', tailscale: false), isFalse); expect(WifiShare.shareInterface('Wi-Fi', '192.168.0.5', tailscale: false), isTrue); - expect(AppSettings().wifiShareTailscale, isTrue); - expect(AppSettings.fromJson(const {'wifiShareTailscale': false}).wifiShareTailscale, isFalse); - expect(AppSettings.fromJson(const {'wifiShareTailscale': 'x'}).wifiShareTailscale, isTrue, reason: '틀린 값은 처음 값'); + expect(AppSettings().tools.wifiShareTailscale, isTrue); + expect(AppSettings.fromJson(const {'wifiShareTailscale': false}).tools.wifiShareTailscale, isFalse); + expect(AppSettings.fromJson(const {'wifiShareTailscale': 'x'}).tools.wifiShareTailscale, isTrue, reason: '틀린 값은 처음 값'); }); test('11: mDNS 로 찾은 주소는 사설망만', () { diff --git a/test/root_test.dart b/test/root_test.dart index 8442fcf..bbe069e 100644 --- a/test/root_test.dart +++ b/test/root_test.dart @@ -129,7 +129,7 @@ void main() { AppController ctl({bool on = true}) { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..rootEnabled = on + ..tools.rootEnabled = on ..explorer.paths = [tmp.path, tmp.path] ..explorer.layout = 'single'; return c; @@ -221,9 +221,9 @@ void main() { await t.pump(); await t.tap(find.widgetWithText(FilledButton, '켜기')); await settle(); - expect(c.settings.rootEnabled, isTrue); + expect(c.settings.tools.rootEnabled, isTrue); await t.tap(sw.first); await settle(); - expect(c.settings.rootEnabled, isFalse); + expect(c.settings.tools.rootEnabled, isFalse); }); } diff --git a/test/shizuku_test.dart b/test/shizuku_test.dart index e768470..672f2a4 100644 --- a/test/shizuku_test.dart +++ b/test/shizuku_test.dart @@ -112,7 +112,7 @@ void main() { AppController ctl({bool on = true}) { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..shizukuEnabled = on + ..tools.shizukuEnabled = on ..explorer.paths = [tmp.path, tmp.path] ..explorer.layout = 'single'; return c; @@ -231,16 +231,16 @@ void main() { expect(t.widget(find.widgetWithText(FilledButton, '켜기')).onPressed, isNull, reason: '체크해야 켜짐'); await t.tap(find.text('취소')); await settle(); - expect(c.settings.shizukuEnabled, isFalse); + expect(c.settings.tools.shizukuEnabled, isFalse); await t.tap(sw.first); await settle(); await t.tap(find.text('위 내용을 알고 켭니다')); await t.pump(); await t.tap(find.widgetWithText(FilledButton, '켜기')); await settle(); - expect(c.settings.shizukuEnabled, isTrue); + expect(c.settings.tools.shizukuEnabled, isTrue); await t.tap(sw.first); await settle(); - expect(c.settings.shizukuEnabled, isFalse, reason: '끄기는 묻지 않음'); + expect(c.settings.tools.shizukuEnabled, isFalse, reason: '끄기는 묻지 않음'); }); } diff --git a/test/wifi_share_page_test.dart b/test/wifi_share_page_test.dart index faeb993..e151a77 100644 --- a/test/wifi_share_page_test.dart +++ b/test/wifi_share_page_test.dart @@ -29,7 +29,7 @@ void main() { testWidgets('꺼짐 → [켜기] → 코드 · 남은 시간 → [끄기] → 꺼짐', (tester) async { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); - expect(c.settings.wifiShareMinutes, 30, reason: '처음 값 30분 (관리자 지시)'); + expect(c.settings.tools.wifiShareMinutes, 30, reason: '처음 값 30분 (관리자 지시)'); // 폰 세로처럼 좁고 긴 화면 (아래 줄까지 다 그려지게) tester.view.physicalSize = const Size(412, 1600); tester.view.devicePixelRatio = 1; @@ -49,7 +49,7 @@ void main() { expect(find.text(s.code), findsOneWidget); expect(find.textContaining('분 뒤에 저절로 꺼집니다'), findsOneWidget); expect(find.text('아직 접속한 기기가 없습니다'), findsOneWidget); - expect(c.settings.wifiShareFolder, tmp.path); + expect(c.settings.tools.wifiShareFolder, tmp.path); await tester.tap(find.text('끄기')); await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 100)));