refactor(frontend): storage panel answers in place instead of toasts, empty groups hidden, weight bar animates by transform

This commit is contained in:
hh committed 2026-10-14 03:14:51 +02:00
1 parent 55634984fd
commit 06ae3e928f
4 files changed
+110 -62

No files matched your search

@@ -31,7 +31,6 @@
messagesLabel,
} from "#lib/format/storage.js";
import { chats } from "#lib/stores/chats.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js";
import { ui } from "#lib/stores/ui.svelte.js";
import { isMobile } from "#lib/viewport.js";
import { goto } from "$app/navigation";
@@ -47,6 +46,7 @@
let done = $state(false);
let busy = $state(false);
let undo = $state<TrashOutcome | null>(null);
let problem = $state<string | null>(null);
let viewerOpen = $state(false);
let viewerIndex = $state(0);
const selected = new SvelteSet<number>();
@@ -61,8 +61,9 @@
const chosenGone = $derived(
chosen.filter((file) => !file.refetchable).length
);
const segments = $derived(
summary?.groups.filter((item) => item.bytes > 0) ?? []
const groups = $derived(
summary?.groups.filter((item) => item.files > 0 || item.group === group) ??
[]
);
const viewerItems = $derived<ViewerItem[]>(
files.map((file) => ({
@@ -133,6 +134,7 @@
function toggle(mediaId: number) {
hideUndo();
problem = null;
if (!selected.delete(mediaId)) {
selected.add(mediaId);
}
@@ -156,6 +158,7 @@
return;
}
busy = true;
problem = null;
try {
const outcome = await moveToTrash(mediaIds);
const moved = new Set(
@@ -170,11 +173,11 @@
selected.clear();
showUndo(outcome);
if (outcome.blocked.length > 0) {
toasts.error("Файл занят в сторис или аватаре и остался на диске");
problem = "Файл занят в сторис или аватаре и остался на диске";
}
await loadSummary();
} catch {
toasts.error("Не удалось переместить в корзину");
problem = "Не удалось переместить в корзину";
} finally {
busy = false;
}
@@ -190,7 +193,7 @@
await restoreFromTrash(keys);
reload();
} catch {
toasts.error("Не удалось вернуть файлы");
problem = "Не удалось вернуть файлы";
}
}
@@ -198,7 +201,7 @@
try {
await downloadMedia(file.media_id);
} catch {
toasts.error("Не удалось скачать файл");
problem = "Не удалось скачать файл";
}
}
@@ -217,6 +220,7 @@
function pick(next: StorageGroupName) {
group = group === next ? null : next;
problem = null;
reload();
}
@@ -226,14 +230,14 @@
<div class="storage">
{#if summary}
<section class="summary">
<div class="hero">
<span class="total">{formatBytes(summary.bytes) || "0 B"}</span>
<span class="count">{filesLabel(summary.files)} на диске</span>
</div>
{#if segments.length > 0}
{#if summary.files > 0}
<section class="summary">
<div class="hero">
<span class="total">{formatBytes(summary.bytes)}</span>
<span class="count">{filesLabel(summary.files)} на диске</span>
</div>
<div class="stack">
{#each segments as item (item.group)}
{#each groups as item (item.group)}
<span
title="{GROUP_LABELS[item.group]}: {formatBytes(item.bytes)}"
style:background-color="var(--color-storage-{item.group})"
@@ -241,27 +245,26 @@
></span>
{/each}
</div>
{/if}
<div class="legend">
{#each summary.groups as item (item.group)}
<button
aria-pressed={group === item.group}
class="chip"
disabled={item.files === 0}
onclick={() => pick(item.group)}
type="button"
class:on={group === item.group}
>
<span
class="dot"
style:background-color="var(--color-storage-{item.group})"
></span>
{GROUP_LABELS[item.group]}
<span class="value">{formatBytes(item.bytes) || "0"}</span>
</button>
{/each}
</div>
</section>
<div class="legend">
{#each groups as item (item.group)}
<button
aria-pressed={group === item.group}
class="chip"
onclick={() => pick(item.group)}
type="button"
class:on={group === item.group}
>
<span
class="dot"
style:background-color="var(--color-storage-{item.group})"
></span>
{GROUP_LABELS[item.group]}
<span class="value">{formatBytes(item.bytes)}</span>
</button>
{/each}
</div>
</section>
{/if}
<button
class="trash-link"
@@ -279,16 +282,18 @@
</button>
{/if}
<div class="section-title">
<h3>Самые тяжёлые</h3>
{#if summary && group === null && summary.heavy_files > 0}
<span>
{filesLabel(summary.heavy_files)}
крупнее {formatBytes(summary.heavy_threshold)}
= {formatBytes(summary.heavy_bytes)}
</span>
{/if}
</div>
{#if files.length > 0}
<div class="section-title">
<h3>Самые тяжёлые</h3>
{#if summary && group === null && summary.heavy_files > 0}
<span>
{filesLabel(summary.heavy_files)}
крупнее {formatBytes(summary.heavy_threshold)}
= {formatBytes(summary.heavy_bytes)}
</span>
{/if}
</div>
{/if}
<div class="list">
{#each files as file, index (file.media_id)}
@@ -350,7 +355,8 @@
{#if chosen.length > 0}
<SelectionBar
note={chosenGone > 0 ? `${chosenGone} не скачать заново` : undefined}
note={problem ??
(chosenGone > 0 ? `${chosenGone} не скачать заново` : undefined)}
title="{filesLabel(chosen.length)} · {formatBytes(chosenBytes)}"
warn
>
@@ -370,14 +376,27 @@
</SelectionBar>
{:else if undo}
<SelectionBar
note="{formatBytes(undo.bytes)} освободится через 7 дней"
note={problem ?? `${formatBytes(undo.bytes)} освободится через 7 дней`}
title="{filesLabel(undo.files)} в корзине"
warn={problem !== null}
>
<Button onclick={restoreLast} smaller variant="text">
<Icon name="undo" size="1.25rem" />
<span>Вернуть</span>
</Button>
</SelectionBar>
{:else if problem}
<SelectionBar title={problem}>
<Button
onclick={() => {
problem = null;
}}
smaller
variant="text"
>
Понятно
</Button>
</SelectionBar>
{/if}
</div>
@@ -482,18 +501,13 @@
box-shadow: inset 0 0 0 1px var(--color-primary);
}
&:disabled {
cursor: default;
opacity: 0.5;
}
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 1px;
}
@media (hover: hover) {
&:not(:disabled, .on):hover {
&:not(.on):hover {
background-color: var(--color-background-secondary-accent);
}
}
@@ -42,7 +42,7 @@
const MIN_WEIGHT = 0.02;
const width = $derived(`${Math.max(MIN_WEIGHT, Math.min(1, weight)) * 100}%`);
const scale = $derived(Math.max(MIN_WEIGHT, Math.min(1, weight)));
</script>
<div
@@ -81,7 +81,7 @@
{/if}
</span>
<span class="size">{formatBytes(size)}</span>
<span class="weight"><span style:width></span></span>
<span class="weight"><span style:--scale={scale}></span></span>
</button>
{/snippet}
</ContextMenu>
@@ -237,6 +237,9 @@
background-color: rgba(var(--color-text-secondary-rgb), 0.16);
span {
transform: scaleX(var(--scale));
transform-origin: 0 50%;
display: block;
height: 100%;
@@ -244,7 +247,7 @@
background-color: var(--group);
transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
}
</style>
@@ -84,7 +84,7 @@
use:poster
></video>
{#if shown}
<span class="play"><Icon name="play" size="1.125rem" /></span>
<span class="play"><Icon name="play" size="0.875rem" /></span>
{/if}
{/if}
</span>
@@ -138,7 +138,15 @@
.play {
position: absolute;
display: flex;
text-shadow: 0 0 0.375rem rgb(0 0 0 / 70%);
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background-color: rgb(0 0 0 / 45%);
}
</style>
@@ -21,7 +21,6 @@
import { plural } from "#lib/format/shares.js";
import { filesLabel, purgeLabel } from "#lib/format/storage.js";
import { chats } from "#lib/stores/chats.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js";
import { ui } from "#lib/stores/ui.svelte.js";
import { isMobile } from "#lib/viewport.js";
import { goto } from "$app/navigation";
@@ -31,6 +30,7 @@
let failed = $state(false);
let busy = $state(false);
let doomed = $state<TrashItem[] | null>(null);
let problem = $state<string | null>(null);
let viewerOpen = $state(false);
let viewerIndex = $state(0);
const selected = new SvelteSet<string>();
@@ -83,6 +83,7 @@
}
function toggle(key: string) {
problem = null;
if (!selected.delete(key)) {
selected.add(key);
}
@@ -99,12 +100,13 @@
return;
}
busy = true;
problem = null;
const keys = list.map((item) => item.storage_key);
try {
await restoreFromTrash(keys);
drop(keys);
} catch {
toasts.error("Не удалось вернуть файлы");
problem = "Не удалось вернуть файлы";
} finally {
busy = false;
}
@@ -115,13 +117,14 @@
return;
}
busy = true;
problem = null;
const keys = doomed.map((item) => item.storage_key);
try {
await purgeTrash(keys);
drop(keys);
doomed = null;
} catch {
toasts.error("Не удалось удалить файлы");
problem = "Не удалось удалить файлы";
} finally {
busy = false;
}
@@ -215,7 +218,8 @@
{#if chosen.length > 0}
<SelectionBar
note={chosenGone > 0 ? `${chosenGone} не скачать заново` : undefined}
note={problem ??
(chosenGone > 0 ? `${chosenGone} не скачать заново` : undefined)}
title="{filesLabel(chosen.length)} · {formatBytes(bytesOf(chosen))}"
warn
>
@@ -240,6 +244,18 @@
<span>Вернуть</span>
</Button>
</SelectionBar>
{:else if problem && doomed === null}
<SelectionBar title={problem}>
<Button
onclick={() => {
problem = null;
}}
smaller
variant="text"
>
Понятно
</Button>
</SelectionBar>
{/if}
</div>
@@ -247,6 +263,7 @@
onOpenChange={(next) => {
if (!next) {
doomed = null;
problem = null;
}
}}
open={doomed !== null}
@@ -280,6 +297,12 @@
можно скачать из 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" />