238 lines
12 KiB
Dart
238 lines
12 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
|
|
import 'app/app_controller.dart';
|
|
import 'app/net_guard.dart';
|
|
import 'ui/settings_problem.dart';
|
|
import 'ui/migration_notice.dart';
|
|
import 'ui/license_texts.dart';
|
|
import 'platform/windows/data_dir_override.dart';
|
|
import 'ui/master_prompt.dart';
|
|
import 'ui/secret_issue.dart';
|
|
import 'ui/sync_alert.dart';
|
|
import 'app/settings.dart';
|
|
import 'core/app_update.dart';
|
|
import 'core/playlist.dart';
|
|
import 'platform/windows/app_paths.dart';
|
|
import 'platform/windows/cef_runtime.dart';
|
|
import 'platform/windows/com_guard.dart';
|
|
import 'platform/windows/desktop_shell.dart';
|
|
import 'platform/windows/exit_trace.dart';
|
|
import 'platform/windows/session_marker.dart';
|
|
import 'platform/windows/single_instance.dart';
|
|
import 'platform/windows/start_menu.dart';
|
|
import 'platform/windows/window_memory.dart';
|
|
import 'services/platform_services.dart';
|
|
import 'ui/app_actions.dart';
|
|
import 'ui/downloads_page.dart';
|
|
import 'ui/monitor_page.dart' show askLiveSyncStart, MonitorPage;
|
|
import 'ui/setup_dialog.dart';
|
|
import 'ui/update_dialog.dart';
|
|
import 'ui/version_restore.dart';
|
|
import 'l10n/tr.dart';
|
|
import 'app/i18n_controller.dart';
|
|
import 'ui/app_root.dart';
|
|
import 'startup/startup_helpers.dart';
|
|
import 'startup/android_start.dart';
|
|
import 'startup/second_window.dart';
|
|
import 'startup/app_start.dart';
|
|
import 'startup/launch_requests.dart';
|
|
import 'startup/windows_quit.dart';
|
|
|
|
// 시험 · 다른 곳이 main.dart 로 부르던 이름 (그 모듈에 있음)
|
|
export 'ui/app_root.dart';
|
|
export 'startup/startup_helpers.dart';
|
|
export 'startup/android_start.dart' show runAndroid;
|
|
export 'startup/second_window.dart' show runSecondWindow;
|
|
|
|
/// 버전을 읽어 제목을 정하고, 설정 폴더를 돌려준다
|
|
Future<String> _prepare() async {
|
|
try {
|
|
final info = await PackageInfo.fromPlatform();
|
|
appTitle = 'JJ_MKVMaker v${formatVersion('${info.version}+${info.buildNumber}')}';
|
|
} catch (_) {}
|
|
return (await getApplicationSupportDirectory()).path;
|
|
}
|
|
|
|
Future<void> main(List<String> args) async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
// DNS 강제: 앱의 HttpClient 는 만들 때 findProxy 를 받으므로, 다른 무엇보다 먼저 건다 (일찍 만들어 두는 업데이트 · 컴포넌트 ·
|
|
// 도구 받기 · 자막 검색의 HttpClient 가 정한 DNS 를 거치지 않던 것, 10/12). 설정을 읽기 전에는 바로 연결 (그 사이 나가는 연결 없음)
|
|
HttpOverrides.global = JjHttpOverrides();
|
|
// 62 · 63 · 64 · 123: GPL · LGPL 원문 · 고지 문서 · AI 구성 요소 · 모델을 앱 안 라이선스 화면에
|
|
registerAppLicenses();
|
|
applyDataDirOverride(); // 시험판: 사용자 데이터와 다른 폴더 (JJ_MKVMAKER_DATA)
|
|
final dataDir = await _prepare();
|
|
if (Platform.isAndroid) return runAndroid(dataDir);
|
|
|
|
// 이미 실행 중이면 인수(탐색기에서 고른 파일)를 넘기고 끝낸다
|
|
final request = LaunchRequest.parse(args);
|
|
final startSettings = await SettingsStore().load();
|
|
final newWindow = wantsNewPlayerWindow(request, startSettings);
|
|
// 228: 데이터 폴더마다 하나 (데이터 폴더의 잠금 파일) - 시험판 (따로 데이터) 은 사용자가 켜 둔 앱 · 다른 시험판에 넘기지 않고 따로 뜬다
|
|
final instance = SingleInstance(dataDir: dataDir);
|
|
final claimed = await instance.claim(request, waitForExit: args.contains('--restart'), forward: !newWindow);
|
|
if (claimed == ClaimResult.unresponsive) {
|
|
// 같은 데이터 위에 창이 두 개 뜨지 않게 묻는다 (처음 고른 것은 [닫기] - 관리자 결정 10/12)
|
|
await i18n.apply(startSettings.display.language, save: false);
|
|
if (askOpenAnyway(appTitle)) {
|
|
await instance.takeOverAnyway(request);
|
|
} else {
|
|
endProcessNow();
|
|
}
|
|
} else if (claimed != ClaimResult.first) {
|
|
// 설정이 "새 창에서" 이고 이미 켜져 있으면: 재생만 하는 창으로 따로 뜬다
|
|
if (newWindow) return runSecondWindow(request.files.where(isVideoFile).toList(), dataDir);
|
|
endProcessNow(); // exit(0) 은 막 뜬 엔진을 정리하다 coremessaging.dll 에서 꺼진다
|
|
|
|
}
|
|
|
|
final services = PlatformServices.create();
|
|
// 작업 기록: 배포 폴더의 Logs (쓸 수 없으면 설정 폴더 아래 Logs)
|
|
final logsDir = AppPaths.logsDir(dataDir);
|
|
// 내장 Chrome 엔진이 이번 실행 시작 때 있었는지 (실행 파일이 그때 준비함)
|
|
CefRuntime.rememberStartState();
|
|
final controller = AppController(services, settingsStore: SettingsStore())
|
|
..logFile = p.join(logsDir, 'app.log');
|
|
ExitTrace.file = p.join(logsDir, 'exit.log');
|
|
// 지난 실행이 정상으로 끝났는지 (아니면 알려 주고 기록에 남긴다)
|
|
final session = SessionMarker(p.join(dataDir, 'session.json'));
|
|
final crashed = session.start();
|
|
controller.note(trf('── 시작 {0} ({1}) ──', [appTitle, AppPaths.exeDir]));
|
|
// 예전 구조 (한 폴더) 에서 Lib 구조로 바뀐 뒤 맨 위에 남은 예전 프로그램 파일 정리
|
|
final cleaned = AppPaths.cleanupOldLayout();
|
|
if (cleaned.isNotEmpty) controller.note(trf('예전 구조의 프로그램 파일 정리 ({0}개): {1}', [cleaned.length, cleaned.join(', ')]));
|
|
if (crashed != null) {
|
|
String hm(DateTime t) => '${t.month}/${t.day} ${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
|
controller.note(trf('⚠ 지난 실행이 정상적으로 끝나지 않았습니다 (시작 {0}, 마지막 확인 {1}). ' '그 전의 기록은 Logs 폴더의 app.log 에 있습니다.', [hm(crashed.$1), hm(crashed.$2)]));
|
|
}
|
|
// 버전별 설정 보관 (업데이트 · 예전 버전으로 되돌리기): 시작할 때 설정 파일이 없었는지 (새로 설치)
|
|
final freshInstall = !File(p.join(dataDir, 'settings.json')).existsSync();
|
|
// 공용 시작 단계 (Android 와 같은 차례 - app_start)
|
|
final (:live, :bookmarks) = await startCore(controller, dataDir);
|
|
|
|
final navigatorKey = GlobalKey<NavigatorState>();
|
|
final messengerKey = GlobalKey<ScaffoldMessengerState>();
|
|
final downloads = createDownloads(controller, messengerKey: messengerKey, navigatorKey: navigatorKey);
|
|
attachMasterPrompt(navigatorKey);
|
|
|
|
final memory = WindowMemory(p.join(dataDir, 'window.json'), 'main');
|
|
|
|
/// 종료 요청 (true 면 프로그램을 끝낸다 - [WindowsQuit])
|
|
final confirmQuit = WindowsQuit(
|
|
controller: controller, downloads: downloads, navigatorKey: navigatorKey, instance: instance, memory: memory, session: session);
|
|
|
|
await services.shell.init(
|
|
hotkey: controller.settings.run.showHotkey,
|
|
minimizeToTray: () => controller.settings.run.minimizeToTray,
|
|
onCloseRequested: confirmQuit.call,
|
|
onDownloadsRequested: () {
|
|
final nav = navigatorKey.currentState;
|
|
if (nav != null) unawaited(DownloadsPage.open(nav, downloads));
|
|
},
|
|
);
|
|
await services.shell.setAppIcon(controller.settings.display.appIcon);
|
|
// 99: 시작 메뉴 바로 가기 (앱 ID) · jjmkvmaker:// 연결 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 (설정에서 끔)
|
|
StartMenu.setProcessAppId();
|
|
final shell0 = services.shell;
|
|
final startMenu = services.startMenu;
|
|
if (startMenu.userAreaLocked()) {
|
|
// 관리자 지시 (10/20): 시험판 · 시험 (따로 데이터) - 사용자 바로 가기 · 주소 연결은 그대로 (StartMenu 가 막음), 작업 기록에 한 줄
|
|
controller.note(tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다'));
|
|
} else if (controller.settings.run.startMenuShortcut) {
|
|
unawaited(startMenu.ensure().then((ok) {
|
|
if (shell0 is DesktopShell) shell0.toastAppReady = ok;
|
|
}));
|
|
} else {
|
|
unawaited(startMenu.remove());
|
|
}
|
|
// 제목 표시줄에 버전 · 지난번 창 위치에서 열기 · 동영상 목록 불러오기 (창끼리 공유)
|
|
await windowManager.setTitle(appTitle);
|
|
final shell = services.shell;
|
|
if (shell is DesktopShell) memory.paused = () => shell.isPopup;
|
|
await memory.restoreAndWatch();
|
|
unawaited(controller.shareVideoList(p.join(dataDir, 'videos.json')));
|
|
|
|
watchDnsFailure(messengerKey, controller);
|
|
// 트레이 글
|
|
// 92: 트레이에 둔 채 실시간 동기화가 멈추면 Windows 알림 · 트레이 툴팁, 누르면 모니터링 > lsync 카드로
|
|
final syncAlert = SyncAlert(live, services.shell,
|
|
toastEnabled: () => controller.settings.copy.syncStopToast,
|
|
openLsync: () {
|
|
final nav = navigatorKey.currentState;
|
|
if (nav != null) unawaited(nav.push(MaterialPageRoute<void>(builder: (_) => MonitorPage(c: controller, initialTab: 1))));
|
|
},
|
|
baseTooltip: () {
|
|
final n = downloads.downloadingCount;
|
|
return n > 0 ? trf('JJ_MKVMaker - 다운로드 {0}개', [n]) : 'JJ_MKVMaker';
|
|
});
|
|
downloads.addListener(() => services.shell.setTooltip(syncAlert.tooltip()));
|
|
// COM 준비 상태 지킴이: 풀려 있으면 (앱 안 브라우저가 "CoInitialize 가 호출되지 않았습니다" 로 안 뜨는 원인)
|
|
// 다시 준비하고 작업 기록에 남긴다. 바로 앞의 기록을 보면 무엇을 한 뒤에 풀렸는지 알 수 있다.
|
|
Timer.periodic(const Duration(seconds: 3), (_) {
|
|
if (ComGuard.repairIfNeeded()) {
|
|
controller.note(tr('⚠ Windows COM 준비 상태가 풀려 있어 다시 준비했습니다 (브라우저 · 끌어다 놓기에 필요)'));
|
|
}
|
|
});
|
|
unawaited(downloads.startClipboardWatch());
|
|
|
|
// 탐색기 메뉴 · 두 번째 실행에서 온 요청 처리
|
|
instance.listen((req) => handleLaunchRequest(req, controller, navigatorKey));
|
|
|
|
controller.confirmQuit = () => confirmQuit(force: true); // 업데이트 설치: 항상 종료
|
|
|
|
// 처음 화면이 뜬 뒤: 필수 프로그램 점검 (없으면 물어보고 설치) → 새 버전 확인 (하루 한 번)
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
final ctx = navigatorKey.currentContext;
|
|
if (ctx == null) return;
|
|
// 124: "처음 시작 시" 면 마스터 비밀번호
|
|
await askMasterAtStartup(ctx, controller);
|
|
if (!ctx.mounted) return;
|
|
if (crashed != null) {
|
|
messengerKey.currentState?.showSnackBar(SnackBar(
|
|
duration: const Duration(seconds: 10),
|
|
content: Text(trf('지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 ' '{0}:{1}). ' '작업 기록에 남겨 두었습니다.', [crashed.$2.hour.toString().padLeft(2, '0'), crashed.$2.minute.toString().padLeft(2, '0')])),
|
|
));
|
|
}
|
|
// 홈 화면이 MKV 화면이 아니면 (또는 MKV 만들기를 제거했으면) 그 화면을 연다 (MKV 화면은 그 아래에 있음)
|
|
final home = AppNavButtons.homeId(controller.settings);
|
|
final nav0 = navigatorKey.currentState;
|
|
if (home != 'mkv' && nav0 != null) {
|
|
unawaited(AppNavButtons.openPage(nav0, home, c: controller, downloads: downloads, bookmarks: bookmarks));
|
|
}
|
|
if (controller.settings.copy.liveSyncOnStart == 'ask' && ctx.mounted) await askLiveSyncStart(ctx, live);
|
|
// 설정 파일을 읽지 못했으면 (백업으로 되살렸거나 처음 설정으로 켰으면) 알린다
|
|
if (ctx.mounted) await showSettingsProblem(ctx, controller);
|
|
watchSecretIssue(messengerKey, controller);
|
|
// 업데이트로 예전 기본값을 새 기본값으로 옮겼으면 한 번 알린다
|
|
if (ctx.mounted) await showMigrationNotice(ctx, controller);
|
|
// 다른 버전을 쓰다가 이 버전으로 돌아왔으면 보관해 둔 이 버전의 설정을 되살릴지
|
|
final current = await currentAppVersion(services);
|
|
if (ctx.mounted) {
|
|
await checkVersionRestore(ctx, controller, current: current, freshInstall: freshInstall, restart: services.shell.restart);
|
|
}
|
|
if (!ctx.mounted) return;
|
|
await checkRequiredTools(ctx, services.shell);
|
|
final ctx2 = navigatorKey.currentContext;
|
|
if (ctx2 != null && ctx2.mounted) await checkForUpdate(ctx2, controller);
|
|
});
|
|
|
|
runApp(JjMkvMakerApp(
|
|
controller: controller,
|
|
downloads: downloads,
|
|
bookmarks: bookmarks,
|
|
navigatorKey: navigatorKey,
|
|
messengerKey: messengerKey,
|
|
onExit: () async {
|
|
if (await confirmQuit()) await services.shell.quit();
|
|
},
|
|
));
|
|
}
|
|
|