252 lines
6.1 KiB
Svelte
252 lines
6.1 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from "svelte";
|
||
import {
|
||
getStorageScope,
|
||
restoreFromTrash,
|
||
trashScope,
|
||
} from "#lib/api/endpoints.js";
|
||
import type {
|
||
ScopeTarget,
|
||
StorageScope,
|
||
TrashOutcome,
|
||
} from "#lib/api/types.js";
|
||
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";
|
||
|
||
interface Props {
|
||
label?: string;
|
||
onchange?: () => void;
|
||
target: ScopeTarget;
|
||
}
|
||
|
||
let { target, label, onchange }: Props = $props();
|
||
|
||
let files = $state<StorageScope | null>(null);
|
||
let confirming = $state(false);
|
||
let moving = $state(false);
|
||
let moved = $state<TrashOutcome | null>(null);
|
||
let problem = $state<string | null>(null);
|
||
|
||
function where(scope: StorageScope): string {
|
||
return "folderId" in target
|
||
? `в ${scope.chats} ${plural(scope.chats, "чате", "чатах", "чатах")} папки`
|
||
: "в этом чате";
|
||
}
|
||
|
||
async function load() {
|
||
try {
|
||
files = await getStorageScope(target);
|
||
} catch {
|
||
files = null;
|
||
}
|
||
}
|
||
|
||
async function move() {
|
||
if (moving) {
|
||
return;
|
||
}
|
||
moving = true;
|
||
problem = null;
|
||
try {
|
||
moved = await trashScope(target);
|
||
confirming = false;
|
||
await load();
|
||
onchange?.();
|
||
} catch {
|
||
problem = "Не удалось переместить файлы в корзину";
|
||
} finally {
|
||
moving = false;
|
||
}
|
||
}
|
||
|
||
async function undo() {
|
||
if (moved === null || moving) {
|
||
return;
|
||
}
|
||
moving = true;
|
||
problem = null;
|
||
try {
|
||
await restoreFromTrash(moved.keys);
|
||
moved = null;
|
||
await load();
|
||
onchange?.();
|
||
} catch {
|
||
problem = "Не удалось вернуть файлы";
|
||
} finally {
|
||
moving = false;
|
||
}
|
||
}
|
||
|
||
export function refresh() {
|
||
return load();
|
||
}
|
||
|
||
onMount(load);
|
||
</script>
|
||
|
||
{#if moved && moved.files > 0}
|
||
<div class="files">
|
||
<div class="text">
|
||
<span>{filesLabel(moved.files)} в корзине</span>
|
||
<small>{formatBytes(moved.bytes)} освободится через 7 дней</small>
|
||
{#if problem}
|
||
<small class="failed">{problem}</small>
|
||
{/if}
|
||
</div>
|
||
<Button disabled={moving} onclick={undo} smaller variant="text">
|
||
<Icon name="undo" size="1.25rem" />
|
||
<span>Вернуть</span>
|
||
</Button>
|
||
</div>
|
||
{:else if files && files.files > 0}
|
||
<div class="files">
|
||
{#if confirming}
|
||
<div class="text">
|
||
<span>
|
||
{filesLabel(files.files)}, {formatBytes(files.bytes)},
|
||
{plural(
|
||
files.files,
|
||
"уйдёт в корзину и перестанет скачиваться сам",
|
||
"уйдут в корзину и перестанут скачиваться сами",
|
||
"уйдут в корзину и перестанут скачиваться сами"
|
||
)}.
|
||
</span>
|
||
{#if files.gone > 0}
|
||
<small class="warn">
|
||
{files.gone}
|
||
из них потом не скачать заново
|
||
</small>
|
||
{/if}
|
||
{#if problem}
|
||
<small class="failed">{problem}</small>
|
||
{/if}
|
||
<div class="actions">
|
||
<Button
|
||
onclick={() => {
|
||
confirming = false;
|
||
}}
|
||
smaller
|
||
variant="text"
|
||
>
|
||
Отмена
|
||
</Button>
|
||
<Button
|
||
disabled={moving}
|
||
loading={moving}
|
||
onclick={move}
|
||
pill
|
||
smaller
|
||
>
|
||
<Icon name="delete" size="1.25rem" />
|
||
<span>В корзину</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
{:else}
|
||
<div class="text">
|
||
{#if label}
|
||
<span>{label}</span>
|
||
{#if files.kept > 0}
|
||
<small>
|
||
{filesLabel(files.files)}
|
||
· {formatBytes(files.bytes)}
|
||
</small>
|
||
{/if}
|
||
{:else}
|
||
<span>{formatBytes(files.bytes)} уже скачано</span>
|
||
<small>{filesLabel(files.files)} {where(files)}</small>
|
||
{/if}
|
||
{#if files.kept > 0}
|
||
<small>
|
||
Ещё {filesLabel(files.kept)} есть и в других чатах,
|
||
{plural(
|
||
files.kept,
|
||
"он останется",
|
||
"они останутся",
|
||
"они останутся"
|
||
)}
|
||
</small>
|
||
{/if}
|
||
</div>
|
||
<Button
|
||
onclick={() => {
|
||
confirming = true;
|
||
}}
|
||
pill
|
||
smaller
|
||
variant="gray"
|
||
>
|
||
<Icon name="delete" size="1.25rem" />
|
||
<span>В корзину</span>
|
||
</Button>
|
||
{/if}
|
||
</div>
|
||
{:else if label && files && files.kept > 0}
|
||
<div class="files">
|
||
<div class="text">
|
||
<span>{label}</span>
|
||
<small>
|
||
{filesLabel(files.kept)}
|
||
есть и в других чатах, убрать
|
||
{plural(files.kept, "его", "их", "их")}
|
||
можно из списка ниже
|
||
</small>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<style lang="scss">
|
||
.files {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
|
||
margin: 0.5rem 1rem 0.25rem;
|
||
padding: 0.625rem 0.5rem 0.625rem 0.875rem;
|
||
border-radius: 0.75rem;
|
||
|
||
background-color: var(--color-background-secondary);
|
||
|
||
:global(.Button) {
|
||
gap: 0.375rem;
|
||
padding-inline: 0.875rem;
|
||
}
|
||
}
|
||
|
||
.text {
|
||
display: flex;
|
||
flex: 1 1 10rem;
|
||
flex-direction: column;
|
||
gap: 0.125rem;
|
||
|
||
min-width: 0;
|
||
|
||
font-size: 0.875rem;
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
small {
|
||
font-size: 0.8125rem;
|
||
color: var(--color-text-secondary);
|
||
|
||
&.warn {
|
||
color: var(--color-storage-warning);
|
||
}
|
||
|
||
&.failed {
|
||
color: var(--color-error);
|
||
}
|
||
}
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 0.25rem;
|
||
justify-content: flex-end;
|
||
margin-top: 0.375rem;
|
||
}
|
||
</style>
|