diff --git a/integration_test/tor_dns_real_test.dart b/integration_test/tor_dns_real_test.dart index dbf5b3e..59ac2c4 100644 --- a/integration_test/tor_dns_real_test.dart +++ b/integration_test/tor_dns_real_test.dart @@ -127,7 +127,7 @@ void main() { expect(TorBundle.installedVersion(), TorBundle.version); c.settings ..components = [...c.settings.components, 'tor'] - ..torNoticeSeen = true; + ..tor.noticeSeen = true; await tester.pumpWidget(MaterialApp( builder: (context, child) => AppScope(controller: c, bookmarks: bm, child: child!), home: TorPage(c: c), diff --git a/integration_test/windows_misc_shot_test.dart b/integration_test/windows_misc_shot_test.dart index 7d9b4b2..fbffba8 100644 --- a/integration_test/windows_misc_shot_test.dart +++ b/integration_test/windows_misc_shot_test.dart @@ -60,7 +60,7 @@ void main() { Directory(out).createSync(recursive: true); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..torNoticeSeen = true + ..tor.noticeSeen = true ..components = [...c.settings.components, 'tor']; final key = GlobalKey(); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 227b8cf..678f2c7 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -16,12 +16,14 @@ import '../services/app_shell.dart' show appIconOf; import '../core/remote_idle.dart'; import 'settings/records.dart'; import 'settings/settings_group.dart'; +import 'settings/tor_settings.dart'; import 'settings/network_settings.dart'; import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/tor_settings.dart'; export 'settings/network_settings.dart'; export 'settings/explorer_settings.dart'; @@ -161,21 +163,10 @@ class AppSettings { // ── Tor 브라우저 (사용자 요청 10/10) ── - /// Tor 화면의 쿠키 · 기록 · 캐시를 닫은 뒤에도 남기기 (기본: 닫으면 지움) - bool torKeepData = false; - /// Tor 화면을 떠난 뒤 tor 를 몇 분 더 켜 둘지 (0 = 바로 끔, 처음 2분 - 관리자 결정 10/11). 웹 뷰 · 다리 · 프록시는 늘 바로 정리 - int torIdleMinutes = 2; - /// Tor 화면의 처음 · 🏠 주소 - String torHomeUrl = defaultTorHome; - static const defaultTorHome = 'https://check.torproject.org/'; - /// Tor 화면의 JavaScript (끄면 더 안전하지만 많은 사이트가 보이지 않음) - bool torJavaScript = true; - /// "공식 Tor Browser 가 아닙니다" 안내를 보았는지 (처음 열 때 한 번) - bool torNoticeSeen = false; // ── 파일 탐색기 (X-plore 참고) ── @@ -505,8 +496,11 @@ class AppSettings { /// 네트워크 · DNS (사용자 요청 10/10) final network = NetworkSettings(); + /// Tor 釉뚮씪?곗? (?ъ슜???붿껌 10/10) + final tor = TorBrowserSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network]; + List get groups => [explorer, network, tor]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -568,11 +562,6 @@ class AppSettings { 'externalBrowser': externalBrowser, 'webTranslate': webTranslate, 'webJavaScript': webJavaScript, - 'torKeepData': torKeepData, - 'torIdleMinutes': torIdleMinutes, - 'torHomeUrl': torHomeUrl, - 'torJavaScript': torJavaScript, - 'torNoticeSeen': torNoticeSeen, 'zipLevel': zipLevel, 'wifiShareMinutes': wifiShareMinutes, 'wifiShareTailscale': wifiShareTailscale, @@ -764,15 +753,6 @@ class AppSettings { ..externalBrowser = j['externalBrowser'] as String? ?? 'system' ..webTranslate = j['webTranslate'] as bool? ?? false ..webJavaScript = j['webJavaScript'] as bool? ?? true - ..torKeepData = j['torKeepData'] == true - ..torIdleMinutes = j['torIdleMinutes'] is int && (j['torIdleMinutes'] as int) >= 0 && (j['torIdleMinutes'] as int) <= 60 - ? j['torIdleMinutes'] as int - : 2 - ..torHomeUrl = j['torHomeUrl'] is String && (j['torHomeUrl'] as String).trim().isNotEmpty - ? (j['torHomeUrl'] as String).trim() - : defaultTorHome - ..torJavaScript = j['torJavaScript'] != false - ..torNoticeSeen = j['torNoticeSeen'] == true ..vaultEnabled = j['vaultEnabled'] == true ..shizukuEnabled = j['shizukuEnabled'] == true ..rootEnabled = j['rootEnabled'] == true diff --git a/lib/app/settings/tor_settings.dart b/lib/app/settings/tor_settings.dart new file mode 100644 index 0000000..3040cbe --- /dev/null +++ b/lib/app/settings/tor_settings.dart @@ -0,0 +1,44 @@ + +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > Tor 釉뚮씪?곗? (?ъ슜???붿껌 10/10) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class TorBrowserSettings implements SettingsGroup { + /// Tor 화면의 쿠키 · 기록 · 캐시를 닫은 뒤에도 남기기 (기본: 닫으면 지움) + bool keepData = false; + + /// Tor 화면을 떠난 뒤 tor 를 몇 분 더 켜 둘지 (0 = 바로 끔, 처음 2분 - 관리자 결정 10/11). 웹 뷰 · 다리 · 프록시는 늘 바로 정리 + int idleMinutes = 2; + + /// Tor 화면의 처음 · 🏠 주소 + String homeUrl = defaultHome; + static const defaultHome = 'https://check.torproject.org/'; + + /// Tor 화면의 JavaScript (끄면 더 안전하지만 많은 사이트가 보이지 않음) + bool javaScript = true; + + /// "공식 Tor Browser 가 아닙니다" 안내를 보았는지 (처음 열 때 한 번) + bool noticeSeen = false; + + @override + void write(Map j, {required bool plainSecrets}) { + j['torKeepData'] = keepData; + j['torIdleMinutes'] = idleMinutes; + j['torHomeUrl'] = homeUrl; + j['torJavaScript'] = javaScript; + j['torNoticeSeen'] = noticeSeen; + } + + @override + void read(Map j) { + keepData = j['torKeepData'] == true; + idleMinutes = j['torIdleMinutes'] is int && (j['torIdleMinutes'] as int) >= 0 && (j['torIdleMinutes'] as int) <= 60 + ? j['torIdleMinutes'] as int + : 2; + homeUrl = j['torHomeUrl'] is String && (j['torHomeUrl'] as String).trim().isNotEmpty + ? (j['torHomeUrl'] as String).trim() + : defaultHome; + javaScript = j['torJavaScript'] != false; + noticeSeen = j['torNoticeSeen'] == true; + } +} diff --git a/lib/ui/network_settings.dart b/lib/ui/network_settings.dart index d431c96..436c010 100644 --- a/lib/ui/network_settings.dart +++ b/lib/ui/network_settings.dart @@ -275,8 +275,8 @@ class TorSettings extends StatelessWidget { ), ), SwitchListTile( - value: s.torKeepData, - onChanged: (v) => c.updateSettings((x) => x.torKeepData = v), + value: s.tor.keepData, + onChanged: (v) => c.updateSettings((x) => x.tor.keepData = v), title: Text(tr('Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기')), subtitle: Text(tr('끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.')), ), @@ -284,17 +284,17 @@ class TorSettings extends StatelessWidget { title: Text(tr('Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간')), subtitle: Text(tr('다른 화면으로 가거나 앱이 뒤로 가면 Tor 화면의 페이지 · 연결은 바로 정리합니다. Tor 자체는 이 시간 동안 켜 두어 다시 들어오면 바로 쓸 수 있습니다.')), trailing: DropdownButton( - value: const [0, 1, 2, 5, 10].contains(s.torIdleMinutes) ? s.torIdleMinutes : 2, + value: const [0, 1, 2, 5, 10].contains(s.tor.idleMinutes) ? s.tor.idleMinutes : 2, items: [ DropdownMenuItem(value: 0, child: Text(tr('바로 끔'))), for (final m in const [1, 2, 5, 10]) DropdownMenuItem(value: m, child: Text(trf('{0}분', [m]))), ], - onChanged: (v) => c.updateSettings((x) => x.torIdleMinutes = v ?? 2), + onChanged: (v) => c.updateSettings((x) => x.tor.idleMinutes = v ?? 2), ), ), SwitchListTile( - value: s.torJavaScript, - onChanged: (v) => c.updateSettings((x) => x.torJavaScript = v), + value: s.tor.javaScript, + onChanged: (v) => c.updateSettings((x) => x.tor.javaScript = v), title: Text(tr('Tor 화면의 JavaScript 사용')), subtitle: Text(tr('끄면 더 안전하지만 많은 사이트가 보이지 않습니다 (다음에 열 때부터).')), ), @@ -304,9 +304,9 @@ class TorSettings extends StatelessWidget { trailing: SizedBox( width: 300, child: TextFormField( - initialValue: s.torHomeUrl, + initialValue: s.tor.homeUrl, decoration: const InputDecoration(isDense: true, border: OutlineInputBorder()), - onChanged: (v) => c.updateSettings((x) => x.torHomeUrl = v.trim().isEmpty ? AppSettings.defaultTorHome : v.trim()), + onChanged: (v) => c.updateSettings((x) => x.tor.homeUrl = v.trim().isEmpty ? TorBrowserSettings.defaultHome : v.trim()), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), ), ), diff --git a/lib/ui/tor_page.dart b/lib/ui/tor_page.dart index 7c522b9..6b151e7 100644 --- a/lib/ui/tor_page.dart +++ b/lib/ui/tor_page.dart @@ -196,7 +196,7 @@ Future showTorNotice(BuildContext context) => showDialog( class _TorPageState extends State with WebProxyVisibility { final _tor = TorService.instance; - late String _url = widget.c.settings.torHomeUrl; + late String _url = widget.c.settings.tor.homeUrl; late final _address = TextEditingController(text: _url); final _addressFocus = FocusNode(); TorNav? _nav; @@ -307,7 +307,7 @@ class _TorPageState extends State with WebProxyVisibility { void _enter() { _tor.cancelIdleStop(); if (!_away) return; - final home = widget.c.settings.torHomeUrl; + final home = widget.c.settings.tor.homeUrl; setState(() { _away = false; _url = home; @@ -338,7 +338,7 @@ class _TorPageState extends State with WebProxyVisibility { await (widget.onLeft ?? () async {})(); return; } - await _cleanup(widget.c.settings.torKeepData, widget.c.settings.torIdleMinutes); + await _cleanup(widget.c.settings.tor.keepData, widget.c.settings.tor.idleMinutes); } /// 떠난 뒤 정리 (화면을 닫았을 때도 같다) @@ -368,9 +368,9 @@ class _TorPageState extends State with WebProxyVisibility { if (_begun) return; _begun = true; final c = widget.c; - if (!c.settings.torNoticeSeen && mounted) { + if (!c.settings.tor.noticeSeen && mounted) { await showTorNotice(context); - await c.updateSettings((x) => x.torNoticeSeen = true); + await c.updateSettings((x) => x.tor.noticeSeen = true); } final missing = await (widget.missingFeatures ?? WebProxy.torMissingFeatures)(); if (!mounted) return; @@ -443,7 +443,7 @@ class _TorPageState extends State with WebProxyVisibility { _shown = false; TorPage._visible--; if (widget.viewBuilder == null && widget.startTor == null) { - final keep = widget.c.settings.torKeepData, idle = widget.c.settings.torIdleMinutes; + final keep = widget.c.settings.tor.keepData, idle = widget.c.settings.tor.idleMinutes; // 남은 Tor 웹 뷰가 있는 동안은 WebProxy 가 Tor 쪽 · 닫힌 포트만 건다 ([WebProxy.torViewCreated]) - 기다리지 않고 정리 unawaited(_cleanup(keep, idle)); } else { @@ -507,7 +507,7 @@ class _TorPageState extends State with WebProxyVisibility { _progress < 1 ? btn(Icons.close, tr('중지'), ready ? () => _nav?.stop() : null) : btn(Icons.refresh, keyHint(tr('새로고침'), 'F5'), ready ? () => _nav?.reload() : null), - if (!compact) btn(Icons.home_outlined, tr('홈'), ready ? () => _go(widget.c.settings.torHomeUrl) : null), + if (!compact) btn(Icons.home_outlined, tr('홈'), ready ? () => _go(widget.c.settings.tor.homeUrl) : null), const SizedBox(width: 6), Expanded( // 일반 브라우저의 기록 · 키보드 학습을 쓰지 않는다 (제안 없음) @@ -550,7 +550,7 @@ class _TorPageState extends State with WebProxyVisibility { tooltip: tr('더 보기'), icon: const Icon(Icons.more_vert, size: 20), onSelected: (v) => switch (v) { - 'home' => _go(widget.c.settings.torHomeUrl), + 'home' => _go(widget.c.settings.tor.homeUrl), 'notice' => showTorNotice(context), _ => Navigator.push(context, MaterialPageRoute(builder: (_) => SettingsPage(c: widget.c, initialGroup: 'tor'))), }, @@ -621,7 +621,7 @@ class _TorPageState extends State with WebProxyVisibility { } final b = widget.viewBuilder; if (b != null) return b(_host, _url); - return _TorWebView(host: _host, url: _url, env: _env, javaScript: widget.c.settings.torJavaScript, keepData: widget.c.settings.torKeepData); + return _TorWebView(host: _host, url: _url, env: _env, javaScript: widget.c.settings.tor.javaScript, keepData: widget.c.settings.tor.keepData); } Widget _message(IconData icon, String title, String body, {bool busy = false}) => Center( diff --git a/test/dns_proxy_test.dart b/test/dns_proxy_test.dart index 4182619..d48b074 100644 --- a/test/dns_proxy_test.dart +++ b/test/dns_proxy_test.dart @@ -474,7 +474,7 @@ void main() { final back = AppSettings.fromJson(mine.toJson()); expect(back.network.config.key, mine.network.config.key); // Tor 설정 처음 값 - expect((s.torKeepData, s.torHomeUrl, s.torJavaScript, s.torNoticeSeen), (false, 'https://check.torproject.org/', true, false)); + expect((s.tor.keepData, s.tor.homeUrl, s.tor.javaScript, s.tor.noticeSeen), (false, 'https://check.torproject.org/', true, false)); }); test('너무 긴 이름은 이름 탓 (닿지 않음 아님) - DNS 안내 · 작업 기록에 사이트 이름이 남지 않는다 (15 지적)', () async { diff --git a/test/tor_test.dart b/test/tor_test.dart index 955f0f2..38168c8 100644 --- a/test/tor_test.dart +++ b/test/tor_test.dart @@ -164,7 +164,7 @@ void main() { addTearDown(() => t.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null)); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..torNoticeSeen = true + ..tor.noticeSeen = true ..components = [...c.settings.components, 'tor']; await t.pumpWidget(MaterialApp( navigatorKey: navKey, @@ -270,7 +270,7 @@ void main() { nav.currentState!.pop(); await t.pumpAndSettle(); expect(applied, 2); - expect(loads.last, c.settings.torHomeUrl); + expect(loads.last, c.settings.tor.homeUrl); expect(find.textContaining('VIEW'), findsOneWidget); // 앱이 뒤로 가도 같다 t.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); @@ -284,7 +284,7 @@ void main() { await t.pump(); await t.pump(); expect(applied, 3, reason: '돌아오면 프록시를 다시 건 뒤에'); - expect(loads.last, c.settings.torHomeUrl); + expect(loads.last, c.settings.tor.homeUrl); expect(find.textContaining('VIEW'), findsOneWidget); }); @@ -315,11 +315,11 @@ void main() { }); test('설정: Tor 를 켜 둘 시간 (처음 2분, 틀린 값은 2분)', () { - expect(AppSettings().torIdleMinutes, 2); - expect(AppSettings.fromJson({'torIdleMinutes': 0}).torIdleMinutes, 0); - expect(AppSettings.fromJson({'torIdleMinutes': 5}).torIdleMinutes, 5); - expect(AppSettings.fromJson({'torIdleMinutes': -1}).torIdleMinutes, 2); - expect(AppSettings.fromJson({'torIdleMinutes': 'x'}).torIdleMinutes, 2); + expect(AppSettings().tor.idleMinutes, 2); + expect(AppSettings.fromJson({'torIdleMinutes': 0}).tor.idleMinutes, 0); + expect(AppSettings.fromJson({'torIdleMinutes': 5}).tor.idleMinutes, 5); + expect(AppSettings.fromJson({'torIdleMinutes': -1}).tor.idleMinutes, 2); + expect(AppSettings.fromJson({'torIdleMinutes': 'x'}).tor.idleMinutes, 2); }); testWidgets('227: 막은 파일 주소는 주소 칸에 남지 않고 보이던 페이지 주소로', (t) async {