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
@@ -13,8 +13,9 @@
|
||||
import Button from "#lib/components/ui/Button.svelte";
|
||||
import Icon from "#lib/components/ui/Icon.svelte";
|
||||
import { formatBytes } from "#lib/format/media.js";
|
||||
import { plural } from "#lib/format/shares.js";
|
||||
import { filesLabel } from "#lib/format/storage.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
@@ -28,12 +29,12 @@
|
||||
let confirming = $state(false);
|
||||
let moving = $state(false);
|
||||
let moved = $state<TrashOutcome | null>(null);
|
||||
let problem = $state<string | null>(null);
|
||||
let problem = $state<MessageKey | null>(null);
|
||||
|
||||
function where(scope: StorageScope): string {
|
||||
return "folderId" in target
|
||||
? `в ${scope.chats} ${plural(scope.chats, "чате", "чатах", "чатах")} папки`
|
||||
: "в этом чате";
|
||||
? t("storage.inFolder", { count: scope.chats })
|
||||
: t("storage.inChat");
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@@ -56,7 +57,7 @@
|
||||
await load();
|
||||
onchange?.();
|
||||
} catch {
|
||||
problem = "Не удалось переместить файлы в корзину";
|
||||
problem = "storage.scope.trashFailed";
|
||||
} finally {
|
||||
moving = false;
|
||||
}
|
||||
@@ -74,7 +75,7 @@
|
||||
await load();
|
||||
onchange?.();
|
||||
} catch {
|
||||
problem = "Не удалось вернуть файлы";
|
||||
problem = "storage.restoreFailed";
|
||||
} finally {
|
||||
moving = false;
|
||||
}
|
||||
@@ -90,15 +91,15 @@
|
||||
{#if moved && moved.files > 0}
|
||||
<div class="files">
|
||||
<div class="text">
|
||||
<span>{filesLabel(moved.files)} в корзине</span>
|
||||
<small>{formatBytes(moved.bytes)} освободится через 7 дней</small>
|
||||
<span>{t("storage.inTrash", { files: filesLabel(moved.files) })}</span>
|
||||
<small>{t("storage.freed", { size: formatBytes(moved.bytes) })}</small>
|
||||
{#if problem}
|
||||
<small class="failed">{problem}</small>
|
||||
<small class="failed">{t(problem)}</small>
|
||||
{/if}
|
||||
</div>
|
||||
<Button disabled={moving} onclick={undo} smaller variant="text">
|
||||
<Icon name="undo" size="1.25rem" />
|
||||
<span>Вернуть</span>
|
||||
<span>{t("storage.restore")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
{:else if files && files.files > 0}
|
||||
@@ -106,22 +107,19 @@
|
||||
{#if confirming}
|
||||
<div class="text">
|
||||
<span>
|
||||
{filesLabel(files.files)}, {formatBytes(files.bytes)},
|
||||
{plural(
|
||||
files.files,
|
||||
"уйдёт в корзину и перестанет скачиваться сам",
|
||||
"уйдут в корзину и перестанут скачиваться сами",
|
||||
"уйдут в корзину и перестанут скачиваться сами"
|
||||
)}.
|
||||
{t("storage.scope.confirm", {
|
||||
count: files.files,
|
||||
files: filesLabel(files.files),
|
||||
size: formatBytes(files.bytes),
|
||||
})}
|
||||
</span>
|
||||
{#if files.gone > 0}
|
||||
<small class="warn">
|
||||
{files.gone}
|
||||
из них потом не скачать заново
|
||||
{t("storage.scope.gone", { count: files.gone })}
|
||||
</small>
|
||||
{/if}
|
||||
{#if problem}
|
||||
<small class="failed">{problem}</small>
|
||||
<small class="failed">{t(problem)}</small>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<Button
|
||||
@@ -131,7 +129,7 @@
|
||||
smaller
|
||||
variant="text"
|
||||
>
|
||||
Отмена
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={moving}
|
||||
@@ -141,7 +139,7 @@
|
||||
smaller
|
||||
>
|
||||
<Icon name="delete" size="1.25rem" />
|
||||
<span>В корзину</span>
|
||||
<span>{t("storage.toTrash")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,18 +154,17 @@
|
||||
</small>
|
||||
{/if}
|
||||
{:else}
|
||||
<span>{formatBytes(files.bytes)} уже скачано</span>
|
||||
<span>
|
||||
{t("storage.scope.downloaded", { size: formatBytes(files.bytes) })}
|
||||
</span>
|
||||
<small>{filesLabel(files.files)} {where(files)}</small>
|
||||
{/if}
|
||||
{#if files.kept > 0}
|
||||
<small>
|
||||
Ещё {filesLabel(files.kept)} есть и в других чатах,
|
||||
{plural(
|
||||
files.kept,
|
||||
"он останется",
|
||||
"они останутся",
|
||||
"они останутся"
|
||||
)}
|
||||
{t("storage.scope.kept", {
|
||||
count: files.kept,
|
||||
files: filesLabel(files.kept),
|
||||
})}
|
||||
</small>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -180,7 +177,7 @@
|
||||
variant="gray"
|
||||
>
|
||||
<Icon name="delete" size="1.25rem" />
|
||||
<span>В корзину</span>
|
||||
<span>{t("storage.toTrash")}</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -189,10 +186,10 @@
|
||||
<div class="text">
|
||||
<span>{label}</span>
|
||||
<small>
|
||||
{filesLabel(files.kept)}
|
||||
есть и в других чатах, убрать
|
||||
{plural(files.kept, "его", "их", "их")}
|
||||
можно из списка ниже
|
||||
{t("storage.scope.keptOnly", {
|
||||
count: files.kept,
|
||||
files: filesLabel(files.kept),
|
||||
})}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Avatar from "#lib/components/ui/Avatar.svelte";
|
||||
import ContextMenu from "#lib/components/ui/ContextMenu.svelte";
|
||||
import { formatBytes } from "#lib/format/media.js";
|
||||
import { filesLabel, GROUP_LABELS } from "#lib/format/storage.js";
|
||||
import { filesLabel, groupLabel } from "#lib/format/storage.js";
|
||||
|
||||
interface Props {
|
||||
chat: StorageChat;
|
||||
@@ -24,7 +24,7 @@
|
||||
const parts = $derived(chat.groups.filter((item) => item.bytes > 0));
|
||||
const makeup = $derived(
|
||||
parts
|
||||
.map((item) => `${GROUP_LABELS[item.group]}: ${formatBytes(item.bytes)}`)
|
||||
.map((item) => `${groupLabel(item.group)}: ${formatBytes(item.bytes)}`)
|
||||
.join(", ")
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Icon from "#lib/components/ui/Icon.svelte";
|
||||
import { formatBytes } from "#lib/format/media.js";
|
||||
import { groupOf } from "#lib/format/storage.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
|
||||
interface Props {
|
||||
fileName: string;
|
||||
@@ -54,7 +55,7 @@
|
||||
>
|
||||
<span class="check">
|
||||
<input
|
||||
aria-label="Выбрать {fileName}"
|
||||
aria-label={t("storage.select", { name: fileName })}
|
||||
checked={selected}
|
||||
onchange={ontoggle}
|
||||
type="checkbox"
|
||||
@@ -82,7 +83,7 @@
|
||||
<span class="note">{note}</span>
|
||||
{/if}
|
||||
{#if !refetchable}
|
||||
<span class="gone">Не скачать заново</span>
|
||||
<span class="gone">{t("storage.gone")}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="size">{formatBytes(size)}</span>
|
||||
|
||||
@@ -19,12 +19,13 @@
|
||||
import Icon from "#lib/components/ui/Icon.svelte";
|
||||
import Spinner from "#lib/components/ui/Spinner.svelte";
|
||||
import { formatBytes } from "#lib/format/media.js";
|
||||
import { plural } from "#lib/format/shares.js";
|
||||
import {
|
||||
filesLabel,
|
||||
messagesLabel,
|
||||
purgeLabel,
|
||||
} 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 { ui } from "#lib/stores/ui.svelte.js";
|
||||
import { isMobile } from "#lib/viewport.js";
|
||||
@@ -40,7 +41,7 @@
|
||||
let failed = $state(false);
|
||||
let busy = $state(false);
|
||||
let doomed = $state<TrashItem[] | 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<string>();
|
||||
@@ -58,6 +59,7 @@
|
||||
const doomedGone = $derived(
|
||||
doomed?.filter((item) => !item.refetchable).length ?? 0
|
||||
);
|
||||
const failure = $derived(problem && t(problem));
|
||||
const viewerItems = $derived<ViewerItem[]>(
|
||||
items.map((item) => ({
|
||||
caption: `${formatBytes(item.file_size)} · ${chatTitle(item.chat_id)}`,
|
||||
@@ -77,7 +79,7 @@
|
||||
}
|
||||
|
||||
function chatTitle(chatId: number): string {
|
||||
return chats.byId(chatId)?.title ?? `Чат ${chatId}`;
|
||||
return chats.byId(chatId)?.title ?? t("ui.chat", { id: chatId });
|
||||
}
|
||||
|
||||
async function loadTotal() {
|
||||
@@ -132,7 +134,7 @@
|
||||
await restoreFromTrash(keys);
|
||||
drop(keys);
|
||||
} catch {
|
||||
problem = "Не удалось вернуть файлы";
|
||||
problem = "storage.restoreFailed";
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
@@ -148,7 +150,7 @@
|
||||
await restoreFromTrash(null);
|
||||
await load();
|
||||
} catch {
|
||||
problem = "Не удалось вернуть файлы";
|
||||
problem = "storage.restoreFailed";
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
@@ -167,7 +169,7 @@
|
||||
everything = false;
|
||||
await load();
|
||||
} catch {
|
||||
problem = "Не удалось удалить файлы";
|
||||
problem = "storage.trash.deleteFailed";
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
@@ -190,15 +192,12 @@
|
||||
</script>
|
||||
|
||||
<div class="trash">
|
||||
<p class="note">
|
||||
Файлы лежат здесь 7 дней, потом удаляются с диска. До этого их можно
|
||||
вернуть.
|
||||
</p>
|
||||
<p class="note">{t("storage.trash.note")}</p>
|
||||
|
||||
{#if total.files > 0}
|
||||
<button class="act" disabled={busy} onclick={restoreAll} type="button">
|
||||
<Icon name="undo" size="1.375rem" />
|
||||
<span class="label">Вернуть всё</span>
|
||||
<span class="label">{t("storage.trash.restoreAll")}</span>
|
||||
<span class="value">{filesLabel(total.files)}</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -209,7 +208,7 @@
|
||||
type="button"
|
||||
>
|
||||
<Icon name="delete" size="1.375rem" />
|
||||
<span class="label">Очистить корзину</span>
|
||||
<span class="label">{t("storage.trash.clear")}</span>
|
||||
<span class="value">{formatBytes(total.bytes)}</span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -233,13 +232,13 @@
|
||||
>
|
||||
{#snippet menu()}
|
||||
<ContextMenuItem icon="open-in-new-tab" onselect={() => open(index)}>
|
||||
Открыть
|
||||
{t("ui.open")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="undo" onselect={() => restore([item])}>
|
||||
Вернуть
|
||||
{t("storage.restore")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="reply" onselect={() => jump(item)}>
|
||||
Перейти к сообщению
|
||||
{t("ui.jumpToMessage")}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
icon="delete"
|
||||
@@ -247,7 +246,7 @@
|
||||
doomed = [item];
|
||||
}}
|
||||
>
|
||||
Удалить насовсем
|
||||
{t("storage.trash.deleteForever")}
|
||||
</ContextMenuItem>
|
||||
{/snippet}
|
||||
</StorageRow>
|
||||
@@ -257,22 +256,24 @@
|
||||
<div class="state"><Spinner /></div>
|
||||
{:else if failed}
|
||||
<div class="state">
|
||||
<p>Не удалось загрузить корзину.</p>
|
||||
<Button onclick={load} smaller variant="text">Повторить</Button>
|
||||
<p>{t("storage.trash.loadFailed")}</p>
|
||||
<Button onclick={load} smaller variant="text">{t("ui.retry")}</Button>
|
||||
</div>
|
||||
{:else if items.length === 0}
|
||||
<div class="state"><p>Корзина пуста.</p></div>
|
||||
<div class="state"><p>{t("storage.trash.empty")}</p></div>
|
||||
{:else if !done}
|
||||
<button class="more" onclick={loadMore} type="button">
|
||||
Показать ещё
|
||||
{t("ui.showMore")}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if chosen.length > 0}
|
||||
<SelectionBar
|
||||
failure={dialogOpen ? null : problem}
|
||||
note={chosenGone > 0 ? `${chosenGone} не скачать заново` : undefined}
|
||||
failure={dialogOpen ? null : failure}
|
||||
note={chosenGone > 0
|
||||
? t("storage.cantRefetch", { count: chosenGone })
|
||||
: undefined}
|
||||
title="{filesLabel(chosen.length)} · {formatBytes(bytesOf(chosen))}"
|
||||
warn
|
||||
>
|
||||
@@ -284,7 +285,7 @@
|
||||
smaller
|
||||
variant="danger"
|
||||
>
|
||||
Удалить
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={busy}
|
||||
@@ -294,11 +295,11 @@
|
||||
smaller
|
||||
>
|
||||
<Icon name="undo" size="1.25rem" />
|
||||
<span>Вернуть</span>
|
||||
<span>{t("storage.restore")}</span>
|
||||
</Button>
|
||||
</SelectionBar>
|
||||
{:else if problem && !dialogOpen}
|
||||
<SelectionBar failure={problem} title="Не получилось">
|
||||
{:else if failure && !dialogOpen}
|
||||
<SelectionBar {failure} title={t("storage.failed")}>
|
||||
<Button
|
||||
onclick={() => {
|
||||
problem = null;
|
||||
@@ -306,7 +307,7 @@
|
||||
smaller
|
||||
variant="text"
|
||||
>
|
||||
Понятно
|
||||
{t("ui.gotIt")}
|
||||
</Button>
|
||||
</SelectionBar>
|
||||
{/if}
|
||||
@@ -327,59 +328,53 @@
|
||||
<Dialog.Content class="dialog-content">
|
||||
<header class="dialog-head">
|
||||
<Dialog.Title class="dialog-title">
|
||||
{everything ? "Очистить корзину?" : "Удалить насовсем?"}
|
||||
{everything
|
||||
? t("storage.trash.clearTitle")
|
||||
: t("storage.trash.deleteTitle")}
|
||||
</Dialog.Title>
|
||||
<Dialog.Close aria-label="Закрыть" class="dialog-close">
|
||||
<Dialog.Close aria-label={t("common.close")} class="dialog-close">
|
||||
<Icon name="close" size="1.25rem" />
|
||||
</Dialog.Close>
|
||||
</header>
|
||||
<div class="dialog-body">
|
||||
{#if everything}
|
||||
<p class="confirm">
|
||||
{filesLabel(total.files)}, {formatBytes(total.bytes)},
|
||||
{plural(
|
||||
total.files,
|
||||
"будет удалён",
|
||||
"будут удалены",
|
||||
"будут удалены"
|
||||
)}
|
||||
с диска.
|
||||
</p>
|
||||
<p class="confirm rest">
|
||||
То, что ещё есть в Telegram, потом можно скачать заново. Файлы из
|
||||
удалённых там сообщений пропадут насовсем.
|
||||
{t("storage.trash.willDelete", {
|
||||
count: total.files,
|
||||
files: filesLabel(total.files),
|
||||
size: formatBytes(total.bytes),
|
||||
})}
|
||||
</p>
|
||||
<p class="confirm rest">{t("storage.trash.clearRest")}</p>
|
||||
{:else if doomed}
|
||||
<p class="confirm">
|
||||
{filesLabel(doomed.length)}, {formatBytes(bytesOf(doomed))},
|
||||
{plural(
|
||||
doomed.length,
|
||||
"будет удалён",
|
||||
"будут удалены",
|
||||
"будут удалены"
|
||||
)}
|
||||
с диска.
|
||||
{t("storage.trash.willDelete", {
|
||||
count: doomed.length,
|
||||
files: filesLabel(doomed.length),
|
||||
size: formatBytes(bytesOf(doomed)),
|
||||
})}
|
||||
</p>
|
||||
{#if doomedGone > 0}
|
||||
<p class="confirm warn">
|
||||
<Icon name="warning" size="1.25rem" />
|
||||
<span>
|
||||
{filesLabel(doomedGone)}
|
||||
скачать заново нельзя: в Telegram
|
||||
{doomedGone === 1 ? "его" : "их"}
|
||||
уже нет.
|
||||
{t("storage.trash.goneWarn", {
|
||||
count: doomedGone,
|
||||
files: filesLabel(doomedGone),
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
{/if}
|
||||
{#if doomedGone < doomed.length}
|
||||
<p class="confirm rest">
|
||||
{doomedGone === 0 ? "Потом их" : "Остальные"}
|
||||
можно скачать из Telegram заново.
|
||||
{doomedGone === 0
|
||||
? t("storage.trash.laterAll", { count: doomed.length })
|
||||
: t("storage.trash.laterRest")}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if problem}
|
||||
<p class="confirm failed">{problem}.</p>
|
||||
{#if failure}
|
||||
<p class="confirm failed">{failure}.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
@@ -387,7 +382,9 @@
|
||||
{#snippet child({
|
||||
props,
|
||||
})}
|
||||
<Button {...props} pill variant="text">Отмена</Button>
|
||||
<Button {...props} pill variant="text">
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Dialog.Close>
|
||||
<Button
|
||||
@@ -397,7 +394,7 @@
|
||||
pill
|
||||
variant="danger"
|
||||
>
|
||||
Удалить
|
||||
{t("common.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
|
||||
Reference in new issue
Block a user