feat(hosted-files): enhance download component with style variants and robust progress states
- refactor(ae_comp__hosted_files_download_button): add 'variant' (tonal, filled, outline, ghost) and 'color' props. - fix(ae_comp__hosted_files_download_button): ensure Tailwind bundles variant classes using a literal lookup map. - fix(ae_comp__hosted_files_download_button): prevent premature "Failed to download" message during initialization. - feat(testing): add comprehensive Hosted Files testing dashboard at /testing/hosted_files with large file progress trials. - chore: remove legacy v1 download button component.
This commit is contained in:
@@ -24,6 +24,8 @@
|
||||
download_complete?: null | boolean;
|
||||
download_percent?: number;
|
||||
download_status_msg?: string;
|
||||
variant?: 'tonal' | 'filled' | 'outline' | 'ghost';
|
||||
color?: 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'error' | 'surface';
|
||||
classes?: string;
|
||||
label?: import('svelte').Snippet;
|
||||
}
|
||||
@@ -40,10 +42,64 @@
|
||||
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',
|
||||
variant = 'tonal',
|
||||
color = 'primary',
|
||||
classes = '',
|
||||
label
|
||||
}: Props = $props();
|
||||
|
||||
// Map variant/color to classes using literal strings so Tailwind can find them
|
||||
const color_map: Record<string, Record<string, string>> = {
|
||||
primary: {
|
||||
tonal: 'preset-tonal-primary border border-primary-500/30 hover:preset-filled-primary-500',
|
||||
filled: 'preset-filled-primary-500 hover:preset-filled-primary-600',
|
||||
outline: 'border border-primary-500 hover:preset-tonal-primary',
|
||||
ghost: 'hover:preset-tonal-primary'
|
||||
},
|
||||
secondary: {
|
||||
tonal: 'preset-tonal-secondary border border-secondary-500/30 hover:preset-filled-secondary-500',
|
||||
filled: 'preset-filled-secondary-500 hover:preset-filled-secondary-600',
|
||||
outline: 'border border-secondary-500 hover:preset-tonal-secondary',
|
||||
ghost: 'hover:preset-tonal-secondary'
|
||||
},
|
||||
tertiary: {
|
||||
tonal: 'preset-tonal-tertiary border border-tertiary-500/30 hover:preset-filled-tertiary-500',
|
||||
filled: 'preset-filled-tertiary-500 hover:preset-filled-tertiary-600',
|
||||
outline: 'border border-tertiary-500 hover:preset-tonal-tertiary',
|
||||
ghost: 'hover:preset-tonal-tertiary'
|
||||
},
|
||||
success: {
|
||||
tonal: 'preset-tonal-success border border-success-500/30 hover:preset-filled-success-500',
|
||||
filled: 'preset-filled-success-500 hover:preset-filled-success-600',
|
||||
outline: 'border border-success-500 hover:preset-tonal-success',
|
||||
ghost: 'hover:preset-tonal-success'
|
||||
},
|
||||
warning: {
|
||||
tonal: 'preset-tonal-warning border border-warning-500/30 hover:preset-filled-warning-500',
|
||||
filled: 'preset-filled-warning-500 hover:preset-filled-warning-600',
|
||||
outline: 'border border-warning-500 hover:preset-tonal-warning',
|
||||
ghost: 'hover:preset-tonal-warning'
|
||||
},
|
||||
error: {
|
||||
tonal: 'preset-tonal-error border border-error-500/30 hover:preset-filled-error-500',
|
||||
filled: 'preset-filled-error-500 hover:preset-filled-error-600',
|
||||
outline: 'border border-error-500 hover:preset-tonal-error',
|
||||
ghost: 'hover:preset-tonal-error'
|
||||
},
|
||||
surface: {
|
||||
tonal: 'preset-tonal-surface border border-surface-500/30 hover:preset-filled-surface-500',
|
||||
filled: 'preset-filled-surface-500 hover:preset-filled-surface-600',
|
||||
outline: 'border border-surface-500 hover:preset-tonal-surface',
|
||||
ghost: 'hover:preset-tonal-surface'
|
||||
}
|
||||
};
|
||||
|
||||
let variant_classes = $derived.by(() => {
|
||||
const base = 'btn btn-sm lg:btn-md min-w-48 transition-all';
|
||||
const style = color_map[color]?.[variant] || color_map.primary.tonal;
|
||||
return `${base} ${style} ${classes}`.trim();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
@@ -69,9 +125,9 @@
|
||||
<button
|
||||
type="button"
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
class={classes ?? 'btn'}
|
||||
class={variant_classes}
|
||||
onclick={() => {
|
||||
download_complete = false;
|
||||
download_complete = undefined;
|
||||
download_status_msg = 'Downloading...';
|
||||
ae_promises[file_id] = download_ae_obj_id__hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
<script lang="ts">
|
||||
// *** Import Svelte specific
|
||||
|
||||
// Eventually this should use Lucide icons instead of FontAwesome
|
||||
// import {
|
||||
// ArrowDown01, ArrowDown10, ArrowDownUp,
|
||||
// BookHeart, BriefcaseBusiness,
|
||||
// CalendarClock, CalendarOff, Clock, CodeXml, Copy,
|
||||
// Eye, EyeOff,
|
||||
// Flag, FlagOff, FileX, Fingerprint,
|
||||
// Globe, Group,
|
||||
// Hash, History,
|
||||
// LockKeyhole, LockKeyholeOpen,
|
||||
// MessageSquareWarning, Menu, Minus,
|
||||
// NotebookPen, NotebookText, NotepadTextDashed,
|
||||
// Pencil, PenLine, Plus,
|
||||
// RemoveFormatting,
|
||||
// Search, Settings,
|
||||
// Shapes, Share2, ShieldCheck, ShieldMinus, Siren, Skull,
|
||||
// SquareLibrary,
|
||||
// Tags, Trash2, TypeOutline,
|
||||
// X
|
||||
// } 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 { api } from '$lib/api/api';
|
||||
import {
|
||||
ae_snip,
|
||||
ae_loc,
|
||||
ae_sess,
|
||||
ae_api,
|
||||
ae_trig,
|
||||
slct,
|
||||
slct_trigger
|
||||
} 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;
|
||||
}
|
||||
|
||||
let {
|
||||
log_lvl = $bindable(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-tertiary border border-tertiary-500 hover:preset-filled-tertiary-500 min-w-48'
|
||||
}: Props = $props();
|
||||
|
||||
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(() => {
|
||||
if ($ae_sess?.api_download_kv[hosted_file_obj?.hosted_file_id]?.percent_completed) {
|
||||
download_percent =
|
||||
$ae_sess.api_download_kv[hosted_file_obj?.hosted_file_id].percent_completed;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if hosted_file_id && hosted_file_obj}
|
||||
<button
|
||||
type="button"
|
||||
disabled={!$ae_loc.trusted_access}
|
||||
class={classes ?? 'btn'}
|
||||
onclick={() => {
|
||||
download_complete = false;
|
||||
download_status_msg = 'Downloading...';
|
||||
ae_promises[hosted_file_obj.hosted_file_id] = api
|
||||
.download_hosted_file({
|
||||
api_cfg: $ae_api,
|
||||
hosted_file_id: hosted_file_obj.hosted_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: ${hosted_file_obj?.hosted_file_id}\n Linked to: ${linked_to_type} ID: ${linked_to_id}`}
|
||||
>
|
||||
{#await ae_promises[hosted_file_obj.hosted_file_id]}
|
||||
<span class="fas fa-spinner fa-spin mx-1"></span>
|
||||
<span class="">
|
||||
Downloading
|
||||
{#if $ae_sess.api_download_kv[hosted_file_obj.hosted_file_id]}
|
||||
{$ae_sess.api_download_kv[hosted_file_obj.hosted_file_id]
|
||||
.percent_completed}%
|
||||
{/if}
|
||||
:
|
||||
</span>
|
||||
{:then}
|
||||
<span class="fas fa-{ae_util.file_extension_icon(hosted_file_obj?.extension)}"></span>
|
||||
{/await}
|
||||
|
||||
{#if download_complete === null}
|
||||
<span class="text-red-800 dark:text-red-200">File not found</span>
|
||||
{:else if download_complete === false}
|
||||
<span class="text-red-800 dark:text-red-200">Failed to download!</span>
|
||||
{/if}
|
||||
|
||||
<span class="grow">
|
||||
{ae_util.shorten_filename({
|
||||
filename: filename ?? hosted_file_obj?.filename,
|
||||
max_length: max_length
|
||||
})}
|
||||
</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button type="button" disabled class={classes ?? 'btn'} title="No file selected">
|
||||
<span class="fas fa-{ae_util.file_extension_icon(hosted_file_obj?.extension)}"></span>
|
||||
<span class="grow"> No file info </span>
|
||||
</button>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user