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);
|
clearInterval(idle_timer_interval);
|
||||||
saver_looping = false;
|
saver_looping = false;
|
||||||
restartCountdown();
|
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:')) {
|
} else if (cmd.startsWith('ae_refresh:')) {
|
||||||
if (cmd.split(':')[1] == 'now') location.reload();
|
if (cmd.split(':')[1] == 'now') location.reload();
|
||||||
}
|
}
|
||||||
@@ -417,6 +424,9 @@
|
|||||||
|
|
||||||
let idle_timer_interval: any = $state();
|
let idle_timer_interval: any = $state();
|
||||||
let saver_looping: boolean = $state(false);
|
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() {
|
function handle_idle_client() {
|
||||||
if (
|
if (
|
||||||
@@ -473,6 +483,13 @@
|
|||||||
saver_looping = false;
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -908,7 +925,7 @@
|
|||||||
{$events_loc.launcher.controller == 'remote' ? 'min-h-full' : ''}
|
{$events_loc.launcher.controller == 'remote' ? 'min-h-full' : ''}
|
||||||
min-w-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'}`}
|
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"
|
footerClass="text-center hidden"
|
||||||
>
|
>
|
||||||
@@ -932,96 +949,130 @@
|
|||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<button
|
<!-- WHY: overflow-auto on wrapper + touch-action: pinch-zoom on the img enables
|
||||||
type="button"
|
native pinch-to-zoom on mobile/tablet without any JS library.
|
||||||
onclick={() => {
|
Toggling modal_zoom_fit switches between 'fit to viewport' (default, clean display)
|
||||||
$events_sess.launcher.controller_cmd = `ae_close:event_file_modal`;
|
and 'natural size' mode where the operator can pan/scroll and pinch-zoom freely
|
||||||
$events_sess.launcher.controller_trigger_send = true;
|
for closer inspection or accessibility accommodation. -->
|
||||||
}}
|
<div
|
||||||
class="
|
class="w-full flex-1 flex items-center justify-center"
|
||||||
absolute top-0 right-12
|
class:overflow-auto={!modal_zoom_fit}
|
||||||
m-1 p-1
|
class:overflow-hidden={modal_zoom_fit}
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<span class="fas fa-times m-1"></span>
|
{#if $events_sess.launcher.modal__event_file_obj?.hosted_file_id}
|
||||||
<span class="fas fa-tv"></span>
|
<!-- WHY: Use hosted_file endpoint (not event_file) — the event_file download
|
||||||
Close Remote Poster Display Only
|
endpoint requires auth headers that a plain <img> tag cannot send (→ 403).
|
||||||
</button>
|
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}
|
<!-- Bottom control bar — hidden on the remote display (operator-free kiosk) -->
|
||||||
<!-- WHY: Use hosted_file endpoint (not event_file) — the event_file download
|
<!-- WHY: pb-14 on bodyClass reserves space so these buttons don't obscure poster content. -->
|
||||||
endpoint requires auth headers that a plain <img> tag cannot send (→ 403).
|
<div
|
||||||
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;
|
|
||||||
}}
|
|
||||||
class="
|
class="
|
||||||
absolute bottom-0 left-12
|
absolute bottom-0 left-0 right-0
|
||||||
m-1 p-1
|
flex flex-row items-center justify-between gap-2
|
||||||
btn btn-sm
|
p-1.5
|
||||||
preset-tonal-error preset-outlined-error hover:preset-filled-success-200-800
|
bg-black/30 backdrop-blur-sm
|
||||||
opacity-80 hover:opacity-100 active:opactiy-100
|
|
||||||
transition-all
|
|
||||||
"
|
"
|
||||||
class:hidden={$events_loc.launcher.controller != 'local_push' ||
|
class:hidden={$events_loc.launcher.controller == 'remote'}
|
||||||
$events_sess.launcher.ws_connect_status != 'connected'}
|
|
||||||
title="Close the remote device's display of the poster"
|
|
||||||
>
|
>
|
||||||
<span class="fas fa-times m-1"></span>
|
<!-- Zoom / Fit toggle: accessibility accommodation — lets operators and general
|
||||||
<span class="fas fa-tv"></span>
|
public zoom in to read details, pinch on mobile, or double-tap the image. -->
|
||||||
Close Remote Poster Display Only
|
<button
|
||||||
</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
|
<!-- Close Both: sends WS close to remote display AND dismisses this modal.
|
||||||
type="button"
|
Remote screensaver will resume cycling after idle timeout. -->
|
||||||
onclick={() => {
|
<button
|
||||||
$events_sess.launcher.modal__title = '';
|
type="button"
|
||||||
$events_sess.launcher.modal__open_event_file_id = null;
|
onclick={() => {
|
||||||
$events_sess.launcher.modal__event_file_obj = null;
|
$events_sess.launcher.controller_cmd = `ae_close:event_file_modal`;
|
||||||
}}
|
$events_sess.launcher.controller_trigger_send = true;
|
||||||
class="
|
$events_sess.launcher.modal__title = '';
|
||||||
absolute bottom-0 right-12
|
$events_sess.launcher.modal__open_event_file_id = null;
|
||||||
m-1 p-1
|
$events_sess.launcher.modal__event_file_obj = null;
|
||||||
btn btn-sm
|
}}
|
||||||
preset-tonal-success preset-outlined-success hover:preset-filled-success-200-800
|
class="btn btn-sm preset-tonal-error opacity-80 hover:opacity-100 transition-all"
|
||||||
opacity-80 hover:opacity-100 active:opactiy-100
|
class:hidden={$events_loc.launcher.controller != 'local_push' ||
|
||||||
transition-all
|
$events_sess.launcher.ws_connect_status != 'connected'}
|
||||||
"
|
title="Close poster on this device and on the remote display (screensaver resumes)"
|
||||||
class:hidden={!$ae_loc.trusted_access &&
|
>
|
||||||
($events_loc.launcher.controller != 'local_push' ||
|
<span class="fas fa-tv mr-1"></span>
|
||||||
$events_sess.launcher.ws_connect_status != 'connected')}
|
<span class="fas fa-times"></span>
|
||||||
title="Close this controller's local modal display of this poster"
|
<span class="hidden sm:inline ml-1">Close Both</span>
|
||||||
>
|
</button>
|
||||||
<span class="fas fa-times m-1"></span>
|
|
||||||
<span class="fas fa-list"></span>
|
<!-- Back to List: dismisses this controller's view only.
|
||||||
Close Poster on This Device
|
Remote display keeps showing the current poster until next action or screensaver. -->
|
||||||
</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="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>
|
</Modal>
|
||||||
|
|
||||||
{#if $events_loc.launcher.controller_group_code && $events_loc.launcher.ws_connect}
|
{#if $events_loc.launcher.controller_group_code && $events_loc.launcher.ws_connect}
|
||||||
|
|||||||
Reference in New Issue
Block a user