/// 약속: 시작에 쓰는 도우미 - 다른 앱에서 연 파일 · DNS 강제 · Tor 준비 · DNS 실패 알림 · 새 재생 창 판단 · 지금 버전 · 마지막 작업 글. library; import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as p; import '../app/app_controller.dart'; import '../app/net_guard.dart'; import '../app/tor_service.dart'; import '../app/web_proxy.dart'; import '../app/settings.dart'; import '../core/playlist.dart'; import '../core/sftp.dart' show SftpRegistry; import '../core/ftp.dart' show FtpRegistry; import '../core/smb.dart' show SmbRemote; import '../services/platform_services.dart'; import '../ui/network_settings.dart' show DnsFailureNotice; import '../ui/player_page.dart'; import '../l10n/tr.dart'; import '../ui/notice.dart'; /// Android: 다른 앱 (파일 앱 "다음으로 열기" · 공유) 에서 받은 동영상. /// 설정 "다른 앱에서 동영상을 열 때" 에 따라 바로 재생 (기본) 또는 MKV 목록에 추가. 실제 경로를 모르는 것은 재생만. Future openFromOtherApp( GlobalKey navigatorKey, AppController c, List> files) async { final paths = [for (final f in files) f['path'] as String?].whereType().toList(); final uris = [ for (final f in files) if (f['path'] == null) f['uri'] as String, ]; c.note(trf('다른 앱에서 열기: {0}', [files.map((f) => f['name'] ?? f['uri']).join(', ')])); // MKV 화면으로 돌아온 뒤 (재생 중이었다면 그 화면은 닫는다) navigatorKey.currentState?.popUntil((r) => r.isFirst); final ctx = navigatorKey.currentContext; if (ctx == null || !ctx.mounted) return; if (paths.isNotEmpty) { final added = c.addVideos(paths, allowOutputFolder: true); if (c.settings.playback.openFileAction == 'add') { await added; final first = c.videos.where((v) => paths.any((f) => p.equals(f, v.path))).firstOrNull; if (first != null) c.select(first); } else { unawaited(added); await playFiles(ctx, c, paths); } } else if (uris.isNotEmpty) { // 파일 위치를 알려 주지 않는 앱: 재생만 (MKV 만들기 · 자막은 파일 고르기로 추가해야 함) ScaffoldMessenger.maybeOf(ctx)?.showSnackBar(SnackBar( content: Text(tr('파일 위치를 알 수 없어 재생만 합니다. MKV 를 만들려면 "동영상 추가" 로 고르세요.')))); final create = c.services.createMediaPlayer; if (create != null) { await Navigator.of(ctx).push(MaterialPageRoute( builder: (_) => PlayerPage(c: c, player: create(), files: uris, start: 0))); } } } /// DNS 강제 (사용자 요청 10/10) · Tor 준비: 앱의 HTTP 가 정한 DNS 를 쓰게 하고, 설정이 바뀌면 다시 건다. /// [second] (재생만 하는 새 창) 은 설정을 읽기만 한다 Future setupNetwork(AppController c, String dataDir, {bool second = false}) async { final g = NetGuard.instance ..config = (() => c.settings.network.config) ..log = c.note; try { await g.ensure(); } catch (e) { c.note(trf('DNS 강제 프록시를 시작하지 못했습니다: {0}', [e])); } HttpOverrides.global = JjHttpOverrides(); // 88: SFTP 는 HTTP 가 아니라 직접 잇는다 - 같은 DNS 설정을 따르게 (집 안 이름 · 주소는 기기 DNS) SftpRegistry.connect = SftpRegistry.connectWithDns(g.resolver); FtpRegistry.resolve = FtpRegistry.resolveWithDns(g.resolver); SmbRemote.resolve = FtpRegistry.resolveWithDns(g.resolver); if (second) return; TorService.dataDir = dataDir; TorService.instance.log = c.note; if (Platform.isAndroid) { WebProxy.log = c.note; // 환경 설정 "다른 화면으로 가면 소리 멈춤" 이 꺼져 있으면 일반 브라우저가 뒤에 있어도 DNS 문을 열어 둔다 WebProxy.keepPlayingWhenHidden = () => !c.settings.browser.pauseOnLeave; try { await WebProxy.apply(); } catch (e) { c.note(trf('웹 뷰 프록시를 걸지 못했습니다: {0}', [e])); } // 앱이 뒤로 가면 웹 뷰 프록시 문을 닫고, 돌아오면 다시 (관리자 결정 10/11 - 열려 있는 시간을 줄인다) AppLifecycleListener( onResume: () => unawaited(WebProxy.setForeground(true)), onHide: () => unawaited(WebProxy.setForeground(false)), ); } var last = c.settings.network.config.key; var lastPause = c.settings.browser.pauseOnLeave; c.addListener(() async { if (Platform.isAndroid && c.settings.browser.pauseOnLeave != lastPause) { lastPause = c.settings.browser.pauseOnLeave; await WebProxy.apply(); } final now = c.settings.network.config.key; if (now == last) return; last = now; c.note(trf('DNS 설정: {0}', [c.settings.network.mode == 'system' ? tr('시스템 DNS') : c.settings.network.config.label])); await g.ensure(); if (Platform.isAndroid) await WebProxy.apply(); }); } /// DNS 서버에 닿지 않으면 알린다 (기기 DNS 로 몰래 바꾸지 않음). [시스템 DNS 로 바꾸기] 를 함께 void watchDnsFailure(GlobalKey messengerKey, AppController c) { NetGuard.instance.failure.addListener(() { final f = NetGuard.instance.failure.value; if (f == null) return; // 248: 같은 안내를 보이는 화면 (브라우저 덮개 · 설정의 DNS 칸) 이 열려 있으면 띄우지 않는다 (상태는 설정 · 작업 기록 줄에) if (NetGuard.instance.noticeBlockers.value > 0) return; showNotice( messengerKey.currentState, // 248: 버튼이 있는 안내라 사용자가 닫거나 누를 때까지 둔다 (한 번만 뜨고, 상태는 설정 · 작업 현황에 남음 - 249) life: NoticeLife.untilClosed, // 사용자에게는 무엇이 안 되고 무엇을 할지만 (예외 원문은 작업 기록에 - NetGuard.reportError). // 상태가 풀리거나 같은 안내를 보이는 화면이 열리면 스스로 닫힌다 (248) content: DnsFailureNotice(failure: f), action: (tr('시스템 DNS 로 바꾸기'), () => c.updateSettings((x) => x.network.mode = 'system')), ); }); } /// 탐색기에서 연 동영상을 새 재생 창으로 띄울지 (재생 요청 + 설정 "새 창에서") bool wantsNewPlayerWindow(LaunchRequest r, AppSettings s) => s.playback.openFileWindow == 'new' && r.files.any(isVideoFile) && (r.action == LaunchAction.play || (r.action == LaunchAction.add && s.playback.openFileAction == 'play')); /// 지금 실행 중인 버전 (예: 2026.10.05_003). 모르면 ''. Future currentAppVersion(PlatformServices services) async { try { return await services.updater?.currentVersion() ?? ''; } catch (_) { return ''; } } /// 종료 창에 보일 "바로 전 작업": 하던 작업, 없으면 마지막 작업 기록 (시각 표시는 뺀다) String lastWorkText(AppController c) { if (c.currentJob != null) return c.currentJob!; if (c.logs.isEmpty) return tr('없음'); return c.logs.last.replaceFirst(RegExp(r'^\[[\d:]+\]\s*'), '').split('\n').first; }