From 121dd4350bcdaee1f26f67b971bcec8683a69c18 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:03:12 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=ED=97=98=ED=8C=90=20=C2=B7=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=20(=EB=94=B0=EB=A1=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0)=20=EC=9D=B4=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EB=A9=94=EB=89=B4=20=EB=B0=94=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EA=B8=B0=20=C2=B7=20jjmkvmaker://=20=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=94=EA=BE=B8=EA=B1=B0=EB=82=98=20=EC=A7=80=EC=9A=B0?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EA=B2=8C=20(=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=20=EC=A7=80=EC=8B=9C=2010/20,=2015=20=EC=B0=BE=EC=9D=8C,=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원인: 앱은 켤 때마다 StartMenu.ensure() (설정을 끄면 remove()) 로 사용자 시작 메뉴의 JJ_MKVMaker.lnk 와 HKCU\Software\Classes\jjmkvmaker 를 지금 실행 파일로 바꾼다. JJ_MKVMAKER_DATA (시험판 · 시험의 따로 데이터) 로 켜도 그랬다 - 시험 폴더의 판을 켜면 사용자 바로 가기 · 알림 누르기가 그 판을 가리키고, 시험 폴더를 지우면 깨졌다 (10.18_001 도 같음) - "따로 데이터" 는 코드에서 JJ_MKVMAKER_DATA 하나뿐 (시험판 표시 파일 없음) - 그때 StartMenu 가 사용자 영역을 건드리는 ensure (바로 가기 · 주소 연결) · remove 를 막음. 시험이 따로 준 바로 가기 파일만 쓰는 부름은 그대로. 켤 때 작업 기록에 한 줄, 환경 설정의 [시작 메뉴에 등록] 아래 글도 그 사실로 (en · ja · zh) - 사용자 판 (JJ_MKVMAKER_DATA 없음) 동작은 그대로 - 시험: 명령 실행을 가짜로 바꿔 (StartMenu.run) 막힌 동안 아무것도 부르지 않음 · 바로 가기 파일도 시험 폴더로 (shortcutPathOverride - 시험이 잘못돼도 사용자 것을 지우지 않게). 시험 전후 사용자 바로 가기 · 주소 연결이 그대로인지 확인함 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 1 + assets/l10n/ja.json | 1 + assets/l10n/zh-Hans.json | 1 + lib/main.dart | 5 +++- lib/platform/windows/start_menu.dart | 23 ++++++++++++++--- lib/ui/settings_page.dart | 4 ++- test/start_menu_test.dart | 37 ++++++++++++++++++++++++++++ 7 files changed, 66 insertions(+), 6 deletions(-) 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);