설정 A 분야 3: Tor 브라우저 (TorBrowserSettings) - 키 5 · defaultTorHome → TorBrowserSettings.defaultHome

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
RyuJongJin 2026-10-22 22:43:02 +09:00
parent 1db0bda829
commit ee7524e1c7
8 changed files with 78 additions and 54 deletions

View File

@ -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),

View File

@ -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();

View File

@ -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<SettingsGroup> get groups => [explorer, network];
List<SettingsGroup> get groups => [explorer, network, tor];
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
Map<String, Object?> 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

View File

@ -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<String, Object?> j, {required bool plainSecrets}) {
j['torKeepData'] = keepData;
j['torIdleMinutes'] = idleMinutes;
j['torHomeUrl'] = homeUrl;
j['torJavaScript'] = javaScript;
j['torNoticeSeen'] = noticeSeen;
}
@override
void read(Map<String, dynamic> 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;
}
}

View File

@ -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<int>(
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(),
),
),

View File

@ -196,7 +196,7 @@ Future<void> showTorNotice(BuildContext context) => showDialog<void>(
class _TorPageState extends State<TorPage> 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<TorPage> 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<TorPage> 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<TorPage> 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<TorPage> 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<TorPage> 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<TorPage> 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<void>(builder: (_) => SettingsPage(c: widget.c, initialGroup: 'tor'))),
},
@ -621,7 +621,7 @@ class _TorPageState extends State<TorPage> 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(

View File

@ -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 {

View File

@ -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 {