JJ_MKVMaker/lib/startup/startup_helpers.dart

151 lines
7.0 KiB
Dart

/// 약속: 시작에 쓰는 도우미 - 다른 앱에서 연 파일 · 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<void> openFromOtherApp(
GlobalKey<NavigatorState> navigatorKey, AppController c, List<Map<Object?, Object?>> files) async {
final paths = [for (final f in files) f['path'] as String?].whereType<String>().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<void>(
builder: (_) => PlayerPage(c: c, player: create(), files: uris, start: 0)));
}
}
}
/// DNS 강제 (사용자 요청 10/10) · Tor 준비: 앱의 HTTP 가 정한 DNS 를 쓰게 하고, 설정이 바뀌면 다시 건다.
/// [second] (재생만 하는 새 창) 은 설정을 읽기만 한다
Future<void> 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<ScaffoldMessengerState> 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<String> 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;
}