From 580927da0a3746706f5c58e589dac8042c2a347d Mon Sep 17 00:00:00 2001 From: h Date: Wed, 14 Oct 2026 03:18:59 +0200 Subject: [PATCH] fix(frontend,backend): storage categories keep one hue in both themes, trash counted in the total, undo keeps the list, larger touch targets --- backend/src/utils/read/models.py | 1 + backend/src/utils/read/storage.py | 4 ++ frontend/src/lib/api/media.ts | 1 + frontend/src/lib/api/types.ts | 1 + .../src/lib/components/MediaViewer.svelte | 38 +++++++++---- .../lib/components/disk/StoragePanel.svelte | 57 ++++++++++++++----- .../src/lib/components/disk/StorageRow.svelte | 3 +- .../src/lib/components/disk/TrashPanel.svelte | 34 +++++++---- frontend/src/lib/format/storage.ts | 6 +- frontend/src/lib/styles/dark-theme.scss | 2 +- frontend/src/lib/styles/variables.scss | 12 ++-- 11 files changed, 112 insertions(+), 47 deletions(-) diff --git a/backend/src/utils/read/models.py b/backend/src/utils/read/models.py index 88b801a..2ec8ccf 100644 --- a/backend/src/utils/read/models.py +++ b/backend/src/utils/read/models.py @@ -276,6 +276,7 @@ class TrashItemView(BaseModel): file_size: int | None trashed_at: datetime purge_at: datetime + refs: int refetchable: bool diff --git a/backend/src/utils/read/storage.py b/backend/src/utils/read/storage.py index bdf9819..736d807 100644 --- a/backend/src/utils/read/storage.py +++ b/backend/src/utils/read/storage.py @@ -76,6 +76,7 @@ ORDER BY file_size DESC NULLS LAST, storage_key _TRASH_REFS = """ SELECT * FROM ( SELECT evicted_key AS storage_key, chat_id, message_id, kind, ttl_seconds, + count(*) OVER (PARTITION BY evicted_key) AS refs, row_number() OVER (PARTITION BY evicted_key ORDER BY id) AS n FROM media WHERE account_id = $1 AND evicted_key = ANY($2::text[]) @@ -185,12 +186,15 @@ async def list_trash(pool: asyncpg.Pool, account_id: int) -> list[TrashItemView] ) messages = await _messages(pool, account_id, refs) alive: dict[str, bool] = defaultdict(bool) + counts: dict[str, int] = {} for ref, message in zip(refs, messages, strict=True): alive[ref["storage_key"]] |= _alive(ref, message) + counts[ref["storage_key"]] = ref["refs"] return [ TrashItemView( **dict(row), purge_at=row["trashed_at"] + RETENTION, + refs=counts.get(row["storage_key"], 0), refetchable=alive.get(row["storage_key"], True), ) for row in rows diff --git a/frontend/src/lib/api/media.ts b/frontend/src/lib/api/media.ts index d4a01a4..1199060 100644 --- a/frontend/src/lib/api/media.ts +++ b/frontend/src/lib/api/media.ts @@ -19,6 +19,7 @@ export type InlineMedia = | { state: "missing" }; export interface ViewerItem { + caption?: string; downloaded: boolean; fileName: string | null; fileSize: number | null; diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 8c1535a..75f554a 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -602,6 +602,7 @@ export interface TrashItem { mime: string | null; purge_at: string; refetchable: boolean; + refs: number; storage_key: string; trashed_at: string; } diff --git a/frontend/src/lib/components/MediaViewer.svelte b/frontend/src/lib/components/MediaViewer.svelte index 414615a..8802188 100644 --- a/frontend/src/lib/components/MediaViewer.svelte +++ b/frontend/src/lib/components/MediaViewer.svelte @@ -62,6 +62,7 @@ currentMediaId !== null && (fileOnly || result?.state === "ready") ); const title = $derived(fileName ?? mediaKindLabel(kind) ?? "Медиа"); + const caption = $derived(items[index]?.caption); function revoke() { if (result?.state === "ready" && result.url && !streamed) { @@ -217,8 +218,21 @@ {title} + {#if caption} + {caption} + {/if}
{#if canSave} + {#if ontrash} + + {/if} {#snippet children({ props, @@ -250,16 +264,6 @@ > - {#if ontrash} - - {/if} {/if} @@ -371,6 +375,20 @@ white-space: nowrap; } + .media-caption { + position: absolute; + top: 2.75rem; + left: 1.25rem; + + overflow: hidden; + max-width: min(24rem, calc(100% - 14rem)); + + font-size: 0.8125rem; + color: rgba(255, 255, 255, 0.6); + text-overflow: ellipsis; + white-space: nowrap; + } + .media-counter { position: absolute; top: 1rem; diff --git a/frontend/src/lib/components/disk/StoragePanel.svelte b/frontend/src/lib/components/disk/StoragePanel.svelte index ced8346..8a7c1d8 100644 --- a/frontend/src/lib/components/disk/StoragePanel.svelte +++ b/frontend/src/lib/components/disk/StoragePanel.svelte @@ -45,7 +45,9 @@ let failed = $state(false); let done = $state(false); let busy = $state(false); - let undo = $state(null); + let undo = $state<{ files: StorageFile[]; outcome: TrashOutcome } | null>( + null + ); let problem = $state(null); let viewerOpen = $state(false); let viewerIndex = $state(0); @@ -67,6 +69,7 @@ ); const viewerItems = $derived( files.map((file) => ({ + caption: `${formatBytes(file.file_size)} · ${chatTitle(file.chat_id)}`, downloaded: true, fileName: file.file_name, fileSize: file.file_size, @@ -133,7 +136,6 @@ } function toggle(mediaId: number) { - hideUndo(); problem = null; if (!selected.delete(mediaId)) { selected.add(mediaId); @@ -145,10 +147,10 @@ undo = null; } - function showUndo(outcome: TrashOutcome) { + function showUndo(outcome: TrashOutcome, trashed: StorageFile[]) { hideUndo(); if (outcome.files > 0) { - undo = outcome; + undo = { files: trashed, outcome }; undoTimer = setTimeout(hideUndo, UNDO_MS); } } @@ -164,14 +166,13 @@ const moved = new Set( mediaIds.filter((id) => !outcome.blocked.includes(id)) ); - for (const file of files) { - if (moved.has(file.media_id)) { - forgetMedia(file.chat_id, file.message_id, file.media_id); - } + const trashed = files.filter((file) => moved.has(file.media_id)); + for (const file of trashed) { + forgetMedia(file.chat_id, file.message_id, file.media_id); } files = files.filter((file) => !moved.has(file.media_id)); selected.clear(); - showUndo(outcome); + showUndo(outcome, trashed); if (outcome.blocked.length > 0) { problem = "Файл занят в сторис или аватаре и остался на диске"; } @@ -187,11 +188,14 @@ if (undo === null) { return; } - const { keys } = undo; + const restored = undo; hideUndo(); try { - await restoreFromTrash(keys); - reload(); + await restoreFromTrash(restored.outcome.keys); + files = [...files, ...restored.files].sort( + (a, b) => b.file_size - a.file_size + ); + await loadSummary(); } catch { problem = "Не удалось вернуть файлы"; } @@ -234,7 +238,12 @@
{formatBytes(summary.bytes)} - {filesLabel(summary.files)} на диске + + {filesLabel(summary.files)} + {summary.trash_bytes > 0 + ? `· ещё ${formatBytes(summary.trash_bytes)} в корзине` + : "на диске"} +
{#each groups as item (item.group)} @@ -244,6 +253,13 @@ style:flex-grow={item.bytes} > {/each} + {#if summary.trash_bytes > 0} + + {/if}
{#each groups as item (item.group)} @@ -376,8 +392,9 @@ {:else if undo}
{/if}
@@ -431,6 +439,10 @@ font-size: 0.9375rem; line-height: 1.45; + &.rest { + margin-top: 0.75rem; + } + &.warn { display: flex; gap: 0.625rem; diff --git a/frontend/src/lib/format/storage.ts b/frontend/src/lib/format/storage.ts index e2963e1..a664b7c 100644 --- a/frontend/src/lib/format/storage.ts +++ b/frontend/src/lib/format/storage.ts @@ -42,10 +42,10 @@ export function fileExtension(fileName: string): string { export function purgeLabel(iso: string): string { const days = Math.ceil((new Date(iso).getTime() - Date.now()) / DAY_MS); if (days <= 0) { - return "Удалится сегодня"; + return "сегодня"; } if (days === 1) { - return "Удалится завтра"; + return "завтра"; } - return `Удалится через ${days} дн.`; + return `через ${days} дн.`; } diff --git a/frontend/src/lib/styles/dark-theme.scss b/frontend/src/lib/styles/dark-theme.scss index d199592..d3ac27c 100644 --- a/frontend/src/lib/styles/dark-theme.scss +++ b/frontend/src/lib/styles/dark-theme.scss @@ -5,7 +5,7 @@ html.theme-dark { --color-storage-audio: #c98500; --color-storage-other: #3987e5; --color-storage-warning: #fb8c00; - --storage-tile-mix: 78%; + --storage-tile-mix: 90%; --color-primary: #8774e1; --color-primary-opacity: #8378db1e; --color-primary-opacity-hover: #8378db40; diff --git a/frontend/src/lib/styles/variables.scss b/frontend/src/lib/styles/variables.scss index 76a7a8b..128eb1e 100644 --- a/frontend/src/lib/styles/variables.scss +++ b/frontend/src/lib/styles/variables.scss @@ -133,13 +133,13 @@ --color-warning: #{$color-warning}; - --color-storage-video: #3390ec; - --color-storage-file: #eb6834; - --color-storage-voice: #1baf7a; - --color-storage-audio: #eda100; - --color-storage-other: #6250d6; + --color-storage-video: #6250d6; + --color-storage-file: #c4501c; + --color-storage-voice: #12805a; + --color-storage-audio: #a36a00; + --color-storage-other: #2a78c8; --color-storage-warning: #a85a00; - --storage-tile-mix: 62%; + --storage-tile-mix: 100%; --color-yellow: #{$color-yellow};