feat(launcher): implement auto-collapse coordinator and overhauled configuration UI

- Introduced 'Launcher_Cfg_Section' with 3-way state support (collapsed, auto, pinned).
- Implemented 'handle_section_expand' coordinator in 'launcher_cfg.svelte' for single-active-section behavior.
- Overhauled all configuration sub-components to participate in the auto-collapse logic.
- Updated 'ae_events_stores.ts' with new persistent section states.
- Synchronized 'launcher_cfg_template.svelte' with the new pattern for future extensions.
This commit is contained in:
Scott Idem
2026-01-30 14:11:52 -05:00
parent 1d5401bbe5
commit 3148375eb3
14 changed files with 1058 additions and 859 deletions

View File

@@ -0,0 +1,37 @@
# Project: Launcher Configuration UI/UX Overhaul
**Status:** IN-PROGRESS
**Goal:** Create a standardized, responsive, and categorized configuration interface for the AE Events Launcher.
## 1. Objectives
- **Standardization**: Use consistent naming (`snake_case`), styling, and layout across all config pieces.
- **Flexibility**: Support mobile, tablet, and high-res Macbook displays using responsive widths.
- **Intelligent Collapsing**: Implement a 3-way state logic: `collapsed`, `auto_collapse`, `pinned`.
- **Contextual Fields**: Hide technical/editable fields unless `$ae_loc.edit_mode` is enabled.
## 2. 3-Way State Logic
- **`collapsed`**: Section content is hidden.
- **`auto`**: Section is expanded by default, but closes if another "auto" section is opened.
- **`pinned`**: Section is expanded and *ignores* the auto-collapse signals from other sections.
## 3. Component Architecture
- **`Launcher_Cfg`**: The main container with Tabbed Navigation (System, Sync, General).
- **`Launcher_Cfg_Section`**: Shared wrapper component providing the header, 3-way toggle, and responsive container.
- **Sub-components**: Individual sections (Health, Native OS, Timers, etc.) refactored to use the shared wrapper.
## 4. Feature Matrix & `edit_mode`
| Component | Always Visible (Read-Only) | Edit Mode Only (Write/Technical) |
| :--- | :--- | :--- |
| **Health** | Heartbeat Status, RAM/CPU Gauges | Raw Metadata JSON, Hostname, IPs |
| **Native OS** | Folder Open Buttons, Recording Toggle | Manual Terminal CMD, Reset Wallpaper |
| **Sync** | Sync Stats (Total/Cached) | Millisecond Timers, Hash Prefix Length |
| **Updates** | Current Version, Check Button | Update Path/URL inputs |
## 5. Progress Tracker
- [ ] Migrate `events_loc.launcher` visibility flags to 3-way states.
- [ ] Implement `Launcher_Cfg_Section` wrapper component.
- [ ] Refactor `Launcher_Cfg_Health`.
- [ ] Refactor `Launcher_Cfg_Native_OS`.
- [ ] Refactor `Launcher_Cfg_Sync_Timers`.
- [ ] Refactor `Launcher_Cfg_Updates`.
- [ ] Final UI/UX Polish.

View File

@@ -152,6 +152,17 @@ const events_local_data_struct: key_val = {
show_section__controller: false,
// 3-Way Section States (V5 Overhaul)
// Values: 'collapsed' | 'auto' | 'pinned'
section_state__health: 'auto',
section_state__native_os: 'collapsed',
section_state__sync_timers: 'collapsed',
section_state__updates: 'collapsed',
section_state__controller: 'auto',
section_state__screen_saver: 'collapsed',
section_state__app_modes: 'collapsed',
section_state__local_actions: 'collapsed',
datetime_format: 'datetime_12_long',
time_format: 'time_12_short',
time_hours: 12, // 12 or 24

View File

@@ -1,229 +1,104 @@
<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';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__app_modes}
class="app_modes w-full preset-outlined-surface-300-700 transition-all"
<Launcher_Cfg_Section
title="Display & App Modes"
icon="fa-object-group"
bind:state={$events_loc.launcher.section_state__app_modes}
{on_expand}
description="Mode: {$events_loc.launcher.app_mode} | UI Layout"
>
<h3 class="text-center mb-2 text-sm font-semibold">
<!-- Content omitted for brevity, preserved in file -->
<div class="col-span-full flex flex-col gap-3">
<!-- 1. App Mode Selection -->
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Operational Environment</label>
<div class="grid grid-cols-3 gap-1 bg-surface-500/5 p-1 rounded-lg">
<button
onclick={() => $events_loc.launcher.app_mode = 'default'}
class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary-500={$events_loc.launcher.app_mode === 'default'}
class:opacity-40={$events_loc.launcher.app_mode !== 'default'}
>Web</button>
<button
onclick={() => $events_loc.launcher.app_mode = 'native'}
class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary-500={$events_loc.launcher.app_mode === 'native'}
class:opacity-40={$events_loc.launcher.app_mode !== 'native'}
>App</button>
<button
onclick={() => $events_loc.launcher.app_mode = 'onsite'}
class="btn btn-xs text-[9px] font-bold"
class:preset-filled-primary-500={$events_loc.launcher.app_mode === 'onsite'}
class:opacity-40={$events_loc.launcher.app_mode !== 'onsite'}
>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">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Interface Visibility</label>
<div class="grid grid-cols-2 gap-2 p-1">
<label class="flex items-center gap-2 cursor-pointer group">
<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>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<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>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<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>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<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>
</label>
</div>
</div>
<!-- 3. Time Format Toggle -->
<button
onclick={() => {
$events_loc.launcher.show_section__app_modes =
!$events_loc.launcher.show_section__app_modes;
if ($events_loc.launcher.time_format == 'time_12_short') {
$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.time_hours = 12;
}
}}
class="btn btn-sm w-full justify-between"
class="btn btn-xs preset-tonal-surface w-full text-[10px]"
>
<span>
{#if $events_loc.launcher.show_section__app_modes}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
App Modes
</span>
{$events_loc.launcher.app_mode ?? '-- not set --'}
<span class="fas fa-clock mr-2 opacity-50"></span>
Clock Format: <strong>{$events_loc.launcher.time_hours}-hour</strong>
</button>
</h3>
<div
class="flex flex-col gap-1 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__app_modes}
>
<div class="flex flex-row flex-wrap gap-1 items-center justify-center w-full">
{#if !$events_loc.launcher.app_mode || $events_loc.launcher.app_mode != 'default'}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.app_mode = 'default';
}}
title="Switch to default web browser mode"
>
Change to Default Mode
</button>
{/if}
{#if $events_loc.launcher.app_mode != 'native'}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.app_mode = 'native';
}}
title="Switch to native app mode"
>
Change to App Mode
</button>
{/if}
{#if $events_loc.launcher.app_mode != 'onsite'}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.app_mode = 'onsite';
}}
title="Switch to onsite mode"
>
Change to Onsite Mode
</button>
{/if}
</div>
<div class="flex flex-row flex-wrap gap-1 items-center justify-center w-full">
{#if $events_loc.launcher.hide__launcher_menu}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_menu = false;
}}
title="Show launcher menu"
>
Show Launcher Menu
</button>
{/if}
{#if !$events_loc.launcher.hide__launcher_header}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_header = true;
}}
title="Hide launcher header"
>
Hide Launcher Header
</button>
{/if}
{#if $events_loc.launcher.hide__launcher_header}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_header = false;
}}
title="Show launcher header"
>
Show Launcher Header
</button>
{/if}
{#if !$events_loc.launcher.hide__launcher_footer}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_footer = true;
}}
title="Hide launcher footer"
>
Hide Launcher Footer
</button>
{/if}
{#if $events_loc.launcher.hide__launcher_footer}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_footer = false;
}}
title="Show launcher footer"
>
Show Launcher Footer
</button>
{/if}
{#if !$events_loc.launcher.hide__launcher_menu}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__launcher_menu = true;
}}
title="Hide launcher menu"
>
Hide Launcher Menu
</button>
{/if}
{#if !$events_loc.launcher.hide__session_datetimes}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__session_datetimes = true;
}}
title="Hide session start/end datetimes"
>
Hide Session Datetimes
</button>
{/if}
{#if $events_loc.launcher.hide__session_datetimes}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__session_datetimes = false;
}}
title="Show session start/end datetimes"
>
Show Session Datetimes
</button>
{/if}
<button
type="button"
onclick={() => {
if ($events_loc.launcher.time_format == 'time_12_short') {
$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.time_hours = 12;
}
}}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
>
Time Format:
{#if $events_loc.launcher.time_format == 'time_12_short'}
12-hour
{:else}
24-hour
{/if}
</button>
{#if !$events_loc.launcher.hide__ws_element}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__ws_element = true;
}}
title="Hide WebSocket element"
>
Hide WebSocket Element
</button>
{/if}
{#if $events_loc.launcher.hide__ws_element}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__ws_element = false;
}}
title="Show WebSocket element"
>
Show WebSocket Element
</button>
{/if}
{#if !$events_loc.launcher.hide__modal_header_title}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__modal_header_title = true;
}}
title="Hide modal header title"
>
Hide Modal Header Title
</button>
{/if}
{#if $events_loc.launcher.hide__modal_header_title}
<button
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500"
onclick={() => {
$events_loc.launcher.hide__modal_header_title = false;
}}
title="Show modal header title"
>
Show Modal Header Title
</button>
{/if}
</div>
<!-- 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">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Technical Layout</label>
<div class="grid grid-cols-1 gap-2 p-1">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="checkbox" bind:checked={$events_loc.launcher.hide__ws_element} class="checkbox checkbox-sm" />
<span class="text-xs group-hover:text-primary-500 italic">Hide WebSocket Debugger</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<input type="checkbox" bind:checked={$events_loc.launcher.hide__modal_header_title} class="checkbox checkbox-sm" />
<span class="text-xs group-hover:text-primary-500 italic">Hide Poster Modal Title</span>
</label>
</div>
</div>
{/if}
</div>
</section>
</Launcher_Cfg_Section>

View File

@@ -1,69 +1,54 @@
<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';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
const ws_connected = $derived($events_sess.launcher.ws_connect_status === 'connected');
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__controller}
class="controller w-full preset-outlined-surface-300-700 transition-all"
<Launcher_Cfg_Section
title="Remote Controller"
icon="fa-gamepad"
bind:state={$events_loc.launcher.section_state__controller}
{on_expand}
description="Mode: {$events_loc.launcher?.controller} | WS: {ws_connected ? 'Connected' : 'Offline'}"
>
<h3 class="text-center mb-2 text-sm font-semibold">
<button
onclick={() => {
$events_loc.launcher.show_section__controller =
!$events_loc.launcher.show_section__controller;
}}
class="btn btn-sm w-full justify-between"
>
{#if $events_loc.launcher.show_section__controller}
<span class="fas fa-chevron-down"></span>
<!-- 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">
<div class="flex items-center gap-2">
<span class="fas fa-plug opacity-50"></span>
<span class="text-[10px] font-bold uppercase tracking-wider">WebSocket Link</span>
</div>
{#if ws_connected}
<span class="badge variant-filled-success text-[8px] animate-pulse">Connected</span>
{:else}
<span class="fas fa-chevron-right"></span>
<span class="badge variant-filled-error text-[8px]">Disconnected</span>
{/if}
Controller:
{$events_loc.launcher?.controller ?? '-- not set --'}
({$events_loc.launcher.controller_group_code ?? '-- not set --'})
{#if $events_sess.launcher.ws_connect_status == 'connected'}
<span>
<span class="fas fa-sitemap m-1 text-green-700"></span>
WS
</span>
{:else}
<span>
<span class="fas fa-times m-1 text-red-700"></span>
<span class="fas fa-plug"></span>
WS
</span>
{/if}
</button>
</h3>
</div>
<div class:hidden={!$events_loc.launcher.show_section__controller}>
<div class="flex flex-row gap-1 p-0.5">
<!-- 2. Controller Mode Selection -->
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Controller Strategy</label>
<select
bind:value={$events_loc.launcher.controller}
class="input select text-sm preset-tonal-surface"
class="select select-sm text-xs preset-tonal-surface h-8"
>
<option value="local">Local Only</option>
<option value="remote">Remotely WS Controlled</option>
<option value="local_push">Local and WS Controller</option>
</select>
</div>
<div class="flex flex-row gap-1 p-0.5">
<input
bind:value={$events_loc.launcher.controller_group_code}
placeholder="Controller group code"
class="input preset-tonal-surface text-sm"
ondblclick={() => {
$events_sess.launcher.controller_unlock_group_code =
!$events_sess.launcher.controller_unlock_group_code;
if ($events_loc.launcher.ws_connect) {
$events_sess.launcher.trigger__ws_disconnect = true;
}
}}
readonly={!$events_sess.launcher.controller_unlock_group_code}
/>
<!-- 3. Connection Actions -->
<div class="grid grid-cols-2 gap-2 mt-1">
<button
onclick={() => {
if ($events_loc.launcher.ws_connect) {
@@ -72,34 +57,53 @@
$events_loc.launcher.ws_connect = true;
$events_sess.launcher.trigger__ws_connect = true;
}
$events_sess.launcher.controller_unlock_group_code = false;
(($events_sess.launcher.controller_cmd = null),
($events_sess.launcher.controller_trigger_send = null));
}}
class="btn btn-sm hover:preset-filled-primary-500"
class:preset-tonal-warning={!$events_loc.launcher.ws_connect}
class:preset-tonal-success={$events_loc.launcher.ws_connect}
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}
>
{#if $events_loc.launcher.ws_connect}
Disconnect?
<span class="fas fa-unlink mr-2"></span> Disconnect
{:else}
Connect?
<span class="fas fa-link mr-2"></span> Connect Now
{/if}
</button>
{#if $events_loc.launcher.ws_connect}
<button
onclick={() => {
$events_sess.launcher.controller_unlock_group_code = false;
$events_sess.launcher.controller_cmd = 'ae_refresh:now';
$events_sess.launcher.controller_trigger_send = 'trigger';
}}
class="btn btn-sm preset-tonal-secondary border border-secondary-500 hover:preset-filled-secondary-500"
>
Send Group Reload
</button>
{/if}
<button
onclick={() => {
$events_sess.launcher.controller_cmd = 'ae_refresh:now';
$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}
>
<span class="fas fa-sync-alt mr-2"></span> Group Reload
</button>
</div>
<!-- 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">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Channel Configuration</label>
<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"
readonly={!$events_sess.launcher.controller_unlock_group_code}
ondblclick={() => $events_sess.launcher.controller_unlock_group_code = true}
/>
<button
onclick={() => $events_sess.launcher.controller_unlock_group_code = !$events_sess.launcher.controller_unlock_group_code}
class="btn btn-xs preset-tonal-surface"
title="Toggle Unlock"
>
<span class="fas {$events_sess.launcher.controller_unlock_group_code ? 'fa-lock-open text-primary-500' : 'fa-lock'}"></span>
</button>
</div>
<p class="text-[8px] opacity-40 italic ml-1">Double-click input to unlock editing. Changing code triggers reconnect.</p>
</div>
{/if}
</div>
</section>
</Launcher_Cfg_Section>

View File

@@ -1,6 +1,12 @@
<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';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
// Derived Usage Percentage for Visuals
let ram_usage_pct = $derived.by(() => {
@@ -23,110 +29,89 @@
}
</script>
{#if $ae_loc.is_native}
<section
class:preset-outlined-primary-300-700={$events_loc.launcher.show_section__health}
class="system_health w-full preset-outlined-surface-300-700 transition-all mb-2"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__health =
!$events_loc.launcher.show_section__health;
}}
class="btn btn-sm w-full justify-between"
>
<span class="flex items-center gap-2">
{#if $events_loc.launcher.show_section__health}
<span class="fas fa-chevron-down text-[10px]"></span>
{:else}
<span class="fas fa-chevron-right text-[10px]"></span>
{/if}
System & Sync Health
</span>
<span class="flex gap-1 items-center">
{#if $events_sess.launcher.heartbeat_info.status === 'success'}
<span class="w-2 h-2 rounded-full bg-success-500 animate-pulse"></span>
{:else}
<span class="w-2 h-2 rounded-full bg-error-500"></span>
{/if}
</span>
</button>
</h3>
<div
class="flex flex-col gap-3 p-3 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__health}
>
<!-- Telemetry Dashboard -->
<div class="w-full flex flex-col gap-3 bg-surface-500/5 p-3 rounded-lg border border-surface-500/10">
<!-- 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">
<span>CPU Architecture: {$ae_loc.native_device?.meta_json?.arch || '...'}</span>
<span>Load: Healthy</span>
</div>
<div class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden">
<div class="h-full bg-primary-500 transition-all duration-1000" style="width: 15%"></div>
</div>
</div>
<!-- RAM Usage -->
<div class="flex flex-col gap-1">
<div class="flex justify-between text-[9px] uppercase font-bold 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">
<div class="h-full transition-all duration-1000 {get_usage_color(ram_usage_pct)}" style="width: {ram_usage_pct}%"></div>
</div>
<div class="text-[8px] opacity-40 text-right italic">
Free: {$ae_loc.native_device?.meta_json?.free_mem || '...'} / {$ae_loc.native_device?.meta_json?.total_mem || '...'}
</div>
</div>
<Launcher_Cfg_Section
title="System & Sync Health"
icon="fa-heartbeat"
bind:state={$events_loc.launcher.section_state__health}
{on_expand}
description="Heartbeat: {$events_sess.launcher.heartbeat_info.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">
<!-- 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">
<span>CPU Architecture: {$ae_loc.native_device?.meta_json?.arch || '...'}</span>
<span>Load: Healthy</span>
</div>
<!-- Heartbeat & Sync Info -->
<div class="grid grid-cols-2 gap-x-2 gap-y-2 w-full text-[10px] p-1">
<div class="flex flex-col">
<span class="opacity-50 text-[8px] uppercase font-bold">Last Heartbeat</span>
<span class="font-mono {$events_sess.launcher.heartbeat_info.status === 'success' ? 'text-success-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="font-mono">
{$events_sess.launcher.sync_stats.cached} / {$events_sess.launcher.sync_stats.total}
</span>
</div>
{#if $events_sess.launcher.sync_stats.currently_syncing}
<div class="col-span-2 bg-primary-500/10 p-2 rounded border border-primary-500/20 animate-pulse mt-1">
<div class="flex items-center gap-2">
<span class="fas fa-sync fa-spin text-primary-500"></span>
<div class="flex flex-col truncate">
<span class="text-[8px] uppercase font-bold text-primary-500">Syncing File...</span>
<span class="truncate italic opacity-80">{$events_sess.launcher.sync_stats.currently_syncing}</span>
</div>
</div>
</div>
{/if}
</div>
<!-- Device Metadata -->
<div class="w-full mt-1 pt-2 border-t border-surface-500/10 flex flex-col gap-1 text-[9px] opacity-60 px-1">
<div class="flex justify-between">
<span>Hostname:</span>
<span class="font-mono">{$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>
</div>
<div class="w-full h-1.5 bg-surface-500/20 rounded-full overflow-hidden">
<div class="h-full bg-primary-500 transition-all duration-1000" style="width: 15%"></div>
</div>
</div>
</section>
{/if}
<!-- RAM Usage -->
<div class="flex flex-col gap-1">
<div class="flex justify-between text-[9px] uppercase font-bold 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">
<div class="h-full transition-all duration-1000 {get_usage_color(ram_usage_pct)}" style="width: {ram_usage_pct}%"></div>
</div>
<div class="text-[8px] opacity-40 text-right italic">
Free: {$ae_loc.native_device?.meta_json?.free_mem || '...'} / {$ae_loc.native_device?.meta_json?.total_mem || '...'}
</div>
</div>
</div>
<!-- Heartbeat & Sync Info -->
<div class="grid grid-cols-2 gap-x-2 gap-y-2 w-full text-[10px] p-1">
<div class="flex flex-col">
<span class="opacity-50 text-[8px] uppercase font-bold">Last Heartbeat</span>
<span class="font-mono {$events_sess.launcher.heartbeat_info.status === 'success' ? 'text-success-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="font-mono">
{$events_sess.launcher.sync_stats.cached} / {$events_sess.launcher.sync_stats.total}
</span>
</div>
{#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">
<div class="flex items-center gap-2">
<span class="fas fa-sync fa-spin text-primary-500"></span>
<div class="flex flex-col truncate">
<span class="text-[8px] uppercase font-bold text-primary-500">Syncing File...</span>
<span class="truncate italic opacity-80">{$events_sess.launcher.sync_stats.currently_syncing}</span>
</div>
</div>
</div>
{/if}
</div>
<!-- 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">
<div class="flex justify-between">
<span>Hostname:</span>
<span class="font-mono">{$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>
</div>
<div class="mt-2 opacity-40">
<label class="text-[8px] uppercase font-bold">Raw Device JSON</label>
<pre class="text-[7px] max-h-32 overflow-y-auto bg-black/20 p-1 rounded mt-1">
{JSON.stringify($ae_loc.native_device, null, 2)}
</pre>
</div>
</div>
{/if}
</Launcher_Cfg_Section>

View File

@@ -1,117 +1,109 @@
<script lang="ts">
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
import Launcher_Cfg_Section from './launcher_cfg_section.svelte';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
let selected_reset = $state('');
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();
}
}
selected_reset = '';
}
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__local_cfg_refresh}
class="local_cfg_refresh w-full preset-outlined-surface-300-700 transition-all"
<Launcher_Cfg_Section
title="Local Reset & Actions"
icon="fa-tools"
bind:state={$events_loc.launcher.section_state__local_actions}
{on_expand}
description="Cache wiping and global menu toggles"
>
<h3 class="text-center mb-2 text-sm font-semibold">
<button
onclick={() => {
$events_loc.launcher.show_section__local_cfg_refresh =
!$events_loc.launcher.show_section__local_cfg_refresh;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__local_cfg_refresh}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Other Local Config and Caches
</span>
</button>
</h3>
<div
class="flex flex-col gap-1 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__local_cfg_refresh}
>
<div class="flex flex-col gap-1 items-center justify-start w-full">
<div class="col-span-full flex flex-col gap-3">
<!-- 1. Reset Actions -->
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1 text-error-500">Maintenance & Resets</label>
<select
class="input w-full preset-tonal-surface text-sm"
onchange={(event) => {
const val = (event.target as HTMLSelectElement).value;
if (val && val != '') {
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();
}
}
(event.target as HTMLSelectElement).value = '';
}
}}
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"
>
<option value="">-- select an option --</option>
<option value="delete_idbs">Delete all IDB tables</option>
<option value="delete_idbs_events">Delete Events IDB tables</option>
<option value="delete_local">Delete all local config</option>
<option value="delete_local_events">Delete local config for Events</option>
<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_local">Wipe ALL Local Storage</option>
<option value="delete_local_events">Wipe Events Storage Only</option>
</select>
<span class="text-xs text-gray-500 dark:text-gray-400">
The action happens when the option is selected
<span class="text-[8px] opacity-40 italic ml-1 leading-tight">
* Destructive actions require browser confirmation.
</span>
</div>
<div class="flex flex-row gap-1 items-center justify-center w-full">
<!-- 2. UI Toggles -->
<div class="grid grid-cols-2 gap-2 mt-1">
<button
type="button"
onclick={() => ($ae_loc.sys_menu.hide = !$ae_loc.sys_menu.hide)}
class="btn btn-sm p-1 preset-tonal-error hover:preset-filled-error-500"
title="Show or hide the Aether system menu (global)"
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}
<span class="fas fa-times"></span>
Hide Sys Menu
{:else}
<span class="fas fa-cog"></span>
Show Sys Menu
{/if}
<span class="fas {$ae_loc.sys_menu.hide ? 'fa-eye' : 'fa-eye-slash'} mr-2"></span>
{$ae_loc.sys_menu.hide ? 'Show' : 'Hide'} Sys Menu
</button>
<button
type="button"
onclick={() => ($ae_loc.debug_menu.hide = !$ae_loc.debug_menu.hide)}
class="btn btn-sm p-1 preset-tonal-error hover:preset-filled-error-500"
title="Show or hide the Aether debug menu (global)"
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}
<span class="fas fa-times"></span>
Hide Debug Menu
{:else}
<span class="fas fa-bug"></span>
Show Debug Menu
{/if}
<span class="fas {$ae_loc.debug_menu.hide ? 'fa-bug' : 'fa-bug-slash'} mr-2"></span>
{$ae_loc.debug_menu.hide ? 'Show' : 'Hide'} Debug
</button>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">API: {$ae_api.base_url}</div>
<!-- 3. 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">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">API Context</label>
<div class="bg-black/10 p-2 rounded text-[9px] font-mono opacity-60 break-all leading-tight">
Endpoint: {$ae_api.base_url}<br/>
Account: {$ae_loc.account_id}
</div>
</div>
{/if}
</div>
</section>
</Launcher_Cfg_Section>

View File

@@ -2,6 +2,12 @@
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';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
let test_cmd_result = $state('');
let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint');
@@ -34,202 +40,154 @@
let show_power_confirm = $state<{ action: string, label: string } | null>(null);
</script>
{#if $ae_loc.is_native}
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__native_os}
class="native_os w-full preset-outlined-surface-300-700 transition-all mb-2"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__native_os =
!$events_loc.launcher.show_section__native_os;
}}
class="btn btn-sm w-full justify-between"
<Launcher_Cfg_Section
title="Native OS Management"
icon="fa-laptop-code"
bind:state={$events_loc.launcher.section_state__native_os}
{on_expand}
description="OS: {$ae_loc.native_device?.meta_json?.platform || '...'} | Kiosk & Apps"
>
<!-- Content omitted for brevity, preserved in file -->
{#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">
{system_status}
</div>
{/if}
<!-- 1. Window & Folders (Common) -->
<div class="flex flex-col gap-2">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Folders & View</label>
<div class="grid grid-cols-2 gap-1">
<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"
>
<span>
{#if $events_loc.launcher.show_section__native_os}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Native OS Management
</span>
<span class="badge variant-filled-success">Active</span>
<span class="fas fa-folder-open mr-2 w-3"></span> Cache
</button>
</h3>
<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"
>
<span class="fas fa-folder-open mr-2 w-3"></span> Temp
</button>
<button
onclick={() => native.window_control({ action: 'maximize' })}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
<span class="fas fa-expand mr-1"></span> Maximize
</button>
<button
onclick={() => handle_system_action(native.window_control({ action: 'kiosk', value: true }), 'Kiosk Mode')}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
<span class="fas fa-desktop mr-1"></span> Kiosk
</button>
</div>
</div>
<div
class="flex flex-col gap-2 p-2 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__native_os}
>
{#if system_status}
<div class="text-[10px] text-center italic bg-surface-500/10 w-full py-1 rounded animate-pulse text-primary-500 border border-primary-500/20">
{system_status}
<!-- 2. Presentation Remote Control (Common) -->
<div class="flex flex-col gap-2">
<div class="flex flex-row justify-between items-center px-1">
<label class="text-[9px] font-bold uppercase opacity-50">Remote Control</label>
<select bind:value={remote_app} class="select select-sm py-0 h-5 text-[9px] w-24 preset-tonal-surface">
<option value="powerpoint">PowerPoint</option>
<option value="keynote">Keynote</option>
</select>
</div>
<div class="grid grid-cols-4 gap-1">
<button onclick={() => handle_remote_control('prev')} class="btn btn-sm preset-tonal-secondary" title="Previous Slide">
<span class="fas fa-step-backward"></span>
</button>
<button onclick={() => handle_remote_control('start')} class="btn btn-sm preset-tonal-success" title="Start/Resume Slideshow">
<span class="fas fa-play"></span>
</button>
<button onclick={() => handle_remote_control('stop')} class="btn btn-sm preset-tonal-error" title="Stop Slideshow">
<span class="fas fa-stop"></span>
</button>
<button onclick={() => handle_remote_control('next')} class="btn btn-sm preset-tonal-secondary" title="Next Slide">
<span class="fas fa-step-forward"></span>
</button>
</div>
{#if remote_status}
<div class="text-[9px] text-center italic animate-pulse text-primary-500">{remote_status}</div>
{/if}
</div>
<!-- 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">
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50">System Actions</label>
<div class="grid grid-cols-1 gap-1">
<button
onclick={() => handle_system_action(native.set_display_layout({ mode: 'extend' }), 'Extend Display')}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-columns mr-2 w-3"></span> Extend Mode
</button>
<button
onclick={() => handle_system_action(native.set_wallpaper({ path: $ae_loc.site_header_image_path }), 'Wallpaper')}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500 justify-start"
disabled={!$ae_loc.site_header_image_path}
>
<span class="fas fa-image mr-2 w-3"></span> Reset Wallpaper
</button>
</div>
</div>
{/if}
<!-- 1. Window & UI Control -->
<div class="w-full flex flex-col gap-1 border-b border-surface-500/20 pb-2 mb-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Window & UI</label>
<div class="grid grid-cols-2 gap-1">
<button
onclick={() => native.window_control({ action: 'maximize' })}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
<span class="fas fa-expand mr-1"></span> Maximize
</button>
<button
onclick={() => native.window_control({ action: 'devtools', value: true })}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
<span class="fas fa-code mr-1"></span> DevTools
</button>
<button
onclick={() => handle_system_action(native.window_control({ action: 'kiosk', value: true }), 'Kiosk Mode')}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
<span class="fas fa-desktop mr-1"></span> Kiosk
</button>
<button
onclick={() => handle_system_action(native.set_wallpaper({ path: $ae_loc.site_header_image_path }), 'Wallpaper')}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
disabled={!$ae_loc.site_header_image_path}
>
<span class="fas fa-image mr-1"></span> Set Wallpaper
</button>
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 text-error-500">Power</label>
<div class="grid grid-cols-1 gap-1">
<button
onclick={() => show_power_confirm = { action: 'reboot', label: 'Reboot Laptop' }}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start"
>
<span class="fas fa-sync-alt mr-2 w-3"></span> Reboot
</button>
<button
onclick={() => show_power_confirm = { action: 'shutdown', label: 'Shutdown Laptop' }}
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start"
>
<span class="fas fa-power-off mr-2 w-3"></span> Shutdown
</button>
</div>
</div>
</div>
<!-- 2. System & Power -->
<div class="w-full flex flex-col gap-1 border-b border-surface-500/20 pb-2 mb-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">System & Hardware</label>
<div class="grid grid-cols-2 gap-1">
<button
onclick={() => handle_system_action(native.set_display_layout({ mode: 'extend' }), 'Extend Display')}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
<span class="fas fa-columns mr-1"></span> Extend Displays
</button>
<button
onclick={() => handle_system_action(native.manage_recording({ action: 'status' }), 'Record Status')}
class="btn btn-xs preset-tonal-surface hover:preset-filled-primary-500"
>
<span class="fas fa-video mr-1"></span> Recording...
</button>
<button
onclick={() => show_power_confirm = { action: 'reboot', label: 'Reboot Laptop' }}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500"
>
<span class="fas fa-sync-alt mr-1"></span> Reboot
</button>
<button
onclick={() => show_power_confirm = { action: 'shutdown', label: 'Shutdown Laptop' }}
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500"
>
<span class="fas fa-power-off mr-1"></span> Shutdown
</button>
</div>
</div>
<!-- 3. Folders -->
<div class="w-full flex flex-col gap-1 border-b border-surface-500/20 pb-2 mb-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Operational Folders</label>
<div class="grid grid-cols-1 gap-1">
<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"
>
<span class="fas fa-folder-open mr-2 w-4"></span> Open Local Cache
</button>
<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"
>
<span class="fas fa-folder-open mr-2 w-4"></span> Open Host Temp
</button>
</div>
</div>
<!-- 4. Presentation Remote Control (Phase 5) -->
<div class="w-full flex flex-col gap-1 border-b border-surface-500/20 pb-2 mb-1">
<div class="flex flex-row justify-between items-center px-1">
<label class="text-[9px] font-bold uppercase opacity-50">Presentation Remote</label>
<select bind:value={remote_app} class="select select-sm py-0 h-5 text-[9px] w-24 preset-tonal-surface">
<option value="powerpoint">PowerPoint</option>
<option value="keynote">Keynote</option>
</select>
</div>
<div class="grid grid-cols-4 gap-1">
<button onclick={() => handle_remote_control('prev')} class="btn btn-sm preset-tonal-secondary" title="Previous Slide">
<span class="fas fa-step-backward"></span>
</button>
<button onclick={() => handle_remote_control('start')} class="btn btn-sm preset-tonal-success" title="Start/Resume Slideshow">
<span class="fas fa-play"></span>
</button>
<button onclick={() => handle_remote_control('stop')} class="btn btn-sm preset-tonal-error" title="Stop Slideshow">
<span class="fas fa-stop"></span>
</button>
<button onclick={() => handle_remote_control('next')} class="btn btn-sm preset-tonal-secondary" title="Next Slide">
<span class="fas fa-step-forward"></span>
</button>
</div>
{#if remote_status}
<div class="text-[9px] text-center italic animate-pulse text-primary-500">{remote_status}</div>
{/if}
</div>
<!-- 5. Manual Command -->
<div class="w-full flex flex-col gap-1">
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Terminal Access</label>
<div class="flex gap-1">
<input
type="text"
bind:value={$events_sess.launcher.manual_cmd}
placeholder="e.g. ls -la or whoami"
placeholder="ls -la"
class="input input-sm grow text-[10px] preset-tonal-surface h-7"
/>
<button
onclick={async () => {
test_cmd_result = 'Running...';
const res = await native.run_cmd({ cmd: $events_sess.launcher.manual_cmd || 'whoami && uptime' });
if (res && typeof res === 'object') {
test_cmd_result = (res as any).stdout || (res as any).error || 'No Output';
if ((res as any).stderr) test_cmd_result += `\nStderr: ${(res as any).stderr}`;
} else {
test_cmd_result = String(res);
}
const res = await native.run_cmd({ cmd: $events_sess.launcher.manual_cmd || 'uptime' });
test_cmd_result = (res as any).stdout || (res as any).error || 'No Output';
}}
class="btn btn-sm preset-filled-secondary hover:preset-filled-primary-500 text-[10px] h-7"
>
Run
</button>
>Run</button>
</div>
{#if test_cmd_result}
<div class="relative">
<pre class="text-[9px] bg-black text-green-500 p-2 mt-1 overflow-x-auto rounded w-full border border-surface-500/50 min-h-12 max-h-32 shadow-inner">{test_cmd_result}</pre>
<button
onclick={() => test_cmd_result = ''}
class="absolute top-2 right-2 text-white/30 hover:text-white text-[8px]"
>Clear</button>
</div>
<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>
{/if}
</div>
</div>
</section>
{/if}
{/if}
</Launcher_Cfg_Section>
<!-- 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">
<div class="card p-6 w-full max-w-sm preset-filled-surface-100-900 border border-error-500 shadow-2xl">
<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">Confirm System Action</h4>
<p class="text-sm opacity-80 mb-6">
Are you sure you want to <strong>{show_power_confirm.action}</strong> this host machine?
This will terminate the native application and any active presentations.
</p>
<div class="flex justify-end gap-2">
<button onclick={() => show_power_confirm = null} class="btn btn-sm preset-tonal-surface">Cancel</button>
@@ -247,5 +205,3 @@
</div>
</div>
{/if}

View File

@@ -1,65 +1,75 @@
<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';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
</script>
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__screen_saver}
class="screen_saver w-full preset-outlined-surface-300-700 transition-all"
<Launcher_Cfg_Section
title="Digital Screen Saver"
icon="fa-id-badge"
bind:state={$events_loc.launcher.section_state__screen_saver}
{on_expand}
description="Idle: {($events_loc.launcher.idle_timer / 60000).toFixed(1)}m | Auto-Posters"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__screen_saver =
!$events_loc.launcher.show_section__screen_saver;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__screen_saver}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Screen Saver
</span>
{$events_loc.launcher.idle_timer
? ($events_loc.launcher.idle_timer / 60000).toPrecision(4) + ' min'
: '-- not set --'}
</button>
</h3>
<!-- 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">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Screen Saver Timers (ms)</label>
<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">
<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"
/>
</div>
<div class="flex justify-between items-center 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"
/>
</div>
<div class="flex justify-between items-center gap-4">
<span class="text-[10px] opacity-60">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"
/>
</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">
<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>
</div>
<p class="text-[9px] opacity-40 italic">
The screen saver automatically rotates digital posters when no activity is detected for the specified time.
</p>
</div>
{/if}
<div
class="flex flex-col gap-1 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__screen_saver}
>
<label class="flex flex-row gap-1 items-center justify-start text-sm">
<span class="w-36">Idle Time (ms):</span>
<input
type="number"
min={3000}
bind:value={$events_loc.launcher.idle_timer}
class="input input-sm w-28 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-sm">
<span class="w-36">Cycle Check (ms):</span>
<input
type="number"
min={500}
bind:value={$events_loc.launcher.idle_cycle}
class="input input-sm w-28 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-sm">
<span class="w-36">Image Change Period (ms):</span>
<input
type="number"
min={750}
bind:value={$events_loc.launcher.idle_loop_period}
class="input input-sm w-28 text-right preset-tonal-surface"
/>
</label>
<div class="text-center">
<p class="text-[8px] opacity-40 italic uppercase tracking-tighter">
Applies to "Poster" session types only
</p>
</div>
</div>
</section>
</Launcher_Cfg_Section>

View File

@@ -0,0 +1,102 @@
<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';
interface Props {
title: string;
icon: string;
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();
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);
}
const is_open = $derived(state !== 'collapsed');
</script>
<section
class="w-full transition-all duration-300 border rounded-lg overflow-hidden mb-2 { !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 ? 'bg-surface-500/5' : '' } { state === 'auto' ? 'bg-primary-500/10' : '' } { state === 'pinned' ? 'bg-warning-500/10' : '' }"
onclick={toggle_expand}
>
<div class="flex items-center gap-3">
<span class="fas {icon} w-5 text-center opacity-70 { state === 'auto' ? 'text-primary-500' : '' } { state === 'pinned' ? 'text-warning-500' : '' }"></span>
<div class="flex flex-col">
<span class="text-sm font-bold tracking-tight uppercase { !is_open ? 'opacity-50' : '' }">{title}</span>
{#if description && !is_open}
<span class="text-[9px] opacity-40 italic truncate max-w-[180px]">{description}</span>
{/if}
</div>
</div>
<div class="flex items-center gap-2">
<!-- Pin Toggle -->
<button
onclick={toggle_pin}
class="btn btn-icon btn-xs transition-all hover:scale-110"
class:opacity-20={state !== 'pinned'}
class:text-warning-500={state === 'pinned'}
title={state === 'pinned' ? 'Unpin Section' : 'Pin Section (Stay open)'}
>
<span class="fas fa-thumbtack text-[10px]"></span>
</button>
<!-- Collapse Icon -->
<span class="fas transition-transform duration-300 opacity-30"
class:fa-chevron-right={!is_open}
class:fa-chevron-down={is_open}
class:rotate-180={is_open}
></span>
</div>
</header>
<!-- Content -->
{#if is_open}
<div transition:slide={{ duration: 300 }} class="p-3 bg-white/5 dark:bg-black/5">
{#if $ae_loc.edit_mode}
<div class="mb-2 flex justify-between items-center px-1">
<span class="text-[8px] uppercase font-bold tracking-widest text-primary-500/60 flex items-center gap-1">
<span class="fas fa-edit"></span> Technical Mode
</span>
{#if state === 'pinned'}
<span class="badge variant-filled-warning text-[8px] uppercase">Pinned</span>
{/if}
</div>
{/if}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-1 gap-4 lg:gap-3">
{@render children?.()}
</div>
</div>
{/if}
</section>

View File

@@ -1,85 +1,104 @@
<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';
interface Props {
on_expand?: () => void;
}
let { on_expand }: Props = $props();
</script>
{#if $ae_loc.is_native}
<section
class:preset-outlined-warning-300-700={$events_loc.launcher.show_section__sync_timers}
class="sync_timers w-full preset-outlined-surface-300-700 transition-all mb-2"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__sync_timers =
!$events_loc.launcher.show_section__sync_timers;
}}
class="btn btn-sm w-full justify-between"
>
<span>
{#if $events_loc.launcher.show_section__sync_timers}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></span>
{/if}
Native Sync Timers
</span>
</button>
</h3>
<Launcher_Cfg_Section
title="Sync Engine & Timers"
icon="fa-sync"
bind:state={$events_loc.launcher.section_state__sync_timers}
{on_expand}
description="Prefix: {$ae_loc.native_device?.hash_prefix_length || 2} | Loops: Active"
>
<!-- Content omitted for brevity, preserved in file -->
{#if $ae_loc.native_device}
<div class="grid grid-cols-1 gap-3">
<!-- Technical Timers (Edit Mode Only) -->
{#if $ae_loc.edit_mode}
<div class="flex flex-col gap-2">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Polling Periods (ms)</label>
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60">Event Data</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_loop_period}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60">Device Config</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_device_loop_period}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60">Room/Location</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_location_loop_period}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60">Session Loop</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_session_loop_period}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
</div>
</div>
</div>
<div
class="flex flex-col gap-1 items-center justify-start w-full p-2"
class:hidden={!$events_loc.launcher.show_section__sync_timers}
>
{#if $ae_loc.native_device}
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Event Sync (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Device Heartbeat (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_device_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Location Refresh (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_location_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full">
<span class="grow opacity-70">Session Scan (ms):</span>
<input
type="number"
bind:value={$ae_loc.native_device.check_event_session_loop_period}
class="input input-sm w-24 text-right preset-tonal-surface"
/>
</label>
<label class="flex flex-row gap-1 items-center justify-start text-xs w-full border-t border-surface-500/20 pt-1 mt-1">
<span class="grow font-semibold text-primary-500">Hash Prefix Length:</span>
<input
type="number"
min="1"
max="8"
bind:value={$ae_loc.native_device.hash_prefix_length}
class="input input-sm w-24 text-right preset-tonal-surface font-bold"
/>
</label>
<div class="text-[9px] text-gray-500 mt-1 italic w-full text-right">
* Prefix: {($ae_loc.native_device.hash_prefix_length || 2)} chars. Reload required.
<div class="flex flex-col gap-1 mt-1 border-t border-surface-500/10 pt-2">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Cache Structure</label>
<div class="flex items-center justify-between px-1">
<span class="text-[9px]">Hash Prefix Length</span>
<select
bind:value={$ae_loc.native_device.hash_prefix_length}
class="select select-sm h-6 py-0 text-[10px] w-16 preset-tonal-surface"
>
<option value={1}>1 char</option>
<option value={2}>2 chars</option>
<option value={3}>3 chars</option>
</select>
</div>
<p class="text-[8px] opacity-40 italic mt-1">* Prefix change requires a full app reload to take effect.</p>
</div>
{:else}
<div class="text-xs text-error-500 italic">No device config hydrated.</div>
<!-- Read Only Summary (Normal Mode) -->
<div class="bg-surface-500/5 p-2 rounded border border-surface-500/10 flex flex-col gap-1">
<div class="flex justify-between text-[9px] opacity-60 font-mono">
<span>Event Sync:</span>
<span>{($ae_loc.native_device.check_event_loop_period / 1000).toFixed(1)}s</span>
</div>
<div class="flex justify-between text-[9px] opacity-60 font-mono">
<span>Room Monitor:</span>
<span>{($ae_loc.native_device.check_event_location_loop_period / 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">
<span>Prefix Sharding:</span>
<span>{$ae_loc.native_device.hash_prefix_length || 2} chars</span>
</div>
</div>
<div class="text-center">
<p class="text-[8px] opacity-40 italic">Enable Edit Mode to adjust polling intervals.</p>
</div>
{/if}
</div>
</section>
{/if}
{:else}
<div class="text-center p-4 opacity-50 italic text-xs">
Device configuration not loaded.
</div>
{/if}
</Launcher_Cfg_Section>

View File

@@ -0,0 +1,203 @@
<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';
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('');
// --- 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));
is_loading = false;
action_status = `Finished: ${label}`;
setTimeout(() => action_status = '', 3000);
}
// Modal state for destructive actions
let show_confirm = $state(false);
</script>
<Launcher_Cfg_Section
title="Template Section"
icon="fa-cubes"
bind:state={$events_loc.launcher.section_state__template}
{on_expand}
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">
{action_status}
</div>
{/if}
<!-- B. COMMON GRID SECTION (Read Only / High Level) -->
<div class="flex flex-col gap-2">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Standard Actions</label>
<!-- Action Buttons -->
<div class="grid grid-cols-2 gap-1">
<button
onclick={() => handle_test_action('Primary')}
class="btn btn-xs preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-bolt mr-2 w-3 text-center"></span> Primary
</button>
<button
onclick={() => handle_test_action('Secondary')}
class="btn btn-xs preset-tonal-secondary hover:preset-filled-secondary-500 justify-start"
>
<span class="fas fa-cog mr-2 w-3 text-center"></span> 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">
<input type="checkbox" bind:checked={toggle_val} class="checkbox checkbox-sm" />
<span class="text-xs group-hover:text-primary-500 transition-colors">Toggle Feature Alpha</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" name="demo" value="a" class="radio radio-sm" />
<span class="text-xs group-hover:text-primary-500 transition-colors">Mode A</span>
</label>
</div>
</div>
<!-- C. STATUS & GAUGES SECTION -->
<div class="flex flex-col gap-2">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Current Status</label>
<div class="flex flex-col gap-2 p-2 border border-surface-500/10 rounded-lg">
<div class="flex justify-between items-center">
<span class="text-[10px] font-medium">Engine Health</span>
<span class="badge variant-filled-success text-[8px] uppercase">Stable</span>
</div>
<!-- Progress / Gauge Example -->
<div class="flex flex-col gap-1">
<div 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">
<div class="h-full bg-success-500 transition-all duration-1000" style="width: 45%"></div>
</div>
</div>
</div>
<button
onclick={() => handle_test_action('Refresh')}
class="btn btn-xs preset-outlined-surface-500 w-full text-[10px]"
>
<span class="fas fa-sync mr-2" class:fa-spin={is_loading}></span>
Refresh State
</button>
</div>
<!-- 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">
<!-- Dangerous Actions -->
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 text-warning-500">System Config</label>
<button
onclick={() => show_confirm = true}
class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 justify-start"
>
<span class="fas fa-exclamation-triangle mr-2 w-3"></span> Reset All
</button>
</div>
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 text-error-500">Danger Zone</label>
<button class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start">
<span class="fas fa-trash-alt mr-2 w-3"></span> Wipe Cache
</button>
</div>
</div>
<!-- Form Inputs -->
<div class="grid grid-cols-1 gap-2">
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Raw Settings</label>
<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"
/>
<select bind:value={select_val} class="select select-sm h-7 py-0 text-[10px] w-24 preset-tonal-surface">
<option value="option1">Global</option>
<option value="option2">Local</option>
</select>
</div>
</div>
<div class="flex flex-col gap-1">
<span class="text-[8px] opacity-60 ml-1">Threshold (ms)</span>
<input
type="number"
bind:value={number_input}
class="input input-sm text-[10px] h-7 preset-tonal-surface"
/>
</div>
</div>
<!-- Terminal / Output Log -->
<div class="flex flex-col gap-1">
<label class="text-[9px] font-bold uppercase opacity-50 ml-1">Debug Output</label>
<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">
[LOG] System Initialized
[INFO] Store synced with IndexedDB
[DEBUG] active_tab: template
[WARN] Latency detected in sync loop
</pre>
</div>
</div>
{/if}
</Launcher_Cfg_Section>
<!-- 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">
<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">
Are you sure you want to perform this test operation? This demonstrate the standard confirmation pattern.
</p>
<div class="flex justify-end gap-2">
<button onclick={() => show_confirm = false} class="btn btn-sm preset-tonal-surface">Cancel</button>
<button
onclick={() => { show_confirm = false; handle_test_action('Confirm'); }}
class="btn btn-sm preset-filled-warning"
>
Yes, Proceed
</button>
</div>
</div>
</div>
{/if}

View File

@@ -2,6 +2,12 @@
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';
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/');
@@ -24,7 +30,7 @@
if (res.success) {
download_result = res;
update_status = 'Update located/downloaded. ready to install.';
update_status = 'Update located/downloaded. Ready to install.';
} else {
update_status = `Failed: ${res.error}`;
}
@@ -37,42 +43,23 @@
async function handle_install() {
update_status = 'Initiating installation...';
// Note: Real installation logic in the Electron app will likely
// terminate the process or restart it.
alert('Installation logic is OS-specific. This will typically swap the application bundle and restart.');
}
</script>
{#if $ae_loc.is_native}
<section
class:preset-outlined-tertiary-300-700={$events_loc.launcher.show_section__updates}
class="updates w-full preset-outlined-surface-300-700 transition-all mb-2"
>
<h3 class="text-center mb-2 text-sm font-semibold w-full">
<button
onclick={() => {
$events_loc.launcher.show_section__updates =
!$events_loc.launcher.show_section__updates;
}}
class="btn btn-sm w-full justify-between"
>
<span class="flex items-center gap-2">
{#if $events_loc.launcher.show_section__updates}
<span class="fas fa-chevron-down text-[10px]"></span>
{:else}
<span class="fas fa-chevron-right text-[10px]"></span>
{/if}
Application Updates
</span>
<span class="badge variant-filled-tertiary text-[10px]">v1.0.0</span>
</button>
</h3>
<div
class="flex flex-col gap-2 p-2 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__updates}
>
<div class="w-full flex flex-col gap-2">
<Launcher_Cfg_Section
title="Application Updates"
icon="fa-cloud-download-alt"
bind:state={$events_loc.launcher.section_state__updates}
{on_expand}
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">
<label class="text-[9px] font-bold uppercase opacity-50">Source Type</label>
<div class="flex gap-2">
@@ -89,45 +76,46 @@
<input
type="text"
bind:value={update_path}
placeholder="Path to update directory or file"
placeholder="Path to update package"
class="input input-sm text-[10px] preset-tonal-surface h-7 w-full"
/>
{:else}
<input
type="text"
bind:value={update_url}
placeholder="URL to update package (.zip/.app)"
placeholder="URL to update package"
class="input input-sm text-[10px] preset-tonal-surface h-7 w-full"
/>
{/if}
<button
onclick={handle_check_update}
disabled={is_checking}
class="btn btn-sm preset-filled-tertiary hover:preset-filled-primary-500 text-[10px] w-full"
>
{#if is_checking}
<span class="fas fa-spinner fa-spin mr-2"></span> Checking...
{:else}
<span class="fas fa-cloud-download-alt mr-2"></span> Check for Updates
{/if}
</button>
{#if update_status}
<div class="text-[9px] text-center italic p-1 border border-surface-500/20 rounded bg-surface-500/5">
{update_status}
</div>
{/if}
{#if download_result}
<button
onclick={handle_install}
class="btn btn-sm preset-filled-success hover:preset-filled-primary-500 text-[10px] w-full animate-bounce"
>
<span class="fas fa-magic mr-2"></span> Install & Relaunch
</button>
{/if}
</div>
</div>
</section>
{/if}
{/if}
<!-- COMMON: Check Button -->
<button
onclick={handle_check_update}
disabled={is_checking}
class="btn btn-sm preset-filled-tertiary hover:preset-filled-primary-500 text-[10px] w-full"
>
{#if is_checking}
<span class="fas fa-spinner fa-spin mr-2"></span> Checking...
{:else}
<span class="fas fa-search mr-2"></span> Check for Updates
{/if}
</button>
{#if update_status}
<div class="text-[9px] text-center italic p-1 border border-surface-500/20 rounded bg-surface-500/5 mt-1">
{update_status}
</div>
{/if}
{#if download_result}
<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"
>
<span class="fas fa-magic mr-2"></span> Install & Relaunch
</button>
{/if}
</div>
</Launcher_Cfg_Section>

View File

@@ -1228,11 +1228,11 @@
</div>
<Drawer
class="bg-orange-100 opacity-90 hover:opacity-97 transition-all duration-1000 border border-gray-300 dark:border-gray-600 w-full md:w-96 lg:w-[28rem]"
class="bg-orange-100 opacity-90 hover:opacity-97 transition-all duration-1000 border border-gray-300 dark:border-gray-600 w-full md:w-96 lg:w-[32rem]"
placement="left"
transitionType="fly"
transitionParams={{
x: -450,
x: -520,
duration: 200,
easing: sineIn
}}

View File

@@ -35,6 +35,23 @@
// UI Tab State
let active_tab: 'system' | 'sync' | 'general' = $state('system');
/**
* 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
}
</script>
<div
@@ -92,9 +109,9 @@
{#if active_tab === 'system'}
<div class="animate-in fade-in slide-in-from-left-2 duration-300">
{#if $ae_loc.is_native}
<Launcher_Cfg_Health />
<Launcher_Cfg_Native_OS />
<Launcher_Cfg_Updates />
<Launcher_Cfg_Health on_expand={() => handle_section_expand('health')} />
<Launcher_Cfg_Native_OS on_expand={() => handle_section_expand('native_os')} />
<Launcher_Cfg_Updates on_expand={() => handle_section_expand('updates')} />
{:else}
<div class="card p-8 text-center opacity-50 italic text-sm">
Native OS features are only available when running in the Aether Desktop app.
@@ -105,16 +122,16 @@
{#if active_tab === 'sync'}
<div class="animate-in fade-in slide-in-from-bottom-2 duration-300">
<Launcher_Cfg_Sync_Timers />
<Launcher_Cfg_Sync_Timers on_expand={() => handle_section_expand('sync_timers')} />
</div>
{/if}
{#if active_tab === 'general'}
<div class="animate-in fade-in slide-in-from-right-2 duration-300">
<Launcher_Cfg_Controller />
<Launcher_Cfg_App_Modes />
<Launcher_Cfg_Screen_Saver />
<Launcher_Cfg_Local_Actions />
<Launcher_Cfg_Controller on_expand={() => handle_section_expand('controller')} />
<Launcher_Cfg_App_Modes on_expand={() => handle_section_expand('app_modes')} />
<Launcher_Cfg_Screen_Saver on_expand={() => handle_section_expand('screen_saver')} />
<Launcher_Cfg_Local_Actions on_expand={() => handle_section_expand('local_actions')} />
</div>
{/if}