Prettier for Event Launcher

This commit is contained in:
Scott Idem
2026-03-24 12:13:59 -04:00
parent a3ed379b17
commit 7f6e286b73
27 changed files with 3766 additions and 3807 deletions

View File

@@ -1,17 +1,17 @@
<script lang="ts">
/**
* events/[event_id]/(launcher)/+layout.svelte
* Root layout for the launcher area.
* Ensures background sync runs globally regardless of active tab.
*/
// import { ae_loc } from '$lib/stores/ae_stores';
import Launcher_Background_Sync from './launcher_background_sync.svelte';
/**
* events/[event_id]/(launcher)/+layout.svelte
* Root layout for the launcher area.
* Ensures background sync runs globally regardless of active tab.
*/
// import { ae_loc } from '$lib/stores/ae_stores';
import Launcher_Background_Sync from './launcher_background_sync.svelte';
interface Props {
children?: import('svelte').Snippet;
}
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
let { children }: Props = $props();
</script>
<!-- Background Sync Process (Invisible) -->

View File

@@ -1,55 +1,56 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Clock, GraduationCap, IdCard, LayoutGrid } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
// Poster Kiosk mode presets:
// iframe=true hides global site chrome
// hide__launcher_menu/header/footer removes all launcher panels
// Oral/Default restores everything.
// WHY: A single tap lets event staff reconfigure a device (or all WS-connected
// remote devices) for a session type without touching individual toggles.
const POSTER_PRESET = {
iframe: true,
hide__launcher_menu: true,
hide__launcher_header: true,
hide__launcher_footer: true
};
const ORAL_PRESET = {
iframe: false,
hide__launcher_menu: false,
hide__launcher_header: false,
hide__launcher_footer: false
};
// Detect current mode: if both iframe AND hide_menu are on, we're in poster mode.
// Individual overrides are still possible via the checkboxes below.
let is_poster_mode = $derived(
$ae_loc.iframe === true && $events_loc.launcher.hide__launcher_menu === true
);
function apply_mode(mode: 'poster' | 'oral') {
const preset = mode === 'poster' ? POSTER_PRESET : ORAL_PRESET;
$ae_loc.iframe = preset.iframe;
$events_loc.launcher.hide__launcher_menu = preset.hide__launcher_menu;
$events_loc.launcher.hide__launcher_header = preset.hide__launcher_header;
$events_loc.launcher.hide__launcher_footer = preset.hide__launcher_footer;
// Push to WS-connected remote devices when we're acting as a controller.
// Only send when connected so the UI button doesn't silently no-op.
if (
$events_loc.launcher.ws_connect &&
($events_loc.launcher.controller === 'local_push' || $events_loc.launcher.controller === 'remote')
) {
$events_sess.launcher.controller_cmd = `ae_mode:${mode}`;
$events_sess.launcher.controller_trigger_send = 'trigger';
}
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Clock, GraduationCap, IdCard, LayoutGrid } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
// Poster Kiosk mode presets:
// iframe=true hides global site chrome
// hide__launcher_menu/header/footer removes all launcher panels
// Oral/Default restores everything.
// WHY: A single tap lets event staff reconfigure a device (or all WS-connected
// remote devices) for a session type without touching individual toggles.
const POSTER_PRESET = {
iframe: true,
hide__launcher_menu: true,
hide__launcher_header: true,
hide__launcher_footer: true
};
const ORAL_PRESET = {
iframe: false,
hide__launcher_menu: false,
hide__launcher_header: false,
hide__launcher_footer: false
};
// Detect current mode: if both iframe AND hide_menu are on, we're in poster mode.
// Individual overrides are still possible via the checkboxes below.
let is_poster_mode = $derived(
$ae_loc.iframe === true && $events_loc.launcher.hide__launcher_menu === true
);
function apply_mode(mode: 'poster' | 'oral') {
const preset = mode === 'poster' ? POSTER_PRESET : ORAL_PRESET;
$ae_loc.iframe = preset.iframe;
$events_loc.launcher.hide__launcher_menu = preset.hide__launcher_menu;
$events_loc.launcher.hide__launcher_header = preset.hide__launcher_header;
$events_loc.launcher.hide__launcher_footer = preset.hide__launcher_footer;
// Push to WS-connected remote devices when we're acting as a controller.
// Only send when connected so the UI button doesn't silently no-op.
if (
$events_loc.launcher.ws_connect &&
($events_loc.launcher.controller === 'local_push' ||
$events_loc.launcher.controller === 'remote')
) {
$events_sess.launcher.controller_cmd = `ae_mode:${mode}`;
$events_sess.launcher.controller_trigger_send = 'trigger';
}
}
</script>
<Launcher_Cfg_Section
@@ -57,135 +58,126 @@
icon={LayoutGrid}
bind:state={$events_loc.launcher.section_state__app_modes}
{on_expand}
description="Mode: {$events_loc.launcher.app_mode} | UI Layout"
>
description="Mode: {$events_loc.launcher.app_mode} | UI Layout">
<!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-3">
<!-- 0. Oral / Poster Kiosk Mode Preset Toggle -->
<div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1">Session Mode Preset</p>
<div class="grid grid-cols-2 gap-1 bg-surface-500/5 p-1 rounded-lg">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Session Mode Preset
</p>
<div class="bg-surface-500/5 grid grid-cols-2 gap-1 rounded-lg p-1">
<button
type="button"
onclick={() => apply_mode('oral')}
class="btn btn-xs font-bold text-[10px]"
class="btn btn-xs text-[10px] font-bold"
class:preset-filled-secondary={!is_poster_mode}
class:preset-tonal-surface={is_poster_mode}
title="Standard oral/presentation layout — menus and headers visible"
>
title="Standard oral/presentation layout — menus and headers visible">
<GraduationCap size="0.85em" class="mr-1 opacity-70" />
Oral / Default
</button>
<button
type="button"
onclick={() => apply_mode('poster')}
class="btn btn-xs font-bold text-[10px]"
class="btn btn-xs text-[10px] font-bold"
class:preset-filled-primary={is_poster_mode}
class:preset-tonal-surface={!is_poster_mode}
title="Digital Poster kiosk — hides site chrome, menu, header & footer"
>
title="Digital Poster kiosk — hides site chrome, menu, header & footer">
<IdCard size="0.85em" class="mr-1 opacity-70" />
Poster Kiosk
</button>
</div>
{#if $events_loc.launcher.ws_connect && ($events_loc.launcher.controller === 'local_push' || $events_loc.launcher.controller === 'remote')}
<p class="text-[8px] opacity-40 italic ml-1">Applies to all connected WS devices</p>
<p class="ml-1 text-[8px] italic opacity-40">
Applies to all connected WS devices
</p>
{/if}
</div>
<!-- 1. App Mode Selection -->
<div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Operational Environment</p
>
<div class="grid grid-cols-3 gap-1 bg-surface-500/5 p-1 rounded-lg">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Operational Environment
</p>
<div class="bg-surface-500/5 grid grid-cols-3 gap-1 rounded-lg p-1">
<button
type="button"
onclick={() => ($events_loc.launcher.app_mode = 'default')}
class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary={$events_loc.launcher
.app_mode === 'default'}
class:preset-tonal-surface={$events_loc.launcher.app_mode !==
'default'}
class:preset-tonal-surface={$events_loc.launcher
.app_mode !== 'default'}
title="Default standard web browser (Chromium, Firefox, Safari based) launcher, for remote presenters and testing before being onsite">
Web</button
>
Web</button>
<button
type="button"
onclick={() => ($events_loc.launcher.app_mode = 'native')}
class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary={$events_loc.launcher
.app_mode === 'native'}
class:preset-tonal-surface={$events_loc.launcher.app_mode !==
'native'}
title="Native Electron based app launcher, for onsite presenters in session rooms">App</button
>
class:preset-tonal-surface={$events_loc.launcher
.app_mode !== 'native'}
title="Native Electron based app launcher, for onsite presenters in session rooms"
>App</button>
<button
type="button"
onclick={() => ($events_loc.launcher.app_mode = 'onsite')}
class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary={$events_loc.launcher
.app_mode === 'onsite'}
class:preset-tonal-surface={$events_loc.launcher.app_mode !==
'onsite'}
title="Customized onsite OS and web browser (Chromium or Firefox based) launcher, for onsite presenters in for practice and onsite backup">Onsite</button
>
class:preset-tonal-surface={$events_loc.launcher
.app_mode !== 'onsite'}
title="Customized onsite OS and web browser (Chromium or Firefox based) launcher, for onsite presenters in for practice and onsite backup"
>Onsite</button>
</div>
</div>
<!-- 2. UI Layout Toggles -->
<div
class="flex flex-col gap-1 border-t border-surface-500/10 pt-2 mt-1"
>
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Interface Visibility</p
>
class="border-surface-500/10 mt-1 flex flex-col gap-1 border-t pt-2">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Interface Visibility
</p>
<div class="grid grid-cols-2 gap-2 p-1">
<label class="flex items-center gap-2 cursor-pointer group">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={
$events_loc.launcher.hide__launcher_header
}
class="checkbox checkbox-sm"
/>
<span class="text-xs group-hover:text-primary-500"
>Hide Header</span
>
class="checkbox checkbox-sm" />
<span class="group-hover:text-primary-500 text-xs"
>Hide Header</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={$events_loc.launcher.hide__launcher_menu}
class="checkbox checkbox-sm"
/>
<span class="text-xs group-hover:text-primary-500"
>Hide Menu</span
>
class="checkbox checkbox-sm" />
<span class="group-hover:text-primary-500 text-xs"
>Hide Menu</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={
$events_loc.launcher.hide__launcher_footer
}
class="checkbox checkbox-sm"
/>
<span class="text-xs group-hover:text-primary-500"
>Hide Footer</span
>
class="checkbox checkbox-sm" />
<span class="group-hover:text-primary-500 text-xs"
>Hide Footer</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={
$events_loc.launcher.hide__session_datetimes
}
class="checkbox checkbox-sm"
/>
<span class="text-xs group-hover:text-primary-500"
>Hide Times</span
>
class="checkbox checkbox-sm" />
<span class="group-hover:text-primary-500 text-xs"
>Hide Times</span>
</label>
</div>
</div>
@@ -202,8 +194,7 @@
$events_loc.launcher.time_hours = 12;
}
}}
class="btn btn-xs preset-tonal-surface w-full text-[10px]"
>
class="btn btn-xs preset-tonal-surface w-full text-[10px]">
<Clock size="0.85em" class="mr-1 opacity-50" />
Clock Format:
<strong>{$events_loc.launcher.time_hours}-hour</strong>
@@ -212,35 +203,30 @@
<!-- 4. Advanced Toggles (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-2"
>
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Technical Layout</p
>
class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-2 border-t pt-3">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Technical Layout
</p>
<div class="grid grid-cols-1 gap-2 p-1">
<label class="flex items-center gap-2 cursor-pointer group">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={$events_loc.launcher.hide__ws_element}
class="checkbox checkbox-sm"
/>
class="checkbox checkbox-sm" />
<span
class="text-xs group-hover:text-primary-500 italic"
>Hide WebSocket Debugger</span
>
class="group-hover:text-primary-500 text-xs italic"
>Hide WebSocket Debugger</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={
$events_loc.launcher.hide__modal_header_title
}
class="checkbox checkbox-sm"
/>
class="checkbox checkbox-sm" />
<span
class="text-xs group-hover:text-primary-500 italic"
>Hide Poster Modal Title</span
>
class="group-hover:text-primary-500 text-xs italic"
>Hide Poster Modal Title</span>
</label>
</div>
</div>

View File

@@ -1,16 +1,24 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Gamepad2, Link, Lock, LockOpen, Plug, RefreshCw, Unlink } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import {
Gamepad2,
Link,
Lock,
LockOpen,
Plug,
RefreshCw,
Unlink
} from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
const ws_connected = $derived(
$events_sess.launcher.ws_connect_status === 'connected'
);
const ws_connected = $derived(
$events_sess.launcher.ws_connect_status === 'connected'
);
</script>
<Launcher_Cfg_Section
@@ -20,41 +28,35 @@
{on_expand}
description="Mode: {$events_loc.launcher?.controller} | WS: {ws_connected
? 'Connected'
: 'Offline'}"
>
: 'Offline'}">
<!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-3">
<!-- 1. Connection Status Badge -->
<div
class="flex items-center justify-between bg-surface-500/5 p-2 rounded border border-surface-500/10"
>
class="bg-surface-500/5 border-surface-500/10 flex items-center justify-between rounded border p-2">
<div class="flex items-center gap-2">
<Plug size="1em" class="opacity-50" />
<span class="text-[10px] font-bold uppercase tracking-wider"
>WebSocket Link</span
>
<span class="text-[10px] font-bold tracking-wider uppercase"
>WebSocket Link</span>
</div>
{#if ws_connected}
<span
class="badge preset-filled-success text-[8px] animate-pulse"
>Connected</span
>
class="badge preset-filled-success animate-pulse text-[8px]"
>Connected</span>
{:else}
<span class="badge preset-filled-error text-[8px]"
>Disconnected</span
>
>Disconnected</span>
{/if}
</div>
<!-- 2. Controller Mode Selection -->
<div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Controller Strategy</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Controller Strategy
</p>
<select
bind:value={$events_loc.launcher.controller}
class="select select-sm text-xs preset-tonal-surface h-8"
>
class="select select-sm preset-tonal-surface h-8 text-xs">
<option value="local">Local Only</option>
<option value="remote">Remotely WS Controlled</option>
<option value="local_push">Local and WS Controller</option>
@@ -62,7 +64,7 @@
</div>
<!-- 3. Connection Actions -->
<div class="grid grid-cols-2 gap-2 mt-1">
<div class="mt-1 grid grid-cols-2 gap-2">
<button
type="button"
onclick={() => {
@@ -76,8 +78,7 @@
}}
class="btn btn-sm text-[10px] font-bold transition-all"
class:preset-tonal-error={$events_loc.launcher.ws_connect}
class:preset-tonal-success={!$events_loc.launcher.ws_connect}
>
class:preset-tonal-success={!$events_loc.launcher.ws_connect}>
{#if $events_loc.launcher.ws_connect}
<Unlink size="0.85em" class="mr-1" /> Disconnect
{:else}
@@ -92,8 +93,7 @@
$events_sess.launcher.controller_trigger_send = 'trigger';
}}
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 text-[10px] font-bold"
disabled={!ws_connected}
>
disabled={!ws_connected}>
<RefreshCw size="0.85em" class="mr-1" /> Group Reload
</button>
</div>
@@ -101,21 +101,19 @@
<!-- 4. Technical Config (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-2"
>
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Channel Configuration</p
>
class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-2 border-t pt-3">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Channel Configuration
</p>
<div class="flex gap-1">
<input
bind:value={$events_loc.launcher.controller_group_code}
placeholder="Group Code"
class="input input-sm grow text-[10px] h-7 preset-tonal-surface font-mono"
class="input input-sm preset-tonal-surface h-7 grow font-mono text-[10px]"
readonly={!$events_sess.launcher
.controller_unlock_group_code}
ondblclick={() =>
($events_sess.launcher.controller_unlock_group_code = true)}
/>
($events_sess.launcher.controller_unlock_group_code = true)} />
<button
type="button"
onclick={() =>
@@ -123,8 +121,7 @@
!$events_sess.launcher
.controller_unlock_group_code)}
class="btn btn-xs preset-tonal-surface"
title="Toggle Unlock"
>
title="Toggle Unlock">
{#if $events_sess.launcher.controller_unlock_group_code}
<LockOpen size="0.85em" class="text-primary-500" />
{:else}
@@ -132,7 +129,7 @@
{/if}
</button>
</div>
<p class="text-[8px] opacity-40 italic ml-1">
<p class="ml-1 text-[8px] italic opacity-40">
Double-click input to unlock editing. Changing code triggers
reconnect.
</p>

View File

@@ -1,44 +1,44 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { HeartPulse, RefreshCw } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { HeartPulse, RefreshCw } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
// Derived Usage Percentage for Visuals
let cpu_load_pct = $derived.by(() => {
const meta = $ae_loc.native_device?.meta_json;
// loadavg is [1m, 5m, 15m] - use 1m load
if (!meta?.loadavg || !Array.isArray(meta.loadavg)) return 15;
// Derived Usage Percentage for Visuals
let cpu_load_pct = $derived.by(() => {
const meta = $ae_loc.native_device?.meta_json;
// loadavg is [1m, 5m, 15m] - use 1m load
if (!meta?.loadavg || !Array.isArray(meta.loadavg)) return 15;
// Load average is usually 0.0 to N (cores). Normalize to 0-100 based on cores if available.
const load = meta.loadavg[0];
const cores = (meta.cpus || []).length || 1;
const pct = Math.round((load / cores) * 100);
return Math.min(Math.max(pct, 5), 100); // Clamp 5-100
});
// Load average is usually 0.0 to N (cores). Normalize to 0-100 based on cores if available.
const load = meta.loadavg[0];
const cores = (meta.cpus || []).length || 1;
const pct = Math.round((load / cores) * 100);
return Math.min(Math.max(pct, 5), 100); // Clamp 5-100
});
let ram_usage_pct = $derived.by(() => {
const meta = $ae_loc.native_device?.meta_json;
if (!meta?.total_mem || !meta?.free_mem) return 0;
let ram_usage_pct = $derived.by(() => {
const meta = $ae_loc.native_device?.meta_json;
if (!meta?.total_mem || !meta?.free_mem) return 0;
// Parse "16384MB" strings
const total = parseInt(meta.total_mem);
const free = parseInt(meta.free_mem);
if (isNaN(total) || isNaN(free)) return 0;
// Parse "16384MB" strings
const total = parseInt(meta.total_mem);
const free = parseInt(meta.free_mem);
if (isNaN(total) || isNaN(free)) return 0;
return Math.round(((total - free) / total) * 100);
});
return Math.round(((total - free) / total) * 100);
});
// Helper for usage color
function get_usage_color(pct: number) {
if (pct > 90) return 'bg-error-500';
if (pct > 70) return 'bg-warning-500';
return 'bg-success-500';
}
// Helper for usage color
function get_usage_color(pct: number) {
if (pct > 90) return 'bg-error-500';
if (pct > 70) return 'bg-warning-500';
return 'bg-success-500';
}
</script>
<Launcher_Cfg_Section
@@ -47,53 +47,48 @@
bind:state={$events_loc.launcher.section_state__health}
{on_expand}
description="Heartbeat: {$events_sess.launcher.heartbeat_info
.last_timestamp || 'Pending'}"
>
.last_timestamp || 'Pending'}">
<!-- Content omitted for brevity in instruction, but preserved in file -->
<!-- Telemetry Dashboard -->
<div
class="col-span-full flex flex-col gap-3 bg-surface-500/5 p-3 rounded-lg border border-surface-500/10"
>
class="bg-surface-500/5 border-surface-500/10 col-span-full flex flex-col gap-3 rounded-lg border p-3">
<!-- CPU Usage (Mock Logic if load not available yet) -->
<div class="flex flex-col gap-1">
<div
class="flex justify-between text-[9px] uppercase font-bold opacity-60"
>
class="flex justify-between text-[9px] font-bold uppercase opacity-60">
<span
>CPU Architecture: {$ae_loc.native_device?.meta_json
?.arch || '...'}</span
>
?.arch || '...'}</span>
<span>Load: {cpu_load_pct}%</span>
</div>
<div
class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden"
>
class="bg-surface-500/20 h-1.5 w-full overflow-hidden rounded-full">
<div
class="h-full transition-all duration-1000 {get_usage_color(cpu_load_pct)}"
style="width: {cpu_load_pct}%"
></div>
class="h-full transition-all duration-1000 {get_usage_color(
cpu_load_pct
)}"
style="width: {cpu_load_pct}%">
</div>
</div>
</div>
<!-- RAM Usage -->
<div class="flex flex-col gap-1">
<div
class="flex justify-between text-[9px] uppercase font-bold opacity-60"
>
class="flex justify-between text-[9px] font-bold uppercase opacity-60">
<span>Memory (RAM)</span>
<span>{ram_usage_pct}% Used</span>
</div>
<div
class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden"
>
class="bg-surface-500/20 h-1.5 w-full overflow-hidden rounded-full">
<div
class="h-full transition-all duration-1000 {get_usage_color(
ram_usage_pct
)}"
style="width: {ram_usage_pct}%"
></div>
style="width: {ram_usage_pct}%">
</div>
</div>
<div class="text-[8px] opacity-40 text-right italic">
<div class="text-right text-[8px] italic opacity-40">
Free: {$ae_loc.native_device?.meta_json?.free_mem || '...'} / {$ae_loc
.native_device?.meta_json?.total_mem || '...'}
</div>
@@ -101,26 +96,23 @@
</div>
<!-- Heartbeat & Sync Info -->
<div class="grid grid-cols-2 gap-x-2 gap-y-2 w-full text-[10px] p-1">
<div class="grid w-full grid-cols-2 gap-x-2 gap-y-2 p-1 text-[10px]">
<div class="flex flex-col">
<span class="opacity-50 text-[8px] uppercase font-bold"
>Last Heartbeat</span
>
<span class="text-[8px] font-bold uppercase opacity-50"
>Last Heartbeat</span>
<span
class="font-mono {$events_sess.launcher.heartbeat_info
.status === 'success'
? 'text-success-500'
: 'text-error-500'}"
>
: 'text-error-500'}">
{$events_sess.launcher.heartbeat_info.last_timestamp ||
'Pending...'}
</span>
</div>
<div class="flex flex-col text-right">
<span class="opacity-50 text-[8px] uppercase font-bold"
>Local File Cache</span
>
<span class="text-[8px] font-bold uppercase opacity-50"
>Local File Cache</span>
<span class="font-mono">
{$events_sess.launcher.sync_stats.cached} / {$events_sess
.launcher.sync_stats.total}
@@ -129,19 +121,18 @@
{#if $events_sess.launcher.sync_stats.currently_syncing}
<div
class="col-span-full bg-primary-500/10 p-2 rounded border border-primary-500/20 animate-pulse mt-1"
>
class="bg-primary-500/10 border-primary-500/20 col-span-full mt-1 animate-pulse rounded border p-2">
<div class="flex items-center gap-2">
<RefreshCw size="1em" class="animate-spin text-primary-500" />
<RefreshCw
size="1em"
class="text-primary-500 animate-spin" />
<div class="flex flex-col truncate">
<span
class="text-[8px] uppercase font-bold text-primary-500"
>Syncing File...</span
>
class="text-primary-500 text-[8px] font-bold uppercase"
>Syncing File...</span>
<span class="truncate italic opacity-80"
>{$events_sess.launcher.sync_stats
.currently_syncing}</span
>
.currently_syncing}</span>
</div>
</div>
</div>
@@ -151,26 +142,22 @@
<!-- Device Metadata (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div
class="col-span-full mt-1 pt-2 border-t border-surface-500/10 flex flex-col gap-1 text-[9px] opacity-60 px-1"
>
class="border-surface-500/10 col-span-full mt-1 flex flex-col gap-1 border-t px-1 pt-2 text-[9px] opacity-60">
<div class="flex justify-between">
<span>Hostname:</span>
<span class="font-mono"
>{$ae_loc.native_device.info_hostname || '...'}</span
>
>{$ae_loc.native_device.info_hostname || '...'}</span>
</div>
<div class="flex justify-between gap-4">
<span>IP Addresses:</span>
<span class="font-mono truncate"
>{$ae_loc.native_device.info_ip_list || '...'}</span
>
<span class="truncate font-mono"
>{$ae_loc.native_device.info_ip_list || '...'}</span>
</div>
<div class="mt-2 opacity-40">
<span class="text-[8px] uppercase font-bold"
>Raw Device JSON</span
>
<span class="text-[8px] font-bold uppercase"
>Raw Device JSON</span>
<pre
class="text-[7px] max-h-32 overflow-y-auto bg-black/20 p-1 rounded mt-1">
class="mt-1 max-h-32 overflow-y-auto rounded bg-black/20 p-1 text-[7px]">
{JSON.stringify($ae_loc.native_device, null, 2)}
</pre>
</div>

View File

@@ -1,77 +1,80 @@
<script lang="ts">
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { cleanup_tmp_files } from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Bug, BugOff, Eraser, Eye, EyeOff, Wrench } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { cleanup_tmp_files } from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Bug, BugOff, Eraser, Eye, EyeOff, Wrench } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
let selected_reset = $state('');
let cleanup_status = $state('');
async function handle_cleanup_now() {
const cache_root = $ae_loc.local_file_cache_path;
if (!cache_root) {
cleanup_status = 'Error: Cache path not set.';
return;
}
let { on_expand }: Props = $props();
const max_age_hours = $events_loc.launcher.cleanup_tmp_max_age_hours ?? 24;
cleanup_status = 'Cleaning...';
const result = await cleanup_tmp_files({
cache_root,
max_age_minutes: max_age_hours * 60
});
cleanup_status =
(result as any).success !== false
? 'Done.'
: `Error: ${(result as any).error}`;
setTimeout(() => (cleanup_status = ''), 4000);
}
let selected_reset = $state('');
let cleanup_status = $state('');
function handle_reset_action(val: string) {
if (!val) return;
async function handle_cleanup_now() {
const cache_root = $ae_loc.local_file_cache_path;
if (!cache_root) { cleanup_status = 'Error: Cache path not set.'; return; }
const max_age_hours = $events_loc.launcher.cleanup_tmp_max_age_hours ?? 24;
cleanup_status = 'Cleaning...';
const result = await cleanup_tmp_files({ cache_root, max_age_minutes: max_age_hours * 60 });
cleanup_status = (result as any).success !== false ? 'Done.' : `Error: ${(result as any).error}`;
setTimeout(() => (cleanup_status = ''), 4000);
}
function handle_reset_action(val: string) {
if (!val) return;
if (val == 'delete_idbs') {
if (
confirm(
'Are you sure you want to delete ALL IndexedDB databases?'
)
) {
indexedDB.deleteDatabase('ae_archives_db');
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db');
indexedDB.deleteDatabase('ae_journals_db');
indexedDB.deleteDatabase('ae_posts_db');
indexedDB.deleteDatabase('ae_sponsorships_db');
alert(
'All IndexedDB databases deleted. Please reload the app.'
);
}
} else if (val == 'delete_idbs_events') {
if (
confirm(
'Are you sure you want to delete ONLY the Events IndexedDB database?'
)
) {
indexedDB.deleteDatabase('ae_events_db');
alert(
'Events IndexedDB database deleted. Please reload the app.'
);
}
} else if (val == 'delete_local') {
if (confirm('Are you sure you want to delete ALL local config?')) {
localStorage.removeItem('ae_loc');
localStorage.removeItem('ae_events_loc');
localStorage.removeItem('ae_idaa_loc');
localStorage.removeItem('ae_journals_loc');
location.reload();
}
} else if (val == 'delete_local_events') {
if (
confirm(
'Are you sure you want to delete ONLY the Events local config?'
)
) {
localStorage.removeItem('ae_events_loc');
location.reload();
}
if (val == 'delete_idbs') {
if (
confirm('Are you sure you want to delete ALL IndexedDB databases?')
) {
indexedDB.deleteDatabase('ae_archives_db');
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db');
indexedDB.deleteDatabase('ae_journals_db');
indexedDB.deleteDatabase('ae_posts_db');
indexedDB.deleteDatabase('ae_sponsorships_db');
alert('All IndexedDB databases deleted. Please reload the app.');
}
} else if (val == 'delete_idbs_events') {
if (
confirm(
'Are you sure you want to delete ONLY the Events IndexedDB database?'
)
) {
indexedDB.deleteDatabase('ae_events_db');
alert('Events IndexedDB database deleted. Please reload the app.');
}
} else if (val == 'delete_local') {
if (confirm('Are you sure you want to delete ALL local config?')) {
localStorage.removeItem('ae_loc');
localStorage.removeItem('ae_events_loc');
localStorage.removeItem('ae_idaa_loc');
localStorage.removeItem('ae_journals_loc');
location.reload();
}
} else if (val == 'delete_local_events') {
if (
confirm(
'Are you sure you want to delete ONLY the Events local config?'
)
) {
localStorage.removeItem('ae_events_loc');
location.reload();
}
selected_reset = '';
}
selected_reset = '';
}
</script>
<Launcher_Cfg_Section
@@ -79,44 +82,43 @@
icon={Wrench}
bind:state={$events_loc.launcher.section_state__local_actions}
{on_expand}
description="Cache wiping and global menu toggles"
>
description="Cache wiping and global menu toggles">
<div class="col-span-full flex flex-col gap-3">
<!-- 1. Reset Actions -->
<div class="flex flex-col gap-1">
<span
class="text-[9px] font-bold uppercase opacity-50 ml-1 text-error-500"
>Maintenance & Resets</span
>
class="text-error-500 ml-1 text-[9px] font-bold uppercase opacity-50"
>Maintenance & Resets</span>
<select
bind:value={selected_reset}
onchange={(e) =>
handle_reset_action((e.target as HTMLSelectElement).value)}
class="select select-sm text-xs preset-tonal-surface h-8 text-error-500 border-error-500/20"
>
class="select select-sm preset-tonal-surface text-error-500 border-error-500/20 h-8 text-xs">
<option value="">-- Select a reset action --</option>
<option value="delete_idbs">Delete ALL Databases</option>
<option value="delete_idbs_events">Delete Events DB Only</option
>
<option value="delete_idbs_events"
>Delete Events DB Only</option>
<option value="delete_local">Wipe ALL Local Storage</option>
<option value="delete_local_events"
>Wipe Events Storage Only</option
>
>Wipe Events Storage Only</option>
</select>
<span class="text-[8px] opacity-40 italic ml-1 leading-tight">
<span class="ml-1 text-[8px] leading-tight italic opacity-40">
* Destructive actions require browser confirmation.
</span>
</div>
<!-- 2. UI Toggles -->
<div class="grid grid-cols-2 gap-2 mt-1">
<div class="mt-1 grid grid-cols-2 gap-2">
<button
type="button"
onclick={() => ($ae_loc.sys_menu.hide = !$ae_loc.sys_menu.hide)}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
title="Show/Hide Aether global system menu"
>
{#if $ae_loc.sys_menu.hide}<Eye size="1em" class="mr-2" />{:else}<EyeOff size="1em" class="mr-2" />{/if}
title="Show/Hide Aether global system menu">
{#if $ae_loc.sys_menu.hide}<Eye
size="1em"
class="mr-2" />{:else}<EyeOff
size="1em"
class="mr-2" />{/if}
{$ae_loc.sys_menu.hide ? 'Show' : 'Hide'} Sys Menu
</button>
@@ -125,41 +127,51 @@
onclick={() =>
($ae_loc.debug_menu.hide = !$ae_loc.debug_menu.hide)}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
title="Show/Hide Aether global debug menu"
>
{#if $ae_loc.debug_menu.hide}<Bug size="1em" class="mr-2" />{:else}<BugOff size="1em" class="mr-2" />{/if}
title="Show/Hide Aether global debug menu">
{#if $ae_loc.debug_menu.hide}<Bug
size="1em"
class="mr-2" />{:else}<BugOff
size="1em"
class="mr-2" />{/if}
{$ae_loc.debug_menu.hide ? 'Show' : 'Hide'} Debug
</button>
</div>
<!-- 3. Cache .tmp Cleanup (Native Only) -->
{#if $ae_loc.is_native && $ae_loc.local_file_cache_path}
<div class="flex flex-col gap-1 border-t border-surface-500/20 pt-2 mt-1">
<span class="text-[9px] font-bold uppercase opacity-50 ml-1">Cache Maintenance</span>
<div
class="border-surface-500/20 mt-1 flex flex-col gap-1 border-t pt-2">
<span class="ml-1 text-[9px] font-bold uppercase opacity-50"
>Cache Maintenance</span>
<div class="flex items-center gap-2">
<label for="cleanup_max_age" class="text-[10px] opacity-70 whitespace-nowrap">Max age (hrs):</label>
<label
for="cleanup_max_age"
class="text-[10px] whitespace-nowrap opacity-70"
>Max age (hrs):</label>
<input
id="cleanup_max_age"
type="number"
min="1"
max="168"
bind:value={$events_loc.launcher.cleanup_tmp_max_age_hours}
class="input input-sm w-16 h-7 text-xs text-center preset-tonal-surface"
placeholder="24"
/>
bind:value={
$events_loc.launcher.cleanup_tmp_max_age_hours
}
class="input input-sm preset-tonal-surface h-7 w-16 text-center text-xs"
placeholder="24" />
<button
type="button"
onclick={handle_cleanup_now}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 grow"
>
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 grow">
<Eraser size="0.85em" class="mr-1" /> Clean .tmp Now
</button>
</div>
{#if cleanup_status}
<span class="text-[9px] italic opacity-60 ml-1">{cleanup_status}</span>
<span class="ml-1 text-[9px] italic opacity-60"
>{cleanup_status}</span>
{/if}
<span class="text-[8px] opacity-40 italic ml-1 leading-tight">
Removes stale in-progress download artifacts. Auto-runs on startup.
<span class="ml-1 text-[8px] leading-tight italic opacity-40">
Removes stale in-progress download artifacts. Auto-runs on
startup.
</span>
</div>
{/if}
@@ -167,14 +179,12 @@
<!-- 4. Connection Summary (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div
class="col-span-full border-t border-surface-500/20 pt-2 mt-1 flex flex-col gap-1"
>
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>API Context</p
>
class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-1 border-t pt-2">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
API Context
</p>
<div
class="bg-black/10 p-2 rounded text-[9px] font-mono opacity-60 break-all leading-tight"
>
class="rounded bg-black/10 p-2 font-mono text-[9px] leading-tight break-all opacity-60">
Endpoint: {$ae_api.base_url}<br />
Account: {$ae_loc.account_id}
</div>

View File

@@ -1,50 +1,62 @@
<script lang="ts">
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Code, Columns2, FlaskConical, FolderOpen, Image, Maximize2, Monitor, Play, Power, RefreshCw, SkipBack, SkipForward, Square } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import {
Code,
Columns2,
FlaskConical,
FolderOpen,
Image,
Maximize2,
Monitor,
Play,
Power,
RefreshCw,
SkipBack,
SkipForward,
Square
} from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
let test_cmd_result = $state('');
let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint');
let remote_status = $state('');
let system_status = $state('');
async function handle_remote_control(
action: 'next' | 'prev' | 'start' | 'stop'
) {
remote_status = `Sending ${action}...`;
const res = await native.control_presentation({
app: remote_app,
action
});
if (res.success) {
remote_status = `Success: ${action}`;
} else {
remote_status = `Error: ${res.error}`;
}
let { on_expand }: Props = $props();
setTimeout(() => (remote_status = ''), 3000);
}
let test_cmd_result = $state('');
let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint');
let remote_status = $state('');
let system_status = $state('');
async function handle_remote_control(
action: 'next' | 'prev' | 'start' | 'stop'
) {
remote_status = `Sending ${action}...`;
const res = await native.control_presentation({
app: remote_app,
action
});
if (res.success) {
remote_status = `Success: ${action}`;
} else {
remote_status = `Error: ${res.error}`;
}
setTimeout(() => (remote_status = ''), 3000);
async function handle_system_action(promise: Promise<any>, label: string) {
system_status = `Executing ${label}...`;
const res = await promise;
if (res.success) {
system_status = `Success: ${label}`;
} else {
system_status = `Error: ${res.error || 'Unknown error'}`;
}
setTimeout(() => (system_status = ''), 3000);
}
async function handle_system_action(promise: Promise<any>, label: string) {
system_status = `Executing ${label}...`;
const res = await promise;
if (res.success) {
system_status = `Success: ${label}`;
} else {
system_status = `Error: ${res.error || 'Unknown error'}`;
}
setTimeout(() => (system_status = ''), 3000);
}
// Modal state for dangerous actions
let show_power_confirm = $state<{ action: string; label: string } | null>(
null
);
// Modal state for dangerous actions
let show_power_confirm = $state<{ action: string; label: string } | null>(null);
</script>
<Launcher_Cfg_Section
@@ -53,51 +65,50 @@
bind:state={$events_loc.launcher.section_state__native_os}
{on_expand}
description="OS: {$ae_loc.native_device?.meta_json?.platform ||
'...'} | Kiosk & Apps"
>
'...'} | Kiosk & Apps">
<!-- Dev preview banner: shown when edit_mode is on but not running in Electron.
electron_relay functions all return null when native is absent — no errors. -->
{#if $ae_loc.edit_mode && !$ae_loc.is_native}
<div class="flex items-center gap-2 px-2 py-1.5 rounded-lg bg-warning-500/10 border border-warning-500/30 mb-1">
<div
class="bg-warning-500/10 border-warning-500/30 mb-1 flex items-center gap-2 rounded-lg border px-2 py-1.5">
<FlaskConical size="0.75em" class="text-warning-500" />
<span class="text-[9px] text-warning-500 font-bold uppercase tracking-wide">Dev Preview — controls visible but non-functional without Electron</span>
<span
class="text-warning-500 text-[9px] font-bold tracking-wide uppercase"
>Dev Preview — controls visible but non-functional without
Electron</span>
</div>
{/if}
{#if system_status}
<div
class="col-span-full text-[10px] text-center italic bg-surface-500/10 py-1 rounded animate-pulse text-primary-500 border border-primary-500/20"
>
class="bg-surface-500/10 text-primary-500 border-primary-500/20 col-span-full animate-pulse rounded border py-1 text-center text-[10px] italic">
{system_status}
</div>
{/if}
<!-- 1. Window & Folders (Common) -->
<div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Folders & View</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Folders & View
</p>
<div class="grid grid-cols-2 gap-1">
<button
type="button"
onclick={() =>
native.open_folder($ae_loc.local_file_cache_path)}
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start">
<FolderOpen size="0.85em" class="mr-1 shrink-0" /> Cache
</button>
<button
type="button"
onclick={() => native.open_folder($ae_loc.host_file_temp_path)}
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start">
<FolderOpen size="0.85em" class="mr-1 shrink-0" /> Temp
</button>
<button
type="button"
onclick={() => native.window_control({ action: 'maximize' })}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500">
<Maximize2 size="0.85em" class="mr-1" /> Maximize
</button>
<button
@@ -107,8 +118,7 @@
native.window_control({ action: 'kiosk', value: true }),
'Kiosk Mode'
)}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500">
<Monitor size="0.85em" class="mr-1" /> Kiosk
</button>
</div>
@@ -116,14 +126,13 @@
<!-- 2. Presentation Remote Control (Common) -->
<div class="flex flex-col gap-2">
<div class="flex flex-row justify-between items-center px-1">
<p class="text-[9px] font-bold uppercase opacity-50"
>Remote Control</p
>
<div class="flex flex-row items-center justify-between px-1">
<p class="text-[9px] font-bold uppercase opacity-50">
Remote Control
</p>
<select
bind:value={remote_app}
class="select select-sm py-0 h-5 text-[9px] w-24 preset-tonal-surface"
>
class="select select-sm preset-tonal-surface h-5 w-24 py-0 text-[9px]">
<option value="powerpoint">PowerPoint</option>
<option value="keynote">Keynote</option>
</select>
@@ -134,39 +143,34 @@
type="button"
onclick={() => handle_remote_control('prev')}
class="btn btn-sm preset-tonal-secondary"
title="Previous Slide"
>
title="Previous Slide">
<SkipBack size="1em" />
</button>
<button
type="button"
onclick={() => handle_remote_control('start')}
class="btn btn-sm preset-tonal-success"
title="Start/Resume Slideshow"
>
title="Start/Resume Slideshow">
<Play size="1em" />
</button>
<button
type="button"
onclick={() => handle_remote_control('stop')}
class="btn btn-sm preset-tonal-error"
title="Stop Slideshow"
>
title="Stop Slideshow">
<Square size="1em" />
</button>
<button
type="button"
onclick={() => handle_remote_control('next')}
class="btn btn-sm preset-tonal-secondary"
title="Next Slide"
>
title="Next Slide">
<SkipForward size="1em" />
</button>
</div>
{#if remote_status}
<div
class="text-[9px] text-center italic animate-pulse text-primary-500"
>
class="text-primary-500 animate-pulse text-center text-[9px] italic">
{remote_status}
</div>
{/if}
@@ -175,13 +179,12 @@
<!-- 3. Technical Management (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-3"
>
class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-3 border-t pt-3">
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50"
>System Actions</p
>
<p class="text-[9px] font-bold uppercase opacity-50">
System Actions
</p>
<div class="grid grid-cols-1 gap-1">
<button
type="button"
@@ -192,9 +195,9 @@
}),
'Extend Display'
)}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start"
>
<Columns2 size="0.85em" class="mr-1 shrink-0" /> Extend Mode
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start">
<Columns2 size="0.85em" class="mr-1 shrink-0" /> Extend
Mode
</button>
<button
type="button"
@@ -206,17 +209,15 @@
'Wallpaper'
)}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start"
disabled={!$ae_loc.site_header_image_path}
>
disabled={!$ae_loc.site_header_image_path}>
<Image size="0.85em" class="mr-1 shrink-0" /> Reset Wallpaper
</button>
</div>
</div>
<div class="flex flex-col gap-1">
<span
class="text-[9px] font-bold uppercase opacity-50 text-error-500"
>Power</span
>
class="text-error-500 text-[9px] font-bold uppercase opacity-50"
>Power</span>
<div class="grid grid-cols-1 gap-1">
<button
type="button"
@@ -225,8 +226,7 @@
action: 'reboot',
label: 'Reboot Laptop'
})}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start"
>
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start">
<RefreshCw size="0.85em" class="mr-1 shrink-0" /> Reboot
</button>
<button
@@ -236,8 +236,7 @@
action: 'shutdown',
label: 'Shutdown Laptop'
})}
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start"
>
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start">
<Power size="0.85em" class="mr-1 shrink-0" /> Shutdown
</button>
</div>
@@ -245,16 +244,15 @@
</div>
<div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Terminal Access</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Terminal Access
</p>
<div class="flex gap-1">
<input
type="text"
bind:value={$events_sess.launcher.manual_cmd}
placeholder="ls -la"
class="input input-sm grow text-[10px] preset-tonal-surface h-7"
/>
class="input input-sm preset-tonal-surface h-7 grow text-[10px]" />
<button
type="button"
onclick={async () => {
@@ -268,13 +266,12 @@
(res as any).error ||
'No Output';
}}
class="btn btn-sm preset-filled-secondary hover:preset-filled-primary-500 text-[10px] h-7"
>Run</button
>
class="btn btn-sm preset-filled-secondary hover:preset-filled-primary-500 h-7 text-[10px]"
>Run</button>
</div>
{#if test_cmd_result}
<pre
class="text-[8px] bg-black text-green-500 p-2 mt-1 overflow-x-auto rounded border border-surface-500/50 max-h-24 shadow-inner">{test_cmd_result}</pre>
class="border-surface-500/50 mt-1 max-h-24 overflow-x-auto rounded border bg-black p-2 text-[8px] text-green-500 shadow-inner">{test_cmd_result}</pre>
{/if}
</div>
</div>
@@ -284,25 +281,21 @@
<!-- Power Confirmation Modal -->
{#if show_power_confirm}
<div
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
>
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
<div
class="card p-6 w-full max-w-sm preset-filled-surface-100-900 border border-error-500 shadow-2xl animate-in zoom-in-95 duration-200"
>
<h4 class="h4 text-error-500 font-bold mb-2">
class="card preset-filled-surface-100-900 border-error-500 animate-in zoom-in-95 w-full max-w-sm border p-6 shadow-2xl duration-200">
<h4 class="h4 text-error-500 mb-2 font-bold">
Confirm System Action
</h4>
<p class="text-sm opacity-80 mb-6">
<p class="mb-6 text-sm opacity-80">
Are you sure you want to <strong
>{show_power_confirm.action}</strong
> this host machine?
>{show_power_confirm.action}</strong> this host machine?
</p>
<div class="flex justify-end gap-2">
<button
type="button"
onclick={() => (show_power_confirm = null)}
class="btn btn-sm preset-tonal-surface">Cancel</button
>
class="btn btn-sm preset-tonal-surface">Cancel</button>
<button
type="button"
onclick={() => {
@@ -314,8 +307,7 @@
action
);
}}
class="btn btn-sm preset-filled-error"
>
class="btn btn-sm preset-filled-error">
Confirm {show_power_confirm.action}
</button>
</div>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { IdCard } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { IdCard } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
</script>
<Launcher_Cfg_Section
@@ -16,62 +16,54 @@
{on_expand}
description="Idle: {($events_loc.launcher.idle_timer / 60000).toFixed(
1
)}m | Auto-Posters"
>
)}m | Auto-Posters">
<!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-3">
<!-- 1. Technical Timers (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Screen Saver Timers (ms)</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Screen Saver Timers (ms)
</p>
<div
class="grid grid-cols-1 gap-2 bg-surface-500/5 p-2 rounded border border-surface-500/10"
>
<div class="flex justify-between items-center gap-4">
class="bg-surface-500/5 border-surface-500/10 grid grid-cols-1 gap-2 rounded border p-2">
<div class="flex items-center justify-between gap-4">
<span class="text-[10px] opacity-60">Idle Wait</span>
<input
type="number"
min={3000}
bind:value={$events_loc.launcher.idle_timer}
class="input input-sm text-[10px] h-7 w-24 text-right preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 w-24 text-right text-[10px]" />
</div>
<div class="flex justify-between items-center gap-4">
<div class="flex items-center justify-between gap-4">
<span class="text-[10px] opacity-60">Cycle Check</span>
<input
type="number"
min={500}
bind:value={$events_loc.launcher.idle_cycle}
class="input input-sm text-[10px] h-7 w-24 text-right preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 w-24 text-right text-[10px]" />
</div>
<div class="flex justify-between items-center gap-4">
<div class="flex items-center justify-between gap-4">
<span class="text-[10px] opacity-60"
>Image Rotation</span
>
>Image Rotation</span>
<input
type="number"
min={750}
bind:value={$events_loc.launcher.idle_loop_period}
class="input input-sm text-[10px] h-7 w-24 text-right preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 w-24 text-right text-[10px]" />
</div>
</div>
</div>
{:else}
<!-- 2. Read Only Summary (Normal Mode) -->
<div
class="bg-surface-500/5 p-3 rounded-lg border border-surface-500/10 flex flex-col gap-2"
>
<div class="flex justify-between items-center text-xs">
class="bg-surface-500/5 border-surface-500/10 flex flex-col gap-2 rounded-lg border p-3">
<div class="flex items-center justify-between text-xs">
<span class="opacity-60">Active Idle Timeout:</span>
<span class="font-bold text-primary-500"
>{($events_loc.launcher.idle_timer / 60000).toFixed(1)} minutes</span
>
<span class="text-primary-500 font-bold"
>{($events_loc.launcher.idle_timer / 60000).toFixed(1)} minutes</span>
</div>
<p class="text-[9px] opacity-40 italic">
<p class="text-[9px] italic opacity-40">
The screen saver automatically rotates digital posters when
no activity is detected for the specified time.
</p>
@@ -79,7 +71,7 @@
{/if}
<div class="text-center">
<p class="text-[8px] opacity-40 italic uppercase tracking-tighter">
<p class="text-[8px] tracking-tighter uppercase italic opacity-40">
Applies to "Poster" session types only
</p>
</div>

View File

@@ -1,97 +1,92 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { slide } from 'svelte/transition';
import { ChevronDown, ChevronRight, Pencil, Pin } from '@lucide/svelte';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyComponent = any;
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { slide } from 'svelte/transition';
import { ChevronDown, ChevronRight, Pencil, Pin } from '@lucide/svelte';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyComponent = any;
interface Props {
title: string;
icon: AnyComponent;
state: 'collapsed' | 'auto' | 'pinned';
description?: string;
children?: import('svelte').Snippet;
on_expand?: () => void;
on_toggle?: (new_state: 'collapsed' | 'auto' | 'pinned') => void;
interface Props {
title: string;
icon: AnyComponent;
state: 'collapsed' | 'auto' | 'pinned';
description?: string;
children?: import('svelte').Snippet;
on_expand?: () => void;
on_toggle?: (new_state: 'collapsed' | 'auto' | 'pinned') => void;
}
let {
title,
icon,
state = $bindable(),
description,
children,
on_expand,
on_toggle
}: Props = $props();
// Uppercase alias required: Svelte 5 treats lowercase tags as HTML elements.
let Icon = $derived(icon);
function toggle_expand() {
if (state === 'collapsed') {
state = 'auto';
if (on_expand) on_expand();
} else {
state = 'collapsed';
}
if (on_toggle) on_toggle(state);
}
let {
title,
icon,
state = $bindable(),
description,
children,
on_expand,
on_toggle
}: Props = $props();
// Uppercase alias required: Svelte 5 treats lowercase tags as HTML elements.
let Icon = $derived(icon);
function toggle_expand() {
if (state === 'collapsed') {
state = 'auto';
if (on_expand) on_expand();
} else {
state = 'collapsed';
}
if (on_toggle) on_toggle(state);
function toggle_pin(e: MouseEvent) {
e.stopPropagation();
if (state === 'pinned') {
state = 'auto';
if (on_expand) on_expand();
} else {
state = 'pinned';
}
if (on_toggle) on_toggle(state);
}
function toggle_pin(e: MouseEvent) {
e.stopPropagation();
if (state === 'pinned') {
state = 'auto';
if (on_expand) on_expand();
} else {
state = 'pinned';
}
if (on_toggle) on_toggle(state);
}
const is_open = $derived(state !== 'collapsed');
const is_open = $derived(state !== 'collapsed');
</script>
<section
class="w-full transition-all duration-300 border rounded-lg overflow-hidden mb-2 {!is_open
class="mb-2 w-full overflow-hidden rounded-lg border transition-all duration-300 {!is_open
? 'preset-outlined-surface-300-700'
: ''} {state === 'auto'
? 'preset-outlined-primary-500 shadow-xl'
: ''} {state === 'pinned'
? 'preset-outlined-warning-500 shadow-xl'
: ''}"
>
: ''}">
<!-- Header -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<header
class="flex flex-row items-center justify-between p-2 cursor-pointer transition-colors {!is_open
class="flex cursor-pointer flex-row items-center justify-between p-2 transition-colors {!is_open
? 'bg-surface-500/5'
: ''} {state === 'auto' ? 'bg-primary-500/10' : ''} {state ===
'pinned'
? 'bg-warning-500/10'
: ''}"
onclick={toggle_expand}
>
onclick={toggle_expand}>
<div class="flex items-center gap-3">
<Icon
size="1em"
class="w-5 text-center opacity-70 {state === 'auto'
? 'text-primary-500'
: ''} {state === 'pinned' ? 'text-warning-500' : ''}"
/>
: ''} {state === 'pinned' ? 'text-warning-500' : ''}" />
<div class="flex flex-col">
<span
class="text-sm font-bold tracking-tight uppercase {!is_open
? 'opacity-50'
: ''}">{title}</span
>
: ''}">{title}</span>
{#if description && !is_open}
<span
class="text-[9px] opacity-40 italic truncate max-w-[180px]"
>{description}</span
>
class="max-w-[180px] truncate text-[9px] italic opacity-40"
>{description}</span>
{/if}
</div>
</div>
@@ -106,16 +101,19 @@
class:text-warning-500={state === 'pinned'}
title={state === 'pinned'
? 'Unpin Section'
: 'Pin Section (Stay open)'}
>
: 'Pin Section (Stay open)'}>
<Pin size="0.7em" />
</button>
<!-- Collapse Icon -->
{#if is_open}
<ChevronDown size="1em" class="transition-transform duration-300 opacity-30" />
<ChevronDown
size="1em"
class="opacity-30 transition-transform duration-300" />
{:else}
<ChevronRight size="1em" class="transition-transform duration-300 opacity-30" />
<ChevronRight
size="1em"
class="opacity-30 transition-transform duration-300" />
{/if}
</div>
</header>
@@ -124,27 +122,22 @@
{#if is_open}
<div
transition:slide={{ duration: 300 }}
class="p-3 bg-white/5 dark:bg-black/5"
>
class="bg-white/5 p-3 dark:bg-black/5">
{#if $ae_loc.edit_mode}
<div class="mb-2 flex justify-between items-center px-1">
<div class="mb-2 flex items-center justify-between px-1">
<span
class="text-[8px] uppercase font-bold tracking-widest text-primary-500/60 flex items-center gap-1"
>
class="text-primary-500/60 flex items-center gap-1 text-[8px] font-bold tracking-widest uppercase">
<Pencil size="0.7em" /> Technical Mode
</span>
{#if state === 'pinned'}
<span
class="badge preset-filled-warning text-[8px] uppercase"
>Pinned</span
>
>Pinned</span>
{/if}
</div>
{/if}
<div
class="grid grid-cols-1 gap-3"
>
<div class="grid grid-cols-1 gap-3">
{@render children?.()}
</div>
</div>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Pause, Play, RefreshCw } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { Pause, Play, RefreshCw } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
</script>
<Launcher_Cfg_Section
@@ -15,21 +15,24 @@
bind:state={$events_loc.launcher.section_state__sync_timers}
{on_expand}
description="Prefix: {$ae_loc.native_device?.hash_prefix_length ||
2} | Loops: Active"
>
2} | Loops: Active">
<!-- Content omitted for brevity, preserved in file -->
<!-- Pause toggle: always visible — useful during testing or onsite troubleshooting -->
<div class="flex items-center justify-between mb-2 p-2 rounded border border-surface-500/10 bg-surface-500/5">
<span class="text-[10px] font-bold uppercase tracking-wider opacity-70">
{$events_loc.launcher.sync_paused ? '⏸ Sync Paused' : '▶ Sync Active'}
<div
class="border-surface-500/10 bg-surface-500/5 mb-2 flex items-center justify-between rounded border p-2">
<span class="text-[10px] font-bold tracking-wider uppercase opacity-70">
{$events_loc.launcher.sync_paused
? '⏸ Sync Paused'
: '▶ Sync Active'}
</span>
<button
type="button"
onclick={() => ($events_loc.launcher.sync_paused = !$events_loc.launcher.sync_paused)}
onclick={() =>
($events_loc.launcher.sync_paused =
!$events_loc.launcher.sync_paused)}
class="btn btn-xs transition-all"
class:preset-tonal-warning={$events_loc.launcher.sync_paused}
class:preset-tonal-success={!$events_loc.launcher.sync_paused}
>
class:preset-tonal-success={!$events_loc.launcher.sync_paused}>
{#if $events_loc.launcher.sync_paused}
<Play size="0.85em" class="mr-1" /> Resume
{:else}
@@ -43,92 +46,80 @@
<!-- Technical Timers (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div class="flex flex-col gap-2">
<p
class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Polling Periods (ms)</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Polling Periods (ms)
</p>
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60">Event Data</span
>
<span class="text-[8px] opacity-60"
>Event Data</span>
<input
type="number"
bind:value={
$events_loc.launcher.sync_intervals.event
}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 text-[10px]" />
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60"
>Device Config</span
>
>Device Config</span>
<input
type="number"
bind:value={
$events_loc.launcher.sync_intervals.device
}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 text-[10px]" />
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60"
>Room/Location</span
>
>Room/Location</span>
<input
type="number"
bind:value={
$events_loc.launcher.sync_intervals.location
}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 text-[10px]" />
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60"
>Session Loop</span
>
>Session Loop</span>
<input
type="number"
bind:value={
$events_loc.launcher.sync_intervals.session
}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 text-[10px]" />
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60"
>Presentation Loop</span
>
>Presentation Loop</span>
<input
type="number"
bind:value={
$events_loc.launcher.sync_intervals.presentation
$events_loc.launcher.sync_intervals
.presentation
}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 text-[10px]" />
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60"
>Presenter Loop</span
>
>Presenter Loop</span>
<input
type="number"
bind:value={
$events_loc.launcher.sync_intervals.presenter
$events_loc.launcher.sync_intervals
.presenter
}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 text-[10px]" />
</div>
</div>
</div>
<div
class="flex flex-col gap-1 mt-1 border-t border-surface-500/10 pt-2"
>
<p
class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Cache Structure</p
>
class="border-surface-500/10 mt-1 flex flex-col gap-1 border-t pt-2">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Cache Structure
</p>
<div class="flex items-center justify-between px-1">
<span class="text-[9px]">Hash Prefix Length</span>
{#if $ae_loc.native_device}
@@ -136,17 +127,17 @@
bind:value={
$ae_loc.native_device.hash_prefix_length
}
class="select select-sm h-6 py-0 text-[10px] w-16 preset-tonal-surface"
>
class="select select-sm preset-tonal-surface h-6 w-16 py-0 text-[10px]">
<option value={1}>1 char</option>
<option value={2}>2 chars</option>
<option value={3}>3 chars</option>
</select>
{:else}
<span class="text-[9px] opacity-50 italic">loading…</span>
<span class="text-[9px] italic opacity-50"
>loading…</span>
{/if}
</div>
<p class="text-[8px] opacity-40 italic mt-1">
<p class="mt-1 text-[8px] italic opacity-40">
* Prefix change requires a full app reload to take
effect.
</p>
@@ -154,47 +145,40 @@
{:else}
<!-- Read Only Summary (Normal Mode) -->
<div
class="bg-surface-500/5 p-2 rounded border border-surface-500/10 flex flex-col gap-1"
>
class="bg-surface-500/5 border-surface-500/10 flex flex-col gap-1 rounded border p-2">
<div
class="flex justify-between text-[9px] opacity-60 font-mono"
>
class="flex justify-between font-mono text-[9px] opacity-60">
<span>Event Sync:</span>
<span
>{(
$events_loc.launcher.sync_intervals.event /
1000
).toFixed(1)}s</span
>
$events_loc.launcher.sync_intervals.event / 1000
).toFixed(1)}s</span>
</div>
<div
class="flex justify-between text-[9px] opacity-60 font-mono"
>
class="flex justify-between font-mono text-[9px] opacity-60">
<span>Room Monitor:</span>
<span
>{(
$events_loc.launcher.sync_intervals.location / 1000
).toFixed(1)}s</span
>
$events_loc.launcher.sync_intervals.location /
1000
).toFixed(1)}s</span>
</div>
<div
class="flex justify-between text-[9px] opacity-60 font-mono border-t border-surface-500/10 pt-1"
>
class="border-surface-500/10 flex justify-between border-t pt-1 font-mono text-[9px] opacity-60">
<span>Prefix Sharding:</span>
<span
>{$ae_loc.native_device?.hash_prefix_length || 2} chars</span
>
>{$ae_loc.native_device?.hash_prefix_length || 2} chars</span>
</div>
</div>
<div class="text-center">
<p class="text-[8px] opacity-40 italic">
<p class="text-[8px] italic opacity-40">
Enable Edit Mode to adjust polling intervals.
</p>
</div>
{/if}
</div>
{:else}
<div class="text-center p-4 opacity-50 italic text-xs">
<div class="p-4 text-center text-xs italic opacity-50">
Device configuration not loaded.
</div>
{/if}

