diff --git a/assets/l10n/en.json b/assets/l10n/en.json index b84387f..e685d2f 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1691,6 +1691,7 @@ "시작할 때 새 버전 확인 (하루 한 번)": "Check for a new version at startup (once a day)", "시작할 때마다 설정 창 보기": "Show the setup window at every start", "시즌": "Season", + "시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "Started with a test data folder (JJ_MKVMAKER_DATA), so the Start menu shortcut and the jjmkvmaker:// link are left unchanged", "시험판": "Pre-release", "실시간 동기화 (lsyncd 처럼)": "Live sync (like lsyncd)", "실시간 동기화 ({0}): {1} → {2}": "Live sync ({0}): {1} → {2}", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 68d1cff..56e525f 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1691,6 +1691,7 @@ "시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)", "시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示", "시즌": "シーズン", + "시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "テスト用データフォルダー (JJ_MKVMAKER_DATA) で起動したため、スタートメニューのショートカットと jjmkvmaker:// の関連付けは変更しません", "시험판": "プレリリース", "실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)", "실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 299f513..c027a46 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1691,6 +1691,7 @@ "시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)", "시작할 때마다 설정 창 보기": "每次启动时显示设置窗口", "시즌": "季", + "시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "以测试数据文件夹 (JJ_MKVMAKER_DATA) 启动,因此不更改开始菜单快捷方式和 jjmkvmaker:// 关联", "시험판": "预发布", "실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)", "실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}", diff --git a/lib/main.dart b/lib/main.dart index 14812a1..311219f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -262,7 +262,10 @@ Future main(List args) async { // 99: 시작 메뉴 바로 가기 (앱 ID) · jjmkvmaker:// 연결 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 (설정에서 끔) StartMenu.setProcessAppId(); final shell0 = services.shell; - if (controller.settings.startMenuShortcut) { + if (StartMenu.userAreaLocked()) { + // 관리자 지시 (10/20): 시험판 · 시험 (따로 데이터) - 사용자 바로 가기 · 주소 연결은 그대로 (StartMenu 가 막음), 작업 기록에 한 줄 + controller.note(tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다')); + } else if (controller.settings.startMenuShortcut) { unawaited(StartMenu.ensure().then((ok) { if (shell0 is DesktopShell) shell0.toastAppReady = ok; })); diff --git a/lib/platform/windows/start_menu.dart b/lib/platform/windows/start_menu.dart index 4927c8c..9d0cfae 100644 --- a/lib/platform/windows/start_menu.dart +++ b/lib/platform/windows/start_menu.dart @@ -5,6 +5,8 @@ import 'dart:io'; import 'package:ffi/ffi.dart'; import 'package:path/path.dart' as p; +import 'data_dir_override.dart'; + /// 99 (사용자 결정): 지금 사용자의 시작 메뉴에 JJ_MKVMaker 바로 가기를 앱 ID (AUMID) 와 함께 만들고, /// jjmkvmaker:// 주소를 이 앱으로 연결한다 (관리자 권한 없이 - 사용자 영역만). /// 그래야 Windows 알림의 출처가 "JJ_MKVMaker" 로 보이고, 알림을 누르면 앱이 열린다 (jjmkvmaker://lsync). @@ -12,8 +14,19 @@ class StartMenu { static const aumid = 'JJ.MKVMaker'; static const protocol = 'jjmkvmaker'; + /// 관리자 지시 (10/20, 15 찾음): 시험판 · 시험 (따로 데이터 JJ_MKVMAKER_DATA) 이면 사용자의 시작 메뉴 바로 가기 · + /// jjmkvmaker:// 를 자기 쪽으로 바꾸거나 지우지 않는다. 예전에는 시험 폴더의 판을 켜면 사용자 바로 가기가 그 판을 가리키고, + /// 시험 폴더를 지우면 시작 메뉴 · 알림 누르기가 깨졌다. 시험이 바꿀 수 있게 함수로 둔다 + static bool Function() userAreaLocked = () => dataDirOverride != null; + + /// 바로 가기 · 레지스트리를 바꾸는 명령 실행 (시험이 가짜로 바꿔 무엇을 부르는지 본다 - 사용자 영역을 건드리지 않고) + static Future Function(String exe, List args) run = Process.run; + + /// 시험용: 바로 가기 파일 자리 (null = 사용자의 시작 메뉴). 시험이 잘못돼도 사용자 바로 가기를 지우지 않게 + static String? shortcutPathOverride; + /// 바로 가기 파일 (사용자의 시작 메뉴) - static String get shortcutPath => p.join(Platform.environment['APPDATA'] ?? '', 'Microsoft', 'Windows', 'Start Menu', + static String get shortcutPath => shortcutPathOverride ?? p.join(Platform.environment['APPDATA'] ?? '', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'JJ_MKVMaker.lnk'); /// 이 프로세스의 앱 ID (작업 표시줄 · 알림이 바로 가기와 같은 앱으로 묶이게) @@ -35,10 +48,12 @@ class StartMenu { /// [lnk] · [registerProtocol]: 시험용 (사용자의 시작 메뉴 · 레지스트리를 건드리지 않고) static Future ensure({String? exe, String? lnk, bool registerProtocol = true}) async { if (!Platform.isWindows) return false; + // 사용자 영역 (시작 메뉴 바로 가기 · HKCU 주소 연결) 을 건드리는 부름만 막는다 - 시험이 따로 준 바로 가기 파일만 쓰면 그대로 + if ((lnk == null || registerProtocol) && userAreaLocked()) return false; final target = exe ?? appLauncher(); final script = _script(lnk ?? shortcutPath, target, registerProtocol: registerProtocol); try { - final r = await Process.run( + final r = await run( 'powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-EncodedCommand', _encode(script)], ).timeout(const Duration(seconds: 60)); @@ -61,11 +76,11 @@ class StartMenu { /// 설정에서 끄면: 바로 가기 · 주소 연결을 지운다 (이 앱이 만든 것만) static Future remove() async { - if (!Platform.isWindows) return; + if (!Platform.isWindows || userAreaLocked()) return; try { final f = File(shortcutPath); if (await f.exists()) await f.delete(); - await Process.run('reg.exe', ['delete', r'HKCU\Software\Classes\' + protocol, '/f']); + await run('reg.exe', ['delete', r'HKCU\Software\Classes\' + protocol, '/f']); } catch (_) {} } diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 676b9d5..88cbe31 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -1141,7 +1141,9 @@ class _SettingsPageState extends State { } }, title: Text(tr('시작 메뉴에 등록')), - subtitle: Text(tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')), + subtitle: Text(StartMenu.userAreaLocked() + ? tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다') + : tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')), ), SettingTile( title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')), diff --git a/test/start_menu_test.dart b/test/start_menu_test.dart index 0c3cafe..dc7d0a3 100644 --- a/test/start_menu_test.dart +++ b/test/start_menu_test.dart @@ -32,6 +32,43 @@ void main() { expect(await read('id'), StartMenu.aumid); }, skip: !Platform.isWindows, timeout: const Timeout(Duration(minutes: 2))); + test('관리자 지시 (10/20): 시험판 · 시험 (따로 데이터) 이면 사용자 시작 메뉴 · jjmkvmaker:// 를 바꾸거나 지우지 않는다', () async { + // 명령은 가짜로만 (사용자 영역을 건드리지 않음) - 무엇을 부르는지만 본다 + final calls = []; + final runBefore = StartMenu.run, lockedBefore = StartMenu.userAreaLocked; + final tmp = Directory.systemTemp.createTempSync('jj_lnk_'); + addTearDown(() { + StartMenu.run = runBefore; + StartMenu.userAreaLocked = lockedBefore; + StartMenu.shortcutPathOverride = null; + tmp.deleteSync(recursive: true); + }); + // 바로 가기 파일도 시험 폴더로 (시험이 잘못돼도 사용자 시작 메뉴의 것을 지우지 않게) + final lnk = p.join(tmp.path, 'JJ_MKVMaker.lnk'); + StartMenu.shortcutPathOverride = lnk; + File(lnk).writeAsStringSync('x'); + StartMenu.run = (exe, args) async { + calls.add(exe); + return ProcessResult(0, 0, 'JJ_OK', ''); + }; + StartMenu.userAreaLocked = () => true; + expect(await StartMenu.ensure(), isFalse, reason: '사용자 바로 가기 · 주소 연결'); + expect(await StartMenu.ensure(lnk: lnk), isFalse, reason: '주소 연결 (HKCU) 은 사용자 영역'); + await StartMenu.remove(); + expect(calls, isEmpty, reason: '막힌 동안은 아무 명령도 부르지 않음'); + expect(File(lnk).existsSync(), isTrue, reason: '바로 가기를 지우지 않음'); + // 시험이 따로 준 바로 가기 파일만 쓰는 것은 그대로 + expect(await StartMenu.ensure(exe: Platform.resolvedExecutable, lnk: lnk, registerProtocol: false), isTrue); + expect(calls, ['powershell.exe']); + // 막지 않으면 (사용자 판) 예전처럼 부른다 + StartMenu.userAreaLocked = () => false; + calls.clear(); + expect(await StartMenu.ensure(), isTrue); + await StartMenu.remove(); + expect(calls, ['powershell.exe', 'reg.exe']); + expect(File(lnk).existsSync(), isFalse, reason: '사용자 판에서 끄면 지움 (여기서는 시험 폴더의 것)'); + }, skip: !Platform.isWindows); + test('알림을 누르면 오는 주소 (jjmkvmaker://lsync) 는 실시간 동기화 화면 요청', () { expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).action, LaunchAction.lsync); expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).files, isEmpty);