Prettier for Event Launcher
This commit is contained in:
@@ -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) -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 & native OS controls"
|
||||
>
|
||||
title="Sync engine, device health & 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 & debug tools"
|
||||
>
|
||||
title="Developer & 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 • Events Launcher v3.0
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}×)
|
||||
</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}×)
|
||||
</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}×)
|
||||
{/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}×)
|
||||
{/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" />
|
||||
|
||||
@@ -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}×
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}×)
|
||||
</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)}
|
||||
|
||||
@@ -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 0–20 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 0–20 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}×)
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user