View File

@@ -1,41 +1,48 @@
<script lang="ts">
/**
* Launcher_Cfg_Template.svelte
* A "Kitchen Sink" scaffold demonstrating all standard UI patterns
* for the Launcher Configuration overhaul.
*/
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { AlertTriangle, Boxes, RefreshCw, Settings, Trash2, Zap } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
/**
* Launcher_Cfg_Template.svelte
* A "Kitchen Sink" scaffold demonstrating all standard UI patterns
* for the Launcher Configuration overhaul.
*/
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import {
AlertTriangle,
Boxes,
RefreshCw,
Settings,
Trash2,
Zap
} from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
// --- 1. LOCAL STATE ---
let toggle_val = $state(false);
let text_input = $state('');
let number_input = $state(100);
let select_val = $state('option1');
let is_loading = $state(false);
let action_status = $state('');
// --- 1. LOCAL STATE ---
let toggle_val = $state(false);
let text_input = $state('');
let number_input = $state(100);
let select_val = $state('option1');
let is_loading = $state(false);
let action_status = $state('');
// --- 2. LOGIC HANDLERS ---
async function handle_test_action(label: string) {
is_loading = true;
action_status = `Executing ${label}...`;
// --- 2. LOGIC HANDLERS ---
async function handle_test_action(label: string) {
is_loading = true;
action_status = `Executing ${label}...`;
// Simulate async work
await new Promise((r) => setTimeout(r, 1500));
// Simulate async work
await new Promise((r) => setTimeout(r, 1500));
is_loading = false;
action_status = `Finished: ${label}`;
setTimeout(() => (action_status = ''), 3000);
}
is_loading = false;
action_status = `Finished: ${label}`;
setTimeout(() => (action_status = ''), 3000);
}
// Modal state for destructive actions
let show_confirm = $state(false);
// Modal state for destructive actions
let show_confirm = $state(false);
</script>
<Launcher_Cfg_Section
@@ -43,100 +50,88 @@
icon={Boxes}
bind:state={$events_loc.launcher.section_state__template}
{on_expand}
description="Kitchen Sink Scaffold | Demo Only"
>
description="Kitchen Sink Scaffold | Demo Only">
<!-- A. TOP STATUS BAR (Optional) -->
{#if action_status}
<div
class="col-span-full text-[10px] text-center italic bg-primary-500/10 py-1 rounded animate-pulse text-primary-500 border border-primary-500/20"
>
class="bg-primary-500/10 text-primary-500 border-primary-500/20 col-span-full animate-pulse rounded border py-1 text-center text-[10px] italic">
{action_status}
</div>
{/if}
<!-- B. COMMON GRID SECTION (Read Only / High Level) -->
<div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Standard Actions</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Standard Actions
</p>
<!-- Action Buttons -->
<div class="grid grid-cols-2 gap-1">
<button
type="button"
onclick={() => handle_test_action('Primary')}
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start">
<Zap size="0.85em" class="mr-1 shrink-0" /> Primary
</button>
<button
type="button"
onclick={() => handle_test_action('Secondary')}
class="btn btn-xs preset-tonal-secondary hover:preset-filled-secondary-500 justify-start"
>
class="btn btn-xs preset-tonal-secondary hover:preset-filled-secondary-500 justify-start">
<Settings size="0.85em" class="mr-1 shrink-0" /> Secondary
</button>
</div>
<!-- Toggles & Checkboxes -->
<div class="flex flex-col gap-1 mt-1 bg-surface-500/5 p-2 rounded">
<label class="flex items-center gap-2 cursor-pointer group">
<div class="bg-surface-500/5 mt-1 flex flex-col gap-1 rounded p-2">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="checkbox"
bind:checked={toggle_val}
class="checkbox checkbox-sm"
/>
class="checkbox checkbox-sm" />
<span
class="text-xs group-hover:text-primary-500 transition-colors"
>Toggle Feature Alpha</span
>
class="group-hover:text-primary-500 text-xs transition-colors"
>Toggle Feature Alpha</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<label class="group flex cursor-pointer items-center gap-2">
<input
type="radio"
name="demo"
value="a"
class="radio radio-sm"
/>
class="radio radio-sm" />
<span
class="text-xs group-hover:text-primary-500 transition-colors"
>Mode A</span
>
class="group-hover:text-primary-500 text-xs transition-colors"
>Mode A</span>
</label>
</div>
</div>
<!-- C. STATUS & GAUGES SECTION -->
<div class="flex flex-col gap-2">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Current Status</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Current Status
</p>
<div
class="flex flex-col gap-2 p-2 border border-surface-500/10 rounded-lg"
>
<div class="flex justify-between items-center">
class="border-surface-500/10 flex flex-col gap-2 rounded-lg border p-2">
<div class="flex items-center justify-between">
<span class="text-[10px] font-medium">Engine Health</span>
<span class="badge preset-filled-success text-[8px] uppercase"
>Stable</span
>
>Stable</span>
</div>
<!-- Progress / Gauge Example -->
<div class="flex flex-col gap-1">
<div
class="flex justify-between text-[8px] uppercase opacity-60"
>
class="flex justify-between text-[8px] uppercase opacity-60">
<span>Processing Load</span>
<span>45%</span>
</div>
<div
class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden"
>
class="bg-surface-500/20 h-1.5 w-full overflow-hidden rounded-full">
<div
class="h-full bg-success-500 transition-all duration-1000"
style="width: 45%"
></div>
class="bg-success-500 h-full transition-all duration-1000"
style="width: 45%">
</div>
</div>
</div>
</div>
@@ -144,9 +139,10 @@
<button
type="button"
onclick={() => handle_test_action('Refresh')}
class="btn btn-xs preset-outlined-surface-500 w-full text-[10px]"
>
<RefreshCw size="0.85em" class="mr-1 {is_loading ? 'animate-spin' : ''}" />
class="btn btn-xs preset-outlined-surface-500 w-full text-[10px]">
<RefreshCw
size="0.85em"
class="mr-1 {is_loading ? 'animate-spin' : ''}" />
Refresh State
</button>
</div>
@@ -154,32 +150,28 @@
<!-- D. TECHNICAL SECTION (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div
class="col-span-full border-t border-surface-500/20 pt-3 mt-1 flex flex-col gap-3"
>
class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-3 border-t pt-3">
<!-- Dangerous Actions -->
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<span
class="text-[9px] font-bold uppercase opacity-50 text-warning-500"
>System Config</span
>
class="text-warning-500 text-[9px] font-bold uppercase opacity-50"
>System Config</span>
<button
type="button"
onclick={() => (show_confirm = true)}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start"
>
<AlertTriangle size="0.85em" class="mr-1 shrink-0" /> Reset All
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start">
<AlertTriangle size="0.85em" class="mr-1 shrink-0" /> Reset
All
</button>
</div>
<div class="flex flex-col gap-1">
<span
class="text-[9px] font-bold uppercase opacity-50 text-error-500"
>Danger Zone</span
>
class="text-error-500 text-[9px] font-bold uppercase opacity-50"
>Danger Zone</span>
<button
type="button"
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start"
>
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start">
<Trash2 size="0.85em" class="mr-1 shrink-0" /> Wipe Cache
</button>
</div>
@@ -188,21 +180,17 @@
<!-- Form Inputs -->
<div class="grid grid-cols-1 gap-2">
<div class="flex flex-col gap-1">
<span
class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Raw Settings</span
>
<span class="ml-1 text-[9px] font-bold uppercase opacity-50"
>Raw Settings</span>
<div class="flex gap-1">
<input
type="text"
bind:value={text_input}
placeholder="Enter string parameter..."
class="input input-sm grow text-[10px] preset-tonal-surface h-7"
/>
class="input input-sm preset-tonal-surface h-7 grow text-[10px]" />
<select
bind:value={select_val}
class="select select-sm h-7 py-0 text-[10px] w-24 preset-tonal-surface"
>
class="select select-sm preset-tonal-surface h-7 w-24 py-0 text-[10px]">
<option value="option1">Global</option>
<option value="option2">Local</option>
</select>
@@ -210,24 +198,22 @@
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60 ml-1"
>Threshold (ms)</span
>
<span class="ml-1 text-[8px] opacity-60"
>Threshold (ms)</span>
<input
type="number"
bind:value={number_input}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
class="input input-sm preset-tonal-surface h-7 text-[10px]" />
</div>
</div>
<!-- Terminal / Output Log -->
<div class="flex flex-col gap-1">
<p class="text-[9px] font-bold uppercase opacity-50 ml-1"
>Debug Output</p
>
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Debug Output
</p>
<pre
class="text-[8px] bg-black text-green-500 p-2 overflow-x-auto rounded border border-surface-500/50 max-h-24 shadow-inner">
class="border-surface-500/50 max-h-24 overflow-x-auto rounded border bg-black p-2 text-[8px] text-green-500 shadow-inner">
[LOG] System Initialized
[INFO] Store synced with IndexedDB
[DEBUG] active_tab: template
@@ -241,13 +227,11 @@
<!-- Confirmation Modal Demo -->
{#if show_confirm}
<div
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
>
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 p-4 backdrop-blur-sm">
<div
class="card p-6 w-full max-w-sm preset-filled-surface-100-900 border border-warning-500 shadow-2xl animate-in zoom-in-95 duration-200"
>
<h4 class="h4 text-warning-500 font-bold mb-2">Confirm Action</h4>
<p class="text-sm opacity-80 mb-6">
class="card preset-filled-surface-100-900 border-warning-500 animate-in zoom-in-95 w-full max-w-sm border p-6 shadow-2xl duration-200">
<h4 class="h4 text-warning-500 mb-2 font-bold">Confirm Action</h4>
<p class="mb-6 text-sm opacity-80">
Are you sure you want to perform this test operation? This
demonstrate the standard confirmation pattern.
</p>
@@ -255,16 +239,14 @@
<button
type="button"
onclick={() => (show_confirm = false)}
class="btn btn-sm preset-tonal-surface">Cancel</button
>
class="btn btn-sm preset-tonal-surface">Cancel</button>
<button
type="button"
onclick={() => {
show_confirm = false;
handle_test_action('Confirm');
}}
class="btn btn-sm preset-filled-warning"
>
class="btn btn-sm preset-filled-warning">
Yes, Proceed
</button>
</div>

View File

@@ -1,57 +1,55 @@
<script lang="ts">
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { CloudDownload, LoaderCircle, Search, Wand2 } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
import { ae_loc, ae_api, ae_sess } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import * as native from '$lib/electron/electron_relay';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
import { CloudDownload, LoaderCircle, Search, Wand2 } from '@lucide/svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
let update_source: 'url' | 'file' = $state('file');
let update_path = $state(
'~/OSIT/Speaker Ready System/Admin Share/Custom Applications/osit_binaries/'
);
let update_url = $state(
'https://dev-demo.oneskyit.com/updates/ae_native.zip'
);
let update_source: 'url' | 'file' = $state('file');
let update_path = $state(
'~/OSIT/Speaker Ready System/Admin Share/Custom Applications/osit_binaries/'
);
let update_url = $state('https://dev-demo.oneskyit.com/updates/ae_native.zip');
let update_status = $state('');
let is_checking = $state(false);
let download_result = $state<any>(null);
let update_status = $state('');
let is_checking = $state(false);
let download_result = $state<any>(null);
async function handle_check_update() {
is_checking = true;
update_status = 'Checking for updates...';
async function handle_check_update() {
is_checking = true;
update_status = 'Checking for updates...';
try {
const args =
update_source === 'url'
? { source: 'url' as const, url: update_url }
: { source: 'file' as const, path: update_path };
try {
const args =
update_source === 'url'
? { source: 'url' as const, url: update_url }
: { source: 'file' as const, path: update_path };
const res = await native.update_app(args);
const res = await native.update_app(args);
if (res.success) {
download_result = res;
update_status = 'Update located/downloaded. Ready to install.';
} else {
update_status = `Failed: ${res.error}`;
}
} catch (err: any) {
update_status = `Error: ${err.message}`;
} finally {
is_checking = false;
if (res.success) {
download_result = res;
update_status = 'Update located/downloaded. Ready to install.';
} else {
update_status = `Failed: ${res.error}`;
}
} catch (err: any) {
update_status = `Error: ${err.message}`;
} finally {
is_checking = false;
}
}
async function handle_install() {
update_status = 'Initiating installation...';
alert(
'Installation logic is OS-specific. This will typically swap the application bundle and restart.'
);
}
async function handle_install() {
update_status = 'Initiating installation...';
alert(
'Installation logic is OS-specific. This will typically swap the application bundle and restart.'
);
}
</script>
<Launcher_Cfg_Section
@@ -59,35 +57,31 @@
icon={CloudDownload}
bind:state={$events_loc.launcher.section_state__updates}
{on_expand}
description="v1.0.0 | Source: {update_source}"
>
description="v1.0.0 | Source: {update_source}">
<!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-2">
<!-- TECHNICAL: Source Config (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div
class="flex flex-col gap-2 bg-surface-500/5 p-2 rounded border border-surface-500/10 mb-1"
>
<div class="flex flex-row justify-between items-center px-1">
<p class="text-[9px] font-bold uppercase opacity-50"
>Source Type</p
>
class="bg-surface-500/5 border-surface-500/10 mb-1 flex flex-col gap-2 rounded border p-2">
<div class="flex flex-row items-center justify-between px-1">
<p class="text-[9px] font-bold uppercase opacity-50">
Source Type
</p>
<div class="flex gap-2">
<label class="flex items-center gap-1 text-[10px]">
<input
type="radio"
bind:group={update_source}
value="file"
class="radio radio-sm"
/> Local
class="radio radio-sm" /> Local
</label>
<label class="flex items-center gap-1 text-[10px]">
<input
type="radio"
bind:group={update_source}
value="url"
class="radio radio-sm"
/> Web
class="radio radio-sm" /> Web
</label>
</div>
</div>
@@ -97,15 +91,13 @@
type="text"
bind:value={update_path}
placeholder="Path to update package"
class="input input-sm text-[10px] preset-tonal-surface h-7 w-full"
/>
class="input input-sm preset-tonal-surface h-7 w-full text-[10px]" />
{:else}
<input
type="text"
bind:value={update_url}
placeholder="URL to update package"
class="input input-sm text-[10px] preset-tonal-surface h-7 w-full"
/>
class="input input-sm preset-tonal-surface h-7 w-full text-[10px]" />
{/if}
</div>
{/if}
@@ -115,10 +107,9 @@
type="button"
onclick={handle_check_update}
disabled={is_checking}
class="btn btn-sm preset-filled-tertiary hover:preset-filled-primary-500 text-[10px] w-full"
>
class="btn btn-sm preset-filled-tertiary hover:preset-filled-primary-500 w-full text-[10px]">
{#if is_checking}
<LoaderCircle size="0.85em" class="animate-spin mr-1" /> Checking...
<LoaderCircle size="0.85em" class="mr-1 animate-spin" /> Checking...
{:else}
<Search size="0.85em" class="mr-1" /> Check for Updates
{/if}
@@ -126,8 +117,7 @@
{#if update_status}
<div
class="text-[9px] text-center italic p-1 border border-surface-500/20 rounded bg-surface-500/5 mt-1"
>
class="border-surface-500/20 bg-surface-500/5 mt-1 rounded border p-1 text-center text-[9px] italic">
{update_status}
</div>
{/if}
@@ -136,8 +126,7 @@
<button
type="button"
onclick={handle_install}
class="btn btn-sm preset-filled-success hover:preset-filled-primary-500 text-[10px] w-full animate-bounce mt-2 shadow-lg"
>
class="btn btn-sm preset-filled-success hover:preset-filled-primary-500 mt-2 w-full animate-bounce text-[10px] shadow-lg">
<Wand2 size="0.85em" class="mr-1" /> Install & Relaunch
</button>
{/if}

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,54 @@
<script lang="ts">
interface Props {
data: any;
}
interface Props {
data: any;
}
let { data }: Props = $props();
let { data }: Props = $props();
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { ae_api } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { ae_api } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import { events_func } from '$lib/ae_events/ae_events_functions';
// Magic redirect: when ?session_id= is present but no event_location_id is in the route
// (e.g. navigating here from the Presenter View which has no location context),
// look up the session's location and redirect to the proper location-specific launcher URL.
// The [event_location_id] page handles the normal case where location is already in the route.
$effect(() => {
if (!browser) return;
// Magic redirect: when ?session_id= is present but no event_location_id is in the route
// (e.g. navigating here from the Presenter View which has no location context),
// look up the session's location and redirect to the proper location-specific launcher URL.
// The [event_location_id] page handles the normal case where location is already in the route.
$effect(() => {
if (!browser) return;
const session_id = data.url.searchParams.get('session_id');
const event_id = data.params.event_id;
if (!session_id || !event_id) return;
const session_id = data.url.searchParams.get('session_id');
const event_id = data.params.event_id;
if (!session_id || !event_id) return;
// Snapshot API config to avoid making it a reactive dependency of this effect
const api_cfg = $ae_api;
// Snapshot API config to avoid making it a reactive dependency of this effect
const api_cfg = $ae_api;
(async () => {
// Check Dexie cache first — sessions are typically cached from prior page visits
// Typed as any: Dexie returns Session|undefined, API returns ae_EventSession|null — both duck-type fine
let session: any = await db_events.session.get(session_id);
if (!session) {
// Not cached — fetch from API and save to Dexie
session = await events_func.load_ae_obj_id__event_session({
api_cfg,
event_session_id: session_id,
try_cache: false,
log_lvl: 0
});
}
(async () => {
// Check Dexie cache first — sessions are typically cached from prior page visits
// Typed as any: Dexie returns Session|undefined, API returns ae_EventSession|null — both duck-type fine
let session: any = await db_events.session.get(session_id);
if (!session) {
// Not cached — fetch from API and save to Dexie
session = await events_func.load_ae_obj_id__event_session({
api_cfg,
event_session_id: session_id,
try_cache: false,
log_lvl: 0
});
}
if (session?.event_location_id) {
// Session has a location — redirect to the location-specific launcher URL.
// replaceState: true so the user doesn't need to hit back twice to leave the launcher.
goto(
`/events/${event_id}/launcher/${session.event_location_id}?session_id=${session_id}`,
{ replaceState: true }
);
}
// If the session has no location set, the user stays on the base launcher page
// and the "Please select a location from the menu" prompt is shown by the layout.
})();
});
if (session?.event_location_id) {
// Session has a location — redirect to the location-specific launcher URL.
// replaceState: true so the user doesn't need to hit back twice to leave the launcher.
goto(
`/events/${event_id}/launcher/${session.event_location_id}?session_id=${session_id}`,
{ replaceState: true }
);
}
// If the session has no location set, the user stays on the base launcher page
// and the "Please select a location from the menu" prompt is shown by the layout.
})();
});
</script>

View File

@@ -1,76 +1,75 @@
<script lang="ts">
interface Props {
/** @type {import('./$types').PageData} */
data: any;
interface Props {
/** @type {import('./$types').PageData} */
data: any;
}
let { data }: Props = $props();
let log_lvl: number = $state(0);
// Imports
import { untrack } from 'svelte';
import { ae_loc, ae_sess, ae_api } from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct
} from '$lib/stores/ae_events_stores';
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]);
let url_event_id = $derived(data.params.event_id);
let url_event_location_id = $derived(data.params.event_location_id);
$effect(() => {
if (log_lvl > 1) {
console.log(`event_id: ${url_event_id}`);
console.log(`event_location_id: ${url_event_location_id}`);
}
untrack(() => {
$events_slct.event_id = url_event_id;
$events_slct.event_location_id = url_event_location_id;
});
});
let { data }: Props = $props();
let log_lvl: number = $state(0);
// Imports
import { untrack } from 'svelte';
import {
ae_loc,
ae_sess,
ae_api,
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
} from '$lib/stores/ae_events_stores';
// NOTE: Derived from data.account_id (prop) instead of $slct.account_id (store)
// to prevent circular dependency loops during hydration.
let ae_acct = $derived(data[data.account_id]);
let url_event_id = $derived(data.params.event_id);
let url_event_location_id = $derived(data.params.event_location_id);
$effect(() => {
if (log_lvl > 1) {
console.log(`event_id: ${url_event_id}`);
console.log(`event_location_id: ${url_event_location_id}`);
}
$effect(() => {
if (ae_acct) {
untrack(() => {
$events_slct.event_id = url_event_id;
$events_slct.event_location_id = url_event_location_id;
$events_slct.event_location_obj_li = ae_acct.slct
.event_location_obj_li ?? [''];
$events_slct.id_li__event_location = ae_acct.slct
.id_li__event_location ?? [''];
$events_slct.event_session_obj_li = ae_acct.slct
.event_session_obj_li ?? [''];
});
});
$effect(() => {
if (ae_acct) {
untrack(() => {
$events_slct.event_location_obj_li = ae_acct.slct.event_location_obj_li ?? [''];
$events_slct.id_li__event_location = ae_acct.slct.id_li__event_location ?? [''];
$events_slct.event_session_obj_li = ae_acct.slct.event_session_obj_li ?? [''];
});
}
});
// Set localStorage defaults for launcher state
if (!$events_loc.launcher) {
$events_loc.launcher = {};
$events_loc.launcher.slct = { event_id: null };
$events_loc.launcher.show_content__session_code = true;
$events_loc.launcher.show_content__presentation_code = true;
$events_loc.launcher.show_content__presenter_code = true;
}
if (!$events_loc.launcher.slct) {
$events_loc.launcher.slct = {
event_id: null,
event_location_id: null,
event_session_id: null,
event_presentation_id: null,
event_presenter_id: null
};
}
});
// Set session storage defaults
if (!$events_sess.launcher) $events_sess.launcher = {};
$events_sess.launcher.show_content__session_code = true;
$events_sess.launcher.show_content__presentation_code = true;
$events_sess.launcher.show_content__presenter_code = true;
// Set localStorage defaults for launcher state
if (!$events_loc.launcher) {
$events_loc.launcher = {};
$events_loc.launcher.slct = { event_id: null };
$events_loc.launcher.show_content__session_code = true;
$events_loc.launcher.show_content__presentation_code = true;
$events_loc.launcher.show_content__presenter_code = true;
}
if (!$events_loc.launcher.slct) {
$events_loc.launcher.slct = {
event_id: null,
event_location_id: null,
event_session_id: null,
event_presentation_id: null,
event_presenter_id: null
};
}
// Set session storage defaults
if (!$events_sess.launcher) $events_sess.launcher = {};
$events_sess.launcher.show_content__session_code = true;
$events_sess.launcher.show_content__presentation_code = true;
$events_sess.launcher.show_content__presenter_code = true;
</script>
<div class="hidden">This is for forcing data loading.</div>

View File

@@ -71,7 +71,8 @@ export async function load({ params, parent, url }) {
const session_id = url.searchParams.get('session_id');
if (browser && session_id) {
if (log_lvl) console.log(`Triggering deep load for session_id: ${session_id}`);
if (log_lvl)
console.log(`Triggering deep load for session_id: ${session_id}`);
events_func.load_ae_obj_id__event_session({
api_cfg: ae_acct.api,
event_session_id: session_id,

View File

@@ -1,78 +1,84 @@
<script lang="ts">
interface Props {
log_lvl?: number;
}
interface Props {
log_lvl?: number;
}
let { log_lvl = 0 }: Props = $props();
let { log_lvl = 0 }: Props = $props();
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger,
time
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger,
events_trig
} from '$lib/stores/ae_events_stores';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger,
time
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger,
events_trig
} from '$lib/stores/ae_events_stores';
// Sub-components
import Launcher_Cfg_Native_OS from './cfg_components/launcher_cfg_native_os.svelte';
import Launcher_Cfg_Sync_Timers from './cfg_components/launcher_cfg_sync_timers.svelte';
import Launcher_Cfg_Health from './cfg_components/launcher_cfg_health.svelte';
import Launcher_Cfg_Updates from './cfg_components/launcher_cfg_updates.svelte';
import Launcher_Cfg_Controller from './cfg_components/launcher_cfg_controller.svelte';
import Launcher_Cfg_Screen_Saver from './cfg_components/launcher_cfg_screen_saver.svelte';
import Launcher_Cfg_App_Modes from './cfg_components/launcher_cfg_app_modes.svelte';
import Launcher_Cfg_Local_Actions from './cfg_components/launcher_cfg_local_actions.svelte';
import { Bug, Code, Monitor, Pencil, RefreshCw, Settings, SlidersHorizontal, X } from '@lucide/svelte';
// UI Tab State
// Tabs are audience-oriented:
// setup — what every onsite operator needs (mode preset, display, WS, screen saver)
// device — sync engine (all devices) + native/Electron OS controls (native or edit_mode)
// dev — developer/debug tools; only useful when edit_mode is on
let active_tab: 'setup' | 'device' | 'dev' = $state('setup');
// Sub-components
import Launcher_Cfg_Native_OS from './cfg_components/launcher_cfg_native_os.svelte';
import Launcher_Cfg_Sync_Timers from './cfg_components/launcher_cfg_sync_timers.svelte';
import Launcher_Cfg_Health from './cfg_components/launcher_cfg_health.svelte';
import Launcher_Cfg_Updates from './cfg_components/launcher_cfg_updates.svelte';
import Launcher_Cfg_Controller from './cfg_components/launcher_cfg_controller.svelte';
import Launcher_Cfg_Screen_Saver from './cfg_components/launcher_cfg_screen_saver.svelte';
import Launcher_Cfg_App_Modes from './cfg_components/launcher_cfg_app_modes.svelte';
import Launcher_Cfg_Local_Actions from './cfg_components/launcher_cfg_local_actions.svelte';
import {
Bug,
Code,
Monitor,
Pencil,
RefreshCw,
Settings,
SlidersHorizontal,
X
} from '@lucide/svelte';
// UI Tab State
// Tabs are audience-oriented:
// setup — what every onsite operator needs (mode preset, display, WS, screen saver)
// device — sync engine (all devices) + native/Electron OS controls (native or edit_mode)
// dev — developer/debug tools; only useful when edit_mode is on
let active_tab: 'setup' | 'device' | 'dev' = $state('setup');
/**
* Auto-Collapse Coordinator
* When a section is opened in 'auto' mode, collapse all other 'auto' sections.
* Pinned sections are ignored and remain open.
*/
function handle_section_expand(current_key: string) {
const launcher = $events_loc.launcher;
Object.keys(launcher).forEach((key) => {
if (
key.startsWith('section_state__') &&
key !== `section_state__${current_key}`
) {
if (launcher[key] === 'auto') {
launcher[key] = 'collapsed';
}
/**
* Auto-Collapse Coordinator
* When a section is opened in 'auto' mode, collapse all other 'auto' sections.
* Pinned sections are ignored and remain open.
*/
function handle_section_expand(current_key: string) {
const launcher = $events_loc.launcher;
Object.keys(launcher).forEach((key) => {
if (
key.startsWith('section_state__') &&
key !== `section_state__${current_key}`
) {
if (launcher[key] === 'auto') {
launcher[key] = 'collapsed';
}
});
$events_loc.launcher = launcher; // Trigger store update
}
}
});
$events_loc.launcher = launcher; // Trigger store update
}
</script>
<div
class="
w-full max-w-full
flex flex-col gap-4 items-center justify-start
"
>
flex w-full
max-w-full flex-col items-center justify-start gap-4
">
<div
class="w-full flex flex-row items-center justify-between border-b border-surface-500/20 pb-2"
>
class="border-surface-500/20 flex w-full flex-row items-center justify-between border-b pb-2">
<h2
class="text-center text-lg font-bold text-gray-700 dark:text-gray-200"
>
class="text-center text-lg font-bold text-gray-700 dark:text-gray-200">
<Settings size="1em" class="mr-2 opacity-50" />
Launcher Configuration
</h2>
@@ -88,8 +94,7 @@
class:text-primary-500={$ae_loc.edit_mode}
class:opacity-20={!$ae_loc.edit_mode}
class:hover:opacity-60={!$ae_loc.edit_mode}
title="{$ae_loc.edit_mode ? 'Disable' : 'Enable'} Edit Mode"
>
title="{$ae_loc.edit_mode ? 'Disable' : 'Enable'} Edit Mode">
<Pencil size="0.75em" />
<span class="sr-only">Toggle Edit Mode</span>
</button>
@@ -97,8 +102,7 @@
<button
type="button"
onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)}
class="btn btn-icon dark:text-white hover:bg-surface-500/10 transition-colors"
>
class="btn btn-icon hover:bg-surface-500/10 transition-colors dark:text-white">
<X size="1em" />
<span class="sr-only">Close Config</span>
</button>
@@ -110,88 +114,80 @@
for onsite operators who never need those tools. Edit Mode is toggled via
the pencil icon in the header above. -->
<div
class="w-full gap-1 bg-surface-500/10 p-1 rounded-lg"
class="bg-surface-500/10 w-full gap-1 rounded-lg p-1"
class:grid={true}
class:grid-cols-2={!$ae_loc.edit_mode}
class:grid-cols-3={$ae_loc.edit_mode}
>
class:grid-cols-3={$ae_loc.edit_mode}>
<button
type="button"
onclick={() => (active_tab = 'setup')}
class="btn btn-sm text-[10px] uppercase font-bold transition-all"
class="btn btn-sm text-[10px] font-bold uppercase transition-all"
class:preset-filled-primary={active_tab === 'setup'}
class:preset-tonal-surface={active_tab !== 'setup'}
title="Display presets, interface toggles, WS controller, screen saver"
>
title="Display presets, interface toggles, WS controller, screen saver">
<SlidersHorizontal size="0.85em" class="mr-1" /> Setup
</button>
<button
type="button"
onclick={() => (active_tab = 'device')}
class="btn btn-sm text-[10px] uppercase font-bold transition-all"
class="btn btn-sm text-[10px] font-bold uppercase transition-all"
class:preset-filled-primary={active_tab === 'device'}
class:preset-tonal-surface={active_tab !== 'device'}
title="Sync engine, device health &amp; native OS controls"
>
title="Sync engine, device health &amp; native OS controls">
<Monitor size="0.85em" class="mr-1" /> Device
</button>
{#if $ae_loc.edit_mode}
<button
type="button"
onclick={() => (active_tab = 'dev')}
class="btn btn-sm text-[10px] uppercase font-bold transition-all"
class="btn btn-sm text-[10px] font-bold uppercase transition-all"
class:preset-filled-warning={active_tab === 'dev'}
class:preset-tonal-surface={active_tab !== 'dev'}
title="Developer &amp; debug tools"
>
title="Developer &amp; debug tools">
<Code size="0.85em" class="mr-1" /> Dev
</button>
{/if}
</div>
<!-- Tab Content -->
<div class="w-full flex flex-col gap-2 min-h-[400px]">
<div class="flex min-h-[400px] w-full flex-col gap-2">
<!-- SETUP: everything onsite operators need day-to-day -->
{#if active_tab === 'setup'}
<div class="animate-in fade-in slide-in-from-left-2 duration-300 flex flex-col gap-2">
<div
class="animate-in fade-in slide-in-from-left-2 flex flex-col gap-2 duration-300">
<!-- Mode preset is the #1 onsite action — give it prominent placement -->
<Launcher_Cfg_App_Modes
on_expand={() => handle_section_expand('app_modes')}
/>
on_expand={() => handle_section_expand('app_modes')} />
<Launcher_Cfg_Controller
on_expand={() => handle_section_expand('controller')}
/>
on_expand={() => handle_section_expand('controller')} />
<Launcher_Cfg_Screen_Saver
on_expand={() => handle_section_expand('screen_saver')}
/>
on_expand={() => handle_section_expand('screen_saver')} />
</div>
{/if}
<!-- DEVICE: sync engine first (all devices) + native OS controls (native or edit_mode preview) -->
{#if active_tab === 'device'}
<div class="animate-in fade-in slide-in-from-bottom-2 duration-300 flex flex-col gap-2">
<div
class="animate-in fade-in slide-in-from-bottom-2 flex flex-col gap-2 duration-300">
<!-- Sync pause/timers — relevant to every device, not just native -->
<Launcher_Cfg_Sync_Timers
on_expand={() => handle_section_expand('sync_timers')}
/>
on_expand={() => handle_section_expand('sync_timers')} />
<!-- Native sections: always in Electron; visible in edit_mode for dev preview.
electron_relay.ts guards all calls — safe to import/render without Electron. -->
{#if $ae_loc.is_native || $ae_loc.edit_mode}
<Launcher_Cfg_Health
on_expand={() => handle_section_expand('health')}
/>
on_expand={() => handle_section_expand('health')} />
<Launcher_Cfg_Native_OS
on_expand={() => handle_section_expand('native_os')}
/>
on_expand={() => handle_section_expand('native_os')} />
{#if $ae_loc.is_native}
<Launcher_Cfg_Updates
on_expand={() => handle_section_expand('updates')}
/>
on_expand={() =>
handle_section_expand('updates')} />
{/if}
{:else}
<div class="py-3 text-center opacity-40 italic text-xs flex flex-col gap-1 items-center">
<div
class="flex flex-col items-center gap-1 py-3 text-center text-xs italic opacity-40">
<Monitor size="1.2em" class="opacity-30" />
<p>Native OS controls available in Aether Desktop.</p>
<p class="text-[9px]">Enable Edit Mode to preview.</p>
@@ -202,27 +198,24 @@
<!-- DEV: developer/debug tools — only reachable when Edit Mode is on -->
{#if active_tab === 'dev' && $ae_loc.edit_mode}
<div class="animate-in fade-in slide-in-from-right-2 duration-300 flex flex-col gap-2">
<div
class="animate-in fade-in slide-in-from-right-2 flex flex-col gap-2 duration-300">
<Launcher_Cfg_Local_Actions
on_expand={() => handle_section_expand('local_actions')}
/>
on_expand={() => handle_section_expand('local_actions')} />
</div>
{/if}
</div>
<!-- Global Actions Footer -->
<div
class="w-full flex flex-col gap-2 border-t border-surface-500/20 pt-4 mt-auto"
>
class="border-surface-500/20 mt-auto flex w-full flex-col gap-2 border-t pt-4">
<div class="grid grid-cols-2 gap-2">
<!-- Close button — always visible in lower-left as a second dismissal point.
Useful in kiosk/iframe mode where the top-right close btn may scroll out of view. -->
<button
type="button"
onclick={() => ($events_loc.launcher.hide_drawer__cfg = true)}
class="btn btn-sm preset-tonal-surface hover:preset-filled-surface-500 transition-all"
>
class="btn btn-sm preset-tonal-surface hover:preset-filled-surface-500 transition-all">
<X size="0.85em" class="mr-1" />
Close
</button>
@@ -230,8 +223,7 @@
<button
type="button"
onclick={() => location.reload()}
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition-all"
>
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 transition-all">
<RefreshCw size="0.85em" class="mr-1" />
Reload
</button>
@@ -242,16 +234,14 @@
type="button"
onclick={() =>
($events_loc.launcher.hide_drawer__debug = false)}
class="btn btn-sm preset-tonal-warning hover:preset-filled-warning-500 transition-all w-full"
>
class="btn btn-sm preset-tonal-warning hover:preset-filled-warning-500 w-full transition-all">
<Bug size="0.85em" class="mr-1" />
Debug Panel
</button>
{/if}
<p
class="text-[9px] text-center opacity-40 uppercase font-bold tracking-widest mt-2"
>
class="mt-2 text-center text-[9px] font-bold tracking-widest uppercase opacity-40">
Aether Platform &bull; Events Launcher v3.0
</p>
</div>

View File

@@ -1,224 +1,235 @@
<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;
hide_draft?: 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;
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;
hide_draft?: 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;
}
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),
hide_draft = $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(''),
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),
hide_draft = $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();
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 { 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, LoaderCircle, Monitor, Save, Send } from '@lucide/svelte';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
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,
LoaderCircle,
Monitor,
Save,
Send
} 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 the relay
import * as native from '$lib/electron/electron_relay';
let ae_promises: key_val = $state({});
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_clicked: null | boolean = $state(null);
let open_file_status: null | string = $state(null);
let open_file_status_message: null | string = $state(null);
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
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
};
}
});
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;
// 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...';
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;
}
}
open_file_status = 'opening_file';
open_file_status_message = 'Opening Application';
// Phase 2/5: Use the atomic copy-and-launch operation.
// The main process handler (file_handlers.ts) now handles the
// specialized LibreOffice/AppleScript logic internally after copying.
const launch_result = await native.launch_from_cache({
cache_root,
hash: event_file_obj.hash_sha256,
temp_root,
filename: event_file_obj.filename
});
if (!launch_result.success) {
open_file_status = 'error';
open_file_status_message = `Failed to open: ${launch_result.error}`;
}
setTimeout(() => (open_file_clicked = false), 5000);
return launch_result.success;
}
// 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)...';
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...';
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);
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
};
}
});
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;
// 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...';
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;
}
}
open_file_status = 'opening_file';
open_file_status_message = 'Opening Application';
// Phase 2/5: Use the atomic copy-and-launch operation.
// The main process handler (file_handlers.ts) now handles the
// specialized LibreOffice/AppleScript logic internally after copying.
const launch_result = await native.launch_from_cache({
cache_root,
hash: event_file_obj.hash_sha256,
temp_root,
filename: event_file_obj.filename
});
if (!launch_result.success) {
open_file_status = 'error';
open_file_status_message = `Failed to open: ${launch_result.error}`;
}
setTimeout(() => (open_file_clicked = false), 5000);
return launch_result.success;
}
// 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)...';
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...';
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
@@ -227,24 +238,22 @@
class:hidden={hide_draft &&
(event_file_obj.file_purpose == 'outline' ||
event_file_obj.file_purpose == 'draft')}
class="event_launcher_file_cont grow flex flex-col md:flex-row flex-wrap gap-1 items-center justify-center max-w-full transition-all"
>
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 }}
>
out:fade={{ duration: 2000 }}>
<div class="alert_msg_pulse">
<strong
>*** {open_file_status_message ||
'Please wait while this file downloads...'} ***</strong
>
'Please wait while this file downloads...'} ***</strong>
</div>
{#if $ae_loc.is_native && $events_loc.launcher.app_mode === 'native'}
<p>Most files will automatically be opened full screen.</p>
<p>
PowerPoint or KeyNote will attempt to display in presenter view.
PowerPoint or KeyNote will attempt to display in presenter
view.
</p>
<p>Please close the file when finished.</p>
{/if}
@@ -252,8 +261,7 @@
{/if}
<span
class="event_file_action grow max-w-full flex flex-row flex-wrap gap-1 items-center justify-center"
>
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}
@@ -267,17 +275,24 @@
$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') {
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
<BarChart2
size="1em"
class="{hide_launch_icon ? 'hidden' : ''} m-1" /> Open
Poster
{:else}
<Send size="1em" class="{hide_launch_icon ? 'hidden' : ''} m-1" />
<Send
size="1em"
class="{hide_launch_icon ? 'hidden' : ''} m-1" />
{ae_util.shorten_filename({
filename: event_file_obj.filename,
max_length: max_filename_length
@@ -291,13 +306,14 @@
hosted_file_obj={event_file_obj}
require_auth={false}
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}
>
click={handle_open_file}>
{#snippet label()}
{@const file_id = event_file_obj.hosted_file_id}
<span class="shrink text-xs border-r border-gray-400 pr-1">
<span class="shrink border-r border-gray-400 pr-1 text-xs">
{#await ae_promises[event_file_id]}
<LoaderCircle size="1em" class="inline animate-spin mx-0.5" />
<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]
@@ -307,24 +323,28 @@
{/if}
</span>
{:then result}
{@const FileIcon = ae_util.file_extension_icon_lucide(event_file_obj.extension)}
<FileIcon size="1em" class="inline mx-0.5" />
{@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="inline mx-1" />Failed!</span
>
><AlertTriangle
size="1em"
class="mx-1 inline" />Failed!</span>
{/if}
{:catch error}
<span class="text-error-500" title={error?.message}
><AlertCircle size="1em" class="inline mx-0.5" />Error!</span
>
><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}"
>
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,
begin_length: 45,
@@ -333,9 +353,8 @@
</span>
<span
class="badge my-0 py-0.5 preset-tonal-success hover:preset-filled-success-500 text-xs xl:text-sm"
class:hidden={!event_file_obj.file_purpose}
>
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}
@@ -344,9 +363,8 @@
</span>
<span
class="event_file_meta grow text-sm text-gray-500 flex flex-col sm:flex-row gap-1 wrap items-center justify-between w-64 max-w-80 font-mono"
class:hidden={hide_meta}
>
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 () => {
@@ -366,33 +384,33 @@
log_lvl
});
}}
class="btn btn-sm transition-all group"
class="btn btn-sm group transition-all"
class:preset-tonal-success={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-warning={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access}
>
{#if event_file_obj?.open_in_os == 'win'}<Monitor size="1em" class="m-1" />
{:else if event_file_obj?.open_in_os == 'mac'}<Laptop size="1em" class="m-1" />
disabled={!$ae_loc.trusted_access}>
{#if event_file_obj?.open_in_os == 'win'}<Monitor
size="1em"
class="m-1" />
{:else if event_file_obj?.open_in_os == 'mac'}<Laptop
size="1em"
class="m-1" />
{:else}<FolderOpen size="1em" class="m-1" />{/if}
</button>
<span
class="event_file_created_on text-xs text-center flex flex-row gap-1 items-center justify-end w-24 md:w-44 preset-filled-surface-100-900 rounded px-1 py-0.5"
class:hidden={hide_created_on}
>
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>
<span
class="event_file_size text-xs text-center flex flex-row gap-1 items-center justify-end preset-filled-surface-100-900 w-22 max-w-28 rounded py-0.5"
class:hidden={hide_size}
>
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

View File

@@ -1,160 +1,159 @@
<script lang="ts">
/**
* launcher_menu.svelte — Aether Launcher: Sidebar Menu Container
*
* PURPOSE:
* The main sidebar panel for the Aether Events Launcher. Composes all menu
* sub-components into a single vertical column and passes data down from the
* parent layout. This is intentionally a thin coordinator — business logic lives
* in the individual sub-components and the layout's liveQuery / effect layer.
*
* STRUCTURE (top to bottom):
* 1. Event-level files (lq__event_event_file_obj_li)
* Files attached directly to the event (e.g. a shared opening slide deck).
* Shown regardless of which room/session is selected.
*
* 2. Location selector (Menu_location_list_menu — edit mode only)
* Dropdown that lets operators switch the active room. Triggers a session
* list reload and navigates the URL to /launcher/{location_id}.
*
* 3. Location-level files (lq__location_event_file_obj_li)
* Files attached to the selected room — e.g. A/V setup guides, room schedules.
* Hidden until a room is selected.
*
* 4. Session list (Menu_session_list_menu)
* Compact button list of all sessions in the selected room. Operators click
* to switch the active session; hover pre-loads after a delay timer.
*
* 5. Launcher controls (Menu_launcher_controls)
* Bottom bar for accessibility and visibility settings: font size cycler,
* light/dark toggle, and (edit mode only) show/hide draft files/sessions.
*
* DATA FLOW:
* All liveQuery stores (lq__*) are passed in from +layout.svelte and originate
* from Dexie IndexedDB — never fetched directly here. Selection state is
* coordinated via $events_slct / $events_loc stores.
*/
interface Props {
lq__event_obj: any;
/**
* launcher_menu.svelte — Aether Launcher: Sidebar Menu Container
*
* PURPOSE:
* The main sidebar panel for the Aether Events Launcher. Composes all menu
* sub-components into a single vertical column and passes data down from the
* parent layout. This is intentionally a thin coordinator — business logic lives
* in the individual sub-components and the layout's liveQuery / effect layer.
*
* STRUCTURE (top to bottom):
* 1. Event-level files (lq__event_event_file_obj_li)
* Files attached directly to the event (e.g. a shared opening slide deck).
* Shown regardless of which room/session is selected.
*
* 2. Location selector (Menu_location_list_menu — edit mode only)
* Dropdown that lets operators switch the active room. Triggers a session
* list reload and navigates the URL to /launcher/{location_id}.
*
* 3. Location-level files (lq__location_event_file_obj_li)
* Files attached to the selected room — e.g. A/V setup guides, room schedules.
* Hidden until a room is selected.
*
* 4. Session list (Menu_session_list_menu)
* Compact button list of all sessions in the selected room. Operators click
* to switch the active session; hover pre-loads after a delay timer.
*
* 5. Launcher controls (Menu_launcher_controls)
* Bottom bar for accessibility and visibility settings: font size cycler,
* light/dark toggle, and (edit mode only) show/hide draft files/sessions.
*
* DATA FLOW:
* All liveQuery stores (lq__*) are passed in from +layout.svelte and originate
* from Dexie IndexedDB — never fetched directly here. Selection state is
* coordinated via $events_slct / $events_loc stores.
*/
interface Props {
lq__event_obj: any;
lq__event_event_file_obj_li: any;
lq__location_event_file_obj_li: any;
slct__event_file_id?: string | null;
lq__event_event_file_obj_li: any;
lq__location_event_file_obj_li: any;
slct__event_file_id?: string | null;
lq__event_location_obj_li: any;
lq__event_location_obj?: any;
slct__event_location_id?: string | null;
lq__event_location_obj_li: any;
lq__event_location_obj?: any;
slct__event_location_id?: string | null;
loading__session_li_status?: null | boolean | string;
lq__event_session_obj_li: any;
loading__session_id_status?: null | boolean | string;
lq__event_session_obj?: any;
slct__event_session_id?: string | null;
loading__session_li_status?: null | boolean | string;
lq__event_session_obj_li: any;
loading__session_id_status?: null | boolean | string;
lq__event_session_obj?: any;
slct__event_session_id?: string | null;
trigger_reload__event_obj_id?: boolean | null | string;
trigger_reload__event_session_obj_id?: boolean | null | string;
trigger_reload__event_session_obj_li?: boolean;
trigger_reload__event_location_obj_id?: boolean | null | string;
trigger_reload__event_location_obj_li?: boolean;
trigger_reload__event_obj_id?: boolean | null | string;
trigger_reload__event_session_obj_id?: boolean | null | string;
trigger_reload__event_session_obj_li?: boolean;
trigger_reload__event_location_obj_id?: boolean | null | string;
trigger_reload__event_location_obj_li?: boolean;
log_lvl?: number;
}
log_lvl?: number;
}
let {
lq__event_obj,
let {
lq__event_obj,
lq__event_event_file_obj_li,
lq__location_event_file_obj_li,
slct__event_file_id = $bindable(null),
lq__event_event_file_obj_li,
lq__location_event_file_obj_li,
slct__event_file_id = $bindable(null),
lq__event_location_obj_li,
lq__event_location_obj,
slct__event_location_id = $bindable(null),
lq__event_location_obj_li,
lq__event_location_obj,
slct__event_location_id = $bindable(null),
loading__session_li_status = $bindable(null),
lq__event_session_obj_li,
loading__session_id_status = $bindable(null),
lq__event_session_obj,
slct__event_session_id = $bindable(null),
loading__session_li_status = $bindable(null),
lq__event_session_obj_li,
loading__session_id_status = $bindable(null),
lq__event_session_obj,
slct__event_session_id = $bindable(null),
trigger_reload__event_obj_id = $bindable(false),
trigger_reload__event_session_obj_id = $bindable(false),
trigger_reload__event_session_obj_li = $bindable(false),
trigger_reload__event_location_obj_id = $bindable(false),
trigger_reload__event_location_obj_li = $bindable(false),
trigger_reload__event_obj_id = $bindable(false),
trigger_reload__event_session_obj_id = $bindable(false),
trigger_reload__event_session_obj_li = $bindable(false),
trigger_reload__event_location_obj_id = $bindable(false),
trigger_reload__event_location_obj_li = $bindable(false),
log_lvl = $bindable(0)
}: Props = $props();
log_lvl = $bindable(0)
}: Props = $props();
// *** Import Svelte specific
// import { goto } from '$app/navigation';
// *** Import Svelte specific
// import { goto } from '$app/navigation';
// *** Import other supporting libraries
// import { liveQuery } from "dexie";
// *** Import other supporting libraries
// import { liveQuery } from "dexie";
// *** Import Aether specific variables and functions
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 Element_data_store from '$lib/element_data_store.svelte';
// *** Import Aether specific variables and functions
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 Element_data_store from '$lib/element_data_store.svelte';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
// import { db_events } from "$lib/ae_events/db_events";
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
// import { db_events } from "$lib/ae_events/db_events";
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Menu_location_list_menu from './menu_location_list.svelte';
import Menu_session_list_menu from './menu_session_list.svelte';
import Menu_launcher_controls from './menu_launcher_controls.svelte';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Menu_location_list_menu from './menu_location_list.svelte';
import Menu_session_list_menu from './menu_session_list.svelte';
import Menu_launcher_controls from './menu_launcher_controls.svelte';
// *** Functions and Logic
// *** Functions and Logic
$events_trigger = null;
$events_trigger = null;
let qry__enabled = 'enabled';
let qry__hidden = 'not_hidden';
if ($ae_loc.administrator_access) {
qry__enabled = 'all';
qry__hidden = 'all';
} else if ($ae_loc.trusted_access) {
qry__enabled = 'enabled';
qry__hidden = 'all';
} else {
qry__enabled = 'enabled';
qry__hidden = 'not_hidden';
}
let qry__enabled = 'enabled';
let qry__hidden = 'not_hidden';
if ($ae_loc.administrator_access) {
qry__enabled = 'all';
qry__hidden = 'all';
} else if ($ae_loc.trusted_access) {
qry__enabled = 'enabled';
qry__hidden = 'all';
} else {
qry__enabled = 'enabled';
qry__hidden = 'not_hidden';
}
let ae_promises: key_val = $state({
get_li__event_file: null
});
let ae_promises: key_val = $state({
get_li__event_file: null
});
</script>
<div
class="
event_launcher_menu
shrink h-full w-full max-w-full
flex flex-col flex-wrap gap-1 items-center justify-start
flex h-full w-full max-w-full
shrink flex-col flex-wrap items-center justify-start gap-1
"
>
">
<!-- overflow-x-clip -->
{#if $lq__event_event_file_obj_li}
<div class="w-full flex flex-col gap-0.5 overflow-y-auto">
<div class="flex w-full flex-col gap-0.5 overflow-y-auto">
<!-- <div class="text-xs text-neutral-800/80">
<strong>
Event Files:
@@ -191,8 +190,7 @@
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
} />
{/each}
</div>
{/if}
@@ -201,12 +199,11 @@
<Menu_location_list_menu
{lq__event_location_obj_li}
slct_event_location_id={$events_slct.event_location_id}
bind:loading__session_li_status
/>
bind:loading__session_li_status />
{/if}
{#if $lq__location_event_file_obj_li}
<div class="w-full flex flex-col gap-0.5">
<div class="flex w-full flex-col gap-0.5">
{#each $lq__location_event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)}
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
@@ -235,8 +232,7 @@
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
} />
{/each}
</div>
{/if}
@@ -246,8 +242,7 @@
bind:slct__event_session_id
bind:loading__session_id_status
{lq__event_session_obj_li}
bind:trigger_reload__event_session_obj_id
/>
bind:trigger_reload__event_session_obj_id />
{/if}
<Menu_launcher_controls />

View File

@@ -1,69 +1,67 @@
<script lang="ts">
interface Props {
lq__event_presentation_obj: any;
session_type?: string;
interface Props {
lq__event_presentation_obj: any;
session_type?: string;
}
let { lq__event_presentation_obj, session_type = '' }: Props = $props();
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
// Staggered Load: Trigger deep fetch only when the presentation ID changes.
// WHY: The SWR pattern in load_ae_obj_id__event_presentation always fires a
// background API call that writes to Dexie. That Dexie write triggers the
// liveQuery upstream, which updates the lq__event_presentation_obj prop,
// which re-runs this $effect — creating an infinite loop that crashes the tab.
// Guarding on last_loaded_id breaks the loop: the effect only makes an API
// call when we see a new presentation ID, not on every downstream prop update.
let last_loaded_id: string | null = null;
$effect(() => {
const id = lq__event_presentation_obj?.event_presentation_id;
if (id && id !== last_loaded_id) {
last_loaded_id = id;
events_func.load_ae_obj_id__event_presentation({
api_cfg: $ae_api,
event_presentation_id: id,
inc_file_li: true,
inc_presenter_li: true,
try_cache: true,
log_lvl: 0
});
}
});
let { lq__event_presentation_obj, session_type = '' }: Props = $props();
import { liveQuery } from 'dexie';
import { db_events } from '$lib/ae_events/db_events';
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
// Staggered Load: Trigger deep fetch only when the presentation ID changes.
// WHY: The SWR pattern in load_ae_obj_id__event_presentation always fires a
// background API call that writes to Dexie. That Dexie write triggers the
// liveQuery upstream, which updates the lq__event_presentation_obj prop,
// which re-runs this $effect — creating an infinite loop that crashes the tab.
// Guarding on last_loaded_id breaks the loop: the effect only makes an API
// call when we see a new presentation ID, not on every downstream prop update.
let last_loaded_id: string | null = null;
$effect(() => {
const id = lq__event_presentation_obj?.event_presentation_id;
if (id && id !== last_loaded_id) {
last_loaded_id = id;
events_func.load_ae_obj_id__event_presentation({
api_cfg: $ae_api,
event_presentation_id: id,
inc_file_li: true,
inc_presenter_li: true,
try_cache: true,
log_lvl: 0
});
}
});
// Event File (Directly linked to presentation)
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!lq__event_presentation_obj?.event_presentation_id) return [];
let results = await db_events.file
.where('for_id')
.equals(lq__event_presentation_obj.event_presentation_id)
.reverse()
.sortBy('created_on');
return results;
})
);
// Event File (Directly linked to presentation)
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!lq__event_presentation_obj?.event_presentation_id) return [];
let results = await db_events.file
.where('for_id')
.equals(lq__event_presentation_obj.event_presentation_id)
.reverse()
.sortBy('created_on');
return results;
})
);
</script>
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_presentation_file_list my-1">
<div
class="text-[10px] text-surface-600-400 uppercase font-bold tracking-wider opacity-70"
>
class="text-surface-600-400 text-[10px] font-bold tracking-wider uppercase opacity-70">
Presentation Files:
</div>
<ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li
class="flex flex-col md:flex-row flex-wrap gap-1 items-center justify-start"
class="flex flex-col flex-wrap items-center justify-start gap-1 md:flex-row"
class:hidden={!$events_loc.launcher
.show_content__hidden_files && event_file_obj.hide}
>
.show_content__hidden_files && event_file_obj.hide}>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
@@ -82,8 +80,7 @@
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
} />
</li>
{/each}
</ul>

