Files
OSIT-AE-App-Svelte/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte
2026-05-13 10:26:01 -04:00

910 lines
42 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
interface Props {
log_lvl?: number;
event_file_id: string;
event_file_obj: any;
max_filename_length?: number;
hide_launch_icon?: boolean;
hide_meta?: boolean;
hide_created_on?: boolean;
hide_os?: boolean;
hide_size?: boolean;
show_internal_purpose_files?: boolean;
show_bak_download?: boolean;
btn_size?: string;
btn_text_align?: string;
text_size?: string;
text_size_md?: string;
session_type?: string;
open_method?: null | string;
modal_title?: string;
modal__title?: any;
modal__open_event_file_id?: any;
modal__event_file_obj?: any;
}
let {
log_lvl = $bindable(0),
event_file_id,
event_file_obj = $bindable({}),
max_filename_length = $bindable(50),
hide_launch_icon = $bindable(false),
hide_meta = $bindable(false),
hide_created_on = $bindable(false),
hide_os = $bindable(false),
hide_size = $bindable(false),
show_internal_purpose_files = $bindable(false),
show_bak_download = false,
btn_size = $bindable('btn-sm'),
btn_text_align = $bindable('text-left'),
text_size = $bindable('text-sm'),
text_size_md = $bindable('md:text-base'),
session_type = $bindable('oral'),
open_method = $bindable('download'),
modal_title = $bindable(''),
modal__title = $bindable(''),
modal__open_event_file_id = $bindable(null),
modal__event_file_obj = $bindable(null)
}: Props = $props();
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { api } from '$lib/api/api';
import { ae_loc, ae_api, ae_sess, slct } from '$lib/stores/ae_stores';
import { core_func } from '$lib/ae_core/ae_core_functions';
import {
events_loc,
events_sess,
events_slct
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import {
AlertCircle,
AlertTriangle,
BarChart2,
CalendarDays,
FolderOpen,
Laptop,
Link2,
LoaderCircle,
Monitor,
Save,
Send,
WifiOff
} from '@lucide/svelte';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
// 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);
/** State for the native test mode debug popup */
let test_mode_popup_open: boolean = $state(false);
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'];
/**
* 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. event_device.data_json.launch_scripts (legacy alias)
* 3. $events_loc.launcher.launch_profiles (local persistent override)
* 4. $events_loc.launcher.launch_scripts (legacy alias)
* 5. DEFAULT_LAUNCH_PROFILES[ext] (Svelte built-in defaults)
* 6. DEFAULT_LAUNCH_PROFILES['default'] (catch-all)
* Per-file display_override from event_file.cfg_json overrides display_mode only.
*/
function get_launch_profile(
extension: string,
file_obj?: any
): LaunchProfile {
const device_profiles =
($ae_loc as any).native_device?.launch_profiles ??
($ae_loc as any).native_device?.launch_scripts ??
null;
const local_profiles =
($events_loc as any).launcher?.launch_profiles ??
($events_loc as any).launcher?.launch_scripts ??
null;
const display_override = file_obj?.cfg_json?.display_override ?? null;
return resolve_launch_profile(extension, display_override, device_profiles, local_profiles);
}
onMount(() => {
if (screen_saver_exts.includes(event_file_obj.extension)) {
if (!$events_loc.launcher.screen_saver_img_kv)
$events_loc.launcher.screen_saver_img_kv = {};
$events_loc.launcher.screen_saver_img_kv[event_file_id] = {
...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() {
if (log_lvl) console.log('*** handle_open_file() ***');
if (open_file_clicked) return; // Hard Guard: Already processing
$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.
if ($events_loc.launcher.native_test_mode && $events_loc.launcher.app_mode === 'native') {
open_file_clicked = true;
open_file_status = 'checking_cache';
open_file_status_message = 'Test Mode: simulating cache check...';
await sleep(400); // Brief simulated cache check
open_file_status = 'opening_file';
open_file_status_message = 'Test Mode: resolving launch profile...';
const profile = get_launch_profile(event_file_obj.extension, event_file_obj);
const open_cmd_resolved = profile.open_cmd
? profile.open_cmd.replaceAll('{{path}}', `/tmp/ae_test/${event_file_obj.filename}`)
: null;
test_mode_popup_data = {
filename: event_file_obj.filename,
extension: event_file_obj.extension,
hash_sha256: event_file_obj.hash_sha256,
simulated_temp_path: `/tmp/ae_test/${event_file_obj.filename}`,
profile,
open_cmd_resolved,
display_override: event_file_obj?.cfg_json?.display_override ?? null,
cache_check: 'PASS (simulated)',
copy_to_temp: 'PASS (simulated)'
};
test_mode_popup_open = true;
open_file_status = 'open';
open_file_status_message = 'Test Mode: profile resolved — see popup';
setTimeout(() => (open_file_clicked = false), 6000);
return true;
}
// 1. NATIVE MODE (Electron)
if ($ae_loc.is_native && $events_loc.launcher.app_mode === 'native') {
const cache_root = $ae_loc.local_file_cache_path;
const temp_root = $ae_loc.host_file_temp_path;
open_file_clicked = true;
open_file_status = 'checking_cache';
open_file_status_message = 'Checking local cache...';
open_file_error_detail = null; // Fix 1: clear stale error from any previous attempt
// Fix 2: safety valve — if a native call hangs and no path resets the button,
// force-release it after 60s. All normal paths reset within ~8s so this is last resort.
setTimeout(() => {
if (open_file_clicked) {
open_file_clicked = false;
open_file_status = 'error';
open_file_status_message = 'Timed out — please try again';
}
}, 60_000);
const exists = await native.check_hash_file_cache({
cache_root,
hash: event_file_obj.hash_sha256,
verify_hash: true // Hardened: Trust No One!
});
if (!exists) {
open_file_status = 'downloading_file';
open_file_status_message = 'Downloading file to cache...';
// Use the PROVEN endpoint path from api.ts that is known to work in Default Mode.
const url = `${$ae_api.base_url}/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(event_file_obj.filename)}&key=${$ae_api.account_id}`;
const dl_result = await native.download_to_cache({
url,
cache_root,
hash: event_file_obj.hash_sha256,
api_key: $ae_api.api_secret_key,
account_id: $ae_api.account_id
});
if (!dl_result.success) {
open_file_status = 'error';
open_file_status_message = `Download failed: ${dl_result.error}`;
setTimeout(() => (open_file_clicked = false), 5000);
return false;
}
}
// --- Step 1: Copy cached file to a writable temp path ---
open_file_status = 'opening_file';
open_file_status_message = 'Preparing file...';
open_file_error_detail = null;
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
});
if (!copy_result.success || !copy_result.path) {
open_file_status = '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.
// .catch: IPC reply can be orphaned if the renderer navigates before shell.openPath
// resolves. The file still opens — treat the orphaned reply as success.
const os_result = await native.open_local_file_v2(resolved_path).catch(() => ({ success: true }));
if (!os_result.success) {
open_ok = false;
open_error = os_result.error ?? 'open_local_file_v2 failed';
}
}
// Fix 3: update the status message as soon as the open call returns so "Opening..." doesn't
// appear stuck for the entire post_script sleep. OS has the request; we're just waiting now.
if (open_ok) {
open_file_status_message = profile.post_script
? `${profile.app} opened — running setup...`
: `${profile.app} opened`;
}
// --- Step 5: Wait for app to load before running post-script ---
// Only delay if there is actually a post_script to run — no point waiting for nothing.
if (open_ok && profile.post_script) {
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);
// .catch: same orphaned-reply guard as Step 4.
const fb_result = await native.open_local_file_v2(resolved_path).catch(() => ({ success: true }));
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 true;
}
// 2. ONSITE MODE (Browser with Modified Extensions)
else if ($events_loc.launcher.app_mode === 'onsite') {
open_file_clicked = true;
open_file_status = 'downloading_onsite';
open_file_status_message = 'Downloading (Onsite Mode)...';
open_file_error_detail = null;
let filename = event_file_obj.filename;
if (
(event_file_obj.extension === 'ppt' ||
event_file_obj.extension === 'pptx') &&
event_file_obj.open_in_os === 'win'
) {
filename = event_file_obj.filename + 'win';
}
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: filename,
x_no_account_id_token: 'direct-download'
},
filename: filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
// 3. DEFAULT MODE (Standard Browser)
else {
open_file_clicked = true;
open_file_status = 'downloading_default';
open_file_status_message = 'Downloading...';
open_file_error_detail = null;
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: event_file_obj.filename,
x_no_account_id_token: 'direct-download'
},
filename: event_file_obj.filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
if ($events_loc.launcher.controller == 'local_push') {
$events_sess.launcher.controller_cmd = `ae_download:hosted_file=${event_file_obj.hosted_file_id}:${event_file_obj.filename}:${event_file_obj.extension}`;
$events_sess.launcher.controller_trigger_send = true;
}
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script>
<div
class:justify-between={!hide_meta}
class:justify-center={hide_meta}
class:hidden={!show_internal_purpose_files &&
(event_file_obj.file_purpose == 'outline' ||
event_file_obj.file_purpose == 'draft' ||
event_file_obj.file_purpose == 'admin')}
class="event_launcher_file_cont flex max-w-full grow flex-col flex-wrap items-center justify-center gap-1 transition-all md:flex-row">
{#if open_file_clicked}
<div
class="open_file_clicked alert"
in:fade={{ duration: 250 }}
out:fade={{ duration: 2000 }}>
<div class="alert_msg_pulse">
<strong
>*** {open_file_status_message ||
'Please wait while this file downloads...'} ***</strong>
</div>
{#if $ae_loc.is_native && $events_loc.launcher.app_mode === 'native'}
{#if open_file_status === 'error'}
<p class="text-red-400">Failed to open file.</p>
{#if open_file_error_detail}
<pre class="mt-1 max-w-full overflow-x-auto whitespace-pre-wrap break-all rounded bg-black/30 px-2 py-1 font-mono text-xs text-red-300">{open_file_error_detail}</pre>
{/if}
{:else if open_file_status === 'fallback'}
<p class="text-yellow-400">(opened with OS default)</p>
{:else}
<p>Most files will automatically be opened full screen.</p>
<p>
PowerPoint or KeyNote will attempt to display in presenter
view.
</p>
<p>Please close the file when finished.</p>
{/if}
{/if}
</div>
{/if}
<span
class="event_file_action flex max-w-full grow flex-row flex-wrap items-center justify-center gap-1">
{#if session_type == 'poster' || open_method == 'modal'}
<AE_Comp_Hosted_Files_Download_Button
hosted_file_id={event_file_id}
hosted_file_obj={event_file_obj}
require_auth={false}
classes="btn btn-sm md:btn-md lg:btn-lg preset-tonal-primary border border-primary-500 w-full"
click={() => {
modal__open_event_file_id = event_file_id;
modal__event_file_obj = event_file_obj;
if (!modal__title) modal__title = event_file_obj.filename;
$events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj;
// Push the open command to the remote display when in local_push mode
if (
$events_loc.launcher.controller == 'local_push' &&
$events_sess.launcher.ws_connect_status == 'connected'
) {
$events_sess.launcher.controller_cmd = `ae_open:event_file=${event_file_id}`;
$events_sess.launcher.controller_trigger_send = true;
}
}}>
{#snippet label()}
{#if screen_saver_exts.includes(event_file_obj.extension)}
<BarChart2
size="1em"
class="{hide_launch_icon ? 'hidden' : ''} m-1" /> Open
Poster
{:else}
<Send
size="1em"
class="{hide_launch_icon ? 'hidden' : ''} m-1" />
{ae_util.shorten_filename({
filename: event_file_obj.filename,
max_length: max_filename_length
})}
{/if}
{/snippet}
</AE_Comp_Hosted_Files_Download_Button>
{:else}
<AE_Comp_Hosted_Files_Download_Button
hosted_file_id={event_file_id}
hosted_file_obj={event_file_obj}
require_auth={false}
track_click_promise={!($ae_loc.is_native && $events_loc.launcher.app_mode === 'native')}
classes="btn {btn_size} gap-1 justify-between min-w-full w-full max-w-96 preset-tonal-primary border border-primary-500"
click={handle_open_file}>
{#snippet label()}
{@const file_id = event_file_obj.hosted_file_id}
<span class="shrink border-r border-gray-400 pr-1 text-xs">
{#await ae_promises[event_file_id]}
<LoaderCircle
size="1em"
class="mx-0.5 inline animate-spin" />
<span>
{#if $ae_sess.api_download_kv[file_id]}
{$ae_sess.api_download_kv[file_id]
.percent_completed}%
{:else}
...
{/if}
</span>
{:then result}
{#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}
><AlertCircle
size="1em"
class="mx-0.5 inline" />Error!</span>
{/await}
</span>
<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: is_url
? (event_file_obj.title || event_file_obj.filename)
: event_file_obj.filename_no_ext,
begin_length: 45,
max_length: 65
})}
</span>
<span
class="badge preset-tonal-success hover:preset-filled-success-500 my-0 py-0.5 text-xs xl:text-sm"
class:hidden={!event_file_obj.file_purpose}>
{event_file_obj.file_purpose}
</span>
{/snippet}
</AE_Comp_Hosted_Files_Download_Button>
{/if}
</span>
<span
class="event_file_meta wrap flex w-64 max-w-80 grow flex-col items-center justify-between gap-1 font-mono text-sm text-gray-500 sm:flex-row"
class:hidden={hide_meta}>
<button
type="button"
onclick={async () => {
let new_val: string | null;
if (!event_file_obj?.open_in_os) new_val = 'win';
else if (event_file_obj?.open_in_os == 'win') new_val = 'mac';
else new_val = null;
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_file',
obj_id: event_file_id,
fields: { open_in_os: new_val }
});
events_func.load_ae_obj_id__event_file({
api_cfg: $ae_api,
event_file_id: event_file_obj?.event_file_id,
log_lvl
});
}}
class="btn btn-sm group transition-all"
class:preset-tonal-warning={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-success={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access}
title={`Open in OS: ${
event_file_obj?.open_in_os
? event_file_obj.open_in_os.toUpperCase()
: 'None'
}`}
>
{#if event_file_obj?.open_in_os == 'win'}
<!-- <Monitor
size="1em"
class="m-1" /> -->
Win
{:else if event_file_obj?.open_in_os == 'mac'}
<!-- <Laptop
size="1em"
class="m-1" /> -->
Mac
{:else}
<FolderOpen size="1em" class="m-1" />
{/if}
</button>
{#if $ae_loc.trusted_access && $ae_loc.is_native}
<!-- Display override: per-file display_mode override for this file only.
null = use profile default, 'extend' = force extend, 'mirror' = force mirror.
Stored in event_file.cfg_json.display_override. Cycles null → extend → mirror → null. -->
<button
type="button"
onclick={async () => {
const cur = event_file_obj?.cfg_json?.display_override ?? null;
let next: string | null;
if (!cur) next = 'extend';
else if (cur === 'extend') next = 'mirror';
else next = null;
const new_cfg = { ...(event_file_obj.cfg_json ?? {}), display_override: next };
await api.update_ae_obj({
api_cfg: $ae_api,
obj_type: 'event_file',
obj_id: event_file_id,
fields: { cfg_json: new_cfg }
});
events_func.load_ae_obj_id__event_file({
api_cfg: $ae_api,
event_file_id: event_file_obj?.event_file_id,
log_lvl
});
}}
class="btn btn-sm transition-all"
class:preset-tonal-primary={event_file_obj?.cfg_json?.display_override === 'extend'}
class:preset-tonal-warning={event_file_obj?.cfg_json?.display_override === 'mirror'}
title={`Display override: ${event_file_obj?.cfg_json?.display_override ?? 'default'}`}>
{#if event_file_obj?.cfg_json?.display_override === 'extend'}
Ext
{:else if event_file_obj?.cfg_json?.display_override === 'mirror'}
Mir
{:else}
<Monitor size="1em" class="m-1" />
{/if}
</button>
{/if}
<span
class="event_file_created_on preset-filled-surface-100-900 flex w-24 flex-row items-center justify-end gap-1 rounded px-1 py-0.5 text-center text-xs md:w-44"
class:hidden={hide_created_on}>
<CalendarDays size="0.85em" class="inline" />
<span class="w-18"
>{ae_util.iso_datetime_formatter(
event_file_obj.created_on,
'date_short'
)}</span>
</span>
<span
class="event_file_size preset-filled-surface-100-900 flex w-22 max-w-28 flex-row items-center justify-end gap-1 rounded py-0.5 text-center text-xs"
class:hidden={hide_size}>
<Save size="0.85em" class="inline" />
{#if event_file_obj.file_size}{ae_util.format_bytes(
event_file_obj.file_size
)}{/if}
</span>
</span>
</div>
<!-- Native Test Mode Debug Popup -->
<!-- Shows what WOULD be sent to Electron: resolved profile, open command, post-script.
Appears when native_test_mode is active and a file is "opened". -->
{#if test_mode_popup_open && test_mode_popup_data}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 p-4"
role="presentation"
onclick={() => (test_mode_popup_open = false)}>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="bg-surface-50/95 dark:bg-surface-900/95 border-warning-500/40 relative flex max-h-[90vh] w-full max-w-2xl flex-col gap-0 overflow-hidden rounded-xl border shadow-2xl"
role="dialog"
aria-modal="true"
aria-label="Native Test Mode Debug Info"
tabindex="-1"
onclick={(e) => e.stopPropagation()}>
<!-- Header -->
<div class="bg-warning-500/10 border-warning-500/30 flex items-center gap-2 border-b px-4 py-3">
<span class="text-warning-600 dark:text-warning-400 font-mono text-xs font-bold uppercase tracking-wider">
🧪 Native Test Mode What would run on Mac
</span>
<button
type="button"
onclick={() => (test_mode_popup_open = false)}
class="btn btn-xs preset-tonal-surface ml-auto">
Close
</button>
</div>
<!-- Scrollable content -->
<div class="flex flex-col gap-3 overflow-y-auto p-4 font-mono text-xs">
<!-- File info -->
<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">
{#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>
<!-- Cache / copy -->
<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">
{#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>
<!-- Resolved profile -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Resolved LaunchProfile</span>
<div class="rounded bg-surface-500/10 px-3 py-2 leading-relaxed">
<div><span class="opacity-50">app: </span><span class="text-primary-500">{test_mode_popup_data.profile.app}</span></div>
<div><span class="opacity-50">display_mode: </span><span class:text-primary-500={test_mode_popup_data.profile.display_mode === 'extend'} class:text-warning-500={test_mode_popup_data.profile.display_mode === 'mirror'} class:opacity-40={test_mode_popup_data.profile.display_mode === 'none'}>{test_mode_popup_data.profile.display_mode}</span></div>
{#if test_mode_popup_data.display_override}
<div><span class="text-warning-500 opacity-80">display_override (cfg_json): </span><span class="text-warning-500">{test_mode_popup_data.display_override}</span></div>
{/if}
<div><span class="opacity-50">post_delay_ms: </span>{test_mode_popup_data.profile.post_delay_ms ?? '(default: 2000ms)'}</div>
</div>
</div>
<!-- Step 3: set_display_layout -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Step 3 set_display_layout</span>
<div class="rounded bg-surface-500/10 px-3 py-2">
{#if test_mode_popup_data.profile.display_mode !== 'none'}
<span class="text-primary-500">native.set_display_layout({{ mode: '{test_mode_popup_data.profile.display_mode}' }})</span>
{:else}
<span class="opacity-40">skipped (display_mode: none)</span>
{/if}
</div>
</div>
<!-- Step 4: open command -->
<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.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}
<div class="opacity-50 text-[9px]">native.open_local_file_v2()</div>
<span class="text-warning-500">{test_mode_popup_data.simulated_temp_path}</span>
<span class="ml-2 opacity-40">(OS default handler)</span>
{/if}
</div>
</div>
<!-- Step 5: post-script -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Steps 56 — Wait + Post-Script</span>
<div class="rounded bg-surface-500/10 px-3 py-2">
<div class="mb-1 opacity-50">sleep({test_mode_popup_data.profile.post_script ? (test_mode_popup_data.profile.post_delay_ms ?? 2000) : 0}ms){test_mode_popup_data.profile.post_script ? '' : ' skipped (no post_script)'}</div>
{#if test_mode_popup_data.profile.post_script}
{#if test_mode_popup_data.profile.post_script.startsWith('shell:')}
<div class="mb-1 opacity-50 text-[9px]">native.run_cmd() [shell prefix]</div>
<pre class="whitespace-pre-wrap break-all text-warning-500">{test_mode_popup_data.profile.post_script.slice(6)}</pre>
{:else}
<div class="mb-1 opacity-50 text-[9px]">native.run_osascript() [AppleScript]</div>
<pre class="whitespace-pre-wrap text-purple-500">{test_mode_popup_data.profile.post_script}</pre>
{/if}
{:else}
<span class="opacity-40">no post_script</span>
{/if}
</div>
</div>
</div><!-- end scroll -->
</div>
</div>
{/if}