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({});
|
||||
ae_tmp.show__file_li = true;
|
||||
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 = {};
|
||||
|
||||
onMount(() => {
|
||||
@@ -317,7 +320,7 @@ async function handle_convert_pdf_to_image(event_file_obj: key_val) {
|
||||
</span>
|
||||
<MyClipboard
|
||||
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_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
|
||||
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_title="Copy the renamed download link to the clipboard. Format: [session-code]_[presentation-name]_[presenter-name].[ext]"
|
||||
|
||||
@@ -20,7 +20,7 @@ let {
|
||||
}: Props = $props();
|
||||
|
||||
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_func } from '$lib/ae_events/ae_events_functions';
|
||||
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
|
||||
@@ -30,15 +30,25 @@ import {
|
||||
FileText,
|
||||
LoaderCircle,
|
||||
RefreshCw,
|
||||
User,
|
||||
Users
|
||||
TriangleAlert,
|
||||
User
|
||||
} from '@lucide/svelte';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 {
|
||||
if (!s) return '';
|
||||
const out = ae_util
|
||||
@@ -71,19 +81,18 @@ type FormatKey =
|
||||
| 'session_date_presentation_presenter';
|
||||
|
||||
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_name: { label: 'Session Code + Name', 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_name: { label: 'Session Code + Date + Name', group: 'Session' },
|
||||
session_presenter: { label: 'Session Code + Presenter (Family, Given)', group: 'Presenter' },
|
||||
session_date_presenter: { label: 'Session Code + Date + Presenter', group: 'Presenter' },
|
||||
session_presentation_presenter: { label: 'Session Code + Presentation + Presenter', group: 'Presenter' },
|
||||
session_date_presenter: { label: 'Session Code + Date + 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' }
|
||||
};
|
||||
|
||||
// Separate group labels for the <optgroup> elements
|
||||
const FORMAT_GROUPS: Record<string, FormatKey[]> = {
|
||||
Universal: ['original'],
|
||||
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');
|
||||
|
||||
const FILENAME_WARN_LEN = 120; // show warning above this char count
|
||||
|
||||
function build_filename(file: any, fmt: FormatKey): string {
|
||||
const ext = file.extension ?? 'file';
|
||||
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);
|
||||
const ext = file.extension ?? 'file';
|
||||
const pre = clean_affix(prefix_val);
|
||||
const suf = clean_affix(suffix_val);
|
||||
|
||||
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[];
|
||||
switch (fmt) {
|
||||
case 'session_code': parts = [session_code]; 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_time': parts = [session_code, session_date, session_time]; 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_date_presenter': parts = [session_code, session_date, 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;
|
||||
default: return file.filename ?? `file.${ext}`;
|
||||
let parts: string[];
|
||||
switch (fmt) {
|
||||
case 'session_code': parts = [session_code]; 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_time': parts = [session_code, session_date, session_time]; 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_date_presenter': parts = [session_code, session_date, 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;
|
||||
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 `${stem}.${ext}`;
|
||||
return `${pre}${stem}${suf}.${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 {
|
||||
const fname = build_filename(file, fmt);
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -147,8 +186,8 @@ type SessionGroup = {
|
||||
session_code: string;
|
||||
session_name: string;
|
||||
session_start_datetime: string | null;
|
||||
session_files: any[]; // for_type = 'event_session'
|
||||
presenter_groups: PresenterGroup[]; // for_type = 'event_presenter'
|
||||
session_files: any[];
|
||||
presenter_groups: 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).
|
||||
// This can't be pushed to the API order_by_li — those are view-only joined columns
|
||||
// that trigger a backend "account_id" WHERE ambiguity when used in ORDER BY.
|
||||
// Presenter-level columns are view-only joined fields — per API guide §3B they are
|
||||
// silently dropped from ORDER BY, so sorting must be done client-side.
|
||||
for (const sg of sorted) {
|
||||
sg.presenter_groups.sort((a, b) => {
|
||||
const fam = a.presenter_family_name.localeCompare(b.presenter_family_name);
|
||||
@@ -212,8 +251,8 @@ let session_groups = $derived.by((): SessionGroup[] => {
|
||||
return sorted;
|
||||
});
|
||||
|
||||
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_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));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Query
|
||||
@@ -301,7 +340,7 @@ async function handle_qry() {
|
||||
<div class="flex flex-row flex-wrap items-center gap-2">
|
||||
<label class="flex flex-row items-center gap-2 text-sm font-semibold">
|
||||
<ArrowUpDown size="1em" />
|
||||
Download filename format:
|
||||
Format:
|
||||
<select
|
||||
class="select ae_btn_info w-auto max-w-xs text-sm"
|
||||
bind:value={selected_format}>
|
||||
@@ -314,9 +353,38 @@ async function handle_qry() {
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<span class="text-surface-500 text-xs italic">
|
||||
Applied to all download buttons below. "Copy Link" copies the same URL.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</header>
|
||||
|
||||
@@ -370,49 +438,51 @@ async function handle_qry() {
|
||||
<FileText size="0.9em" />
|
||||
Session Files ({sg.session_files.length})
|
||||
</h4>
|
||||
<table class="w-full table-auto text-sm">
|
||||
<tbody>
|
||||
{#each sg.session_files as file (file.event_file_id)}
|
||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
||||
{@const computed_name = build_filename(file, selected_format)}
|
||||
{@const dl_url = build_download_url(file, selected_format)}
|
||||
<tr class="hover:bg-surface-100-900 border-surface-100-900 border-t transition-colors">
|
||||
<td class="py-1 pr-2">
|
||||
<span class="flex items-center gap-1 text-xs opacity-50" title={file.filename}>
|
||||
<ExtIcon size="0.9em" />
|
||||
{ae_util.shorten_filename({ filename: file.filename, max_length: 30 })}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-1">
|
||||
<span class="font-mono text-xs opacity-70" title="Filename that will be used for download">
|
||||
{computed_name}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-1 py-1 text-right text-xs opacity-50 whitespace-nowrap">
|
||||
{ae_util.format_bytes(file.file_size)}
|
||||
</td>
|
||||
<td class="pl-2 py-1 whitespace-nowrap">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<a
|
||||
href={dl_url}
|
||||
download={computed_name}
|
||||
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
||||
title="Download as: {computed_name}">
|
||||
<Download size="0.9em" class="mr-0.5" />
|
||||
Download
|
||||
</a>
|
||||
<MyClipboard
|
||||
value={dl_url}
|
||||
btn_text="Copy Link"
|
||||
btn_title="Copy direct download link: {computed_name}"
|
||||
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex flex-col">
|
||||
{#each sg.session_files as file (file.event_file_id)}
|
||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
||||
{@const computed_name = build_filename(file, selected_format)}
|
||||
{@const dl_url = build_download_url(file, selected_format)}
|
||||
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
|
||||
<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">
|
||||
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
|
||||
<div class="flex min-w-0 grow flex-col gap-0.5">
|
||||
<span class="flex items-center gap-1 truncate text-xs opacity-50" title={file.filename}>
|
||||
<ExtIcon size="0.9em" class="shrink-0" />
|
||||
{ae_util.shorten_filename({ filename: file.filename, max_length: 40 })}
|
||||
</span>
|
||||
<span
|
||||
class="truncate font-mono text-xs"
|
||||
class:opacity-70={!is_long}
|
||||
class:text-warning-600={is_long}
|
||||
title="{computed_name}{is_long ? ` (${computed_name.length} chars — may be long for some systems)` : ''}">
|
||||
{computed_name}
|
||||
</span>
|
||||
</div>
|
||||
<!-- File size -->
|
||||
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
||||
{ae_util.format_bytes(file.file_size)}
|
||||
</span>
|
||||
<!-- Buttons -->
|
||||
<div class="flex shrink-0 flex-row items-center gap-1">
|
||||
<a
|
||||
href={dl_url}
|
||||
download={computed_name}
|
||||
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
||||
title="Download as: {computed_name}">
|
||||
<Download size="0.9em" class="mr-0.5" />
|
||||
Download
|
||||
</a>
|
||||
<MyClipboard
|
||||
value={dl_url}
|
||||
btn_text="Copy Link"
|
||||
btn_title="Copy direct download link: {computed_name}"
|
||||
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -427,49 +497,51 @@ async function handle_qry() {
|
||||
{/if}
|
||||
<span class="font-normal normal-case">({pg.files.length} file{pg.files.length !== 1 ? 's' : ''})</span>
|
||||
</h4>
|
||||
<table class="w-full table-auto text-sm">
|
||||
<tbody>
|
||||
{#each pg.files as file (file.event_file_id)}
|
||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
||||
{@const computed_name = build_filename(file, selected_format)}
|
||||
{@const dl_url = build_download_url(file, selected_format)}
|
||||
<tr class="hover:bg-surface-100-900 border-surface-100-900 border-t transition-colors">
|
||||
<td class="py-1 pr-2">
|
||||
<span class="flex items-center gap-1 text-xs opacity-50" title={file.filename}>
|
||||
<ExtIcon size="0.9em" />
|
||||
{ae_util.shorten_filename({ filename: file.filename, max_length: 30 })}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-1">
|
||||
<span class="font-mono text-xs opacity-70" title="Filename that will be used for download">
|
||||
{computed_name}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-1 py-1 text-right text-xs opacity-50 whitespace-nowrap">
|
||||
{ae_util.format_bytes(file.file_size)}
|
||||
</td>
|
||||
<td class="pl-2 py-1 whitespace-nowrap">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<a
|
||||
href={dl_url}
|
||||
download={computed_name}
|
||||
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
||||
title="Download as: {computed_name}">
|
||||
<Download size="0.9em" class="mr-0.5" />
|
||||
Download
|
||||
</a>
|
||||
<MyClipboard
|
||||
value={dl_url}
|
||||
btn_text="Copy Link"
|
||||
btn_title="Copy direct download link: {computed_name}"
|
||||
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex flex-col">
|
||||
{#each pg.files as file (file.event_file_id)}
|
||||
{@const ExtIcon = ae_util.file_extension_icon_lucide(file.extension)}
|
||||
{@const computed_name = build_filename(file, selected_format)}
|
||||
{@const dl_url = build_download_url(file, selected_format)}
|
||||
{@const is_long = computed_name.length > FILENAME_WARN_LEN}
|
||||
<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">
|
||||
<!-- Left: filenames (grows, min-w-0 allows truncation) -->
|
||||
<div class="flex min-w-0 grow flex-col gap-0.5">
|
||||
<span class="flex items-center gap-1 truncate text-xs opacity-50" title={file.filename}>
|
||||
<ExtIcon size="0.9em" class="shrink-0" />
|
||||
{ae_util.shorten_filename({ filename: file.filename, max_length: 40 })}
|
||||
</span>
|
||||
<span
|
||||
class="truncate font-mono text-xs"
|
||||
class:opacity-70={!is_long}
|
||||
class:text-warning-600={is_long}
|
||||
title="{computed_name}{is_long ? ` (${computed_name.length} chars — may be long for some systems)` : ''}">
|
||||
{computed_name}
|
||||
</span>
|
||||
</div>
|
||||
<!-- File size -->
|
||||
<span class="shrink-0 whitespace-nowrap text-xs opacity-50">
|
||||
{ae_util.format_bytes(file.file_size)}
|
||||
</span>
|
||||
<!-- Buttons -->
|
||||
<div class="flex shrink-0 flex-row items-center gap-1">
|
||||
<a
|
||||
href={dl_url}
|
||||
download={computed_name}
|
||||
class="btn btn-xs preset-tonal-primary border-primary-500/30 border"
|
||||
title="Download as: {computed_name}">
|
||||
<Download size="0.9em" class="mr-0.5" />
|
||||
Download
|
||||
</a>
|
||||
<MyClipboard
|
||||
value={dl_url}
|
||||
btn_text="Copy Link"
|
||||
btn_title="Copy direct download link: {computed_name}"
|
||||
btn_class="btn btn-xs preset-tonal-warning hover:preset-filled-warning-500 border-warning-500/30 border"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user