/// 창 · 트레이 · 전역 단축키 경계 (데스크톱 전용 기능) /// /// Windows: platform/windows/desktop_shell.dart /// Android·테스트: [NoopShell] abstract class AppShell { /// [onShowRequested]: 트레이 클릭·단축키로 창 보이기 /// [onCloseRequested]: 창의 X 또는 트레이 "종료" (true 를 돌려주면 종료) /// [onDownloadsRequested]: 트레이 "다운로드 목록" Future init({ required String hotkey, required bool Function() minimizeToTray, required Future Function() onCloseRequested, void Function()? onDownloadsRequested, }); Future show(); Future hide(); /// 단축키 변경 (예: "Ctrl+Shift+X"). 형식이 틀리면 false. Future setHotkey(String hotkey); /// 트레이 알림 글 (다운로드 개수 등) Future setTooltip(String text); /// 프로그램 종료 Future quit(); // ───────── 플레이어 창 ───────── Future setFullScreen(bool on); Future isFullScreen(); /// 팝업 보기: 작은 창 · 항상 위 · 제목 표시줄 없음. 끄면 원래 크기로. Future setPopup(bool on); /// 창 크기를 영상 크기의 [factor] 배로 (화면보다 크면 화면에 맞춤) Future fitVideo(int width, int height, double factor); // ───────── 탐색기 · 외부 프로그램 ───────── Future isContextMenuRegistered(); Future registerContextMenu(); Future unregisterContextMenu(); /// [program]: 'system' = 기본 연결 프로그램, 그 외 = 실행 파일 경로 Future openExternal(String program, List files); /// 설치된 VLC 경로 (없으면 null) String? get vlcPath; // ───────── 필수 프로그램 ───────── /// 없는 필수 프로그램 설명 (예: "FFmpeg - MKV 만들기 (약 100MB)") Future> missingTools(); /// 없는 필수 프로그램을 공식 배포처에서 내려받아 설치 Future installMissingTools(void Function(String name, double progress) onProgress); /// 프로그램 다시 시작 (새로 설치한 프로그램 인식) Future restart(); } class NoopShell implements AppShell { @override Future init({ required String hotkey, required bool Function() minimizeToTray, required Future Function() onCloseRequested, void Function()? onDownloadsRequested, }) async {} @override Future show() async {} @override Future hide() async {} @override Future setHotkey(String hotkey) async => true; @override Future setTooltip(String text) async {} @override Future quit() async {} @override Future setFullScreen(bool on) async {} @override Future isFullScreen() async => false; @override Future setPopup(bool on) async {} @override Future fitVideo(int width, int height, double factor) async {} @override Future isContextMenuRegistered() async => false; @override Future registerContextMenu() async => false; @override Future unregisterContextMenu() async {} @override Future openExternal(String program, List files) async {} @override String? get vlcPath => null; @override Future> missingTools() async => const []; @override Future installMissingTools(void Function(String name, double progress) onProgress) async {} @override Future restart() async {} } /// "Ctrl+Shift+X" → (수식키 목록, 키 이름). 형식이 틀리면 null. (List, String)? parseHotkey(String text) { final parts = text.split('+').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(); if (parts.length < 2) return null; const mods = {'ctrl', 'shift', 'alt', 'win'}; final m = parts.sublist(0, parts.length - 1).map((s) => s.toLowerCase()).toList(); if (m.any((s) => !mods.contains(s)) || m.toSet().length != m.length) return null; final key = parts.last.toUpperCase(); final ok = RegExp(r'^([A-Z0-9]|F([1-9]|1[0-2]))$').hasMatch(key); return ok ? (m, key) : null; }