feat(events): restore PDF→webp convert button in event file table

Adds a per-row "Convert PDF → Image" button in ae_comp__event_file_obj_tbl.
Only shown when edit_mode is on, the file is a PDF, and the session
type_code is 'poster' — poster sessions need images in the Launcher modal
(which uses <img>, not a PDF viewer).

Calls GET /v3/action/hosted_file/{id}/convert_file (pdf2image, 3840px wide,
first page, saves as a new hosted_file linked to the same parent object).
Per-row status tracking: idle → converting → done | error with retry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-11 13:01:26 -04:00
parent 241e05bc79
commit 2e08f5ff15

View File

@@ -65,6 +65,53 @@
let horiz_scroll_warning: boolean = $state(false);
let horiz_check_element: HTMLElement | null = $state(null);
// PDF → Image conversion state (keyed by event_file_id)
// WHY: Poster sessions display files in the Launcher modal via <img> tag — PDFs can't render there.
// Presenters upload PDFs which must be converted server-side to high-res webp images.
// Button is only shown in edit_mode for PDF files linked to a poster-type session.
// Backend: /v3/action/hosted_file/{id}/convert_file runs pdf2image (3840px wide, first page only)
// and saves the result as a new hosted_file record linked to the same parent object.
type ConvertStatus = 'idle' | 'converting' | 'done' | 'error';
let convert_status_kv: Record<string, ConvertStatus> = $state({});
let convert_result_kv: Record<string, any> = $state({});
async function handle_convert_pdf_to_image(event_file_obj: any) {
const file_id = event_file_obj.event_file_id;
convert_status_kv[file_id] = 'converting';
try {
// Link the new image to the most specific parent available
const link_to_type = event_file_obj.event_session_id ? 'event_session'
: event_file_obj.event_presentation_id ? 'event_presentation'
: 'event';
const link_to_id = event_file_obj.event_session_id
|| event_file_obj.event_presentation_id
|| event_file_obj.event_id;
const filename_no_ext = (event_file_obj.filename ?? 'poster_image').replace(/\.pdf$/i, '');
const url = `${$ae_api.base_url}/v3/action/hosted_file/${event_file_obj.hosted_file_id}/convert_file`
+ `?link_to_type=${encodeURIComponent(link_to_type)}`
+ `&link_to_id=${encodeURIComponent(link_to_id)}`
+ `&filename_no_ext=${encodeURIComponent(filename_no_ext)}`
+ `&to_type=webp`;
const resp = await fetch(url, {
headers: {
'x-aether-api-key': $ae_api.api_secret_key,
'x-account-id': String($ae_api.account_id ?? '')
}
});
const body = await resp.json();
if (resp.ok && body?.data) {
convert_result_kv[file_id] = body.data;
convert_status_kv[file_id] = 'done';
} else {
console.error('[convert_pdf] API error:', body);
convert_status_kv[file_id] = 'error';
}
} catch (err) {
console.error('[convert_pdf] Fetch failed:', err);
convert_status_kv[file_id] = 'error';
}
}
// Check if element is scrolling horizontally
$effect(() => {
if (
@@ -437,7 +484,44 @@
max_filename={50}
classes="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 min-w-72"
/>
<!-- {event_file_obj?.filename} -->
<!-- PDF → webp convert button: only for poster sessions in edit mode -->
{#if $ae_loc.edit_mode && event_file_obj?.extension === 'pdf' && event_file_obj?.event_session_type_code === 'poster'}
<div class="mt-1">
{#if !convert_status_kv[event_file_obj.event_file_id] || convert_status_kv[event_file_obj.event_file_id] === 'idle'}
<button
type="button"
class="btn btn-sm preset-tonal-warning border border-warning-500"
title="Convert this PDF to a high-res webp image for use in the Launcher poster display."
onclick={() => handle_convert_pdf_to_image(event_file_obj)}
>
<span class="fas fa-file-image mx-1"></span>
Convert PDF → Image
</button>
{:else if convert_status_kv[event_file_obj.event_file_id] === 'converting'}
<span class="btn btn-sm preset-tonal-surface opacity-60 cursor-wait">
<span class="fas fa-spinner fa-spin mx-1"></span>
Converting…
</span>
{:else if convert_status_kv[event_file_obj.event_file_id] === 'done'}
<span class="btn btn-sm preset-tonal-success" title="Conversion complete. New webp hosted_file created: {convert_result_kv[event_file_obj.event_file_id]?.filename ?? ''}">
<span class="fas fa-check mx-1"></span>
Done — {convert_result_kv[event_file_obj.event_file_id]?.filename ?? 'image created'}
</span>
{:else if convert_status_kv[event_file_obj.event_file_id] === 'error'}
<button
type="button"
class="btn btn-sm preset-tonal-error border border-error-500"
title="Conversion failed. Click to retry."
onclick={() => {
convert_status_kv[event_file_obj.event_file_id] = 'idle';
}}
>
<span class="fas fa-exclamation-triangle mx-1"></span>
Failed — Retry?
</button>
{/if}
</div>
{/if}
</td>
<td
class="px-4 py-2 flex flex-col gap-0.5"