feat(launcher): URL-type event_file support + displayplacer Electron task

URL files: event_file.filename = 'https://...' is now a first-class
file type in the launcher.

- is_url derived rune detects https/http filename prefix
- URL branch in handle_open_file() runs before cache/native branches
  (no download, no temp copy, no hash)
- Offline guard: warns and blocks click if navigator.onLine is false;
  online/offline listeners registered only for URL rows (no-op on files)
- Native mode: opens via native.open_external({ url, app: 'chrome' })
  with silent fallback to default browser
- Browser mode: window.open() with noopener
- display_mode default: 'mirror' (URLs typically just need the screen
  mirrored, not extended presenter view)
- Button badge shows Link2 icon + WifiOff warning when offline
- Button text uses event_file.title as label (falls back to URL)
- Test mode popup: skips Steps 1-2 (N/A), shows open_external call

DEFAULT_LAUNCH_PROFILES: add 'url' key (display_mode: 'mirror')

Electron TODO: added set_display_layout / displayplacer per-device
config task to aether_app_native_electron/documentation/TODO_AGENTS.md
with full contract details and resources
This commit is contained in:
Scott Idem
2026-05-12 13:14:58 -04:00
parent e74dc7a388
commit 768fdbfb21
2 changed files with 151 additions and 20 deletions

View File

