feat(frontend,backend): storage panel with weight rows and trash, heavy media streams by url in the viewer
This commit is contained in:
1 parent
f2c245707a
commit
55634984fd
19 files changed
+1822
-24
No files matched your search
@@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Dialog } from "bits-ui";
|
||||
import { untrack } from "svelte";
|
||||
import { type MediaResult, requestMedia } from "#lib/api/client.js";
|
||||
import { type MediaResult, mediaUrl, requestMedia } from "#lib/api/client.js";
|
||||
import { downloadMedia } from "#lib/api/download.js";
|
||||
import { fetchMedia, getMessageMedia } from "#lib/api/endpoints.js";
|
||||
import { isPreviewable, type ViewerItem } from "#lib/api/media.js";
|
||||
import {
|
||||
isPreviewable,
|
||||
isStreamable,
|
||||
type ViewerItem,
|
||||
} from "#lib/api/media.js";
|
||||
import Button from "#lib/components/ui/Button.svelte";
|
||||
import ContextMenu from "#lib/components/ui/ContextMenu.svelte";
|
||||
import ContextMenuItem from "#lib/components/ui/ContextMenuItem.svelte";
|
||||
@@ -19,6 +23,7 @@
|
||||
chatId: number;
|
||||
index: number;
|
||||
items: ViewerItem[];
|
||||
ontrash?: (item: ViewerItem) => void;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
@@ -27,14 +32,18 @@
|
||||
index = $bindable(),
|
||||
chatId,
|
||||
items,
|
||||
ontrash,
|
||||
}: Props = $props();
|
||||
|
||||
const STREAM_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
let kind = $state("");
|
||||
let messageId = $state<number | null>(null);
|
||||
let currentMediaId = $state<number | null>(null);
|
||||
let fileName = $state<string | null>(null);
|
||||
let fileSize = $state<number | null>(null);
|
||||
let fileOnly = $state(false);
|
||||
let streamed = $state(false);
|
||||
let result = $state<MediaResult | null>(null);
|
||||
let loading = $state(false);
|
||||
let saving = $state(false);
|
||||
@@ -55,33 +64,51 @@
|
||||
const title = $derived(fileName ?? mediaKindLabel(kind) ?? "Медиа");
|
||||
|
||||
function revoke() {
|
||||
if (result?.state === "ready" && result.url) {
|
||||
if (result?.state === "ready" && result.url && !streamed) {
|
||||
URL.revokeObjectURL(result.url);
|
||||
}
|
||||
}
|
||||
|
||||
function streamSource(
|
||||
item: ViewerItem,
|
||||
mediaId: number | null,
|
||||
size: number | null
|
||||
): string | null {
|
||||
if (item.src) {
|
||||
return item.src;
|
||||
}
|
||||
const heavy = size !== null && size > STREAM_BYTES;
|
||||
return mediaId !== null && heavy && isStreamable(kind, item.mime)
|
||||
? mediaUrl(mediaId)
|
||||
: null;
|
||||
}
|
||||
|
||||
async function load(item: ViewerItem) {
|
||||
revoke();
|
||||
loading = true;
|
||||
result = null;
|
||||
currentMediaId = null;
|
||||
fileOnly = false;
|
||||
streamed = false;
|
||||
({ kind, messageId, fileName, fileSize } = item);
|
||||
token += 1;
|
||||
const current = token;
|
||||
try {
|
||||
let { mediaId, downloaded, mime: mimeType } = item;
|
||||
let { fileName: name, fileSize: size } = item;
|
||||
if (mediaId === null) {
|
||||
if (mediaId === null && !item.src) {
|
||||
const meta = await getMessageMedia(chatId, item.messageId);
|
||||
({ id: mediaId, downloaded, kind, mime: mimeType } = meta);
|
||||
({ file_name: name, file_size: size } = meta);
|
||||
}
|
||||
const plainFile = downloaded && !isPreviewable(kind, mimeType);
|
||||
const source = downloaded ? streamSource(item, mediaId, size) : null;
|
||||
let next: MediaResult;
|
||||
if (plainFile) {
|
||||
next = { mime: mimeType, state: "ready", url: "" };
|
||||
} else if (downloaded) {
|
||||
} else if (source !== null) {
|
||||
next = { mime: mimeType, state: "ready", url: source };
|
||||
} else if (downloaded && mediaId !== null) {
|
||||
next = await requestMedia(mediaId);
|
||||
} else {
|
||||
next = { state: "not-downloaded" } as MediaResult;
|
||||
@@ -92,6 +119,7 @@
|
||||
fileName = name;
|
||||
fileSize = size;
|
||||
fileOnly = plainFile;
|
||||
streamed = source !== null && !plainFile;
|
||||
}
|
||||
} catch {
|
||||
if (current === token) {
|
||||
@@ -136,6 +164,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function trash() {
|
||||
const item = items[index];
|
||||
if (item && ontrash) {
|
||||
open = false;
|
||||
ontrash(item);
|
||||
}
|
||||
}
|
||||
|
||||
function step(delta: number) {
|
||||
const next = index + delta;
|
||||
if (next >= 0 && next < items.length) {
|
||||
@@ -214,6 +250,16 @@
|
||||
>
|
||||
<Icon name="allow-share" size="1.375rem" />
|
||||
</button>
|
||||
{#if ontrash}
|
||||
<button
|
||||
aria-label="В корзину"
|
||||
class="media-action"
|
||||
onclick={trash}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="delete" size="1.375rem" />
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<Dialog.Close aria-label="Закрыть" class="media-close">
|
||||
<Icon name="close" size="1.5rem" />
|
||||
@@ -234,7 +280,7 @@
|
||||
class="media-video"
|
||||
controls
|
||||
playsinline
|
||||
preload="auto"
|
||||
preload={streamed ? "metadata" : "auto"}
|
||||
src={result.url}
|
||||
use:poster
|
||||
></video>
|
||||
@@ -248,10 +294,12 @@
|
||||
{#if fileSize}
|
||||
<p class="file-size">{formatBytes(fileSize)}</p>
|
||||
{/if}
|
||||
<button class="media-download" onclick={save} type="button">
|
||||
<Icon name="download" />
|
||||
Скачать файл
|
||||
</button>
|
||||
{#if currentMediaId !== null}
|
||||
<button class="media-download" onclick={save} type="button">
|
||||
<Icon name="download" />
|
||||
Скачать файл
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if result?.state === "not-downloaded"}
|
||||
<div class="media-message">
|
||||
|
||||
Reference in new issue
Block a user