모르는 구성 요소 보존 (225) · Tor 대기 표시 (226) · 막은 파일 주소 되돌림 (227) · 포트 다시 잡기

- 225 (기기 확인 a4): Tor 를 모르는 판을 거치면 구성 요소 'tor' 가 지워지던 것. 설정을 읽을 때 components · navOrder 의
  모르는 id 를 버리지 않고 그대로 둔다 (40 과 같은 원칙). 화면 · 버튼은 아는 것만. 화면 순서를 바꿔 저장할 때도 모르는 id 를 남긴다.
- 226: Tor 화면을 떠난 뒤 tor 를 켜 두는 동안 "Tor 대기 중 (n분 뒤 끔)" 을 환경 설정 > Tor 와 일반 브라우저 띠에 [지금 끄기] 와 함께.
- 227: Tor 화면에서 막은 파일 링크 뒤 주소 칸을 마지막으로 보인 페이지 주소로 되돌린다.
- 포트 (관리자 지적): Windows 는 TCP · UDP 마다 막아 둔 포트 범위 (Hyper-V · WSL) 가 있어 번호를 정해 bind 하면 10013 이 날 수 있다.
  시험의 가짜 DNS 는 TCP 를 OS 가 고른 번호로 먼저 열고 그 번호로 UDP (안 되면 다시). aria2 는 넘긴 번호를 열지 못해 끝나면 새 번호로 다시 (2번).
  앱 안 프록시 · Tor 다리 (같은 번호 → 실패하면 OS 가 고름), Tor (auto), Android 웹 뷰 문 (0) 은 이미 그렇다.
- analysis_options: packages/whisper_ggml 도 분석에서 뺌 (자기 analysis_options 가 flutter_lints 를 찾지 못함).
- 시험: components_test 1 · tor_test 2 (226 · 227).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tor_dns
RyuJongJin 2026-10-11 22:44:06 +09:00
parent 697235073e
commit e4b6e33a97
13 changed files with 163 additions and 12 deletions

View File