@@ -345,6 +345,18 @@ end tell`
display_mode: 'none'
// No open_cmd — execution falls through to open_local_file_v2(path)
// No post_script
},
// -------------------------------------------------------------------------
// URL-type files: event_file.filename IS the URL (https://...)
// Opened via native.open_external({ url, app: 'chrome' }) — no local file involved.
// display_mode 'extend' is the default for URL presentations (e.g. Google Slides).
// -------------------------------------------------------------------------
url: {
app: 'Chrome',
display_mode: 'mirror'
// No open_cmd or post_script — URL branch in handle_open_file() handles this
}
};

View File

@@ -70,10 +70,12 @@ import {
CalendarDays,
FolderOpen,
Laptop,
Link2,
LoaderCircle,
Monitor,
Save,
Send
Send,
WifiOff
} from '@lucide/svelte';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
@@ -98,6 +100,19 @@ let test_mode_popup_data: Record<string, any> | null = $state(null);
/** Simple promise-based delay for post-open script timing */
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/** True when the device has network connectivity. Updated reactively for URL-type files. */
let is_online: boolean = $state(typeof navigator !== 'undefined' ? navigator.onLine : true);
/**
* True when this file's filename IS a URL rather than a hosted filename.
* Convention: event_file.filename = 'https://...' or 'http://...'
* Use event_file.title as the human-readable label; extension = 'url' (or omitted).
*/
const is_url = $derived(
(event_file_obj?.filename ?? '').startsWith('https://') ||
(event_file_obj?.filename ?? '').startsWith('http://')
);
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
/**
@@ -127,6 +142,17 @@ onMount(() => {
...event_file_obj
};
}
// Only register online/offline listeners for URL-type files — no point on file rows.
if (is_url && typeof window !== 'undefined') {
const on_online = () => (is_online = true);
const on_offline = () => (is_online = false);
window.addEventListener('online', on_online);
window.addEventListener('offline', on_offline);
return () => {
window.removeEventListener('online', on_online);
window.removeEventListener('offline', on_offline);
};
}
});
async function handle_open_file() {
@@ -136,6 +162,76 @@ async function handle_open_file() {
$events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj;
// URL-TYPE FILE: event_file.filename is a URL (https://...), not a hosted file path.
// Handled entirely here — no cache, no download, no temp copy.
if (is_url) {
const url = event_file_obj.filename as string;
// Test mode: show debug popup instead of opening
if ($events_loc.launcher.native_test_mode && $events_loc.launcher.app_mode === 'native') {
open_file_clicked = true;
open_file_status = 'opening_file';
const profile = get_launch_profile('url', event_file_obj);
test_mode_popup_data = {
is_url: true,
filename: url,
extension: 'url',
title: event_file_obj.title || null,
hash_sha256: null,
simulated_temp_path: null,
profile,
open_cmd_resolved: `native.open_external({ url: "${url}", app: "chrome" })`,
display_override: event_file_obj?.cfg_json?.display_override ?? null,
cache_check: 'N/A — URL file',
copy_to_temp: 'N/A — URL file'
};
test_mode_popup_open = true;
open_file_status = 'open';
open_file_status_message = 'Test Mode: URL profile resolved — see popup';
setTimeout(() => (open_file_clicked = false), 6000);
return true;
}
// Offline guard: warn and abort before attempting to open
if (!is_online) {
open_file_clicked = true;
open_file_status = 'error';
open_file_status_message = 'Network offline — cannot open URL';
open_file_error_detail = `URL: ${url}`;
setTimeout(() => (open_file_clicked = false), 6000);
return false;
}
open_file_clicked = true;
open_file_status = 'opening_file';
const profile = get_launch_profile('url', event_file_obj);
// URL presentations may still want to set display mode (e.g. Google Slides → extend)
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 unconfigured — continue */
});
}
open_file_status_message = `Opening ${event_file_obj.title || 'URL'}...`;
if ($ae_loc.is_native && $events_loc.launcher.app_mode === 'native') {
// Native: open in Chrome for kiosk-style presentation; fall back to default browser
const result = await native.open_external({ url, app: 'chrome' });
if (!result?.success) {
await native.open_external({ url, app: 'default' }).catch(() => {});
}
} else {
window.open(url, '_blank', 'noopener,noreferrer');
}
open_file_status = 'open';
open_file_status_message = `Opened: ${event_file_obj.title || url}`;
setTimeout(() => (open_file_clicked = false), 4000);
return true;
}
// 0. NATIVE TEST MODE — simulate full native flow, show debug popup instead of running commands
// Active when native_test_mode toggle is on (regardless of is_native / app_mode).
// Lets you preview the resolved profile, open command, and post-script from any device.
@@ -491,17 +587,23 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
{/if}
</span>
{:then result}
{@const FileIcon =
ae_util.file_extension_icon_lucide(
event_file_obj.extension
)}
<FileIcon size="1em" class="mx-0.5 inline" />
{event_file_obj.extension}
{#if result === null || result === false}
<span class="text-error-500"
><AlertTriangle
size="1em"
class="mx-1 inline" />Failed!</span>
{#if is_url}
<Link2 size="1em" class="mx-0.5 inline {!is_online ? 'text-warning-500' : ''}" />
<span class:text-warning-500={!is_online}>url</span>
{#if !is_online}<WifiOff size="0.85em" class="mx-0.5 inline text-warning-500" title="Network offline" />{/if}
{:else}
{@const FileIcon =
ae_util.file_extension_icon_lucide(
event_file_obj.extension
)}
<FileIcon size="1em" class="mx-0.5 inline" />
{event_file_obj.extension}
{#if result === null || result === false}
<span class="text-error-500"
><AlertTriangle
size="1em"
class="mx-1 inline" />Failed!</span>
{/if}
{/if}
{:catch error}
<span class="text-error-500" title={error?.message}
@@ -514,7 +616,9 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<span
class="grow {text_size} {text_size_md} w-full max-w-full overflow-hidden text-ellipsis {btn_text_align}">
{ae_util.shorten_string({
string: event_file_obj.filename_no_ext,
string: is_url
? (event_file_obj.title || event_file_obj.filename)
: event_file_obj.filename_no_ext,
begin_length: 45,
max_length: 65
})}
@@ -676,10 +780,18 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">File</span>
<div class="rounded bg-surface-500/10 px-3 py-2 leading-relaxed">
<div><span class="opacity-50">filename: </span>{test_mode_popup_data.filename}</div>
<div><span class="opacity-50">extension: </span>{test_mode_popup_data.extension}</div>
<div><span class="opacity-50">hash: </span><span class="opacity-60">{test_mode_popup_data.hash_sha256}</span></div>
<div><span class="opacity-50">temp path: </span><span class="text-success-600 dark:text-success-400">{test_mode_popup_data.simulated_temp_path}</span></div>
{#if test_mode_popup_data.is_url}
<div><span class="opacity-50">type: </span><span class="text-warning-500">URL file (no local cache)</span></div>
<div><span class="opacity-50">url: </span><span class="text-primary-500 break-all">{test_mode_popup_data.filename}</span></div>
{#if test_mode_popup_data.title}
<div><span class="opacity-50">title: </span>{test_mode_popup_data.title}</div>
{/if}
{:else}
<div><span class="opacity-50">filename: </span>{test_mode_popup_data.filename}</div>
<div><span class="opacity-50">extension: </span>{test_mode_popup_data.extension}</div>
<div><span class="opacity-50">hash: </span><span class="opacity-60">{test_mode_popup_data.hash_sha256}</span></div>
<div><span class="opacity-50">temp path: </span><span class="text-success-600 dark:text-success-400">{test_mode_popup_data.simulated_temp_path}</span></div>
{/if}
</div>
</div>
@@ -687,8 +799,12 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Steps 12</span>
<div class="rounded bg-surface-500/10 px-3 py-2 leading-relaxed">
<div><span class="opacity-50">check_hash_file_cache: </span><span class="text-success-600 dark:text-success-400">{test_mode_popup_data.cache_check}</span></div>
<div><span class="opacity-50">copy_from_cache_to_temp: </span><span class="text-success-600 dark:text-success-400">{test_mode_popup_data.copy_to_temp}</span></div>
{#if test_mode_popup_data.is_url}
<div class="opacity-40">Skipped URL file (no cache download or temp copy)</div>
{:else}
<div><span class="opacity-50">check_hash_file_cache: </span><span class="text-success-600 dark:text-success-400">{test_mode_popup_data.cache_check}</span></div>
<div><span class="opacity-50">copy_from_cache_to_temp: </span><span class="text-success-600 dark:text-success-400">{test_mode_popup_data.copy_to_temp}</span></div>
{/if}
</div>
</div>
@@ -721,7 +837,10 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Step 4 — Open File</span>
<div class="rounded bg-surface-500/10 px-3 py-2">
{#if test_mode_popup_data.open_cmd_resolved}
{#if test_mode_popup_data.is_url}
<div class="mb-1 opacity-50 text-[9px]">native.open_external()</div>
<pre class="whitespace-pre-wrap break-all text-primary-500">{test_mode_popup_data.open_cmd_resolved}</pre>
{:else if test_mode_popup_data.open_cmd_resolved}
<div class="mb-1 opacity-50 text-[9px]">native.run_cmd()</div>
<pre class="whitespace-pre-wrap break-all text-success-600 dark:text-success-400">{test_mode_popup_data.open_cmd_resolved}</pre>
{:else}