From 422c9c341cd944cf44c79fa0bae4b56fabc68c84 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 12 May 2026 12:17:43 -0400 Subject: [PATCH] =?UTF-8?q?feat(launcher):=20implement=20LaunchProfile=20s?= =?UTF-8?q?ystem=20=E2=80=94=20MasterKey=20replacement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ae_launcher__default_launch_profiles.ts with LaunchProfile interface, DEFAULT_LAUNCH_PROFILES constant, and resolve_launch_profile() helper. Covers pptx/ppt/key/odp/pdf, all VLC media formats, Windows/Parallels variants (pptxwin/pptwin/odpwin/pdfwin), and a catch-all 'default'. - Replace get_launch_script_template() with get_launch_profile() in launcher_file_cont.svelte. Override priority: device API config > local persistent config > built-in defaults > 'default' catch-all. - Rewrite handle_open_file() native branch with 9-step profile-driven flow: copy_from_cache_to_temp → resolve profile → set_display_layout (silent fail) → open (run_cmd or OS default) → sleep(post_delay_ms) → run post_script → fallback to OS default on open failure → surface status/error detail. - Add open_file_error_detail state var; show error pre block in status alert for native error state, show fallback note for fallback state. - Add display override toggle button in event_file_meta (visible when trusted_access + is_native): cycles null → extend → mirror → null, PATCHes event_file.cfg_json.display_override via V3 CRUD. --- .../ae_launcher__default_launch_profiles.ts | 382 ++++++++++++++++++ .../(launcher)/launcher_file_cont.svelte | 224 +++++++--- 2 files changed, 557 insertions(+), 49 deletions(-) create mode 100644 src/lib/ae_events/ae_launcher__default_launch_profiles.ts diff --git a/src/lib/ae_events/ae_launcher__default_launch_profiles.ts b/src/lib/ae_events/ae_launcher__default_launch_profiles.ts new file mode 100644 index 00000000..ac07c952 --- /dev/null +++ b/src/lib/ae_events/ae_launcher__default_launch_profiles.ts @@ -0,0 +1,382 @@ +/** + * ae_launcher__default_launch_profiles.ts + * + * Built-in launch profiles for the Aether Events Launcher — the Svelte-side + * replacement for the legacy OSIT MasterKey Swift app. + * + * These are the last-resort defaults. Override priority (high → low): + * 1. event_file.cfg_json.display_override — per-file, display_mode only + * 2. event_device.data_json.launch_profiles[ext] — full profile, per device (API) + * 3. $events_loc.launcher.launch_profiles[ext] — local persistent override + * 4. DEFAULT_LAUNCH_PROFILES[ext] — these built-ins + * 5. DEFAULT_LAUNCH_PROFILES['default'] — catch-all + * + * Keys are lowercase file extensions without the dot: "pptx", "key", "pdf", etc. + * The special key "default" catches any unrecognised extension. + * + * post_script formats: + * - Plain string → run as AppleScript via run_osascript() (macOS only) + * - "shell:..." prefix → run as shell command via run_cmd() + * + * Reserved for future use (not yet read anywhere): + * - speed_factor: number — delay multiplier for slower machines (1.0 = normal) + * - url: string — for URL-type presentations (e.g. Google Slides) + */ + +export interface LaunchProfile { + /** Human-readable label for status messages */ + app: string; + /** Display layout to set before opening. 'extend' only applied if external display found. */ + display_mode: 'extend' | 'mirror' | 'none'; + /** + * Shell command to open the file. {{path}} is replaced with the resolved temp path. + * If omitted, falls back to open_local_file_v2(path) — OS default handler. + */ + open_cmd?: string; + /** + * Script to run after the file opens and post_delay_ms has elapsed. + * Plain string → AppleScript (macOS). "shell:" prefix → shell command. + */ + post_script?: string; + /** Milliseconds to wait after open_cmd before running post_script. Default: 2000 */ + post_delay_ms?: number; + + // --- Reserved for future use — not yet implemented --- + // speed_factor?: number; + // url?: string; +} + +export const DEFAULT_LAUNCH_PROFILES: Record = { + + // ------------------------------------------------------------------------- + // macOS presentation formats + // ------------------------------------------------------------------------- + + pptx: { + app: 'Microsoft PowerPoint', + display_mode: 'extend', + open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "Microsoft PowerPoint" + activate +end tell +tell application "System Events" + tell process "Microsoft PowerPoint" + keystroke return using command down + end tell +end tell` + }, + + ppt: { + app: 'Microsoft PowerPoint', + display_mode: 'extend', + open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "Microsoft PowerPoint" + activate +end tell +tell application "System Events" + tell process "Microsoft PowerPoint" + keystroke return using command down + end tell +end tell` + }, + + key: { + app: 'Keynote', + display_mode: 'extend', + open_cmd: 'open -a "Keynote" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "Keynote" + activate + start (front document) +end tell` + }, + + odp: { + app: 'LibreOffice', + display_mode: 'extend', + open_cmd: 'open -a "LibreOffice" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "LibreOffice" + activate +end tell +tell application "System Events" + tell process "soffice" + key code 96 + end tell +end tell` + }, + + pdf: { + app: 'Adobe Acrobat Reader DC', + display_mode: 'mirror', + open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "Adobe Acrobat Reader DC" + activate +end tell +tell application "System Events" + tell process "AdobeReader" + keystroke "l" using command down + end tell +end tell` + }, + + // ------------------------------------------------------------------------- + // Media (VLC) — mirror display + // ------------------------------------------------------------------------- + + mp4: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + mkv: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + mov: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + mpeg: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + avi: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + flv: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + ogg: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + ogv: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + mp3: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + wmv: { + app: 'VLC', + display_mode: 'mirror', + open_cmd: 'open -a "VLC" "{{path}}"', + post_delay_ms: 2000, + post_script: `tell application "VLC" + activate +end tell +tell application "System Events" + tell process "VLC" + keystroke "f" using command down + end tell +end tell` + }, + + // ------------------------------------------------------------------------- + // Windows / Parallels variants — longer post_delay_ms (Parallels needs more time) + // ------------------------------------------------------------------------- + + pptxwin: { + app: 'Microsoft Office PowerPoint (Windows)', + display_mode: 'extend', + open_cmd: 'open -a "Microsoft Office PowerPoint" "{{path}}"', + post_delay_ms: 3000, + post_script: `tell application "Microsoft Office PowerPoint" + activate +end tell +tell application "System Events" + key code 96 +end tell` + }, + + pptwin: { + app: 'Microsoft Office PowerPoint (Windows)', + display_mode: 'extend', + open_cmd: 'open -a "Microsoft Office PowerPoint" "{{path}}"', + post_delay_ms: 3000, + post_script: `tell application "Microsoft Office PowerPoint" + activate +end tell +tell application "System Events" + key code 96 +end tell` + }, + + odpwin: { + app: 'LibreOffice (Windows)', + display_mode: 'extend', + open_cmd: 'open -a "LibreOffice" "{{path}}"', + post_delay_ms: 3000, + post_script: `tell application "LibreOffice" + activate +end tell +tell application "System Events" + tell process "soffice" + key code 96 + end tell +end tell` + }, + + pdfwin: { + app: 'Acrobat Reader (Windows)', + display_mode: 'mirror', + open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"', + post_delay_ms: 3000, + post_script: `tell application "Acrobat Reader Windows" + activate +end tell +tell application "System Events" + key code 108 using control down +end tell` + }, + + // ------------------------------------------------------------------------- + // Catch-all — OS default handler, no display change + // Works on macOS (open) and Linux (xdg-open via open_local_file_v2) + // ------------------------------------------------------------------------- + + default: { + app: 'OS Default', + display_mode: 'none' + // No open_cmd — execution falls through to open_local_file_v2(path) + // No post_script + } +}; + +/** + * Returns a shallow copy of the built-in profile for the given extension, + * with a display_override applied if provided. + * + * Falls back to 'default' if no specific profile exists. + */ +export function resolve_launch_profile( + extension: string, + display_override?: 'extend' | 'mirror' | 'none' | null, + device_profiles?: Record | null, + local_profiles?: Record | null +): LaunchProfile { + const ext = (extension || '').toLowerCase().replace(/^\./, ''); + + // Priority: device config → local config → built-ins → default + const source = + device_profiles?.[ext] ?? + device_profiles?.['default'] ?? + local_profiles?.[ext] ?? + local_profiles?.['default'] ?? + DEFAULT_LAUNCH_PROFILES[ext] ?? + DEFAULT_LAUNCH_PROFILES['default']; + + const profile = { ...source }; + + // Per-file display override wins over everything + if (display_override) { + profile.display_mode = display_override; + } + + return profile; +} diff --git a/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte b/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte index 71b5e3e5..713e7a9a 100644 --- a/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte +++ b/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte @@ -79,48 +79,40 @@ import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_f // Import the relay import * as native from '$lib/electron/electron_relay'; +import { + type LaunchProfile, + resolve_launch_profile +} from '$lib/ae_events/ae_launcher__default_launch_profiles'; let ae_promises: key_val = $state({}); let open_file_clicked: null | boolean = $state(null); let open_file_status: null | string = $state(null); let open_file_status_message: null | string = $state(null); +let open_file_error_detail: string | null = $state(null); + +/** Simple promise-based delay for post-open script timing */ +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp']; /** - * Resolves a data-driven launch script template for a given file extension. - * Checked in priority order: - * 1. event_device.data_json.launch_scripts (API-driven, per-device, most specific) - * 2. $events_loc.launcher.launch_scripts (local persistent override) - * Keys are lowercase extensions without the dot ("pptx", "key", "pdf", etc.). - * A "default" key acts as a catch-all for unrecognised extensions. - * - * Returns null when no config is found → Electron falls back to its hardcoded defaults. - * - * Template formats: - * - AppleScript (macOS): plain string with {{path}} placeholder - * - Shell command: prefix with "shell:" → "shell:open \"{{path}}\"" + * Resolves the LaunchProfile for a given file extension and optional per-file + * display override. Checked in priority order: + * 1. event_device.data_json.launch_profiles (API-driven, per-device) + * 2. $events_loc.launcher.launch_profiles (local persistent override) + * 3. DEFAULT_LAUNCH_PROFILES[ext] (Svelte built-in defaults) + * 4. DEFAULT_LAUNCH_PROFILES['default'] (catch-all) + * Per-file display_override from event_file.cfg_json overrides display_mode only. */ -function get_launch_script_template(extension: string): string | null { - const ext = (extension || '').toLowerCase().replace('.', ''); - - // 1. Device-level config (from API, per device — highest priority) - const device_scripts = ($ae_loc as any).native_device?.launch_scripts; - if (device_scripts) { - if (device_scripts[ext]) return device_scripts[ext]; - if (device_scripts['default']) return device_scripts['default']; - } - - // 2. Launcher local config override (set manually via Launcher config UI) - const local_scripts = ($events_loc as any).launcher?.launch_scripts; - if (local_scripts) { - if (local_scripts[ext]) return local_scripts[ext]; - if (local_scripts['default']) return local_scripts['default']; - } - - // 3. No override — let Electron use its built-in hardcoded defaults - return null; +function get_launch_profile( + extension: string, + file_obj?: any +): LaunchProfile { + const device_profiles = ($ae_loc as any).native_device?.launch_profiles ?? null; + const local_profiles = ($events_loc as any).launcher?.launch_profiles ?? null; + const display_override = file_obj?.cfg_json?.display_override ?? null; + return resolve_launch_profile(extension, display_override, device_profiles, local_profiles); } onMount(() => { @@ -178,29 +170,115 @@ async function handle_open_file() { } } + // --- Step 1: Copy cached file to a writable temp path --- open_file_status = 'opening_file'; - open_file_status_message = 'Opening Application'; + open_file_status_message = 'Preparing file...'; + open_file_error_detail = null; - // Phase 2/5: Use the atomic copy-and-launch operation. - // The main process handler (file_handlers.ts) now handles the - // specialized LibreOffice/AppleScript logic internally after copying. - // script_template is null when no device/local config exists → Electron uses hardcoded defaults. - const script_template = get_launch_script_template(event_file_obj.extension); - const launch_result = await native.launch_from_cache({ + const copy_result = await native.copy_from_cache_to_temp({ cache_root, hash: event_file_obj.hash_sha256, temp_root, - filename: event_file_obj.filename, - script_template + filename: event_file_obj.filename }); - if (!launch_result.success) { + if (!copy_result.success || !copy_result.path) { open_file_status = 'error'; - open_file_status_message = `Failed to open: ${launch_result.error}`; + open_file_status_message = 'Failed to prepare file'; + open_file_error_detail = copy_result.error ?? 'copy_from_cache_to_temp returned no path'; + setTimeout(() => (open_file_clicked = false), 6000); + return false; } + const resolved_path = copy_result.path; + + // --- Step 2: Resolve launch profile --- + const profile = get_launch_profile(event_file_obj.extension, event_file_obj); + if (log_lvl) console.log('LaunchProfile:', profile); + + // --- Step 3: Set display layout (skip silently on failure / no external display) --- + if (profile.display_mode !== 'none') { + open_file_status_message = `Setting display (${profile.display_mode})...`; + await native.set_display_layout({ mode: profile.display_mode }).catch(() => { + /* No external display or displayplacer unavailable — continue */ + }); + } + + // --- Step 4: Open the file --- + open_file_status_message = `Opening ${profile.app}...`; + let open_ok = true; + let open_error: string | null = null; + + if (profile.open_cmd) { + const cmd = profile.open_cmd.replaceAll('{{path}}', resolved_path); + const cmd_result = await native.run_cmd({ cmd }); + if (!cmd_result.success) { + open_ok = false; + open_error = cmd_result.error ?? 'run_cmd failed'; + } + } else { + // No open_cmd → OS default handler via shell.openPath + const os_result = await native.open_local_file_v2(resolved_path); + if (!os_result.success) { + open_ok = false; + open_error = os_result.error ?? 'open_local_file_v2 failed'; + } + } + + // --- Step 5: Wait for app to load before running post-script --- + if (open_ok) { + const delay = profile.post_delay_ms ?? 2000; + open_file_status_message = `Waiting for ${profile.app}...`; + await sleep(delay); + } + + // --- Step 6: Run post-script (AppleScript or shell) --- + if (open_ok && profile.post_script) { + open_file_status_message = 'Running post-open automation...'; + let script_ok = true; + let script_error: string | null = null; + + if (profile.post_script.startsWith('shell:')) { + const shell_cmd = profile.post_script.slice('shell:'.length); + const sr = await native.run_cmd({ cmd: shell_cmd }); + if (!sr.success) { script_ok = false; script_error = sr.error ?? 'run_cmd (post) failed'; } + } else { + const sr = await native.run_osascript(profile.post_script); + if (!sr.success) { script_ok = false; script_error = sr.error ?? 'run_osascript failed'; } + } + + if (!script_ok) { + // Non-fatal: file is already open. Surface as warning, not error. + if (log_lvl) console.warn('post_script failed:', script_error); + open_file_status = 'fallback'; + open_file_status_message = `Opened (post-script failed: ${script_error})`; + setTimeout(() => (open_file_clicked = false), 8000); + return true; + } + } + + // --- Step 7: Fallback if open_cmd itself failed --- + if (!open_ok) { + if (log_lvl) console.warn('open_cmd failed, falling back to OS default:', open_error); + const fb_result = await native.open_local_file_v2(resolved_path); + if (!fb_result.success) { + open_file_status = 'error'; + open_file_status_message = 'Failed to open file'; + open_file_error_detail = `${profile.app} failed: ${open_error}; OS fallback: ${fb_result.error}`; + setTimeout(() => (open_file_clicked = false), 8000); + return false; + } + open_file_status = 'fallback'; + open_file_status_message = '(opened with OS default)'; + setTimeout(() => (open_file_clicked = false), 5000); + return true; + } + + // --- Success --- + open_file_status = 'open'; + open_file_status_message = `Opened in ${profile.app}`; setTimeout(() => (open_file_clicked = false), 5000); - return launch_result.success; + return true; } // 2. ONSITE MODE (Browser with Modified Extensions) else if ($events_loc.launcher.app_mode === 'onsite') { @@ -289,12 +367,21 @@ function prevent_default(fn: (event: T) => void) { 'Please wait while this file downloads...'} *** {#if $ae_loc.is_native && $events_loc.launcher.app_mode === 'native'} -

Most files will automatically be opened full screen.

-

- PowerPoint or KeyNote will attempt to display in presenter - view. -

-

Please close the file when finished.

+ {#if open_file_status === 'error'} +

Failed to open file.

+ {#if open_file_error_detail} +
{open_file_error_detail}
+ {/if} + {:else if open_file_status === 'fallback'} +

(opened with OS default)

+ {:else} +

Most files will automatically be opened full screen.

+

+ PowerPoint or KeyNote will attempt to display in presenter + view. +

+

Please close the file when finished.

+ {/if} {/if} {/if} @@ -448,6 +535,45 @@ function prevent_default(fn: (event: T) => void) { {/if} + {#if $ae_loc.trusted_access && $ae_loc.is_native} + + + {/if} +