feat(frontend): storage, trash, policy, stories, shares, jobs, bots, media, login and notes speak russian and english
This commit is contained in:
1 parent
e5a5edcaae
commit
4a5c92f0e9
51 files changed
+1504
-588
No files matched your search
@@ -31,9 +31,11 @@
|
||||
import { formatBytes } from "#lib/format/media.js";
|
||||
import {
|
||||
filesLabel,
|
||||
GROUP_LABELS,
|
||||
groupLabel,
|
||||
messagesLabel,
|
||||
} from "#lib/format/storage.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||
import { chats } from "#lib/stores/chats.svelte.js";
|
||||
import { policyUi } from "#lib/stores/policy.svelte.js";
|
||||
import {
|
||||
@@ -53,9 +55,9 @@
|
||||
const PAGE = 30;
|
||||
const UNDO_MS = 12_000;
|
||||
const UNDO_GUARD_MS = 600;
|
||||
const LISTINGS: { label: string; value: StorageListing }[] = [
|
||||
{ label: "Файлы", value: "files" },
|
||||
{ label: "Чаты", value: "chats" },
|
||||
const LISTINGS: { label: MessageKey; value: StorageListing }[] = [
|
||||
{ label: "storage.listing.files", value: "files" },
|
||||
{ label: "storage.listing.chats", value: "chats" },
|
||||
];
|
||||
|
||||
let summary = $state<StorageSummary | null>(null);
|
||||
@@ -70,7 +72,7 @@
|
||||
let undo = $state<{ files: StorageFile[]; outcome: TrashOutcome } | null>(
|
||||
null
|
||||
);
|
||||
let problem = $state<string | null>(null);
|
||||
let problem = $state<MessageKey | null>(null);
|
||||
let viewerOpen = $state(false);
|
||||
let viewerIndex = $state(0);
|
||||
const selected = new SvelteSet<number>();
|
||||
@@ -89,12 +91,11 @@
|
||||
const heaviestChat = $derived(heavyChats[0]?.bytes ?? 0);
|
||||
const emptiness = $derived.by(() => {
|
||||
if (group !== null) {
|
||||
return "В этой группе нет файлов.";
|
||||
return t("storage.empty.group");
|
||||
}
|
||||
return chatId === null
|
||||
? "На диске нет скачанных файлов."
|
||||
: "В этом чате нет скачанных файлов.";
|
||||
return chatId === null ? t("storage.empty.all") : t("storage.empty.chat");
|
||||
});
|
||||
const failure = $derived(problem && t(problem));
|
||||
const chosen = $derived(files.filter((file) => selected.has(file.media_id)));
|
||||
const chosenBytes = $derived(
|
||||
chosen.reduce((sum, file) => sum + file.file_size, 0)
|
||||
@@ -120,7 +121,7 @@
|
||||
);
|
||||
|
||||
function chatTitle(id: number): string {
|
||||
return chats.byId(id)?.title ?? `Чат ${id}`;
|
||||
return chats.byId(id)?.title ?? t("ui.chat", { id });
|
||||
}
|
||||
|
||||
async function loadSummary() {
|
||||
@@ -249,11 +250,11 @@
|
||||
scope?.refresh();
|
||||
showUndo(outcome, trashed);
|
||||
if (outcome.blocked.length > 0) {
|
||||
problem = "Файл занят в сторис или аватаре и остался на диске";
|
||||
problem = "storage.blocked";
|
||||
}
|
||||
await loadSummary();
|
||||
} catch {
|
||||
problem = "Не удалось переместить в корзину";
|
||||
problem = "storage.trashFailed";
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
@@ -273,7 +274,7 @@
|
||||
scope?.refresh();
|
||||
await loadSummary();
|
||||
} catch {
|
||||
problem = "Не удалось вернуть файлы";
|
||||
problem = "storage.restoreFailed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +282,7 @@
|
||||
try {
|
||||
await downloadMedia(file.media_id);
|
||||
} catch {
|
||||
problem = "Не удалось скачать файл";
|
||||
problem = "ui.downloadFailed";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,22 +331,26 @@
|
||||
{#if summary.files > 0 || chatId !== null}
|
||||
<section class="summary">
|
||||
<div class="hero">
|
||||
<span class="total">{formatBytes(summary.bytes) || "0 B"}</span>
|
||||
<span class="total">
|
||||
{formatBytes(summary.bytes) || t("storage.zero")}
|
||||
</span>
|
||||
<span class="count">
|
||||
{filesLabel(summary.files)}
|
||||
{#if chatId !== null}
|
||||
в этом чате
|
||||
{t("storage.hero.chat", { files: filesLabel(summary.files) })}
|
||||
{:else if summary.trash_bytes > 0}
|
||||
· ещё {formatBytes(summary.trash_bytes)} в корзине
|
||||
{t("storage.hero.trash", {
|
||||
files: filesLabel(summary.files),
|
||||
size: formatBytes(summary.trash_bytes),
|
||||
})}
|
||||
{:else}
|
||||
на диске
|
||||
{t("storage.hero.disk", { files: filesLabel(summary.files) })}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stack" class:bare={groups.length === 0}>
|
||||
{#each groups as item (item.group)}
|
||||
<span
|
||||
title="{GROUP_LABELS[item.group]}: {formatBytes(item.bytes)}"
|
||||
title="{groupLabel(item.group)}: {formatBytes(item.bytes)}"
|
||||
style:background-color="var(--color-storage-{item.group})"
|
||||
style:flex-grow={item.bytes}
|
||||
></span>
|
||||
@@ -353,7 +358,9 @@
|
||||
{#if chatId === null && summary.trash_bytes > 0}
|
||||
<span
|
||||
class="trashed"
|
||||
title="Корзина: {formatBytes(summary.trash_bytes)}"
|
||||
title={t("storage.trash.title", {
|
||||
size: formatBytes(summary.trash_bytes),
|
||||
})}
|
||||
style:flex-grow={summary.trash_bytes}
|
||||
></span>
|
||||
{/if}
|
||||
@@ -371,7 +378,7 @@
|
||||
class="dot"
|
||||
style:background-color="var(--color-storage-{item.group})"
|
||||
></span>
|
||||
{GROUP_LABELS[item.group]}
|
||||
{groupLabel(item.group)}
|
||||
<span class="value">{formatBytes(item.bytes)}</span>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -386,14 +393,14 @@
|
||||
type="button"
|
||||
>
|
||||
<Icon name="delete" size="1.375rem" />
|
||||
<span class="label">Корзина</span>
|
||||
<span class="label">{t("storage.trash")}</span>
|
||||
<span class="value">
|
||||
{#if summary.trash_files > 0}
|
||||
<span class="swatch"></span>
|
||||
{filesLabel(summary.trash_files)}
|
||||
· {formatBytes(summary.trash_bytes)}
|
||||
{:else}
|
||||
пусто
|
||||
{t("storage.trash.nothing")}
|
||||
{/if}
|
||||
</span>
|
||||
<Icon name="next" size="1.125rem" />
|
||||
@@ -407,13 +414,13 @@
|
||||
type="button"
|
||||
class:on={listing === item.value}
|
||||
>
|
||||
{item.label}
|
||||
{t(item.label)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<ScopeFiles
|
||||
label="Все файлы чата"
|
||||
label={t("storage.chatFiles")}
|
||||
onchange={() => {
|
||||
hideUndo();
|
||||
reload();
|
||||
@@ -426,12 +433,14 @@
|
||||
|
||||
{#if listing === "files" && files.length > 0}
|
||||
<div class="section-title">
|
||||
<h3>Самые тяжёлые</h3>
|
||||
<h3>{t("storage.heaviest")}</h3>
|
||||
{#if summary && group === null && summary.heavy_files > 0}
|
||||
<span>
|
||||
{filesLabel(summary.heavy_files)}
|
||||
крупнее {formatBytes(summary.heavy_threshold)}
|
||||
= {formatBytes(summary.heavy_bytes)}
|
||||
{t("storage.heavy", {
|
||||
files: filesLabel(summary.heavy_files),
|
||||
size: formatBytes(summary.heavy_bytes),
|
||||
threshold: formatBytes(summary.heavy_threshold),
|
||||
})}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -448,17 +457,17 @@
|
||||
>
|
||||
{#snippet menu()}
|
||||
<ContextMenuItem icon="data" onselect={() => openChat(chat.chat_id)}>
|
||||
Файлы чата
|
||||
{t("storage.menu.chatFiles")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="reply" onselect={() => visit(chat)}>
|
||||
Перейти в чат
|
||||
{t("storage.menu.openChat")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
icon="settings"
|
||||
onselect={() =>
|
||||
policyUi.edit({ chatId: chat.chat_id }, chatTitle(chat.chat_id))}
|
||||
>
|
||||
Политика захвата
|
||||
{t("storage.menu.policy")}
|
||||
</ContextMenuItem>
|
||||
{/snippet}
|
||||
</StorageChatRow>
|
||||
@@ -484,19 +493,19 @@
|
||||
>
|
||||
{#snippet menu()}
|
||||
<ContextMenuItem icon="open-in-new-tab" onselect={() => open(index)}>
|
||||
Открыть
|
||||
{t("ui.open")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="reply" onselect={() => jump(file)}>
|
||||
Перейти к сообщению
|
||||
{t("ui.jumpToMessage")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="download" onselect={() => save(file)}>
|
||||
Скачать файл
|
||||
{t("ui.downloadFile")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
icon="delete"
|
||||
onselect={() => trash([file.media_id])}
|
||||
>
|
||||
В корзину
|
||||
{t("storage.toTrash")}
|
||||
</ContextMenuItem>
|
||||
{/snippet}
|
||||
</StorageRow>
|
||||
@@ -506,29 +515,31 @@
|
||||
<div class="state"><Spinner /></div>
|
||||
{:else if failed}
|
||||
<div class="state">
|
||||
<p>Не удалось загрузить хранилище.</p>
|
||||
<p>{t("storage.loadFailed")}</p>
|
||||
<Button onclick={() => reload()} smaller variant="text">
|
||||
Повторить
|
||||
{t("ui.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
{:else if shown === 0}
|
||||
<div class="state"><p>{emptiness}</p></div>
|
||||
{:else if !done}
|
||||
<button class="more" onclick={() => loadMore()} type="button">
|
||||
Показать ещё
|
||||
{t("ui.showMore")}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if chosen.length > 0}
|
||||
<SelectionBar
|
||||
failure={problem}
|
||||
note={chosenGone > 0 ? `${chosenGone} не скачать заново` : undefined}
|
||||
{failure}
|
||||
note={chosenGone > 0
|
||||
? t("storage.cantRefetch", { count: chosenGone })
|
||||
: undefined}
|
||||
title="{filesLabel(chosen.length)} · {formatBytes(chosenBytes)}"
|
||||
warn
|
||||
>
|
||||
<Button onclick={() => selected.clear()} smaller variant="text">
|
||||
Снять
|
||||
{t("storage.deselect")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={busy}
|
||||
@@ -538,22 +549,22 @@
|
||||
smaller
|
||||
>
|
||||
<Icon name="delete" size="1.25rem" />
|
||||
<span>В корзину</span>
|
||||
<span>{t("storage.toTrash")}</span>
|
||||
</Button>
|
||||
</SelectionBar>
|
||||
{:else if undo}
|
||||
<SelectionBar
|
||||
failure={problem}
|
||||
note="{formatBytes(undo.outcome.bytes)} освободится через 7 дней"
|
||||
title="{filesLabel(undo.outcome.files)} в корзине"
|
||||
{failure}
|
||||
note={t("storage.freed", { size: formatBytes(undo.outcome.bytes) })}
|
||||
title={t("storage.inTrash", { files: filesLabel(undo.outcome.files) })}
|
||||
>
|
||||
<Button onclick={restoreLast} smaller variant="text">
|
||||
<Icon name="undo" size="1.25rem" />
|
||||
<span>Вернуть</span>
|
||||
<span>{t("storage.restore")}</span>
|
||||
</Button>
|
||||
</SelectionBar>
|
||||
{:else if problem}
|
||||
<SelectionBar failure={problem} title="Не получилось">
|
||||
{:else if failure}
|
||||
<SelectionBar {failure} title={t("storage.failed")}>
|
||||
<Button
|
||||
onclick={() => {
|
||||
problem = null;
|
||||
@@ -561,7 +572,7 @@
|
||||
smaller
|
||||
variant="text"
|
||||
>
|
||||
Понятно
|
||||
{t("ui.gotIt")}
|
||||
</Button>
|
||||
</SelectionBar>
|
||||
{/if}
|
||||
|
||||
Reference in new issue
Block a user