View File

@@ -1,56 +1,56 @@
<script lang="ts">
interface Props {
// export let slct_event_presenter_id: string;
lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter.
session_type?: string;
}
interface Props {
// export let slct_event_presenter_id: string;
lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter.
session_type?: string;
}
let { lq__event_presenter_obj, session_type = 'oral' }: Props = $props();
let { lq__event_presenter_obj, session_type = 'oral' }: Props = $props();
import type { key_val } from '$lib/stores/ae_stores';
// import { ae_util } from '$lib/ae_utils/ae_utils';
// import { api } from '$lib/api';
import type { key_val } from '$lib/stores/ae_stores';
// import { ae_util } from '$lib/ae_utils/ae_utils';
// import { api } from '$lib/api';
import { liveQuery } from 'dexie';
import { liveQuery } from 'dexie';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
// import { events_func } from '$lib/ae_events/ae_events_functions';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
// import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Archive, User, Users } from '@lucide/svelte';
// export let slct_event_presentation_id: string;
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Archive, User, Users } from '@lucide/svelte';
// export let slct_event_presentation_id: string;
let ae_promises: key_val = {
get_li__event_file: null
};
let ae_promises: key_val = {
get_li__event_file: null
};
// Event File
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
let results = await db_events.file
// .where('event_session_id')
.where('for_id')
.equals(lq__event_presenter_obj?.event_presenter_id)
.reverse()
.sortBy('created_on');
return results;
})
);
// Event File
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
let results = await db_events.file
// .where('event_session_id')
.where('for_id')
.equals(lq__event_presenter_obj?.event_presenter_id)
.reverse()
.sortBy('created_on');
return results;
})
);
</script>
<strong>
@@ -66,7 +66,7 @@
</strong>
{#if !lq__event_presenter_obj?.file_count}
<p class="text-sm text-center text-gray-400">
<p class="text-center text-sm text-gray-400">
<!-- <span class="fas fa-exclamation"></span> -->
No files to show for this presenter at this time.
<!-- <span class="fas fa-exclamation"></span> -->
@@ -76,7 +76,7 @@
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_session_file_list">
<div>
<div class="text-xs text-surface-600-400">
<div class="text-surface-600-400 text-xs">
<strong>
<Archive size="1em" class="inline" />
Presenter Files:
@@ -89,10 +89,9 @@
<ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)}
<li
class="flex flex-col md:flex-row flex-wrap gap-1 items-center justify-start"
class="flex flex-col flex-wrap items-center justify-start gap-1 md:flex-row"
class:hidden={!$events_loc.launcher
.show_content__hidden_files && event_file_obj.hide}
>
.show_content__hidden_files && event_file_obj.hide}>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
@@ -110,8 +109,7 @@
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
} />
</li>
{/each}
</ul>

