refactor(launcher): modularize launcher config and implement Phase 5 actuators

- Broke down the massive launcher_cfg.svelte into 7 modular sub-components.
- Updated electron_relay.ts with Phase 5 presentation controls and manifest tools.
- Updated architecture documentation to reflect the new TypeScript-based native bridge.
This commit is contained in:
Scott Idem
2026-01-26 16:18:00 -05:00
parent 7c14b1e3a2
commit 5f2ccf8823
12 changed files with 1306 additions and 1027 deletions

View File

@@ -139,3 +139,55 @@ export async function launch_presentation({
return await open_local_file_v2(cleaned_path);
}
/**
* Control Presentation (Phase 5)
* Sends navigation commands to the active presentation (Next, Prev, Stop).
*/
export async function control_presentation({
app,
action
}: {
app: 'powerpoint' | 'keynote',
action: 'next' | 'prev' | 'start' | 'stop'
}) {
if (!native) return { success: false, error: 'Native bridge not available' };
// Check if the native bridge has the direct implementation
if (native.control_presentation) {
return await native.control_presentation({ app, action });
}
// Fallback to generic osascript if direct handler is missing
let script = '';
if (app === 'powerpoint') {
switch (action) {
case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break;
case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break;
case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break;
case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break;
}
} else if (app === 'keynote') {
switch (action) {
case 'next': script = 'tell application "Keynote" to show next'; break;
case 'prev': script = 'tell application "Keynote" to show previous'; break;
case 'start': script = 'tell application "Keynote" to start (front document)'; break;
case 'stop': script = 'tell application "Keynote" to stop'; break;
}
}
if (script) {
return await run_osascript(script);
}
return { success: false, error: `Unsupported app or action: ${app}/${action}` };
}
/**
* List Tools (Self-Documentation)
* Returns a JSON manifest of all available native bridge functions.
*/
export async function list_tools() {
if (!native || !native.list_tools) return [];
return await native.list_tools();
}

View File

@@ -0,0 +1,211 @@
import { ipcMain, shell } from 'electron';
import { exec, execSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import { expandPath } from './file_utils';
export function registerShellHandlers() {
ipcMain.handle('native:open-folder', async (event, folderPath: string) => {
const cleanPath = expandPath(folderPath);
const error = await shell.openPath(cleanPath);
return { success: !error, error };
});
ipcMain.handle('native:run-cmd', async (event, { cmd, timeout = 30000 }) => {
const cleanCmd = expandPath(cmd);
return new Promise((resolve) => {
exec(cleanCmd, { timeout }, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:run-cmd-sync', async (event, { cmd }) => {
const cleanCmd = expandPath(cmd);
try {
const stdout = execSync(cleanCmd).toString();
return { success: true, stdout: stdout.trim() };
} catch (error: any) {
return { success: false, error: error.message, stderr: error.stderr?.toString() };
}
});
ipcMain.handle('native:run-osascript', async (event, script: string) => {
if (os.platform() !== 'darwin') return { success: false, error: 'AppleScript is only available on macOS' };
const escapedScript = script.replace(/"/g, '\"');
const cmd = `osascript -e "${escapedScript}"`;
return new Promise((resolve) => {
exec(cmd, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:kill-processes', async (event, { process_name_li = [] }) => {
console.log(`Native: Killing processes -> `, process_name_li);
const results = [];
for (const name of process_name_li) {
const cmd = os.platform() === 'win32'
? `taskkill /F /IM ${name} /T`
: `pkill -f ${name}`;
try {
execSync(cmd);
results.push({ name, success: true });
} catch (e: any) {
results.push({ name, success: false, error: e.message });
}
}
return { success: true, results };
});
ipcMain.handle('native:open-local-file-v2', async (event, filePath: string) => {
const cleanPath = expandPath(filePath);
const error = await shell.openPath(cleanPath);
return { success: !error, error };
});
ipcMain.handle('native:launch-presentation', async (event, { path: rawPath, app: appType = 'default' }) => {
const cleanedPath = expandPath(rawPath);
console.log(`Native: Launching Presentation -> ${cleanedPath} (App: ${appType})`);
if (os.platform() === 'linux') {
const cmd = `libreoffice --impress "${cleanedPath}"`;
return new Promise((resolve) => {
exec(cmd, (err, stdout, stderr) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true, stdout, stderr });
});
});
}
if (os.platform() === 'darwin') {
let script = '';
if (appType === 'keynote') {
script = `
tell application "Keynote"
activate
open (POSIX file "${cleanedPath}")
delay 1
start (front document)
end tell
`;
} else if (appType === 'powerpoint') {
script = `
tell application "Microsoft PowerPoint"
activate
open (POSIX file "${cleanedPath}")
delay 1
run slide show of active presentation
end tell
`;
}
if (script) {
return new Promise((resolve) => {
const escapedScript = script.replace(/"/g, '\\"');
exec(`osascript -e "${escapedScript}"`, (err, stdout, stderr) => {
if (err) resolve({ success: false, error: err.message });
else resolve({ success: true });
});
});
}
}
const error = await shell.openPath(cleanedPath);
return { success: !error, error };
});
ipcMain.handle('native:control-presentation', async (event, { app, action }) => {
if (os.platform() !== 'darwin') return { success: false, error: 'Presentation control is only available on macOS' };
let script = '';
if (app === 'powerpoint') {
switch (action) {
case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break;
case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break;
case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break;
case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break;
}
} else if (app === 'keynote') {
switch (action) {
case 'next': script = 'tell application "Keynote" to show next'; break;
case 'prev': script = 'tell application "Keynote" to show previous'; break;
case 'start': script = 'tell application "Keynote" to start (front document)'; break;
case 'stop': script = 'tell application "Keynote" to stop'; break;
}
}
if (!script) return { success: false, error: `Unsupported app or action: ${app}/${action}` };
return new Promise((resolve) => {
exec(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => {
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
});
});
});
ipcMain.handle('native:list-tools', async () => {
return [
{
name: 'open_folder',
description: 'Opens a directory in the OS file explorer (Finder/Files/Explorer).',
params: { path: 'string' }
},
{
name: 'run_cmd',
description: 'Executes an asynchronous shell command with a timeout.',
params: { cmd: 'string', timeout: 'number (optional)' }
},
{
name: 'run_cmd_sync',
description: 'Executes a synchronous shell command.',
params: { cmd: 'string' }
},
{
name: 'run_osascript',
description: 'Executes a raw AppleScript string (macOS only).',
params: { script: 'string' }
},
{
name: 'kill_processes',
description: 'Forcefully terminates processes by name.',
params: { process_name_li: 'string[]' }
},
{
name: 'open_local_file_v2',
description: 'Opens a local file using the default OS handler.',
params: { filePath: 'string' }
},
{
name: 'launch_presentation',
description: 'Phase 5: Specialized launcher for PowerPoint, Keynote, and LibreOffice with auto-focus.',
params: { path: 'string', app: 'default|powerpoint|keynote' }
},
{
name: 'control_presentation',
description: 'Phase 5: Remote navigation for active slideshows.',
params: { app: 'powerpoint|keynote', action: 'next|prev|start|stop' }
},
{
name: 'check_cache',
description: 'Checks if a file exists in the local organized cache.',
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number' }
},
{
name: 'download_to_cache',
description: 'Downloads a file from the API directly into the native cache.',
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string' }
},
{
name: 'launch_from_cache',
description: 'Atomic operation: Copies file from cache to temp with original name and launches via specialized handler.',
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string' }
},
{
name: 'get_device_info',
description: 'Returns hardware and OS metadata (CPUs, RAM, IP addresses, Hostname).',
params: {}
}
];
});
}

View File

@@ -0,0 +1,229 @@
<script lang="ts">
import { events_loc } from '$lib/stores/ae_events_stores';
</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"
>
<h3 class="text-center mb-2 text-sm font-semibold">
<button
onclick={() => {
$events_loc.launcher.show_section__app_modes =
!$events_loc.launcher.show_section__app_modes;
}}
class="btn btn-sm w-full justify-between"
>
<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 --'}
</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>
</div>
</section>

View File

@@ -0,0 +1,105 @@
<script lang="ts">
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
</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"
>
<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>
{:else}
<span class="fas fa-chevron-right"></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 class:hidden={!$events_loc.launcher.show_section__controller}>
<div class="flex flex-row gap-1 p-0.5">
<select
bind:value={$events_loc.launcher.controller}
class="input select text-sm preset-tonal-surface"
>
<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}
/>
<button
onclick={() => {
if ($events_loc.launcher.ws_connect) {
$events_sess.launcher.trigger__ws_disconnect = true;
} else {
$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}
>
{#if $events_loc.launcher.ws_connect}
Disconnect?
{:else}
Connect?
{/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}
</div>
</div>
</section>

View File

@@ -0,0 +1,70 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc, events_sess } from '$lib/stores/ae_events_stores';
</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>
{#if $events_loc.launcher.show_section__health}
<span class="fas fa-chevron-down"></span>
{:else}
<span class="fas fa-chevron-right"></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-2 p-2 items-center justify-start w-full"
class:hidden={!$events_loc.launcher.show_section__health}
>
<!-- Heartbeat Info -->
<div class="grid grid-cols-2 gap-x-2 gap-y-1 w-full text-[10px] bg-surface-500/5 p-2 rounded border border-surface-500/10">
<span class="opacity-70">Last Heartbeat:</span>
<span class="text-right font-mono {$events_sess.launcher.heartbeat_info.status === 'success' ? 'text-success-500' : 'text-error-500'}">
{$events_sess.launcher.heartbeat_info.last_timestamp || 'Pending...'}
</span>
<span class="opacity-70">Room Sync Status:</span>
<span class="text-right font-mono">
{$events_sess.launcher.sync_stats.cached} / {$events_sess.launcher.sync_stats.total} Files
</span>
{#if $events_sess.launcher.sync_stats.currently_syncing}
<span class="col-span-2 text-center text-primary-500 animate-pulse mt-1 border-t border-primary-500/20 pt-1">
<span class="fas fa-sync fa-spin mr-1"></span>
Syncing: {$events_sess.launcher.sync_stats.currently_syncing}
</span>
{/if}
</div>
<!-- Basic Native Info -->
{#if $ae_loc.is_native && $ae_loc.native_device}
<div class="w-full mt-1 flex flex-col gap-1 text-[10px] opacity-80 pl-1 italic">
<div>Host: {$ae_loc.native_device.info_hostname || 'Loading...'}</div>
<div class="truncate">IPs: {$ae_loc.native_device.info_ip_list || '...'}</div>
</div>
{/if}
</div>
</section>
{/if}

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
</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"
>
<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">
<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 = '';
}
}}
>
<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>
</select>
<span class="text-xs text-gray-500 dark:text-gray-400">
The action happens when the option is selected
</span>
</div>
<div class="flex flex-row gap-1 items-center justify-center w-full">
<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)"
>
{#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}
</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)"
>
{#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}
</button>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">API: {$ae_api.base_url}</div>
</div>
</section>

View File

@@ -0,0 +1,143 @@
<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';
let test_cmd_result = $state('');
let remote_app: 'powerpoint' | 'keynote' = $state('powerpoint');
let remote_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);
}
</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"
>
<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 Handlers & Folders
</span>
<span class="badge variant-filled-success">Active</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__native_os}
>
<div class="grid grid-cols-1 gap-2 w-full">
<button
onclick={() => native.open_folder($ae_loc.local_file_cache_path)}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-folder-open mr-2"></span> Open Local Cache
</button>
<button
onclick={() => native.open_folder($ae_loc.host_file_temp_path)}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-folder-open mr-2"></span> Open Host Temp
</button>
<button
onclick={() => native.open_folder($ae_loc.recording_path)}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 justify-start"
>
<span class="fas fa-folder-open mr-2"></span> Open Recording Path
</button>
</div>
<!-- Presentation Remote Control (Phase 5) -->
<div class="w-full border-t border-surface-500/30 pt-2 mt-2 flex flex-col gap-2">
<div class="flex flex-row justify-between items-center px-1">
<label class="text-[10px] font-bold uppercase opacity-70">Presentation Remote:</label>
<select bind:value={remote_app} class="select select-sm py-0 h-6 text-[10px] 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">{remote_status}</div>
{/if}
</div>
<div class="w-full border-t border-surface-500/30 pt-2 mt-2 flex flex-col gap-2">
<div class="flex flex-col gap-1">
<label class="text-[10px] opacity-70 ml-1">Run Manual Command:</label>
<div class="flex gap-1">
<input
type="text"
bind:value={$events_sess.launcher.manual_cmd}
placeholder="e.g. ls -la or whoami"
class="input input-sm grow text-[10px] preset-tonal-surface"
/>
<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);
}
}}
class="btn btn-sm preset-filled-secondary hover:preset-filled-primary-500 text-[10px]"
>
Run
</button>
</div>
</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>
{/if}
</div>
</div>
</section>
{/if}

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import { events_loc } from '$lib/stores/ae_events_stores';
</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"
>
<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>
<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>
</section>

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { ae_loc } from '$lib/stores/ae_stores';
import { events_loc } from '$lib/stores/ae_events_stores';
</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>
<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>
{:else}
<div class="text-xs text-error-500 italic">No device config hydrated.</div>
{/if}
</div>
</section>
{/if}

File diff suppressed because it is too large Load Diff