@ -18,6 +18,7 @@ analyzer:
- packages/flutter_inappwebview_android/** - packages/flutter_inappwebview_android/**
- packages/ffmpeg_kit_flutter_new_min_gpl/** # 고친 외부 부품 (원본 코드 그대로) - packages/ffmpeg_kit_flutter_new_min_gpl/** # 고친 외부 부품 (원본 코드 그대로)
- packages/flutter_inappwebview_windows/** # 고친 외부 부품 (Tor 화면 다운로드 막기 · 프록시 인증, JJ_PATCH.md) - packages/flutter_inappwebview_windows/** # 고친 외부 부품 (Tor 화면 다운로드 막기 · 프록시 인증, JJ_PATCH.md)
- packages/whisper_ggml/** # 고친 외부 부품 (자기 analysis_options 가 flutter_lints 를 찾지 못함)
linter: linter:
# The lint rules applied to this project can be customized in the # The lint rules applied to this project can be customized in the

View File

@ -246,6 +246,7 @@
"Tor 내려받기 시작: {0}": "Tor download started: {0}", "Tor 내려받기 시작: {0}": "Tor download started: {0}",
"Tor 대기 ({0}분 뒤 끔)": "Tor idle (stops in {0} min)", "Tor 대기 ({0}분 뒤 끔)": "Tor idle (stops in {0} min)",
"Tor 대기 중 ({0}분 뒤 끔)": "Tor waiting (stops in {0} min)", "Tor 대기 중 ({0}분 뒤 끔)": "Tor waiting (stops in {0} min)",
"Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다": "Tor waiting (stops in {0} min) - this browser does not use Tor",
"Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "In-app web browser that goes out through Tor (separate cookies and history, cleared when closed)", "Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "In-app web browser that goes out through Tor (separate cookies and history, cleared when closed)",
"Tor 를 받을 수 없습니다: {0}": "Cannot download Tor: {0}", "Tor 를 받을 수 없습니다: {0}": "Cannot download Tor: {0}",
"Tor 를 시작하는 중": "Starting Tor", "Tor 를 시작하는 중": "Starting Tor",
@ -274,6 +275,7 @@
"Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "Keep cookies, history and cache after closing the Tor screen", "Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "Keep cookies, history and cache after closing the Tor screen",
"Tor 화면을 떠나 페이지를 정리했습니다": "You left the Tor screen, so its pages were cleared", "Tor 화면을 떠나 페이지를 정리했습니다": "You left the Tor screen, so its pages were cleared",
"Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "Keep Tor running after leaving the Tor screen", "Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "Keep Tor running after leaving the Tor screen",
"Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.": "Tor is kept running so you can use it right away when you return to the Tor screen.",
"Tor 화면을 준비할 수 없습니다": "Cannot prepare the Tor screen", "Tor 화면을 준비할 수 없습니다": "Cannot prepare the Tor screen",
"Tor 화면을 준비할 수 없습니다: {0}": "Cannot prepare the Tor screen: {0}", "Tor 화면을 준비할 수 없습니다: {0}": "Cannot prepare the Tor screen: {0}",
"Tor 화면의 JavaScript 사용": "JavaScript on the Tor screen", "Tor 화면의 JavaScript 사용": "JavaScript on the Tor screen",
@ -1817,6 +1819,7 @@
"즐겨찾기 표시줄": "Bookmarks bar", "즐겨찾기 표시줄": "Bookmarks bar",
"지금": "Current", "지금": "Current",
"지금 {0}%": "Now {0}%", "지금 {0}%": "Now {0}%",
"지금 끄기": "Stop now",
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running copy uses the previous options. Stop it and run again with the new options?", "지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "The running copy uses the previous options. Stop it and run again with the new options?",
"지금 마스터 (또는 최상) 비밀번호": "Current master (or top) password", "지금 마스터 (또는 최상) 비밀번호": "Current master (or top) password",
"지금 맞추기": "Sync now", "지금 맞추기": "Sync now",

View File

@ -246,6 +246,7 @@
"Tor 내려받기 시작: {0}": "Tor のダウンロード開始: {0}", "Tor 내려받기 시작: {0}": "Tor のダウンロード開始: {0}",
"Tor 대기 ({0}분 뒤 끔)": "Tor 待機 ({0} 分後に終了)", "Tor 대기 ({0}분 뒤 끔)": "Tor 待機 ({0} 分後に終了)",
"Tor 대기 중 ({0}분 뒤 끔)": "Tor 待機中 ({0} 分後に終了)", "Tor 대기 중 ({0}분 뒤 끔)": "Tor 待機中 ({0} 分後に終了)",
"Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다": "Tor 待機中 ({0} 分後に終了) - このブラウザーは Tor を使いません",
"Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "Tor 経由で通信するアプリ内 Web ブラウザー (Cookie・履歴は別、閉じると消去)", "Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "Tor 経由で通信するアプリ内 Web ブラウザー (Cookie・履歴は別、閉じると消去)",
"Tor 를 받을 수 없습니다: {0}": "Tor をダウンロードできません: {0}", "Tor 를 받을 수 없습니다: {0}": "Tor をダウンロードできません: {0}",
"Tor 를 시작하는 중": "Tor を起動中", "Tor 를 시작하는 중": "Tor を起動中",
@ -274,6 +275,7 @@
"Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "Tor 画面を閉じても Cookie・履歴・キャッシュを残す", "Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "Tor 画面を閉じても Cookie・履歴・キャッシュを残す",
"Tor 화면을 떠나 페이지를 정리했습니다": "Tor 画面を離れたため、ページを片付けました", "Tor 화면을 떠나 페이지를 정리했습니다": "Tor 画面を離れたため、ページを片付けました",
"Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "Tor 画面を離れた後に Tor を動かしておく時間", "Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "Tor 画面を離れた後に Tor を動かしておく時間",
"Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.": "Tor 画面に戻ったときすぐ使えるよう、Tor を動かしたままにしています。",
"Tor 화면을 준비할 수 없습니다": "Tor 画面を準備できません", "Tor 화면을 준비할 수 없습니다": "Tor 画面を準備できません",
"Tor 화면을 준비할 수 없습니다: {0}": "Tor 画面を準備できません: {0}", "Tor 화면을 준비할 수 없습니다: {0}": "Tor 画面を準備できません: {0}",
"Tor 화면의 JavaScript 사용": "Tor 画面で JavaScript を使用", "Tor 화면의 JavaScript 사용": "Tor 画面で JavaScript を使用",
@ -1817,6 +1819,7 @@
"즐겨찾기 표시줄": "ブックマークバー", "즐겨찾기 표시줄": "ブックマークバー",
"지금": "現在", "지금": "現在",
"지금 {0}%": "現在 {0}%", "지금 {0}%": "現在 {0}%",
"지금 끄기": "今すぐ終了",
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?", "지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?",
"지금 마스터 (또는 최상) 비밀번호": "現在のマスター (または最上位) パスワード", "지금 마스터 (또는 최상) 비밀번호": "現在のマスター (または最上位) パスワード",
"지금 맞추기": "今すぐ同期", "지금 맞추기": "今すぐ同期",

View File

@ -246,6 +246,7 @@
"Tor 내려받기 시작: {0}": "开始下载 Tor: {0}", "Tor 내려받기 시작: {0}": "开始下载 Tor: {0}",
"Tor 대기 ({0}분 뒤 끔)": "Tor 待机({0} 分钟后关闭)", "Tor 대기 ({0}분 뒤 끔)": "Tor 待机({0} 分钟后关闭)",
"Tor 대기 중 ({0}분 뒤 끔)": "Tor 待机中({0} 分钟后关闭)", "Tor 대기 중 ({0}분 뒤 끔)": "Tor 待机中({0} 分钟后关闭)",
"Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다": "Tor 待机中({0} 分钟后关闭)- 此浏览器不使用 Tor",
"Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "经由 Tor 访问的应用内网页浏览器 (Cookie · 记录分开,关闭时清除)", "Tor 로 나가는 앱 안 웹 브라우저 (쿠키 · 기록 따로, 닫으면 지움)": "经由 Tor 访问的应用内网页浏览器 (Cookie · 记录分开,关闭时清除)",
"Tor 를 받을 수 없습니다: {0}": "无法下载 Tor: {0}", "Tor 를 받을 수 없습니다: {0}": "无法下载 Tor: {0}",
"Tor 를 시작하는 중": "正在启动 Tor", "Tor 를 시작하는 중": "正在启动 Tor",
@ -274,6 +275,7 @@
"Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "关闭 Tor 画面后仍保留 Cookie · 记录 · 缓存", "Tor 화면을 닫아도 쿠키 · 기록 · 캐시 남기기": "关闭 Tor 画面后仍保留 Cookie · 记录 · 缓存",
"Tor 화면을 떠나 페이지를 정리했습니다": "已离开 Tor 页面,页面已清理", "Tor 화면을 떠나 페이지를 정리했습니다": "已离开 Tor 页面,页面已清理",
"Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "离开 Tor 页面后 Tor 保持运行的时间", "Tor 화면을 떠난 뒤 Tor 를 켜 둘 시간": "离开 Tor 页面后 Tor 保持运行的时间",
"Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.": "Tor 保持运行,以便返回 Tor 页面时可立即使用。",
"Tor 화면을 준비할 수 없습니다": "无法准备 Tor 画面", "Tor 화면을 준비할 수 없습니다": "无法准备 Tor 画面",
"Tor 화면을 준비할 수 없습니다: {0}": "无法准备 Tor 画面: {0}", "Tor 화면을 준비할 수 없습니다: {0}": "无法准备 Tor 画面: {0}",
"Tor 화면의 JavaScript 사용": "Tor 画面使用 JavaScript", "Tor 화면의 JavaScript 사용": "Tor 画面使用 JavaScript",
@ -1817,6 +1819,7 @@
"즐겨찾기 표시줄": "书签栏", "즐겨찾기 표시줄": "书签栏",
"지금": "当前", "지금": "当前",
"지금 {0}%": "当前 {0}%", "지금 {0}%": "当前 {0}%",
"지금 끄기": "立即关闭",
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?", "지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?",
"지금 마스터 (또는 최상) 비밀번호": "当前主密码 (或最高密码)", "지금 마스터 (또는 최상) 비밀번호": "当前主密码 (或最高密码)",
"지금 맞추기": "立即同步", "지금 맞추기": "立即同步",

View File

@ -885,13 +885,16 @@ class AppSettings {
..rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x'] ..rsyncPaths = [for (final x in (j['rsyncPaths'] as List?) ?? const []) '$x']
..components = j['components'] is List ..components = j['components'] is List
? [ ? [
for (final x in j['components'] as List) if (AppComponent.byId('$x') != null) '$x', // 225: 이 판이 모르는 구성 요소 (새 판의 Tor 등) 도 지우지 않고 그대로 둔다 - 예전 판을 거쳐 다시 올려도 켠 것이 남게 (40 과 같은 원칙).
// 화면 · 버튼은 아는 것만 쓴다 (AppComponent.pages · contains)
for (final x in j['components'] as List) if (x is String && x.isNotEmpty) x,
// 123: 새 화면 "AI 그림" 은 예전 설정에도 한 번 켜서 넣는다 (끄기: 환경 설정 > 컴포넌트) // 123: 새 화면 "AI 그림" 은 예전 설정에도 한 번 켜서 넣는다 (끄기: 환경 설정 > 컴포넌트)
if (j['aiImageV1'] != true && !(j['components'] as List).contains('aiimage')) 'aiimage', if (j['aiImageV1'] != true && !(j['components'] as List).contains('aiimage')) 'aiimage',
] ]
: AppComponent.defaultInstalled : AppComponent.defaultInstalled
..navOrder = j['navOrder'] is List ..navOrder = j['navOrder'] is List
? [for (final x in j['navOrder'] as List) if (AppComponent.defaultOrder.contains('$x')) '$x'] // 225: 모르는 화면 id 도 순서에 그대로 (그 판으로 돌아가면 사용자가 정한 순서대로)
? [for (final x in j['navOrder'] as List) if (x is String && x.isNotEmpty) x]
: AppComponent.defaultOrder : AppComponent.defaultOrder
..swipeNav = j['swipeNav'] as bool? ?? true ..swipeNav = j['swipeNav'] as bool? ?? true
..imageExts = j['imageExts'] is List ..imageExts = j['imageExts'] is List

View File

@ -44,7 +44,18 @@ class Aria2Backend implements DownloadBackend {
Future<void> _ensureDaemon() async { Future<void> _ensureDaemon() async {
if (_daemon != null) return; if (_daemon != null) return;
// 빈 포트 고르기 // 번호를 넘겨 aria2 가 직접 여는 방식이라, 닫고 넘기는 사이에 다른 프로그램이 가져가면 aria2 가 끝난다 - 그때는 새 번호로 몇 번 다시
for (var attempt = 0;; attempt++) {
try {
return await _startDaemon();
} on SocketException {
if (attempt >= 2) rethrow;
}
}
}
Future<void> _startDaemon() async {
// 빈 포트 고르기 (OS 가 고른 번호 - Windows 가 막아 둔 범위에는 걸리지 않는다)
final s = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); final s = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
_port = s.port; _port = s.port;
await s.close(); await s.close();
@ -63,9 +74,13 @@ class Aria2Backend implements DownloadBackend {
], environment: environment.isEmpty ? null : environment); ], environment: environment.isEmpty ? null : environment);
_daemon!.stdout.drain<void>(); _daemon!.stdout.drain<void>();
_daemon!.stderr.drain<void>(); _daemon!.stderr.drain<void>();
unawaited(_daemon!.exitCode.then((_) => _daemon = null)); // 이 프로세스가 끝나면 (다시 띄운 새 것은 건드리지 않게)
// RPC 준비 대기 final proc = _daemon!;
for (var i = 0; i < 50; i++) { unawaited(proc.exitCode.then((_) {
if (identical(_daemon, proc)) _daemon = null;
}));
// RPC 준비 대기 (aria2 가 그 사이 끝났으면 - 포트를 열지 못함 등 - 기다리지 않는다)
for (var i = 0; i < 50 && _daemon != null; i++) {
try { try {
await _call('aria2.getVersion', []); await _call('aria2.getVersion', []);
return; return;
@ -73,6 +88,8 @@ class Aria2Backend implements DownloadBackend {
await Future<void>.delayed(const Duration(milliseconds: 100)); await Future<void>.delayed(const Duration(milliseconds: 100));
} }
} }
_daemon?.kill();
_daemon = null;
throw SocketException(tr('aria2 RPC 에 연결할 수 없습니다.')); throw SocketException(tr('aria2 RPC 에 연결할 수 없습니다.'));
} }

View File

@ -12,6 +12,7 @@ import '../app/app_controller.dart';
import '../app/bookmarks_controller.dart'; import '../app/bookmarks_controller.dart';
import '../app/download_manager.dart'; import '../app/download_manager.dart';
import '../app/net_guard.dart'; import '../app/net_guard.dart';
import '../app/tor_service.dart';
import '../app/web_proxy.dart'; import '../app/web_proxy.dart';
import '../core/bookmarks.dart'; import '../core/bookmarks.dart';
import '../core/local_proxy.dart' show ProxyAuth; import '../core/local_proxy.dart' show ProxyAuth;
@ -321,6 +322,26 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
), ),
); );
Widget _torIdleBanner(DateTime at) => Material(
color: JjColors.panel,
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 2, 4, 2),
child: Row(children: [
const Icon(Icons.timer_outlined, size: 16, color: JjColors.textDim),
const SizedBox(width: 8),
Expanded(
child: Text(
trf('Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다', [(at.difference(DateTime.now()).inSeconds / 60).ceil().clamp(0, 60)]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
TextButton(onPressed: () => TorService.instance.stop(), child: Text(tr('지금 끄기'))),
]),
),
);
Widget _torBanner() => Material( Widget _torBanner() => Material(
color: const Color(0xFF9C6ADE).withValues(alpha: 0.18), color: const Color(0xFF9C6ADE).withValues(alpha: 0.18),
child: Padding( child: Padding(
@ -552,6 +573,11 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
valueListenable: WebProxy.torOpen, valueListenable: WebProxy.torOpen,
builder: (_, on, _) => on ? _torBanner() : const SizedBox(), builder: (_, on, _) => on ? _torBanner() : const SizedBox(),
), ),
// 226: Tor 화면을 떠난 뒤 tor 를 잠시 켜 두는 중 (이 브라우저는 Tor 를 쓰지 않음) - 언제 끄는지 · 지금 끄기
ValueListenableBuilder<DateTime?>(
valueListenable: TorService.instance.idleStopAt,
builder: (_, at, _) => at == null || WebProxy.torOpen.value ? const SizedBox() : _torIdleBanner(at),
),
Expanded(child: Row(children: [ Expanded(child: Row(children: [
if (_panel) if (_panel)
SizedBox( SizedBox(

View File

@ -183,6 +183,8 @@ class ComponentSettings extends StatelessWidget {
c.updateSettings((v) => v.navOrder = [ c.updateSettings((v) => v.navOrder = [
...ids, ...ids,
...AppComponent.defaultOrder.where((x) => !ids.contains(x) && x != AppComponent.tor.id), ...AppComponent.defaultOrder.where((x) => !ids.contains(x) && x != AppComponent.tor.id),
// 225: 이 판이 모르는 화면 id 는 지우지 않고 뒤에 그대로
...v.navOrder.where((x) => AppComponent.byId(x) == null),
]); ]);
} }

View File

@ -148,6 +148,18 @@ class TorSettings extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = c.settings; final s = c.settings;
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// Tor 화면을 떠난 뒤 잠시 켜 두는 중이면 보인다 (언제 끄는지 · 지금 끄기)
ValueListenableBuilder<DateTime?>(
valueListenable: TorService.instance.idleStopAt,
builder: (context, at, _) => at == null
? const SizedBox.shrink()
: SettingTile(
leading: const Icon(Icons.timer_outlined),
title: Text(trf('Tor 대기 중 ({0}분 뒤 끔)', [(at.difference(DateTime.now()).inSeconds / 60).ceil().clamp(0, 60)])),
subtitle: Text(tr('Tor 화면을 떠난 뒤 다시 들어오면 바로 쓸 수 있게 켜 두고 있습니다.')),
trailing: OutlinedButton(onPressed: () => TorService.instance.stop(), child: Text(tr('지금 끄기'))),
),
),
SwitchListTile( SwitchListTile(
value: s.torKeepData, value: s.torKeepData,
onChanged: (v) => c.updateSettings((x) => x.torKeepData = v), onChanged: (v) => c.updateSettings((x) => x.torKeepData = v),

View File

@ -34,6 +34,9 @@ abstract class TorNav {
/// 웹 뷰 → 화면으로 알리는 통로 /// 웹 뷰 → 화면으로 알리는 통로
class TorViewHost { class TorViewHost {
final void Function(String url) onUrl; final void Function(String url) onUrl;
/// 페이지가 실제로 열림 (읽기 끝 · 기록에 남음). 막은 파일 주소로 주소 칸이 바뀌었으면 이 주소로 되돌린다
final void Function(String url)? onShown;
final void Function(double progress) onProgress; final void Function(double progress) onProgress;
final void Function(bool back, bool forward) onHistory; final void Function(bool back, bool forward) onHistory;
final void Function(TorNav nav) attach; final void Function(TorNav nav) attach;
@ -41,7 +44,12 @@ class TorViewHost {
/// 파일 받기를 막았다 (Tor 화면은 받지 않는다 - 관리자 결정 10/11) /// 파일 받기를 막았다 (Tor 화면은 받지 않는다 - 관리자 결정 10/11)
final void Function(String url) onDownloadBlocked; final void Function(String url) onDownloadBlocked;
const TorViewHost( const TorViewHost(
{required this.onUrl, required this.onProgress, required this.onHistory, required this.attach, required this.onDownloadBlocked}); {required this.onUrl,
required this.onProgress,
required this.onHistory,
required this.attach,
required this.onDownloadBlocked,
this.onShown});
} }
/// WebRTC 끄기 (모든 틀 · 페이지 스크립트보다 먼저): STUN 으로 진짜 주소가 새지 않게. /// WebRTC 끄기 (모든 틀 · 페이지 스크립트보다 먼저): STUN 으로 진짜 주소가 새지 않게.
@ -230,11 +238,23 @@ class _TorPageState extends State<TorPage> with WebProxyVisibility {
}, },
attach: (n) => _nav = TorPage.debugNav = n, attach: (n) => _nav = TorPage.debugNav = n,
onDownloadBlocked: (_) => _downloadBlocked(), onDownloadBlocked: (_) => _downloadBlocked(),
onShown: (u) {
if (u.isNotEmpty) _shownUrl = u;
},
); );
/// 지금 보이는 페이지의 주소 (막은 파일 링크는 열리지 않으므로 주소 칸을 이것으로 되돌린다)
String? _shownUrl;
/// 파일 링크를 누르면: 받지 않고 이유를 알린다 (아무 반응이 없지 않게) /// 파일 링크를 누르면: 받지 않고 이유를 알린다 (아무 반응이 없지 않게)
void _downloadBlocked() { void _downloadBlocked() {
if (!mounted) return; if (!mounted) return;
// 주소 칸이 막은 파일 주소로 바뀌었으면 보이는 페이지 주소로 (페이지와 주소가 다르지 않게)
final shown = _shownUrl;
if (shown != null) {
setState(() => _url = shown);
if (!_addressFocus.hasFocus) _address.text = shown;
}
final m = ScaffoldMessenger.maybeOf(context); final m = ScaffoldMessenger.maybeOf(context);
m?.hideCurrentSnackBar(); m?.hideCurrentSnackBar();
m?.showSnackBar(SnackBar(content: Text(tr('Tor 화면에서는 파일을 받지 않습니다 (Tor 밖으로 나가지 않게)')))); m?.showSnackBar(SnackBar(content: Text(tr('Tor 화면에서는 파일을 받지 않습니다 (Tor 밖으로 나가지 않게)'))));
@ -671,11 +691,13 @@ class _TorWebViewState extends State<_TorWebView> {
onLoadStart: (c, u) => host.onUrl(u?.toString() ?? ''), onLoadStart: (c, u) => host.onUrl(u?.toString() ?? ''),
onLoadStop: (c, u) async { onLoadStop: (c, u) async {
host.onUrl(u?.toString() ?? ''); host.onUrl(u?.toString() ?? '');
host.onShown?.call(u?.toString() ?? '');
host.onProgress(1); host.onProgress(1);
await history(c); await history(c);
}, },
onUpdateVisitedHistory: (c, u, _) async { onUpdateVisitedHistory: (c, u, _) async {
host.onUrl(u?.toString() ?? ''); host.onUrl(u?.toString() ?? '');
host.onShown?.call(u?.toString() ?? '');
await history(c); await history(c);
}, },
onProgressChanged: (c, v) => host.onProgress(v / 100), onProgressChanged: (c, v) => host.onProgress(v / 100),

View File

@ -34,6 +34,19 @@ void main() {
expect(AppSettings.fromJson({}).components, AppComponent.defaultInstalled); expect(AppSettings.fromJson({}).components, AppComponent.defaultInstalled);
}); });
test('225: 이 판이 모르는 구성 요소 · 화면 id 는 지우지 않는다 (예전 판을 거쳐 다시 올려도 켠 것이 남게)', () {
final j = {'aiImageV1': true, 'components': ['mkv', 'browser', 'future_screen'], 'navOrder': ['browser', 'future_screen', 'mkv']};
final s = AppSettings.fromJson(j);
expect(s.components, ['mkv', 'browser', 'future_screen']);
expect(s.navOrder, ['browser', 'future_screen', 'mkv']);
// 화면 · 버튼에는 아는 것만
expect([for (final p in AppComponent.pages(s.components, s.navOrder)) p.id], ['browser', 'mkv']);
// 저장했다 다시 읽어도 그대로
final back = AppSettings.fromJson(s.toJson());
expect(back.components, contains('future_screen'));
expect(back.navOrder, contains('future_screen'));
});
testWidgets('위쪽 이동 버튼: 설치한 화면만 · 설정을 바꾸면 바로 · 하나뿐이면 그것만', (tester) async { testWidgets('위쪽 이동 버튼: 설치한 화면만 · 설정을 바꾸면 바로 · 하나뿐이면 그것만', (tester) async {
tester.view.physicalSize = const Size(1500, 900); tester.view.physicalSize = const Size(1500, 900);
tester.view.devicePixelRatio = 1; tester.view.devicePixelRatio = 1;

View File

@ -21,14 +21,17 @@ class FakeDns {
if (!truncateUdp) { if (!truncateUdp) {
_udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); _udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0);
} else { } else {
// TCP 로 다시 묻는 시험: UDP 와 TCP 가 같은 포트 (Windows 는 막아 둔 포트가 있어 몇 번 시도) // TCP 로 다시 묻는 시험: UDP 와 TCP 가 같은 포트. Windows 는 TCP · UDP 마다 막아 둔 포트 범위 (Hyper-V · WSL,
// netsh int ipv4 show excludedportrange) 가 달라, TCP 를 먼저 OS 가 고른 번호로 열고 그 번호로 UDP 를 연다.
// 그 번호의 UDP 가 막혀 있거나 (10013) 쓰는 중이면 다른 번호로 다시
for (var i = 0;; i++) { for (var i = 0;; i++) {
_udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, 0); final tcp = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
try { try {
_tcp = await ServerSocket.bind(InternetAddress.loopbackIPv4, _udp.port); _udp = await RawDatagramSocket.bind(InternetAddress.loopbackIPv4, tcp.port);
_tcp = tcp;
break; break;
} catch (_) { } on SocketException {
_udp.close(); await tcp.close();
if (i > 50) rethrow; if (i > 50) rethrow;
} }
} }

View File

@ -18,6 +18,7 @@ import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/app_actions.dart'; import 'package:jj_mkvmaker/ui/app_actions.dart';
import 'package:jj_mkvmaker/ui/tor_page.dart'; import 'package:jj_mkvmaker/ui/tor_page.dart';
import 'package:jj_mkvmaker/ui/network_settings.dart';
void main() { void main() {
test('tor 알림 줄 읽기 · torrc', () { test('tor 알림 줄 읽기 · torrc', () {
@ -319,6 +320,48 @@ void main() {
expect(AppSettings.fromJson({'torIdleMinutes': 'x'}).torIdleMinutes, 2); expect(AppSettings.fromJson({'torIdleMinutes': 'x'}).torIdleMinutes, 2);
}); });
testWidgets('227: 막은 파일 주소는 주소 칸에 남지 않고 보이던 페이지 주소로', (t) async {
final loads = <String>[];
final hosts = <TorViewHost>[];
await open(t,
startTor: () async => TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100),
applyProxy: () async => true,
loads: loads,
hosts: hosts);
await t.pump();
await t.pump();
final h = hosts.last;
h.onUrl('https://check.torproject.org/');
h.onShown?.call('https://check.torproject.org/');
// 파일 링크: 읽기 시작에 주소 칸이 파일 주소로 바뀐 뒤 막힘
h.onUrl('https://dist.torproject.org/x.tar.gz.asc');
await t.pump();
expect(find.text('https://dist.torproject.org/x.tar.gz.asc'), findsOneWidget);
h.onDownloadBlocked('https://dist.torproject.org/x.tar.gz.asc');
await t.pump();
expect(find.text('https://dist.torproject.org/x.tar.gz.asc'), findsNothing);
expect(find.text('https://check.torproject.org/'), findsOneWidget);
});
testWidgets('226: 떠난 뒤 tor 를 켜 두는 동안 환경 설정 > Tor 에 "Tor 대기 중 (n분 뒤 끔)" · [지금 끄기]', (t) async {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final s = TorService.instance;
addTearDown(() async {
s.cancelIdleStop();
s.debugRunning = false;
});
await t.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: TorSettings(c: c)))));
expect(find.textContaining('Tor 대기 중'), findsNothing);
s.debugRunning = true;
await s.stopAfter(const Duration(minutes: 2));
await t.pump();
expect(find.text('Tor 대기 중 (2분 뒤 끔)'), findsOneWidget);
await t.tap(find.text('지금 끄기'));
await t.pump();
expect(s.running, isFalse);
expect(find.textContaining('Tor 대기 중'), findsNothing);
});
testWidgets('파일 링크를 누르면 받지 않고 이유를 알린다 (아무 반응 없음이 아니게)', (t) async { testWidgets('파일 링크를 누르면 받지 않고 이유를 알린다 (아무 반응 없음이 아니게)', (t) async {
final loads = <String>[]; final loads = <String>[];
final hosts = <TorViewHost>[]; final hosts = <TorViewHost>[];