import 'dart:io'; import 'package:path/path.dart' as p; import '../platform/common/media_kit_full_player.dart'; import '../platform/common/media_kit_player.dart'; import '../app/ai_upscale.dart' show AiUpscaler; import '../app/settings.dart'; import '../app/vault_location.dart'; import '../core/shizuku.dart' show PrivilegedFs; import '../core/download_detect.dart'; import '../core/lan_scan.dart' show LanHost, deviceMetered; import '../platform/common/nllb_translator.dart'; import '../platform/common/opensubtitles_provider.dart'; import '../platform/common/whisper_recognizer.dart'; import '../platform/android/android_download_tools.dart'; import '../platform/android/android_shell.dart'; import '../platform/android/android_storage.dart'; import '../platform/android/android_updater.dart'; import '../platform/android/ffmpeg_kit_media_tool.dart'; import '../platform/windows/aria2_backend.dart'; import '../platform/windows/desktop_shell.dart'; import '../platform/windows/desktop_storage_service.dart'; import '../platform/windows/start_menu.dart'; import '../platform/windows/github_updater.dart'; import '../platform/windows/process_media_tool.dart'; import '../platform/windows/windows_usage.dart'; import '../platform/windows/ytdlp_backend.dart'; import 'ai_services.dart'; import 'app_shell.dart'; import 'downloader.dart'; import 'media_player.dart'; import 'media_tool.dart'; import 'model_store.dart'; import 'platform_info.dart'; import 'preview_player.dart'; import 'storage_service.dart'; import 'subtitle_provider.dart'; import 'system_usage.dart'; import 'updater.dart'; import '../l10n/tr.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; /// 플랫폼별 구현을 한 곳에서 고른다. /// Android 이식 시 이 파일에 분기만 추가하면 된다. class PlatformServices { final MediaTool mediaTool; final StorageService storage; /// 영상 재생기 생성 (없으면 영상 없이 자막만 편집) final PreviewPlayer Function()? createPlayer; /// 동영상 플레이어 (재생 목록 · 트랙) final MediaPlayer Function()? createMediaPlayer; /// AI 자막 (없으면 AI 자막 기능 숨김) final SpeechRecognizer Function()? createRecognizer; final Translator Function()? createTranslator; final ModelStore models; /// 다운로드 엔진 (yt-dlp, aria2). 설정값(형식 · 화질)을 읽는 함수를 받는다. final List Function(AppSettings Function() settings)? createDownloadBackends; /// 트레이 · 전역 단축키 · 창 닫기 final AppShell shell; /// 새 버전 확인 · 설치 (없으면 업데이트 기능 숨김) final Updater? updater; /// PC 의 CPU · 메모리 사용량 (없으면 표시 숨김) final SystemUsage? usage; /// 엔진 · 모델을 이미 정해 둔 해상도 올리기 (없으면 설정대로 준비하고, 없는 엔진 · 모델은 그 자리에서 받는다) final AiUpscaler Function()? createUpscaler; /// 같은 망의 SMB 서버 찾기 (없으면 이 기기의 사설망을 실제로 훑는다 - smb_settings) final Future> Function({bool Function()? cancelled, void Function(int done, int total)? onProgress})? findLanSmb; /// 디스크 남은 · 전체 바이트 재기 (없으면 실제 디스크 - copy_center 의 diskSpace). 작업 현황의 "대상 디스크" 줄 final Future<(int, int)?> Function(String path)? diskSpace; /// 지금 시각 (없으면 실제 시계). 작업 기록의 [시:분:초] final DateTime Function()? clock; /// 암호 보관함 자리 (설계 M3). 앱 조립 ([PlatformServices.create]) 은 실제 자리 ([VaultLocation.device]), /// 손으로 조립하면 처음 값은 앱 임시 폴더 아래 ([VaultLocation.under] - 사용자 보관함 · 앱 데이터를 건드리지 않음) final VaultLocation vault; /// 시스템 폴더 (Shizuku) · 루트 (su) 읽기 (설계 M3). 있으면 AppController 가 켤 때 PrivilegedFs.use 로 건다 - Android 조립만 넘기고, /// 없으면 PrivilegedFs.current 를 그대로 둔다 (시험이 건 가짜를 덮지 않게) final PrivilegedFs? privileged; /// 지금 쓰는 망이 데이터 요금제인지 (원격 썸네일 "Wi-Fi 에서만" - 설계 M3 ThumbPolicy). 처음 값 = 실제 (Android 만 묻고 PC 는 false) final Future Function() isMetered; /// 화면을 켜 둔다 / 놓는다 (보기 화면의 자동 넘기기 - 설계 M4 WakeLock). 처음 값 = WakelockPlus final Future Function(bool on) keepScreenOn; /// 시작 메뉴 바로 가기 · jjmkvmaker:// 연결 (Windows, 설계 M4). 앱 조립은 [StartMenu.user] (사용자 영역), /// 손으로 조립하면 처음 값은 [StartMenu.locked] (사용자 시작 메뉴 · HKCU 를 건드리지 않음 - 시험이 실제 바로 가기를 만들거나 지우지 않게) final StartMenu startMenu; /// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기) final PlatformInfo platform; /// 인터넷 자막 사이트 (설정값을 읽는 함수를 받아 만든다) final List Function(AppSettings Function() settings)? createSubtitleProviders; PlatformServices({ required this.mediaTool, required this.storage, this.createPlayer, this.createMediaPlayer, this.createRecognizer, this.createTranslator, this.createDownloadBackends, this.createSubtitleProviders, this.createUpscaler, this.findLanSmb, this.diskSpace, this.clock, this.updater, this.usage, ModelStore? models, AppShell? shell, PlatformInfo? platform, VaultLocation? vault, this.privileged, Future Function()? isMetered, Future Function(bool on)? keepScreenOn, StartMenu? startMenu, }) : models = models ?? ModelStore(), vault = vault ?? VaultLocation.under(storage.tempDirectory), isMetered = isMetered ?? deviceMetered, keepScreenOn = keepScreenOn ?? _wakeLock, startMenu = startMenu ?? StartMenu.locked(Directory.systemTemp.path), platform = platform ?? PlatformInfo.host, shell = shell ?? NoopShell(); factory PlatformServices.create() { if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { MediaKitPreviewPlayer.ensureInitialized(); final tool = ProcessMediaTool.locate(); final ffmpegDir = File(tool.ffmpeg).existsSync() ? p.dirname(tool.ffmpeg) : null; return PlatformServices( vault: VaultLocation.device, startMenu: StartMenu.user, mediaTool: tool, storage: DesktopStorageService(), createPlayer: MediaKitPreviewPlayer.new, createMediaPlayer: MediaKitFullPlayer.new, createRecognizer: WhisperRecognizer.new, createTranslator: NllbTranslator.new, createDownloadBackends: (s) => [ YtDlpBackend( ffmpegDir: ffmpegDir, formatArgs: () => ytDlpFormatArgs(s().download.ytContainer, s().download.ytQuality, preferH264: s().download.ytPreferH264), cookieArgs: () => ytDlpCookieArgs( browser: s().download.ytCookiesBrowser, file: s().download.ytCookiesFile, internalProfile: s().internalCookieProfile, internalCookieFile: s().internalCookieFile), // 고른 브라우저의 쿠키를 못 읽으면 앱 안 브라우저의 쿠키로 fallbackCookieArgs: () => ytDlpCookieArgs( browser: internalBrowserCookies, internalCookieFile: s().internalCookieFile), ), Aria2Backend(stallAfter: () => Duration(seconds: s().download.torrentStallSeconds)), ], shell: DesktopShell(), updater: GitHubUpdater(), usage: Platform.isWindows ? WindowsUsage() : null, createSubtitleProviders: (s) => [ OpenSubtitlesProvider( apiKey: () => s().mkv.openSubtitlesKey, username: () => s().mkv.openSubtitlesUser, password: () => s().mkv.openSubtitlesPassword, ), ], ); } if (Platform.isAndroid) { // 탐색기 연결은 데스크톱 전용. 업데이트는 APK 를 받아 Android 설치 화면으로. 다운로드는 앱에 넣은 yt-dlp · aria2c (youtubedl-android) MediaKitPreviewPlayer.ensureInitialized(); final storage = AndroidStorageService(); return PlatformServices( vault: VaultLocation.device, privileged: PrivilegedFs.device(storage.tempDirectory), mediaTool: FfmpegKitMediaTool(), storage: storage, updater: AndroidUpdater(storage), createPlayer: MediaKitPreviewPlayer.new, createMediaPlayer: MediaKitFullPlayer.new, createRecognizer: WhisperRecognizer.new, createTranslator: NllbTranslator.new, createDownloadBackends: AndroidDownloadTools.backends, shell: AndroidShell(), createSubtitleProviders: (s) => [ OpenSubtitlesProvider( apiKey: () => s().mkv.openSubtitlesKey, username: () => s().mkv.openSubtitlesUser, password: () => s().mkv.openSubtitlesPassword, ), ], ); } throw UnsupportedError(trf('아직 지원하지 않는 플랫폼입니다: {0}', [Platform.operatingSystem])); } } Future _wakeLock(bool on) => on ? WakelockPlus.enable() : WakelockPlus.disable();