feat(launcher): poster modal zoom sync, clean close buttons
- Add modal_zoom_fit state (default: fit); resets on every new poster open
- Zoom/Fit toggle button + image double-tap on controller tablet
- Both zoom triggers send ae_zoom:fit/zoom over WS to remote display (local_push)
- ae_zoom: handler added to handle_ws_recv() for remote device
- Replace 3 scattered close buttons with single bottom control bar:
- [Zoom/Fit] always visible on controller
- [Close Both] sends ae_close WS + clears local modal (local_push only)
- [Back to List] clears local modal only; remote keeps showing current poster
- Bottom bar hidden on controller=remote (kiosk display-screen mode)
- native pinch-to-zoom via touch-action: pinch-zoom on img (no JS library needed)
- pb-14 on modal bodyClass prevents buttons from overlapping poster content
This commit is contained in:
@@ -371,6 +371,13 @@
|
||||
clearInterval(idle_timer_interval);
|
||||
saver_looping = false;
|
||||
restartCountdown();
|
||||
} else if (cmd.startsWith('ae_zoom:')) {
|
||||
// WHY: Controller can push zoom state to the remote display so both
|
||||
// devices stay in sync (e.g. operator zooms to show detail to an attendee
|
||||
// while the wall screen also zooms in for the room to see).
|
||||
const zoom_target = cmd.split(':')[1];
|
||||
if (zoom_target === 'fit') modal_zoom_fit = true;
|
||||
else if (zoom_target === 'zoom') modal_zoom_fit = false;
|
||||
} else if (cmd.startsWith('ae_refresh:')) {
|
||||
if (cmd.split(':')[1] == 'now') location.reload();
|
||||
}
|
||||
@@ -417,6 +424,9 @@
|
||||
|
||||
let idle_timer_interval: any = $state();
|
||||
let saver_looping: boolean = $state(false);
|
||||
// Tracks fit vs. zoom mode for the poster modal.
|
||||
// Reset to fit whenever a new poster opens so every poster starts clean.
|
||||
let modal_zoom_fit: boolean = $state(true);
|
||||
|
||||
function handle_idle_client() {
|
||||
if (
|
||||
@@ -473,6 +483,13 @@
|
||||
saver_looping = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset to fit-mode whenever a different poster is opened.
|
||||
$effect(() => {
|
||||
if ($events_sess.launcher.modal__open_event_file_id) {
|
||||
modal_zoom_fit = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -908,7 +925,7 @@
|
||||
{$events_loc.launcher.controller == 'remote' ? 'min-h-full' : ''}
|
||||
min-w-full
|
||||
"
|
||||
bodyClass="p-0 space-y-0 overflow-y-auto flex flex-col gap-1 items-center justify-center"
|
||||
bodyClass="p-0 space-y-0 overflow-auto flex flex-col gap-1 items-center justify-center pb-14"
|
||||
headerClass={`fixed top-0 right-0 left-0 p-1 md:p-2 flex flex-row items-center ${$events_loc.launcher.controller == 'remote' ? 'hidden' : ''} bg-white dark:bg-gray-800 opacity-50 ${$events_loc.launcher.hide__modal_header_title ? 'justify-center' : 'justify-between'}`}
|
||||
footerClass="text-center hidden"
|
||||
>
|
||||
@@ -932,96 +949,130 @@
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$events_sess.launcher.controller_cmd = `ae_close:event_file_modal`;
|
||||
$events_sess.launcher.controller_trigger_send = true;
|
||||
}}
|
||||
class="
|
||||
absolute top-0 right-12
|
||||
m-1 p-1
|
||||
btn btn-sm
|
||||
preset-tonal-error preset-outlined-error hover:preset-filled-success-200-800
|
||||
opacity-80 hover:opacity-100 active:opactiy-100
|
||||
transition-all
|
||||
"
|
||||
class:hidden={$events_loc.launcher.controller != 'local_push' ||
|
||||
$events_sess.launcher.ws_connect_status != 'connected'}
|
||||
title="Close the remote device's display of the poster"
|
||||
<!-- WHY: overflow-auto on wrapper + touch-action: pinch-zoom on the img enables
|
||||
native pinch-to-zoom on mobile/tablet without any JS library.
|
||||
Toggling modal_zoom_fit switches between 'fit to viewport' (default, clean display)
|
||||
and 'natural size' mode where the operator can pan/scroll and pinch-zoom freely
|
||||
for closer inspection or accessibility accommodation. -->
|
||||
<div
|
||||
class="w-full flex-1 flex items-center justify-center"
|
||||
class:overflow-auto={!modal_zoom_fit}
|
||||
class:overflow-hidden={modal_zoom_fit}
|
||||
>
|
||||
<span class="fas fa-times m-1"></span>
|
||||
<span class="fas fa-tv"></span>
|
||||
Close Remote Poster Display Only
|
||||
</button>
|
||||
{#if $events_sess.launcher.modal__event_file_obj?.hosted_file_id}
|
||||
<!-- WHY: Use hosted_file endpoint (not event_file) — the event_file download
|
||||
endpoint requires auth headers that a plain <img> tag cannot send (→ 403).
|
||||
The hosted_file endpoint accepts key=account_id as a query param and is
|
||||
the proven browser-compatible path for direct file display. -->
|
||||
<img
|
||||
src="{$ae_api.base_url}/v3/action/hosted_file/{$events_sess.launcher
|
||||
.modal__event_file_obj.hosted_file_id}/download?return_file=true&filename={encodeURIComponent(
|
||||
$events_sess.launcher.modal__event_file_obj.filename ?? ''
|
||||
)}&key={$ae_api.account_id}"
|
||||
alt="Poster: {$events_sess.launcher.modal__title}"
|
||||
ondblclick={() => {
|
||||
modal_zoom_fit = !modal_zoom_fit;
|
||||
// Sync zoom state to the remote display when acting as controller.
|
||||
if ($events_loc.launcher.controller == 'local_push' && $events_sess.launcher.ws_connect_status == 'connected') {
|
||||
$events_sess.launcher.controller_cmd = `ae_zoom:${modal_zoom_fit ? 'fit' : 'zoom'}`;
|
||||
$events_sess.launcher.controller_trigger_send = true;
|
||||
}
|
||||
}}
|
||||
title="Double-tap to toggle zoom / fit"
|
||||
class="block transition-[max-height,max-width,width] duration-200"
|
||||
class:max-h-[85dvh]={modal_zoom_fit}
|
||||
class:max-w-full={modal_zoom_fit}
|
||||
class:w-auto={modal_zoom_fit}
|
||||
class:object-contain={modal_zoom_fit}
|
||||
class:cursor-zoom-in={modal_zoom_fit}
|
||||
class:cursor-zoom-out={!modal_zoom_fit}
|
||||
style="touch-action: pinch-zoom;"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex flex-row items-center justify-center p-4">
|
||||
<span class="fas fa-info-circle mx-1"></span>
|
||||
<span>No image selected</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $events_sess.launcher.modal__event_file_obj?.hosted_file_id}
|
||||
<!-- WHY: Use hosted_file endpoint (not event_file) — the event_file download
|
||||
endpoint requires auth headers that a plain <img> tag cannot send (→ 403).
|
||||
The hosted_file endpoint accepts key=account_id as a query param and is
|
||||
the proven browser-compatible path for direct file display. -->
|
||||
<img
|
||||
src="{$ae_api.base_url}/v3/action/hosted_file/{$events_sess.launcher
|
||||
.modal__event_file_obj.hosted_file_id}/download?return_file=true&filename={encodeURIComponent(
|
||||
$events_sess.launcher.modal__event_file_obj.filename ?? ''
|
||||
)}&key={$ae_api.account_id}"
|
||||
alt="Poster"
|
||||
class="min-h-28 min-w-md max-h-full max-w-full"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex flex-row items-center justify-center p-4">
|
||||
<span class="fas fa-info-circle mx-1"></span>
|
||||
<span>No image selected</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$events_sess.launcher.controller_cmd = `ae_close:event_file_modal`;
|
||||
$events_sess.launcher.controller_trigger_send = true;
|
||||
}}
|
||||
<!-- Bottom control bar — hidden on the remote display (operator-free kiosk) -->
|
||||
<!-- WHY: pb-14 on bodyClass reserves space so these buttons don't obscure poster content. -->
|
||||
<div
|
||||
class="
|
||||
absolute bottom-0 left-12
|
||||
m-1 p-1
|
||||
btn btn-sm
|
||||
preset-tonal-error preset-outlined-error hover:preset-filled-success-200-800
|
||||
opacity-80 hover:opacity-100 active:opactiy-100
|
||||
transition-all
|
||||
absolute bottom-0 left-0 right-0
|
||||
flex flex-row items-center justify-between gap-2
|
||||
p-1.5
|
||||
bg-black/30 backdrop-blur-sm
|
||||
"
|
||||
class:hidden={$events_loc.launcher.controller != 'local_push' ||
|
||||
$events_sess.launcher.ws_connect_status != 'connected'}
|
||||
title="Close the remote device's display of the poster"
|
||||
class:hidden={$events_loc.launcher.controller == 'remote'}
|
||||
>
|
||||
<span class="fas fa-times m-1"></span>
|
||||
<span class="fas fa-tv"></span>
|
||||
Close Remote Poster Display Only
|
||||
</button>
|
||||
<!-- Zoom / Fit toggle: accessibility accommodation — lets operators and general
|
||||
public zoom in to read details, pinch on mobile, or double-tap the image. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
modal_zoom_fit = !modal_zoom_fit;
|
||||
// Sync zoom state to the remote display when acting as controller.
|
||||
if ($events_loc.launcher.controller == 'local_push' && $events_sess.launcher.ws_connect_status == 'connected') {
|
||||
$events_sess.launcher.controller_cmd = `ae_zoom:${modal_zoom_fit ? 'fit' : 'zoom'}`;
|
||||
$events_sess.launcher.controller_trigger_send = true;
|
||||
}
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-surface opacity-80 hover:opacity-100 transition-opacity"
|
||||
title={modal_zoom_fit
|
||||
? 'Pan / Zoom mode — pinch or double-tap image to zoom'
|
||||
: 'Fit image to screen'}
|
||||
>
|
||||
{#if modal_zoom_fit}
|
||||
<span class="fas fa-search-plus mr-1"></span>
|
||||
<span class="hidden sm:inline">Zoom</span>
|
||||
{:else}
|
||||
<span class="fas fa-compress mr-1"></span>
|
||||
<span class="hidden sm:inline">Fit</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$events_sess.launcher.modal__title = '';
|
||||
$events_sess.launcher.modal__open_event_file_id = null;
|
||||
$events_sess.launcher.modal__event_file_obj = null;
|
||||
}}
|
||||
class="
|
||||
absolute bottom-0 right-12
|
||||
m-1 p-1
|
||||
btn btn-sm
|
||||
preset-tonal-success preset-outlined-success hover:preset-filled-success-200-800
|
||||
opacity-80 hover:opacity-100 active:opactiy-100
|
||||
transition-all
|
||||
"
|
||||
class:hidden={!$ae_loc.trusted_access &&
|
||||
($events_loc.launcher.controller != 'local_push' ||
|
||||
$events_sess.launcher.ws_connect_status != 'connected')}
|
||||
title="Close this controller's local modal display of this poster"
|
||||
>
|
||||
<span class="fas fa-times m-1"></span>
|
||||
<span class="fas fa-list"></span>
|
||||
Close Poster on This Device
|
||||
</button>
|
||||
<!-- Close Both: sends WS close to remote display AND dismisses this modal.
|
||||
Remote screensaver will resume cycling after idle timeout. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$events_sess.launcher.controller_cmd = `ae_close:event_file_modal`;
|
||||
$events_sess.launcher.controller_trigger_send = true;
|
||||
$events_sess.launcher.modal__title = '';
|
||||
$events_sess.launcher.modal__open_event_file_id = null;
|
||||
$events_sess.launcher.modal__event_file_obj = null;
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-error opacity-80 hover:opacity-100 transition-all"
|
||||
class:hidden={$events_loc.launcher.controller != 'local_push' ||
|
||||
$events_sess.launcher.ws_connect_status != 'connected'}
|
||||
title="Close poster on this device and on the remote display (screensaver resumes)"
|
||||
>
|
||||
<span class="fas fa-tv mr-1"></span>
|
||||
<span class="fas fa-times"></span>
|
||||
<span class="hidden sm:inline ml-1">Close Both</span>
|
||||
</button>
|
||||
|
||||
<!-- Back to List: dismisses this controller's view only.
|
||||
Remote display keeps showing the current poster until next action or screensaver. -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
$events_sess.launcher.modal__title = '';
|
||||
$events_sess.launcher.modal__open_event_file_id = null;
|
||||
$events_sess.launcher.modal__event_file_obj = null;
|
||||
}}
|
||||
class="btn btn-sm preset-tonal-surface border border-surface-400/50 opacity-80 hover:opacity-100 transition-all"
|
||||
class:hidden={!$ae_loc.trusted_access &&
|
||||
($events_loc.launcher.controller != 'local_push' ||
|
||||
$events_sess.launcher.ws_connect_status != 'connected')}
|
||||
title="Close poster on this device only — remote display keeps showing"
|
||||
>
|
||||
<span class="fas fa-list mr-1"></span>
|
||||
<span class="hidden sm:inline">Back to List</span>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{#if $events_loc.launcher.controller_group_code && $events_loc.launcher.ws_connect}
|
||||
|
||||
Reference in New Issue
Block a user