View File

@@ -1,56 +1,56 @@
<script lang="ts">
interface Props {
// export let slct_event_presenter_id: string;
lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter.
hide_name?: boolean;
}
interface Props {
// export let slct_event_presenter_id: string;
lq__event_presenter_obj: any; // This is not actually the LiveQuery object. This was pulled from the list of presenters for a presentation. With Svelte 5 this should not matter.
hide_name?: boolean;
}
let { lq__event_presenter_obj, hide_name = false }: Props = $props();
let { lq__event_presenter_obj, hide_name = false }: Props = $props();
import type { key_val } from '$lib/stores/ae_stores';
// import { ae_util } from '$lib/ae_utils/ae_utils';
// import { api } from '$lib/api';
import type { key_val } from '$lib/stores/ae_stores';
// import { ae_util } from '$lib/ae_utils/ae_utils';
// import { api } from '$lib/api';
import { liveQuery } from 'dexie';
import { liveQuery } from 'dexie';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
// import { events_func } from '$lib/ae_events/ae_events_functions';
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
// import { events_func } from '$lib/ae_events/ae_events_functions';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Archive, User, Users } from '@lucide/svelte';
// export let slct_event_presentation_id: string;
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Archive, User, Users } from '@lucide/svelte';
// export let slct_event_presentation_id: string;
let ae_promises: key_val = {
get_li__event_file: null
};
let ae_promises: key_val = {
get_li__event_file: null
};
// Event File
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
let results = await db_events.file
// .where('event_session_id')
.where('for_id')
.equals(lq__event_presenter_obj?.event_presenter_id)
.reverse()
.sortBy('created_on');
return results;
})
);
// Event File
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
let results = await db_events.file
// .where('event_session_id')
.where('for_id')
.equals(lq__event_presenter_obj?.event_presenter_id)
.reverse()
.sortBy('created_on');
return results;
})
);
</script>
<strong class:hidden={hide_name}>
@@ -83,10 +83,9 @@
<ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj, index (event_file_obj.event_file_id)}
<li
class="flex flex-col md:flex-row wrap gap items-center justify-center"
class="wrap gap flex flex-col items-center justify-center md:flex-row"
class:hidden={!$events_loc.launcher
.show_content__hidden_files && event_file_obj.hide}
>
.show_content__hidden_files && event_file_obj.hide}>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
@@ -102,8 +101,7 @@
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
} />
</li>
{/each}
</ul>

View File

@@ -1,160 +1,174 @@
<script lang="ts">
interface Props {
slct__event_session_id?: string | null;
log_lvl?: number;
}
interface Props {
slct__event_session_id?: string | null;
log_lvl?: number;
}
let {
slct__event_session_id = $bindable(null),
log_lvl = $bindable(1)
}: Props = $props();
let {
slct__event_session_id = $bindable(null),
log_lvl = $bindable(1)
}: Props = $props();
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view from './launcher_presenter_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
// WHY: Poster sessions get a dedicated card-grid view optimised for touch/PWA use.
import Launcher_session_view_posters from './launcher_session_view_posters.svelte';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view from './launcher_presenter_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
// WHY: Poster sessions get a dedicated card-grid view optimised for touch/PWA use.
import Launcher_session_view_posters from './launcher_session_view_posters.svelte';
import { liveQuery } from 'dexie';
// import { core_func } from '$lib/ae_core_functions';
// import { db_core } from "$lib/db_core";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { AlertTriangle, Archive, Barcode, Image, LoaderCircle, Monitor, User, Users } from '@lucide/svelte';
// Event Session (Main View Trigger)
// WHY: We use a simple derived observable. The template handles the $ prefix.
let lq__event_session_obj = $derived(
liveQuery(() => db_events.session.get(slct__event_session_id))
);
import { liveQuery } from 'dexie';
// import { core_func } from '$lib/ae_core_functions';
// import { db_core } from "$lib/db_core";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import {
AlertTriangle,
Archive,
Barcode,
Image,
LoaderCircle,
Monitor,
User,
Users
} from '@lucide/svelte';
// Event Session (Main View Trigger)
// WHY: We use a simple derived observable. The template handles the $ prefix.
let lq__event_session_obj = $derived(
liveQuery(() => db_events.session.get(slct__event_session_id))
);
// WHY: type_code drives poster vs. oral UI branching throughout this component.
// It was previously a prop that was never passed by the parent, so all poster
// code paths were silently dead. Deriving it here from the session object
// ensures it always reflects the current session.
let type_code = $derived($lq__event_session_obj?.type_code ?? '');
// WHY: type_code drives poster vs. oral UI branching throughout this component.
// It was previously a prop that was never passed by the parent, so all poster
// code paths were silently dead. Deriving it here from the session object
// ensures it always reflects the current session.
let type_code = $derived($lq__event_session_obj?.type_code ?? '');
// Event File (for a Session)
// WHY: Pure data retrieval. Side effects (updating global stores) are removed
// to prevent circular reactivity loops during rapid navigation.
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
// Event File (for a Session)
// WHY: Pure data retrieval. Side effects (updating global stores) are removed
// to prevent circular reactivity loops during rapid navigation.
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching files for session: ${slct__event_session_id}`);
}
if (log_lvl > 1) {
console.log(
`[LQ] Fetching files for session: ${slct__event_session_id}`
);
}
return await db_events.file
.where('for_id')
.equals(slct__event_session_id)
.reverse()
.sortBy('created_on');
})
);
return await db_events.file
.where('for_id')
.equals(slct__event_session_id)
.reverse()
.sortBy('created_on');
})
);
// Event Presentation
let lq__event_presentation_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
// Event Presentation
let lq__event_presentation_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching presentations for session: ${slct__event_session_id}`);
}
if (log_lvl > 1) {
console.log(
`[LQ] Fetching presentations for session: ${slct__event_session_id}`
);
}
let sort_by = 'start_datetime';
if (type_code == 'poster') {
sort_by = 'name';
}
return await db_events.presentation
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy(sort_by);
})
);
let sort_by = 'start_datetime';
if (type_code == 'poster') {
sort_by = 'name';
}
return await db_events.presentation
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy(sort_by);
})
);
// Event Presenter
let lq__event_presenter_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
// Event Presenter
let lq__event_presenter_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
if (log_lvl > 1) {
console.log(`[LQ] Fetching presenters for session: ${slct__event_session_id}`);
}
if (log_lvl > 1) {
console.log(
`[LQ] Fetching presenters for session: ${slct__event_session_id}`
);
}
return await db_events.presenter
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy('full_name');
})
);
return await db_events.presenter
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy('full_name');
})
);
// let show_modal_upload_files: boolean = false;
// let link_to_type: null|string = null;
// let link_to_id: null|string = null;
// let show_modal_upload_files: boolean = false;
// let link_to_type: null|string = null;
// let link_to_id: null|string = null;
let ae_promises: key_val = $state({});
// $events_slct.id_li__event_presenter = [];
// await tick();
// ae_promises[slct__event_session_id] = events_func.load_ae_obj_li__event_presenter({
// api_cfg: $ae_api,
// for_obj_type: 'event_session',
// for_obj_id: slct__event_session_id,
// // inc_file_li: false,
// params: {qry__enabled: 'enabled', qry__limit: 550},
// try_cache: true,
// log_lvl: 1,
// })
// .then(async function (load_results) {
// console.log(`load_results = `, load_results);
let ae_promises: key_val = $state({});
// $events_slct.id_li__event_presenter = [];
// await tick();
// ae_promises[slct__event_session_id] = events_func.load_ae_obj_li__event_presenter({
// api_cfg: $ae_api,
// for_obj_type: 'event_session',
// for_obj_id: slct__event_session_id,
// // inc_file_li: false,
// params: {qry__enabled: 'enabled', qry__limit: 550},
// try_cache: true,
// log_lvl: 1,
// })
// .then(async function (load_results) {
// console.log(`load_results = `, load_results);
// // let event_presenter_id_li = [];
// // let event_presenter_id_li = [];
// // let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery.
// // let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery.
// // for (let i = 0; i < load_results.length; i++) {
// // let event_presenter_obj = load_results[i];
// // let event_presenter_id = event_presenter_obj.event_presenter_id;
// // tmp_li.push(event_presenter_id);
// // }
// // event_presenter_id_li = tmp_li;
// // console.log(`event_presenter_id_li:`, event_presenter_id_li);
// // $events_slct.id_li__event_presenter = event_presenter_id_li;
// // for (let i = 0; i < load_results.length; i++) {
// // let event_presenter_obj = load_results[i];
// // let event_presenter_id = event_presenter_obj.event_presenter_id;
// // tmp_li.push(event_presenter_id);
// // }
// // event_presenter_id_li = tmp_li;
// // console.log(`event_presenter_id_li:`, event_presenter_id_li);
// // $events_slct.id_li__event_presenter = event_presenter_id_li;
// return load_results;
// });
// return load_results;
// });
</script>
<div
class="
event_launcher_session_view
grow h-full w-full
relative h-full w-full
grow
space-y-1
relative
"
>
">
<!-- <slot name="event_session_message">event session message</slot> -->
{#if $events_sess.launcher.loading__session_id_status}
<span class="absolute top-0 right-0 text-sm text-center text-gray-400">
<span class="absolute top-0 right-0 text-center text-sm text-gray-400">
<LoaderCircle size="1em" class="inline animate-spin" />
Loading session information...
</span>
@@ -169,157 +183,151 @@
<!-- WHY: Poster sessions use a dedicated touch-first card-grid layout. -->
<Launcher_session_view_posters {slct__event_session_id} {log_lvl} />
{:else}
<!--
<!--
Session header: flex-col keeps datetime and name on separate rows so
the header height is predictable regardless of session name length.
Long names (300+ chars) are clamped to 2 lines; short names never
collapse the header below that height. Zero layout shift between sessions.
-->
<header
class="event_session_about border-b-2 border-gray-400 dark:border-gray-600 flex flex-col gap-0.5 items-stretch"
>
<h3
class:hidden={!$lq__event_session_obj?.start_datetime ||
$events_loc.launcher.hide__session_datetimes}
class="event_session_datetimes text-sm text-center"
>
<button
type="button"
onclick={() => {
if (
$events_loc.launcher.time_format == 'time_12_short'
) {
// $events_loc.launcher.datetime_format = 'datetime_long';
$events_loc.launcher.time_format = 'time_short';
$events_loc.launcher.time_hours = 24;
} else {
$events_loc.launcher.time_format = 'time_12_short';
// $events_loc.launcher.datetime_format = 'datetime_12_long';
$events_loc.launcher.time_hours = 12;
}
}}
>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'week_long'
)}</strong
>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'date_long_month_day'
)}
</span>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
$events_loc.launcher.time_format
)}</strong
>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.end_datetime,
$events_loc.launcher.time_format
)}
</span>
</button>
</h3>
<header
class="event_session_about flex flex-col items-stretch gap-0.5 border-b-2 border-gray-400 dark:border-gray-600">
<h3
class:hidden={!$lq__event_session_obj?.start_datetime ||
$events_loc.launcher.hide__session_datetimes}
class="event_session_datetimes text-center text-sm">
<button
type="button"
onclick={() => {
if (
$events_loc.launcher.time_format ==
'time_12_short'
) {
// $events_loc.launcher.datetime_format = 'datetime_long';
$events_loc.launcher.time_format = 'time_short';
$events_loc.launcher.time_hours = 24;
} else {
$events_loc.launcher.time_format =
'time_12_short';
// $events_loc.launcher.datetime_format = 'datetime_12_long';
$events_loc.launcher.time_hours = 12;
}
}}>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'week_long'
)}</strong>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
'date_long_month_day'
)}
</span>
<strong
>{ae_util.iso_datetime_formatter(
$lq__event_session_obj.start_datetime,
$events_loc.launcher.time_format
)}</strong>
<span class="font-normal">
{ae_util.iso_datetime_formatter(
$lq__event_session_obj.end_datetime,
$events_loc.launcher.time_format
)}
</span>
</button>
</h3>
<span
class="w-full flex flex-row gap-2 items-center justify-between"
>
<!-- grow + line-clamp-2 = stable 2-line max; title provides full text for screen readers + hover -->
<h2
class="grow text-xl line-clamp-2 min-w-0"
title={`Name: ${$lq__event_session_obj.name}\nType: ${$lq__event_session_obj.type_code} \nCode: ${$lq__event_session_obj.code} \nID: ${$lq__event_session_obj.event_session_id} \nStart Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, 'week_long')} ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, $events_loc.launcher.time_format)} \nEnd Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.end_datetime, $events_loc.launcher.time_format)}`}
>
{$lq__event_session_obj?.name}
</h2>
{#if $lq__event_session_obj?.code}
<!-- shrink-0: code badge never gets squeezed by a long name -->
<span
class="shrink-0 text-base text-gray-500 font-normal p-1"
title="Session code {$lq__event_session_obj.code}"
>
<Barcode size="1em" class="inline" />
{$lq__event_session_obj?.code}
</span>
{/if}
</span>
</header>
<span
class="flex w-full flex-row items-center justify-between gap-2">
<!-- grow + line-clamp-2 = stable 2-line max; title provides full text for screen readers + hover -->
<h2
class="line-clamp-2 min-w-0 grow text-xl"
title={`Name: ${$lq__event_session_obj.name}\nType: ${$lq__event_session_obj.type_code} \nCode: ${$lq__event_session_obj.code} \nID: ${$lq__event_session_obj.event_session_id} \nStart Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, 'week_long')} ${ae_util.iso_datetime_formatter($lq__event_session_obj.start_datetime, $events_loc.launcher.time_format)} \nEnd Date/Time: ${ae_util.iso_datetime_formatter($lq__event_session_obj.end_datetime, $events_loc.launcher.time_format)}`}>
{$lq__event_session_obj?.name}
</h2>
{#if $lq__event_session_obj?.code}
<!-- shrink-0: code badge never gets squeezed by a long name -->
<span
class="shrink-0 p-1 text-base font-normal text-gray-500"
title="Session code {$lq__event_session_obj.code}">
<Barcode size="1em" class="inline" />
{$lq__event_session_obj?.code}
</span>
{/if}
</span>
</header>
<!-- <section class="event_session_description text-xs" class:d_none="{hide_description}">
<!-- <section class="event_session_description text-xs" class:d_none="{hide_description}">
{@html $lq__event_session_obj.description}
</section> -->
{#if $lq__event_session_obj?.file_count_all === 0}
<p class="text-2xl text-center text-red-500 font-bold">
<AlertTriangle size="1em" class="inline" />
Warning
<AlertTriangle size="1em" class="inline" />
<br />
No files available show for this session.
</p>
{/if}
{#if $lq__event_session_obj?.file_count_all === 0}
<p class="text-center text-2xl font-bold text-red-500">
<AlertTriangle size="1em" class="inline" />
Warning
<AlertTriangle size="1em" class="inline" />
<br />
No files available show for this session.
</p>
{/if}
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_session_file_list">
<div>
<div class="text-xs text-surface-600-400">
<strong>
<Archive size="1em" class="inline" />
Session Files:
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="event_session_file_list">
<div>
<div class="text-surface-600-400 text-xs">
<strong>
<Archive size="1em" class="inline" />
Session Files:
<span
class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode}
>
({$lq__event_file_obj_li?.length}&times;)
</span>
</strong>
</div>
<!-- {#if $ae_loc.trusted_access || $events_loc.launcher.trusted_access}
<span
class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode}>
({$lq__event_file_obj_li?.length}&times;)
</span>
</strong>
</div>
<!-- {#if $ae_loc.trusted_access || $events_loc.launcher.trusted_access}
<button type="button"
type="button" class="ae_btn btn_outline_warning btn_xs" title="Upload updated or additional files"
>
<span class="fas fa-upload"></span> Upload Session File(s)
</button>
{/if} -->
</div>
<ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li
class="flex flex-row flex-wrap gap-1 items-center justify-center"
class:hidden={!$events_loc.launcher
.show_content__hidden_files &&
event_file_obj.hide}
>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
hide_created_on={true}
show_bak_download={$ae_loc.trusted_access &&
$ae_loc.edit_mode}
session_type={type_code || 'oral'}
open_method={type_code == 'poster' ? 'modal' : null}
modal_title={$lq__event_session_obj?.name}
bind:modal__title={
$events_sess.launcher.modal__title
}
bind:modal__open_event_file_id={
$events_sess.launcher
.modal__open_event_file_id
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
</div>
<ul class="space-y-1">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li
class="flex flex-row flex-wrap items-center justify-center gap-1"
class:hidden={!$events_loc.launcher
.show_content__hidden_files &&
event_file_obj.hide}>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
hide_created_on={true}
show_bak_download={$ae_loc.trusted_access &&
$ae_loc.edit_mode}
session_type={type_code || 'oral'}
open_method={type_code == 'poster'
? 'modal'
: null}
modal_title={$lq__event_session_obj?.name}
bind:modal__title={
$events_sess.launcher.modal__title
}
bind:modal__open_event_file_id={
$events_sess.launcher
.modal__open_event_file_id
}
bind:modal__event_file_obj={
$events_sess.launcher
.modal__event_file_obj
} />
<!-- <Launcher_file_cont {event_file_obj} hide_created_on={false} show_bak_download={($ae_loc.trusted_access || $events_loc.launcher.trusted_access)} open_file_as={$lq__event_session_obj.type_code} poster_title={$lq__event_session_obj.title} /> -->
<!-- <Launcher_file_cont {event_file_obj} hide_created_on={false} show_bak_download={($ae_loc.trusted_access || $events_loc.launcher.trusted_access)} open_file_as={$lq__event_session_obj.type_code} poster_title={$lq__event_session_obj.title} /> -->
<!-- <a
<!-- <a
href="{$ae_api.base_url}/event/file/{event_file_obj.event_file_id}/download?filename={event_file_obj.filename}&key={$ae_api.account_id}"
class="btn btn-sm variant-soft-secondary m-0.5 *:hover:inline"
class:hidden={!ae_tmp.show__direct_download}
@@ -330,94 +338,93 @@
Download
</div>
</a> -->
</li>
{/each}
</ul>
</section>
{/if}
</li>
{/each}
</ul>
</section>
{/if}
<!-- <hr class="w-full border border-gray-200" /> -->
<!-- <hr class="w-full border border-gray-200" /> -->
<section class="event_presentation_list">
<!-- {$lq__event_session_obj?.event_presentation_li?.length ?? 'loading...?'} -->
<section class="event_presentation_list">
<!-- {$lq__event_session_obj?.event_presentation_li?.length ?? 'loading...?'} -->
{#if $lq__event_presentation_obj_li}
<div class="text-xs text-surface-600-400">
<strong>
{#if type_code == 'poster'}
<Image size="1em" class="inline" />
Posters:
{:else}
<Monitor size="1em" class="inline" />
Presentations:
{/if}
{#if $ae_loc.administrator_access && $ae_loc.edit_mode}
({$lq__event_presentation_obj_li?.length}&times;)
{/if}
</strong>
</div>
{#if $lq__event_presentation_obj_li}
<div class="text-surface-600-400 text-xs">
<strong>
{#if type_code == 'poster'}
<Image size="1em" class="inline" />
Posters:
{:else}
<Monitor size="1em" class="inline" />
Presentations:
{/if}
{#if $ae_loc.administrator_access && $ae_loc.edit_mode}
({$lq__event_presentation_obj_li?.length}&times;)
{/if}
</strong>
</div>
<!-- Maybe set max with? max-w-(--breakpoint-md) -->
<ul class="event_presentation_list max-w-full space-y-2">
{#each $lq__event_presentation_obj_li as event_presentation_obj (event_presentation_obj.event_presentation_id)}
<li
class="border-b-2 border-gray-300 dark:border-gray-700 my-1 py-1 text-center md:text-left"
>
<!-- The presentation information -->
<div
class="event_presentation_datetime_name flex flex-row justify-evenly gap-4"
>
<!-- <div class="event_presentation_datetime_name"> -->
{#if event_presentation_obj?.start_datetime}
<span class="event_presentation_datetime"
><strong
>{ae_util.iso_datetime_formatter(
event_presentation_obj?.start_datetime,
'time_12_short_no_leading'
)}</strong
></span
>
{/if}
<!-- Maybe set max with? max-w-(--breakpoint-md) -->
<ul class="event_presentation_list max-w-full space-y-2">
{#each $lq__event_presentation_obj_li as event_presentation_obj (event_presentation_obj.event_presentation_id)}
<li
class="my-1 border-b-2 border-gray-300 py-1 text-center md:text-left dark:border-gray-700">
<!-- The presentation information -->
<div
class="event_presentation_datetime_name flex flex-row justify-evenly gap-4">
<!-- <div class="event_presentation_datetime_name"> -->
{#if event_presentation_obj?.start_datetime}
<span
class="event_presentation_datetime"
><strong
>{ae_util.iso_datetime_formatter(
event_presentation_obj?.start_datetime,
'time_12_short_no_leading'
)}</strong
></span>
{/if}
<span class="event_presentation_name grow"
>{event_presentation_obj?.name}</span
>
<!-- </div> -->
<span class="event_presentation_name grow"
>{event_presentation_obj?.name}</span>
<!-- </div> -->
<!-- Yes, this is kind of inefficient, but it works for now. -->
{#if $lq__event_presenter_obj_li && type_code == 'poster'}
{#each $lq__event_presenter_obj_li as event_presenter_obj, index (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<span
class="event_presentation_single_presenter italic text-sm text-gray-500"
>
{#if $lq__event_presenter_obj_li[index]?.given_name && $lq__event_presenter_obj_li[index]?.given_name != 'Group'}
<User size="0.85em" class="inline" />
{$lq__event_presenter_obj_li[
index
]?.full_name}
{:else if $lq__event_presenter_obj_li[index]?.given_name == 'Group'}
<Users size="0.85em" class="inline" />
{$lq__event_presenter_obj_li[
index
]?.affiliations}
{:else}
--not set--
{/if}
</span>
{/if}
{/each}
{/if}
</div>
<!-- Yes, this is kind of inefficient, but it works for now. -->
{#if $lq__event_presenter_obj_li && type_code == 'poster'}
{#each $lq__event_presenter_obj_li as event_presenter_obj, index (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<span
class="event_presentation_single_presenter text-sm text-gray-500 italic">
{#if $lq__event_presenter_obj_li[index]?.given_name && $lq__event_presenter_obj_li[index]?.given_name != 'Group'}
<User
size="0.85em"
class="inline" />
{$lq__event_presenter_obj_li[
index
]?.full_name}
{:else if $lq__event_presenter_obj_li[index]?.given_name == 'Group'}
<Users
size="0.85em"
class="inline" />
{$lq__event_presenter_obj_li[
index
]?.affiliations}
{:else}
--not set--
{/if}
</span>
{/if}
{/each}
{/if}
</div>
<!-- Presentation-level files -->
<Launcher_presentation_view
lq__event_presentation_obj={event_presentation_obj}
session_type={type_code}
/>
<!-- Presentation-level files -->
<Launcher_presentation_view
lq__event_presentation_obj={event_presentation_obj}
session_type={type_code} />
<!-- The presenter list -->
<!-- WHY: In poster mode, presenter names are already shown inline
<!-- The presenter list -->
<!-- WHY: In poster mode, presenter names are already shown inline
in the presentation header above, so hide_name=true.
We still render Launcher_presenter_view_posters here because
some events store files at the PRESENTER level (for_id=event_presenter_id)
@@ -425,43 +432,41 @@
The component renders nothing if there are no presenter-level files,
so this has no visual cost for events that use presentation-level files. -->
{#if $lq__event_presenter_obj_li && $lq__event_presenter_obj_li.length}
<ul class="event_presentation_presenter_list">
{#each $lq__event_presenter_obj_li as event_presenter_obj (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<li
class="
border border-transparent
{#if $lq__event_presenter_obj_li && $lq__event_presenter_obj_li.length}
<ul
class="event_presentation_presenter_list">
{#each $lq__event_presenter_obj_li as event_presenter_obj (event_presenter_obj.event_presenter_id)}
{#if event_presenter_obj.event_presentation_id == event_presentation_obj.event_presentation_id}
<li
class="
hover:bg-surface-100-900 hover:border-surface-400-600
rounded-lg
hover:bg-surface-100-900
hover:border-surface-400-600
border
border-transparent
p-1
transition-all
"
>
{#if type_code == 'poster'}
<Launcher_presenter_view_posters
lq__event_presenter_obj={event_presenter_obj}
hide_name={true}
/>
{:else}
<Launcher_presenter_view
lq__event_presenter_obj={event_presenter_obj}
session_type={type_code}
/>
{/if}
</li>
{/if}
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
{:else}
<p>No presentations available to display.</p>
{/if}
</section>
">
{#if type_code == 'poster'}
<Launcher_presenter_view_posters
lq__event_presenter_obj={event_presenter_obj}
hide_name={true} />
{:else}
<Launcher_presenter_view
lq__event_presenter_obj={event_presenter_obj}
session_type={type_code} />
{/if}
</li>
{/if}
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
{:else}
<p>No presentations available to display.</p>
{/if}
</section>
{/if}<!-- end type_code !== 'poster' -->
{:else}
<LoaderCircle size="1em" class="inline animate-spin" />

View File

@@ -1,78 +1,78 @@
<script lang="ts">
/**
* launcher_session_view_posters.svelte
*
* WHY: Digital Poster sessions need a dedicated card-grid layout optimised for
* touch-first PWA use on tablets and phones. Keeping this separate from the oral
* session view avoids cluttering that template with a growing set of poster-specific
* branches and lets each view own its own layout concerns cleanly.
*
* The menu will typically be hidden and the page may be in iframe mode
* ($ae_loc.iframe == true), so this view fills the full available width.
*
* Deployment context: operator tablet / phone PWA, also works on desktop.
*/
interface Props {
slct__event_session_id?: string | null;
log_lvl?: number;
}
/**
* launcher_session_view_posters.svelte
*
* WHY: Digital Poster sessions need a dedicated card-grid layout optimised for
* touch-first PWA use on tablets and phones. Keeping this separate from the oral
* session view avoids cluttering that template with a growing set of poster-specific
* branches and lets each view own its own layout concerns cleanly.
*
* The menu will typically be hidden and the page may be in iframe mode
* ($ae_loc.iframe == true), so this view fills the full available width.
*
* Deployment context: operator tablet / phone PWA, also works on desktop.
*/
interface Props {
slct__event_session_id?: string | null;
log_lvl?: number;
}
let {
slct__event_session_id = $bindable(null),
log_lvl = $bindable(1)
}: Props = $props();
let {
slct__event_session_id = $bindable(null),
log_lvl = $bindable(1)
}: Props = $props();
import { liveQuery } from 'dexie';
import { ae_loc } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import { liveQuery } from 'dexie';
import { ae_loc } from '$lib/stores/ae_stores';
import { db_events } from '$lib/ae_events/db_events';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Image, Images, LoaderCircle, User, Users } from '@lucide/svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
import Event_launcher_file_cont from './launcher_file_cont.svelte';
import { Image, Images, LoaderCircle, User, Users } from '@lucide/svelte';
import Launcher_presentation_view from './launcher_presentation_view.svelte';
import Launcher_presenter_view_posters from './launcher_presenter_view_posters.svelte';
// Session object
let lq__event_session_obj = $derived(
liveQuery(() => db_events.session.get(slct__event_session_id))
);
// Session object
let lq__event_session_obj = $derived(
liveQuery(() => db_events.session.get(slct__event_session_id))
);
// Session-level files (rare — program PDFs, flyers, etc.)
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
return await db_events.file
.where('for_id')
.equals(slct__event_session_id)
.reverse()
.sortBy('created_on');
})
);
// Session-level files (rare — program PDFs, flyers, etc.)
let lq__event_file_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
return await db_events.file
.where('for_id')
.equals(slct__event_session_id)
.reverse()
.sortBy('created_on');
})
);
// Presentations sorted alphabetically — typical for poster sessions
let lq__event_presentation_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
return await db_events.presentation
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy('name');
})
);
// Presentations sorted alphabetically — typical for poster sessions
let lq__event_presentation_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
return await db_events.presentation
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy('name');
})
);
// All presenters for this session; filtered per card in the template
let lq__event_presenter_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
return await db_events.presenter
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy('full_name');
})
);
// All presenters for this session; filtered per card in the template
let lq__event_presenter_obj_li = $derived(
liveQuery(async () => {
if (!slct__event_session_id) return [];
return await db_events.presenter
.where('event_session_id')
.equals(slct__event_session_id)
.sortBy('full_name');
})
);
// Poster count for the session header badge
let poster_count = $derived($lq__event_presentation_obj_li?.length ?? 0);
// Poster count for the session header badge
let poster_count = $derived($lq__event_presentation_obj_li?.length ?? 0);
</script>
<!--
@@ -80,43 +80,42 @@
The outer div mirrors the grow/h-full/w-full contract expected by
+layout.svelte so this slots in as a drop-in replacement for the oral view.
-->
<div class="poster_session_view flex flex-col gap-0 w-full h-full overflow-hidden">
<div
class="poster_session_view flex h-full w-full flex-col gap-0 overflow-hidden">
{#if $events_sess.launcher?.loading__session_id_status}
<span class="absolute top-0 right-0 text-sm text-gray-400 flex items-center gap-1 p-1 z-10">
<span
class="absolute top-0 right-0 z-10 flex items-center gap-1 p-1 text-sm text-gray-400">
<LoaderCircle size="1em" class="animate-spin" />
Loading...
</span>
{/if}
{#if $lq__event_session_obj && $lq__event_session_obj.event_session_id}
<!-- ── Compact session identity strip ──────────────────────────────── -->
<header
class="
poster_session_header
flex flex-row gap-2 items-center justify-between
px-2 py-1.5
border-b-2 border-surface-300 dark:border-surface-600
bg-surface-100/60 dark:bg-surface-800/60
shrink-0
"
>
border-surface-300 dark:border-surface-600 bg-surface-100/60 dark:bg-surface-800/60 flex
shrink-0 flex-row
items-center justify-between gap-2
border-b-2 px-2
py-1.5
">
<h2
class="text-base font-bold line-clamp-1 grow min-w-0"
title={$lq__event_session_obj.name}
>
<Images size="1em" class="inline mr-1.5 text-primary-500 opacity-70" />
class="line-clamp-1 min-w-0 grow text-base font-bold"
title={$lq__event_session_obj.name}>
<Images
size="1em"
class="text-primary-500 mr-1.5 inline opacity-70" />
{$lq__event_session_obj.name}
</h2>
<span class="flex flex-row gap-1.5 items-center shrink-0">
<span class="flex shrink-0 flex-row items-center gap-1.5">
<!-- Poster count badge -->
{#if poster_count > 0}
<span
class="text-xs font-mono font-semibold text-surface-500 bg-surface-200 dark:bg-surface-700 px-2 py-0.5 rounded-full"
title="Number of posters in this session"
>
class="text-surface-500 bg-surface-200 dark:bg-surface-700 rounded-full px-2 py-0.5 font-mono text-xs font-semibold"
title="Number of posters in this session">
{poster_count}&times;
</span>
{/if}
@@ -124,9 +123,8 @@
<!-- Session code -->
{#if $lq__event_session_obj.code}
<span
class="text-xs font-mono font-bold text-surface-400 bg-surface-100 dark:bg-surface-800 px-2 py-0.5 rounded border border-surface-300 dark:border-surface-600"
title="Session code: {$lq__event_session_obj.code}"
>
class="text-surface-400 bg-surface-100 dark:bg-surface-800 border-surface-300 dark:border-surface-600 rounded border px-2 py-0.5 font-mono text-xs font-bold"
title="Session code: {$lq__event_session_obj.code}">
{$lq__event_session_obj.code}
</span>
{/if}
@@ -135,16 +133,18 @@
<!-- ── Session-level files (rarely present — program, schedule, etc.) ── -->
{#if $lq__event_file_obj_li && $lq__event_file_obj_li.length}
<section class="session_resources_strip px-2 pb-2 pt-1 border-b border-surface-200 dark:border-surface-700 shrink-0">
<p class="text-[10px] text-surface-500 uppercase font-bold tracking-wider mb-1 opacity-60">
<section
class="session_resources_strip border-surface-200 dark:border-surface-700 shrink-0 border-b px-2 pt-1 pb-2">
<p
class="text-surface-500 mb-1 text-[10px] font-bold tracking-wider uppercase opacity-60">
Session Resources:
</p>
<ul class="flex flex-row flex-wrap gap-2">
{#each $lq__event_file_obj_li as event_file_obj (event_file_obj.event_file_id)}
<li
class:hidden={!$events_loc.launcher.show_content__hidden_files &&
event_file_obj.hide}
>
class:hidden={!$events_loc.launcher
.show_content__hidden_files &&
event_file_obj.hide}>
<Event_launcher_file_cont
event_file_id={event_file_obj.event_file_id}
{event_file_obj}
@@ -163,8 +163,7 @@
}
bind:modal__event_file_obj={
$events_sess.launcher.modal__event_file_obj
}
/>
} />
</li>
{/each}
</ul>
@@ -174,18 +173,20 @@
<!-- ── Poster card grid ────────────────────────────────────────────── -->
{#if $lq__event_presentation_obj_li === undefined}
<!-- Still resolving from Dexie -->
<div class="flex items-center justify-center gap-2 p-10 opacity-40 grow">
<div
class="flex grow items-center justify-center gap-2 p-10 opacity-40">
<LoaderCircle size="2em" class="animate-spin" />
<span>Loading posters…</span>
</div>
{:else if $lq__event_presentation_obj_li.length === 0}
<!-- Loaded but empty -->
<div class="flex flex-col items-center justify-center gap-3 p-12 opacity-40 text-center grow">
<div
class="flex grow flex-col items-center justify-center gap-3 p-12 text-center opacity-40">
<Image size="3em" />
<p class="text-lg font-medium">No posters in this session yet.</p>
<p class="text-lg font-medium">
No posters in this session yet.
</p>
</div>
{:else}
<!--
Grid: 1 col on phone, 2 on tablet (sm), 3 on large desktop (xl).
@@ -197,15 +198,14 @@
class="
poster_card_grid
grid
grow
grid-cols-1
gap-3
overflow-y-auto
p-3
sm:grid-cols-2
xl:grid-cols-3
gap-3
p-3
overflow-y-auto
grow
"
>
">
{#each $lq__event_presentation_obj_li as presentation, i (presentation.event_presentation_id)}
{@const presenters_for_this = (
$lq__event_presenter_obj_li ?? []
@@ -223,18 +223,17 @@
<li
class="
poster_card
relative flex flex-col gap-2
rounded-xl
border border-surface-200 dark:border-surface-700
bg-white dark:bg-surface-900
hover:border-primary-400 dark:hover:border-primary-500
active:scale-[0.98] active:shadow-sm
transition-all duration-150
shadow-sm hover:shadow-md
p-3
min-h-40
"
>
border-surface-200 dark:border-surface-700 dark:bg-surface-900 hover:border-primary-400
dark:hover:border-primary-500
relative flex min-h-40
flex-col gap-2
rounded-xl border
bg-white p-3
shadow-sm transition-all
duration-150 hover:shadow-md
active:scale-[0.98]
active:shadow-sm
">
<!--
Top-right badge: prefer the presentation code (e.g. "P-042")
as it matches physical poster board numbers; fall back to
@@ -242,18 +241,17 @@
-->
<span
class="
absolute top-2 right-2
text-xs font-mono font-bold leading-tight
text-primary-600 dark:text-primary-400
bg-primary-50 dark:bg-primary-950/60
border border-primary-200 dark:border-primary-800
px-2 py-0.5
rounded-full
text-primary-600 dark:text-primary-400 bg-primary-50
dark:bg-primary-950/60 border-primary-200 dark:border-primary-800 absolute
top-2 right-2
rounded-full border
px-2 py-0.5 font-mono
text-xs leading-tight
font-bold
"
title="{presentation.code
title={presentation.code
? 'Poster code: ' + presentation.code
: 'Poster #' + (i + 1)}"
>
: 'Poster #' + (i + 1)}>
{presentation.code || '#' + (i + 1)}
</span>
@@ -266,15 +264,14 @@
<h3
class="
poster_title
text-base md:text-lg
font-bold leading-snug
line-clamp-3
text-surface-950 dark:text-surface-50
line-clamp-3 grow
pr-14
grow
text-base leading-snug
font-bold
md:text-lg
"
title={presentation.name}
>
title={presentation.name}>
{presentation.name}
</h3>
@@ -286,37 +283,38 @@
"Group" presenter whose full name is stored in affiliations.
-->
{#if presenters_for_this.length}
<div class="presenter_info space-y-0.5 shrink-0">
<div class="presenter_info shrink-0 space-y-0.5">
{#each presenters_for_this as presenter (presenter.event_presenter_id)}
<p
class="
flex flex-row flex-wrap items-baseline gap-x-1.5
text-sm text-surface-500 dark:text-surface-400
text-surface-500 dark:text-surface-400 flex flex-row flex-wrap
items-baseline gap-x-1.5 text-sm
leading-snug
"
>
">
{#if presenter.given_name && presenter.given_name !== 'Group'}
<User size="0.7em" class="opacity-50 shrink-0 mt-px" />
<User
size="0.7em"
class="mt-px shrink-0 opacity-50" />
<span
class="font-medium text-surface-700 dark:text-surface-300"
>{presenter.full_name}</span
>
class="text-surface-700 dark:text-surface-300 font-medium"
>{presenter.full_name}</span>
{#if presenter.affiliations}
<span
class="italic text-xs opacity-70 line-clamp-1 min-w-0"
title={presenter.affiliations}
>
class="line-clamp-1 min-w-0 text-xs italic opacity-70"
title={presenter.affiliations}>
{presenter.affiliations}
</span>
{/if}
{:else if presenter.given_name === 'Group'}
<Users size="0.7em" class="opacity-50 shrink-0 mt-px" />
<Users
size="0.7em"
class="mt-px shrink-0 opacity-50" />
<span
class="font-medium text-surface-700 dark:text-surface-300"
>{presenter.affiliations}</span
>
class="text-surface-700 dark:text-surface-300 font-medium"
>{presenter.affiliations}</span>
{:else}
<span class="opacity-40 text-xs"></span>
<span class="text-xs opacity-40"
></span>
{/if}
</p>
{/each}
@@ -329,12 +327,12 @@
presenter level, or both — render both sub-components so
neither source is missed.
-->
<div class="poster_actions flex flex-col gap-1 mt-auto pt-1 shrink-0">
<div
class="poster_actions mt-auto flex shrink-0 flex-col gap-1 pt-1">
<!-- Presentation-level files (the most common attachment point) -->
<Launcher_presentation_view
lq__event_presentation_obj={presentation}
session_type="poster"
/>
session_type="poster" />
<!--
Presenter-level files (some events attach the PDF here instead).
@@ -344,18 +342,16 @@
{#each presenters_for_this as presenter (presenter.event_presenter_id)}
<Launcher_presenter_view_posters
lq__event_presenter_obj={presenter}
hide_name={true}
/>
hide_name={true} />
{/each}
</div>
</li>
{/each}
</ul>
{/if}
{:else}
<!-- No session selected or still loading -->
<div class="flex items-center justify-center gap-2 p-8 opacity-40 grow">
<div class="flex grow items-center justify-center gap-2 p-8 opacity-40">
<LoaderCircle size="1em" class="animate-spin" />
<span>No session selected</span>
</div>

View File

@@ -1,39 +1,40 @@
<script lang="ts">
/**
* menu_launcher_controls.svelte — Aether Launcher: Bottom Control Bar
*
* PURPOSE:
* Accessibility and visibility controls for the Launcher sidebar. Placed at the
* bottom of launcher_menu.svelte so presenters and operators can adjust display
* settings without needing access to the global AE System Menu (which may be
* unavailable in kiosk, locked, or podium setups).
*
* SECTIONS:
* 1. Visibility toggles (edit mode only) — show/hide draft files and hidden sessions
* 2. Accessibility controls (always visible) — font size cycler and light/dark toggle
*
* WHY ALWAYS-VISIBLE ACCESSIBILITY CONTROLS:
* Projector-connected screens, tablets, and phones at conference venues vary wildly
* in lighting. Operators and presenters need quick one-tap access to font size and
* theme mode without hunting through the system menu or requiring admin access.
*/
/**
* menu_launcher_controls.svelte — Aether Launcher: Bottom Control Bar
*
* PURPOSE:
* Accessibility and visibility controls for the Launcher sidebar. Placed at the
* bottom of launcher_menu.svelte so presenters and operators can adjust display
* settings without needing access to the global AE System Menu (which may be
* unavailable in kiosk, locked, or podium setups).
*
* SECTIONS:
* 1. Visibility toggles (edit mode only) — show/hide draft files and hidden sessions
* 2. Accessibility controls (always visible) — font size cycler and light/dark toggle
*
* WHY ALWAYS-VISIBLE ACCESSIBILITY CONTROLS:
* Projector-connected screens, tablets, and phones at conference venues vary wildly
* in lighting. Operators and presenters need quick one-tap access to font size and
* theme mode without hunting through the system menu or requiring admin access.
*/
import { Moon, Sun, Eye, EyeOff } from '@lucide/svelte';
import { Moon, Sun, Eye, EyeOff } from '@lucide/svelte';
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
interface Props {
log_lvl?: number;
}
interface Props {
log_lvl?: number;
}
let { log_lvl = $bindable(0) }: Props = $props();
let { log_lvl = $bindable(0) }: Props = $props();
</script>
<div class="w-full max-w-full flex flex-col gap-1 items-center justify-center">
<div class="flex w-full max-w-full flex-col items-center justify-center gap-1">
<!-- ── Visibility toggles — edit mode only ── -->
{#if $ae_loc.edit_mode}
<div class="w-full max-w-full flex flex-row gap-1 items-center justify-center">
<div
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
<button
type="button"
onclick={() => {
@@ -46,13 +47,12 @@
}
}}
class="
btn btn-sm text-xs
w-1/2 max-w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 w-1/2
max-w-1/2 text-xs
transition-all
"
title="Toggle visibility of hidden and draft files in the launcher file list."
>
title="Toggle visibility of hidden and draft files in the launcher file list.">
{#if $events_loc.launcher.show_content__hidden_files}
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
Hide Files
@@ -69,13 +69,12 @@
!$events_loc.launcher.show_content__hidden_sessions;
}}
class="
btn btn-sm text-xs
w-1/2 max-w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 w-1/2
max-w-1/2 text-xs
transition-all
"
title="Toggle visibility of hidden and cancelled sessions in the launcher session list."
>
title="Toggle visibility of hidden and cancelled sessions in the launcher session list.">
{#if $events_loc.launcher.show_content__hidden_sessions}
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
Hide Sessions
@@ -88,7 +87,8 @@
{/if}
<!-- ── Accessibility controls — always visible ── -->
<div class="w-full max-w-full flex flex-row gap-1 items-center justify-center">
<div
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
<!-- Font size cycler: default → larger → smaller → default -->
<button
type="button"
@@ -103,22 +103,28 @@
}
}}
class="
btn btn-sm text-xs
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 group
w-1/2 max-w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500
transition-all group
text-xs transition-all
"
title="Cycle font size (default → larger → smaller). Current: {$ae_loc.font_size_mode ?? 'default'}"
>
title="Cycle font size (default → larger → smaller). Current: {$ae_loc.font_size_mode ??
'default'}">
{#if !$ae_loc.font_size_mode || $ae_loc.font_size_mode === 'default'}
<span class="font-bold text-sm font-mono leading-none m-1">A</span>
<span class="hidden group-hover:inline-block text-xs">Font: Normal</span>
<span class="m-1 font-mono text-sm leading-none font-bold"
>A</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Normal</span>
{:else if $ae_loc.font_size_mode === 'larger'}
<span class="font-bold text-base font-mono leading-none m-1">A+</span>
<span class="hidden group-hover:inline-block text-xs">Font: Larger</span>
<span class="m-1 font-mono text-base leading-none font-bold"
>A+</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Larger</span>
{:else}
<span class="font-bold text-xs font-mono leading-none m-1">A</span>
<span class="hidden group-hover:inline-block text-xs">Font: Smaller</span>
<span class="m-1 font-mono text-xs leading-none font-bold"
>A</span>
<span class="hidden text-xs group-hover:inline-block"
>Font: Smaller</span>
{/if}
</button>
@@ -126,16 +132,17 @@
<button
type="button"
onclick={() => {
$ae_loc.theme_mode = $ae_loc.theme_mode === 'dark' ? 'light' : 'dark';
$ae_loc.theme_mode =
$ae_loc.theme_mode === 'dark' ? 'light' : 'dark';
}}
class="
btn btn-sm text-xs
btn btn-sm preset-tonal-tertiary
hover:preset-filled-tertiary-500 group
w-1/2 max-w-1/2
preset-tonal-tertiary hover:preset-filled-tertiary-500
transition-all group
text-xs transition-all
"
title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ?? 'light'}"
>
title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ??
'light'}">
{#if $ae_loc.theme_mode === 'dark'}
<Moon class="m-1 inline-block" size="1em" />
<span class="hidden group-hover:inline-block">Dark Mode</span>

View File

@@ -1,170 +1,171 @@
<script lang="ts">
/**
* menu_location_list.svelte — Aether Launcher: Room / Location Selector
*
* PURPOSE:
* Provides a dropdown for operators to switch between venue rooms (event_location
* objects). Selecting a room triggers a session list reload for that room and
* navigates the URL to /launcher/{location_id} so the back button works correctly.
*
* VISIBILITY:
* Rendered only when $ae_loc.edit_mode is true (operator-level access). Regular
* attendees and kiosk displays do not see the room selector — they are taken
* directly to a URL-encoded room and session without UI controls.
*
* DATA FLOW:
* lq__event_location_obj_li (Dexie liveQuery, passed from launcher_menu.svelte)
* → rendered as <select> options
* → onchange writes to $events_slct.event_location_id and $events_loc.launcher.slct
* → calls handle_load_ae_obj_li__event_session() to fetch sessions for the new room
* → navigates to /launcher/{location_id} via goto()
*
* NOTE — slct_event_location_id binding:
* Declared $bindable() so the parent can pass the initial selected value via bind:.
* The onchange handler also writes directly to the canonical stores
* ($events_slct.event_location_id) to keep other parts of the launcher in sync.
*/
interface Props {
loading__session_li_status?: null | boolean | string;
lq__event_location_obj_li: any;
slct_event_location_id?: string | null;
/**
* menu_location_list.svelte — Aether Launcher: Room / Location Selector
*
* PURPOSE:
* Provides a dropdown for operators to switch between venue rooms (event_location
* objects). Selecting a room triggers a session list reload for that room and
* navigates the URL to /launcher/{location_id} so the back button works correctly.
*
* VISIBILITY:
* Rendered only when $ae_loc.edit_mode is true (operator-level access). Regular
* attendees and kiosk displays do not see the room selector — they are taken
* directly to a URL-encoded room and session without UI controls.
*
* DATA FLOW:
* lq__event_location_obj_li (Dexie liveQuery, passed from launcher_menu.svelte)
* → rendered as <select> options
* → onchange writes to $events_slct.event_location_id and $events_loc.launcher.slct
* → calls handle_load_ae_obj_li__event_session() to fetch sessions for the new room
* → navigates to /launcher/{location_id} via goto()
*
* NOTE — slct_event_location_id binding:
* Declared $bindable() so the parent can pass the initial selected value via bind:.
* The onchange handler also writes directly to the canonical stores
* ($events_slct.event_location_id) to keep other parts of the launcher in sync.
*/
interface Props {
loading__session_li_status?: null | boolean | string;
lq__event_location_obj_li: any;
slct_event_location_id?: string | null;
trigger_reload__event_session_obj_li?: boolean;
trigger_reload__event_location_obj_li?: boolean;
trigger_reload__event_session_obj_li?: boolean;
trigger_reload__event_location_obj_li?: boolean;
log_lvl?: number;
log_lvl?: number;
}
let {
loading__session_li_status = $bindable(null),
lq__event_location_obj_li,
slct_event_location_id = $bindable(null),
trigger_reload__event_session_obj_li = $bindable(false),
trigger_reload__event_location_obj_li = $bindable(false),
log_lvl = $bindable(0)
}: Props = $props();
// *** Import Svelte specific
import { page } from '$app/state';
import { goto } from '$app/navigation';
// import { liveQuery } from "dexie";
// import { tick } from 'svelte';
// *** Import other supporting libraries
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
// import { db_events } from "$lib/db_events";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { Check, LoaderCircle } from '@lucide/svelte';
// export let slct_event_session_id: any;
// *** Functions and Logic
let ae_promises: key_val = $state({
slct_event_location_id: null
});
// let hover_timer_wait = 1000;
// let hover_timer: any = $state(null);
function handle_load_ae_obj_li__event_session(event_location_id: string) {
if (log_lvl) {
console.log(
`handle_load_ae_obj_li__event_session: event_location_id = ${event_location_id}`
);
}
if (!event_location_id) {
console.warn(
`handle_load_ae_obj_li__event_session: No event_location_id provided.`
);
return;
}
let {
loading__session_li_status = $bindable(null),
lq__event_location_obj_li,
slct_event_location_id = $bindable(null),
loading__session_li_status = true;
trigger_reload__event_session_obj_li = $bindable(false),
trigger_reload__event_location_obj_li = $bindable(false),
ae_promises[event_location_id] = events_func
.load_ae_obj_li__event_session({
api_cfg: $ae_api,
for_obj_type: 'event_location',
for_obj_id: event_location_id,
inc_file_li: true, // Only include files directly under the session?
inc_all_file_li: false, // Also include files under presentations and presenters as well?
inc_presentation_li: true,
inc_presenter_li: true,
enabled: $events_loc.launcher.show_content__enabled_sessions
? 'all'
: 'enabled',
hidden: $events_loc.launcher.show_content__hidden_sessions
? 'all'
: 'not_hidden',
limit: 49,
try_cache: true,
log_lvl: 1
})
.then(async function (load_results) {
console.log(`load_results = `, load_results);
log_lvl = $bindable(0)
}: Props = $props();
let event_session_id_li = [];
// *** Import Svelte specific
import { page } from '$app/state';
import { goto } from '$app/navigation';
// import { liveQuery } from "dexie";
// import { tick } from 'svelte';
let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery.
// *** Import other supporting libraries
for (let i = 0; i < load_results.length; i++) {
let event_session_obj = load_results[i];
let event_session_id = event_session_obj.event_session_id;
tmp_li.push(event_session_id);
}
event_session_id_li = tmp_li;
console.log(`event_session_id_li:`, event_session_id_li);
// $events_slct.id_li__event_session = event_session_id_li;
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
// import { db_events } from "$lib/db_events";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { Check, LoaderCircle } from '@lucide/svelte';
// export let slct_event_session_id: any;
loading__session_li_status = false;
// *** Functions and Logic
return load_results;
});
let ae_promises: key_val = $state({
slct_event_location_id: null
});
// let hover_timer_wait = 1000;
// let hover_timer: any = $state(null);
function handle_load_ae_obj_li__event_session(event_location_id: string) {
if (log_lvl) {
console.log(
`handle_load_ae_obj_li__event_session: event_location_id = ${event_location_id}`
);
}
if (!event_location_id) {
console.warn(
`handle_load_ae_obj_li__event_session: No event_location_id provided.`
);
return;
}
loading__session_li_status = true;
ae_promises[event_location_id] = events_func
.load_ae_obj_li__event_session({
api_cfg: $ae_api,
for_obj_type: 'event_location',
for_obj_id: event_location_id,
inc_file_li: true, // Only include files directly under the session?
inc_all_file_li: false, // Also include files under presentations and presenters as well?
inc_presentation_li: true,
inc_presenter_li: true,
enabled: $events_loc.launcher.show_content__enabled_sessions
? 'all'
: 'enabled',
hidden: $events_loc.launcher.show_content__hidden_sessions
? 'all'
: 'not_hidden',
limit: 49,
try_cache: true,
log_lvl: 1
})
.then(async function (load_results) {
console.log(`load_results = `, load_results);
let event_session_id_li = [];
let tmp_li = []; // This is to prevent the array from constantly updating and triggering the liveQuery.
for (let i = 0; i < load_results.length; i++) {
let event_session_obj = load_results[i];
let event_session_id = event_session_obj.event_session_id;
tmp_li.push(event_session_id);
}
event_session_id_li = tmp_li;
console.log(`event_session_id_li:`, event_session_id_li);
// $events_slct.id_li__event_session = event_session_id_li;
loading__session_li_status = false;
return load_results;
});
return ae_promises[event_location_id];
}
return ae_promises[event_location_id];
}
</script>
<!-- text-neutral-800/80 -->
<div
class="
w-full max-w-full
flex flex-col md:flex-row flex-wrap gap-1 items-center justify-center
"
>
flex w-full
max-w-full flex-col flex-wrap items-center justify-center gap-1 md:flex-row
">
{#if $lq__event_location_obj_li && $lq__event_location_obj_li.length > 0}
<div class="text-xs text-surface-600-400">
<div class="text-surface-600-400 text-xs">
<strong>
Location:
<span
class:hidden={!$ae_loc.trusted_access || !$ae_loc.edit_mode}
>
class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode}>
({$lq__event_location_obj_li?.length}&times;)
</span>
<!-- This should fade out once the data is loaded. -->
{#await ae_promises[slct_event_location_id ?? '']}
<LoaderCircle size="0.85em" class="inline animate-spin text-blue-500" />
<LoaderCircle
size="0.85em"
class="inline animate-spin text-blue-500" />
{:then result}
<Check size="0.85em" class="inline text-green-500/80" />
{/await}
@@ -172,7 +173,7 @@
</div>
<select
class="select text-xs p-1 max-w-42"
class="select max-w-42 p-1 text-xs"
bind:value={slct_event_location_id}
onchange={async () => {
// console.log(`slct_event_location_id:`, slct_event_location_id);
@@ -222,9 +223,8 @@
loading__session_li_status = null;
// goto(new_url, {replaceState: true}); // Updates the URL without reloading the page
goto(new_url, { replaceState: false }); // Updates the URL history without reloading the page
}}
>
<option value="" class="italic text-surface-800-200">
}}>
<option value="" class="text-surface-800-200 italic">
-- select --
</option>
{#each $lq__event_location_obj_li as event_location_obj (event_location_obj.event_location_id)}

View File

@@ -1,224 +1,236 @@
<script lang="ts">
/**
* menu_session_list.svelte — Aether Launcher: Session Selector
*
* PURPOSE:
* This is the primary navigation control for conference operators using
* the Aether Events Launcher. It lists all sessions in the selected room
* (event_location) and lets the operator switch the room's active session.
*
* ENVIRONMENT:
* The Launcher runs on operator laptops, dedicated podium/kiosk tablets,
* projector-connected desktops, and occasionally phones in breakout rooms.
* Users range from tech-savvy AV staff to volunteers with limited computer
* experience. Some users have motor impairments or shaky hands (e.g. older
* members common at IDAA and similar events).
*
* KEY DESIGN CONSTRAINTS:
* - Must show 020 sessions without scrolling (compact fixed-height rows)
* - Session names can be extremely long (~300 chars) — must truncate at
* rest but reveal fully on hover without pushing other rows around
* - Hover-to-switch fires after a delay timer (not instantly) to prevent
* accidental session changes from casual cursor movement
* - Strongly prefer click-to-confirm over hair-trigger hover activation
* - Works in light and dark mode; projector-safe high-contrast overlay
*
* DATA FLOW:
* lq__event_session_obj_li (Dexie liveQuery, passed from launcher/+layout.svelte)
* → rendered here as buttons
* → click / hover-timer sets trigger_reload__event_session_obj_id
* → $effect fires load + URL navigation + optional WS remote-control push
*
* SESSION VISIBILITY (operator toggle — show_content__hidden_sessions):
* Normal sessions: always visible
* hide_event_launcher = true: hidden from list by default (launcher-specific
* suppression, e.g. overflow/backup sessions)
* hide = true: globally hidden across all views (draft/cancelled)
*
* Both hidden states are fetched into Dexie with hidden:'all' by the background
* sync so the operator can reveal them via the "All Sessions" menu toggle.
* When revealed they appear dimmed (opacity-40) with an eye-slash indicator.
*/
/**
* menu_session_list.svelte — Aether Launcher: Session Selector
*
* PURPOSE:
* This is the primary navigation control for conference operators using
* the Aether Events Launcher. It lists all sessions in the selected room
* (event_location) and lets the operator switch the room's active session.
*
* ENVIRONMENT:
* The Launcher runs on operator laptops, dedicated podium/kiosk tablets,
* projector-connected desktops, and occasionally phones in breakout rooms.
* Users range from tech-savvy AV staff to volunteers with limited computer
* experience. Some users have motor impairments or shaky hands (e.g. older
* members common at IDAA and similar events).
*
* KEY DESIGN CONSTRAINTS:
* - Must show 020 sessions without scrolling (compact fixed-height rows)
* - Session names can be extremely long (~300 chars) — must truncate at
* rest but reveal fully on hover without pushing other rows around
* - Hover-to-switch fires after a delay timer (not instantly) to prevent
* accidental session changes from casual cursor movement
* - Strongly prefer click-to-confirm over hair-trigger hover activation
* - Works in light and dark mode; projector-safe high-contrast overlay
*
* DATA FLOW:
* lq__event_session_obj_li (Dexie liveQuery, passed from launcher/+layout.svelte)
* → rendered here as buttons
* → click / hover-timer sets trigger_reload__event_session_obj_id
* → $effect fires load + URL navigation + optional WS remote-control push
*
* SESSION VISIBILITY (operator toggle — show_content__hidden_sessions):
* Normal sessions: always visible
* hide_event_launcher = true: hidden from list by default (launcher-specific
* suppression, e.g. overflow/backup sessions)
* hide = true: globally hidden across all views (draft/cancelled)
*
* Both hidden states are fetched into Dexie with hidden:'all' by the background
* sync so the operator can reveal them via the "All Sessions" menu toggle.
* When revealed they appear dimmed (opacity-40) with an eye-slash indicator.
*/
interface Props {
slct__event_session_id?: null | boolean | string;
loading__session_id_status?: null | boolean | string;
// export let lq__event_session_obj: any;
lq__event_session_obj_li: any;
interface Props {
slct__event_session_id?: null | boolean | string;
loading__session_id_status?: null | boolean | string;
// export let lq__event_session_obj: any;
lq__event_session_obj_li: any;
trigger_reload__event_session_obj_id?: boolean | null | string;
// trigger_reload__event_session_obj_li?: boolean;
trigger_reload__event_session_obj_id?: boolean | null | string;
// trigger_reload__event_session_obj_li?: boolean;
log_lvl?: number;
}
log_lvl?: number;
}
let {
slct__event_session_id = $bindable(null),
loading__session_id_status = $bindable(null),
lq__event_session_obj_li,
let {
slct__event_session_id = $bindable(null),
loading__session_id_status = $bindable(null),
lq__event_session_obj_li,
trigger_reload__event_session_obj_id = $bindable(false),
// trigger_reload__event_session_obj_li = $bindable(false),
trigger_reload__event_session_obj_id = $bindable(false),
// trigger_reload__event_session_obj_li = $bindable(false),
log_lvl = $bindable(1)
}: Props = $props();
log_lvl = $bindable(1)
}: Props = $props();
// *** Import Svelte specific
import { untrack } from 'svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
// import { liveQuery } from "dexie";
// *** Import Svelte specific
import { untrack } from 'svelte';
import { page } from '$app/state';
import { goto } from '$app/navigation';
// import { liveQuery } from "dexie";
// *** Import other supporting libraries
// *** Import other supporting libraries
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
// import { db_events } from "$lib/db_events";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import { CalendarCheck, CalendarDays, Check, EyeOff, Image, LoaderCircle } from '@lucide/svelte';
// export let slct__event_session_id: any;
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
// import { db_events } from "$lib/db_events";
import {
ae_snip,
ae_loc,
ae_sess,
ae_api,
ae_trig,
slct,
slct_trigger
} from '$lib/stores/ae_stores';
import {
events_loc,
events_sess,
events_slct,
events_trigger
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';
import {
CalendarCheck,
CalendarDays,
Check,
EyeOff,
Image,
LoaderCircle
} from '@lucide/svelte';
// export let slct__event_session_id: any;
// *** Functions and Logic
// *** Functions and Logic
let ae_promises: key_val = $state({
slct__event_session_id: null,
slct__event_presentation_li: null
});
let ae_promises: key_val = $state({
slct__event_session_id: null,
slct__event_presentation_li: null
});
// WHY 1200ms: Aether Launcher is used at conferences by operators of all ages and
// motor abilities — shaky hands, imprecise trackpads, and fat-finger tablet taps are
// routine. 750ms (the previous value) triggered accidental session changes when the
// cursor drifted across the list. 1200ms means the operator must deliberately hold
// focus on a row for over a second before it fires — still fast for intentional use.
// NOTE: hover-timer only triggers a data PRE-LOAD (preview). The session does not
// actually switch until the operator clicks. See onclick handler below.
let hover_timer_wait = 1200;
let hover_timer: any = $state(null);
// WHY 1200ms: Aether Launcher is used at conferences by operators of all ages and
// motor abilities — shaky hands, imprecise trackpads, and fat-finger tablet taps are
// routine. 750ms (the previous value) triggered accidental session changes when the
// cursor drifted across the list. 1200ms means the operator must deliberately hold
// focus on a row for over a second before it fires — still fast for intentional use.
// NOTE: hover-timer only triggers a data PRE-LOAD (preview). The session does not
// actually switch until the operator clicks. See onclick handler below.
let hover_timer_wait = 1200;
let hover_timer: any = $state(null);
// Navigation Shield Pattern (Refactored 2026-02-11)
// WHY: We use untrack for store updates to prevent circular reactivity loops
// with the layout's sync effect. Standardizing on page.url ensures
// the back button and browser history work correctly.
$effect(() => {
if (trigger_reload__event_session_obj_id) {
const start = performance.now();
const event_session_id = String(trigger_reload__event_session_obj_id);
if (log_lvl) {
console.log(`[UI Trace] trigger_reload changed to: ${event_session_id}`);
}
untrack(() => {
// 1. Reset trigger immediately
trigger_reload__event_session_obj_id = false;
// 2. Local State Updates (Sync store for instant LiveQuery reaction)
if (slct__event_session_id !== event_session_id) {
slct__event_session_id = event_session_id;
}
// 3. Background Data Fetch
handle_load_ae_obj_id__event_session(event_session_id);
// 4. Remote Control Sync
if ($events_loc.launcher.controller == 'local_push') {
$events_sess.launcher.controller_cmd = `ae_load:event_session=${event_session_id}`;
$events_sess.launcher.controller_trigger_send = true;
}
// 5. URL Navigation
let new_url_obj = new URL(page.url);
new_url_obj.searchParams.set('session_id', event_session_id);
if (log_lvl) console.log(`[UI Trace] Initiating SvelteKit goto...`);
goto(new_url_obj.toString(), {
replaceState: false,
noScroll: true,
keepFocus: true
}).then(() => {
if (log_lvl)
console.log(`🏁 [Trace] Navigation Roundtrip: ${(performance.now() - start).toFixed(2)}ms`);
});
});
}
});
function handle_load_ae_obj_id__event_session(event_session_id: any) {
// Navigation Shield Pattern (Refactored 2026-02-11)
// WHY: We use untrack for store updates to prevent circular reactivity loops
// with the layout's sync effect. Standardizing on page.url ensures
// the back button and browser history work correctly.
$effect(() => {
if (trigger_reload__event_session_obj_id) {
const start = performance.now();
const event_session_id = String(trigger_reload__event_session_obj_id);
if (log_lvl) {
console.log(
`[UI Trace] handle_load_ae_obj_id__event_session: Calling library for id=${event_session_id}`
`[UI Trace] trigger_reload changed to: ${event_session_id}`
);
}
loading__session_id_status = 'loading';
untrack(() => {
// 1. Reset trigger immediately
trigger_reload__event_session_obj_id = false;
ae_promises.slct__event_session_id = events_func
.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: event_session_id,
inc_file_li: true,
inc_all_file_li: true,
inc_presentation_li: true,
inc_presenter_li: true,
log_lvl: log_lvl
})
.then(async (load_results) => {
// 2. Local State Updates (Sync store for instant LiveQuery reaction)
if (slct__event_session_id !== event_session_id) {
slct__event_session_id = event_session_id;
}
// 3. Background Data Fetch
handle_load_ae_obj_id__event_session(event_session_id);
// 4. Remote Control Sync
if ($events_loc.launcher.controller == 'local_push') {
$events_sess.launcher.controller_cmd = `ae_load:event_session=${event_session_id}`;
$events_sess.launcher.controller_trigger_send = true;
}
// 5. URL Navigation
let new_url_obj = new URL(page.url);
new_url_obj.searchParams.set('session_id', event_session_id);
if (log_lvl) console.log(`[UI Trace] Initiating SvelteKit goto...`);
goto(new_url_obj.toString(), {
replaceState: false,
noScroll: true,
keepFocus: true
}).then(() => {
if (log_lvl)
console.log(
`[UI Trace] handle_load_ae_obj_id: Library returned results in ${(performance.now() - start).toFixed(2)}ms.`
`🏁 [Trace] Navigation Roundtrip: ${(performance.now() - start).toFixed(2)}ms`
);
if (load_results) {
$events_slct.event_session_obj = load_results;
$events_slct.event_file_obj_li =
load_results.event_file_li ?? [];
$events_slct.event_presentation_obj_li =
load_results.event_presentation_li ?? [];
}
})
.finally(() => {
loading__session_id_status = false;
});
});
}
});
function handle_load_ae_obj_id__event_session(event_session_id: any) {
const start = performance.now();
if (log_lvl) {
console.log(
`[UI Trace] handle_load_ae_obj_id__event_session: Calling library for id=${event_session_id}`
);
}
loading__session_id_status = 'loading';
ae_promises.slct__event_session_id = events_func
.load_ae_obj_id__event_session({
api_cfg: $ae_api,
event_session_id: event_session_id,
inc_file_li: true,
inc_all_file_li: true,
inc_presentation_li: true,
inc_presenter_li: true,
log_lvl: log_lvl
})
.then(async (load_results) => {
if (log_lvl)
console.log(
`[UI Trace] handle_load_ae_obj_id: Library returned results in ${(performance.now() - start).toFixed(2)}ms.`
);
if (load_results) {
$events_slct.event_session_obj = load_results;
$events_slct.event_file_obj_li =
load_results.event_file_li ?? [];
$events_slct.event_presentation_obj_li =
load_results.event_presentation_li ?? [];
}
})
.finally(() => {
loading__session_id_status = false;
});
}
</script>
<div
class="
w-full max-w-80
flex flex-col flex-wrap gap-1 items-center justify-start md:justify-center
"
>
flex w-full
max-w-80 flex-col flex-wrap items-center justify-start gap-1 md:justify-center
">
{#if $lq__event_session_obj_li && $lq__event_session_obj_li.length > 0}
<div class="text-xs text-surface-600-400">
<div class="text-surface-600-400 text-xs">
<strong>
Sessions:
<span
class:hidden={!$ae_loc.trusted_access || !$ae_loc.edit_mode}
>
class:hidden={!$ae_loc.trusted_access ||
!$ae_loc.edit_mode}>
({$lq__event_session_obj_li?.length}&times;)
</span>
<!-- This should fade out once the data is loaded. -->
{#await ae_promises.slct__event_session_id}
<LoaderCircle size="0.85em" class="inline animate-spin text-blue-500" />
<LoaderCircle
size="0.85em"
class="inline animate-spin text-blue-500" />
{:then result}
<Check size="0.85em" class="inline text-green-500/80" />
{/await}
@@ -227,22 +239,20 @@
<ul
class="
m-0 flex
w-full max-w-full
p-0 m-0
flex flex-col gap-0 items-start justify-start
"
>
flex-col items-start justify-start gap-0 p-0
">
{#each $lq__event_session_obj_li as event_session_obj (event_session_obj.event_session_id)}
<li
class="
session-item
relative
p-0 m-0
w-full max-w-full
m-0 w-full
max-w-full p-0
"
class:session-active={slct__event_session_id ===
event_session_obj?.id}
>
event_session_obj?.id}>
<button
type="button"
onmouseenter={() => {
@@ -272,17 +282,17 @@
class="
session-btn
btn btn-sm
focus-visible:ring-2 focus-visible:ring-primary-400 focus-visible:ring-offset-1
focus-visible:ring-primary-400 m-0 flex
text-sm
w-full max-w-full
text-left
m-0
px-1.5 py-1
w-full
max-w-full flex-row
items-center
justify-start
rounded-md px-1.5
rounded-md
flex flex-row items-center justify-start
transition-colors duration-200
py-1
text-left text-sm transition-colors duration-200
focus-visible:ring-2 focus-visible:ring-offset-1
"
class:preset-filled-primary={slct__event_session_id ===
event_session_obj?.id}
@@ -298,8 +308,7 @@
event_session_obj?.hide_event_launcher)}
class:opacity-40={event_session_obj?.hide ||
event_session_obj?.hide_event_launcher}
title={`Session: ${event_session_obj?.name}\nID: ${event_session_obj?.id} | ${ae_util.iso_datetime_formatter(event_session_obj?.start_datetime, $events_loc.launcher.time_format)}`}
>
title={`Session: ${event_session_obj?.name}\nID: ${event_session_obj?.id} | ${ae_util.iso_datetime_formatter(event_session_obj?.start_datetime, $events_loc.launcher.time_format)}`}>
<!-- Session row layout: [date column | session name]
Date column is fixed-width (shrink-0) so name column always
gets consistent space regardless of date string length.
@@ -311,8 +320,7 @@
When revealed, dimmed (opacity-40) with eye-slash icon. -->
<span
class="border-r border-surface-400-600 pr-1 min-w-20 shrink-0"
>
class="border-surface-400-600 min-w-20 shrink-0 border-r pr-1">
{#if slct__event_session_id === event_session_obj?.id}
<CalendarCheck size="0.85em" class="inline" />
{:else}
@@ -321,8 +329,7 @@
<span
class="text-xs"
class:hidden={slct__event_session_id ===
event_session_obj?.id}
>
event_session_obj?.id}>
{ae_util.iso_datetime_formatter(
event_session_obj?.start_datetime,
'week_medium'
@@ -339,20 +346,28 @@
<span
class="
session-name
grow text-sm
min-w-0
"
>
min-w-0 grow
text-sm
">
{#if event_session_obj?.type_code == 'poster'}
<span title="Digital Poster Session"><Image size="0.85em" class="inline mr-1 text-primary-500" /></span>
<span title="Digital Poster Session"
><Image
size="0.85em"
class="text-primary-500 mr-1 inline" /></span>
{/if}
<!-- Distinct icon styles distinguish the two hidden states:
amber = hide (globally hidden — draft, cancelled, or admin-only)
muted = hide_event_launcher (suppressed in Launcher view only) -->
{#if event_session_obj?.hide}
<span title="Hidden session"><EyeOff size="0.85em" class="inline mr-1 text-warning-600" /></span>
<span title="Hidden session"
><EyeOff
size="0.85em"
class="text-warning-600 mr-1 inline" /></span>
{:else if event_session_obj?.hide_event_launcher}
<span title="Hidden from Launcher"><EyeOff size="0.85em" class="inline mr-1 opacity-60" /></span>
<span title="Hidden from Launcher"
><EyeOff
size="0.85em"
class="mr-1 inline opacity-60" /></span>
{/if}
{event_session_obj?.name}
</span>
@@ -366,7 +381,7 @@
</div>
<style>
/*
/*
* ═══════════════════════════════════════════════════════════════════
* Aether Launcher — Compact Session List Styles
* One Sky IT — specialized for conference operator use
@@ -413,76 +428,80 @@
* can instantly see which row is being previewed.
*/
/* ── Inactive row: fixed compact height ── */
.session-item {
height: 2rem;
}
/* ── Inactive row: fixed compact height ── */
.session-item {
height: 2rem;
}
/* ── Active row: always fully visible in flow ── */
.session-item.session-active {
height: auto;
min-height: 2rem;
}
/* ── Active row: always fully visible in flow ── */
.session-item.session-active {
height: auto;
min-height: 2rem;
}
/* Clip inactive button content to row height */
.session-btn {
overflow: hidden;
}
/* Clip inactive button content to row height */
.session-btn {
overflow: hidden;
}
/* Active button: never clip — operator can always read it */
.session-item.session-active .session-btn {
overflow: visible;
}
/* Active button: never clip — operator can always read it */
.session-item.session-active .session-btn {
overflow: visible;
}
/*
/*
* Inactive hover/focus: pop button out as an overlay panel.
* - position:absolute keeps the <li> placeholder at 2rem (no layout shift)
* - Solid opaque background prevents tonal transparency from showing through
* - Left border accent gives a clear "you are here" cue
*/
.session-item:not(.session-active):hover .session-btn,
.session-item:not(.session-active):focus-within .session-btn {
position: absolute;
top: 0;
left: 0;
right: 0;
height: auto;
z-index: 20;
overflow: visible;
opacity: 1;
background-color: #f1f5f9; /* slate-100 — solid light surface */
border-left: 3px solid rgb(var(--color-primary-500, 99 102 241));
border-radius: 0.375rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15), 0 1px 4px rgba(0, 0, 0, 0.08);
}
.session-item:not(.session-active):hover .session-btn,
.session-item:not(.session-active):focus-within .session-btn {
position: absolute;
top: 0;
left: 0;
right: 0;
height: auto;
z-index: 20;
overflow: visible;
opacity: 1;
background-color: #f1f5f9; /* slate-100 — solid light surface */
border-left: 3px solid rgb(var(--color-primary-500, 99 102 241));
border-radius: 0.375rem;
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.15),
0 1px 4px rgba(0, 0, 0, 0.08);
}
/* Dark mode overlay — solid dark surface, light readable text */
:global(.dark) .session-item:not(.session-active):hover .session-btn,
:global(.dark) .session-item:not(.session-active):focus-within .session-btn {
background-color: #1e293b; /* slate-800 */
color: #f1f5f9; /* slate-100 */
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.6), 0 1px 4px rgba(0, 0, 0, 0.4);
}
/* Dark mode overlay — solid dark surface, light readable text */
:global(.dark) .session-item:not(.session-active):hover .session-btn,
:global(.dark) .session-item:not(.session-active):focus-within .session-btn {
background-color: #1e293b; /* slate-800 */
color: #f1f5f9; /* slate-100 */
box-shadow:
0 4px 24px rgba(0, 0, 0, 0.6),
0 1px 4px rgba(0, 0, 0, 0.4);
}
/* ── Session name: single-line truncated at rest ── */
.session-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* ── Session name: single-line truncated at rest ── */
.session-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Active session: name always wraps fully */
.session-item.session-active .session-name {
white-space: normal;
overflow: visible;
text-overflow: unset;
}
/* Active session: name always wraps fully */
.session-item.session-active .session-name {
white-space: normal;
overflow: visible;
text-overflow: unset;
}
/* Overlay: name wraps fully (300-char titles readable) */
.session-item:not(.session-active):hover .session-name,
.session-item:not(.session-active):focus-within .session-name {
white-space: normal;
overflow: visible;
text-overflow: unset;
}
/* Overlay: name wraps fully (300-char titles readable) */
.session-item:not(.session-active):hover .session-name,
.session-item:not(.session-active):focus-within .session-name {
white-space: normal;
overflow: visible;
text-overflow: unset;
}
</style>