fix(frontend,backend): storage categories keep one hue in both themes, trash counted in the total, undo keeps the list, larger touch targets

This commit is contained in:
hh committed 2026-10-14 03:18:59 +02:00
1 parent 06ae3e928f
commit 580927da0a
11 files changed
+112 -47

No files matched your search

+28 -10
View File
@@ -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 @@
<Dialog.Overlay class="media-overlay" />
<Dialog.Content class="media-content">
<Dialog.Title class="media-title">{title}</Dialog.Title>
{#if caption}
<span class="media-caption">{caption}</span>
{/if}
<div class="media-actions">
{#if canSave}
{#if ontrash}
<button
aria-label="В корзину"
class="media-action"
onclick={trash}
type="button"
>
<Icon name="delete" size="1.375rem" />
</button>
{/if}
<ContextMenu>
{#snippet children({
props,
@@ -250,16 +264,6 @@
>
<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" />
@@ -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;
@@ -45,7 +45,9 @@
let failed = $state(false);
let done = $state(false);
let busy = $state(false);
let undo = $state<TrashOutcome | null>(null);
let undo = $state<{ files: StorageFile[]; outcome: TrashOutcome } | null>(
null
);
let problem = $state<string | null>(null);
let viewerOpen = $state(false);
let viewerIndex = $state(0);
@@ -67,6 +69,7 @@
);
const viewerItems = $derived<ViewerItem[]>(
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 @@
<section class="summary">
<div class="hero">
<span class="total">{formatBytes(summary.bytes)}</span>
<span class="count">{filesLabel(summary.files)} на диске</span>
<span class="count">
{filesLabel(summary.files)}
{summary.trash_bytes > 0
? `· ещё ${formatBytes(summary.trash_bytes)} в корзине`
: "на диске"}
</span>
</div>
<div class="stack">
{#each groups as item (item.group)}
@@ -244,6 +253,13 @@
style:flex-grow={item.bytes}
></span>
{/each}
{#if summary.trash_bytes > 0}
<span
class="trashed"
title="Корзина: {formatBytes(summary.trash_bytes)}"
style:flex-grow={summary.trash_bytes}
></span>
{/if}
</div>
<div class="legend">
{#each groups as item (item.group)}
@@ -376,8 +392,9 @@
</SelectionBar>
{:else if undo}
<SelectionBar
note={problem ?? `${formatBytes(undo.bytes)} освободится через 7 дней`}
title="{filesLabel(undo.files)} в корзине"
note={problem ??
`${formatBytes(undo.outcome.bytes)} освободится через 7 дней`}
title="{filesLabel(undo.outcome.files)} в корзине"
warn={problem !== null}
>
<Button onclick={restoreLast} smaller variant="text">
@@ -454,6 +471,10 @@
min-width: 0.1875rem;
border-radius: 2px;
&.trashed {
background-color: rgba(var(--color-text-secondary-rgb), 0.35);
}
&:first-child {
border-start-start-radius: 0.3125rem;
border-end-start-radius: 0.3125rem;
@@ -501,6 +522,12 @@
box-shadow: inset 0 0 0 1px var(--color-primary);
}
@media (pointer: coarse) {
height: 2.25rem;
padding: 0 0.75rem 0 0.625rem;
border-radius: 1.125rem;
}
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 1px;
@@ -113,8 +113,9 @@
align-items: center;
justify-content: center;
align-self: stretch;
width: 2.5rem;
height: 2.5rem;
min-height: 2.5rem;
input {
cursor: pointer;
@@ -19,7 +19,11 @@
import Spinner from "#lib/components/ui/Spinner.svelte";
import { formatBytes } from "#lib/format/media.js";
import { plural } from "#lib/format/shares.js";
import { filesLabel, purgeLabel } from "#lib/format/storage.js";
import {
filesLabel,
messagesLabel,
purgeLabel,
} from "#lib/format/storage.js";
import { chats } from "#lib/stores/chats.svelte.js";
import { ui } from "#lib/stores/ui.svelte.js";
import { isMobile } from "#lib/viewport.js";
@@ -50,6 +54,7 @@
);
const viewerItems = $derived<ViewerItem[]>(
items.map((item) => ({
caption: `${formatBytes(item.file_size)} · ${chatTitle(item.chat_id)}`,
downloaded: true,
fileName: item.file_name,
fileSize: item.file_size,
@@ -180,6 +185,7 @@
size={item.file_size}
src={trashedUrl(item.storage_key)}
sub={chatTitle(item.chat_id)}
tail={item.refs > 1 ? messagesLabel(item.refs) : undefined}
weight={heaviest > 0 ? (item.file_size ?? 0) / heaviest : 0}
>
{#snippet menu()}
@@ -292,17 +298,7 @@
"будут удалены"
)}
с диска.
{#if doomedGone < doomed.length}
{doomedGone === 0 ? "Потом их" : "Остальные потом"}
можно скачать из Telegram заново.
{/if}
</p>
{#if problem}
<p class="confirm warn">
<Icon name="warning" size="1.25rem" />
<span>{problem}.</span>
</p>
{/if}
{#if doomedGone > 0}
<p class="confirm warn">
<Icon name="warning" size="1.25rem" />
@@ -314,6 +310,18 @@
</span>
</p>
{/if}
{#if doomedGone < doomed.length}
<p class="confirm rest">
{doomedGone === 0 ? "Потом их" : "Остальные"}
можно скачать из Telegram заново.
</p>
{/if}
{#if problem}
<p class="confirm warn">
<Icon name="warning" size="1.25rem" />
<span>{problem}.</span>
</p>
{/if}
</div>
{/if}
<div class="dialog-actions">
@@ -431,6 +439,10 @@
font-size: 0.9375rem;
line-height: 1.45;
&.rest {
margin-top: 0.75rem;
}
&.warn {
display: flex;
gap: 0.625rem;