모르는 구성 요소 보존 (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
parent
697235073e
commit
e4b6e33a97
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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}%",
|
||||||
|
"지금 끄기": "今すぐ終了",
|
||||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?",
|
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "実行中のコピーは前のオプションで動いています。止めて新しいオプションで再実行しますか?",
|
||||||
"지금 마스터 (또는 최상) 비밀번호": "現在のマスター (または最上位) パスワード",
|
"지금 마스터 (또는 최상) 비밀번호": "現在のマスター (または最上位) パスワード",
|
||||||
"지금 맞추기": "今すぐ同期",
|
"지금 맞추기": "今すぐ同期",
|
||||||
|
|
|
||||||
|
|
@ -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}%",
|
||||||
|
"지금 끄기": "立即关闭",
|
||||||
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?",
|
"지금 도는 복사는 앞의 옵션으로 돌고 있습니다. 멈추고 새 옵션으로 다시 실행할까요?": "正在运行的复制使用的是之前的选项。停止并用新选项重新运行吗?",
|
||||||
"지금 마스터 (또는 최상) 비밀번호": "当前主密码 (或最高密码)",
|
"지금 마스터 (또는 최상) 비밀번호": "当前主密码 (或最高密码)",
|
||||||
"지금 맞추기": "立即同步",
|
"지금 맞추기": "立即同步",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 에 연결할 수 없습니다.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>[];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue