records. Update the Svelte resolver to read both keys so per-device tweaks remain backward compatible during the transition.
910 lines
42 KiB
Svelte
910 lines
42 KiB
Svelte
<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 1–2</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 5–6 — 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}
|