feat(hosted-files): introduce standardized download component and V3 action support

- Created AE_Comp_Hosted_Files_Download_Button using Svelte 5 and Lucide icons.
- Added file_extension_icon_lucide utility for direct Lucide icon mapping.
- Refactored download logic to core__hosted_files.ts using V3 Action endpoint (/v3/action/hosted_file/.../download).
- Integrated new component into Event File Object Table.
- Cleaned up legacy window.postMessage calls in several file management views.

NOTE: The new download component is currently in development and may not be fully functional.
This commit is contained in:
Scott Idem
2026-02-03 11:19:23 -05:00
parent 1ae7b5642d
commit aaead82c1a
11 changed files with 275 additions and 61 deletions

View File

@@ -224,7 +224,6 @@
log_lvl: 0
});
// window.postMessage({ type: 'download_event_file', hosted_file_id: idaa_archive_content_obj.hosted_file_id, filename: idaa_archive_content_obj.filename, auto_download: true }, '*');
}}
class:hidden={!$ae_loc.edit_mode}
class="novi_btn btn btn-sm lg:btn-md preset-tonal-primary hover:preset-filled-primary-500 min-w-72 lg:min-w-96"

View File

@@ -60,7 +60,6 @@
log_lvl: log_lvl
});
// window.postMessage({ type: 'download_event_file', hosted_file_id: idaa_archive_content_obj.hosted_file_id, filename: idaa_archive_content_obj.filename, auto_download: true }, '*');
}}
class="novi_btn btn btn-sm lg:btn-md preset-tonal-primary hover:preset-filled-primary-500 min-w-72 lg:min-w-96"
title={`Download this file:\n${hosted_file_obj.filename}\n[API] SHA256: ${hosted_file_obj?.hash_sha256?.slice(0, 10)}... Hosted ID: ${hosted_file_obj.hosted_file_id}`}

View File

@@ -0,0 +1,141 @@
<script lang="ts">
// *** Import Svelte specific
import * as Lucide from 'lucide-svelte';
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/stores/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { download_ae_obj_id__hosted_file } from '$lib/ae_core/core__hosted_files';
import {
ae_loc,
ae_sess,
ae_api
} from '$lib/stores/ae_stores';
interface Props {
log_lvl?: number;
hosted_file_id: null | string;
hosted_file_obj: null | key_val;
filename?: null | string;
max_length?: number;
auto_download?: boolean;
linked_to_type?: null | string;
linked_to_id?: null | string;
download_complete?: null | boolean;
download_percent?: number;
download_status_msg?: string;
classes?: string;
label?: import('svelte').Snippet;
}
let {
log_lvl = 0,
hosted_file_id,
hosted_file_obj,
filename = $bindable(null),
max_length = $bindable(30),
auto_download = true,
linked_to_type = $bindable(null),
linked_to_id = $bindable(null),
download_complete = $bindable(),
download_percent = $bindable(),
download_status_msg = $bindable('Not started'),
classes = 'btn btn-sm lg:btn-md preset-tonal-primary border border-primary-500 hover:preset-filled-primary-500 min-w-48',
label
}: Props = $props();
$effect(() => {
if (log_lvl) {
console.log(
`ae_comp__hosted_files_download_button.svelte hosted_file_id=${hosted_file_id}`,
hosted_file_obj
);
}
});
let ae_promises: key_val = $state({});
$effect(() => {
const file_id = hosted_file_obj?.id || hosted_file_obj?.hosted_file_id || hosted_file_id;
if (file_id && $ae_sess?.api_download_kv[file_id]?.percent_completed) {
download_percent =
$ae_sess.api_download_kv[file_id].percent_completed;
}
});
</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={classes ?? 'btn'}
onclick={() => {
download_complete = false;
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]}
<Lucide.Loader2 class="animate-spin mr-2" size={18} />
<span class="">
Downloading
{#if $ae_sess.api_download_kv[file_id]}
{$ae_sess.api_download_kv[file_id]
.percent_completed}%
{/if}
:
</span>
{:then}
{#if label}
{@render label()}
{:else}
{@const IconComp = ae_util.file_extension_icon_lucide(hosted_file_obj?.extension)}
<IconComp size={18} class="mr-2" />
<span class="grow">
{ae_util.shorten_filename({
filename: filename ?? hosted_file_obj?.filename,
max_length: max_length
})}
</span>
{/if}
{/await}
{#if download_complete === null}
<span class="text-red-800 dark:text-red-200 ml-2">File not found</span>
{:else if download_complete === false}
<span class="text-red-800 dark:text-red-200 ml-2">Failed to download!</span>
{/if}
</button>
{:else}
<button type="button" disabled class={classes ?? 'btn'} title="No file selected">
<Lucide.FileX size={18} class="mr-2" />
<span class="grow"> No file info </span>
</button>
{/if}

View File

@@ -180,6 +180,53 @@ export async function delete_ae_obj_id__hosted_file({
return result;
}
/**
* Download a hosted file (V3 Action)
* Uses the new /v3/action/... standard.
* Updated 2026-02-03
*/
export async function download_ae_obj_id__hosted_file({
api_cfg,
hosted_file_id,
return_file = true,
filename,
auto_download = false,
params = {},
log_lvl = 0
}: {
api_cfg: any;
hosted_file_id: string;
return_file?: boolean;
filename?: string;
auto_download?: boolean;
params?: key_val;
log_lvl?: number;
}) {
if (log_lvl) {
console.log(`*** download_ae_obj_id__hosted_file() *** id=${hosted_file_id}`);
}
const task_id = hosted_file_id;
const endpoint = `/v3/action/hosted_file/${hosted_file_id}/download`;
const query_params: key_val = { ...params };
if (filename) {
query_params['filename'] = filename;
}
query_params['return_file'] = 'true'; // V3 prefers string 'true' for bool flags in query
return await api.get_object({
api_cfg,
endpoint,
params: query_params,
return_blob: return_file,
filename,
auto_download,
task_id,
log_lvl
});
}
export const properties_to_save = [
'id',
'hosted_file_id',

View File

@@ -8,6 +8,7 @@ import {
} from './ae_utils__files';
import { get_obj_li_w_match_prop } from './ae_utils__get_obj_li_w_match_prop';
import { file_extension_icon } from './ae_utils__file_extension_icon';
import { file_extension_icon_lucide } from './ae_utils__file_extension_icon_lucide';
import { process_permission_checks } from './ae_utils__perm_checks';
import { iso_datetime_formatter } from './ae_utils__datetime_format';
import { is_datetime_recent } from './ae_utils__is_datetime_recent';
@@ -337,6 +338,7 @@ export const ae_util = {
shorten_string: shorten_string,
shorten_filename: shorten_filename,
file_extension_icon: file_extension_icon,
file_extension_icon_lucide: file_extension_icon_lucide,
format_html: format_html,
set_obj_prop_display_name: set_obj_prop_display_name,
return_obj_type_path: return_obj_type_path,

View File

@@ -0,0 +1,66 @@
import * as Lucide from 'lucide-svelte';
/**
* Returns a Lucide icon component based on the provided file extension.
* @param extension The file extension (e.g., 'pdf', 'jpg').
* @returns The Lucide icon component.
*/
export function file_extension_icon_lucide(extension: string | undefined | null): any {
const ext = extension?.toLowerCase() || '';
const icon_map: Record<string, any> = {
'pdf': Lucide.FileText,
'doc': Lucide.FileText,
'docx': Lucide.FileText,
'txt': Lucide.FileText,
'rtf': Lucide.FileText,
'xls': Lucide.FileSpreadsheet,
'xlsx': Lucide.FileSpreadsheet,
'csv': Lucide.FileSpreadsheet,
'png': Lucide.FileImage,
'jpg': Lucide.FileImage,
'jpeg': Lucide.FileImage,
'gif': Lucide.FileImage,
'webp': Lucide.FileImage,
'bmp': Lucide.FileImage,
'svg': Lucide.FileImage,
'mp3': Lucide.FileAudio,
'wav': Lucide.FileAudio,
'm4a': Lucide.FileAudio,
'flac': Lucide.FileAudio,
'aac': Lucide.FileAudio,
'aif': Lucide.FileAudio,
'aiff': Lucide.FileAudio,
'mp4': Lucide.FileVideo,
'mkv': Lucide.FileVideo,
'mov': Lucide.FileVideo,
'avi': Lucide.FileVideo,
'3gp': Lucide.FileVideo,
'ppt': Lucide.Presentation,
'pptx': Lucide.Presentation,
'key': Lucide.Presentation,
'odp': Lucide.Presentation,
'zip': Lucide.FileArchive,
'7z': Lucide.FileArchive,
'rar': Lucide.FileArchive,
'tar': Lucide.FileArchive,
'gz': Lucide.FileArchive,
'json': Lucide.FileJson,
'html': Lucide.FileCode,
'htm': Lucide.FileCode,
'js': Lucide.FileCode,
'ts': Lucide.FileCode,
'css': Lucide.FileCode,
'php': Lucide.FileCode
};
return icon_map[ext] || Lucide.File;
}

View File

@@ -183,7 +183,6 @@
log_lvl: 0
});
// window.postMessage({ type: 'download_event_file', event_file_id: event_file_obj.event_file_id, filename: event_file_obj.filename, auto_download: true }, '*');
}}
class="btn btn-sm lg:btn-md preset-tonal-primary hover:preset-filled-primary-500 min-w-72 lg:min-w-96"
title={`Download this file:\n${event_file_obj.filename}\n[API] SHA256: ${event_file_obj.hash_sha256.slice(0, 10)}... Hosted ID: ${event_file_obj.hosted_file_id} Event File ID: ${event_file_obj.event_file_id}`}

View File

@@ -50,6 +50,7 @@
// import { events_loc, events_sess, events_slct, events_trigger, events_trig_kv } from '$lib/stores/ae_events_stores';
// import { events_func } from '$lib/ae_events_functions';
import MyClipboard from '$lib/app_components/e_app_clipboard.svelte';
import AE_Comp_Hosted_Files_Download_Button from '$lib/ae_core/ae_comp__hosted_files_download_button.svelte';
// export let display_mode: string = 'default'; // 'default', 'compact', 'minimal', 'launcher'
@@ -371,64 +372,27 @@
{#each $lq__event_file_obj_li as event_file_obj}
<tr class:dim={event_file_obj?.hide}>
<td class="px-4 py-2">
<button
disabled={!allow_basic &&
!allow_moderator &&
!$ae_loc.trusted_access}
onclick={() => {
// ae_promises[event_file_obj?.event_file_id]
ae_promises[event_file_obj?.event_file_id] =
api.download_hosted_file({
api_cfg: $ae_api,
hosted_file_id: event_file_obj?.hosted_file_id,
return_file: true,
filename: event_file_obj?.filename,
auto_download: true,
log_lvl: 0
});
// window.postMessage({ type: 'download_event_file', event_file_id: event_file_obj?.event_file_id, filename: event_file_obj?.filename, auto_download: true }, '*');
}}
class="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 min-w-72"
title={`Download this file: ${event_file_obj?.filename} [API] -- SHA256 hash: ${event_file_obj?.hash_sha256.slice(0, 10)}...`}
<AE_Comp_Hosted_Files_Download_Button
hosted_file_id={event_file_obj?.hosted_file_id}
hosted_file_obj={event_file_obj}
classes="btn btn-sm preset-tonal-primary hover:preset-filled-primary-500 min-w-72"
>
{#await ae_promises[event_file_obj?.event_file_id]}
<span class="fas fa-spinner fa-spin mx-1"></span>
<span class="">
Downloading
{#if $ae_sess.api_download_kv[event_file_obj?.hosted_file_id]}
{$ae_sess.api_download_kv[
event_file_obj?.hosted_file_id
].percent_completed}%
{/if}
:
{#snippet label()}
<span class="grow">
{ae_util.shorten_filename({
filename: event_file_obj?.filename,
max_length: 30
})}
</span>
{:then}
<!-- <span class="fas fa-download mx-1"></span> -->
<span
class="fas fa-{ae_util.file_extension_icon(
event_file_obj?.extension
)}"
></span>
<!-- <span class="text-sm">
Download:
</span> -->
{/await}
<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>
</button>
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

View File

@@ -204,7 +204,6 @@
auto_download: true
});
// window.postMessage({ type: 'download_event_file', hosted_file_id: idaa_archive_content_obj.hosted_file_id, filename: idaa_archive_content_obj.filename, auto_download: true }, '*');
}}
class="novi_btn btn btn-sm lg:btn-md preset-tonal-primary hover:preset-filled-primary-500 min-w-72 lg:min-w-96"
title={`Download this file:\n${idaa_archive_content_obj.filename}\n[API] SHA256: ${idaa_archive_content_obj?.hash_sha256.slice(0, 10)}... Hosted ID: ${idaa_archive_content_obj.hosted_file_id} Archive Content ID: ${idaa_archive_content_obj.archive_content_id}`}

View File

@@ -581,7 +581,6 @@ Copy and paste link: <a href="${link_base_url}?post_id=${$idaa_slct.post_id}">${
log_lvl: 0
});
// window.postMessage({ type: 'download_event_file', hosted_file_id: linked_obj.hosted_file_id_random, filename: linked_obj.filename, auto_download: true }, '*');
}}
class="novi_btn btn btn-sm lg:btn-md preset-tonal-tertiary border border-tertiary-500 hover:preset-filled-tertiary-500 min-w-48"
title={`Download this file:\n${linked_obj.filename}\n[API] SHA256: ${linked_obj?.hash_sha256.slice(0, 10)}... Hosted ID: ${linked_obj.hosted_file_id_random} Archive Content ID: ${linked_obj.archive_content_id}`}

View File

@@ -179,7 +179,6 @@
log_lvl: 0
});
// window.postMessage({ type: 'download_event_file', hosted_file_id: linked_obj.hosted_file_id_random, filename: linked_obj.filename, auto_download: true }, '*');
}}
class="novi_btn btn btn-sm lg:btn-md preset-tonal-tertiary border border-tertiary-500 hover:preset-filled-tertiary-500 min-w-48"
title={`Download this file:\n${linked_obj.filename}\n[API] SHA256: ${linked_obj?.hash_sha256.slice(0, 10)}... Hosted ID: ${linked_obj.hosted_file_id_random} Archive Content ID: ${linked_obj.archive_content_id}`}