fix(frontend): render document albums as file rows instead of broken images

This commit is contained in:
hh
2026-08-13 03:59:01 +02:00
parent e833de245a
commit 6ae28cb321
3 changed files with 178 additions and 69 deletions
+26 -5
View File
@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { isPreviewable } from "$lib/api/media";
import type { MediaRef } from "$lib/api/types"; import type { MediaRef } from "$lib/api/types";
import AlbumTile from "$lib/components/media/AlbumTile.svelte"; import AlbumTile from "$lib/components/media/AlbumTile.svelte";
import FileChip from "$lib/components/media/FileChip.svelte";
interface Props { interface Props {
chatId: number; chatId: number;
@@ -10,6 +12,10 @@
let { media, chatId, onopen }: Props = $props(); let { media, chatId, onopen }: Props = $props();
const asFiles = $derived(
media.every((item) => !isPreviewable(item.kind, item.mime))
);
const columns = $derived.by(() => { const columns = $derived.by(() => {
const count = media.length; const count = media.length;
if (count === 2) { if (count === 2) {
@@ -22,13 +28,28 @@
}); });
</script> </script>
<div class="MediaAlbum" style:--cols={columns}> {#if asFiles}
{#each media as item, index (item.id ?? index)} <div class="AlbumFiles">
<AlbumTile media={item} {chatId} onopen={() => onopen(index)} /> {#each media as item, index (item.id ?? index)}
{/each} <FileChip media={item} />
</div> {/each}
</div>
{:else}
<div class="MediaAlbum" style:--cols={columns}>
{#each media as item, index (item.id ?? index)}
<AlbumTile media={item} {chatId} onopen={() => onopen(index)} />
{/each}
</div>
{/if}
<style lang="scss"> <style lang="scss">
.AlbumFiles {
display: flex;
flex-direction: column;
gap: 0.125rem;
margin-bottom: 0.25rem;
}
.MediaAlbum { .MediaAlbum {
display: grid; display: grid;
grid-template-columns: repeat(var(--cols), 1fr); grid-template-columns: repeat(var(--cols), 1fr);
@@ -10,6 +10,7 @@
} from "$lib/api/media"; } from "$lib/api/media";
import type { MessageView } from "$lib/api/types"; import type { MessageView } from "$lib/api/types";
import AudioFile from "$lib/components/media/AudioFile.svelte"; import AudioFile from "$lib/components/media/AudioFile.svelte";
import FileChip from "$lib/components/media/FileChip.svelte";
import TgsSticker from "$lib/components/media/TgsSticker.svelte"; import TgsSticker from "$lib/components/media/TgsSticker.svelte";
import VideoNote from "$lib/components/media/VideoNote.svelte"; import VideoNote from "$lib/components/media/VideoNote.svelte";
import VoiceMessage from "$lib/components/media/VoiceMessage.svelte"; import VoiceMessage from "$lib/components/media/VoiceMessage.svelte";
@@ -142,18 +143,8 @@
<Icon name="timer" size="1.25rem" /> <Icon name="timer" size="1.25rem" />
<span>Самоуничтожающееся медиа</span> <span>Самоуничтожающееся медиа</span>
</button> </button>
{:else if asFile} {:else if asFile && ref}
<button class="media-file" onclick={save} type="button"> <FileChip media={ref} />
<span class="file-glyph">
<Icon name={saving ? "timer" : "document"} size="1.25rem" />
</span>
<span class="file-text">
<span class="file-name">{fileName}</span>
<span class="file-sub">
{formatBytes(ref?.file_size ?? null) || mediaKindLabel(kind)}
</span>
</span>
</button>
{:else if !loaded} {:else if !loaded}
<div class="media-skeleton"><Spinner /></div> <div class="media-skeleton"><Spinner /></div>
{:else if ready && kind === "voice"} {:else if ready && kind === "voice"}
@@ -370,58 +361,6 @@
} }
} }
.media-file {
cursor: pointer;
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
max-width: 18rem;
padding: 0.5rem 0.75rem 0.5rem 0.5rem;
border: 0;
border-radius: var(--border-radius-default-small);
text-align: start;
background-color: var(--color-primary-tint);
}
.file-glyph {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
color: var(--color-white);
background-color: var(--color-primary);
}
.file-text {
display: flex;
flex-direction: column;
min-width: 0;
}
.file-name {
overflow: hidden;
font-size: 0.9375rem;
color: var(--color-text);
text-overflow: ellipsis;
white-space: nowrap;
}
.file-sub {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.media-chip { .media-chip {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@@ -0,0 +1,149 @@
<script lang="ts">
import { downloadMedia } from "$lib/api/download";
import type { MediaRef } from "$lib/api/types";
import Icon from "$lib/components/ui/Icon.svelte";
import { formatBytes, mediaKindLabel } from "$lib/format/media";
import { shareUi } from "$lib/stores/shares.svelte";
import { toasts } from "$lib/stores/toasts.svelte";
interface Props {
media: MediaRef;
}
const { media }: Props = $props();
let saving = $state(false);
const name = $derived(
media.file_name ?? mediaKindLabel(media.kind) ?? "Файл"
);
const size = $derived(formatBytes(media.file_size));
async function save() {
if (media.id === null || saving) {
return;
}
saving = true;
try {
await downloadMedia(media.id);
} catch {
toasts.error("Не удалось скачать файл");
} finally {
saving = false;
}
}
function share() {
if (media.id !== null) {
shareUi.share({ kind: "media", mediaId: media.id }, name);
}
}
</script>
<div class="FileChip">
<button class="file-main" type="button" title={name} onclick={save}>
<span class="file-glyph">
<Icon name={saving ? "timer" : "document"} size="1.25rem" />
</span>
<span class="file-text">
<span class="file-name">{name}</span>
<span class="file-sub">{size || mediaKindLabel(media.kind)}</span>
</span>
</button>
<button
class="file-share"
type="button"
aria-label="Доступ по ссылке"
onclick={share}
>
<Icon name="allow-share" size="1.125rem" />
</button>
</div>
<style lang="scss">
.FileChip {
display: flex;
align-items: center;
gap: 0.25rem;
width: 100%;
max-width: 20rem;
padding: 0.25rem 0.375rem 0.25rem 0.25rem;
border-radius: var(--border-radius-default-small);
background-color: var(--color-primary-tint);
}
.file-main {
cursor: pointer;
display: flex;
flex: 1;
align-items: center;
gap: 0.625rem;
min-width: 0;
padding: 0.25rem;
border: 0;
font-family: inherit;
text-align: start;
background: transparent;
}
.file-glyph {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
color: var(--color-white);
background-color: var(--color-primary);
}
.file-text {
display: flex;
flex-direction: column;
min-width: 0;
}
.file-name {
overflow: hidden;
font-size: 0.9375rem;
color: var(--color-text);
text-overflow: ellipsis;
white-space: nowrap;
}
.file-sub {
font-size: 0.75rem;
color: var(--color-text-secondary);
}
.file-share {
cursor: pointer;
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: 0;
border-radius: 50%;
color: var(--color-primary);
background: transparent;
&:hover {
background-color: var(--color-primary-opacity);
}
}
</style>