Compare commits
15 Commits
17b549a75c
...
194c89f6d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
194c89f6d1 | ||
|
|
469729ce22 | ||
|
|
d1f5d0e2fd | ||
|
|
9c83567430 | ||
|
|
b4d0d82141 | ||
|
|
15bfe6d5d6 | ||
|
|
dddf4b6170 | ||
|
|
587b815446 | ||
|
|
ca51a82dae | ||
|
|
a38320c7f5 | ||
|
|
c76fb8f2b5 | ||
|
|
a26ea8b49c | ||
|
|
21fad1a698 | ||
|
|
33e9eeef78 | ||
|
|
172ea994c7 |
@@ -5,13 +5,14 @@
|
||||
## 🔴 CMSC Charlotte — May 27 (Presentation Management)
|
||||
**Drive down:** May 25 | **Setup:** May 26 morning | **Show:** May 27+
|
||||
|
||||
- [ ] **[Launcher] Composable open flow** — refactor `handle_open_file()` to use
|
||||
`copy_from_cache_to_temp` + `run_osascript` / `run_cmd` directly instead of the all-in-one
|
||||
`launch_from_cache`. Finer error handling at each step.
|
||||
- [ ] **[Launcher] Slide control scripts in Svelte config** — Move AppleScript one-liners from
|
||||
Electron to device config or Svelte constants.
|
||||
- [ ] **[Launcher] `kill_processes` target list in config** — Implement UI for manual "Kill Apps"
|
||||
button and auto-cleanup on file open.
|
||||
- [x] **[Launcher] Composable open flow** — `handle_open_file()` uses `copy_from_cache_to_temp` +
|
||||
`run_osascript` / `run_cmd` directly with per-step error handling. Complete.
|
||||
- [x] **[Launcher] Slide control scripts in Svelte config** — AppleScript post_scripts live in
|
||||
`ae_launcher__default_launch_profiles.ts`. VLC focus-stealing fix applied. Complete.
|
||||
- [x] **[Launcher] Kill Apps button** — "Kill Apps" button added to Native OS config (System
|
||||
Actions, edit mode only). Kills PowerPoint, Keynote, Adobe Acrobat Reader DC, VLC, soffice.
|
||||
List overridable via `event_device.other_json.launcher.kill_process_li`. Auto-cleanup on file
|
||||
open (deferred — manual button sufficient for CMSC).
|
||||
- [ ] **[Launcher] End-to-end test on macOS** — test pptx and key opens on a real podium Mac.
|
||||
- [ ] **[Launcher/Electron] Wallpaper stops applying after several changes (post-CMSC)** —
|
||||
Append timestamp/random suffix to temp filename so macOS always sees a new path.
|
||||
@@ -79,6 +80,12 @@ The app uses `svelte-persisted-store` (coarse reactivity). Migration target: rep
|
||||
|
||||
## ⚙️ DevOps & Backend
|
||||
|
||||
- [ ] **[Backend] `event_file` — add `cfg_json` column (post-CMSC)** — The per-file display
|
||||
override currently uses a localStorage workaround (`$events_loc.launcher.file_display_overrides`)
|
||||
because `event_file` has no JSON blob column. Proper fix: add `cfg_json` to the `event_file` DB
|
||||
table, expose it through the FastAPI model, then migrate the frontend back to reading/writing the
|
||||
backend field (restoring global/cross-device persistence). Frontend code is in
|
||||
`launcher_file_cont.svelte` — search for `file_display_overrides`.
|
||||
- [ ] **[Backend] Re-add `Access-Control-Allow-Private-Network: true` CORS header.**
|
||||
- [ ] **[DevOps] Nginx caching** — Investigate `index.html` cache-pickup issues.
|
||||
- [ ] **[DevOps] Simplify Dockerfile env file selection** — Use plain `.env` instead of `BUILD_MODE`.
|
||||
|
||||
@@ -346,7 +346,20 @@ async function handle_click() {
|
||||
disabled={require_auth && !$ae_loc.authenticated_access}
|
||||
class={variant_classes}
|
||||
onclick={handle_click}
|
||||
title={`Download this file:\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}>
|
||||
title={
|
||||
`Download this file:
|
||||
${final_filename}
|
||||
[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...
|
||||
Hosted ID: ${file_id}
|
||||
|
||||
File size: ${hosted_file_obj.file_size ? ae_util.format_bytes(hosted_file_obj.file_size) : 'Unknown size'}
|
||||
Created on: ${ae_util.iso_datetime_formatter(hosted_file_obj.created_on, 'datetime_short')}
|
||||
Updated on: ${ae_util.iso_datetime_formatter(hosted_file_obj.updated_on, 'datetime_short')}
|
||||
|
||||
Open with: ${hosted_file_obj.open_in_os == 'win' ? 'Windows' : hosted_file_obj.open_in_os == 'mac' ? 'macOS' : hosted_file_obj.open_in_os == 'linux' ? 'Linux' : '--not set--'}
|
||||
|
||||
Linked to Type: ${linked_to_type ?? '--none--'} ID: ${linked_to_id ?? '---'}`
|
||||
}>
|
||||
{@render content()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -55,32 +55,35 @@ export interface LaunchProfile {
|
||||
/**
|
||||
* macOS VLC profile — uses direct binary path for max reliability.
|
||||
* Bypasses `open -a` argument-handling quirks that could lose file path or re-use existing process.
|
||||
*
|
||||
* WHY nohup + &:
|
||||
* run_cmd uses exec() which blocks until the child process exits (or the 30s timeout fires).
|
||||
* The direct VLC binary forks a GUI process then exits — exec returns early and the code
|
||||
* proceeds to the post_script. The old post_script polled for VLC focus (up to 10s) then
|
||||
* sent Cmd+F, which was firing exactly 10–15 seconds into playback and stopping the video.
|
||||
* nohup + & detaches VLC immediately so exec returns in ~0ms, decoupling run_cmd from
|
||||
* VLC's lifecycle entirely.
|
||||
*
|
||||
* WHY --fullscreen:
|
||||
* Starting VLC fullscreen via flag avoids the need to send Cmd+F via AppleScript. The old
|
||||
* keystroke approach was the proximate cause of the video stopping — Cmd+F may have hit the
|
||||
* wrong VLC window, triggered a menu action, or paused playback during the fullscreen
|
||||
* transition. Using the flag is simpler and more reliable.
|
||||
*
|
||||
* WHY > /dev/null 2>&1:
|
||||
* VLC logs verbosely to stdout/stderr. exec() buffers output (1MB default). Without
|
||||
* redirection the buffer could overflow and kill VLC mid-playback.
|
||||
*/
|
||||
function make_vlc_mirror_mac_profile(): LaunchProfile {
|
||||
return {
|
||||
app: 'VLC (macOS)',
|
||||
display_mode: 'mirror',
|
||||
// Direct binary path ensures VLC receives media file + flags reliably.
|
||||
// `--no-play-and-exit` prevents closing on end, `--play-and-pause` holds final frame.
|
||||
open_cmd: '/Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
// Poll until VLC is frontmost before sending Cmd+F. A fixed delay is unreliable because
|
||||
// VLC cold-start on a loaded conference Mac can take 3-5 seconds.
|
||||
// Polling (15 × 0.5 s = up to 7.5 s after the initial wait) fires as soon as VLC is ready.
|
||||
open_cmd: 'nohup /Applications/VLC.app/Contents/MacOS/VLC --no-play-and-exit --play-and-pause --fullscreen "{{path}}" > /dev/null 2>&1 &',
|
||||
post_delay_ms: 3000,
|
||||
// Activate VLC after it has had time to open. Fullscreen is already set by the flag
|
||||
// above — this just ensures VLC is the frontmost app and the presenter sees it.
|
||||
post_script: `tell application "VLC"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "VLC" is true then exit repeat
|
||||
end tell
|
||||
end repeat
|
||||
delay 0.3
|
||||
tell application "System Events"
|
||||
tell process "VLC"
|
||||
keystroke "f" using command down
|
||||
end tell
|
||||
end tell`
|
||||
};
|
||||
}
|
||||
@@ -104,10 +107,10 @@ const POWERPOINT_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "Microsoft PowerPoint" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
post_script: `tell application "Microsoft PowerPoint"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "Microsoft PowerPoint"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "Microsoft PowerPoint" is true then exit repeat
|
||||
@@ -148,10 +151,10 @@ const LIBREOFFICE_MAC_EXTEND_PROFILE: LaunchProfile = {
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
post_script: `tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "soffice" is true then exit repeat
|
||||
@@ -170,10 +173,10 @@ const ACROBAT_MAC_MIRROR_PROFILE: LaunchProfile = {
|
||||
display_mode: 'mirror',
|
||||
open_cmd: 'open -a "Adobe Acrobat Reader DC" "{{path}}"',
|
||||
post_delay_ms: 1000,
|
||||
post_script: `tell application "Adobe Acrobat Reader DC"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "Adobe Acrobat Reader DC"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "AdobeReader" is true then exit repeat
|
||||
@@ -215,10 +218,10 @@ const LIBREOFFICE_WIN_EXTEND_PROFILE: LaunchProfile = {
|
||||
display_mode: 'extend',
|
||||
open_cmd: 'open -a "LibreOffice" "{{path}}"',
|
||||
post_delay_ms: 1500,
|
||||
post_script: `tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "LibreOffice"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "soffice" is true then exit repeat
|
||||
@@ -237,10 +240,10 @@ const ACROBAT_WIN_MIRROR_PROFILE: LaunchProfile = {
|
||||
display_mode: 'mirror',
|
||||
open_cmd: 'open -a "Acrobat Reader Windows" "{{path}}"',
|
||||
post_delay_ms: 1500,
|
||||
post_script: `tell application "Acrobat Reader Windows"
|
||||
activate
|
||||
end tell
|
||||
repeat 15 times
|
||||
post_script: `repeat 20 times
|
||||
tell application "Acrobat Reader Windows"
|
||||
activate
|
||||
end tell
|
||||
delay 0.5
|
||||
tell application "System Events"
|
||||
if frontmost of process "Acrobat Reader Windows" is true then exit repeat
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
RefreshCw,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
Square
|
||||
Square,
|
||||
XCircle
|
||||
} from '@lucide/svelte';
|
||||
interface Props {
|
||||
on_expand?: () => void;
|
||||
@@ -69,6 +70,27 @@ async function handle_display_mode(mode: 'extend' | 'mirror') {
|
||||
setTimeout(() => (system_status = ''), 4000);
|
||||
}
|
||||
|
||||
// Process names sent to kill_processes() when the operator hits "Kill Apps".
|
||||
// Covers the standard conference presentation app set — PowerPoint, Keynote, Acrobat, VLC, LibreOffice.
|
||||
// Override per device via event_device.other_json.launcher.kill_process_li.
|
||||
const DEFAULT_KILL_LIST = [
|
||||
'Microsoft PowerPoint',
|
||||
'Keynote',
|
||||
'Adobe Acrobat Reader DC',
|
||||
'VLC',
|
||||
'soffice'
|
||||
];
|
||||
|
||||
async function handle_kill_apps() {
|
||||
const native_device = ($ae_loc as any).native_device ?? null;
|
||||
const process_name_li: string[] =
|
||||
native_device?.other_json?.launcher?.kill_process_li ?? DEFAULT_KILL_LIST;
|
||||
system_status = `Killing: ${process_name_li.join(', ')}...`;
|
||||
await native.kill_processes({ process_name_li });
|
||||
system_status = 'Kill signal sent';
|
||||
setTimeout(() => (system_status = ''), 4000);
|
||||
}
|
||||
|
||||
// Modal state for dangerous actions
|
||||
let show_power_confirm = $state<{ action: string; label: string } | null>(null);
|
||||
</script>
|
||||
@@ -240,6 +262,16 @@ let show_power_confirm = $state<{ action: string; label: string } | null>(null);
|
||||
disabled={!$ae_loc.site_header_image_path}>
|
||||
<Image size="0.85em" class="mr-1 shrink-0" /> Reset Wallpaper
|
||||
</button>
|
||||
<!-- Kill running presentation apps — PowerPoint, Keynote, Acrobat, VLC, LibreOffice.
|
||||
Use between sessions to ensure a clean slate. List is overridable via
|
||||
event_device.other_json.launcher.kill_process_li. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handle_kill_apps}
|
||||
class="btn btn-xs preset-tonal-error hover:preset-filled-error-500 justify-start"
|
||||
title="Kill presentation apps: {DEFAULT_KILL_LIST.join(', ')}">
|
||||
<XCircle size="0.85em" class="mr-1 shrink-0" /> Kill Apps
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
@@ -601,8 +601,10 @@ $effect(() => {
|
||||
class="
|
||||
static
|
||||
m-auto
|
||||
mb-16 h-full w-full
|
||||
max-w-7xl border-x
|
||||
mb-16
|
||||
lg:min-h-8/12 h-full max-h-screen
|
||||
w-full max-w-7xl
|
||||
border-x
|
||||
border-gray-200
|
||||
transition-all sm:mb-12
|
||||
dark:border-gray-600
|
||||
|
||||
@@ -645,7 +645,7 @@ async function force_location_sync() {
|
||||
and the sys bar (bottom-12 right-2). Panel grows upward from the status chip. -->
|
||||
{#if $events_loc.launcher.app_mode === 'native' || $ae_loc.is_native}
|
||||
<div
|
||||
class="pointer-events-none fixed bottom-20 left-4 z-[9999] flex flex-col items-start gap-2">
|
||||
class="pointer-events-none fixed bottom-15 left-2 z-10 flex flex-col items-start gap-2">
|
||||
{#if show_monitor}
|
||||
<div
|
||||
class="bg-surface-50/95 dark:bg-surface-900/95 text-surface-800 dark:text-surface-100 border-surface-200 dark:border-primary-700 pointer-events-auto min-w-52 rounded-lg border p-3 font-mono text-[10px] shadow-2xl backdrop-blur-sm">
|
||||
|
||||
@@ -93,6 +93,14 @@ let open_file_status: null | string = $state(null);
|
||||
let open_file_status_message: null | string = $state(null);
|
||||
let open_file_error_detail: string | null = $state(null);
|
||||
|
||||
let open_in_os_loading: boolean = $state(false);
|
||||
|
||||
/** Reactive display override for this file — stored in $events_loc (localStorage) not in the backend. */
|
||||
const current_display_override = $derived.by(() => {
|
||||
const overrides = (($events_loc.launcher as Record<string, unknown>)?.file_display_overrides ?? {}) as Record<string, string>;
|
||||
return (overrides[event_file_id] ?? null) as 'extend' | 'mirror' | 'none' | null;
|
||||
});
|
||||
|
||||
/** State for the native test mode debug popup */
|
||||
let test_mode_popup_open: boolean = $state(false);
|
||||
let test_mode_popup_data: Record<string, any> | null = $state(null);
|
||||
@@ -135,9 +143,27 @@ function get_launch_profile(
|
||||
native_device?.launch_profiles ??
|
||||
null;
|
||||
const local_profiles = ($events_loc as any).launcher?.launch_profiles ?? null;
|
||||
const display_override = file_obj?.cfg_json?.display_override ?? null;
|
||||
// Display override is stored per-device in $events_loc — not in the backend (event_file has no JSON column).
|
||||
// This is intentional: display mode is a room/device preference, not a global file property.
|
||||
const launcher_kv = $events_loc.launcher as Record<string, unknown>;
|
||||
const file_display_overrides = (launcher_kv?.file_display_overrides ?? {}) as Record<string, string>;
|
||||
const display_override = (file_display_overrides[event_file_id] ?? null) as 'extend' | 'mirror' | 'none' | null;
|
||||
|
||||
// open_in_os = 'win' routes to the Windows-variant profile for apps that have one.
|
||||
// These profiles target Windows PowerPoint / LibreOffice / Acrobat running via Parallels or CrossOver.
|
||||
const WIN_EXTENSION_MAP: Record<string, string> = {
|
||||
pptx: 'pptxwin',
|
||||
ppt: 'pptwin',
|
||||
odp: 'odpwin',
|
||||
pdf: 'pdfwin'
|
||||
};
|
||||
const effective_extension =
|
||||
file_obj?.open_in_os === 'win'
|
||||
? (WIN_EXTENSION_MAP[extension] ?? extension)
|
||||
: extension;
|
||||
|
||||
return resolve_launch_profile(
|
||||
extension,
|
||||
effective_extension,
|
||||
display_override,
|
||||
device_profiles,
|
||||
local_profiles
|
||||
@@ -455,12 +481,11 @@ async function handle_open_file() {
|
||||
open_file_status_message = 'Downloading (Onsite Mode)...';
|
||||
open_file_error_detail = null;
|
||||
|
||||
// Append 'win' to the filename for extensions that have Windows file associations
|
||||
// (pptx→pptxwin, ppt→pptwin, odp→odpwin, pdf→pdfwin). Must match WIN_EXTENSION_MAP.
|
||||
const WIN_ONSITE_EXTS = ['pptx', 'ppt', 'odp', 'pdf'];
|
||||
let filename = event_file_obj.filename;
|
||||
if (
|
||||
(event_file_obj.extension === 'ppt' ||
|
||||
event_file_obj.extension === 'pptx') &&
|
||||
event_file_obj.open_in_os === 'win'
|
||||
) {
|
||||
if (event_file_obj.open_in_os === 'win' && WIN_ONSITE_EXTS.includes(event_file_obj.extension)) {
|
||||
filename = event_file_obj.filename + 'win';
|
||||
}
|
||||
|
||||
@@ -606,7 +631,7 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
click={handle_open_file}>
|
||||
{#snippet label()}
|
||||
{@const file_id = event_file_obj.hosted_file_id}
|
||||
<span class="shrink border-r border-gray-400 pr-1 text-xs">
|
||||
<span class="shrink border-r border-surface-300-700 pr-1 text-xs">
|
||||
{#await ae_promises[event_file_id]}
|
||||
<LoaderCircle
|
||||
size="1em"
|
||||
@@ -620,26 +645,28 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
{/if}
|
||||
</span>
|
||||
{:then result}
|
||||
<span class=" font-mono">
|
||||
{#if is_url}
|
||||
<Link2 size="1em" class="mx-0.5 inline {!is_online ? 'text-warning-500' : ''}" />
|
||||
<span class:text-warning-500={!is_online}>url</span>
|
||||
{#if !is_online}<WifiOff size="0.85em" class="mx-0.5 inline text-warning-500" title="Network offline" />{/if}
|
||||
<Link2 size="1em" class="inline opacity-50 {!is_online ? 'text-warning-900-100' : ''}" />
|
||||
<span class:text-warning-900-100={!is_online}>url</span>
|
||||
{#if !is_online}<WifiOff size="0.85em" class="inline text-warning-900-100" title="Network offline" />{/if}
|
||||
{:else}
|
||||
{@const FileIcon =
|
||||
ae_util.file_extension_icon_lucide(
|
||||
event_file_obj.extension
|
||||
)}
|
||||
<FileIcon size="1em" class="mx-0.5 inline" />
|
||||
<FileIcon size="1em" class="inline opacity-50" />
|
||||
{event_file_obj.extension}
|
||||
{#if result === null || result === false}
|
||||
<span class="text-error-500"
|
||||
<span class="text-error-900-100"
|
||||
><TriangleAlert
|
||||
size="1em"
|
||||
class="mx-1 inline" />Failed!</span>
|
||||
class="inline" />Failed!</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</span>
|
||||
{:catch error}
|
||||
<span class="text-error-500" title={error?.message}
|
||||
<span class="text-error-900-100" title={error?.message}
|
||||
><AlertCircle
|
||||
size="1em"
|
||||
class="mx-0.5 inline" />Error!</span>
|
||||
@@ -673,79 +700,71 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
let new_val: string | null;
|
||||
if (!event_file_obj?.open_in_os) new_val = 'win';
|
||||
else if (event_file_obj?.open_in_os == 'win') new_val = 'mac';
|
||||
else new_val = null;
|
||||
await api.update_ae_obj({
|
||||
api_cfg: $ae_api,
|
||||
obj_type: 'event_file',
|
||||
obj_id: event_file_id,
|
||||
fields: { open_in_os: new_val }
|
||||
});
|
||||
events_func.load_ae_obj_id__event_file({
|
||||
api_cfg: $ae_api,
|
||||
event_file_id: event_file_obj?.event_file_id,
|
||||
log_lvl
|
||||
});
|
||||
}}
|
||||
class="btn btn-sm group transition-all"
|
||||
class:preset-tonal-warning={event_file_obj?.open_in_os == 'win'}
|
||||
class:preset-tonal-success={event_file_obj?.open_in_os == 'mac'}
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
title={`Open in OS: ${
|
||||
event_file_obj?.open_in_os
|
||||
? event_file_obj.open_in_os.toUpperCase()
|
||||
: 'None'
|
||||
}`}
|
||||
>
|
||||
{#if event_file_obj?.open_in_os == 'win'}
|
||||
<!-- <Monitor
|
||||
size="1em"
|
||||
class="m-1" /> -->
|
||||
Win
|
||||
{:else if event_file_obj?.open_in_os == 'mac'}
|
||||
<!-- <Laptop
|
||||
size="1em"
|
||||
class="m-1" /> -->
|
||||
Mac
|
||||
{:else}
|
||||
<FolderOpen size="1em" class="m-1" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if $ae_loc.trusted_access && $ae_loc.is_native}
|
||||
<!-- Display override: per-file display_mode override for this file only.
|
||||
null = use profile default, 'extend' = force extend, 'mirror' = force mirror.
|
||||
Stored in event_file.cfg_json.display_override. Cycles null → extend → mirror → null. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={async () => {
|
||||
const cur = event_file_obj?.cfg_json?.display_override ?? null;
|
||||
let next: string | null;
|
||||
if (!cur) next = 'extend';
|
||||
else if (cur === 'extend') next = 'mirror';
|
||||
else next = null;
|
||||
const new_cfg = { ...(event_file_obj.cfg_json ?? {}), display_override: next };
|
||||
open_in_os_loading = true;
|
||||
try {
|
||||
let new_val: string | null;
|
||||
if (!event_file_obj?.open_in_os) new_val = 'win';
|
||||
else if (event_file_obj?.open_in_os == 'win') new_val = 'mac';
|
||||
else new_val = null;
|
||||
await api.update_ae_obj({
|
||||
api_cfg: $ae_api,
|
||||
obj_type: 'event_file',
|
||||
obj_id: event_file_id,
|
||||
fields: { cfg_json: new_cfg }
|
||||
fields: { open_in_os: new_val }
|
||||
});
|
||||
events_func.load_ae_obj_id__event_file({
|
||||
api_cfg: $ae_api,
|
||||
event_file_id: event_file_obj?.event_file_id,
|
||||
log_lvl
|
||||
});
|
||||
} finally {
|
||||
open_in_os_loading = false;
|
||||
}
|
||||
}}
|
||||
class="btn btn-sm group transition-all"
|
||||
class:preset-tonal-warning={event_file_obj?.open_in_os == 'win'}
|
||||
class:preset-tonal-success={event_file_obj?.open_in_os == 'mac'}
|
||||
disabled={!$ae_loc.trusted_access || open_in_os_loading}
|
||||
title={`Open in OS: ${
|
||||
event_file_obj?.open_in_os == 'win' ? 'Windows' : event_file_obj?.open_in_os == 'mac' ? 'macOS' : event_file_obj?.open_in_os == 'linux' ? 'Linux' : '--not set--'
|
||||
}`}
|
||||
>
|
||||
{#if open_in_os_loading}
|
||||
<LoaderCircle size="1em" class="m-1 animate-spin" />
|
||||
{:else if event_file_obj?.open_in_os == 'win'}
|
||||
Win
|
||||
{:else if event_file_obj?.open_in_os == 'mac'}
|
||||
Mac
|
||||
{:else}
|
||||
<FolderOpen size="1em" class="m-1" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if $ae_loc.edit_mode}
|
||||
<!-- Display override (temporary — local/device only, stored in $events_loc).
|
||||
Cycles null → extend → mirror → null. Instant write, no API call.
|
||||
TODO: replace with backend cfg_json once event_file gains a JSON column. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
const cur = current_display_override;
|
||||
const next: 'extend' | 'mirror' | null = !cur ? 'extend' : cur === 'extend' ? 'mirror' : null;
|
||||
const launcher = $events_loc.launcher as Record<string, unknown>;
|
||||
const new_overrides = { ...((launcher?.file_display_overrides ?? {}) as Record<string, string>) };
|
||||
if (next === null) {
|
||||
delete new_overrides[event_file_id];
|
||||
} else {
|
||||
new_overrides[event_file_id] = next;
|
||||
}
|
||||
launcher.file_display_overrides = new_overrides;
|
||||
}}
|
||||
class="btn btn-sm transition-all"
|
||||
class:preset-tonal-primary={event_file_obj?.cfg_json?.display_override === 'extend'}
|
||||
class:preset-tonal-warning={event_file_obj?.cfg_json?.display_override === 'mirror'}
|
||||
title={`Display override: ${event_file_obj?.cfg_json?.display_override ?? 'default'}`}>
|
||||
{#if event_file_obj?.cfg_json?.display_override === 'extend'}
|
||||
class:preset-tonal-primary={current_display_override === 'extend'}
|
||||
class:preset-tonal-warning={current_display_override === 'mirror'}
|
||||
title={`Display override: ${current_display_override ?? 'default'}`}>
|
||||
{#if current_display_override === 'extend'}
|
||||
Ext
|
||||
{:else if event_file_obj?.cfg_json?.display_override === 'mirror'}
|
||||
{:else if current_display_override === 'mirror'}
|
||||
Mir
|
||||
{:else}
|
||||
<Monitor size="1em" class="m-1" />
|
||||
@@ -755,8 +774,13 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
|
||||
<span
|
||||
class="event_file_created_on preset-filled-surface-100-900 flex w-24 flex-row items-center justify-end gap-1 rounded px-1 py-0.5 text-center text-xs md:w-44"
|
||||
class:hidden={hide_created_on}>
|
||||
<CalendarDays size="0.85em" class="inline" />
|
||||
class:hidden={hide_created_on}
|
||||
title={`Created on:\n${ae_util.iso_datetime_formatter(
|
||||
event_file_obj.created_on,
|
||||
'datetime_long'
|
||||
)}`}
|
||||
>
|
||||
<CalendarDays size="0.85em" class="inline opacity-50" />
|
||||
<span class="w-18"
|
||||
>{ae_util.iso_datetime_formatter(
|
||||
event_file_obj.created_on,
|
||||
@@ -765,9 +789,11 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="event_file_size preset-filled-surface-100-900 flex w-22 max-w-28 flex-row items-center justify-end gap-1 rounded py-0.5 text-center text-xs"
|
||||
class:hidden={hide_size}>
|
||||
<Save size="0.85em" class="inline" />
|
||||
class="event_file_size preset-filled-surface-100-900 flex min-w-20 w-22 max-w-28 flex-row items-center justify-end gap-1 rounded py-0.5 text-center text-xs"
|
||||
class:hidden={hide_size}
|
||||
title={`File size:\n${event_file_obj.file_size ? ae_util.format_bytes(event_file_obj.file_size) : 'Unknown size'}\nBytes: ${event_file_obj.file_size}`}
|
||||
>
|
||||
<Save size="0.85em" class="inline opacity-50" />
|
||||
{#if event_file_obj.file_size}{ae_util.format_bytes(
|
||||
event_file_obj.file_size
|
||||
)}{/if}
|
||||
|
||||
@@ -146,8 +146,10 @@ let ae_promises: key_val = $state({
|
||||
<div
|
||||
class="
|
||||
event_launcher_menu
|
||||
flex h-full w-full max-w-full
|
||||
shrink flex-col flex-wrap items-center justify-start gap-1
|
||||
h-full
|
||||
w-full max-w-full
|
||||
flex flex-col flex-wrap items-center justify-start gap-1
|
||||
shrink
|
||||
|
||||
">
|
||||
<!-- overflow-x-clip -->
|
||||
@@ -247,5 +249,7 @@ let ae_promises: key_val = $state({
|
||||
bind:trigger_reload__event_session_obj_id />
|
||||
{/if}
|
||||
|
||||
<Menu_launcher_controls />
|
||||
<div class="mt-auto w-full">
|
||||
<Menu_launcher_controls />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,15 +10,20 @@
|
||||
*
|
||||
* SECTIONS:
|
||||
* 1. Visibility toggles (edit mode only) — show/hide draft files and hidden sessions
|
||||
* 2. Accessibility controls (always visible) — font size cycler and light/dark toggle
|
||||
* 2. Accessibility controls (always visible) — Reset Apps, font size cycler, light/dark toggle
|
||||
*
|
||||
* WHY ALWAYS-VISIBLE ACCESSIBILITY CONTROLS:
|
||||
* Projector-connected screens, tablets, and phones at conference venues vary wildly
|
||||
* in lighting. Operators and presenters need quick one-tap access to font size and
|
||||
* theme mode without hunting through the system menu or requiring admin access.
|
||||
*
|
||||
* WHY RESET APPS IS ALWAYS VISIBLE:
|
||||
* If a presentation app (PowerPoint, Keynote, etc.) hangs or stalls on stage, the
|
||||
* presenter needs a one-tap way to force-close it and recover — without entering edit
|
||||
* mode or navigating the config menu. This button is intentionally prominent.
|
||||
*/
|
||||
|
||||
import { Moon, Sun, Eye, EyeOff, Columns2, Copy } from '@lucide/svelte';
|
||||
import { Moon, Sun, Eye, EyeOff, Columns2, Copy, XCircle, Trash, Recycle } from '@lucide/svelte';
|
||||
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
import { events_loc } from '$lib/stores/ae_events_stores';
|
||||
@@ -30,78 +35,203 @@ interface Props {
|
||||
|
||||
let { log_lvl = $bindable(0) }: Props = $props();
|
||||
|
||||
let quick_display_mode = $state<'extend' | 'mirror'>('extend');
|
||||
// Persist display mode across reloads — reflects the last-set state, not hardware-queried state.
|
||||
let quick_display_mode = $state<'extend' | 'mirror'>(
|
||||
($events_loc.launcher as any)?.display_mode ?? 'extend'
|
||||
);
|
||||
|
||||
const is_native_launcher_mode = $derived(
|
||||
!!$ae_loc.is_native && $events_loc.launcher.app_mode === 'native'
|
||||
);
|
||||
|
||||
async function set_quick_display_mode(mode: 'extend' | 'mirror') {
|
||||
if (!is_native_launcher_mode) return;
|
||||
const res = await native.set_display_layout({ mode });
|
||||
if (res?.success) quick_display_mode = mode;
|
||||
async function toggle_display_mode() {
|
||||
const next = quick_display_mode === 'extend' ? 'mirror' : 'extend';
|
||||
if (is_native_launcher_mode) {
|
||||
const res = await native.set_display_layout({ mode: next });
|
||||
if (res?.success) {
|
||||
quick_display_mode = next;
|
||||
($events_loc.launcher as any).display_mode = next;
|
||||
}
|
||||
} else {
|
||||
quick_display_mode = next;
|
||||
($events_loc.launcher as any).display_mode = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Process names closed when presenter hits "Reset Apps". Covers the standard
|
||||
// conference app set. Override per device via event_device.other_json.launcher.kill_process_li.
|
||||
const DEFAULT_KILL_LIST = [
|
||||
'Microsoft PowerPoint',
|
||||
'Keynote',
|
||||
'Adobe Acrobat Reader DC',
|
||||
'VLC',
|
||||
'soffice'
|
||||
];
|
||||
|
||||
let reset_apps_status = $state('');
|
||||
let cache_status = $state('');
|
||||
|
||||
async function handle_reset_apps() {
|
||||
const native_device = ($ae_loc as any).native_device ?? null;
|
||||
const process_name_li: string[] =
|
||||
native_device?.other_json?.launcher?.kill_process_li ?? DEFAULT_KILL_LIST;
|
||||
reset_apps_status = 'Resetting...';
|
||||
await native.kill_processes({ process_name_li });
|
||||
reset_apps_status = 'Done';
|
||||
setTimeout(() => (reset_apps_status = ''), 3000);
|
||||
}
|
||||
|
||||
async function handle_cache_cleanup() {
|
||||
cache_status = 'Clearing...';
|
||||
try {
|
||||
localStorage.removeItem('ae_events_loc');
|
||||
localStorage.removeItem('ae_loc');
|
||||
indexedDB.deleteDatabase('ae_events_db');
|
||||
cache_status = 'Done — reloading...';
|
||||
setTimeout(() => window.location.reload(), 800);
|
||||
} catch {
|
||||
cache_status = 'Error';
|
||||
setTimeout(() => (cache_status = ''), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function handle_reload_launcher() {
|
||||
if ($ae_loc.is_native) {
|
||||
native.window_control({ action: 'reload' });
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex w-full max-w-full flex-col items-center justify-center gap-1">
|
||||
<!-- ── Visibility toggles — edit mode only ── -->
|
||||
{#if $ae_loc.edit_mode}
|
||||
<div
|
||||
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if ($events_loc.launcher.show_content__hidden_files) {
|
||||
$events_loc.launcher.show_content__hidden_files = false;
|
||||
$events_loc.launcher.show_content__internal_files = false;
|
||||
} else {
|
||||
$events_loc.launcher.show_content__hidden_files = true;
|
||||
$events_loc.launcher.show_content__internal_files = true;
|
||||
}
|
||||
}}
|
||||
class="
|
||||
btn btn-sm preset-tonal-tertiary
|
||||
hover:preset-filled-tertiary-500 w-1/2
|
||||
max-w-1/2 text-xs
|
||||
transition-all
|
||||
"
|
||||
title="Toggle visibility of hidden and draft files in the launcher file list.">
|
||||
{#if $events_loc.launcher.show_content__hidden_files}
|
||||
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
|
||||
Hide Files
|
||||
{:else}
|
||||
<Eye size="0.85em" class="m-1 text-neutral-800/80" />
|
||||
All Files
|
||||
{/if}
|
||||
</button>
|
||||
<div class="flex flex-row flex-wrap w-full max-w-full items-center justify-center gap-0.5">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$events_loc.launcher.show_content__hidden_sessions =
|
||||
!$events_loc.launcher.show_content__hidden_sessions;
|
||||
}}
|
||||
class="
|
||||
btn btn-sm preset-tonal-tertiary
|
||||
hover:preset-filled-tertiary-500 w-1/2
|
||||
max-w-1/2 text-xs
|
||||
transition-all
|
||||
"
|
||||
title="Toggle visibility of hidden and cancelled sessions in the launcher session list.">
|
||||
{#if $events_loc.launcher.show_content__hidden_sessions}
|
||||
<EyeOff size="0.85em" class="m-1 text-neutral-800/80" />
|
||||
Hide Sessions
|
||||
{:else}
|
||||
<Eye size="0.85em" class="m-1 text-neutral-800/80" />
|
||||
All Sessions
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- All Files / All Sessions toggles.
|
||||
Warning color when showing hidden content — signals non-default state to operators. -->
|
||||
<div class="min-w-32 flex flex-row flex-wrap items-center justify-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
if ($events_loc.launcher.show_content__hidden_files) {
|
||||
$events_loc.launcher.show_content__hidden_files = false;
|
||||
$events_loc.launcher.show_content__internal_files = false;
|
||||
} else {
|
||||
$events_loc.launcher.show_content__hidden_files = true;
|
||||
$events_loc.launcher.show_content__internal_files = true;
|
||||
}
|
||||
}}
|
||||
class="btn btn-sm min-w-34 w-34 max-w-1/2 text-xs transition-all"
|
||||
class:preset-tonal-warning={$events_loc.launcher.show_content__hidden_files}
|
||||
class:hover:preset-filled-warning-500={$events_loc.launcher
|
||||
.show_content__hidden_files}
|
||||
class:preset-tonal-tertiary={!$events_loc.launcher.show_content__hidden_files}
|
||||
class:hover:preset-filled-tertiary-500={!$events_loc.launcher
|
||||
.show_content__hidden_files}
|
||||
title={$events_loc.launcher.show_content__hidden_files
|
||||
? 'Showing all files including hidden and draft. Tap to hide them again.'
|
||||
: 'Showing only public files. Tap to show all files including hidden and draft.'}>
|
||||
{#if $events_loc.launcher.show_content__hidden_files}
|
||||
<EyeOff size="0.85em" class="m-1" />
|
||||
Hide Files
|
||||
{:else}
|
||||
<Eye size="0.85em" class="m-1" />
|
||||
All Files
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- ── Accessibility controls — always visible ── -->
|
||||
<div
|
||||
class="flex w-full max-w-full flex-row items-center justify-center gap-1">
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$events_loc.launcher.show_content__hidden_sessions =
|
||||
!$events_loc.launcher.show_content__hidden_sessions;
|
||||
}}
|
||||
class="btn btn-sm min-w-34 w-34 max-w-1/2 text-xs transition-all"
|
||||
class:preset-tonal-warning={$events_loc.launcher.show_content__hidden_sessions}
|
||||
class:hover:preset-filled-warning-500={$events_loc.launcher
|
||||
.show_content__hidden_sessions}
|
||||
class:preset-tonal-tertiary={!$events_loc.launcher.show_content__hidden_sessions}
|
||||
class:hover:preset-filled-tertiary-500={!$events_loc.launcher
|
||||
.show_content__hidden_sessions}
|
||||
title={$events_loc.launcher.show_content__hidden_sessions
|
||||
? 'Showing all sessions including cancelled and hidden. Tap to hide them again.'
|
||||
: 'Showing only active sessions. Tap to show all sessions including hidden and cancelled.'}>
|
||||
{#if $events_loc.launcher.show_content__hidden_sessions}
|
||||
<EyeOff size="0.85em" class="shrink-0" />
|
||||
Hide Sessions
|
||||
{:else}
|
||||
<Eye size="0.85em" class="shrink-0" />
|
||||
All Sessions
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="min-w-32 flex flex-row flex-wrap items-center justify-center gap-0.5">
|
||||
<!-- Display mode toggle — warning color when mirroring (non-default operator setting).
|
||||
Tooltip describes the CURRENT state and what pressing will do, so operators know
|
||||
which way they are switching before they tap. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggle_display_mode}
|
||||
class="btn btn-sm min-w-34 w-34 max-w-1/2 text-xs transition-all"
|
||||
class:preset-tonal-tertiary={quick_display_mode === 'extend'}
|
||||
class:hover:preset-filled-tertiary-500={quick_display_mode === 'extend'}
|
||||
class:preset-tonal-warning={quick_display_mode === 'mirror'}
|
||||
class:hover:preset-filled-warning-500={quick_display_mode === 'mirror'}
|
||||
title={quick_display_mode === 'extend'
|
||||
? 'Screens are extended: laptop can show notes while projector shows slides. Tap to mirror — make both screens show the same content.'
|
||||
: 'Screens are mirrored: both screens show the same content. Tap to extend — allow the laptop and projector to show different content.'}>
|
||||
{#if quick_display_mode === 'extend'}
|
||||
<Columns2 size="0.85em" class="shrink-0" />
|
||||
Display: Extend
|
||||
{:else}
|
||||
<Copy size="0.85em" class="shrink-0" />
|
||||
Display: Mirror
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ── Always-visible controls ── -->
|
||||
<div class="w-full flex flex-row flex-wrap items-center justify-center gap-0.5">
|
||||
|
||||
{#if $ae_loc.edit_mode}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handle_cache_cleanup}
|
||||
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-34 max-w-1/2 text-xs transition-all"
|
||||
title="Clear localStorage and IDB caches used by the Launcher. Does *not* delete cached files."
|
||||
>
|
||||
<Trash size="0.85em" class="shrink-0" />
|
||||
{cache_status || 'Clear Cache'}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={handle_reload_launcher}
|
||||
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-34 max-w-1/2 text-xs transition-all"
|
||||
title="Reload the Launcher interface."
|
||||
>
|
||||
<Recycle size="0.85em" class="shrink-0" />
|
||||
Reload Launcher
|
||||
</button>
|
||||
|
||||
|
||||
<!-- Reset Apps: force-closes hung presentation apps without requiring edit mode.
|
||||
Essential recovery tool for presenters stuck on stage with a frozen app. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={handle_reset_apps}
|
||||
class="btn btn-sm preset-tonal-error hover:preset-filled-error-500 w-34 max-w-1/2 text-xs transition-all"
|
||||
title="Close all presentation apps (PowerPoint, Keynote, Adobe Acrobat, VLC). Use this if a presentation is frozen or stuck.">
|
||||
<XCircle size="0.85em" class="shrink-0" />
|
||||
{reset_apps_status || 'Reset Apps'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-row flex-wrap items-center justify-center gap-0.5">
|
||||
<!-- Font size cycler: default → larger → smaller → default -->
|
||||
<button
|
||||
type="button"
|
||||
@@ -115,29 +245,18 @@ async function set_quick_display_mode(mode: 'extend' | 'mirror') {
|
||||
$ae_loc.font_size_mode = 'default';
|
||||
}
|
||||
}}
|
||||
class="
|
||||
btn btn-sm preset-tonal-tertiary
|
||||
hover:preset-filled-tertiary-500 group
|
||||
w-1/2 max-w-1/2
|
||||
text-xs transition-all
|
||||
"
|
||||
class="btn btn-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 group min-w-32 max-w-1/2 text-xs transition-all"
|
||||
title="Cycle font size (default → larger → smaller). Current: {$ae_loc.font_size_mode ??
|
||||
'default'}">
|
||||
{#if !$ae_loc.font_size_mode || $ae_loc.font_size_mode === 'default'}
|
||||
<span class="m-1 font-mono text-sm leading-none font-bold"
|
||||
>A</span>
|
||||
<span class="hidden text-xs group-hover:inline-block"
|
||||
>Font: Normal</span>
|
||||
<span class="m-1 font-mono text-sm leading-none font-bold">A</span>
|
||||
<span class="hidden text-xs group-hover:inline-block">Font: Normal</span>
|
||||
{:else if $ae_loc.font_size_mode === 'larger'}
|
||||
<span class="m-1 font-mono text-base leading-none font-bold"
|
||||
>A+</span>
|
||||
<span class="hidden text-xs group-hover:inline-block"
|
||||
>Font: Larger</span>
|
||||
<span class="m-1 font-mono text-base leading-none font-bold">A+</span>
|
||||
<span class="hidden text-xs group-hover:inline-block">Font: Larger</span>
|
||||
{:else}
|
||||
<span class="m-1 font-mono text-xs leading-none font-bold"
|
||||
>A−</span>
|
||||
<span class="hidden text-xs group-hover:inline-block"
|
||||
>Font: Smaller</span>
|
||||
<span class="m-1 font-mono text-xs leading-none font-bold">A−</span>
|
||||
<span class="hidden text-xs group-hover:inline-block">Font: Smaller</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -145,17 +264,10 @@ async function set_quick_display_mode(mode: 'extend' | 'mirror') {
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$ae_loc.theme_mode =
|
||||
$ae_loc.theme_mode === 'dark' ? 'light' : 'dark';
|
||||
$ae_loc.theme_mode = $ae_loc.theme_mode === 'dark' ? 'light' : 'dark';
|
||||
}}
|
||||
class="
|
||||
btn btn-sm preset-tonal-tertiary
|
||||
hover:preset-filled-tertiary-500 group
|
||||
w-1/2 max-w-1/2
|
||||
text-xs transition-all
|
||||
"
|
||||
title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ??
|
||||
'light'}">
|
||||
class="btn btn-sm preset-tonal-tertiary hover:preset-filled-tertiary-500 group min-w-32 max-w-1/2 text-xs transition-all"
|
||||
title="Toggle light/dark display mode. Current: {$ae_loc.theme_mode ?? 'light'}">
|
||||
{#if $ae_loc.theme_mode === 'dark'}
|
||||
<Moon class="m-1 inline-block" size="1em" />
|
||||
<span class="hidden group-hover:inline-block">Dark Mode</span>
|
||||
@@ -165,51 +277,4 @@ async function set_quick_display_mode(mode: 'extend' | 'mirror') {
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Quick display mode controls — always visible (native-only action) ── -->
|
||||
<div class="flex w-full max-w-full flex-row items-center justify-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => set_quick_display_mode('extend')}
|
||||
disabled={!is_native_launcher_mode}
|
||||
class="
|
||||
btn btn-sm group w-1/2 max-w-1/2 text-xs transition-all
|
||||
border-2
|
||||
"
|
||||
class:border-primary-500={quick_display_mode === 'extend'}
|
||||
class:preset-tonal-primary={quick_display_mode === 'extend'}
|
||||
class:border-surface-400={quick_display_mode !== 'extend'}
|
||||
class:preset-tonal-surface={quick_display_mode !== 'extend'}
|
||||
title="Set display layout to Extend (separate laptop and projector screens).">
|
||||
<Columns2 size="0.9em" class="m-1 inline-block" />
|
||||
<span class="hidden group-hover:inline-block">Display: Extend</span>
|
||||
<span class="group-hover:hidden">Extend</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => set_quick_display_mode('mirror')}
|
||||
disabled={!is_native_launcher_mode}
|
||||
class="
|
||||
btn btn-sm group w-1/2 max-w-1/2 text-xs transition-all
|
||||
border-2
|
||||
"
|
||||
class:border-warning-500={quick_display_mode === 'mirror'}
|
||||
class:preset-tonal-warning={quick_display_mode === 'mirror'}
|
||||
class:border-surface-400={quick_display_mode !== 'mirror'}
|
||||
class:preset-tonal-surface={quick_display_mode !== 'mirror'}
|
||||
title="Set display layout to Mirror (same content on laptop and projector).">
|
||||
<Copy size="0.9em" class="m-1 inline-block" />
|
||||
<span class="hidden group-hover:inline-block">Display: Mirror</span>
|
||||
<span class="group-hover:hidden">Mirror</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if !is_native_launcher_mode}
|
||||
<div
|
||||
class="text-[10px] leading-tight opacity-70 text-center px-2"
|
||||
title="Shown here as a visual preview. Active in native app mode in the session room.">
|
||||
Display toggle shown as an example preview. Active in native app mode in the session room.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -275,11 +275,11 @@ $effect(() => {
|
||||
When revealed, dimmed (opacity-40) with eye-slash icon. -->
|
||||
|
||||
<span
|
||||
class="border-surface-400-600 min-w-20 shrink-0 border-r pr-1">
|
||||
class="border-surface-300-700 min-w-20 shrink-0 border-r pr-1 font-mono">
|
||||
{#if slct__event_session_id === event_session_obj?.id}
|
||||
<CalendarCheck size="0.85em" class="inline" />
|
||||
<CalendarCheck size="0.85em" class="inline opacity-50" />
|
||||
{:else}
|
||||
<CalendarDays size="0.85em" class="inline" />
|
||||
<CalendarDays size="0.85em" class="inline opacity-50" />
|
||||
{/if}
|
||||
<span
|
||||
class="text-xs"
|
||||
|
||||
Reference in New Issue
Block a user