fix(launcher): use per-profile timing overrides

This commit is contained in:
Scott Idem
2026-05-13 12:48:43 -04:00
parent 4923099cfb
commit 39749c608a
7 changed files with 258 additions and 117 deletions

View File

@@ -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 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 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 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. tune it later without changing the profile table.
| Profile name | Extension aliases | Default app | Display mode | Post delay | Notes | | Profile name | Extension aliases | Default app | Display mode | Post delay | Notes |

View File

@@ -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 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. 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 The profile object also carries `post_delay_ms`, and a device-specific per-profile
`other_json.launcher.post_delay_ms` override can tune the delay without changing the bridge `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 contract. URL-based presentations remain a special pseudo-extension handled separately from
the cache open flow. the cache open flow.

View File

@@ -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] `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] `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] 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:** **Svelte-side migration — remaining before May 26:**
- [ ] **[Launcher] Built-in Svelte default profiles** — move the built-in presentation/media - [ ] **[Launcher] Built-in Svelte default profiles** — move the built-in presentation/media

View File

@@ -5,11 +5,11 @@
* replacement for the legacy OSIT MasterKey Swift app. * replacement for the legacy OSIT MasterKey Swift app.
* *
* These are the last-resort defaults. Override priority (high → low): * These are the last-resort defaults. Override priority (high → low):
* 1. event_file.cfg_json.display_override — per-file, display_mode only * 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) * 2. event_device.data_json.launch_profiles[profile] — per-profile override, per device (API)
* 3. $events_loc.launcher.launch_profiles[ext] — local persistent override * 3. $events_loc.launcher.launch_profiles[profile] — local persistent override
* 4. DEFAULT_LAUNCH_PROFILES[ext] — extension aliases to canonical built-ins * 4. DEFAULT_LAUNCH_PROFILES[profile/alias] — canonical built-ins + aliases
* 5. DEFAULT_LAUNCH_PROFILES['default'] — catch-all * 5. DEFAULT_LAUNCH_PROFILES['default'] — catch-all
* *
* Keys are lowercase file extensions without the dot: "pptx", "key", "pdf", etc. * Keys are lowercase file extensions without the dot: "pptx", "key", "pdf", etc.
* The special key "default" catches any unrecognised extension. * The special key "default" catches any unrecognised extension.
@@ -43,7 +43,7 @@ export interface LaunchProfile {
post_script?: string; post_script?: string;
/** /**
* Milliseconds to wait after open_cmd before running post_script. * 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; post_delay_ms?: number;
@@ -266,22 +266,33 @@ export const DEFAULT_LAUNCH_PROFILES: Record<string, LaunchProfile> = Object.fro
export function resolve_launch_profile( export function resolve_launch_profile(
extension: string, extension: string,
display_override?: 'extend' | 'mirror' | 'none' | null, display_override?: 'extend' | 'mirror' | 'none' | null,
device_profiles?: Record<string, LaunchProfile> | null, device_profiles?: Record<string, Partial<LaunchProfile>> | null,
local_profiles?: Record<string, LaunchProfile> | null local_profiles?: Record<string, Partial<LaunchProfile>> | null
): LaunchProfile { ): LaunchProfile {
const ext = (extension || '').toLowerCase().replace(/^\./, ''); 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 built_in_profile =
const source = DEFAULT_LAUNCH_PROFILE_LIBRARY[canonical_profile_name] ??
device_profiles?.[ext] ??
device_profiles?.['default'] ??
local_profiles?.[ext] ??
local_profiles?.['default'] ??
DEFAULT_LAUNCH_PROFILES[default_profile_name] ??
DEFAULT_LAUNCH_PROFILE_LIBRARY.os_default; 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 // Per-file display override wins over everything
if (display_override) { if (display_override) {

View File

@@ -80,7 +80,7 @@ export async function launch_from_cache({
* - AppleScript: multi-line string with {{path}} placeholder (macOS only) * - AppleScript: multi-line string with {{path}} placeholder (macOS only)
* - Shell command: prefix with "shell:" → e.g. "shell:open \"{{path}}\"" * - 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. * If null, Electron should treat that as a missing profile error.
*/ */
native_template?: string | null; native_template?: string | null;

View File

@@ -2,6 +2,11 @@
import { ae_api, ae_loc } from '$lib/stores/ae_stores'; import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events/ae_events_functions'; import { events_func } from '$lib/ae_events/ae_events_functions';
import { events_loc } from '$lib/stores/ae_events_stores'; 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 Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { AlarmClock, RotateCcw, Save, Timer } from '@lucide/svelte'; import { AlarmClock, RotateCcw, Save, Timer } from '@lucide/svelte';
@@ -11,16 +16,40 @@ interface Props {
let { on_expand }: Props = $props(); let { on_expand }: Props = $props();
type LaunchProfileOverride = Record<string, unknown> & {
post_delay_ms?: number | string | null;
};
type LaunchProfilesBucket = Record<string, LaunchProfileOverride>;
type NativeDeviceLike = { type NativeDeviceLike = {
event_device_id?: string; event_device_id?: string;
id?: string; id?: string;
other_json?: string | Record<string, unknown> | null; other_json?: string | Record<string, unknown> | null;
post_delay_ms?: number | string | null; launch_profiles?: LaunchProfilesBucket | null;
[key: string]: unknown; [key: string]: unknown;
}; };
let delay_ms_input = $state(''); type ProfileRow = {
let save_status = $state(''); 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<string, ProfileRow> = Object.fromEntries(
profile_rows.map((row) => [row.name, row])
);
let delay_inputs = $state<Record<string, string>>({});
let row_status = $state<Record<string, string>>({});
let last_device_id: string | null = null; let last_device_id: string | null = null;
function get_native_device(): NativeDeviceLike | 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<string, unknown> { function parse_other_json(raw_json: unknown): Record<string, unknown> {
if (!raw_json) return {}; if (!raw_json) return {};
if (typeof raw_json === 'object') { if (typeof raw_json === 'object') return { ...(raw_json as Record<string, unknown>) };
return { ...(raw_json as Record<string, unknown>) };
}
if (typeof raw_json !== 'string') return {}; if (typeof raw_json !== 'string') return {};
try { try {
const parsed = JSON.parse(raw_json); const parsed = JSON.parse(raw_json);
@@ -47,18 +74,35 @@ function parse_other_json(raw_json: unknown): Record<string, unknown> {
} }
} }
function get_current_delay_ms(): number | null { function get_device_launch_profiles(): LaunchProfilesBucket {
const native_device = get_native_device(); const native_device = get_native_device();
const parsed_other_json = parse_other_json(native_device?.other_json); const parsed_other_json = parse_other_json(native_device?.other_json);
const launcher_cfg = const launcher_cfg =
(parsed_other_json.launcher as Record<string, unknown> | undefined) ?? {}; (parsed_other_json.launcher as Record<string, unknown> | 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<string, Partial<LaunchProfile>>,
null
);
const delay = profile.post_delay_ms;
const parsed_delay = const parsed_delay =
typeof raw_delay === 'string' typeof delay === 'string'
? Number(raw_delay) ? Number(delay)
: typeof raw_delay === 'number' : typeof delay === 'number'
? raw_delay ? delay
: Number.NaN; : Number.NaN;
return Number.isFinite(parsed_delay) && parsed_delay >= 0 return Number.isFinite(parsed_delay) && parsed_delay >= 0
? parsed_delay ? parsed_delay
: null; : null;
@@ -69,7 +113,11 @@ function sync_from_device() {
if (device_id === last_device_id) return; if (device_id === last_device_id) return;
last_device_id = device_id; 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(); sync_from_device();
@@ -78,17 +126,72 @@ $effect(() => {
sync_from_device(); 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 native_device = get_native_device();
const other_json = parse_other_json(native_device?.other_json); const other_json = parse_other_json(native_device?.other_json);
const launcher_cfg = { const launcher_cfg = {
...((other_json.launcher as Record<string, unknown> | undefined) ?? {}) ...((other_json.launcher as Record<string, unknown> | 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) { let source_key: string | null = null;
delete launcher_cfg.post_delay_ms; if (profile_name in next_profiles) {
source_key = profile_name;
} else { } 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) { 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); return JSON.stringify(other_json);
} }
async function save_delay_override() { async function save_profile_delay(profile_name: string) {
const device_id = get_device_id(); const device_id = get_device_id();
if (!device_id) { if (!device_id) {
save_status = 'No native device loaded'; set_row_status(profile_name, 'No native device loaded');
return; 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; let delay_value: number | null = null;
if (trimmed !== '') { if (trimmed !== '') {
const parsed = Number(trimmed); const parsed = Number(trimmed);
if (!Number.isFinite(parsed) || parsed < 0) { 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; return;
} }
delay_value = Math.round(parsed); 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({ const result = await events_func.update_ae_obj__event_device({
api_cfg: $ae_api, api_cfg: $ae_api,
event_device_id: device_id, event_device_id: device_id,
data_kv: { data_kv: {
other_json: build_other_json(delay_value) other_json
}, },
log_lvl: 0 log_lvl: 0
}); });
if (!result) { if (!result) {
save_status = 'Save failed'; set_row_status(profile_name, 'Save failed');
setTimeout(() => (save_status = ''), 3000);
return; return;
} }
@@ -139,16 +244,24 @@ async function save_delay_override() {
store_loc.native_device = { store_loc.native_device = {
...get_native_device(), ...get_native_device(),
...result, ...result,
other_json: build_other_json(delay_value) other_json
}; };
save_status = delay_value === null ? 'Reset to profile defaults' : 'Saved'; if (delay_value === null || delay_value === default_delay_ms) {
setTimeout(() => (save_status = ''), 3000); 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() { async function reset_profile_delay(profile_name: string) {
delay_ms_input = ''; const row = profile_row_map[profile_name];
await save_delay_override(); delay_inputs[profile_name] = row?.profile.post_delay_ms === null
? ''
: String(row?.profile.post_delay_ms ?? '');
await save_profile_delay(profile_name);
} }
</script> </script>
@@ -157,67 +270,99 @@ async function reset_delay_override() {
icon={Timer} icon={Timer}
bind:state={$events_loc.launcher.section_state__launch_timing} bind:state={$events_loc.launcher.section_state__launch_timing}
{on_expand} {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} {#if $ae_loc.edit_mode && !$ae_loc.is_native}
<div <div
class="bg-warning-500/10 border-warning-500/30 mb-1 flex items-center gap-2 rounded-lg border px-2 py-1.5"> class="bg-warning-500/10 border-warning-500/30 mb-1 flex items-center gap-2 rounded-lg border px-2 py-1.5">
<AlarmClock size="0.75em" class="text-warning-500" /> <AlarmClock size="0.75em" class="text-warning-500" />
<span <span
class="text-warning-500 text-[9px] font-bold tracking-wide uppercase" class="text-warning-500 text-[9px] font-bold tracking-wide uppercase"
>Dev Preview — visible for layout only; save requires the native >Dev Preview — visible for layout only; save requires a native
device record</span> device record</span>
</div> </div>
{/if} {/if}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50"> <p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Native Open Delay Per-Profile Native Open Delay
</p> </p>
<p class="ml-1 text-[10px] leading-snug opacity-60"> <p class="ml-1 text-[10px] leading-snug opacity-60">
Overrides the built-in `post_delay_ms` for this device. Leave blank Each built-in profile keeps its own timing. Leave a value at the
to use the profile default for each extension/profile. built-in default to avoid storing an override for that profile.
</p> </p>
<div class="grid grid-cols-[1fr_auto] gap-2"> {#if !get_device_id()}
<input <div
type="number" class="bg-warning-500/10 border-warning-500/20 rounded-lg border px-2 py-2 text-[9px] opacity-80">
min="0" No native device record is loaded yet, so these controls are
step="100" read-only until the device config arrives.
bind:value={delay_ms_input}
placeholder="Use profile defaults"
class="input input-sm preset-tonal-surface h-8 text-[10px]" />
<button
type="button"
onclick={save_delay_override}
class="btn btn-sm preset-filled-primary h-8 px-3 text-[10px] font-bold"
disabled={!get_device_id()}>
<Save size="0.8em" class="mr-1" /> Save
</button>
</div>
<div class="flex items-center justify-between gap-2 px-1 text-[9px] opacity-60">
<span>
Current:
{#if get_current_delay_ms() === null}
profile defaults
{:else}
{get_current_delay_ms()} ms
{/if}
</span>
<button
type="button"
onclick={reset_delay_override}
class="btn btn-xs preset-tonal-surface h-6 px-2 text-[9px]"
disabled={!get_device_id()}>
<RotateCcw size="0.75em" class="mr-1" /> Reset
</button>
</div>
{#if save_status}
<div class="text-primary-500 text-center text-[9px] italic">
{save_status}
</div> </div>
{/if} {/if}
<div class="flex flex-col gap-2">
{#each profile_rows as row (row.name)}
<section class="border-surface-500/10 rounded-lg border p-2">
<div class="mb-2 flex items-start justify-between gap-3">
<div class="flex flex-col gap-0.5">
<p class="text-[10px] font-bold uppercase tracking-wide">
{row.profile.app}
</p>
<p class="text-[9px] opacity-55">
{row.name}
{#if row.aliases.length}
<span class="opacity-50"
>{row.aliases.join(', ')}</span
>
{/if}
</p>
</div>
<div class="text-right text-[9px] leading-tight opacity-65">
<div>
Built-in:
{row.profile.post_delay_ms ?? 'n/a'} ms
</div>
<div>
Current:
{get_effective_delay_ms(row.name) ?? 'n/a'} ms
</div>
</div>
</div>
<div class="grid grid-cols-[1fr_auto_auto] gap-2">
<input
type="number"
min="0"
step="100"
bind:value={delay_inputs[row.name]}
placeholder={`Default: ${row.profile.post_delay_ms ?? 'n/a'} ms`}
class="input input-sm preset-tonal-surface h-8 text-[10px]"
disabled={!get_device_id()} />
<button
type="button"
onclick={() => save_profile_delay(row.name)}
class="btn btn-sm preset-filled-primary h-8 px-3 text-[10px] font-bold"
disabled={!get_device_id()}>
<Save size="0.8em" class="mr-1" /> Save
</button>
<button
type="button"
onclick={() => reset_profile_delay(row.name)}
class="btn btn-sm preset-tonal-surface h-8 px-3 text-[10px]"
disabled={!get_device_id()}>
<RotateCcw size="0.8em" class="mr-1" /> Reset
</button>
</div>
{#if row_status[row.name]}
<div class="text-primary-500 mt-1 text-[9px] italic">
{row_status[row.name]}
</div>
{/if}
</section>
{/each}
</div>
</div> </div>
</Launcher_Cfg_Section> </Launcher_Cfg_Section>

View File

@@ -118,10 +118,10 @@ let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
/** /**
* Resolves the LaunchProfile for a given file extension and optional per-file * Resolves the LaunchProfile for a given file extension and optional per-file
* display override. Checked in priority order: * display override. Checked in priority order:
* 1. event_device.data_json.launch_profiles (API-driven, per-device) * 1. event_device.data_json.launch_profiles[profile] (API-driven, per-device)
* 2. $events_loc.launcher.launch_profiles (local persistent override) * 2. $events_loc.launcher.launch_profiles[profile] (local persistent override)
* 3. DEFAULT_LAUNCH_PROFILES[ext] (Svelte built-in defaults) * 3. DEFAULT_LAUNCH_PROFILES[profile/alias] (Svelte built-in defaults)
* 4. DEFAULT_LAUNCH_PROFILES['default'] (catch-all) * 4. DEFAULT_LAUNCH_PROFILES['default'] (catch-all)
* Per-file display_override from event_file.cfg_json overrides display_mode only. * Per-file display_override from event_file.cfg_json overrides display_mode only.
*/ */
function get_launch_profile( function get_launch_profile(
@@ -135,27 +135,12 @@ function get_launch_profile(
null; null;
const local_profiles = ($events_loc as any).launcher?.launch_profiles ?? null; const local_profiles = ($events_loc as any).launcher?.launch_profiles ?? null;
const display_override = file_obj?.cfg_json?.display_override ?? null; const display_override = file_obj?.cfg_json?.display_override ?? null;
const profile = resolve_launch_profile( return resolve_launch_profile(
extension, extension,
display_override, display_override,
device_profiles, device_profiles,
local_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(() => { onMount(() => {