M4 3묶음: 시작 메뉴 (StartMenu) 의 static 셋을 인스턴스 · PlatformServices.startMenu 주입으로 - 시험이 사용자 시작 메뉴를 건드리지 않게
- StartMenu 는 run · userAreaLocked · shortcutPath 를 가진 인스턴스. StartMenu.user = 사용자 시작 메뉴 · HKCU (앱 조립 create 가 넘김), StartMenu.locked(dir) = 늘 막힘 · 바로 가기 자리는 dir 아래 (PlatformServices 를 손으로 조립할 때의 처음 값). 예전 static run · userAreaLocked · shortcutPathOverride 를 바꾸던 것을 대신 - main · 설정 [시작 메뉴에 등록] 은 c.services.startMenu 를 부른다 - 시험: start_menu_test 는 가짜 run · 시험 폴더 바로 가기를 넘긴 인스턴스로 (사용자 영역 0), 손 조립 처음 값이 막혀 있고 사용자 시작 메뉴가 아님을 새로 봄 (2회 통과). start_menu_real_test (실제 사용자 시작 메뉴) 는 JJ_TEST_LAUNCHER 가 있을 때만 - 예전과 같음 - 사용자가 보게 될 동작: 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
2913332a8c
commit
e82c34a0b8
|
|
@ -16,10 +16,10 @@ void main() {
|
|||
Future<String> ps(String cmd) async => '${(await Process.run('powershell.exe', ['-NoProfile', '-Command', cmd])).stdout}'.trim();
|
||||
|
||||
testWidgets('시작 메뉴 바로 가기 (AUMID) · jjmkvmaker:// · JJ_MKVMaker 이름의 알림 · 폴더를 옮기면 고침 · 정리', (t) async {
|
||||
final lnk = StartMenu.shortcutPath;
|
||||
final lnk = StartMenu.user.shortcutPath;
|
||||
expect(File(lnk).existsSync(), isFalse, reason: '시험 전에는 없어야 한다');
|
||||
try {
|
||||
expect(await StartMenu.ensure(exe: launcher), isTrue);
|
||||
expect(await StartMenu.user.ensure(exe: launcher), isTrue);
|
||||
expect(File(lnk).existsSync(), isTrue);
|
||||
final dir = p.dirname(lnk), name = p.basename(lnk);
|
||||
expect(await ps("(New-Object -ComObject Shell.Application).Namespace('$dir').ParseName('$name').ExtendedProperty('System.AppUserModel.ID')"),
|
||||
|
|
@ -39,10 +39,10 @@ void main() {
|
|||
}
|
||||
// 앱 폴더를 옮긴 경우: 다음 실행 때 새 위치로 고친다
|
||||
final moved = p.join(p.dirname(launcher), 'Lib', p.basename(launcher));
|
||||
expect(await StartMenu.ensure(exe: moved), isTrue);
|
||||
expect(await StartMenu.user.ensure(exe: moved), isTrue);
|
||||
expect((await ps("(New-Object -ComObject WScript.Shell).CreateShortcut('$lnk').TargetPath")).toLowerCase(), moved.toLowerCase());
|
||||
} finally {
|
||||
await StartMenu.remove();
|
||||
await StartMenu.user.remove();
|
||||
}
|
||||
expect(File(lnk).existsSync(), isFalse);
|
||||
expect(await ps(r"Test-Path 'HKCU:\Software\Classes\jjmkvmaker'"), 'False');
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ PlatformServices shotServices() {
|
|||
privileged: r.privileged,
|
||||
isMetered: r.isMetered,
|
||||
keepScreenOn: r.keepScreenOn,
|
||||
startMenu: r.startMenu,
|
||||
updater: r.updater,
|
||||
usage: r.usage == null ? null : const FixedUsage(),
|
||||
models: r.models,
|
||||
|
|
|
|||
|
|
@ -141,15 +141,16 @@ Future<void> main(List<String> args) async {
|
|||
// 99: 시작 메뉴 바로 가기 (앱 ID) · jjmkvmaker:// 연결 - 알림이 JJ_MKVMaker 로 보이고 눌러서 열린다 (설정에서 끔)
|
||||
StartMenu.setProcessAppId();
|
||||
final shell0 = services.shell;
|
||||
if (StartMenu.userAreaLocked()) {
|
||||
final startMenu = services.startMenu;
|
||||
if (startMenu.userAreaLocked()) {
|
||||
// 관리자 지시 (10/20): 시험판 · 시험 (따로 데이터) - 사용자 바로 가기 · 주소 연결은 그대로 (StartMenu 가 막음), 작업 기록에 한 줄
|
||||
controller.note(tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다'));
|
||||
} else if (controller.settings.run.startMenuShortcut) {
|
||||
unawaited(StartMenu.ensure().then((ok) {
|
||||
unawaited(startMenu.ensure().then((ok) {
|
||||
if (shell0 is DesktopShell) shell0.toastAppReady = ok;
|
||||
}));
|
||||
} else {
|
||||
unawaited(StartMenu.remove());
|
||||
unawaited(startMenu.remove());
|
||||
}
|
||||
// 제목 표시줄에 버전 · 지난번 창 위치에서 열기 · 동영상 목록 불러오기 (창끼리 공유)
|
||||
await windowManager.setTitle(appTitle);
|
||||
|
|
|
|||
|
|
@ -14,20 +14,30 @@ class StartMenu {
|
|||
static const aumid = 'JJ.MKVMaker';
|
||||
static const protocol = 'jjmkvmaker';
|
||||
|
||||
/// 설계 M4: 바로 가기 · 주소 연결을 바꾸는 쪽은 인스턴스 ([user] = 이 기기의 사용자 영역). 시험은 가짜 [run] · 시험 폴더의 [shortcutPath] 를
|
||||
/// 넘긴 인스턴스를 만든다 (예전 static run · userAreaLocked · shortcutPathOverride 를 바꾸던 것 - 사용자 영역을 건드리지 않고)
|
||||
StartMenu({Future<ProcessResult> Function(String exe, List<String> args)? run, bool Function()? userAreaLocked, String? shortcutPath})
|
||||
: run = run ?? Process.run,
|
||||
userAreaLocked = userAreaLocked ?? (() => dataDirOverride != null),
|
||||
shortcutPath = shortcutPath ??
|
||||
p.join(Platform.environment['APPDATA'] ?? '', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'JJ_MKVMaker.lnk');
|
||||
|
||||
/// 이 기기의 사용자 시작 메뉴 · HKCU (앱 조립 PlatformServices.create 가 넘김)
|
||||
static final user = StartMenu();
|
||||
|
||||
/// 사용자 영역 (시작 메뉴 · HKCU) 을 건드리지 않는 것 - 바로 가기 자리는 [dir] 아래, 늘 막힘. PlatformServices 를 손으로 조립할 때의 처음 값
|
||||
factory StartMenu.locked(String dir) => StartMenu(userAreaLocked: () => true, shortcutPath: p.join(dir, 'JJ_MKVMaker.lnk'));
|
||||
|
||||
/// 관리자 지시 (10/20, 15 찾음): 시험판 · 시험 (따로 데이터 JJ_MKVMAKER_DATA) 이면 사용자의 시작 메뉴 바로 가기 ·
|
||||
/// jjmkvmaker:// 를 자기 쪽으로 바꾸거나 지우지 않는다. 예전에는 시험 폴더의 판을 켜면 사용자 바로 가기가 그 판을 가리키고,
|
||||
/// 시험 폴더를 지우면 시작 메뉴 · 알림 누르기가 깨졌다. 시험이 바꿀 수 있게 함수로 둔다
|
||||
static bool Function() userAreaLocked = () => dataDirOverride != null;
|
||||
/// 시험 폴더를 지우면 시작 메뉴 · 알림 누르기가 깨졌다
|
||||
final bool Function() userAreaLocked;
|
||||
|
||||
/// 바로 가기 · 레지스트리를 바꾸는 명령 실행 (시험이 가짜로 바꿔 무엇을 부르는지 본다 - 사용자 영역을 건드리지 않고)
|
||||
static Future<ProcessResult> Function(String exe, List<String> args) run = Process.run;
|
||||
/// 바로 가기 · 레지스트리를 바꾸는 명령 실행
|
||||
final Future<ProcessResult> Function(String exe, List<String> args) run;
|
||||
|
||||
/// 시험용: 바로 가기 파일 자리 (null = 사용자의 시작 메뉴). 시험이 잘못돼도 사용자 바로 가기를 지우지 않게
|
||||
static String? shortcutPathOverride;
|
||||
|
||||
/// 바로 가기 파일 (사용자의 시작 메뉴)
|
||||
static String get shortcutPath => shortcutPathOverride ?? p.join(Platform.environment['APPDATA'] ?? '', 'Microsoft', 'Windows', 'Start Menu',
|
||||
'Programs', 'JJ_MKVMaker.lnk');
|
||||
/// 바로 가기 파일 (처음 값 = 사용자의 시작 메뉴)
|
||||
final String shortcutPath;
|
||||
|
||||
/// 이 프로세스의 앱 ID (작업 표시줄 · 알림이 바로 가기와 같은 앱으로 묶이게)
|
||||
static void setProcessAppId() {
|
||||
|
|
@ -46,7 +56,7 @@ class StartMenu {
|
|||
|
||||
/// 바로 가기 · 주소 연결을 만든다 (있고 이 실행 파일을 가리키면 그대로, 앱 폴더를 옮겼으면 고친다). 됐으면 true
|
||||
/// [lnk] · [registerProtocol]: 시험용 (사용자의 시작 메뉴 · 레지스트리를 건드리지 않고)
|
||||
static Future<bool> ensure({String? exe, String? lnk, bool registerProtocol = true}) async {
|
||||
Future<bool> ensure({String? exe, String? lnk, bool registerProtocol = true}) async {
|
||||
if (!Platform.isWindows) return false;
|
||||
// 사용자 영역 (시작 메뉴 바로 가기 · HKCU 주소 연결) 을 건드리는 부름만 막는다 - 시험이 따로 준 바로 가기 파일만 쓰면 그대로
|
||||
if ((lnk == null || registerProtocol) && userAreaLocked()) return false;
|
||||
|
|
@ -75,7 +85,7 @@ class StartMenu {
|
|||
}
|
||||
|
||||
/// 설정에서 끄면: 바로 가기 · 주소 연결을 지운다 (이 앱이 만든 것만)
|
||||
static Future<void> remove() async {
|
||||
Future<void> remove() async {
|
||||
if (!Platform.isWindows || userAreaLocked()) return;
|
||||
try {
|
||||
final f = File(shortcutPath);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import '../platform/android/ffmpeg_kit_media_tool.dart';
|
|||
import '../platform/windows/aria2_backend.dart';
|
||||
import '../platform/windows/desktop_shell.dart';
|
||||
import '../platform/windows/desktop_storage_service.dart';
|
||||
import '../platform/windows/start_menu.dart';
|
||||
import '../platform/windows/github_updater.dart';
|
||||
import '../platform/windows/process_media_tool.dart';
|
||||
import '../platform/windows/windows_usage.dart';
|
||||
|
|
@ -95,6 +96,10 @@ class PlatformServices {
|
|||
/// 화면을 켜 둔다 / 놓는다 (보기 화면의 자동 넘기기 - 설계 M4 WakeLock). 처음 값 = WakelockPlus
|
||||
final Future<void> Function(bool on) keepScreenOn;
|
||||
|
||||
/// 시작 메뉴 바로 가기 · jjmkvmaker:// 연결 (Windows, 설계 M4). 앱 조립은 [StartMenu.user] (사용자 영역),
|
||||
/// 손으로 조립하면 처음 값은 [StartMenu.locked] (사용자 시작 메뉴 · HKCU 를 건드리지 않음 - 시험이 실제 바로 가기를 만들거나 지우지 않게)
|
||||
final StartMenu startMenu;
|
||||
|
||||
/// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기)
|
||||
final PlatformInfo platform;
|
||||
|
||||
|
|
@ -123,10 +128,12 @@ class PlatformServices {
|
|||
this.privileged,
|
||||
Future<bool> Function()? isMetered,
|
||||
Future<void> Function(bool on)? keepScreenOn,
|
||||
StartMenu? startMenu,
|
||||
}) : models = models ?? ModelStore(),
|
||||
vault = vault ?? VaultLocation.under(storage.tempDirectory),
|
||||
isMetered = isMetered ?? deviceMetered,
|
||||
keepScreenOn = keepScreenOn ?? _wakeLock,
|
||||
startMenu = startMenu ?? StartMenu.locked(Directory.systemTemp.path),
|
||||
platform = platform ?? PlatformInfo.host,
|
||||
shell = shell ?? NoopShell();
|
||||
|
||||
|
|
@ -137,6 +144,7 @@ class PlatformServices {
|
|||
final ffmpegDir = File(tool.ffmpeg).existsSync() ? p.dirname(tool.ffmpeg) : null;
|
||||
return PlatformServices(
|
||||
vault: VaultLocation.device,
|
||||
startMenu: StartMenu.user,
|
||||
mediaTool: tool,
|
||||
storage: DesktopStorageService(),
|
||||
createPlayer: MediaKitPreviewPlayer.new,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import '../../services/app_shell.dart';
|
|||
import '../setup_dialog.dart';
|
||||
import '../update_dialog.dart';
|
||||
import '../../platform/windows/desktop_shell.dart' show DesktopShell;
|
||||
import '../../platform/windows/start_menu.dart';
|
||||
import '../app_actions.dart';
|
||||
import '../cleanup_dialog.dart';
|
||||
import '../theme.dart';
|
||||
|
|
@ -64,17 +63,17 @@ class _RunSettingsSectionState extends State<RunSettingsSection> {
|
|||
onChanged: (v) async {
|
||||
await c.updateSettings((x) => x.run.startMenuShortcut = v);
|
||||
if (v) {
|
||||
final ok = await StartMenu.ensure();
|
||||
final ok = await c.services.startMenu.ensure();
|
||||
final sh = c.services.shell;
|
||||
if (sh is DesktopShell) sh.toastAppReady = ok;
|
||||
} else {
|
||||
await StartMenu.remove();
|
||||
await c.services.startMenu.remove();
|
||||
final sh = c.services.shell;
|
||||
if (sh is DesktopShell) sh.toastAppReady = false;
|
||||
}
|
||||
},
|
||||
title: Text(tr('시작 메뉴에 등록')),
|
||||
subtitle: Text(StartMenu.userAreaLocked()
|
||||
subtitle: Text(c.services.startMenu.userAreaLocked()
|
||||
? tr('시험용 데이터 폴더 (JJ_MKVMAKER_DATA) 로 켜져 시작 메뉴 바로 가기 · jjmkvmaker:// 연결은 건드리지 않습니다')
|
||||
: tr('알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)')),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:jj_mkvmaker/core/playlist.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/start_menu.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
|
||||
/// 99: 시작 메뉴 바로 가기 (앱 ID 포함) · 알림을 누르면 jjmkvmaker://lsync 로 실시간 동기화 화면
|
||||
/// (시험은 임시 폴더에 만든다 - 사용자의 시작 메뉴 · 레지스트리는 건드리지 않음)
|
||||
|
|
@ -13,7 +16,7 @@ void main() {
|
|||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
final lnk = p.join(tmp.path, 'JJ_MKVMaker.lnk');
|
||||
final exe1 = Platform.resolvedExecutable;
|
||||
expect(await StartMenu.ensure(exe: exe1, lnk: lnk, registerProtocol: false), isTrue);
|
||||
expect(await StartMenu.user.ensure(exe: exe1, lnk: lnk, registerProtocol: false), isTrue);
|
||||
expect(File(lnk).existsSync(), isTrue);
|
||||
Future<String> read(String what) async => '${(await Process.run('powershell.exe', [
|
||||
'-NoProfile',
|
||||
|
|
@ -27,7 +30,7 @@ void main() {
|
|||
expect((await read('target')).toLowerCase(), exe1.toLowerCase());
|
||||
// 앱 폴더를 옮긴 경우: 다음 실행 때 새 위치로
|
||||
final exe2 = p.join(Platform.environment['SystemRoot'] ?? r'C:\Windows', 'notepad.exe');
|
||||
expect(await StartMenu.ensure(exe: exe2, lnk: lnk, registerProtocol: false), isTrue);
|
||||
expect(await StartMenu.user.ensure(exe: exe2, lnk: lnk, registerProtocol: false), isTrue);
|
||||
expect((await read('target')).toLowerCase(), exe2.toLowerCase());
|
||||
expect(await read('id'), StartMenu.aumid);
|
||||
}, skip: !Platform.isWindows, timeout: const Timeout(Duration(minutes: 2)));
|
||||
|
|
@ -35,36 +38,33 @@ void main() {
|
|||
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);
|
||||
});
|
||||
// 바로 가기 파일도 시험 폴더로 (시험이 잘못돼도 사용자 시작 메뉴의 것을 지우지 않게)
|
||||
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||
// 바로 가기 파일도 시험 폴더로 (시험이 잘못돼도 사용자 시작 메뉴의 것을 지우지 않게) - 설계 M4: 가짜를 넘긴 인스턴스
|
||||
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();
|
||||
var locked = true;
|
||||
final menu = StartMenu(
|
||||
shortcutPath: lnk,
|
||||
userAreaLocked: () => locked,
|
||||
run: (exe, args) async {
|
||||
calls.add(exe);
|
||||
return ProcessResult(0, 0, 'JJ_OK', '');
|
||||
},
|
||||
);
|
||||
expect(await menu.ensure(), isFalse, reason: '사용자 바로 가기 · 주소 연결');
|
||||
expect(await menu.ensure(lnk: lnk), isFalse, reason: '주소 연결 (HKCU) 은 사용자 영역');
|
||||
await menu.remove();
|
||||
expect(calls, isEmpty, reason: '막힌 동안은 아무 명령도 부르지 않음');
|
||||
expect(File(lnk).existsSync(), isTrue, reason: '바로 가기를 지우지 않음');
|
||||
// 시험이 따로 준 바로 가기 파일만 쓰는 것은 그대로
|
||||
expect(await StartMenu.ensure(exe: Platform.resolvedExecutable, lnk: lnk, registerProtocol: false), isTrue);
|
||||
expect(await menu.ensure(exe: Platform.resolvedExecutable, lnk: lnk, registerProtocol: false), isTrue);
|
||||
expect(calls, ['powershell.exe']);
|
||||
// 막지 않으면 (사용자 판) 예전처럼 부른다
|
||||
StartMenu.userAreaLocked = () => false;
|
||||
locked = false;
|
||||
calls.clear();
|
||||
expect(await StartMenu.ensure(), isTrue);
|
||||
await StartMenu.remove();
|
||||
expect(await menu.ensure(), isTrue);
|
||||
await menu.remove();
|
||||
expect(calls, ['powershell.exe', 'reg.exe']);
|
||||
expect(File(lnk).existsSync(), isFalse, reason: '사용자 판에서 끄면 지움 (여기서는 시험 폴더의 것)');
|
||||
}, skip: !Platform.isWindows);
|
||||
|
|
@ -76,4 +76,13 @@ void main() {
|
|||
expect(LaunchRequest.parse(LaunchRequest(LaunchAction.lsync, const []).toArgs()).action, LaunchAction.lsync);
|
||||
expect(LaunchRequest.parse(['jjmkvmaker://other']).files, isEmpty); // 모르는 주소를 파일로 열지 않음
|
||||
});
|
||||
|
||||
test('설계 M4: PlatformServices 를 손으로 조립하면 시작 메뉴는 사용자 영역을 건드리지 않는 것 (늘 막힘 · 바로 가기 자리는 임시 폴더)', () async {
|
||||
final s = PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService());
|
||||
final userLnk = p.join(Platform.environment['APPDATA'] ?? '', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'JJ_MKVMaker.lnk');
|
||||
expect(s.startMenu.userAreaLocked(), isTrue);
|
||||
expect(p.equals(s.startMenu.shortcutPath, userLnk), isFalse, reason: '사용자 시작 메뉴가 아님');
|
||||
expect(p.isWithin(Directory.systemTemp.path, s.startMenu.shortcutPath), isTrue);
|
||||
expect(await s.startMenu.ensure(), isFalse, reason: '막힘 - 아무것도 만들지 않음');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue