시험판 · 시험 (따로 데이터) 이 사용자 시작 메뉴 바로 가기 · 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)",
|
"시작할 때 새 버전 확인 (하루 한 번)": "Check for a new version at startup (once a day)",
|
||||||
"시작할 때마다 설정 창 보기": "Show the setup window at every start",
|
"시작할 때마다 설정 창 보기": "Show the setup window at every start",
|
||||||
"시즌": "Season",
|
"시즌": "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",
|
"시험판": "Pre-release",
|
||||||
"실시간 동기화 (lsyncd 처럼)": "Live sync (like lsyncd)",
|
"실시간 동기화 (lsyncd 처럼)": "Live sync (like lsyncd)",
|
||||||
"실시간 동기화 ({0}): {1} → {2}": "Live sync ({0}): {1} → {2}",
|
"실시간 동기화 ({0}): {1} → {2}": "Live sync ({0}): {1} → {2}",
|
||||||
|
|
|
||||||
|
|
@ -1691,6 +1691,7 @@
|
||||||
"시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)",
|
"시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)",
|
||||||
"시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示",
|
"시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示",
|
||||||
"시즌": "シーズン",
|
"시즌": "シーズン",
|
||||||
|
"시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "テスト用データフォルダー (JJ_MKVMAKER_DATA) で起動したため、スタートメニューのショートカットと jjmkvmaker:// の関連付けは変更しません",
|
||||||
"시험판": "プレリリース",
|
"시험판": "プレリリース",
|
||||||
"실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)",
|
"실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)",
|
||||||
"실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}",
|
"실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}",
|
||||||
|
|
|
||||||
|
|
@ -1691,6 +1691,7 @@
|
||||||
"시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)",
|
"시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)",
|
||||||
"시작할 때마다 설정 창 보기": "每次启动时显示设置窗口",
|
"시작할 때마다 설정 창 보기": "每次启动时显示设置窗口",
|
||||||
"시즌": "季",
|
"시즌": "季",
|
||||||
|
"시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다": "以测试数据文件夹 (JJ_MKVMAKER_DATA) 启动,因此不更改开始菜单快捷方式和 jjmkvmaker:// 关联",
|
||||||
"시험판": "预发布",
|
"시험판": "预发布",
|
||||||
"실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)",
|
"실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)",
|
||||||
"실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}",
|
"실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}",
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,10 @@ Future<void> main(List<String> args) async {
|
||||||
// 99: 시작 메뉴 바로 가기 (앱 ID) · jjmkvmaker:// 연결 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 (설정에서 끔)
|
// 99: 시작 메뉴 바로 가기 (앱 ID) · jjmkvmaker:// 연결 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 (설정에서 끔)
|
||||||
StartMenu.setProcessAppId();
|
StartMenu.setProcessAppId();
|
||||||
final shell0 = services.shell;
|
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) {
|
unawaited(StartMenu.ensure().then((ok) {
|
||||||
if (shell0 is DesktopShell) shell0.toastAppReady = ok;
|
if (shell0 is DesktopShell) shell0.toastAppReady = ok;
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import 'dart:io';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import 'data_dir_override.dart';
|
||||||
|
|
||||||
/// 99 (사용자 결정): 지금 사용자의 시작 메뉴에 JJ_MKVMaker 바로 가기를 앱 ID (AUMID) 와 함께 만들고,
|
/// 99 (사용자 결정): 지금 사용자의 시작 메뉴에 JJ_MKVMaker 바로 가기를 앱 ID (AUMID) 와 함께 만들고,
|
||||||
/// jjmkvmaker:// 주소를 이 앱으로 연결한다 (관리자 권한 없이 - 사용자 영역만).
|
/// jjmkvmaker:// 주소를 이 앱으로 연결한다 (관리자 권한 없이 - 사용자 영역만).
|
||||||
/// 그래야 Windows 알림의 출처가 "JJ_MKVMaker" 로 보이고, 알림을 누르면 앱이 열린다 (jjmkvmaker://lsync).
|
/// 그래야 Windows 알림의 출처가 "JJ_MKVMaker" 로 보이고, 알림을 누르면 앱이 열린다 (jjmkvmaker://lsync).
|
||||||
|
|
@ -12,8 +14,19 @@ class StartMenu {
|
||||||
static const aumid = 'JJ.MKVMaker';
|
static const aumid = 'JJ.MKVMaker';
|
||||||
static const protocol = 'jjmkvmaker';
|
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');
|
'Programs', 'JJ_MKVMaker.lnk');
|
||||||
|
|
||||||
/// 이 프로세스의 앱 ID (작업 표시줄 · 알림이 바로 가기와 같은 앱으로 묶이게)
|
/// 이 프로세스의 앱 ID (작업 표시줄 · 알림이 바로 가기와 같은 앱으로 묶이게)
|
||||||
|
|
@ -35,10 +48,12 @@ class StartMenu {
|
||||||
/// [lnk] · [registerProtocol]: 시험용 (사용자의 시작 메뉴 · 레지스트리를 건드리지 않고)
|
/// [lnk] · [registerProtocol]: 시험용 (사용자의 시작 메뉴 · 레지스트리를 건드리지 않고)
|
||||||
static Future<bool> ensure({String? exe, String? lnk, bool registerProtocol = true}) async {
|
static Future<bool> ensure({String? exe, String? lnk, bool registerProtocol = true}) async {
|
||||||
if (!Platform.isWindows) return false;
|
if (!Platform.isWindows) return false;
|
||||||
|
// 사용자 영역 (시작 메뉴 바로 가기 · HKCU 주소 연결) 을 건드리는 부름만 막는다 - 시험이 따로 준 바로 가기 파일만 쓰면 그대로
|
||||||
|
if ((lnk == null || registerProtocol) && userAreaLocked()) return false;
|
||||||
final target = exe ?? appLauncher();
|
final target = exe ?? appLauncher();
|
||||||
final script = _script(lnk ?? shortcutPath, target, registerProtocol: registerProtocol);
|
final script = _script(lnk ?? shortcutPath, target, registerProtocol: registerProtocol);
|
||||||
try {
|
try {
|
||||||
final r = await Process.run(
|
final r = await run(
|
||||||
'powershell.exe',
|
'powershell.exe',
|
||||||
['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-EncodedCommand', _encode(script)],
|
['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-EncodedCommand', _encode(script)],
|
||||||
).timeout(const Duration(seconds: 60));
|
).timeout(const Duration(seconds: 60));
|
||||||
|
|
@ -61,11 +76,11 @@ class StartMenu {
|
||||||
|
|
||||||
/// 설정에서 끄면: 바로 가기 · 주소 연결을 지운다 (이 앱이 만든 것만)
|
/// 설정에서 끄면: 바로 가기 · 주소 연결을 지운다 (이 앱이 만든 것만)
|
||||||
static Future<void> remove() async {
|
static Future<void> remove() async {
|
||||||
if (!Platform.isWindows) return;
|
if (!Platform.isWindows || userAreaLocked()) return;
|
||||||
try {
|
try {
|
||||||
final f = File(shortcutPath);
|
final f = File(shortcutPath);
|
||||||
if (await f.exists()) await f.delete();
|
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 (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1141,7 +1141,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: Text(tr('시작 메뉴에 등록')),
|
title: Text(tr('시작 메뉴에 등록')),
|
||||||
subtitle: Text(tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
|
subtitle: Text(StartMenu.userAreaLocked()
|
||||||
|
? tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다')
|
||||||
|
: tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
|
||||||
),
|
),
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')),
|
title: Text(tr('종료 (창 닫기 ✕ · 종료 버튼) 를 누르면')),
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,43 @@ void main() {
|
||||||
expect(await read('id'), StartMenu.aumid);
|
expect(await read('id'), StartMenu.aumid);
|
||||||
}, skip: !Platform.isWindows, timeout: const Timeout(Duration(minutes: 2)));
|
}, 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) 는 실시간 동기화 화면 요청', () {
|
test('알림을 누르면 오는 주소 (jjmkvmaker://lsync) 는 실시간 동기화 화면 요청', () {
|
||||||
expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).action, LaunchAction.lsync);
|
expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).action, LaunchAction.lsync);
|
||||||
expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).files, isEmpty);
|
expect(LaunchRequest.parse(['jjmkvmaker://lsync/']).files, isEmpty);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue