feat(hosted-files): introduce direct download mode and automate filename shortening
- Add 'show_direct_download' prop to support native browser downloads via V3 Action paths (/v3/action/...). - Integrate 'shorten_filename' logic directly into the component via 'max_filename' prop. - Refactor internal UI to snippets for consistent rendering across button and anchor modes. - Update 'ae_HostedFile' type with new SQL view fields: 'filename_no_ext' and 'filename_w_ext'. - Add direct download toggle and edit mode indicator to testing dashboard. - Simplify 'ae_comp__event_file_obj_tbl' by removing manual snippets in favor of standardized component logic.
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
hosted_file_id: null | string;
|
||||
hosted_file_obj: null | key_val;
|
||||
filename?: null | string;
|
||||
max_length?: number;
|
||||
max_filename?: number;
|
||||
auto_download?: boolean;
|
||||
linked_to_type?: null | string;
|
||||
linked_to_id?: null | string;
|
||||
@@ -28,16 +28,17 @@
|
||||
variant?: 'tonal' | 'filled' | 'outline' | 'ghost';
|
||||
color?: 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'error' | 'surface';
|
||||
show_divider?: boolean;
|
||||
show_direct_download?: boolean;
|
||||
classes?: string;
|
||||
label?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
let {
|
||||
log_lvl = 0,
|
||||
hosted_file_id,
|
||||
hosted_file_obj,
|
||||
filename = $bindable(null),
|
||||
max_length = $bindable(30),
|
||||
max_filename = $bindable(30),
|
||||
auto_download = true,
|
||||
linked_to_type = $bindable(null),
|
||||
linked_to_id = $bindable(null),
|
||||
@@ -47,6 +48,7 @@
|
||||
variant = 'tonal',
|
||||
color = 'primary',
|
||||
show_divider = true,
|
||||
show_direct_download = false,
|
||||
classes = '',
|
||||
label
|
||||
}: Props = $props();
|
||||
@@ -98,7 +100,7 @@
|
||||
};
|
||||
|
||||
let variant_classes = $derived.by(() => {
|
||||
const base = 'btn btn-sm lg:btn-md min-w-48 transition-all overflow-hidden';
|
||||
const base = 'btn btn-sm lg:btn-md min-w-48 transition-all overflow-hidden px-3';
|
||||
const style = color_map[color]?.[variant] || color_map.primary.tonal;
|
||||
return `${base} ${style} ${classes}`.trim();
|
||||
});
|
||||
@@ -151,109 +153,134 @@
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
let final_filename = $derived(filename ?? hosted_file_obj?.filename ?? 'unknown');
|
||||
let shortened_filename = $derived(ae_util.shorten_filename({
|
||||
filename: final_filename,
|
||||
max_length: max_filename
|
||||
}));
|
||||
|
||||
let direct_download_url = $derived.by(() => {
|
||||
if (!show_direct_download || !hosted_file_obj) return '';
|
||||
// IMPORTANT: For Direct Link Mode, we MUST use the V3 Action endpoint to support Random String IDs.
|
||||
// Legacy endpoints often expect integer IDs and will return 404 for string IDs.
|
||||
const file_id = hosted_file_obj.event_file_id || hosted_file_obj.hosted_file_id || hosted_file_id;
|
||||
const obj_type_path = hosted_file_obj.event_file_id ? 'event_file' : 'hosted_file';
|
||||
return `${$ae_api.base_url}/v3/action/${obj_type_path}/${file_id}/download?filename=${ae_util.clean_filename(final_filename)}&x_no_account_id_token=direct-download`;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if hosted_file_id && hosted_file_obj}
|
||||
{@const file_id = hosted_file_obj.id || hosted_file_obj.hosted_file_id || hosted_file_id}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
class={variant_classes}
|
||||
onclick={() => {
|
||||
download_complete = undefined;
|
||||
download_status_msg = 'Downloading...';
|
||||
ae_promises[file_id] = download_ae_obj_id__hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_id: file_id,
|
||||
return_file: true,
|
||||
filename: filename ?? hosted_file_obj.filename,
|
||||
auto_download: auto_download,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then((result) => {
|
||||
if (result === null) {
|
||||
console.log('File not found (404)');
|
||||
download_complete = null;
|
||||
download_status_msg = 'File not found';
|
||||
} else if (result === false) {
|
||||
console.log(
|
||||
'Possible error with API server (check network and server status)'
|
||||
);
|
||||
download_complete = false;
|
||||
download_status_msg = 'Failed to download';
|
||||
} else {
|
||||
// console.log('File found and downloaded');
|
||||
download_complete = true;
|
||||
download_status_msg = 'File downloaded';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}}
|
||||
title={`Download this file:\n${filename ?? hosted_file_obj?.filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}
|
||||
>
|
||||
{#await ae_promises[file_id]}
|
||||
<div class="flex items-center w-full min-h-[1.5rem]">
|
||||
{#snippet content()}
|
||||
{@const file_id = hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id}
|
||||
{#await ae_promises[file_id]}
|
||||
<div class="flex items-center w-full min-h-[1.5rem]">
|
||||
<div
|
||||
class="flex items-center pr-2 shrink-0 {show_divider ? 'border-r border-surface-500/30 mr-2' : ''}"
|
||||
>
|
||||
<Lucide.Loader2 class="animate-spin" size={18} />
|
||||
</div>
|
||||
<div class="grow relative text-left h-full">
|
||||
{#if show_filename_view}
|
||||
<div in:fade={{ duration: 250 }} out:fade={{ duration: 250 }} class="flex items-center h-full">
|
||||
<span class="truncate">
|
||||
{shortened_filename}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fade={{ duration: 250 }} out:fade={{ duration: 250 }} class="absolute inset-0 flex items-center h-full">
|
||||
<span class="font-bold whitespace-nowrap">
|
||||
Downloading:
|
||||
{#if $ae_sess.api_download_kv[file_id]}
|
||||
{$ae_sess.api_download_kv[file_id].percent_completed}%
|
||||
{:else}
|
||||
...
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:then}
|
||||
{#if label}
|
||||
{@render label()}
|
||||
{:else}
|
||||
{@const IconComp = ae_util.file_extension_icon_lucide(hosted_file_obj?.extension)}
|
||||
<div class="flex items-center w-full">
|
||||
<div
|
||||
class="flex items-center pr-2 shrink-0 {show_divider ? 'border-r border-surface-500/30 mr-2' : ''}"
|
||||
>
|
||||
<Lucide.Loader2 class="animate-spin" size={18} />
|
||||
<IconComp size={18} />
|
||||
</div>
|
||||
<div class="grow relative text-left h-full">
|
||||
{#if show_filename_view}
|
||||
<div in:fade={{ duration: 250 }} out:fade={{ duration: 250 }} class="flex items-center h-full">
|
||||
<span class="truncate">
|
||||
{ae_util.shorten_filename({
|
||||
filename: filename ?? hosted_file_obj?.filename,
|
||||
max_length: max_length
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fade={{ duration: 250 }} out:fade={{ duration: 250 }} class="absolute inset-0 flex items-center h-full">
|
||||
<span class="font-bold whitespace-nowrap">
|
||||
Downloading:
|
||||
{#if $ae_sess.api_download_kv[file_id]}
|
||||
{$ae_sess.api_download_kv[file_id].percent_completed}%
|
||||
{:else}
|
||||
...
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:then}
|
||||
{#if label}
|
||||
{@render label()}
|
||||
{:else}
|
||||
{@const IconComp = ae_util.file_extension_icon_lucide(hosted_file_obj?.extension)}
|
||||
<div class="flex items-center w-full">
|
||||
<div
|
||||
class="flex items-center pr-2 shrink-0 {show_divider ? 'border-r border-surface-500/30 mr-2' : ''}"
|
||||
>
|
||||
<IconComp size={18} />
|
||||
</div>
|
||||
<span class="grow truncate text-left">
|
||||
{ae_util.shorten_filename({
|
||||
filename: filename ?? hosted_file_obj?.filename,
|
||||
max_length: max_length
|
||||
})}
|
||||
<span class="grow truncate text-left">
|
||||
{shortened_filename}
|
||||
</span>
|
||||
{#if hosted_file_obj?.file_purpose || hosted_file_obj?.group}
|
||||
<span class="badge preset-tonal-success ml-2 text-[10px] uppercase font-bold shrink-0">
|
||||
{hosted_file_obj.file_purpose || hosted_file_obj.group}
|
||||
</span>
|
||||
{#if hosted_file_obj?.file_purpose || hosted_file_obj?.group}
|
||||
<span class="badge preset-tonal-success ml-2 text-[10px] uppercase font-bold shrink-0">
|
||||
{hosted_file_obj.file_purpose || hosted_file_obj.group}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
{#if download_complete === null}
|
||||
<span class="text-red-800 dark:text-red-200 ml-2 whitespace-nowrap">File not found</span>
|
||||
{:else if download_complete === false}
|
||||
<span class="text-red-800 dark:text-red-200 ml-2 whitespace-nowrap text-xs">Failed!</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/await}
|
||||
|
||||
{#if download_complete === null}
|
||||
<span class="text-red-800 dark:text-red-200 ml-2 whitespace-nowrap">File not found</span>
|
||||
{:else if download_complete === false}
|
||||
<span class="text-red-800 dark:text-red-200 ml-2 whitespace-nowrap text-xs">Failed!</span>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#if hosted_file_id && hosted_file_obj}
|
||||
{@const file_id = hosted_file_obj.id || hosted_file_obj.hosted_file_id || hosted_file_id}
|
||||
|
||||
{#if show_direct_download}
|
||||
<a
|
||||
href={direct_download_url}
|
||||
class={variant_classes}
|
||||
title={`Direct download (V3 Action):\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}`}
|
||||
>
|
||||
{@render content()}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
class={variant_classes}
|
||||
onclick={() => {
|
||||
download_complete = undefined;
|
||||
download_status_msg = 'Downloading...';
|
||||
ae_promises[file_id] = download_ae_obj_id__hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_id: file_id,
|
||||
return_file: true,
|
||||
filename: final_filename,
|
||||
auto_download: auto_download,
|
||||
log_lvl: log_lvl
|
||||
})
|
||||
.then((result) => {
|
||||
if (result === null) {
|
||||
console.log('File not found (404)');
|
||||
download_complete = null;
|
||||
download_status_msg = 'File not found';
|
||||
} else if (result === false) {
|
||||
console.log(
|
||||
'Possible error with API server (check network and server status)'
|
||||
);
|
||||
download_complete = false;
|
||||
download_status_msg = 'Failed to download';
|
||||
} else {
|
||||
// console.log('File found and downloaded');
|
||||
download_complete = true;
|
||||
download_status_msg = 'File downloaded';
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}}
|
||||
title={`Download this file:\n${final_filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}...\nHosted ID: ${file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}
|
||||
>
|
||||
{@render content()}
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button type="button" disabled class={variant_classes} title="No file selected">
|
||||
<div class="flex items-center w-full">
|
||||
@@ -265,4 +292,4 @@
|
||||
<span class="grow text-left"> No file info </span>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -674,6 +674,8 @@ export interface ae_HostedFile extends ae_BaseObj {
|
||||
hash_sha256?: string;
|
||||
subdirectory_path?: string;
|
||||
filename?: string;
|
||||
filename_no_ext?: string;
|
||||
filename_w_ext?: string;
|
||||
extension?: string;
|
||||
mimetype?: string;
|
||||
size?: number;
|
||||
|
||||
@@ -375,24 +375,11 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={event_file_obj?.hosted_file_id}
|
||||
hosted_file_obj={event_file_obj}
|
||||
show_divider={true}
|
||||
show_direct_download={show_direct_download}
|
||||
max_filename={50}
|
||||
classes="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 min-w-72"
|
||||
>
|
||||
{#snippet label()}
|
||||
<span class="grow">
|
||||
{ae_util.shorten_filename({
|
||||
filename: event_file_obj?.filename,
|
||||
max_length: 30
|
||||
})}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="badge preset-filled-success-600-400 hover:preset-filled-success-700-300 text-xs"
|
||||
class:hidden={!event_file_obj?.file_purpose}
|
||||
>
|
||||
{event_file_obj?.file_purpose}
|
||||
</span>
|
||||
{/snippet}
|
||||
</AE_Comp_Hosted_Files_Download_Button>
|
||||
/>
|
||||
<!-- {event_file_obj?.filename} -->
|
||||
</td>
|
||||
<td
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
let large_file_obj: any = $state(null);
|
||||
let loading = $state(true);
|
||||
|
||||
// Testing Toggles
|
||||
let test_direct_download = $state(false);
|
||||
|
||||
const LARGE_FILE_ID = 'sPw1he_RGCW';
|
||||
const test_ids = [
|
||||
'QpyZNtEb2LQBmNJPQU_aOA', // small.mp4
|
||||
@@ -64,13 +67,32 @@
|
||||
<!-- Outer wrapper to enable scrolling if parent is overflow-hidden -->
|
||||
<div class="h-full w-full overflow-y-auto overflow-x-hidden bg-transparent">
|
||||
<div class="container mx-auto p-4 space-y-8 pb-20">
|
||||
<header class="flex justify-between items-center bg-surface-50-900-token p-6 rounded-container shadow-xl border border-gray-500">
|
||||
<header class="flex flex-col md:flex-row justify-between items-start md:items-center bg-surface-50-900-token p-6 rounded-container shadow-xl border border-gray-500 gap-4">
|
||||
<div class="space-y-1">
|
||||
<h1 class="h1 flex items-center gap-3">
|
||||
<Lucide.Download class="text-primary-500" /> Hosted Files Testing
|
||||
</h1>
|
||||
<p class="opacity-60 text-sm">Testing the AE_Comp_Hosted_Files_Download_Button component</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 items-center bg-black/20 p-3 rounded-lg border border-white/10">
|
||||
<div class="flex items-center gap-2 pr-3 border-r border-white/10">
|
||||
<span class="text-[10px] font-bold uppercase opacity-50">Edit Mode</span>
|
||||
<span class="badge {$ae_loc.edit_mode ? 'variant-filled-success' : 'variant-filled-surface'}">
|
||||
{$ae_loc.edit_mode ? 'ON' : 'OFF'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-[10px] font-bold uppercase opacity-50">Direct Download</span>
|
||||
<button
|
||||
class="btn btn-sm {test_direct_download ? 'variant-filled-primary' : 'variant-soft-surface'}"
|
||||
onclick={() => test_direct_download = !test_direct_download}
|
||||
>
|
||||
{test_direct_download ? 'Anchor Mode (<a>)' : 'Action Mode (<button>)'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dedicated Large File Section -->
|
||||
@@ -79,7 +101,12 @@
|
||||
<h2 class="h2 flex items-center gap-2">
|
||||
<Lucide.Zap class="text-warning-500" /> Large File Progress Test
|
||||
</h2>
|
||||
<span class="badge variant-filled-warning animate-pulse">Testing Percentage</span>
|
||||
<div class="flex gap-2">
|
||||
<span class="badge variant-filled-warning animate-pulse">Testing Percentage</span>
|
||||
{#if test_direct_download}
|
||||
<span class="badge variant-filled-secondary">Direct Link</span>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="text-sm opacity-80 italic">
|
||||
@@ -103,6 +130,7 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={LARGE_FILE_ID}
|
||||
hosted_file_obj={large_file_obj}
|
||||
show_direct_download={test_direct_download}
|
||||
variant="filled"
|
||||
color="primary"
|
||||
classes="w-full md:min-w-64 py-4"
|
||||
@@ -138,6 +166,7 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={file.id}
|
||||
hosted_file_obj={file}
|
||||
show_direct_download={test_direct_download}
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
classes="w-full"
|
||||
@@ -149,6 +178,7 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={file.id}
|
||||
hosted_file_obj={file}
|
||||
show_direct_download={test_direct_download}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
classes="w-full"
|
||||
@@ -160,6 +190,7 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={file.id}
|
||||
hosted_file_obj={file}
|
||||
show_direct_download={test_direct_download}
|
||||
variant="outline"
|
||||
color="tertiary"
|
||||
classes="w-full"
|
||||
@@ -171,6 +202,7 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={file.id}
|
||||
hosted_file_obj={file}
|
||||
show_direct_download={test_direct_download}
|
||||
variant="ghost"
|
||||
color="surface"
|
||||
classes="w-full"
|
||||
@@ -183,6 +215,7 @@
|
||||
<AE_Comp_Hosted_Files_Download_Button
|
||||
hosted_file_id={file.id}
|
||||
hosted_file_obj={file}
|
||||
show_direct_download={test_direct_download}
|
||||
variant="tonal"
|
||||
color="success"
|
||||
classes="w-full mt-1"
|
||||
|
||||
Reference in New Issue
Block a user