Files
OSIT-AE-App-Svelte/src/routes/events/[event_id]/(launcher)/launcher_file_cont.svelte
Scott Idem 752dca290a feat(launcher): dedicated Digital Poster session card-grid view
Create launcher_session_view_posters.svelte — a touch-first card-grid
layout for Digital Poster sessions, designed for tablet/phone PWA use.

Layout:
- 1 column on mobile, 2 on sm, 3 on xl
- Each poster card: title (line-clamp-3) + presenter name/affiliation +
  'Open Poster' action button at card bottom
- Poster code (or 1-based index fallback) badge in top-right corner
- Card active:scale-[0.98] for tactile touch press feedback
- Sticky compact session header strip with name, code, and poster count
- Optional 'Session Resources' strip for rare session-level files
- overflow-y-auto + grow so the grid scrolls; header strip stays fixed

Integration:
- launcher_session_view.svelte: import + delegate when type_code==='poster'
- launcher_file_cont.svelte: min-w-96 → w-full on poster button so it
  fills its container (card or list row) without overflow on small screens
- WS open/close/zoom command pipeline unchanged (all in launcher_file_cont
  and +layout.svelte which were not modified for the WS paths)
2026-03-13 12:42:12 -04:00

417 lines
17 KiB
Svelte

<script lang="ts">
interface Props {
log_lvl?: number;
event_file_id: string;
event_file_obj: any;
max_filename_length?: number;
hide_launch_icon?: boolean;
hide_meta?: boolean;
hide_created_on?: boolean;
hide_os?: boolean;
hide_size?: boolean;
hide_draft?: boolean;
show_bak_download?: boolean;
btn_size?: string;
btn_text_align?: string;
text_size?: string;
text_size_md?: string;
session_type?: string;
open_method?: null | string;
modal_title?: string;
modal__title?: any;
modal__open_event_file_id?: any;
modal__event_file_obj?: any;
}
let {
log_lvl = $bindable(0),
event_file_id,
event_file_obj = $bindable({}),
max_filename_length = $bindable(50),
hide_launch_icon = $bindable(false),
hide_meta = $bindable(false),
hide_created_on = $bindable(false),
hide_os = $bindable(false),
hide_size = $bindable(false),
hide_draft = $bindable(false),
show_bak_download = false,
btn_size = $bindable('btn-sm'),
btn_text_align = $bindable('text-left'),
text_size = $bindable('text-sm'),
text_size_md = $bindable('md:text-base'),
session_type = $bindable('oral'),
open_method = $bindable('download'),
modal_title = $bindable(''),
modal__title = $bindable(''),
modal__open_event_file_id = $bindable(null),
modal__event_file_obj = $bindable(null)
}: Props = $props();
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { api } from '$lib/api/api';
import { ae_loc, ae_api, ae_sess, slct } from '$lib/stores/ae_stores';
import { core_func } from '$lib/ae_core/ae_core_functions';
import {
events_loc,
events_sess,
events_slct
} from '$lib/stores/ae_events_stores';
import { events_func } from '$lib/ae_events_functions';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
// Import the relay
import * as native from '$lib/electron/electron_relay';
let ae_promises: key_val = $state({});
let open_file_clicked: null | boolean = $state(null);
let open_file_status: null | string = $state(null);
let open_file_status_message: null | string = $state(null);
let screen_saver_exts = ['jpg', 'png', 'PNG', 'webp'];
onMount(() => {
if (screen_saver_exts.includes(event_file_obj.extension)) {
if (!$events_loc.launcher.screen_saver_img_kv)
$events_loc.launcher.screen_saver_img_kv = {};
$events_loc.launcher.screen_saver_img_kv[event_file_id] = {
...event_file_obj
};
}
});
async function handle_open_file() {
if (log_lvl) console.log('*** handle_open_file() ***');
if (open_file_clicked) return; // Hard Guard: Already processing
$events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj;
// 1. NATIVE MODE (Electron)
if ($ae_loc.is_native && $events_loc.launcher.app_mode === 'native') {
const cache_root = $ae_loc.local_file_cache_path;
const temp_root = $ae_loc.host_file_temp_path;
open_file_clicked = true;
open_file_status = 'checking_cache';
open_file_status_message = 'Checking local cache...';
const exists = await native.check_hash_file_cache({
cache_root,
hash: event_file_obj.hash_sha256,
verify_hash: true // Hardened: Trust No One!
});
if (!exists) {
open_file_status = 'downloading_file';
open_file_status_message = 'Downloading file to cache...';
// Use the PROVEN endpoint path from api.ts that is known to work in Default Mode.
const url = `${$ae_api.base_url}/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download?return_file=true&filename=${encodeURIComponent(event_file_obj.filename)}&key=${$ae_api.account_id}`;
const dl_result = await native.download_to_cache({
url,
cache_root,
hash: event_file_obj.hash_sha256,
api_key: $ae_api.api_secret_key,
account_id: $ae_api.account_id
});
if (!dl_result.success) {
open_file_status = 'error';
open_file_status_message = `Download failed: ${dl_result.error}`;
setTimeout(() => (open_file_clicked = false), 5000);
return false;
}
}
open_file_status = 'opening_file';
open_file_status_message = 'Opening Application';
// Phase 2/5: Use the atomic copy-and-launch operation.
// The main process handler (file_handlers.ts) now handles the
// specialized LibreOffice/AppleScript logic internally after copying.
const launch_result = await native.launch_from_cache({
cache_root,
hash: event_file_obj.hash_sha256,
temp_root,
filename: event_file_obj.filename
});
if (!launch_result.success) {
open_file_status = 'error';
open_file_status_message = `Failed to open: ${launch_result.error}`;
}
setTimeout(() => (open_file_clicked = false), 5000);
return launch_result.success;
}
// 2. ONSITE MODE (Browser with Modified Extensions)
else if ($events_loc.launcher.app_mode === 'onsite') {
open_file_clicked = true;
open_file_status = 'downloading_onsite';
open_file_status_message = 'Downloading (Onsite Mode)...';
let filename = event_file_obj.filename;
if (
(event_file_obj.extension === 'ppt' ||
event_file_obj.extension === 'pptx') &&
event_file_obj.open_in_os === 'win'
) {
filename = event_file_obj.filename + 'win';
}
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: filename,
x_no_account_id_token: 'direct-download'
},
filename: filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
// 3. DEFAULT MODE (Standard Browser)
else {
open_file_clicked = true;
open_file_status = 'downloading_default';
open_file_status_message = 'Downloading...';
const dl_promise = api.get_object({
api_cfg: $ae_api,
endpoint: `/v3/action/hosted_file/${event_file_obj.hosted_file_id}/download`,
params: {
filename: event_file_obj.filename,
x_no_account_id_token: 'direct-download'
},
filename: event_file_obj.filename,
return_blob: true,
auto_download: true,
log_lvl: 1
});
if ($events_loc.launcher.controller == 'local_push') {
$events_sess.launcher.controller_cmd = `ae_download:hosted_file=${event_file_obj.hosted_file_id}:${event_file_obj.filename}:${event_file_obj.extension}`;
$events_sess.launcher.controller_trigger_send = true;
}
setTimeout(() => (open_file_clicked = false), 5000);
return dl_promise;
}
}
function prevent_default<T extends Event>(fn: (event: T) => void) {
return function (event: T) {
event.preventDefault();
fn(event);
};
}
</script>
<div
class:justify-between={!hide_meta}
class:justify-center={hide_meta}
class:hidden={hide_draft &&
(event_file_obj.file_purpose == 'outline' ||
event_file_obj.file_purpose == 'draft')}
class="event_launcher_file_cont grow flex flex-col md:flex-row flex-wrap gap-1 items-center justify-center max-w-full transition-all"
>
{#if open_file_clicked}
<div
class="open_file_clicked alert"
in:fade={{ duration: 250 }}
out:fade={{ duration: 2000 }}
>
<div class="alert_msg_pulse">
<strong
>*** {open_file_status_message ||
'Please wait while this file downloads...'} ***</strong
>
</div>
{#if $ae_loc.is_native && $events_loc.launcher.app_mode === 'native'}
<p>Most files will automatically be opened full screen.</p>
<p>
PowerPoint or KeyNote will attempt to display in presenter view.
</p>
<p>Please close the file when finished.</p>
{/if}
</div>
{/if}
<span
class="event_file_action grow max-w-full flex flex-row flex-wrap gap-1 items-center justify-center"
>
{#if session_type == 'poster' || open_method == 'modal'}
<AE_Comp_Hosted_Files_Download_Button
hosted_file_id={event_file_id}
hosted_file_obj={event_file_obj}
classes="btn btn-sm md:btn-md lg:btn-lg preset-tonal-primary border border-primary-500 w-full"
click={() => {
modal__open_event_file_id = event_file_id;
modal__event_file_obj = event_file_obj;
if (!modal__title) modal__title = event_file_obj.filename;
$events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj;
// Push the open command to the remote display when in local_push mode
if ($events_loc.launcher.controller == 'local_push' && $events_sess.launcher.ws_connect_status == 'connected') {
$events_sess.launcher.controller_cmd = `ae_open:event_file=${event_file_id}`;
$events_sess.launcher.controller_trigger_send = true;
}
}}
>
{#snippet label()}
{#if screen_saver_exts.includes(event_file_obj.extension)}
<span
class="fas fa-chart-bar m-1"
class:hidden={hide_launch_icon}
></span> Open Poster
{:else}
<span
class="fas fa-paper-plane m-1"
class:hidden={hide_launch_icon}
></span>
{ae_util.shorten_filename({
filename: event_file_obj.filename,
max_length: max_filename_length
})}
{/if}
{/snippet}
</AE_Comp_Hosted_Files_Download_Button>
{:else}
<AE_Comp_Hosted_Files_Download_Button
hosted_file_id={event_file_id}
hosted_file_obj={event_file_obj}
classes="btn {btn_size} gap-1 justify-between min-w-full w-full max-w-96 preset-tonal-primary border border-primary-500"
click={handle_open_file}
>
{#snippet label()}
{@const file_id = event_file_obj.hosted_file_id}
<span class="shrink text-xs border-r border-gray-400 pr-1">
{#await ae_promises[event_file_id]}
<span class="fas fa-spinner fa-spin mx-0.5"></span>
<span>
{#if $ae_sess.api_download_kv[file_id]}
{$ae_sess.api_download_kv[file_id]
.percent_completed}%
{:else}
...
{/if}
</span>
{:then result}
<span
class="fas fa-{ae_util.file_extension_icon(
event_file_obj.extension
)} mx-0.5"
></span>
{event_file_obj.extension}
{#if result === null || result === false}
<span class="text-error-500"
><span
class="fas fa-exclamation-triangle mx-1"
></span>Failed!</span
>
{/if}
{:catch error}
<span class="text-error-500" title={error?.message}
><span class="fas fa-exclamation-circle mx-0.5"
></span>Error!</span
>
{/await}
</span>
<span
class="grow {text_size} {text_size_md} w-full max-w-full overflow-hidden text-ellipsis {btn_text_align}"
>
{ae_util.shorten_string({
string: event_file_obj.filename_no_ext,
begin_length: 45,
max_length: 65
})}
</span>
<span
class="badge my-0 py-0.5 preset-tonal-success hover:preset-filled-success-500 text-xs xl:text-sm"
class:hidden={!event_file_obj.file_purpose}
>
{event_file_obj.file_purpose}
</span>
{/snippet}
</AE_Comp_Hosted_Files_Download_Button>
{/if}
</span>
<span
class="event_file_meta grow text-sm text-gray-500 flex flex-col sm:flex-row gap-1 wrap items-center justify-between w-64 max-w-80 font-mono"
class:hidden={hide_meta}
>
<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_v3({
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 transition-all group"
class:preset-tonal-success={event_file_obj?.open_in_os == 'win'}
class:preset-tonal-warning={event_file_obj?.open_in_os == 'mac'}
disabled={!$ae_loc.trusted_access}
>
{#if event_file_obj?.open_in_os == 'win'}<span
class="fab fa-windows m-1"
></span>
{:else if event_file_obj?.open_in_os == 'mac'}<span
class="fab fa-apple m-1"
></span>
{:else}<span class="fas fa-folder-open m-1"></span>{/if}
</button>
<span
class="event_file_created_on text-xs text-center flex flex-row gap-1 items-center justify-end w-24 md:w-44 preset-filled-surface-100-900 rounded px-1 py-0.5"
class:hidden={hide_created_on}
>
<span class="fas fa-calendar-day"></span>
<span class="w-18"
>{ae_util.iso_datetime_formatter(
event_file_obj.created_on,
'date_short'
)}</span
>
</span>
<span
class="event_file_size text-xs text-center flex flex-row gap-1 items-center justify-end preset-filled-surface-100-900 w-22 max-w-28 rounded py-0.5"
class:hidden={hide_size}
>
<span class="fas fa-save"></span>
{#if event_file_obj.file_size}{ae_util.format_bytes(
event_file_obj.file_size
)}{/if}
</span>
</span>
</div>