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:
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 1–2</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}
|
||||
|
||||
Reference in New Issue
Block a user