JJ_MKVMaker/lib/services/app_shell.dart

157 lines
6.0 KiB
Dart

/// 창 · 트레이 · 전역 단축키 경계 (데스크톱 전용 기능)
///
/// Windows: platform/windows/desktop_shell.dart
/// Android·테스트: [NoopShell]
abstract class AppShell {
/// [onShowRequested]: 트레이 클릭·단축키로 창 보이기
/// [onCloseRequested]: 창의 X 또는 트레이 "종료" (true 를 돌려주면 종료)
/// [onDownloadsRequested]: 트레이 "다운로드 목록"
Future<void> init({
required String hotkey,
required bool Function() minimizeToTray,
required Future<bool> Function() onCloseRequested,
void Function()? onDownloadsRequested,
});
Future<void> show();
Future<void> hide();
/// 단축키 변경 (예: "Ctrl+Shift+X"). 형식이 틀리면 false.
Future<bool> setHotkey(String hotkey);
/// 트레이 알림 글 (다운로드 개수 등)
Future<void> setTooltip(String text);
/// 프로그램 종료
Future<void> quit();
/// 앱 아이콘 바꾸기 ([appIconIds] 중 하나). Windows: 창 · 작업 표시줄 · 트레이, Android: 앱 목록 · 홈 화면
Future<void> setAppIcon(String id);
// ───────── 플레이어 창 ─────────
Future<void> setFullScreen(bool on);
Future<bool> isFullScreen();
/// 팝업 보기: 작은 창 · 항상 위 · 제목 표시줄 없음. 끄면 원래 크기로.
Future<void> setPopup(bool on);
/// 창 크기를 영상 크기의 [factor] 배로 (화면보다 크면 화면에 맞춤)
Future<void> fitVideo(int width, int height, double factor);
// ───────── 탐색기 · 외부 프로그램 ─────────
Future<bool> isContextMenuRegistered();
Future<bool> registerContextMenu();
Future<void> unregisterContextMenu();
/// [program]: 'system' = 기본 연결 프로그램, 그 외 = 실행 파일 경로
Future<void> openExternal(String program, List<String> files);
/// 설치된 VLC 경로 (없으면 null)
String? get vlcPath;
/// 웹 주소를 외부 브라우저로 열기. [browser]: 'system' · 'chrome' · 'firefox' · 'edge' · 'whale'
Future<void> openUrl(String url, {String browser = 'system'});
/// 파일이 있는 폴더를 파일 관리자(탐색기)로 열고 그 파일을 선택해 보여 준다
Future<void> revealFile(String path);
/// 파일을 다른 프로그램으로 열기 (파일 탐색기). [choose]: 프로그램 고르기 창을 띄움, 아니면 기본 연결 프로그램.
/// 열지 못하면 false.
Future<bool> openWith(String path, {bool choose = false});
/// 설치된 외부 브라우저 ('chrome', 'firefox', 'edge', 'whale' 중)
List<String> installedBrowsers();
// ───────── 필수 프로그램 ─────────
/// 없는 필수 프로그램 설명 (예: "FFmpeg - MKV 만들기 (약 100MB)")
Future<List<String>> missingTools();
/// 없는 필수 프로그램을 공식 배포처에서 내려받아 설치
Future<void> installMissingTools(void Function(String name, double progress) onProgress);
/// 프로그램 다시 시작 (새로 설치한 프로그램 인식)
Future<void> restart();
}
class NoopShell implements AppShell {
@override
Future<void> init({
required String hotkey,
required bool Function() minimizeToTray,
required Future<bool> Function() onCloseRequested,
void Function()? onDownloadsRequested,
}) async {}
@override
Future<void> show() async {}
@override
Future<void> hide() async {}
@override
Future<bool> setHotkey(String hotkey) async => true;
@override
Future<void> setTooltip(String text) async {}
@override
Future<void> quit() async {}
@override
Future<void> setAppIcon(String id) async {}
@override
Future<void> setFullScreen(bool on) async {}
@override
Future<bool> isFullScreen() async => false;
@override
Future<void> setPopup(bool on) async {}
@override
Future<void> fitVideo(int width, int height, double factor) async {}
@override
Future<bool> isContextMenuRegistered() async => false;
@override
Future<bool> registerContextMenu() async => false;
@override
Future<void> unregisterContextMenu() async {}
@override
Future<void> openExternal(String program, List<String> files) async {}
@override
String? get vlcPath => null;
@override
Future<void> openUrl(String url, {String browser = 'system'}) async {}
@override
Future<void> revealFile(String path) async {}
@override
Future<bool> openWith(String path, {bool choose = false}) async => false;
@override
List<String> installedBrowsers() => const [];
@override
Future<List<String>> missingTools() async => const [];
@override
Future<void> installMissingTools(void Function(String name, double progress) onProgress) async {}
@override
Future<void> restart() async {}
}
/// 고를 수 있는 앱 아이콘 (환경 설정 > 일반 > 앱 아이콘). 첫 번째가 기본.
/// 그림은 tool/make_icon.py 가 만든다: `assets/icon/variants/<id>_256.png` · `<id>_button.png` · `<id>.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>, 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;
}