설정 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())); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings c.settings
..webViewDataDir = dataDir ..webViewDataDir = dataDir
..homeUrl = url; ..browser.homeUrl = url;
final bm = BookmarksController(p.join(dataDir, 'bm.json')); final bm = BookmarksController(p.join(dataDir, 'bm.json'));
final d = DownloadManager(backends: const [], settings: () => c.settings, readClipboard: () async => null); final d = DownloadManager(backends: const [], settings: () => c.settings, readClipboard: () async => null);
final navKey = GlobalKey<NavigatorState>(); final navKey = GlobalKey<NavigatorState>();

View File

@ -39,9 +39,9 @@ void main() {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings c.settings
..webViewDataDir = p.join(data, 'webview') ..webViewDataDir = p.join(data, 'webview')
..browserEngine = 'chrome' ..browser.engine = 'chrome'
// 207: 이 PC 처럼 관리자 권한으로 실행 중이면 설정을 켠 때만 Chrome 엔진이 돈다 (--do-not-de-elevate) // 207: 이 PC 처럼 관리자 권한으로 실행 중이면 설정을 켠 때만 Chrome 엔진이 돈다 (--do-not-de-elevate)
..cefWhenElevated = true; ..browser.cefWhenElevated = true;
final answered = <String>[]; final answered = <String>[];
final logs = <String>[]; final logs = <String>[];
final g = NetGuard.instance final g = NetGuard.instance

View File

@ -57,7 +57,7 @@ void main() {
c.settings c.settings
..webViewDataDir = dataDir ..webViewDataDir = dataDir
..uiLanguage = 'ko' ..uiLanguage = 'ko'
..webTranslate = true; ..browser.translate = true;
final bm = BookmarksController(p.join(dataDir, 'bm.json')); final bm = BookmarksController(p.join(dataDir, 'bm.json'));
await tester.pumpWidget(MaterialApp(home: BrowserPage(c: c, bookmarks: bm, initialUrl: url))); await tester.pumpWidget(MaterialApp(home: BrowserPage(c: c, bookmarks: bm, initialUrl: url)));
@ -118,7 +118,7 @@ void main() {
// 3. JavaScript 끄기 → 페이지를 다시 읽고, 페이지 스크립트는 돌지 않는다 // 3. JavaScript 끄기 → 페이지를 다시 읽고, 페이지 스크립트는 돌지 않는다
expect(scriptRuns, greaterThan(0)); expect(scriptRuns, greaterThan(0));
final loads = pageLoads, runs = scriptRuns; 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 waitServer(() => pageLoads > loads);
await tester.runAsync(() => Future<void>.delayed(const Duration(seconds: 3))); await tester.runAsync(() => Future<void>.delayed(const Duration(seconds: 3)));
// ignore: avoid_print // ignore: avoid_print
@ -148,7 +148,7 @@ void main() {
// 5. 다시 켜기 → 다시 읽고 페이지 스크립트가 돈다, 번역도 다시 // 5. 다시 켜기 → 다시 읽고 페이지 스크립트가 돈다, 번역도 다시
final runs2 = scriptRuns; final runs2 = scriptRuns;
await c.updateSettings((s) => s.webJavaScript = true); await c.updateSettings((s) => s.browser.javaScript = true);
await waitServer(() => scriptRuns > runs2); await waitServer(() => scriptRuns > runs2);
expect(scriptRuns, greaterThan(runs2)); expect(scriptRuns, greaterThan(runs2));
d = await waitFor((d) => d['late']!.isNotEmpty && hangul.hasMatch(d['h']!), 'JavaScript 다시 켬'); 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 '../core/remote_idle.dart';
import 'settings/records.dart'; import 'settings/records.dart';
import 'settings/settings_group.dart'; import 'settings/settings_group.dart';
import 'settings/browser_settings.dart';
import 'settings/download_settings.dart'; import 'settings/download_settings.dart';
import 'settings/reader_settings.dart'; import 'settings/reader_settings.dart';
import 'settings/ai_image_settings.dart'; import 'settings/ai_image_settings.dart';
@ -26,6 +27,7 @@ import 'settings/explorer_settings.dart';
export 'settings/records.dart'; export 'settings/records.dart';
export 'settings/settings_group.dart'; export 'settings/settings_group.dart';
export 'settings/settings_store.dart'; export 'settings/settings_store.dart';
export 'settings/browser_settings.dart';
export 'settings/download_settings.dart'; export 'settings/download_settings.dart';
export 'settings/reader_settings.dart'; export 'settings/reader_settings.dart';
export 'settings/ai_image_settings.dart'; export 'settings/ai_image_settings.dart';
@ -46,14 +48,8 @@ class AppSettings {
/// 73: 파일 탐색기 · Rsync 즐겨찾기 (폴더, Rsync 는 짝) /// 73: 파일 탐색기 · Rsync 즐겨찾기 (폴더, Rsync 는 짝)
List<FolderBookmark> folderBookmarks = []; 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) /// 폰 세로 (좁은 화면) MKV 목록 크기: 'auto' 목록은 내용만큼 · 자세히 칸에 최소 높이 (처음 값) · 'small' · 'large' (관리자 판단 10/17)
String mkvCompactList = 'auto'; String mkvCompactList = 'auto';
/// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([homeUrl]), 'files' = 파일 탐색기 /// 처음 화면: 'home' = 지금처럼 MKV 화면 (기본), 'browser' = 웹 브라우저 ([browser.homeUrl]), 'files' = 파일 탐색기
String startScreen = 'home'; String startScreen = 'home';
/// 화면 (글자 · 버튼) 크기 배율. [uiScale] 은 지금 크기 (위쪽 막대의 − · + 로 바꿈), /// 화면 (글자 · 버튼) 크기 배율. [uiScale] 은 지금 크기 (위쪽 막대의 − · + 로 바꿈),
@ -127,22 +123,11 @@ class AppSettings {
/// 프로그램이 켜져 있을 때 탐색기에서 연 동영상 재생: 'same' 켜져 있는 창에서 / 'new' 새 재생 창 /// 프로그램이 켜져 있을 때 탐색기에서 연 동영상 재생: 'same' 켜져 있는 창에서 / 'new' 새 재생 창
String openFileWindow = 'same'; 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 - 관리자 판단) ── // ── 네트워크 · DNS (사용자 요청 10/10: "DNS 주소를 강제로 내가 설정한 주소로", 기본 8.8.8.8 · 보조 8.8.4.4 - 관리자 판단) ──
@ -253,8 +238,6 @@ class AppSettings {
/// 66: 좌우로 밀 때 끝에서 처음으로 (끄면 끝에서 멈춤) /// 66: 좌우로 밀 때 끝에서 처음으로 (끄면 끝에서 멈춤)
bool swipeWrap = true; bool swipeWrap = true;
/// 66-2: 웹 브라우저에서 화면 옮기기 (위쪽 막대를 좌우로) 안내를 한 번 보였는지
bool browserSwipeHinted = false;
/// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드) /// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드)
List<String> cleanupPrechecked = ['work', 'download']; List<String> cleanupPrechecked = ['work', 'download'];
@ -403,8 +386,11 @@ class AppSettings {
/// 다운로드 · YouTube (yt-dlp · aria2) /// 다운로드 · YouTube (yt-dlp · aria2)
final download = DownloadSettings(); final download = DownloadSettings();
/// 웹 브라우저 (앱 안 · 외부)
final browser = BrowserSettings();
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다 /// 설정 분야들 (설정 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]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다 /// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
Map<String, Object?> toJson({bool plainSecrets = false}) { Map<String, Object?> toJson({bool plainSecrets = false}) {
@ -414,9 +400,6 @@ class AppSettings {
'mkvOutputRoot': mkvOutputRoot, 'mkvOutputRoot': mkvOutputRoot,
'moveTargets': [for (final t in moveTargets) t.toJson()], 'moveTargets': [for (final t in moveTargets) t.toJson()],
'folderBookmarks': [for (final b in folderBookmarks) b.toJson()], 'folderBookmarks': [for (final b in folderBookmarks) b.toJson()],
'youtubeAdSkip': youtubeAdSkip,
'youtubeAdHide': youtubeAdHide,
'webPauseOnLeave': webPauseOnLeave,
'minimizeToTray': minimizeToTray, 'minimizeToTray': minimizeToTray,
'closeAction': closeAction, 'closeAction': closeAction,
'showHotkey': showHotkey, 'showHotkey': showHotkey,
@ -448,12 +431,6 @@ class AppSettings {
'appIcon': appIcon, 'appIcon': appIcon,
'uiLanguagesAdded': uiLanguagesAdded, 'uiLanguagesAdded': uiLanguagesAdded,
'openFileWindow': openFileWindow, 'openFileWindow': openFileWindow,
'homeUrl': homeUrl,
'browserEngine': browserEngine,
'cefWhenElevated': cefWhenElevated,
'externalBrowser': externalBrowser,
'webTranslate': webTranslate,
'webJavaScript': webJavaScript,
'zipLevel': zipLevel, 'zipLevel': zipLevel,
'wifiShareMinutes': wifiShareMinutes, 'wifiShareMinutes': wifiShareMinutes,
'wifiShareTailscale': wifiShareTailscale, 'wifiShareTailscale': wifiShareTailscale,
@ -472,7 +449,6 @@ class AppSettings {
'imageExts': imageExts, 'imageExts': imageExts,
'startMenuShortcut': startMenuShortcut, 'startMenuShortcut': startMenuShortcut,
'swipeWrap': swipeWrap, 'swipeWrap': swipeWrap,
'browserSwipeHinted': browserSwipeHinted,
'cleanupPrechecked': cleanupPrechecked, 'cleanupPrechecked': cleanupPrechecked,
'migrationNotice': migrationNotice, 'migrationNotice': migrationNotice,
'allowInnerToOuter': allowInnerToOuter, 'allowInnerToOuter': allowInnerToOuter,
@ -542,9 +518,6 @@ class AppSettings {
// 예전 판의 이동 폴더 하나 → "이동" 버튼으로 // 예전 판의 이동 폴더 하나 → "이동" 버튼으로
if (j['moveTargets'] == null && j['moveTargetDir'] is String) MoveTarget('이동', j['moveTargetDir'] as String), 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 ..minimizeToTray = j['minimizeToTray'] as bool? ?? true
..closeAction = const ['background', 'quit', 'ask'].contains(j['closeAction']) ? j['closeAction'] as String : 'background' ..closeAction = const ['background', 'quit', 'ask'].contains(j['closeAction']) ? j['closeAction'] as String : 'background'
..showHotkey = j['showHotkey'] as String? ?? 'Ctrl+Shift+X' ..showHotkey = j['showHotkey'] as String? ?? 'Ctrl+Shift+X'
@ -567,12 +540,6 @@ class AppSettings {
? j['screenOrientation'] as String ? j['screenOrientation'] as String
: 'auto' : 'auto'
..openFileWindow = j['openFileWindow'] == 'new' ? 'new' : 'same' ..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 ..vaultEnabled = j['vaultEnabled'] == true
..shizukuEnabled = j['shizukuEnabled'] == true ..shizukuEnabled = j['shizukuEnabled'] == true
..rootEnabled = j['rootEnabled'] == true ..rootEnabled = j['rootEnabled'] == true
@ -602,7 +569,6 @@ class AppSettings {
: [...defaultImageExtensions] : [...defaultImageExtensions]
..startMenuShortcut = j['startMenuShortcut'] != false ..startMenuShortcut = j['startMenuShortcut'] != false
..swipeWrap = j['swipeWrap'] != false ..swipeWrap = j['swipeWrap'] != false
..browserSwipeHinted = j['browserSwipeHinted'] == true
..cleanupPrechecked = j['cleanupPrechecked'] is List ..cleanupPrechecked = j['cleanupPrechecked'] is List
? [for (final x in j['cleanupPrechecked'] as List) '$x'] ? [for (final x in j['cleanupPrechecked'] as List) '$x']
: ['work', 'download'] : ['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) { if (Platform.isAndroid) {
WebProxy.log = c.note; WebProxy.log = c.note;
// 환경 설정 "다른 화면으로 가면 소리 멈춤" 이 꺼져 있으면 일반 브라우저가 뒤에 있어도 DNS 문을 열어 둔다 // 환경 설정 "다른 화면으로 가면 소리 멈춤" 이 꺼져 있으면 일반 브라우저가 뒤에 있어도 DNS 문을 열어 둔다
WebProxy.keepPlayingWhenHidden = () => !c.settings.webPauseOnLeave; WebProxy.keepPlayingWhenHidden = () => !c.settings.browser.pauseOnLeave;
try { try {
await WebProxy.apply(); await WebProxy.apply();
} catch (e) { } catch (e) {
@ -499,10 +499,10 @@ Future<void> setupNetwork(AppController c, String dataDir, {bool second = false}
); );
} }
var last = c.settings.network.config.key; var last = c.settings.network.config.key;
var lastPause = c.settings.webPauseOnLeave; var lastPause = c.settings.browser.pauseOnLeave;
c.addListener(() async { c.addListener(() async {
if (Platform.isAndroid && c.settings.webPauseOnLeave != lastPause) { if (Platform.isAndroid && c.settings.browser.pauseOnLeave != lastPause) {
lastPause = c.settings.webPauseOnLeave; lastPause = c.settings.browser.pauseOnLeave;
await WebProxy.apply(); await WebProxy.apply();
} }
final now = c.settings.network.config.key; final now = c.settings.network.config.key;

View File

@ -207,7 +207,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
/// 207: Chrome 엔진 [다시 시도] 때마다 새로 /// 207: Chrome 엔진 [다시 시도] 때마다 새로
int _cefTry = 0; 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 = ''; String _title = '';
double _progress = 1; double _progress = 1;
@ -355,7 +355,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
static final _translator = WebPageTranslator(); static final _translator = WebPageTranslator();
/// 이 화면에서 번역 중인지. 처음엔 설정을 따르고, 도구 막대의 [번역] 버튼으로 바꾼다. /// 이 화면에서 번역 중인지. 처음엔 설정을 따르고, 도구 막대의 [번역] 버튼으로 바꾼다.
late bool _trOn = widget.c.settings.webTranslate; late bool _trOn = widget.c.settings.browser.translate;
/// 버튼으로 직접 켰다: 이미 화면 언어로 보이는 페이지도 번역한다 /// 버튼으로 직접 켰다: 이미 화면 언어로 보이는 페이지도 번역한다
bool _trForce = false; bool _trForce = false;
@ -374,12 +374,12 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
if (!mounted) return; if (!mounted) return;
_applyCookieExport(); _applyCookieExport();
final s = widget.c.settings; final s = widget.c.settings;
if (s.webJavaScript != _lastJs) { if (s.browser.javaScript != _lastJs) {
_lastJs = s.webJavaScript; _lastJs = s.browser.javaScript;
_nav?.setJavaScript(_lastJs); _nav?.setJavaScript(_lastJs);
} }
if (s.webTranslate != _lastTranslate) { if (s.browser.translate != _lastTranslate) {
_lastTranslate = s.webTranslate; _lastTranslate = s.browser.translate;
_setTranslate(_lastTranslate, force: false); _setTranslate(_lastTranslate, force: false);
} else if (s.uiLanguage != _lastLang && _trOn) { } else if (s.uiLanguage != _lastLang && _trOn) {
_translateNow(); // 다른 언어로 다시 번역 (스크립트가 앞의 번역을 되돌린 뒤 번역) _translateNow(); // 다른 언어로 다시 번역 (스크립트가 앞의 번역을 되돌린 뒤 번역)
@ -448,17 +448,17 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
super.initState(); super.initState();
BrowserPage._open++; BrowserPage._open++;
final s = widget.c.settings; final s = widget.c.settings;
_lastTranslate = s.webTranslate; _lastTranslate = s.browser.translate;
_lastJs = s.webJavaScript; _lastJs = s.browser.javaScript;
_lastLang = s.uiLanguage; _lastLang = s.uiLanguage;
_applyCookieExport(); _applyCookieExport();
widget.c.addListener(_onSettings); widget.c.addListener(_onSettings);
if (_trOn) _translateNow(); if (_trOn) _translateNow();
// 66-2: 브라우저 가운데는 페이지가 좌우로 밀리므로 화면 옮기기는 위쪽 막대에서 - 처음 한 번만 알린다. // 66-2: 브라우저 가운데는 페이지가 좌우로 밀리므로 화면 옮기기는 위쪽 막대에서 - 처음 한 번만 알린다.
// 알림 (SnackBar) 은 다른 알림 (다운로드 등) 을 뒤로 밀어 기다리게 하므로 위쪽 막대 아래의 한 줄로 // 알림 (SnackBar) 은 다른 알림 (다운로드 등) 을 뒤로 밀어 기다리게 하므로 위쪽 막대 아래의 한 줄로
if (s.swipeNav && !s.browserSwipeHinted) { if (s.swipeNav && !s.browser.swipeHinted) {
_swipeHint = true; _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() { void didPushNext() {
_visible = false; _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() { void _applyAdSettings() {
if (!(Uri.tryParse(_url)?.host ?? '').endsWith('youtube.com')) return; if (!(Uri.tryParse(_url)?.host ?? '').endsWith('youtube.com')) return;
final s = widget.c.settings; 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 화면 등으로 가며 브라우저 화면이 닫힌다: 살려 둔 웹뷰에서 소리만 나지 않게 멈춘다 /// MKV 화면 등으로 가며 브라우저 화면이 닫힌다: 살려 둔 웹뷰에서 소리만 나지 않게 멈춘다
@override @override
void didPop() { void didPop() {
if (widget.c.settings.webPauseOnLeave) _nav?.pauseMedia(); if (widget.c.settings.browser.pauseOnLeave) _nav?.pauseMedia();
} }
@override @override
@ -670,7 +670,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
} }
void _openExternal(String url) => 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( Expanded(
child: _withDnsBlock(widget.viewBuilder?.call(_host, _url) ?? child: _withDnsBlock(widget.viewBuilder?.call(_host, _url) ??
// 환경 설정에서 Chrome 을 고르고 엔진이 이번 실행에 준비되어 있으면 Chrome, 아니면 Edge // 환경 설정에서 Chrome 을 고르고 엔진이 이번 실행에 준비되어 있으면 Chrome, 아니면 Edge
(widget.c.settings.browserEngine == 'chrome' && CefRuntime.readyThisRun (widget.c.settings.browser.engine == 'chrome' && CefRuntime.readyThisRun
// 207: 관리자 권한이면 처음 값으로 Chrome 엔진을 시작하지 않는다 (보안) - 이유와 고를 것 // 207: 관리자 권한이면 처음 값으로 Chrome 엔진을 시작하지 않는다 (보안) - 이유와 고를 것
? Elevation.elevated && !widget.c.settings.cefWhenElevated ? Elevation.elevated && !widget.c.settings.browser.cefWhenElevated
? _CefElevatedBlocked(c: widget.c) ? _CefElevatedBlocked(c: widget.c)
// Chrome 엔진은 JavaScript 설정을 만든 뒤에 바꿀 수 없어 바뀌면 새로 만든다 // Chrome 엔진은 JavaScript 설정을 만든 뒤에 바꿀 수 없어 바뀌면 새로 만든다
: _CefView( : _CefView(
key: ValueKey('${widget.c.settings.webJavaScript}|$_cefTry'), key: ValueKey('${widget.c.settings.browser.javaScript}|$_cefTry'),
host: _host, host: _host,
initialUrl: _url, initialUrl: _url,
dataDir: widget.c.settings.webViewDataDir, dataDir: widget.c.settings.webViewDataDir,
javaScript: widget.c.settings.webJavaScript, javaScript: widget.c.settings.browser.javaScript,
// 207: 시작하지 못하면 사용자가 바로 고를 수 있게 // 207: 시작하지 못하면 사용자가 바로 고를 수 있게
onUseEdge: () => widget.c.updateSettings((x) => x.browserEngine = 'edge'), onUseEdge: () => widget.c.updateSettings((x) => x.browser.engine = 'edge'),
onRetry: () => setState(() => _cefTry++), onRetry: () => setState(() => _cefTry++),
cefWhenElevated: widget.c.settings.cefWhenElevated) cefWhenElevated: widget.c.settings.browser.cefWhenElevated)
: _EdgeView( : _EdgeView(
host: _host, host: _host,
initialUrl: _url, initialUrl: _url,
dataDir: widget.c.settings.webViewDataDir, 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 _progress < 1
? btn(Icons.close, tr('중지'), () => _nav?.stop()) ? btn(Icons.close, tr('중지'), () => _nav?.stop())
: btn(Icons.refresh, keyHint(tr('새로고침'), 'F5'), () => _nav?.reload()), : 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), const SizedBox(width: 6),
Expanded( Expanded(
child: TextField( child: TextField(
@ -940,7 +940,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
tooltip: tr('더 보기'), tooltip: tr('더 보기'),
icon: const Icon(Icons.more_vert, size: 20), icon: const Icon(Icons.more_vert, size: 20),
onSelected: (v) => switch (v) { onSelected: (v) => switch (v) {
'home' => _go(widget.c.settings.homeUrl), 'home' => _go(widget.c.settings.browser.homeUrl),
'translate' => _setTranslate(!_trOn, force: true), 'translate' => _setTranslate(!_trOn, force: true),
'external' => _openExternal(_url), 'external' => _openExternal(_url),
'work' => _toggleWork(), 'work' => _toggleWork(),
@ -1349,7 +1349,7 @@ class _CefElevatedBlocked extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
Wrap(spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [ Wrap(spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [
FilledButton( FilledButton(
onPressed: () => c.updateSettings((x) => x.browserEngine = 'edge'), onPressed: () => c.updateSettings((x) => x.browser.engine = 'edge'),
child: Text(tr('Edge 엔진으로 바꾸기')), child: Text(tr('Edge 엔진으로 바꾸기')),
), ),
OutlinedButton( OutlinedButton(

View File

@ -35,7 +35,7 @@ Future<bool> chooseChromeEngine(BuildContext context, AppController c) async {
); );
if (installed != true || !context.mounted) return false; 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 (!context.mounted) return true;
if (!CefRuntime.readyThisRun) { if (!CefRuntime.readyThisRun) {
final restart = await showDialog<bool>( final restart = await showDialog<bool>(
@ -147,7 +147,7 @@ Future<bool> confirmCefWhenElevated(BuildContext context, AppController c) async
), ),
); );
if (ok != true) return false; if (ok != true) return false;
await c.updateSettings((x) => x.cefWhenElevated = true); await c.updateSettings((x) => x.browser.cefWhenElevated = true);
c.note(tr('관리자 권한일 때도 Chrome 엔진 쓰기를 켰습니다 (권장하지 않음).')); c.note(tr('관리자 권한일 때도 Chrome 엔진 쓰기를 켰습니다 (권장하지 않음).'));
return true; return true;
} }

View File

@ -244,7 +244,7 @@ class _DnsSettingsState extends State<DnsSettings> {
if (!desk) tr('· Android 의 yt-dlp 새 버전 확인 (다운로드 도구 안에서 따로 연결)'), if (!desk) tr('· Android 의 yt-dlp 새 버전 확인 (다운로드 도구 안에서 따로 연결)'),
].join('\n')), ].join('\n')),
), ),
if (desk && s.browserEngine == 'chrome' && CefRuntime.readyThisRun) if (desk && s.browser.engine == 'chrome' && CefRuntime.readyThisRun)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Text(tr('Chrome 엔진은 앱을 다시 시작하면 바뀐 설정을 씁니다.'), style: TextStyle(fontSize: 12, color: JjColors.textDim)), 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) { Widget build(BuildContext context) {
final s = c.settings; final s = c.settings;
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [
settingsTextTile(tr('홈 주소'), s.homeUrl, tr('웹 브라우저의 시작 · 🏠 주소'), settingsTextTile(tr('홈 주소'), s.browser.homeUrl, tr('웹 브라우저의 시작 · 🏠 주소'),
(v) => c.updateSettings((x) => x.homeUrl = v.trim().isEmpty ? 'https://www.youtube.com/' : v.trim())), (v) => c.updateSettings((x) => x.browser.homeUrl = v.trim().isEmpty ? 'https://www.youtube.com/' : v.trim())),
SwitchListTile( SwitchListTile(
value: s.youtubeAdSkip, value: s.browser.adSkip,
onChanged: (v) => c.updateSettings((x) => x.youtubeAdSkip = v), onChanged: (v) => c.updateSettings((x) => x.browser.adSkip = v),
title: Text(tr('YouTube 광고 자동 건너뛰기')), title: Text(tr('YouTube 광고 자동 건너뛰기')),
subtitle: Text(tr('앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 ' '(넘기는 동안 화면 위쪽에 "광고 건너뛰는 중…")')), subtitle: Text(tr('앱 안 웹 브라우저: [건너뛰기] 를 대신 누르고, 건너뛸 수 없는 광고는 소리를 끄고 빨리 넘깁니다 ' '(넘기는 동안 화면 위쪽에 "광고 건너뛰는 중…")')),
), ),
SwitchListTile( SwitchListTile(
value: s.youtubeAdHide, value: s.browser.adHide,
onChanged: (v) => c.updateSettings((x) => x.youtubeAdHide = v), onChanged: (v) => c.updateSettings((x) => x.browser.adHide = v),
title: Text(tr('YouTube 광고 배너 숨기기')), title: Text(tr('YouTube 광고 배너 숨기기')),
subtitle: Text(tr('목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다')), subtitle: Text(tr('목록 · 영상 옆 · 영상 위에 나오는 광고 영역을 감춥니다')),
), ),
SwitchListTile( SwitchListTile(
value: s.webPauseOnLeave, value: s.browser.pauseOnLeave,
onChanged: (v) => c.updateSettings((x) => x.webPauseOnLeave = v), onChanged: (v) => c.updateSettings((x) => x.browser.pauseOnLeave = v),
title: Text(tr('다른 화면으로 가면 페이지 소리 멈춤')), title: Text(tr('다른 화면으로 가면 페이지 소리 멈춤')),
subtitle: Text(tr('끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다')), subtitle: Text(tr('끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다')),
), ),
SwitchListTile( SwitchListTile(
value: s.webTranslate, value: s.browser.translate,
onChanged: (v) => c.updateSettings((x) => x.webTranslate = v), onChanged: (v) => c.updateSettings((x) => x.browser.translate = v),
title: Text(tr('웹 페이지 자동 번역')), title: Text(tr('웹 페이지 자동 번역')),
subtitle: Text(trf( subtitle: Text(trf(
'다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. ' '다른 언어로 된 페이지를 화면 언어 ({0}) 로 번역합니다. 페이지의 글을 Google 번역으로 보냅니다. '
@ -62,8 +62,8 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
[I18nController.nativeName(s.uiLanguage)])), [I18nController.nativeName(s.uiLanguage)])),
), ),
SwitchListTile( SwitchListTile(
value: s.webJavaScript, value: s.browser.javaScript,
onChanged: (v) => c.updateSettings((x) => x.webJavaScript = v), onChanged: (v) => c.updateSettings((x) => x.browser.javaScript = v),
title: Text(tr('JavaScript 사용')), title: Text(tr('JavaScript 사용')),
subtitle: Text(desk subtitle: Text(desk
? tr('끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). ' ? tr('끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). '
@ -75,10 +75,10 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
SettingTile( SettingTile(
title: Text(tr('브라우저 엔진 (앱 안)')), title: Text(tr('브라우저 엔진 (앱 안)')),
subtitle: Text( 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, isThreeLine: true,
trailing: Row(mainAxisSize: MainAxisSize.min, children: [ trailing: Row(mainAxisSize: MainAxisSize.min, children: [
if (s.browserEngine == 'edge' && CefRuntime.installed && !CefRuntime.readyThisRun) if (s.browser.engine == 'edge' && CefRuntime.installed && !CefRuntime.readyThisRun)
IconButton( IconButton(
tooltip: tr('내려받은 Chrome 엔진 지우기'), tooltip: tr('내려받은 Chrome 엔진 지우기'),
icon: Icon(Icons.delete_outline, color: JjColors.textDim), icon: Icon(Icons.delete_outline, color: JjColors.textDim),
@ -89,7 +89,7 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
}, },
), ),
DropdownButton<String>( DropdownButton<String>(
value: s.browserEngine, value: s.browser.engine,
items: [ items: [
DropdownMenuItem(value: 'edge', child: Text(tr('Edge (내장)'))), DropdownMenuItem(value: 'edge', child: Text(tr('Edge (내장)'))),
DropdownMenuItem( DropdownMenuItem(
@ -101,7 +101,7 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
await chooseChromeEngine(context, c); await chooseChromeEngine(context, c);
if (mounted) setState(() {}); if (mounted) setState(() {});
} else { } 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 엔진 쓰기 (처음 꺼짐, 켤 때 경고) // 207: 관리자 권한일 때도 Chrome 엔진 쓰기 (처음 꺼짐, 켤 때 경고)
SwitchListTile( SwitchListTile(
value: s.cefWhenElevated, value: s.browser.cefWhenElevated,
onChanged: (v) async { onChanged: (v) async {
if (v) { if (v) {
await confirmCefWhenElevated(context, c); await confirmCefWhenElevated(context, c);
} else { } else {
await c.updateSettings((x) => x.cefWhenElevated = false); await c.updateSettings((x) => x.browser.cefWhenElevated = false);
} }
if (mounted) setState(() {}); if (mounted) setState(() {});
}, },
@ -127,10 +127,10 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
title: Text(tr('외부 브라우저')), title: Text(tr('외부 브라우저')),
subtitle: Text(tr('"외부 브라우저로 열기" 에 쓸 브라우저 (이 PC 에 설치된 것)')), subtitle: Text(tr('"외부 브라우저로 열기" 에 쓸 브라우저 (이 PC 에 설치된 것)')),
trailing: DropdownButton<String>( trailing: DropdownButton<String>(
value: s.externalBrowser, value: s.browser.external,
items: [ items: [
DropdownMenuItem(value: 'system', child: Text(tr('Windows 기본 브라우저'))), 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) { DropdownMenuItem(value: b, child: Text(switch (b) {
'chrome' => 'Chrome', 'chrome' => 'Chrome',
'firefox' => 'Firefox', 'firefox' => 'Firefox',
@ -139,7 +139,7 @@ class _BrowserSettingsSectionState extends State<BrowserSettingsSection> {
_ => b, _ => 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(); await tester.pumpAndSettle();
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsOneWidget); expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsOneWidget);
expect(find.byType(SnackBar), findsNothing, reason: '알림 (다운로드 등) 을 뒤로 밀지 않게 알림이 아니라 한 줄'); expect(find.byType(SnackBar), findsNothing, reason: '알림 (다운로드 등) 을 뒤로 밀지 않게 알림이 아니라 한 줄');
expect(c.settings.browserSwipeHinted, isTrue); expect(c.settings.browser.swipeHinted, isTrue);
await tester.tap(find.byTooltip('안내 닫기')); await tester.tap(find.byTooltip('안내 닫기'));
await tester.pump(); await tester.pump();
expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing); expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing);

View File

@ -24,7 +24,7 @@ void main() {
dir = Directory.systemTemp.createTempSync('jj_cef_elev_'); dir = Directory.systemTemp.createTempSync('jj_cef_elev_');
c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings c.settings
..browserEngine = 'chrome' ..browser.engine = 'chrome'
..webViewDataDir = p.join(dir.path, 'webview'); ..webViewDataDir = p.join(dir.path, 'webview');
bm = BookmarksController(p.join(dir.path, 'bookmarks.json')); bm = BookmarksController(p.join(dir.path, 'bookmarks.json'));
CefRuntime.readyThisRun = true; CefRuntime.readyThisRun = true;
@ -52,7 +52,7 @@ void main() {
expect(find.text('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'), findsOneWidget); expect(find.text('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'), findsOneWidget);
await t.tap(find.text('Edge 엔진으로 바꾸기')); await t.tap(find.text('Edge 엔진으로 바꾸기'));
await t.pump(); await t.pump();
expect(c.settings.browserEngine, 'edge'); expect(c.settings.browser.engine, 'edge');
await t.pumpWidget(const SizedBox()); await t.pumpWidget(const SizedBox());
}); });
@ -64,12 +64,12 @@ void main() {
expect(find.textContaining('샌드박스 (웹 페이지를 가두는 보호) 가 꺼져'), findsOneWidget); expect(find.textContaining('샌드박스 (웹 페이지를 가두는 보호) 가 꺼져'), findsOneWidget);
await t.tap(find.text('취소')); await t.tap(find.text('취소'));
await t.pumpAndSettle(); await t.pumpAndSettle();
expect(c.settings.cefWhenElevated, isFalse); expect(c.settings.browser.cefWhenElevated, isFalse);
await t.tap(find.text('그래도 Chrome 엔진 쓰기…')); await t.tap(find.text('그래도 Chrome 엔진 쓰기…'));
await t.pumpAndSettle(); await t.pumpAndSettle();
await t.tap(find.text('그래도 쓰기')); await t.tap(find.text('그래도 쓰기'));
await t.pumpAndSettle(); await t.pumpAndSettle();
expect(c.settings.cefWhenElevated, isTrue); expect(c.settings.browser.cefWhenElevated, isTrue);
expect(find.text('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'), findsNothing); expect(find.text('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'), findsNothing);
await t.pumpWidget(const SizedBox()); await t.pumpWidget(const SizedBox());
}); });
@ -86,13 +86,13 @@ void main() {
expect(find.text('다시 시도'), findsOneWidget); expect(find.text('다시 시도'), findsOneWidget);
await t.tap(find.text('Edge 엔진으로 바꾸기')); await t.tap(find.text('Edge 엔진으로 바꾸기'));
await t.pump(); await t.pump();
expect(c.settings.browserEngine, 'edge'); expect(c.settings.browser.engine, 'edge');
await t.pumpWidget(const SizedBox()); await t.pumpWidget(const SizedBox());
}); });
test('설정: 관리자 권한일 때 Chrome 엔진 쓰기는 처음 꺼짐 · 저장 · 틀린 값은 꺼짐', () { test('설정: 관리자 권한일 때 Chrome 엔진 쓰기는 처음 꺼짐 · 저장 · 틀린 값은 꺼짐', () {
expect(AppSettings().cefWhenElevated, isFalse); expect(AppSettings().browser.cefWhenElevated, isFalse);
expect(AppSettings.fromJson((AppSettings()..cefWhenElevated = true).toJson()).cefWhenElevated, isTrue); expect(AppSettings.fromJson((AppSettings()..browser.cefWhenElevated = true).toJson()).browser.cefWhenElevated, isTrue);
expect(AppSettings.fromJson({'cefWhenElevated': 'yes'}).cefWhenElevated, isFalse); expect(AppSettings.fromJson({'cefWhenElevated': 'yes'}).browser.cefWhenElevated, isFalse);
}); });
} }

View File

@ -175,8 +175,8 @@ void main() {
}); });
test('66-2: 웹 브라우저 화면 옮기기 안내는 한 번만 (설정에 남음)', () { test('66-2: 웹 브라우저 화면 옮기기 안내는 한 번만 (설정에 남음)', () {
expect(AppSettings().browserSwipeHinted, isFalse); expect(AppSettings().browser.swipeHinted, isFalse);
expect(AppSettings.fromJson((AppSettings()..browserSwipeHinted = true).toJson()).browserSwipeHinted, isTrue); expect(AppSettings.fromJson((AppSettings()..browser.swipeHinted = true).toJson()).browser.swipeHinted, isTrue);
}); });
testWidgets('174: MKV 화면이 맨 처음 화면이 아니어도 (홈 화면 = 탐색기 등) MKV 아이콘이 "지금 여기"', (tester) async { testWidgets('174: MKV 화면이 맨 처음 화면이 아니어도 (홈 화면 = 탐색기 등) MKV 아이콘이 "지금 여기"', (tester) async {

View File

@ -34,7 +34,7 @@ void main() {
expect(s.tripleTapAction, 'move'); expect(s.tripleTapAction, 'move');
expect(s.davExternalOpen, 'ask'); 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)); everyElement(isTrue));
// 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22) // 탐색기 처음 배치 = 두 창 (규칙 7 · 관리자 결정 3, 10/22)
expect([s.copy.liveSyncOnStart, s.copy.liveSyncIntervalSec, s.explorer.layout, s.reader.zipComic], ['auto', 30, 'dual', false]); 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.tap(find.text('닫기'));
await tester.pumpAndSettle(); 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(); await tester.pumpAndSettle();
expect(find.text('새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.'), findsNothing); expect(find.text('새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.'), findsNothing);
DavRegistry.configure([]); DavRegistry.configure([]);

View File

@ -63,7 +63,7 @@ void main() {
// 예전 판: 모르는 항목을 지우고 저장 // 예전 판: 모르는 항목을 지우고 저장
File(file).writeAsStringSync(jsonEncode({'homeUrl': 'https://old.example'})); File(file).writeAsStringSync(jsonEncode({'homeUrl': 'https://old.example'}));
final s = await store().load(); 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.url, 'https://nas');
expect(s.webdavServers.single.password, 'pw'); expect(s.webdavServers.single.password, 'pw');
expect(s.openSubtitlesKey, 'KEY-2'); expect(s.openSubtitlesKey, 'KEY-2');
@ -104,7 +104,7 @@ void main() {
final st = store(); final st = store();
final s = await st.load(); final s = await st.load();
expect(s.openSubtitlesKey, 'KEY-3'); 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('KEY-3'));
expect(text(file), contains('plain')); expect(text(file), contains('plain'));
expect(secrets.values, {'dav.pw.d1': 'kept'}); expect(secrets.values, {'dav.pw.d1': 'kept'});

View File

@ -21,14 +21,14 @@ void main() {
test('저장: 임시 파일에 쓴 뒤 바꿔치기 · 직전 정상본은 .bak · 남는 임시 파일 없음', () async { test('저장: 임시 파일에 쓴 뒤 바꿔치기 · 직전 정상본은 .bak · 남는 임시 파일 없음', () async {
final store = SettingsStore(file); final store = SettingsStore(file);
await store.save(AppSettings()..homeUrl = 'https://a.example'); await store.save(AppSettings()..browser.homeUrl = 'https://a.example');
await store.save(AppSettings()..homeUrl = 'https://b.example'); await store.save(AppSettings()..browser.homeUrl = 'https://b.example');
expect((await SettingsStore(file).load()).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.bak').readAsStringSync(), contains('https://a.example'));
expect(File('$file.tmp').existsSync(), isFalse); expect(File('$file.tmp').existsSync(), isFalse);
// 여러 번 빠르게 저장해도 마지막 것이 남는다 (순서대로) // 여러 번 빠르게 저장해도 마지막 것이 남는다 (순서대로)
await Future.wait([for (var i = 0; i < 5; i++) store.save(AppSettings()..homeUrl = 'https://$i.example')]); await Future.wait([for (var i = 0; i < 5; i++) store.save(AppSettings()..browser.homeUrl = 'https://$i.example')]);
expect((await SettingsStore(file).load()).homeUrl, 'https://4.example'); expect((await SettingsStore(file).load()).browser.homeUrl, 'https://4.example');
}); });
test('관리자 지시 (10/21): 보통 저장은 이 판이 만드는 꼴의 열쇠만 지운다 - 모르는 꼴 (새 판의 dav.token.x 등) 은 남김 · 지운 서버의 아는 열쇠만', () async { test('관리자 지시 (10/21): 보통 저장은 이 판이 만드는 꼴의 열쇠만 지운다 - 모르는 꼴 (새 판의 dav.token.x 등) 은 남김 · 지운 서버의 아는 열쇠만', () async {
@ -98,11 +98,11 @@ void main() {
test('깨진 설정 파일: 백업으로 되살리고, 깨진 파일은 보관 · 알림 정보', () async { test('깨진 설정 파일: 백업으로 되살리고, 깨진 파일은 보관 · 알림 정보', () async {
final store = SettingsStore(file); final store = SettingsStore(file);
await store.save(AppSettings()..homeUrl = 'https://good.example'); await store.save(AppSettings()..browser.homeUrl = 'https://good.example');
await store.save(AppSettings()..homeUrl = 'https://good2.example'); // .bak = good await store.save(AppSettings()..browser.homeUrl = 'https://good2.example'); // .bak = good
File(file).writeAsStringSync('{"homeUrl": "https://half'); // 반쯤 쓰다 끊긴 파일 File(file).writeAsStringSync('{"homeUrl": "https://half'); // 반쯤 쓰다 끊긴 파일
final s = await store.load(); final s = await store.load();
expect(s.homeUrl, 'https://good.example'); expect(s.browser.homeUrl, 'https://good.example');
expect(store.problem!.restoredFromBackup, isTrue); expect(store.problem!.restoredFromBackup, isTrue);
final kept = store.problem!.brokenCopy!; final kept = store.problem!.brokenCopy!;
expect(File(kept).readAsStringSync(), '{"homeUrl": "https://half'); expect(File(kept).readAsStringSync(), '{"homeUrl": "https://half');
@ -115,7 +115,7 @@ void main() {
File(file).writeAsStringSync('not json'); File(file).writeAsStringSync('not json');
final store = SettingsStore(file); final store = SettingsStore(file);
final s = await store.load(); final s = await store.load();
expect(s.homeUrl, AppSettings().homeUrl); expect(s.browser.homeUrl, AppSettings().browser.homeUrl);
expect(store.problem!.restoredFromBackup, isFalse); expect(store.problem!.restoredFromBackup, isFalse);
expect(File(store.problem!.brokenCopy!).readAsStringSync(), 'not json'); expect(File(store.problem!.brokenCopy!).readAsStringSync(), 'not json');
@ -123,7 +123,7 @@ void main() {
File(file).deleteSync(); File(file).deleteSync();
File('$file.bak').writeAsStringSync('{"homeUrl": "https://bak.example"}'); File('$file.bak').writeAsStringSync('{"homeUrl": "https://bak.example"}');
final again = SettingsStore(file); 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); expect(again.problem!.restoredFromBackup, isTrue);
// 정상이면 알림 없음 // 정상이면 알림 없음
await again.save(AppSettings()); await again.save(AppSettings());

View File

@ -96,13 +96,13 @@ void main() {
test('설정: 번역 (기본 끔) · JavaScript (기본 켬) 저장 · 읽기', () { test('설정: 번역 (기본 끔) · JavaScript (기본 켬) 저장 · 읽기', () {
final d = AppSettings(); final d = AppSettings();
expect(d.webTranslate, isFalse); expect(d.browser.translate, isFalse);
expect(d.webJavaScript, isTrue); expect(d.browser.javaScript, isTrue);
final s = AppSettings.fromJson(jsonDecode(jsonEncode((AppSettings() final s = AppSettings.fromJson(jsonDecode(jsonEncode((AppSettings()
..webTranslate = true ..browser.translate = true
..webJavaScript = false) ..browser.javaScript = false)
.toJson())) as Map<String, dynamic>); .toJson())) as Map<String, dynamic>);
expect(s.webTranslate, isTrue); expect(s.browser.translate, isTrue);
expect(s.webJavaScript, isFalse); expect(s.browser.javaScript, isFalse);
}); });
} }

View File

@ -46,12 +46,12 @@ console.log(JSON.stringify(r));
void main() { void main() {
test('설정: 광고 건너뛰기 · 숨기기 기본 켜짐, 저장', () { test('설정: 광고 건너뛰기 · 숨기기 기본 켜짐, 저장', () {
final s = AppSettings(); final s = AppSettings();
expect([s.youtubeAdSkip, s.youtubeAdHide], [true, true]); expect([s.browser.adSkip, s.browser.adHide], [true, true]);
s s
..youtubeAdSkip = false ..browser.adSkip = false
..youtubeAdHide = false; ..browser.adHide = false;
final back = AppSettings.fromJson(s.toJson()); 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}')); expect(youtubeAdScript(skip: false, hide: true), contains('{skip: false, hide: true}'));
}); });