설정 A 분야 8: 웹 브라우저 (BrowserSettings) - 키 10

- 이미 옮긴 분야 뒤의 같은 이름 (s.tor.homeUrl) 은 바꾸지 않음
- browser_page 의 _CefView 자기 필드 cefWhenElevated 는 그대로 (같은 이름이라 잘못 바뀐 것을 되돌림)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
RyuJongJin 2026-10-22 23:23:30 +09:00
parent 6beeb22e43
commit 7c6320165a
19 changed files with 163 additions and 134 deletions

View File

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

View File

@ -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 = <String>[];
final logs = <String>[];
final g = NetGuard.instance

View File

@ -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<void>.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 다시 켬');

View File

@ -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<FolderBookmark> 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<String> cleanupPrechecked = ['work', 'download'];
@ -403,8 +386,11 @@ class AppSettings {
/// 다운로드 · YouTube (yt-dlp · aria2)
final download = DownloadSettings();
/// 웹 브라우저 (앱 안 · 외부)
final browser = BrowserSettings();
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download];
List<SettingsGroup> get groups => [explorer, network, tor, copy, aiImage, reader, download, browser];
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
Map<String, Object?> 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']

View File

@ -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<String, Object?> 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<String, dynamic> 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;
}
}

View File

@ -486,7 +486,7 @@ Future<void> 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<void> 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;

View File

@ -207,7 +207,7 @@ class _BrowserPageState extends State<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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<BrowserPage> 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(

View File

@ -35,7 +35,7 @@ Future<bool> 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<bool>(
@ -147,7 +147,7 @@ Future<bool> 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;
}

View File

@ -244,7 +244,7 @@ class _DnsSettingsState extends State<DnsSettings> {
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)),

View File

@ -32,29 +32,29 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
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<BrowserSettingsSection> {
[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<BrowserSettingsSection> {
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<BrowserSettingsSection> {
},
),
DropdownButton<String>(
value: s.browserEngine,
value: s.browser.engine,
items: [
DropdownMenuItem(value: 'edge', child: Text(tr('Edge (내장)'))),
DropdownMenuItem(
@ -101,7 +101,7 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
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<BrowserSettingsSection> {
),
// 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<BrowserSettingsSection> {
title: Text(tr('외부 브라우저')),
subtitle: Text(tr('"외부 브라우저로 열기" 에 쓸 브라우저 (이 PC 에 설치된 것)')),
trailing: DropdownButton<String>(
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<BrowserSettingsSection> {
_ => b,
})),
],
onChanged: (v) => c.updateSettings((x) => x.externalBrowser = v!),
onChanged: (v) => c.updateSettings((x) => x.browser.external = v!),
),
),
],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'});

View File

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

View File

@ -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<String, dynamic>);
expect(s.webTranslate, isTrue);
expect(s.webJavaScript, isFalse);
expect(s.browser.translate, isTrue);
expect(s.browser.javaScript, isFalse);
});
}

View File

@ -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}'));
});