import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import '../../core/download_detect.dart' show CookieExport, CookieRecord, toNetscapeCookies; /// 브라우저 조작 (앱 안 웹뷰) abstract class WebNav { Future load(String url); Future back(); Future forward(); Future reload(); Future stop(); /// YouTube · Google 로그인 쿠키를 yt-dlp 용 cookies.txt 로 내보내기 Future exportCookies(); /// 페이지의 동영상 · 소리 멈추기 (다른 화면으로 갈 때) Future pauseMedia(); /// 페이지에서 스크립트 실행 (YouTube 광고 건너뛰기 등) Future runScript(String js); /// 53: 쿠키 · 사이트 데이터 · 캐시 · 방문 기록 (뒤로 · 앞으로 목록) 지우기 - 엔진이 지원하는 것만 Future clearData(); /// 페이지에서 스크립트를 실행하고 결과를 받는다 (페이지 번역). 실패하면 null. Future evaluate(String js); /// 페이지의 JavaScript 켜기 · 끄기 (바뀌면 지금 페이지를 다시 읽는다) Future setJavaScript(bool on); } /// 250: 사용자가 페이지를 옮길 때 (주소 · 뒤로 · 앞으로 · 새로고침 · 다시 시도) 알린다 - DNS 덮개는 이때만 걷는다. /// 진행률로 걷으면 Android 는 본문 오류 뒤에 엔진의 오류 페이지를 또 읽어 (진행률 0 → 100) 덮개가 뜨자마자 걷혔다 (932 노트 10+) class NavWatch implements WebNav { final WebNav n; final void Function() onUserNavigate; NavWatch(this.n, this.onUserNavigate); @override Future load(String url) { onUserNavigate(); return n.load(url); } @override Future back() { onUserNavigate(); return n.back(); } @override Future forward() { onUserNavigate(); return n.forward(); } @override Future reload() { onUserNavigate(); return n.reload(); } @override Future stop() => n.stop(); @override Future exportCookies() => n.exportCookies(); @override Future pauseMedia() => n.pauseMedia(); @override Future runScript(String js) => n.runScript(js); @override Future clearData() => n.clearData(); @override Future evaluate(String js) => n.evaluate(js); @override Future setJavaScript(bool on) => n.setJavaScript(on); } /// 페이지의 모든 동영상 · 소리를 멈추는 스크립트 const pauseMediaScript = "document.querySelectorAll('video,audio').forEach(function(m){try{m.pause()}catch(e){}})"; /// 화면 이동 알림 (브라우저 위에 다른 화면이 올라오면 동영상을 멈춘다). MaterialApp.navigatorObservers 에 넣는다. final browserRouteObserver = RouteObserver>(); /// 웹뷰 → 화면으로 알리는 통로 class BrowserHost { final void Function(String url) onUrl; final void Function(String title) onTitle; final void Function(double progress) onProgress; final void Function(bool hasVideo) onVideo; final void Function(bool back, bool forward) onHistory; final void Function(WebNav nav) attach; /// 250: 본문 (main frame) 을 읽지 못함 - 엔진이 알려 준 주소와 오류 (예: net::ERR_TUNNEL_CONNECTION_FAILED) final void Function(String url, String error)? onMainFrameError; const BrowserHost({ required this.onUrl, required this.onTitle, required this.onProgress, required this.onVideo, required this.onHistory, required this.attach, this.onMainFrameError, }); } /// 53: 다운로드용으로 내보낸 로그인 쿠키 파일 (평문) 을 지운다 Future deleteExportedCookies(String dataDir) async { if (dataDir.isEmpty) return; for (final name in [CookieExport.fileName, '${CookieExport.fileName}.tmp']) { try { final f = File('$dataDir${Platform.pathSeparator}$name'); if (await f.exists()) await f.delete(); } catch (_) {} } } /// 로그인 쿠키를 yt-dlp 용 cookies.txt 로 쓴다 (Edge · Chrome 같은 파일). 임시 파일에 다 쓴 뒤 한 번에 바꿔 넣는다 /// (yt-dlp 가 쓰는 도중의 파일을 읽지 않도록) Future writeLoginCookies(String dataDir, List all) async { if (dataDir.isEmpty || all.isEmpty) return; final target = '$dataDir${Platform.pathSeparator}cookies_youtube.txt'; final tmp = File('$target.tmp'); await tmp.parent.create(recursive: true); await tmp.writeAsString(toNetscapeCookies(all), flush: true); await tmp.rename(target); }