diff --git a/documentation/MODULE__AE_Events_PressMgmt_Launcher.md b/documentation/MODULE__AE_Events_PressMgmt_Launcher.md index 3b37e31a..179521a2 100644 --- a/documentation/MODULE__AE_Events_PressMgmt_Launcher.md +++ b/documentation/MODULE__AE_Events_PressMgmt_Launcher.md @@ -326,7 +326,7 @@ immediately. See `PROJECT__AE_Events_Launcher_Native_integration.md` Section 8. These are the initial built-in defaults shipped with the Launcher. They are the Svelte-side fallbacks used when neither device config nor event config defines a profile for the file extension. Each canonical profile can have multiple extension aliases. `post_delay_ms` is part -of the profile object, so a device-specific `other_json.launcher.post_delay_ms` override can +of the profile object, so a device-specific `launch_profiles[profile].post_delay_ms` override can tune it later without changing the profile table. | Profile name | Extension aliases | Default app | Display mode | Post delay | Notes | diff --git a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md index c80153d3..27c79d73 100644 --- a/documentation/PROJECT__AE_Events_Launcher_Native_integration.md +++ b/documentation/PROJECT__AE_Events_Launcher_Native_integration.md @@ -202,8 +202,8 @@ The native layer should not invent or guess a default launch path. The built-in defaults are organized as canonical profile names plus extension aliases. That lets multiple file types share one profile without repeating the same app/script details. -The profile object also carries `post_delay_ms`, and a device-specific -`other_json.launcher.post_delay_ms` override can tune the delay without changing the bridge +The profile object also carries `post_delay_ms`, and a device-specific per-profile +`launch_profiles[profile].post_delay_ms` override can tune the delay without changing the bridge contract. URL-based presentations remain a special pseudo-extension handled separately from the cache open flow. diff --git a/documentation/TODO__Agents.md b/documentation/TODO__Agents.md index aec94c90..9ba52989 100644 --- a/documentation/TODO__Agents.md +++ b/documentation/TODO__Agents.md @@ -21,7 +21,7 @@ guessing defaults. - [x] `native:launch-from-cache` executes a provided `native_template` string — AppleScript or `shell:` prefix; no Electron-side fallback - [x] `get_launch_profile()` in `launcher_file_cont.svelte` reads from device config then event config; resolves to a `native_template` string and passes it to `launch_from_cache` - [x] Built-in Launcher defaults refactored into canonical profile names plus extension aliases -- [x] Device-level Launch Timing section added under Launcher Configuration → Device, with `other_json.launcher.post_delay_ms` override +- [x] Device-level Launch Timing section added under Launcher Configuration → Device, with per-profile `launch_profiles[profile].post_delay_ms` overrides **Svelte-side migration — remaining before May 26:** - [ ] **[Launcher] Built-in Svelte default profiles** — move the built-in presentation/media diff --git a/src/lib/ae_events/ae_launcher__default_launch_profiles.ts b/src/lib/ae_events/ae_launcher__default_launch_profiles.ts index 5c57917b..9125fb39 100644 --- a/src/lib/ae_events/ae_launcher__default_launch_profiles.ts +++ b/src/lib/ae_events/ae_launcher__default_launch_profiles.ts @@ -5,11 +5,11 @@ * 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] — extension aliases to canonical built-ins - * 5. DEFAULT_LAUNCH_PROFILES['default'] — catch-all + * 1. event_file.cfg_json.display_override — per-file, display_mode only + * 2. event_device.data_json.launch_profiles[profile] — per-profile override, per device (API) + * 3. $events_loc.launcher.launch_profiles[profile] — local persistent override + * 4. DEFAULT_LAUNCH_PROFILES[profile/alias] — canonical built-ins + aliases + * 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. @@ -43,7 +43,7 @@ export interface LaunchProfile { post_script?: string; /** * Milliseconds to wait after open_cmd before running post_script. - * Default: 2000. Can be overridden per device via other_json.launcher.post_delay_ms. + * Default: 2000. Can be overridden per profile via launch_profiles[profile].post_delay_ms. */ post_delay_ms?: number; @@ -266,22 +266,33 @@ export const DEFAULT_LAUNCH_PROFILES: Record = Object.fro export function resolve_launch_profile( extension: string, display_override?: 'extend' | 'mirror' | 'none' | null, - device_profiles?: Record | null, - local_profiles?: Record | null + device_profiles?: Record> | null, + local_profiles?: Record> | null ): LaunchProfile { const ext = (extension || '').toLowerCase().replace(/^\./, ''); - const default_profile_name = DEFAULT_LAUNCH_PROFILE_ALIASES[ext] ?? ext; + const canonical_profile_name = DEFAULT_LAUNCH_PROFILE_ALIASES[ext] ?? ext; - // Priority: device config → local config → canonical built-ins → default - const source = - device_profiles?.[ext] ?? - device_profiles?.['default'] ?? - local_profiles?.[ext] ?? - local_profiles?.['default'] ?? - DEFAULT_LAUNCH_PROFILES[default_profile_name] ?? + const built_in_profile = + DEFAULT_LAUNCH_PROFILE_LIBRARY[canonical_profile_name] ?? DEFAULT_LAUNCH_PROFILE_LIBRARY.os_default; - const profile = { ...source }; + const local_profile = + local_profiles?.[canonical_profile_name] ?? + local_profiles?.[ext] ?? + local_profiles?.['default'] ?? + null; + + const device_profile = + device_profiles?.[canonical_profile_name] ?? + device_profiles?.[ext] ?? + device_profiles?.['default'] ?? + null; + + const profile = { + ...built_in_profile, + ...(local_profile ?? {}), + ...(device_profile ?? {}) + }; // Per-file display override wins over everything if (display_override) { diff --git a/src/lib/electron/electron_relay.ts b/src/lib/electron/electron_relay.ts index 5b849d4c..7fc4e203 100644 --- a/src/lib/electron/electron_relay.ts +++ b/src/lib/electron/electron_relay.ts @@ -80,7 +80,7 @@ export async function launch_from_cache({ * - AppleScript: multi-line string with {{path}} placeholder (macOS only) * - Shell command: prefix with "shell:" → e.g. "shell:open \"{{path}}\"" * - * Configure via event_device.data_json.launch_profiles or $events_loc.launcher.launch_profiles. + * Configure via per-profile launch_profiles overrides in event_device.data_json or $events_loc.launcher. * If null, Electron should treat that as a missing profile error. */ native_template?: string | null; diff --git a/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_launch_timing.svelte b/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_launch_timing.svelte index cfd240ae..905ac70c 100644 --- a/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_launch_timing.svelte +++ b/src/routes/events/[event_id]/(launcher)/cfg_components/launcher_cfg_launch_timing.svelte @@ -2,6 +2,11 @@ import { ae_api, ae_loc } from '$lib/stores/ae_stores'; import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_loc } from '$lib/stores/ae_events_stores'; +import { + DEFAULT_LAUNCH_PROFILE_DEFS, + resolve_launch_profile, + type LaunchProfile +} from '$lib/ae_events/ae_launcher__default_launch_profiles'; import Launcher_Cfg_Section from './launcher_cfg_section.svelte'; import { AlarmClock, RotateCcw, Save, Timer } from '@lucide/svelte'; @@ -11,16 +16,40 @@ interface Props { let { on_expand }: Props = $props(); +type LaunchProfileOverride = Record & { + post_delay_ms?: number | string | null; +}; + +type LaunchProfilesBucket = Record; + type NativeDeviceLike = { event_device_id?: string; id?: string; other_json?: string | Record | null; - post_delay_ms?: number | string | null; + launch_profiles?: LaunchProfilesBucket | null; [key: string]: unknown; }; -let delay_ms_input = $state(''); -let save_status = $state(''); +type ProfileRow = { + name: string; + aliases: string[]; + profile: LaunchProfile; +}; + +const profile_rows: ProfileRow[] = DEFAULT_LAUNCH_PROFILE_DEFS.filter( + ({ profile }) => profile.post_delay_ms !== undefined +).map(({ name, aliases, profile }) => ({ + name, + aliases, + profile +})); + +const profile_row_map: Record = Object.fromEntries( + profile_rows.map((row) => [row.name, row]) +); + +let delay_inputs = $state>({}); +let row_status = $state>({}); let last_device_id: string | null = null; function get_native_device(): NativeDeviceLike | null { @@ -35,9 +64,7 @@ function get_device_id(): string | null { function parse_other_json(raw_json: unknown): Record { if (!raw_json) return {}; - if (typeof raw_json === 'object') { - return { ...(raw_json as Record) }; - } + if (typeof raw_json === 'object') return { ...(raw_json as Record) }; if (typeof raw_json !== 'string') return {}; try { const parsed = JSON.parse(raw_json); @@ -47,18 +74,35 @@ function parse_other_json(raw_json: unknown): Record { } } -function get_current_delay_ms(): number | null { +function get_device_launch_profiles(): LaunchProfilesBucket { const native_device = get_native_device(); const parsed_other_json = parse_other_json(native_device?.other_json); const launcher_cfg = (parsed_other_json.launcher as Record | undefined) ?? {}; - const raw_delay = launcher_cfg.post_delay_ms; + + return ( + (launcher_cfg.launch_profiles as LaunchProfilesBucket | undefined) ?? + (native_device?.launch_profiles as LaunchProfilesBucket | undefined) ?? + {} + ); +} + +function get_effective_delay_ms(profile_name: string): number | null { + const bucket = get_device_launch_profiles(); + const profile = resolve_launch_profile( + profile_name, + null, + bucket as Record>, + null + ); + const delay = profile.post_delay_ms; const parsed_delay = - typeof raw_delay === 'string' - ? Number(raw_delay) - : typeof raw_delay === 'number' - ? raw_delay + typeof delay === 'string' + ? Number(delay) + : typeof delay === 'number' + ? delay : Number.NaN; + return Number.isFinite(parsed_delay) && parsed_delay >= 0 ? parsed_delay : null; @@ -69,7 +113,11 @@ function sync_from_device() { if (device_id === last_device_id) return; last_device_id = device_id; - delay_ms_input = get_current_delay_ms() === null ? '' : String(get_current_delay_ms()); + + for (const row of profile_rows) { + const current_delay = get_effective_delay_ms(row.name); + delay_inputs[row.name] = current_delay === null ? '' : String(current_delay); + } } sync_from_device(); @@ -78,17 +126,72 @@ $effect(() => { sync_from_device(); }); -function build_other_json(delay_value: number | null): string { +function set_row_status(profile_name: string, message: string) { + row_status[profile_name] = message; + setTimeout(() => { + if (row_status[profile_name] === message) { + row_status[profile_name] = ''; + } + }, 3000); +} + +function build_other_json(profile_name: string, delay_value: number | null): string { const native_device = get_native_device(); const other_json = parse_other_json(native_device?.other_json); const launcher_cfg = { ...((other_json.launcher as Record | undefined) ?? {}) }; + const existing_profiles = + (launcher_cfg.launch_profiles as LaunchProfilesBucket | undefined) ?? + (native_device?.launch_profiles as LaunchProfilesBucket | undefined) ?? + {}; + const next_profiles: LaunchProfilesBucket = { ...existing_profiles }; + const row = profile_row_map[profile_name]; + const alias_keys = row?.aliases ?? []; - if (delay_value === null) { - delete launcher_cfg.post_delay_ms; + let source_key: string | null = null; + if (profile_name in next_profiles) { + source_key = profile_name; } else { - launcher_cfg.post_delay_ms = delay_value; + for (const alias_key of alias_keys) { + if (alias_key in next_profiles) { + source_key = alias_key; + break; + } + } + } + + const source_profile = source_key + ? { ...(next_profiles[source_key] as LaunchProfileOverride) } + : {}; + + if (source_key && source_key !== profile_name) { + delete next_profiles[source_key]; + } + + for (const alias_key of alias_keys) { + if (alias_key !== source_key) { + delete next_profiles[alias_key]; + } + } + + const default_delay_ms = row?.profile.post_delay_ms ?? null; + if (delay_value === null || delay_value === default_delay_ms) { + delete source_profile.post_delay_ms; + } else { + source_profile.post_delay_ms = delay_value; + } + + if (Object.keys(source_profile).length > 0) { + next_profiles[profile_name] = source_profile; + } else { + delete next_profiles[profile_name]; + } + + if (Object.keys(next_profiles).length > 0) { + launcher_cfg.launch_profiles = next_profiles; + } else { + delete launcher_cfg.launch_profiles; } if (Object.keys(launcher_cfg).length > 0) { @@ -100,38 +203,40 @@ function build_other_json(delay_value: number | null): string { return JSON.stringify(other_json); } -async function save_delay_override() { +async function save_profile_delay(profile_name: string) { const device_id = get_device_id(); if (!device_id) { - save_status = 'No native device loaded'; + set_row_status(profile_name, 'No native device loaded'); return; } - const trimmed = delay_ms_input.trim(); + const trimmed = (delay_inputs[profile_name] ?? '').trim(); + const row = profile_row_map[profile_name]; + const default_delay_ms = row?.profile.post_delay_ms ?? null; let delay_value: number | null = null; if (trimmed !== '') { const parsed = Number(trimmed); if (!Number.isFinite(parsed) || parsed < 0) { - save_status = 'Enter a valid non-negative delay'; + set_row_status(profile_name, 'Enter a valid non-negative delay'); return; } delay_value = Math.round(parsed); } - save_status = 'Saving...'; + set_row_status(profile_name, 'Saving...'); + const other_json = build_other_json(profile_name, delay_value); const result = await events_func.update_ae_obj__event_device({ api_cfg: $ae_api, event_device_id: device_id, data_kv: { - other_json: build_other_json(delay_value) + other_json }, log_lvl: 0 }); if (!result) { - save_status = 'Save failed'; - setTimeout(() => (save_status = ''), 3000); + set_row_status(profile_name, 'Save failed'); return; } @@ -139,16 +244,24 @@ async function save_delay_override() { store_loc.native_device = { ...get_native_device(), ...result, - other_json: build_other_json(delay_value) + other_json }; - save_status = delay_value === null ? 'Reset to profile defaults' : 'Saved'; - setTimeout(() => (save_status = ''), 3000); + if (delay_value === null || delay_value === default_delay_ms) { + delay_inputs[profile_name] = default_delay_ms === null ? '' : String(default_delay_ms); + set_row_status(profile_name, 'Reset to built-in default'); + } else { + delay_inputs[profile_name] = String(delay_value); + set_row_status(profile_name, 'Saved'); + } } -async function reset_delay_override() { - delay_ms_input = ''; - await save_delay_override(); +async function reset_profile_delay(profile_name: string) { + const row = profile_row_map[profile_name]; + delay_inputs[profile_name] = row?.profile.post_delay_ms === null + ? '' + : String(row?.profile.post_delay_ms ?? ''); + await save_profile_delay(profile_name); } @@ -157,67 +270,99 @@ async function reset_delay_override() { icon={Timer} bind:state={$events_loc.launcher.section_state__launch_timing} {on_expand} - description="Device-level post-open delay override"> + description="Per-profile post-open delay overrides"> {#if $ae_loc.edit_mode && !$ae_loc.is_native}
Dev Preview — visible for layout only; save requires the native + >Dev Preview — visible for layout only; save requires a native device record
{/if}

- Native Open Delay + Per-Profile Native Open Delay

- Overrides the built-in `post_delay_ms` for this device. Leave blank - to use the profile default for each extension/profile. + Each built-in profile keeps its own timing. Leave a value at the + built-in default to avoid storing an override for that profile.

-
- - - -
- -
- - Current: - {#if get_current_delay_ms() === null} - profile defaults - {:else} - {get_current_delay_ms()} ms - {/if} - - -
- - {#if save_status} -
- {save_status} + {#if !get_device_id()} +
+ No native device record is loaded yet, so these controls are + read-only until the device config arrives.
{/if} + +
+ {#each profile_rows as row (row.name)} +
+
+
+

+ {row.profile.app} +

+

+ {row.name} + {#if row.aliases.length} + • {row.aliases.join(', ')} + {/if} +

+
+ +
+
+ Built-in: + {row.profile.post_delay_ms ?? 'n/a'} ms +
+
+ Current: + {get_effective_delay_ms(row.name) ?? 'n/a'} ms +
+
+
+ +
+ + + + + +
+ + {#if row_status[row.name]} +
+ {row_status[row.name]} +
+ {/if} +
+ {/each} +
\ No newline at end of file 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 862525b4..58d8b033 100644 --- a/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte +++ b/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte @@ -118,10 +118,10 @@ let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp']; /** * 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) + * 1. event_device.data_json.launch_profiles[profile] (API-driven, per-device) + * 2. $events_loc.launcher.launch_profiles[profile] (local persistent override) + * 3. DEFAULT_LAUNCH_PROFILES[profile/alias] (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_profile( @@ -135,27 +135,12 @@ function get_launch_profile( null; const local_profiles = ($events_loc as any).launcher?.launch_profiles ?? null; const display_override = file_obj?.cfg_json?.display_override ?? null; - const profile = resolve_launch_profile( + return resolve_launch_profile( extension, display_override, device_profiles, local_profiles ); - - const device_post_delay_ms = - native_device?.other_json?.launcher?.post_delay_ms ?? - native_device?.post_delay_ms ?? - null; - const parsed_delay_ms = - typeof device_post_delay_ms === 'string' - ? Number(device_post_delay_ms) - : device_post_delay_ms; - - if (Number.isFinite(parsed_delay_ms) && parsed_delay_ms >= 0) { - profile.post_delay_ms = parsed_delay_ms; - } - - return profile; } onMount(() => {