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:
@@ -7,39 +7,37 @@ The Aether Native App serves as the OS-level bridge for the SvelteKit frontend.
|
||||
|
||||
## 2. The Three-Layer Architecture
|
||||
|
||||
### 2.1 The Engine (`electron_native.js`)
|
||||
* **Process:** Main Process (Node.js)
|
||||
### 2.1 The Engine (`src/main/*.ts`)
|
||||
* **Process:** Main Process (Node.js/TypeScript)
|
||||
* **Role:** Performs the actual OS-level operations.
|
||||
* **Key Responsibilities:**
|
||||
* Executing shell commands via `child_process`.
|
||||
* Managing the permanent Hashed Cache (`file_cache/`).
|
||||
* Performing atomic "Safe Handover" copies to the operational `temp/` directory.
|
||||
* Resolving global path placeholders like `[home]` and `[tmp]`.
|
||||
* **Shell Handlers (`shell_handlers.ts`)**: Executing shell commands, AppleScripts, and process management.
|
||||
* **File Handlers (`file_handlers.ts`)**: Managing the permanent Hashed Cache (`file_cache/`) and atomic Safe Handover.
|
||||
* Resolving global path placeholders like `[home]` and `[tmp]` via `file_utils.ts`.
|
||||
|
||||
### 2.2 The Bridge (`preload.js`)
|
||||
### 2.2 The Bridge (`src/preload/index.ts`)
|
||||
* **Process:** Preload Script
|
||||
* **Role:** The security gatekeeper.
|
||||
* **Key Responsibilities:**
|
||||
* Uses Electron's `contextBridge` to securely expose specific Main Process functions to the UI.
|
||||
* Uses Electron's `contextBridge` to securely expose Main Process functions to the UI.
|
||||
* Exposes the `window.aetherNative` object.
|
||||
* Ensures that only whitelisted IPC channels can be triggered by the renderer.
|
||||
|
||||
### 2.3 The Messenger (`electron_relay.ts`)
|
||||
### 2.3 The Messenger (`src/lib/electron/electron_relay.ts`)
|
||||
* **Process:** Renderer Process (SvelteKit)
|
||||
* **Role:** The TypeScript API used by Svelte components.
|
||||
* **Key Responsibilities:**
|
||||
* Detecting if the app is running in "Native Mode".
|
||||
* Providing a clean, typed interface for native calls.
|
||||
* Implementing "Smart Fallbacks" (e.g., resolving placeholders UI-side if the bridge is outdated).
|
||||
* Standardizing IPC calls to snake_case.
|
||||
* Implementing "Smart Fallbacks" (e.g., UI-side placeholder resolution if the bridge is inactive).
|
||||
|
||||
---
|
||||
|
||||
## 3. Core Lifecycle
|
||||
|
||||
1. **Seed Phase:** The Electron shell reads `~/seed.json` to identify the device.
|
||||
2. **Hydration Phase:** The shell authenticates with the Aether V3 API to pull device-specific settings (room assignment, sync timers).
|
||||
3. **Injection Phase:** The shell injects the **JWT (Auth Token)** and **Native Config** into the SvelteKit environment.
|
||||
4. **Observation Phase:** `LauncherBackgroundSync.svelte` force-hydrates absolute OS paths (Home/Tmp) into the global `ae_loc` store for immediate use.
|
||||
1. **Seed Phase:** The Electron shell reads `resources/seed_config.json` to identify the device.
|
||||
2. **Hydration Phase:** The shell authenticates with the Aether V3 API to pull device-specific settings.
|
||||
3. **Injection Phase:** The shell injects the **JWT** and **Native Config** into the SvelteKit environment.
|
||||
4. **Observation Phase:** Background sync loops manage file warming and heartbeat telemetry.
|
||||
|
||||
---
|
||||
|
||||
@@ -48,17 +46,20 @@ The Aether Native App serves as the OS-level bridge for the SvelteKit frontend.
|
||||
### File & Cache Management
|
||||
- `check_cache({hash})`: Verify if a file exists in the hidden hashed storage.
|
||||
- `download_to_cache({url, hash})`: Secure background download using the native API key.
|
||||
- `launch_from_cache({hash, filename})`: **Safe Handover** — copies from cache to temp with the original name and triggers the launcher.
|
||||
- `launch_from_cache({hash, filename})`: **Safe Handover** — copies from cache to temp with the original name and triggers the specialized launcher.
|
||||
|
||||
### OS Execution (The "Actuators")
|
||||
- `launch_presentation({path, app})`: Intelligent application selection.
|
||||
- `launch_presentation({path, app})`: Phase 5 specialized launcher with auto-focus (`activate`) and slideshow start.
|
||||
- **Linux:** Triggers `libreoffice --impress`.
|
||||
- **macOS:** Triggers AppleScript for Keynote/PowerPoint.
|
||||
- `run_cmd({cmd})`: Executes raw shell commands with automatic `[home]` and `[tmp]` resolution.
|
||||
- `kill_processes([names])`: Force-closes presentation apps to clean the podium between sessions.
|
||||
- **macOS:** Triggers AppleScript for Keynote or PowerPoint.
|
||||
- `control_presentation({app, action})`: Remote navigation for active decks.
|
||||
- **Actions:** `next`, `prev`, `start`, `stop`.
|
||||
- `run_cmd({cmd})`: Executes raw shell commands with automatic path resolution.
|
||||
- `kill_processes([names])`: Force-closes apps to clean the podium.
|
||||
|
||||
### System Metadata
|
||||
- `get_device_info()`: Provides OS version, CPU/RAM stats, and absolute system paths.
|
||||
### Metadata & Discovery
|
||||
- `get_device_info()`: Provides CPU/RAM stats and absolute system paths.
|
||||
- `list_tools()`: Returns a JSON manifest of all available native functions for self-documentation.
|
||||
|
||||
---
|
||||
|
||||
|
||||
187
documentation/AETHER_SVELTE5_PERFORMANCE_REVIEW.md
Normal file
187
documentation/AETHER_SVELTE5_PERFORMANCE_REVIEW.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Aether Svelte 5 App — Performance Review
|
||||
|
||||
Date: 2026-01-26
|
||||
|
||||
This document collects findings from a quick code review of the Aether Svelte 5 app (focused on `src/routes/+layout.svelte`, `src/routes/+page.svelte`, and the events launcher layout). It lists concrete performance issues observed, their likely impact, remediation recommendations (quick wins first), code examples for safe refactors, measurement steps, and a prioritized action plan.
|
||||
|
||||
**Scope**
|
||||
- Files inspected: `src/routes/+layout.svelte`, `src/routes/+page.svelte`, event launcher `+layout.svelte` and related store initialization logic.
|
||||
- Platform: SvelteKit with Vite (assumed from repo files). Builds use Tailwind CSS and manual global CSS.
|
||||
|
||||
---
|
||||
|
||||
## Summary of Key Findings
|
||||
|
||||
- Large synchronous imports at module evaluation time (notably `highlight.js` and its stylesheet) increase initial bundle size and run before first paint.
|
||||
- Many components and icon imports are static at top-level even when rendered conditionally (`Analytics`, `MyClipboard`, `E_app_debug_menu`, `E_app_sys_menu`, multiple Lucide icons). This inflates the client bundle.
|
||||
- Significant initialization and cache logic runs synchronously during module evaluation: store writes, cache expiration checks, IndexedDB operations, localStorage population, calls to `invalidateAll()` and conditional `window.location.reload()` paths — these can block initial render and cause reload loops or extra round trips.
|
||||
- Dexie `liveQuery` usages in nested layouts are eager and may execute multiple DB queries on route entry, causing CPU and I/O work up-front.
|
||||
- Multiple synchronous CSS imports (global CSS + highlight CSS) can delay first paint and increase transfer size.
|
||||
- Many localStorage/sessionStorage writes and store updates are done immediately and synchronously; iterating many keys on startup can stall the main thread.
|
||||
- Analytics, WebSocket connections, and external network resource usage may be triggered too early.
|
||||
|
||||
Impact: Slow Time to First Paint (TTFP), increased Time to Interactive (TTI), larger JS bundles, higher CPU main-thread blocking, worse Lighthouse/perceived performance.
|
||||
|
||||
---
|
||||
|
||||
## Concrete, Prioritized Recommendations
|
||||
|
||||
Priority ordering: Quick wins (low code risk) → Short-term (moderate refactor) → Long-term (architectural).
|
||||
|
||||
### Quick Wins (apply first)
|
||||
- Lazy-load `highlight.js` and its CSS in `onMount` only when highlighting is needed. This removes a heavy library and stylesheet from the initial bundle.
|
||||
- Dynamically import non-critical components (`Analytics`, debug menus, clipboard UI, large icon modules) when they will actually render.
|
||||
- Move non-UI, non-blocking initialization to `onMount` and/or `requestIdleCallback` so SSR rendering and first paint aren't blocked by client-only logic.
|
||||
- Guard `invalidateAll()` / `window.location.reload()` so they run only after a user-visible prompt or on explicit action; avoid automatic reload on subtle mismatches.
|
||||
|
||||
### Short-Term Changes (next 1–2 days)
|
||||
- Defer expensive Dexie `liveQuery` and database queries until their containing route/section mounts, or add limits/pagination to reduce initial query size.
|
||||
- Batch localStorage writes (use a single write or `requestIdleCallback`/`setTimeout(…,0)`), or store a single object rather than many keys.
|
||||
- Replace heavy icon imports with an icon-on-demand strategy (SVG sprite, `unplugin-icons`, or inlining only icons used on first paint).
|
||||
- Lazy-load large CSS (e.g., highlight theme) with dynamic import or by adding/removing link elements.
|
||||
|
||||
### Long-Term / Architectural
|
||||
- Split routes to ensure route-level code-splitting is effective — verify that `+layout` doesn't import many route-specific modules. Keep `+layout.svelte` as thin as possible.
|
||||
- Use a bundle analyzer to identify largest modules and tune dependencies.
|
||||
- Serve production build with proper HTTP caching, compression (Brotli), and HTTP/2 or HTTP/3.
|
||||
- Consider server-side rendering vs. partial hydration boundaries (islands) for heavy interactive regions.
|
||||
|
||||
---
|
||||
|
||||
## Concrete Code Examples
|
||||
|
||||
These are safe, copy-pasteable snippets for common quick fixes.
|
||||
|
||||
1) Lazy-load `highlight.js` in `src/routes/+layout.svelte` (move work to `onMount`):
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
let hljs: any;
|
||||
onMount(async () => {
|
||||
const mod = await import('highlight.js/lib/core');
|
||||
hljs = mod.default || mod;
|
||||
// register languages lazily
|
||||
const xml = (await import('highlight.js/lib/languages/xml')).default;
|
||||
const css = (await import('highlight.js/lib/languages/css')).default;
|
||||
const js = (await import('highlight.js/lib/languages/javascript')).default;
|
||||
const ts = (await import('highlight.js/lib/languages/typescript')).default;
|
||||
hljs.registerLanguage('xml', xml);
|
||||
hljs.registerLanguage('css', css);
|
||||
hljs.registerLanguage('javascript', js);
|
||||
hljs.registerLanguage('typescript', ts);
|
||||
// load stylesheet dynamically if needed
|
||||
await import('highlight.js/styles/github-dark.css');
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
2) Dynamic component import for `Analytics`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
let Analytics: any = null;
|
||||
let showAnalytics = false;
|
||||
|
||||
onMount(async () => {
|
||||
if (/* condition to show analytics, or simply true after idle */) {
|
||||
const mod = await import('$lib/app_components/analytics.svelte');
|
||||
Analytics = mod.default;
|
||||
showAnalytics = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if showAnalytics && Analytics}
|
||||
<svelte:component this={Analytics} bind:site_google_tracking_id={$ae_loc.site_google_tracking_id} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
3) Batch localStorage writes using `requestIdleCallback` fallback:
|
||||
|
||||
```ts
|
||||
function batchSetLocalStorage(obj: Record<string, any>) {
|
||||
const work = () => {
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {}
|
||||
}
|
||||
};
|
||||
if ('requestIdleCallback' in window) {
|
||||
(window as any).requestIdleCallback(work);
|
||||
} else {
|
||||
setTimeout(work, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4) Avoid automatic reloads — example guard:
|
||||
|
||||
```ts
|
||||
// Only force reload after user consent or if critical mismatch
|
||||
if (flag_reload) {
|
||||
const reloadKey = 'aether_reload_shown_v' + ($ae_loc?.ver ?? 'unknown');
|
||||
if (!localStorage.getItem(reloadKey)) {
|
||||
if (confirm('A new version is available. Reload now?')) {
|
||||
localStorage.setItem(reloadKey, '1');
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Measurement & Tooling
|
||||
|
||||
Run these locally to find concrete hotspots and bundle contributors.
|
||||
|
||||
- Bundle analysis (Vite):
|
||||
|
||||
```bash
|
||||
# Build and open analyzer (if plugin configured)
|
||||
npm run build
|
||||
npx vite build --sourcemap
|
||||
# Use source-map-explorer or webpack-bundle-analyzer (adapt if using Rollup)
|
||||
npx source-map-explorer dist/assets/*.js
|
||||
```
|
||||
|
||||
- Vite visualizer (install `rollup-plugin-visualizer` or `vite-plugin-visualizer`):
|
||||
|
||||
```bash
|
||||
# add plugin and run build with report
|
||||
# then open .html output from the plugin
|
||||
```
|
||||
|
||||
- Lighthouse / DevTools:
|
||||
- Run Lighthouse (Performance, TTI, Largest Contentful Paint, Main thread) in Chrome.
|
||||
- In Performance panel record page load to find long main-thread tasks (JS parsing/execution) and identify blocking code paths.
|
||||
|
||||
- Runtime marks: add `performance.mark('init-start')` / `performance.mark('init-end')` to measure areas (e.g., store population, Dexie queries) then `performance.measure()`.
|
||||
|
||||
---
|
||||
|
||||
## Prioritized Action Plan (Checklist)
|
||||
|
||||
- [ ] Move `highlight.js` imports and theme stylesheet to `onMount` (quick win).
|
||||
- [ ] Convert `Analytics` and other developer tools to dynamic imports (quick win).
|
||||
- [ ] Move non-critical store population, IndexedDB maintenance, and cache-expiration logic to `onMount` or `requestIdleCallback`.
|
||||
- [ ] Add guard & confirmation around `invalidateAll()` and `location.reload()` to avoid reload loops.
|
||||
- [ ] Defer Dexie `liveQuery` initialization to the route or component that needs it (avoid running all live queries globally).
|
||||
- [ ] Batch localStorage writes and avoid iterating thousands of keys synchronously.
|
||||
- [ ] Run bundle analysis and produce a top-10 biggest modules list.
|
||||
- [ ] Replace heavy icon imports with icon-on-demand solution.
|
||||
- [ ] Re-run Lighthouse and validate improvements.
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Quick Checklist for PRs
|
||||
|
||||
- Ensure client-only code is inside `onMount` or guarded by `if (browser)`.
|
||||
- Where dynamic import used, use `<svelte:component this={Comp} />` pattern.
|
||||
- Add performance marks around expensive blocks and log durations in dev mode.
|
||||
- Avoid unconditional network or socket connections in top-level layout; open them after user interaction or when component mounts.
|
||||
|
||||
---
|
||||
|
||||
If you want, I can implement the first quick wins now: lazy-load `highlight.js` and convert `Analytics` import in `src/routes/+layout.svelte`, then run a local bundle analysis. Tell me which of the quick wins you'd like me to patch first and I'll start.
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
211
src/lib/electron/tmp_shell_handlers.ts
Normal file
211
src/lib/electron/tmp_shell_handlers.ts
Normal 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: {}
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user