/// 창 · 트레이 · 전역 단축키 경계 (데스크톱 전용 기능) /// /// 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); /// 운영 체제 알림 (Windows 토스트). [onClick]: 알림을 누르면 (창을 보인 뒤). 띄웠으면 true Future notify(String title, String body, {void Function()? onClick}); /// 트레이 아이콘을 눌러 창을 보인 뒤 (예: 멈춘 동기화가 있으면 그 카드로 - 92) set onTrayShown(void Function()? f); /// 창이 앞에 보이는지 (숨김 · 최소화 · 다른 창 뒤면 false) Future isInFront(); /// 프로그램 종료 Future quit(); /// 앱 아이콘 바꾸기 ([appIconIds] 중 하나). Windows: 창 · 작업 표시줄 · 트레이, Android: 앱 목록 · 홈 화면 Future setAppIcon(String id); // ───────── 플레이어 창 ───────── 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; /// 웹 주소를 외부 브라우저로 열기. [browser]: 'system' · 'chrome' · 'firefox' · 'edge' · 'whale' Future openUrl(String url, {String browser = 'system'}); /// 파일이 있는 폴더를 파일 관리자(탐색기)로 열고 그 파일을 선택해 보여 준다 Future revealFile(String path); /// 파일을 다른 프로그램으로 열기 (파일 탐색기). [choose]: 프로그램 고르기 창을 띄움, 아니면 기본 연결 프로그램. /// 열지 못하면 false. Future openWith(String path, {bool choose = false}); /// 77: 파일을 다른 앱으로 보내기 (공유 창). 이 기기에서 못 하면 [canShare] 가 false bool get canShare; Future shareFiles(List paths); /// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중) List installedBrowsers(); // ───────── 필수 프로그램 ───────── /// 없는 필수 프로그램 설명 (예: "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 notify(String title, String body, {void Function()? onClick}) async => false; @override set onTrayShown(void Function()? f) {} @override Future isInFront() async => true; @override Future quit() async {} @override Future setAppIcon(String id) 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 openUrl(String url, {String browser = 'system'}) async {} @override Future revealFile(String path) async {} @override Future openWith(String path, {bool choose = false}) async => false; @override bool get canShare => false; @override Future shareFiles(List paths) async => false; @override List installedBrowsers() => const []; @override Future> missingTools() async => const []; @override Future installMissingTools(void Function(String name, double progress) onProgress) async {} @override Future restart() async {} } /// 고를 수 있는 앱 아이콘 (환경 설정 > 일반 > 앱 아이콘). 첫 번째가 기본. /// 그림은 tool/make_icon.py 가 만든다: `assets/icon/variants/_256.png` · `_button.png` · `.ico` const appIconIds = ['yellow', 'black', 'film_jj', 'film', 'blue']; /// 저장된 값이 모르는 것이면 기본 아이콘 String appIconOf(String? id) => appIconIds.contains(id) ? id! : appIconIds.first; /// 앱 안 왼쪽 위 홈 버튼 그림 String appIconButtonAsset(String? id) => 'assets/icon/variants/${appIconOf(id)}_button.png'; /// 설정에서 고를 때 보여 주는 큰 그림 String appIconPreviewAsset(String id) => 'assets/icon/variants/${appIconOf(id)}_256.png'; /// "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; }