diff --git a/integration_test/browser_keep_test.dart b/integration_test/browser_keep_test.dart index 93d1aab..662a681 100644 --- a/integration_test/browser_keep_test.dart +++ b/integration_test/browser_keep_test.dart @@ -33,7 +33,7 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..webViewDataDir = dataDir - ..homeUrl = url; + ..browser.homeUrl = url; final bm = BookmarksController(p.join(dataDir, 'bm.json')); final d = DownloadManager(backends: const [], settings: () => c.settings, readClipboard: () async => null); final navKey = GlobalKey(); diff --git a/integration_test/cef_dns_real_test.dart b/integration_test/cef_dns_real_test.dart index 1a9aadf..4395c3d 100644 --- a/integration_test/cef_dns_real_test.dart +++ b/integration_test/cef_dns_real_test.dart @@ -39,9 +39,9 @@ void main() { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings ..webViewDataDir = p.join(data, 'webview') - ..browserEngine = 'chrome' + ..browser.engine = 'chrome' // 207: 이 PC 처럼 관리자 권한으로 실행 중이면 설정을 켠 때만 Chrome 엔진이 돈다 (--do-not-de-elevate) - ..cefWhenElevated = true; + ..browser.cefWhenElevated = true; final answered = []; final logs = []; final g = NetGuard.instance diff --git a/integration_test/web_translate_test.dart b/integration_test/web_translate_test.dart index f5cc3c8..00e3377 100644 --- a/integration_test/web_translate_test.dart +++ b/integration_test/web_translate_test.dart @@ -57,7 +57,7 @@ void main() { c.settings ..webViewDataDir = dataDir ..uiLanguage = 'ko' - ..webTranslate = true; + ..browser.translate = true; final bm = BookmarksController(p.join(dataDir, 'bm.json')); await tester.pumpWidget(MaterialApp(home: BrowserPage(c: c, bookmarks: bm, initialUrl: url))); @@ -118,7 +118,7 @@ void main() { // 3. JavaScript 끄기 → 페이지를 다시 읽고, 페이지 스크립트는 돌지 않는다 expect(scriptRuns, greaterThan(0)); final loads = pageLoads, runs = scriptRuns; - await c.updateSettings((s) => s.webJavaScript = false); + await c.updateSettings((s) => s.browser.javaScript = false); await waitServer(() => pageLoads > loads); await tester.runAsync(() => Future.delayed(const Duration(seconds: 3))); // ignore: avoid_print @@ -148,7 +148,7 @@ void main() { // 5. 다시 켜기 → 다시 읽고 페이지 스크립트가 돈다, 번역도 다시 final runs2 = scriptRuns; - await c.updateSettings((s) => s.webJavaScript = true); + await c.updateSettings((s) => s.browser.javaScript = true); await waitServer(() => scriptRuns > runs2); expect(scriptRuns, greaterThan(runs2)); d = await waitFor((d) => d['late']!.isNotEmpty && hangul.hasMatch(d['h']!), 'JavaScript 다시 켬'); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 4df1fc1..eb62827 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/browser_settings.dart'; import 'settings/download_settings.dart'; import 'settings/reader_settings.dart'; import 'settings/ai_image_settings.dart'; @@ -26,6 +27,7 @@ import 'settings/explorer_settings.dart'; export 'settings/records.dart'; export 'settings/settings_group.dart'; export 'settings/settings_store.dart'; +export 'settings/browser_settings.dart'; export 'settings/download_settings.dart'; export 'settings/reader_settings.dart'; export 'settings/ai_image_settings.dart'; @@ -46,14 +48,8 @@ class AppSettings { /// 73: 파일 탐색기 · Rsync 즐겨찾기 (폴더, Rsync 는 짝) List folderBookmarks = []; - /// 웹 브라우저에서 YouTube 광고 자동 건너뛰기 (건너뛰기 버튼 누르기 · 건너뛸 수 없는 광고는 빨리 감기 · 소리 끄기) - bool youtubeAdSkip = true; - /// YouTube 페이지의 광고 배너 · 광고 영역 숨기기 - bool youtubeAdHide = true; - /// 웹 브라우저 위에 다른 화면이 올라오거나 다른 화면으로 가면 페이지의 동영상 · 소리를 멈춤 (끄면 계속 들림) - bool webPauseOnLeave = true; @@ -92,7 +88,7 @@ class AppSettings { /// 폰 세로 (좁은 화면) MKV 목록 크기: 'auto' 목록은 내용만큼 · 자세히 칸에 최소 높이 (처음 값) · 'small' · 'large' (관리자 판단 10/17) String mkvCompactList = 'auto'; - /// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([homeUrl]), 'files' = 파일 탐색기 + /// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([browser.homeUrl]), 'files' = 파일 탐색기 String startScreen = 'home'; /// 화면 (글자 · 버튼) 크기 배율. [uiScale] 은 지금 크기 (위쪽 막대의 − · + 로 바꿈), @@ -127,22 +123,11 @@ class AppSettings { /// 프로그램이 켜져 있을 때 탐색기에서 연 동영상 재생: 'same' 켜져 있는 창에서 / 'new' 새 재생 창 String openFileWindow = 'same'; - String homeUrl = 'https://www.youtube.com/'; - /// 앱 안 브라우저 엔진: 'edge' (WebView2, 기본). 'chrome' 은 내려받아 연결 (준비 중) - String browserEngine = 'edge'; - /// 207: 관리자 권한으로 실행 중일 때도 내장 Chrome 엔진을 쓸지 (처음 꺼짐 - 관리자 판단: 보안이 먼저, CEF 는 샌드박스가 꺼져 있다) - bool cefWhenElevated = false; - /// "외부 브라우저로 열기" 에 쓸 브라우저: 'system' · 'chrome' · 'firefox' · 'edge' · 'whale' - String externalBrowser = 'system'; - /// 앱 안 브라우저: 다른 언어로 된 웹 페이지를 화면 언어 ([uiLanguage]) 로 자동 번역 (Google 번역) - bool webTranslate = false; - /// 앱 안 브라우저: 페이지의 JavaScript 실행 (끄면 스크립트 없이 글 · 그림만) - bool webJavaScript = true; // ── 네트워크 · DNS (사용자 요청 10/10: "DNS 주소를 강제로 내가 설정한 주소로", 기본 8.8.8.8 · 보조 8.8.4.4 - 관리자 판단) ── @@ -253,8 +238,6 @@ class AppSettings { /// 66: 좌우로 밀 때 끝에서 처음으로 (끄면 끝에서 멈춤) bool swipeWrap = true; - /// 66-2: 웹 브라우저에서 화면 옮기기 (위쪽 막대를 좌우로) 안내를 한 번 보였는지 - bool browserSwipeHinted = false; /// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드) List cleanupPrechecked = ['work', 'download']; @@ -403,8 +386,11 @@ class AppSettings { /// 다운로드 · YouTube (yt-dlp · aria2) final download = DownloadSettings(); + /// 웹 브라우저 (앱 안 · 외부) + final browser = BrowserSettings(); + /// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 - List get groups => [explorer, network, tor, copy, aiImage, reader, download]; + List get groups => [explorer, network, tor, copy, aiImage, reader, download, browser]; /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 Map toJson({bool plainSecrets = false}) { @@ -414,9 +400,6 @@ class AppSettings { 'mkvOutputRoot': mkvOutputRoot, 'moveTargets': [for (final t in moveTargets) t.toJson()], 'folderBookmarks': [for (final b in folderBookmarks) b.toJson()], - 'youtubeAdSkip': youtubeAdSkip, - 'youtubeAdHide': youtubeAdHide, - 'webPauseOnLeave': webPauseOnLeave, 'minimizeToTray': minimizeToTray, 'closeAction': closeAction, 'showHotkey': showHotkey, @@ -448,12 +431,6 @@ class AppSettings { 'appIcon': appIcon, 'uiLanguagesAdded': uiLanguagesAdded, 'openFileWindow': openFileWindow, - 'homeUrl': homeUrl, - 'browserEngine': browserEngine, - 'cefWhenElevated': cefWhenElevated, - 'externalBrowser': externalBrowser, - 'webTranslate': webTranslate, - 'webJavaScript': webJavaScript, 'zipLevel': zipLevel, 'wifiShareMinutes': wifiShareMinutes, 'wifiShareTailscale': wifiShareTailscale, @@ -472,7 +449,6 @@ class AppSettings { 'imageExts': imageExts, 'startMenuShortcut': startMenuShortcut, 'swipeWrap': swipeWrap, - 'browserSwipeHinted': browserSwipeHinted, 'cleanupPrechecked': cleanupPrechecked, 'migrationNotice': migrationNotice, 'allowInnerToOuter': allowInnerToOuter, @@ -542,9 +518,6 @@ class AppSettings { // 예전 판의 이동 폴더 하나 → "이동" 버튼으로 if (j['moveTargets'] == null && j['moveTargetDir'] is String) MoveTarget('이동', j['moveTargetDir'] as String), ] - ..youtubeAdSkip = j['youtubeAdSkip'] as bool? ?? true - ..youtubeAdHide = j['youtubeAdHide'] as bool? ?? true - ..webPauseOnLeave = j['webPauseOnLeave'] as bool? ?? true ..minimizeToTray = j['minimizeToTray'] as bool? ?? true ..closeAction = const ['background', 'quit', 'ask'].contains(j['closeAction']) ? j['closeAction'] as String : 'background' ..showHotkey = j['showHotkey'] as String? ?? 'Ctrl+Shift+X' @@ -567,12 +540,6 @@ class AppSettings { ? j['screenOrientation'] as String : 'auto' ..openFileWindow = j['openFileWindow'] == 'new' ? 'new' : 'same' - ..homeUrl = j['homeUrl'] as String? ?? 'https://www.youtube.com/' - ..browserEngine = j['browserEngine'] as String? ?? 'edge' - ..cefWhenElevated = j['cefWhenElevated'] == true - ..externalBrowser = j['externalBrowser'] as String? ?? 'system' - ..webTranslate = j['webTranslate'] as bool? ?? false - ..webJavaScript = j['webJavaScript'] as bool? ?? true ..vaultEnabled = j['vaultEnabled'] == true ..shizukuEnabled = j['shizukuEnabled'] == true ..rootEnabled = j['rootEnabled'] == true @@ -602,7 +569,6 @@ class AppSettings { : [...defaultImageExtensions] ..startMenuShortcut = j['startMenuShortcut'] != false ..swipeWrap = j['swipeWrap'] != false - ..browserSwipeHinted = j['browserSwipeHinted'] == true ..cleanupPrechecked = j['cleanupPrechecked'] is List ? [for (final x in j['cleanupPrechecked'] as List) '$x'] : ['work', 'download'] diff --git a/lib/app/settings/browser_settings.dart b/lib/app/settings/browser_settings.dart new file mode 100644 index 0000000..6354573 --- /dev/null +++ b/lib/app/settings/browser_settings.dart @@ -0,0 +1,63 @@ + +import 'settings_group.dart'; + +/// 약속 (설정 A): 환경 설정 > 웹 브라우저 (앱 안 · 외부) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다. +/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키) +class BrowserSettings implements SettingsGroup { + /// 웹 브라우저에서 YouTube 광고 자동 건너뛰기 (건너뛰기 버튼 누르기 · 건너뛸 수 없는 광고는 빨리 감기 · 소리 끄기) + bool adSkip = true; + + /// YouTube 페이지의 광고 배너 · 광고 영역 숨기기 + bool adHide = true; + + /// 웹 브라우저 위에 다른 화면이 올라오거나 다른 화면으로 가면 페이지의 동영상 · 소리를 멈춤 (끄면 계속 들림) + bool pauseOnLeave = true; + + String homeUrl = 'https://www.youtube.com/'; + + /// 앱 안 브라우저 엔진: 'edge' (WebView2, 기본). 'chrome' 은 내려받아 연결 (준비 중) + String engine = 'edge'; + + /// 207: 관리자 권한으로 실행 중일 때도 내장 Chrome 엔진을 쓸지 (처음 꺼짐 - 관리자 판단: 보안이 먼저, CEF 는 샌드박스가 꺼져 있다) + bool cefWhenElevated = false; + + /// "외부 브라우저로 열기" 에 쓸 브라우저: 'system' · 'chrome' · 'firefox' · 'edge' · 'whale' + String external = 'system'; + + /// 앱 안 브라우저: 다른 언어로 된 웹 페이지를 화면 언어 ([uiLanguage]) 로 자동 번역 (Google 번역) + bool translate = false; + + /// 앱 안 브라우저: 페이지의 JavaScript 실행 (끄면 스크립트 없이 글 · 그림만) + bool javaScript = true; + + /// 66-2: 웹 브라우저에서 화면 옮기기 (위쪽 막대를 좌우로) 안내를 한 번 보였는지 + bool swipeHinted = false; + + @override + void write(Map j, {required bool plainSecrets}) { + j['youtubeAdSkip'] = adSkip; + j['youtubeAdHide'] = adHide; + j['webPauseOnLeave'] = pauseOnLeave; + j['homeUrl'] = homeUrl; + j['browserEngine'] = engine; + j['cefWhenElevated'] = cefWhenElevated; + j['externalBrowser'] = external; + j['webTranslate'] = translate; + j['webJavaScript'] = javaScript; + j['browserSwipeHinted'] = swipeHinted; + } + + @override + void read(Map j) { + adSkip = j['youtubeAdSkip'] as bool? ?? true; + adHide = j['youtubeAdHide'] as bool? ?? true; + pauseOnLeave = j['webPauseOnLeave'] as bool? ?? true; + homeUrl = j['homeUrl'] as String? ?? 'https://www.youtube.com/'; + engine = j['browserEngine'] as String? ?? 'edge'; + cefWhenElevated = j['cefWhenElevated'] == true; + external = j['externalBrowser'] as String? ?? 'system'; + translate = j['webTranslate'] as bool? ?? false; + javaScript = j['webJavaScript'] as bool? ?? true; + swipeHinted = j['browserSwipeHinted'] == true; + } +} diff --git a/lib/main.dart b/lib/main.dart index bbe1180..1175f86 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -486,7 +486,7 @@ Future setupNetwork(AppController c, String dataDir, {bool second = false} if (Platform.isAndroid) { WebProxy.log = c.note; // 환경 설정 "다른 화면으로 가면 소리 멈춤" 이 꺼져 있으면 일반 브라우저가 뒤에 있어도 DNS 문을 열어 둔다 - WebProxy.keepPlayingWhenHidden = () => !c.settings.webPauseOnLeave; + WebProxy.keepPlayingWhenHidden = () => !c.settings.browser.pauseOnLeave; try { await WebProxy.apply(); } catch (e) { @@ -499,10 +499,10 @@ Future setupNetwork(AppController c, String dataDir, {bool second = false} ); } var last = c.settings.network.config.key; - var lastPause = c.settings.webPauseOnLeave; + var lastPause = c.settings.browser.pauseOnLeave; c.addListener(() async { - if (Platform.isAndroid && c.settings.webPauseOnLeave != lastPause) { - lastPause = c.settings.webPauseOnLeave; + if (Platform.isAndroid && c.settings.browser.pauseOnLeave != lastPause) { + lastPause = c.settings.browser.pauseOnLeave; await WebProxy.apply(); } final now = c.settings.network.config.key; diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index 901d2d6..6af82ac 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -207,7 +207,7 @@ class _BrowserPageState extends State with RouteAware { /// 207: Chrome 엔진 [다시 시도] 때마다 새로 int _cefTry = 0; - late String _url = widget.initialUrl ?? BrowserPage._lastUrl ?? widget.c.settings.homeUrl; + late String _url = widget.initialUrl ?? BrowserPage._lastUrl ?? widget.c.settings.browser.homeUrl; String _title = ''; double _progress = 1; @@ -355,7 +355,7 @@ class _BrowserPageState extends State with RouteAware { static final _translator = WebPageTranslator(); /// 이 화면에서 번역 중인지. 처음엔 설정을 따르고, 도구 막대의 [번역] 버튼으로 바꾼다. - late bool _trOn = widget.c.settings.webTranslate; + late bool _trOn = widget.c.settings.browser.translate; /// 버튼으로 직접 켰다: 이미 화면 언어로 보이는 페이지도 번역한다 bool _trForce = false; @@ -374,12 +374,12 @@ class _BrowserPageState extends State with RouteAware { if (!mounted) return; _applyCookieExport(); final s = widget.c.settings; - if (s.webJavaScript != _lastJs) { - _lastJs = s.webJavaScript; + if (s.browser.javaScript != _lastJs) { + _lastJs = s.browser.javaScript; _nav?.setJavaScript(_lastJs); } - if (s.webTranslate != _lastTranslate) { - _lastTranslate = s.webTranslate; + if (s.browser.translate != _lastTranslate) { + _lastTranslate = s.browser.translate; _setTranslate(_lastTranslate, force: false); } else if (s.uiLanguage != _lastLang && _trOn) { _translateNow(); // 다른 언어로 다시 번역 (스크립트가 앞의 번역을 되돌린 뒤 번역) @@ -448,17 +448,17 @@ class _BrowserPageState extends State with RouteAware { super.initState(); BrowserPage._open++; final s = widget.c.settings; - _lastTranslate = s.webTranslate; - _lastJs = s.webJavaScript; + _lastTranslate = s.browser.translate; + _lastJs = s.browser.javaScript; _lastLang = s.uiLanguage; _applyCookieExport(); widget.c.addListener(_onSettings); if (_trOn) _translateNow(); // 66-2: 브라우저 가운데는 페이지가 좌우로 밀리므로 화면 옮기기는 위쪽 막대에서 - 처음 한 번만 알린다. // 알림 (SnackBar) 은 다른 알림 (다운로드 등) 을 뒤로 밀어 기다리게 하므로 위쪽 막대 아래의 한 줄로 - if (s.swipeNav && !s.browserSwipeHinted) { + if (s.swipeNav && !s.browser.swipeHinted) { _swipeHint = true; - WidgetsBinding.instance.addPostFrameCallback((_) => widget.c.updateSettings((x) => x.browserSwipeHinted = true)); + WidgetsBinding.instance.addPostFrameCallback((_) => widget.c.updateSettings((x) => x.browser.swipeHinted = true)); } } @@ -580,7 +580,7 @@ class _BrowserPageState extends State with RouteAware { void didPushNext() { _visible = false; // 환경 설정 > 웹 브라우저 > "다른 화면으로 가면 페이지 소리 멈춤" (끄면 다른 화면에서도 계속 들림) - if (widget.c.settings.webPauseOnLeave) _nav?.pauseMedia(); + if (widget.c.settings.browser.pauseOnLeave) _nav?.pauseMedia(); } /// 설정 화면 등에서 돌아왔다: 바뀐 광고 설정을 지금 페이지에 적용 @@ -595,13 +595,13 @@ class _BrowserPageState extends State with RouteAware { void _applyAdSettings() { if (!(Uri.tryParse(_url)?.host ?? '').endsWith('youtube.com')) return; final s = widget.c.settings; - _nav?.runScript(youtubeAdScript(skip: s.youtubeAdSkip, hide: s.youtubeAdHide, badge: tr('광고 건너뛰는 중…'))); + _nav?.runScript(youtubeAdScript(skip: s.browser.adSkip, hide: s.browser.adHide, badge: tr('광고 건너뛰는 중…'))); } /// MKV 화면 등으로 가며 브라우저 화면이 닫힌다: 살려 둔 웹뷰에서 소리만 나지 않게 멈춘다 @override void didPop() { - if (widget.c.settings.webPauseOnLeave) _nav?.pauseMedia(); + if (widget.c.settings.browser.pauseOnLeave) _nav?.pauseMedia(); } @override @@ -670,7 +670,7 @@ class _BrowserPageState extends State with RouteAware { } void _openExternal(String url) => - widget.c.services.shell.openUrl(url, browser: widget.c.settings.externalBrowser); + widget.c.services.shell.openUrl(url, browser: widget.c.settings.browser.external); // ───────── 즐겨찾기 ───────── @@ -774,26 +774,26 @@ class _BrowserPageState extends State with RouteAware { Expanded( child: _withDnsBlock(widget.viewBuilder?.call(_host, _url) ?? // 환경 설정에서 Chrome 을 고르고 엔진이 이번 실행에 준비되어 있으면 Chrome, 아니면 Edge - (widget.c.settings.browserEngine == 'chrome' && CefRuntime.readyThisRun + (widget.c.settings.browser.engine == 'chrome' && CefRuntime.readyThisRun // 207: 관리자 권한이면 처음 값으로 Chrome 엔진을 시작하지 않는다 (보안) - 이유와 고를 것 - ? Elevation.elevated && !widget.c.settings.cefWhenElevated + ? Elevation.elevated && !widget.c.settings.browser.cefWhenElevated ? _CefElevatedBlocked(c: widget.c) // Chrome 엔진은 JavaScript 설정을 만든 뒤에 바꿀 수 없어 바뀌면 새로 만든다 : _CefView( - key: ValueKey('${widget.c.settings.webJavaScript}|$_cefTry'), + key: ValueKey('${widget.c.settings.browser.javaScript}|$_cefTry'), host: _host, initialUrl: _url, dataDir: widget.c.settings.webViewDataDir, - javaScript: widget.c.settings.webJavaScript, + javaScript: widget.c.settings.browser.javaScript, // 207: 시작하지 못하면 사용자가 바로 고를 수 있게 - onUseEdge: () => widget.c.updateSettings((x) => x.browserEngine = 'edge'), + onUseEdge: () => widget.c.updateSettings((x) => x.browser.engine = 'edge'), onRetry: () => setState(() => _cefTry++), - cefWhenElevated: widget.c.settings.cefWhenElevated) + cefWhenElevated: widget.c.settings.browser.cefWhenElevated) : _EdgeView( host: _host, initialUrl: _url, dataDir: widget.c.settings.webViewDataDir, - javaScript: widget.c.settings.webJavaScript))), + javaScript: widget.c.settings.browser.javaScript))), ), ]), ), @@ -852,7 +852,7 @@ class _BrowserPageState extends State with RouteAware { _progress < 1 ? btn(Icons.close, tr('중지'), () => _nav?.stop()) : btn(Icons.refresh, keyHint(tr('새로고침'), 'F5'), () => _nav?.reload()), - if (!compact) btn(Icons.home_outlined, tr('홈'), () => _go(widget.c.settings.homeUrl)), + if (!compact) btn(Icons.home_outlined, tr('홈'), () => _go(widget.c.settings.browser.homeUrl)), const SizedBox(width: 6), Expanded( child: TextField( @@ -940,7 +940,7 @@ class _BrowserPageState extends State with RouteAware { tooltip: tr('더 보기'), icon: const Icon(Icons.more_vert, size: 20), onSelected: (v) => switch (v) { - 'home' => _go(widget.c.settings.homeUrl), + 'home' => _go(widget.c.settings.browser.homeUrl), 'translate' => _setTranslate(!_trOn, force: true), 'external' => _openExternal(_url), 'work' => _toggleWork(), @@ -1349,7 +1349,7 @@ class _CefElevatedBlocked extends StatelessWidget { const SizedBox(height: 16), Wrap(spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [ FilledButton( - onPressed: () => c.updateSettings((x) => x.browserEngine = 'edge'), + onPressed: () => c.updateSettings((x) => x.browser.engine = 'edge'), child: Text(tr('Edge 엔진으로 바꾸기')), ), OutlinedButton( diff --git a/lib/ui/cef_setup.dart b/lib/ui/cef_setup.dart index c1f74bc..9f0de12 100644 --- a/lib/ui/cef_setup.dart +++ b/lib/ui/cef_setup.dart @@ -35,7 +35,7 @@ Future chooseChromeEngine(BuildContext context, AppController c) async { ); if (installed != true || !context.mounted) return false; } - await c.updateSettings((s) => s.browserEngine = 'chrome'); + await c.updateSettings((s) => s.browser.engine = 'chrome'); if (!context.mounted) return true; if (!CefRuntime.readyThisRun) { final restart = await showDialog( @@ -147,7 +147,7 @@ Future confirmCefWhenElevated(BuildContext context, AppController c) async ), ); if (ok != true) return false; - await c.updateSettings((x) => x.cefWhenElevated = true); + await c.updateSettings((x) => x.browser.cefWhenElevated = true); c.note(tr('관리자 권한일 때도 Chrome 엔진 쓰기를 켰습니다 (권장하지 않음).')); return true; } diff --git a/lib/ui/network_settings.dart b/lib/ui/network_settings.dart index 436c010..b768cad 100644 --- a/lib/ui/network_settings.dart +++ b/lib/ui/network_settings.dart @@ -244,7 +244,7 @@ class _DnsSettingsState extends State { if (!desk) tr('· Android 의 yt-dlp 새 버전 확인 (다운로드 도구 안에서 따로 연결)'), ].join('\n')), ), - if (desk && s.browserEngine == 'chrome' && CefRuntime.readyThisRun) + if (desk && s.browser.engine == 'chrome' && CefRuntime.readyThisRun) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: Text(tr('Chrome 엔진은 앱을 다시 시작하면 바뀐 설정을 씁니다.'), style: TextStyle(fontSize: 12, color: JjColors.textDim)), diff --git a/lib/ui/settings/browser_section.dart b/lib/ui/settings/browser_section.dart index 638409c..3bffe6f 100644 --- a/lib/ui/settings/browser_section.dart +++ b/lib/ui/settings/browser_section.dart @@ -32,29 +32,29 @@ class _BrowserSettingsSectionState extends State { Widget build(BuildContext context) { final s = c.settings; return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ - settingsTextTile(tr('홈 주소'), s.homeUrl, tr('웹 브라우저의 시작 · 🏠 주소'), - (v) => c.updateSettings((x) => x.homeUrl = v.trim().isEmpty ? 'https://www.youtube.com/' : v.trim())), + settingsTextTile(tr('홈 주소'), s.browser.homeUrl, tr('웹 브라우저의 시작 · 🏠 주소'), + (v) => c.updateSettings((x) => x.browser.homeUrl = v.trim().isEmpty ? 'https://www.youtube.com/' : v.trim())), SwitchListTile( - value: s.youtubeAdSkip, - onChanged: (v) => c.updateSettings((x) => x.youtubeAdSkip = v), + value: s.browser.adSkip, + onChanged: (v) => c.updateSettings((x) => x.browser.adSkip = v), title: Text(tr('YouTube 광고 자동 건너뛰기')), subtitle: Text(tr('앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 ' '(넘기는 동안 화면 위쪽에 "광고 건너뛰는 중…")')), ), SwitchListTile( - value: s.youtubeAdHide, - onChanged: (v) => c.updateSettings((x) => x.youtubeAdHide = v), + value: s.browser.adHide, + onChanged: (v) => c.updateSettings((x) => x.browser.adHide = v), title: Text(tr('YouTube 광고 배너 숨기기')), subtitle: Text(tr('목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다')), ), SwitchListTile( - value: s.webPauseOnLeave, - onChanged: (v) => c.updateSettings((x) => x.webPauseOnLeave = v), + value: s.browser.pauseOnLeave, + onChanged: (v) => c.updateSettings((x) => x.browser.pauseOnLeave = v), title: Text(tr('다른 화면으로 가면 페이지 소리 멈춤')), subtitle: Text(tr('끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다')), ), SwitchListTile( - value: s.webTranslate, - onChanged: (v) => c.updateSettings((x) => x.webTranslate = v), + value: s.browser.translate, + onChanged: (v) => c.updateSettings((x) => x.browser.translate = v), title: Text(tr('웹 페이지 자동 번역')), subtitle: Text(trf( '다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. ' @@ -62,8 +62,8 @@ class _BrowserSettingsSectionState extends State { [I18nController.nativeName(s.uiLanguage)])), ), SwitchListTile( - value: s.webJavaScript, - onChanged: (v) => c.updateSettings((x) => x.webJavaScript = v), + value: s.browser.javaScript, + onChanged: (v) => c.updateSettings((x) => x.browser.javaScript = v), title: Text(tr('JavaScript 사용')), subtitle: Text(desk ? tr('끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). ' @@ -75,10 +75,10 @@ class _BrowserSettingsSectionState extends State { SettingTile( title: Text(tr('브라우저 엔진 (앱 안)')), subtitle: Text( - trf('Edge: Windows 에 들어 있어 따로 설치하지 않습니다. 여기서 YouTube 에 로그인하면 다운로드에도 그 로그인이 쓰입니다.\n' 'Chrome: 고르면 약 {0}MB 를 내려받고 다시 시작한 뒤 쓸 수 있습니다 ' '(Google 로그인은 막힐 수 있음).' '{1}' '{2}', [CefRuntime.approxDownloadMb, CefRuntime.installed ? trf(' 지금 설치됨 ({0}MB)', [CefRuntime.installedMb()]) : '', s.browserEngine == 'chrome' && !CefRuntime.readyThisRun ? tr(' · 다시 시작해야 Chrome 으로 바뀝니다') : ''])), + trf('Edge: Windows 에 들어 있어 따로 설치하지 않습니다. 여기서 YouTube 에 로그인하면 다운로드에도 그 로그인이 쓰입니다.\n' 'Chrome: 고르면 약 {0}MB 를 내려받고 다시 시작한 뒤 쓸 수 있습니다 ' '(Google 로그인은 막힐 수 있음).' '{1}' '{2}', [CefRuntime.approxDownloadMb, CefRuntime.installed ? trf(' 지금 설치됨 ({0}MB)', [CefRuntime.installedMb()]) : '', s.browser.engine == 'chrome' && !CefRuntime.readyThisRun ? tr(' · 다시 시작해야 Chrome 으로 바뀝니다') : ''])), isThreeLine: true, trailing: Row(mainAxisSize: MainAxisSize.min, children: [ - if (s.browserEngine == 'edge' && CefRuntime.installed && !CefRuntime.readyThisRun) + if (s.browser.engine == 'edge' && CefRuntime.installed && !CefRuntime.readyThisRun) IconButton( tooltip: tr('내려받은 Chrome 엔진 지우기'), icon: Icon(Icons.delete_outline, color: JjColors.textDim), @@ -89,7 +89,7 @@ class _BrowserSettingsSectionState extends State { }, ), DropdownButton( - value: s.browserEngine, + value: s.browser.engine, items: [ DropdownMenuItem(value: 'edge', child: Text(tr('Edge (내장)'))), DropdownMenuItem( @@ -101,7 +101,7 @@ class _BrowserSettingsSectionState extends State { await chooseChromeEngine(context, c); if (mounted) setState(() {}); } else { - await c.updateSettings((x) => x.browserEngine = 'edge'); + await c.updateSettings((x) => x.browser.engine = 'edge'); } }, ), @@ -109,12 +109,12 @@ class _BrowserSettingsSectionState extends State { ), // 207: 관리자 권한일 때도 Chrome 엔진 쓰기 (처음 꺼짐, 켤 때 경고) SwitchListTile( - value: s.cefWhenElevated, + value: s.browser.cefWhenElevated, onChanged: (v) async { if (v) { await confirmCefWhenElevated(context, c); } else { - await c.updateSettings((x) => x.cefWhenElevated = false); + await c.updateSettings((x) => x.browser.cefWhenElevated = false); } if (mounted) setState(() {}); }, @@ -127,10 +127,10 @@ class _BrowserSettingsSectionState extends State { title: Text(tr('외부 브라우저')), subtitle: Text(tr('"외부 브라우저로 열기" 에 쓸 브라우저 (이 PC 에 설치된 것)')), trailing: DropdownButton( - value: s.externalBrowser, + value: s.browser.external, items: [ DropdownMenuItem(value: 'system', child: Text(tr('Windows 기본 브라우저'))), - for (final b in {...c.services.shell.installedBrowsers(), if (s.externalBrowser != 'system') s.externalBrowser}) + for (final b in {...c.services.shell.installedBrowsers(), if (s.browser.external != 'system') s.browser.external}) DropdownMenuItem(value: b, child: Text(switch (b) { 'chrome' => 'Chrome', 'firefox' => 'Firefox', @@ -139,7 +139,7 @@ class _BrowserSettingsSectionState extends State { _ => b, })), ], - onChanged: (v) => c.updateSettings((x) => x.externalBrowser = v!), + onChanged: (v) => c.updateSettings((x) => x.browser.external = v!), ), ), ], diff --git a/test/browser_test.dart b/test/browser_test.dart index 81aba13..56932ce 100644 --- a/test/browser_test.dart +++ b/test/browser_test.dart @@ -633,7 +633,7 @@ void main() { await tester.pumpAndSettle(); expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsOneWidget); expect(find.byType(SnackBar), findsNothing, reason: '알림 (다운로드 등) 을 뒤로 밀지 않게 알림이 아니라 한 줄'); - expect(c.settings.browserSwipeHinted, isTrue); + expect(c.settings.browser.swipeHinted, isTrue); await tester.tap(find.byTooltip('안내 닫기')); await tester.pump(); expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing); diff --git a/test/cef_elevation_test.dart b/test/cef_elevation_test.dart index a044bd9..788a4a3 100644 --- a/test/cef_elevation_test.dart +++ b/test/cef_elevation_test.dart @@ -24,7 +24,7 @@ void main() { dir = Directory.systemTemp.createTempSync('jj_cef_elev_'); c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings - ..browserEngine = 'chrome' + ..browser.engine = 'chrome' ..webViewDataDir = p.join(dir.path, 'webview'); bm = BookmarksController(p.join(dir.path, 'bookmarks.json')); CefRuntime.readyThisRun = true; @@ -52,7 +52,7 @@ void main() { expect(find.text('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'), findsOneWidget); await t.tap(find.text('Edge 엔진으로 바꾸기')); await t.pump(); - expect(c.settings.browserEngine, 'edge'); + expect(c.settings.browser.engine, 'edge'); await t.pumpWidget(const SizedBox()); }); @@ -64,12 +64,12 @@ void main() { expect(find.textContaining('샌드박스 (웹 페이지를 가두는 보호) 가 꺼져'), findsOneWidget); await t.tap(find.text('취소')); await t.pumpAndSettle(); - expect(c.settings.cefWhenElevated, isFalse); + expect(c.settings.browser.cefWhenElevated, isFalse); await t.tap(find.text('그래도 Chrome 엔진 쓰기…')); await t.pumpAndSettle(); await t.tap(find.text('그래도 쓰기')); await t.pumpAndSettle(); - expect(c.settings.cefWhenElevated, isTrue); + expect(c.settings.browser.cefWhenElevated, isTrue); expect(find.text('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'), findsNothing); await t.pumpWidget(const SizedBox()); }); @@ -86,13 +86,13 @@ void main() { expect(find.text('다시 시도'), findsOneWidget); await t.tap(find.text('Edge 엔진으로 바꾸기')); await t.pump(); - expect(c.settings.browserEngine, 'edge'); + expect(c.settings.browser.engine, 'edge'); await t.pumpWidget(const SizedBox()); }); test('설정: 관리자 권한일 때 Chrome 엔진 쓰기는 처음 꺼짐 · 저장 · 틀린 값은 꺼짐', () { - expect(AppSettings().cefWhenElevated, isFalse); - expect(AppSettings.fromJson((AppSettings()..cefWhenElevated = true).toJson()).cefWhenElevated, isTrue); - expect(AppSettings.fromJson({'cefWhenElevated': 'yes'}).cefWhenElevated, isFalse); + expect(AppSettings().browser.cefWhenElevated, isFalse); + expect(AppSettings.fromJson((AppSettings()..browser.cefWhenElevated = true).toJson()).browser.cefWhenElevated, isTrue); + expect(AppSettings.fromJson({'cefWhenElevated': 'yes'}).browser.cefWhenElevated, isFalse); }); } diff --git a/test/components_test.dart b/test/components_test.dart index bb78825..aed8554 100644 --- a/test/components_test.dart +++ b/test/components_test.dart @@ -175,8 +175,8 @@ void main() { }); test('66-2: 웹 브라우저 화면 옮기기 안내는 한 번만 (설정에 남음)', () { - expect(AppSettings().browserSwipeHinted, isFalse); - expect(AppSettings.fromJson((AppSettings()..browserSwipeHinted = true).toJson()).browserSwipeHinted, isTrue); + expect(AppSettings().browser.swipeHinted, isFalse); + expect(AppSettings.fromJson((AppSettings()..browser.swipeHinted = true).toJson()).browser.swipeHinted, isTrue); }); testWidgets('174: MKV 화면이 맨 처음 화면이 아니어도 (홈 화면 = 탐색기 등) MKV 아이콘이 "지금 여기"', (tester) async { diff --git a/test/decisions_test.dart b/test/decisions_test.dart index a82368b..891381f 100644 --- a/test/decisions_test.dart +++ b/test/decisions_test.dart @@ -34,7 +34,7 @@ void main() { expect(s.tripleTapAction, 'move'); expect(s.davExternalOpen, 'ask'); // 이미 있던 설정의 기본값도 결정대로 - expect([s.webPauseOnLeave, s.autoCheckUpdates, s.youtubeAdSkip, s.youtubeAdHide, s.runInBackground, s.swipeNav], + expect([s.browser.pauseOnLeave, s.autoCheckUpdates, s.browser.adSkip, s.browser.adHide, s.runInBackground, s.swipeNav], everyElement(isTrue)); // 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22) expect([s.copy.liveSyncOnStart, s.copy.liveSyncIntervalSec, s.explorer.layout, s.reader.zipComic], ['auto', 30, 'dual', false]); diff --git a/test/secret_issue_test.dart b/test/secret_issue_test.dart index 9b99368..7ace6f5 100644 --- a/test/secret_issue_test.dart +++ b/test/secret_issue_test.dart @@ -82,7 +82,7 @@ void main() { // 닫으면 설정을 바꿔도 (같은 상황) 다시 뜨지 않는다 await tester.tap(find.text('닫기')); await tester.pumpAndSettle(); - await tester.runAsync(() => c.updateSettings((s) => s.homeUrl = 'https://x.example')); + await tester.runAsync(() => c.updateSettings((s) => s.browser.homeUrl = 'https://x.example')); await tester.pumpAndSettle(); expect(find.text('새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.'), findsNothing); DavRegistry.configure([]); diff --git a/test/secret_store_test.dart b/test/secret_store_test.dart index 625e4a3..6e0e95d 100644 --- a/test/secret_store_test.dart +++ b/test/secret_store_test.dart @@ -63,7 +63,7 @@ void main() { // 예전 판: 모르는 항목을 지우고 저장 File(file).writeAsStringSync(jsonEncode({'homeUrl': 'https://old.example'})); final s = await store().load(); - expect(s.homeUrl, 'https://old.example'); + expect(s.browser.homeUrl, 'https://old.example'); expect(s.webdavServers.single.url, 'https://nas'); expect(s.webdavServers.single.password, 'pw'); expect(s.openSubtitlesKey, 'KEY-2'); @@ -104,7 +104,7 @@ void main() { final st = store(); final s = await st.load(); expect(s.openSubtitlesKey, 'KEY-3'); - await st.save(s..homeUrl = 'https://x.example'); + await st.save(s..browser.homeUrl = 'https://x.example'); expect(text(file), contains('KEY-3')); expect(text(file), contains('plain')); expect(secrets.values, {'dav.pw.d1': 'kept'}); diff --git a/test/settings_store_test.dart b/test/settings_store_test.dart index 309e286..580d845 100644 --- a/test/settings_store_test.dart +++ b/test/settings_store_test.dart @@ -21,14 +21,14 @@ void main() { test('저장: 임시 파일에 쓴 뒤 바꿔치기 · 직전 정상본은 .bak · 남는 임시 파일 없음', () async { final store = SettingsStore(file); - await store.save(AppSettings()..homeUrl = 'https://a.example'); - await store.save(AppSettings()..homeUrl = 'https://b.example'); - expect((await SettingsStore(file).load()).homeUrl, 'https://b.example'); + await store.save(AppSettings()..browser.homeUrl = 'https://a.example'); + await store.save(AppSettings()..browser.homeUrl = 'https://b.example'); + expect((await SettingsStore(file).load()).browser.homeUrl, 'https://b.example'); expect(File('$file.bak').readAsStringSync(), contains('https://a.example')); expect(File('$file.tmp').existsSync(), isFalse); // 여러 번 빠르게 저장해도 마지막 것이 남는다 (순서대로) - await Future.wait([for (var i = 0; i < 5; i++) store.save(AppSettings()..homeUrl = 'https://$i.example')]); - expect((await SettingsStore(file).load()).homeUrl, 'https://4.example'); + await Future.wait([for (var i = 0; i < 5; i++) store.save(AppSettings()..browser.homeUrl = 'https://$i.example')]); + expect((await SettingsStore(file).load()).browser.homeUrl, 'https://4.example'); }); test('관리자 지시 (10/21): 보통 저장은 이 판이 만드는 꼴의 열쇠만 지운다 - 모르는 꼴 (새 판의 dav.token.x 등) 은 남김 · 지운 서버의 아는 열쇠만', () async { @@ -98,11 +98,11 @@ void main() { test('깨진 설정 파일: 백업으로 되살리고, 깨진 파일은 보관 · 알림 정보', () async { final store = SettingsStore(file); - await store.save(AppSettings()..homeUrl = 'https://good.example'); - await store.save(AppSettings()..homeUrl = 'https://good2.example'); // .bak = good + await store.save(AppSettings()..browser.homeUrl = 'https://good.example'); + await store.save(AppSettings()..browser.homeUrl = 'https://good2.example'); // .bak = good File(file).writeAsStringSync('{"homeUrl": "https://half'); // 반쯤 쓰다 끊긴 파일 final s = await store.load(); - expect(s.homeUrl, 'https://good.example'); + expect(s.browser.homeUrl, 'https://good.example'); expect(store.problem!.restoredFromBackup, isTrue); final kept = store.problem!.brokenCopy!; expect(File(kept).readAsStringSync(), '{"homeUrl": "https://half'); @@ -115,7 +115,7 @@ void main() { File(file).writeAsStringSync('not json'); final store = SettingsStore(file); final s = await store.load(); - expect(s.homeUrl, AppSettings().homeUrl); + expect(s.browser.homeUrl, AppSettings().browser.homeUrl); expect(store.problem!.restoredFromBackup, isFalse); expect(File(store.problem!.brokenCopy!).readAsStringSync(), 'not json'); @@ -123,7 +123,7 @@ void main() { File(file).deleteSync(); File('$file.bak').writeAsStringSync('{"homeUrl": "https://bak.example"}'); final again = SettingsStore(file); - expect((await again.load()).homeUrl, 'https://bak.example'); + expect((await again.load()).browser.homeUrl, 'https://bak.example'); expect(again.problem!.restoredFromBackup, isTrue); // 정상이면 알림 없음 await again.save(AppSettings()); diff --git a/test/web_translate_test.dart b/test/web_translate_test.dart index 4d17a4d..3929427 100644 --- a/test/web_translate_test.dart +++ b/test/web_translate_test.dart @@ -96,13 +96,13 @@ void main() { test('설정: 번역 (기본 끔) · JavaScript (기본 켬) 저장 · 읽기', () { final d = AppSettings(); - expect(d.webTranslate, isFalse); - expect(d.webJavaScript, isTrue); + expect(d.browser.translate, isFalse); + expect(d.browser.javaScript, isTrue); final s = AppSettings.fromJson(jsonDecode(jsonEncode((AppSettings() - ..webTranslate = true - ..webJavaScript = false) + ..browser.translate = true + ..browser.javaScript = false) .toJson())) as Map); - expect(s.webTranslate, isTrue); - expect(s.webJavaScript, isFalse); + expect(s.browser.translate, isTrue); + expect(s.browser.javaScript, isFalse); }); } diff --git a/test/youtube_ads_test.dart b/test/youtube_ads_test.dart index fc53258..01b6a80 100644 --- a/test/youtube_ads_test.dart +++ b/test/youtube_ads_test.dart @@ -46,12 +46,12 @@ console.log(JSON.stringify(r)); void main() { test('설정: 광고 건너뛰기 · 숨기기 기본 켜짐, 저장', () { final s = AppSettings(); - expect([s.youtubeAdSkip, s.youtubeAdHide], [true, true]); + expect([s.browser.adSkip, s.browser.adHide], [true, true]); s - ..youtubeAdSkip = false - ..youtubeAdHide = false; + ..browser.adSkip = false + ..browser.adHide = false; final back = AppSettings.fromJson(s.toJson()); - expect([back.youtubeAdSkip, back.youtubeAdHide], [false, false]); + expect([back.browser.adSkip, back.browser.adHide], [false, false]); expect(youtubeAdScript(skip: false, hide: true), contains('{skip: false, hide: true}')); });