시험판 · 시험 (따로 데이터) 이 사용자 시작 메뉴 바로 가기 · jjmkvmaker:// 를 바꾸거나 지우지 않게 (관리자 지시 10/20, 15 찾음, 버그)
- 원인: 앱은 켤 때마다 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 <noreply@anthropic.com>rc2
parent
1b3160939c
commit
121dd4350b
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -1691,6 +1691,7 @@
|
|||
"시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)",
|
||||
"시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示",
|
||||
"시즌": "シーズン",
|
||||
"시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "テスト用データフォルダー (JJ_MKVMAKER_DATA) で起動したため、スタートメニューのショートカットと jjmkvmaker:// の関連付けは変更しません",
|
||||
"시험판": "プレリリース",
|
||||
"실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)",
|
||||
"실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}",
|
||||
|
|
|
|||
|
|
@ -1691,6 +1691,7 @@
|
|||
"시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)",
|
||||
"시작할 때마다 설정 창 보기": "每次启动时显示设置窗口",
|
||||
"시즌": "季",
|
||||
"시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "以测试数据文件夹 (JJ_MKVMAKER_DATA) 启动,因此不更改开始菜单快捷方式和 jjmkvmaker:// 关联",
|
||||
"시험판": "预发布",
|
||||
"실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)",
|
||||
"실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}",
|
||||
|
|
|
|||
|
|
@ -262,7 +262,10 @@ Future<void> main(List<String> 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;
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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<ProcessResult> Function(String exe, List<String> 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<bool> 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<void> 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 (_) {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1141,7 +1141,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
}
|
||||
},
|
||||
title: Text(tr('시작 메뉴에 등록')),
|
||||
subtitle: Text(tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
|
||||
subtitle: Text(StartMenu.userAreaLocked()
|
||||
? tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다')
|
||||
: tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')),
|
||||
|
|
|
|||
|
|
@ -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 = <String>[];
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue