feat(pres_mgmt): prefix/suffix inputs, flex row layout, strip :443 from links
File Downloads report: add Prefix and Suffix inputs for filename customization (e.g. "2026_06__"), longest-filename length indicator with warning above 120 chars, and switch file rows from table to flex layout so Download/Copy Link buttons stay right-aligned regardless of filename length. Strip redundant :443 from https download URLs in both the file downloads report and the manage event file list clipboard links. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,9 @@ let ae_promises: key_val = $state({});
|
|||||||
let ae_tmp: key_val = $state({});
|
let ae_tmp: key_val = $state({});
|
||||||
ae_tmp.show__file_li = true;
|
ae_tmp.show__file_li = true;
|
||||||
ae_tmp.show__direct_download = pres_mgmt_loc.current.show__direct_download;
|
ae_tmp.show__direct_download = pres_mgmt_loc.current.show__direct_download;
|
||||||
|
|
||||||
|
// Strip :443 from https URLs — redundant and clutters shareable links.
|
||||||
|
let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2'));
|
||||||
// let ae_triggers: key_val = {};
|
// let ae_triggers: key_val = {};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -317,7 +320,7 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
|
|||||||
</span>
|
</span>
|
||||||
<MyClipboard
|
<MyClipboard
|
||||||
value={encodeURI(
|
value={encodeURI(
|
||||||
`${$ae_api.base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.filename)}&key=${$ae_api.account_id}`
|
`${base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.filename)}&key=${$ae_api.account_id}`
|
||||||
)}
|
)}
|
||||||
btn_text="Copy Original"
|
btn_text="Copy Original"
|
||||||
btn_title="Copy the direct download link to the clipboard."
|
btn_title="Copy the direct download link to the clipboard."
|
||||||
@@ -326,7 +329,7 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
|
|||||||
|
|
||||||
<MyClipboard
|
<MyClipboard
|
||||||
value={encodeURI(
|
value={encodeURI(
|
||||||
`${$ae_api.base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.event_session_code ?? '')}_${ae_util.clean_filename(event_file_obj?.event_presentation_name ?? event_file_obj?.event_session_name ?? '').substring(0, 30)}_${ae_util.clean_filename(event_file_obj?.event_presenter_full_name ?? '')}.${event_file_obj?.extension}&key=${$ae_api.account_id}`
|
`${base_url}/v3/action/event_file/${event_file_obj?.event_file_id}/download?filename=${ae_util.clean_filename(event_file_obj?.event_session_code ?? '')}_${ae_util.clean_filename(event_file_obj?.event_presentation_name ?? event_file_obj?.event_session_name ?? '').substring(0, 30)}_${ae_util.clean_filename(event_file_obj?.event_presenter_full_name ?? '')}.${event_file_obj?.extension}&key=${$ae_api.account_id}`
|
||||||
)}
|
)}
|
||||||
btn_text="Copy Renamed"
|
btn_text="Copy Renamed"
|
||||||
btn_title="Copy the renamed download link to the clipboard. Format: [session-code]_[presentation-name]_[presenter-name].[ext]"
|
btn_title="Copy the renamed download link to the clipboard. Format: [session-code]_[presentation-name]_[presenter-name].[ext]"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||||
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
|
import { ae_api } from '$lib/stores/ae_stores';
|
||||||
import { events_slct, events_sess } from '$lib/stores/ae_events_stores';
|
import { events_slct, events_sess } from '$lib/stores/ae_events_stores';
|
||||||
import { events_func } from '$lib/ae_events/ae_events_functions';
|
import { events_func } from '$lib/ae_events/ae_events_functions';
|
||||||
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
||||||
@@ -30,15 +30,25 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
User,
|
TriangleAlert,
|
||||||
Users
|
User
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Filename helpers
|
// Filename helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Strip OS-unsafe chars AND periods (no periods in filename stems).
|
let prefix_val = $state('');
|
||||||
|
let suffix_val = $state('');
|
||||||
|
|
||||||
|
// For prefix/suffix: clean OS-unsafe chars and periods, but preserve leading/trailing
|
||||||
|
// underscores — users intentionally use them as separators (e.g. "2026_06__").
|
||||||
|
function clean_affix(s: string): string {
|
||||||
|
if (!s) return '';
|
||||||
|
return ae_util.clean_filename(s).replace(/\./g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
// For filename parts: also strip leading/trailing underscores and collapse runs.
|
||||||
function clean_part(s: any, max?: number): string {
|
function clean_part(s: any, max?: number): string {
|
||||||
if (!s) return '';
|
if (!s) return '';
|
||||||
const out = ae_util
|
const out = ae_util
|
||||||
@@ -71,19 +81,18 @@ type FormatKey =
|
|||||||
| 'session_date_presentation_presenter';
|
| 'session_date_presentation_presenter';
|
||||||
|
|
||||||
const FORMAT_PRESETS: Record<FormatKey, { label: string; group: string }> = {
|
const FORMAT_PRESETS: Record<FormatKey, { label: string; group: string }> = {
|
||||||
original: { label: 'Original filename', group: 'Universal' },
|
original: { label: 'Original filename', group: 'Universal' },
|
||||||
session_code: { label: 'Session Code', group: 'Session' },
|
session_code: { label: 'Session Code', group: 'Session' },
|
||||||
session_code_name: { label: 'Session Code + Name', group: 'Session' },
|
session_code_name: { label: 'Session Code + Name', group: 'Session' },
|
||||||
session_code_date: { label: 'Session Code + Date', group: 'Session' },
|
session_code_date: { label: 'Session Code + Date', group: 'Session' },
|
||||||
session_code_date_time: { label: 'Session Code + Date + Time', group: 'Session' },
|
session_code_date_time: { label: 'Session Code + Date + Time', group: 'Session' },
|
||||||
session_code_date_name: { label: 'Session Code + Date + Name', group: 'Session' },
|
session_code_date_name: { label: 'Session Code + Date + Name', group: 'Session' },
|
||||||
session_presenter: { label: 'Session Code + Presenter (Family, Given)', group: 'Presenter' },
|
session_presenter: { label: 'Session Code + Presenter (Family, Given)', group: 'Presenter' },
|
||||||
session_date_presenter: { label: 'Session Code + Date + Presenter', group: 'Presenter' },
|
session_date_presenter: { label: 'Session Code + Date + Presenter', group: 'Presenter' },
|
||||||
session_presentation_presenter: { label: 'Session Code + Presentation + Presenter', group: 'Presenter' },
|
session_presentation_presenter: { label: 'Session Code + Presentation + Presenter', group: 'Presenter' },
|
||||||
session_date_presentation_presenter: { label: 'Session Code + Date + Presentation + Presenter (Full)', group: 'Presenter' }
|
session_date_presentation_presenter: { label: 'Session Code + Date + Presentation + Presenter (Full)', group: 'Presenter' }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Separate group labels for the <optgroup> elements
|
|
||||||
const FORMAT_GROUPS: Record<string, FormatKey[]> = {
|
const FORMAT_GROUPS: Record<string, FormatKey[]> = {
|
||||||
Universal: ['original'],
|
Universal: ['original'],
|
||||||
Session: ['session_code', 'session_code_name', 'session_code_date', 'session_code_date_time', 'session_code_date_name'],
|
Session: ['session_code', 'session_code_name', 'session_code_date', 'session_code_date_time', 'session_code_date_name'],
|
||||||
@@ -92,43 +101,73 @@ const FORMAT_GROUPS: Record<string, FormatKey[]> = {
|
|||||||
|
|
||||||
let selected_format = $state<FormatKey>('session_code_name');
|
let selected_format = $state<FormatKey>('session_code_name');
|
||||||
|
|
||||||
|
const FILENAME_WARN_LEN = 120; // show warning above this char count
|
||||||
|
|
||||||
function build_filename(file: any, fmt: FormatKey): string {
|
function build_filename(file: any, fmt: FormatKey): string {
|
||||||
const ext = file.extension ?? 'file';
|
const ext = file.extension ?? 'file';
|
||||||
const session_code = clean_part(file.event_session_code);
|
const pre = clean_affix(prefix_val);
|
||||||
const session_name = clean_part(file.event_session_name, 30);
|
const suf = clean_affix(suffix_val);
|
||||||
const session_date = date_part(file.event_session_start_datetime);
|
|
||||||
const session_time = time_part(file.event_session_start_datetime);
|
|
||||||
const pres_name = clean_part(file.event_presentation_name ?? file.event_session_name, 25);
|
|
||||||
const fam = clean_part(file.event_presenter_family_name);
|
|
||||||
const given = clean_part(file.event_presenter_given_name);
|
|
||||||
|
|
||||||
if (fmt === 'original') return file.filename ?? `file.${ext}`;
|
let stem: string;
|
||||||
|
if (fmt === 'original') {
|
||||||
|
// Preserve the original stem as-is; only the prefix/suffix are user-controlled.
|
||||||
|
stem = file.filename_no_ext ?? (file.filename ?? 'file').replace(/\.[^.]+$/, '');
|
||||||
|
} else {
|
||||||
|
const session_code = clean_part(file.event_session_code);
|
||||||
|
const session_name = clean_part(file.event_session_name, 30);
|
||||||
|
const session_date = date_part(file.event_session_start_datetime);
|
||||||
|
const session_time = time_part(file.event_session_start_datetime);
|
||||||
|
const pres_name = clean_part(file.event_presentation_name ?? file.event_session_name, 25);
|
||||||
|
const fam = clean_part(file.event_presenter_family_name);
|
||||||
|
const given = clean_part(file.event_presenter_given_name);
|
||||||
|
|
||||||
let parts: string[];
|
let parts: string[];
|
||||||
switch (fmt) {
|
switch (fmt) {
|
||||||
case 'session_code': parts = [session_code]; break;
|
case 'session_code': parts = [session_code]; break;
|
||||||
case 'session_code_name': parts = [session_code, session_name]; break;
|
case 'session_code_name': parts = [session_code, session_name]; break;
|
||||||
case 'session_code_date': parts = [session_code, session_date]; break;
|
case 'session_code_date': parts = [session_code, session_date]; break;
|
||||||
case 'session_code_date_time': parts = [session_code, session_date, session_time]; break;
|
case 'session_code_date_time': parts = [session_code, session_date, session_time]; break;
|
||||||
case 'session_code_date_name': parts = [session_code, session_date, session_name]; break;
|
case 'session_code_date_name': parts = [session_code, session_date, session_name]; break;
|
||||||
case 'session_presenter': parts = [session_code, fam, given]; break;
|
case 'session_presenter': parts = [session_code, fam, given]; break;
|
||||||
case 'session_date_presenter': parts = [session_code, session_date, fam, given]; break;
|
case 'session_date_presenter': parts = [session_code, session_date, fam, given]; break;
|
||||||
case 'session_presentation_presenter': parts = [session_code, pres_name, fam, given]; break;
|
case 'session_presentation_presenter': parts = [session_code, pres_name, fam, given]; break;
|
||||||
case 'session_date_presentation_presenter': parts = [session_code, session_date, pres_name, fam, given]; break;
|
case 'session_date_presentation_presenter': parts = [session_code, session_date, pres_name, fam, given]; break;
|
||||||
default: return file.filename ?? `file.${ext}`;
|
default: stem = file.filename_no_ext ?? 'file'; return `${pre}${stem}${suf}.${ext}`;
|
||||||
|
}
|
||||||
|
stem = parts.filter(Boolean).join('_').replace(/_+/g, '_').replace(/^_|_$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const stem = parts.filter(Boolean).join('_').replace(/_+/g, '_').replace(/^_|_$/, '');
|
return `${pre}${stem}${suf}.${ext}`;
|
||||||
return `${stem}.${ext}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip :443 from https URLs — redundant and clutters shareable links.
|
||||||
|
let base_url = $derived($ae_api.base_url.replace(/^(https:\/\/[^/:]+):443(\/|$)/, '$1$2'));
|
||||||
|
|
||||||
function build_download_url(file: any, fmt: FormatKey): string {
|
function build_download_url(file: any, fmt: FormatKey): string {
|
||||||
const fname = build_filename(file, fmt);
|
const fname = build_filename(file, fmt);
|
||||||
return encodeURI(
|
return encodeURI(
|
||||||
`${$ae_api.base_url}/v3/action/event_file/${file.event_file_id}/download?filename=${ae_util.clean_filename(fname)}&key=${$ae_api.account_id}`
|
`${base_url}/v3/action/event_file/${file.event_file_id}/download?filename=${ae_util.clean_filename(fname)}&key=${$ae_api.account_id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Longest computed filename across all files — reactive to format + prefix + suffix changes.
|
||||||
|
let max_computed_len = $derived.by(() => {
|
||||||
|
let max = 0;
|
||||||
|
for (const sg of session_groups) {
|
||||||
|
for (const f of sg.session_files) {
|
||||||
|
const len = build_filename(f, selected_format).length;
|
||||||
|
if (len > max) max = len;
|
||||||
|
}
|
||||||
|
for (const pg of sg.presenter_groups) {
|
||||||
|
for (const f of pg.files) {
|
||||||
|
const len = build_filename(f, selected_format).length;
|
||||||
|
if (len > max) max = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Grouping
|
// Grouping
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -147,8 +186,8 @@ type SessionGroup = {
|
|||||||
session_code: string;
|
session_code: string;
|
||||||
session_name: string;
|
session_name: string;
|
||||||
session_start_datetime: string | null;
|
session_start_datetime: string | null;
|
||||||
session_files: any[]; // for_type = 'event_session'
|
session_files: any[];
|
||||||
presenter_groups: PresenterGroup[]; // for_type = 'event_presenter'
|
presenter_groups: PresenterGroup[];
|
||||||
_presenter_map: Record<string, PresenterGroup>;
|
_presenter_map: Record<string, PresenterGroup>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,8 +238,8 @@ let session_groups = $derived.by((): SessionGroup[] => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Sort presenter groups within each session client-side (family name, then given name).
|
// Sort presenter groups within each session client-side (family name, then given name).
|
||||||
// This can't be pushed to the API order_by_li — those are view-only joined columns
|
// Presenter-level columns are view-only joined fields — per API guide §3B they are
|
||||||
// that trigger a backend "account_id" WHERE ambiguity when used in ORDER BY.
|
// silently dropped from ORDER BY, so sorting must be done client-side.
|
||||||
for (const sg of sorted) {
|
for (const sg of sorted) {
|
||||||
sg.presenter_groups.sort((a, b) => {
|
sg.presenter_groups.sort((a, b) => {
|
||||||
const fam = a.presenter_family_name.localeCompare(b.presenter_family_name);
|
const fam = a.presenter_family_name.localeCompare(b.presenter_family_name);
|
||||||
@@ -212,8 +251,8 @@ let session_groups = $derived.by((): SessionGroup[] => {
|
|||||||
return sorted;
|
return sorted;
|
||||||
});
|
});
|
||||||
|
|
||||||
let total_session_files = $derived(session_groups.reduce((n, sg) => n + sg.session_files.length, 0));
|
let total_session_files = $derived(session_groups.reduce((n, sg) => n + sg.session_files.length, 0));
|
||||||
let total_presenter_files = $derived(session_groups.reduce((n, sg) => sg.presenter_groups.reduce((m, pg) => m + pg.files.length, 0) + n, 0));
|
let total_presenter_files = $derived(session_groups.reduce((n, sg) => sg.presenter_groups.reduce((m, pg) => m + pg.files.length, 0) + n, 0));
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Query
|
// Query
|
||||||
@@ -301,7 +340,7 @@ async function handle_qry() {
|
|||||||
<div class="flex flex-row flex-wrap items-center gap-2">
|
<div class="flex flex-row flex-wrap items-center gap-2">
|
||||||
<label class="flex flex-row items-center gap-2 text-sm font-semibold">
|
<label class="flex flex-row items-center gap-2 text-sm font-semibold">
|
||||||
<ArrowUpDown size="1em" />
|
<ArrowUpDown size="1em" />
|
||||||
Download filename format:
|
Format:
|
||||||
<select
|
<select
|
||||||
class="select ae_btn_info w-auto max-w-xs text-sm"
|
class="select ae_btn_info w-auto max-w-xs text-sm"
|
||||||
bind:value={selected_format}>
|
bind:value={selected_format}>
|
||||||
@@ -314,9 +353,38 @@ async function handle_qry() {
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<span class="text-surface-500 text-xs italic">
|
</div>
|
||||||
Applied to all download buttons below. "Copy Link" copies the same URL.
|
|
||||||
</span>
|
<!-- Prefix / suffix -->
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-3 text-sm">
|
||||||
|
<label class="flex flex-row items-center gap-1 font-semibold">
|
||||||
|
Prefix:
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input text-sm w-48"
|
||||||
|
placeholder="e.g. 2026_06__"
|
||||||
|
bind:value={prefix_val} />
|
||||||
|
</label>
|
||||||
|
<label class="flex flex-row items-center gap-1 font-semibold">
|
||||||
|
Suffix:
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input text-sm w-36"
|
||||||
|
placeholder="e.g. __final"
|
||||||
|
bind:value={suffix_val} />
|
||||||
|
</label>
|
||||||
|
{#if qry__count > 0}
|
||||||
|
<span
|
||||||
|
class="text-xs"
|
||||||
|
class:text-warning-600={max_computed_len > FILENAME_WARN_LEN}
|
||||||
|
class:opacity-50={max_computed_len <= FILENAME_WARN_LEN}
|
||||||
|
title="Longest computed filename across all files">
|
||||||
|
{#if max_computed_len > FILENAME_WARN_LEN}
|
||||||
|
<TriangleAlert size="0.9em" class="mr-0.5 inline-block" />
|
||||||
|
{/if}
|
||||||
|
longest: {max_computed_len} chars
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -370,49 +438,51 @@ async function handle_qry() {
|
|||||||
<FileText size="0.9em" />
|
<FileText size="0.9em" />
|
||||||
Session Files ({sg.session_files.length})
|
Session Files ({sg.session_files.length})
|
||||||
</h4>
|
</h4>
|
||||||
<table class="w-full table-auto text-sm">
|
<div class="flex flex-col">
|
||||||
<tbody>
|
{#each sg.session_files as file (file.event_file_id)}
|
||||||
{#each sg.session_files as file (file.event_file_id)}
|
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
||||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
{@const computed_name = build_filename(file, selected_format)}
|
||||||
{@const computed_name = build_filename(file, selected_format)}
|
{@const dl_url = build_download_url(file, selected_format)}
|
||||||
{@const dl_url = build_download_url(file, selected_format)}
|
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
|
||||||
<tr class="hover:bg-surface-100-900 border-surface-100-900 border-t transition-colors">
|
<div class="border-surface-100-900 hover:bg-surface-100-900 flex flex-row items-center gap-2 border-t py-1.5 transition-colors">
|
||||||
<td class="py-1 pr-2">
|
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
|
||||||
<span class="flex items-center gap-1 text-xs opacity-50" title={file.filename}>
|
<div class="flex min-w-0 grow flex-col gap-0.5">
|
||||||
<ExtIcon size="0.9em" />
|
<span class="flex items-center gap-1 truncate text-xs opacity-50" title={file.filename}>
|
||||||
{ae_util.shorten_filename({ filename: file.filename, max_length: 30 })}
|
<ExtIcon size="0.9em" class="shrink-0" />
|
||||||
</span>
|
{ae_util.shorten_filename({ filename: file.filename, max_length: 40 })}
|
||||||
</td>
|
</span>
|
||||||
<td class="px-2 py-1">
|
<span
|
||||||
<span class="font-mono text-xs opacity-70" title="Filename that will be used for download">
|
class="truncate font-mono text-xs"
|
||||||
{computed_name}
|
class:opacity-70={!is_long}
|
||||||
</span>
|
class:text-warning-600={is_long}
|
||||||
</td>
|
title="{computed_name}{is_long ? ` (${computed_name.length} chars — may be long for some systems)` : ''}">
|
||||||
<td class="px-1 py-1 text-right text-xs opacity-50 whitespace-nowrap">
|
{computed_name}
|
||||||
{ae_util.format_bytes(file.file_size)}
|
</span>
|
||||||
</td>
|
</div>
|
||||||
<td class="pl-2 py-1 whitespace-nowrap">
|
<!-- File size -->
|
||||||
<div class="flex flex-row items-center gap-1">
|
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
||||||
<a
|
{ae_util.format_bytes(file.file_size)}
|
||||||
href={dl_url}
|
</span>
|
||||||
download={computed_name}
|
<!-- Buttons -->
|
||||||
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
<div class="flex shrink-0 flex-row items-center gap-1">
|
||||||
title="Download as: {computed_name}">
|
<a
|
||||||
<Download size="0.9em" class="mr-0.5" />
|
href={dl_url}
|
||||||
Download
|
download={computed_name}
|
||||||
</a>
|
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
||||||
<MyClipboard
|
title="Download as: {computed_name}">
|
||||||
value={dl_url}
|
<Download size="0.9em" class="mr-0.5" />
|
||||||
btn_text="Copy Link"
|
Download
|
||||||
btn_title="Copy direct download link: {computed_name}"
|
</a>
|
||||||
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
<MyClipboard
|
||||||
/>
|
value={dl_url}
|
||||||
</div>
|
btn_text="Copy Link"
|
||||||
</td>
|
btn_title="Copy direct download link: {computed_name}"
|
||||||
</tr>
|
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
||||||
{/each}
|
/>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -427,49 +497,51 @@ async function handle_qry() {
|
|||||||
{/if}
|
{/if}
|
||||||
<span class="font-normal normal-case">({pg.files.length} file{pg.files.length !== 1 ? 's' : ''})</span>
|
<span class="font-normal normal-case">({pg.files.length} file{pg.files.length !== 1 ? 's' : ''})</span>
|
||||||
</h4>
|
</h4>
|
||||||
<table class="w-full table-auto text-sm">
|
<div class="flex flex-col">
|
||||||
<tbody>
|
{#each pg.files as file (file.event_file_id)}
|
||||||
{#each pg.files as file (file.event_file_id)}
|
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
||||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
{@const computed_name = build_filename(file, selected_format)}
|
||||||
{@const computed_name = build_filename(file, selected_format)}
|
{@const dl_url = build_download_url(file, selected_format)}
|
||||||
{@const dl_url = build_download_url(file, selected_format)}
|
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
|
||||||
<tr class="hover:bg-surface-100-900 border-surface-100-900 border-t transition-colors">
|
<div class="border-surface-100-900 hover:bg-surface-100-900 flex flex-row items-center gap-2 border-t py-1.5 transition-colors">
|
||||||
<td class="py-1 pr-2">
|
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
|
||||||
<span class="flex items-center gap-1 text-xs opacity-50" title={file.filename}>
|
<div class="flex min-w-0 grow flex-col gap-0.5">
|
||||||
<ExtIcon size="0.9em" />
|
<span class="flex items-center gap-1 truncate text-xs opacity-50" title={file.filename}>
|
||||||
{ae_util.shorten_filename({ filename: file.filename, max_length: 30 })}
|
<ExtIcon size="0.9em" class="shrink-0" />
|
||||||
</span>
|
{ae_util.shorten_filename({ filename: file.filename, max_length: 40 })}
|
||||||
</td>
|
</span>
|
||||||
<td class="px-2 py-1">
|
<span
|
||||||
<span class="font-mono text-xs opacity-70" title="Filename that will be used for download">
|
class="truncate font-mono text-xs"
|
||||||
{computed_name}
|
class:opacity-70={!is_long}
|
||||||
</span>
|
class:text-warning-600={is_long}
|
||||||
</td>
|
title="{computed_name}{is_long ? ` (${computed_name.length} chars — may be long for some systems)` : ''}">
|
||||||
<td class="px-1 py-1 text-right text-xs opacity-50 whitespace-nowrap">
|
{computed_name}
|
||||||
{ae_util.format_bytes(file.file_size)}
|
</span>
|
||||||
</td>
|
</div>
|
||||||
<td class="pl-2 py-1 whitespace-nowrap">
|
<!-- File size -->
|
||||||
<div class="flex flex-row items-center gap-1">
|
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
||||||
<a
|
{ae_util.format_bytes(file.file_size)}
|
||||||
href={dl_url}
|
</span>
|
||||||
download={computed_name}
|
<!-- Buttons -->
|
||||||
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
<div class="flex shrink-0 flex-row items-center gap-1">
|
||||||
title="Download as: {computed_name}">
|
<a
|
||||||
<Download size="0.9em" class="mr-0.5" />
|
href={dl_url}
|
||||||
Download
|
download={computed_name}
|
||||||
</a>
|
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
||||||
<MyClipboard
|
title="Download as: {computed_name}">
|
||||||
value={dl_url}
|
<Download size="0.9em" class="mr-0.5" />
|
||||||
btn_text="Copy Link"
|
Download
|
||||||
btn_title="Copy direct download link: {computed_name}"
|
</a>
|
||||||
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
<MyClipboard
|
||||||
/>
|
value={dl_url}
|
||||||
</div>
|
btn_text="Copy Link"
|
||||||
</td>
|
btn_title="Copy direct download link: {computed_name}"
|
||||||
</tr>
|
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
||||||
{/each}
|
/>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user