diff --git a/lib/app/net_guard.dart b/lib/app/net_guard.dart index f9ef161..4861fe6 100644 --- a/lib/app/net_guard.dart +++ b/lib/app/net_guard.dart @@ -37,6 +37,9 @@ class NetGuard { /// 249: 서버가 다시 답하거나 DNS 설정을 바꿀 때까지 남는다 - 아래 안내를 놓쳐도 환경 설정 > 네트워크 · DNS 와 작업 현황에 보인다. final failure = ValueNotifier(null); + /// 248: 아래 알림을 띄우지 않을 화면이 열려 있는 수 (브라우저의 DNS 덮개 · 환경 설정의 DNS 칸 - 같은 안내 · 버튼을 이미 보인다) + final noticeBlockers = ValueNotifier(0); + /// 작업 기록 void Function(String)? log; diff --git a/lib/main.dart b/lib/main.dart index 867a8d4..f321a7e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -57,6 +57,7 @@ import 'ui/downloads_page.dart'; import 'ui/exit_dialog.dart'; import 'ui/home_page.dart'; import 'ui/monitor_page.dart' show askLiveSyncStart, MonitorPage; +import 'ui/network_settings.dart' show DnsFailureNotice; import 'ui/player_page.dart'; import 'ui/setup_dialog.dart'; import 'ui/update_dialog.dart'; @@ -503,13 +504,15 @@ void watchDnsFailure(GlobalKey messengerKey, AppControll NetGuard.instance.failure.addListener(() { final f = NetGuard.instance.failure.value; if (f == null) return; + // 248: 같은 안내를 보이는 화면 (브라우저 덮개 · 설정의 DNS 칸) 이 열려 있으면 띄우지 않는다 (상태는 설정 · 작업 기록 줄에) + if (NetGuard.instance.noticeBlockers.value > 0) return; messengerKey.currentState?.showSnackBar(SnackBar( // 248: 버튼이 있는 안내라 사용자가 닫거나 누를 때까지 둔다 (한 번만 뜨고, 상태는 설정 · 작업 현황에 남음 - 249) persist: true, showCloseIcon: true, - // 사용자에게는 무엇이 안 되고 무엇을 할지만 (예외 원문은 작업 기록에 - NetGuard.reportError) - content: Text(trf('DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.', - [f.server])), + // 사용자에게는 무엇이 안 되고 무엇을 할지만 (예외 원문은 작업 기록에 - NetGuard.reportError). + // 상태가 풀리거나 같은 안내를 보이는 화면이 열리면 스스로 닫힌다 (248) + content: DnsFailureNotice(failure: f), action: SnackBarAction( label: tr('시스템 DNS 로 바꾸기'), onPressed: () => c.updateSettings((x) => x.dnsMode = 'system'), diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index 717ecd5..e7eda26 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -215,10 +215,18 @@ class _BrowserPageState extends State with RouteAware { /// [error]: 엔진이 알려 준 본문 오류. 프록시 터널 실패이고 DNS 서버에 지금 닿지 않는 중이면, 그 이름의 실패 기록이 없어도 /// (Chromium 이 실패한 프록시를 기억해 다시 묻지 않는 때 등) DNS 때문으로 본다. /// 덮개는 여기서 걷지 않는다 (새로 읽기 시작 · [다시 시도] 에서) - Android 는 오류 뒤에 읽기 끝이 또 온다. + /// 248: 덮개가 보이는 동안은 아래 알림을 띄우지 않는다 (같은 안내 · [시스템 DNS 로 바꾸기] 가 두 개 보였다 - a4 933) + void _setDnsBlocked((String, String)? v) { + final g = NetGuard.instance.noticeBlockers; + if (_dnsBlocked == null && v != null) g.value++; + if (_dnsBlocked != null && v == null) g.value--; + _dnsBlocked = v; + } + /// 250: 사용자가 다른 곳으로 가거나 다시 읽음 - 덮개를 걷는다 (다시 실패하면 또 뜬다) void _userNavigated() { _loadStartAt = DateTime.now(); - if (_dnsBlocked != null && mounted) setState(() => _dnsBlocked = null); + if (_dnsBlocked != null && mounted) setState(() => _setDnsBlocked(null)); } void _checkDnsBlocked({String? host, String? error}) { @@ -233,7 +241,7 @@ class _BrowserPageState extends State with RouteAware { if (next == _dnsBlocked) return; // 기기에서 "덮개를 띄웠는데 안 보임" 과 "띄우지 않음" 을 가릴 수 있게 widget.c.note(trf('DNS 안내 덮개를 띄움 ({0} · DNS {1})', [h, server])); - setState(() => _dnsBlocked = next); + setState(() => _setDnsBlocked(next)); } /// 250: DNS 실패면 웹 뷰 위에 덮는다 ([시스템 DNS 로 바꾸기] [다시 시도]). @@ -242,7 +250,7 @@ class _BrowserPageState extends State with RouteAware { Widget _withDnsBlock(Widget view) { final b = _dnsBlocked; void retry() { - setState(() => _dnsBlocked = null); + setState(() => _setDnsBlocked(null)); _nav?.reload(); } @@ -595,6 +603,8 @@ class _BrowserPageState extends State with RouteAware { @override void dispose() { + // 화면을 닫는 중에는 다른 위젯 (아래 알림) 을 바꾸지 않게 다음 차례에 + if (_dnsBlocked != null) scheduleMicrotask(() => NetGuard.instance.noticeBlockers.value--); browserRouteObserver.unsubscribe(this); BrowserPage._open--; widget.c.removeListener(_onSettings); diff --git a/lib/ui/network_settings.dart b/lib/ui/network_settings.dart index 678299f..0bea1ac 100644 --- a/lib/ui/network_settings.dart +++ b/lib/ui/network_settings.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -42,6 +43,50 @@ class DnsFailureLine extends StatelessWidget { ); } +/// 248: 아래 알림 (SnackBar) 의 내용. 스스로 상태를 듣다가 DNS 가 다시 답하거나 설정을 바꿔 상태가 풀리면, +/// 또는 같은 안내를 보이는 화면 (브라우저 덮개 · 설정의 DNS 칸) 이 열리면 알림을 닫는다. +/// (알림 닫기는 "지금 보이는 알림" 에만 해야 해서, 보이는 동안 그려지는 내용 위젯이 스스로 닫는다) +class DnsFailureNotice extends StatefulWidget { + final DnsFailure failure; + const DnsFailureNotice({super.key, required this.failure}); + + @override + State createState() => _DnsFailureNoticeState(); +} + +class _DnsFailureNoticeState extends State { + final g = NetGuard.instance; + bool _closing = false; + + @override + void initState() { + super.initState(); + g.failure.addListener(_check); + g.noticeBlockers.addListener(_check); + // 대기열에 있다가 차례가 왔을 때 이미 풀렸으면 바로 닫는다 + WidgetsBinding.instance.addPostFrameCallback((_) => _check()); + } + + @override + void dispose() { + g.failure.removeListener(_check); + g.noticeBlockers.removeListener(_check); + super.dispose(); + } + + void _check() { + if (_closing || !mounted) return; + if (g.failure.value == widget.failure && g.noticeBlockers.value == 0) return; + _closing = true; + ScaffoldMessenger.maybeOf(context)?.hideCurrentSnackBar(); + } + + @override + Widget build(BuildContext context) => Text(trf( + 'DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.', + [widget.failure.server])); +} + /// 환경 설정 > 네트워크 · DNS (사용자 요청 10/10): DNS 서버 강제 (처음: 직접 지정 8.8.8.8 · 8.8.4.4) class DnsSettings extends StatefulWidget { final AppController c; @@ -59,6 +104,20 @@ class _DnsSettingsState extends State { Future _save(void Function(AppSettings x) f) => c.updateSettings(f); + // 248: 이 칸이 보이는 동안은 아래 알림을 띄우지 않는다 (맨 위 줄 [DnsFailureLine] 이 같은 안내를 하고, 알림이 칸을 가렸다) + @override + void initState() { + super.initState(); + NetGuard.instance.noticeBlockers.value++; + } + + @override + void dispose() { + // 화면을 닫는 중에는 다른 위젯을 바꾸지 않게 다음 차례에 + scheduleMicrotask(() => NetGuard.instance.noticeBlockers.value--); + super.dispose(); + } + /// [지금 확인]: 지금 설정으로 이름 하나를 찾아 본다 (어느 서버가 답했는지) Future _check() async { setState(() { diff --git a/test/browser_test.dart b/test/browser_test.dart index 50d03ff..9a52fe7 100644 --- a/test/browser_test.dart +++ b/test/browser_test.dart @@ -331,7 +331,10 @@ void main() { final dir = Directory.systemTemp.createTempSync('jj_browser_dns_'); addTearDown(() => dir.deleteSync(recursive: true)); final g = NetGuard.instance; - addTearDown(() => g.failure.value = null); + addTearDown(() { + g.failure.value = null; + g.noticeBlockers.value = 0; + }); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); final bm = BookmarksController(p.join(dir.path, 'bookmarks.json')); late BrowserHost host; @@ -360,12 +363,15 @@ void main() { expect(find.text(msg), findsOneWidget); // 덮개를 띄워도 웹 뷰는 그대로 (새로 만들면 페이지를 다시 읽어 덮개가 걷혔다 - 931) expect(_Probe.created, 0); + // 248: 덮개가 보이는 동안은 아래 알림을 막는다 + expect(g.noticeBlockers.value, 1); // [다시 시도]: 덮개를 걷고 다시 읽기 await tester.tap(find.text('다시 시도')); await tester.pump(); expect(find.text(msg), findsNothing); expect(nav.reloads, 1); expect(_Probe.created, 0); + expect(g.noticeBlockers.value, 0); // 다른 이름은 막지 않는다 · 서버가 다시 답한 뒤에는 지난 실패로 막지 않는다 host.onProgress(0.1); host.onUrl('https://ok.example/'); diff --git a/test/tor_test.dart b/test/tor_test.dart index a2cf492..b2b3654 100644 --- a/test/tor_test.dart +++ b/test/tor_test.dart @@ -14,6 +14,7 @@ import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/app/tor_bundle.dart'; import 'package:jj_mkvmaker/app/tor_service.dart'; import 'package:jj_mkvmaker/core/tor_control.dart'; +import 'package:jj_mkvmaker/main.dart' show watchDnsFailure; import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart'; import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart'; import 'package:jj_mkvmaker/services/platform_services.dart'; @@ -363,6 +364,61 @@ void main() { expect(find.textContaining('Tor 대기 중'), findsNothing); }); + testWidgets('248: 아래 알림 - 상태가 풀리면 닫힘 · 같은 안내를 보이는 화면 (덮개 · 설정 DNS 칸) 이 열려 있으면 띄우지 않고, 열리면 닫힘', (t) async { + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final g = NetGuard.instance; + addTearDown(() { + g.failure.value = null; + g.noticeBlockers.value = 0; + }); + final key = GlobalKey(); + watchDnsFailure(key, c); + await t.pumpWidget(MaterialApp(scaffoldMessengerKey: key, home: const Scaffold(body: Text('home')))); + const text = 'DNS 서버 (192.0.2.1) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.'; + Future settle() async { + for (var i = 0; i < 10; i++) { + await t.pump(const Duration(milliseconds: 100)); + } + } + + // 1) 뜬 뒤 상태가 풀리면 (정한 서버가 다시 답함 · 설정을 바꿈) 바로 닫힌다 + g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now()); + await settle(); + expect(find.text(text), findsOneWidget); + await t.pump(const Duration(minutes: 5)); + expect(find.text(text), findsOneWidget, reason: '버튼이 있는 안내는 저절로 사라지지 않는다'); + g.failure.value = null; + await settle(); + expect(find.text(text), findsNothing); + + // 2) 같은 안내를 보이는 화면이 열려 있으면 띄우지 않는다 + g.noticeBlockers.value = 1; + g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now()); + await settle(); + expect(find.text(text), findsNothing); + g.failure.value = null; + g.noticeBlockers.value = 0; + + // 3) 떠 있는 동안 그런 화면이 열리면 닫힌다 + g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now()); + await settle(); + expect(find.text(text), findsOneWidget); + g.noticeBlockers.value = 1; + await settle(); + expect(find.text(text), findsNothing); + }); + + testWidgets('248: 환경 설정의 DNS 칸 · 브라우저 덮개가 열려 있는 동안 알림을 막는 수', (t) async { + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final g = NetGuard.instance; + expect(g.noticeBlockers.value, 0); + await t.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: DnsSettings(c: c))))); + expect(g.noticeBlockers.value, 1); + await t.pumpWidget(const SizedBox()); + await t.pump(); + expect(g.noticeBlockers.value, 0); + }); + testWidgets('249: DNS 서버에 닿지 않는 동안 환경 설정 > 네트워크 · DNS 에 상태 줄 · [시스템 DNS 로 바꾸기]', (t) async { final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); final g = NetGuard.instance;