Overhaul trash page

This commit is contained in:
2026-08-03 23:45:37 +02:00
parent 912fe06ff6
commit f3522f370f
2 changed files with 109 additions and 142 deletions

View File

@@ -1,28 +1,31 @@
<script lang="ts"> <script lang="ts">
import { formatDataVolume, formatDate } from "util/Formatting"; import { formatDataVolume, formatDate } from "util/Formatting";
import { fs_check_response, fs_encode_path, fs_get_trash, fs_node_icon, fs_path_url, type TrashEntry } from "lib/FilesystemAPI.svelte"; import { fs_check_response, fs_encode_path, fs_get_trash, fs_node_icon, fs_path_url, type TrashEntry } from "lib/FilesystemAPI.svelte";
import { loading_finish, loading_start } from "lib/Loading"; import { loading_finish, loading_run, loading_start, loading_store } from "lib/Loading";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { breadcrumbs_store } from "wrap/BreadcrumbStore";
import { FSNavigator } from "filesystem/FSNavigator";
let { root_path = "/me" }: { root_path?: string } = $props(); let {
nav,
root_path = "/me",
}: {
nav: FSNavigator
root_path?: string
} = $props();
let entries: TrashEntry[] = $state([]); let entries: TrashEntry[] = $state([]);
let error: string = $state(""); let error: string = $state("");
let loaded = $state(false);
const load = async () => { const load = async () => {
loaded = false; loading_run(async () => {
entries = []; try {
try { entries = await fs_get_trash(root_path)
loading_start(); error = "";
entries = await fs_get_trash(root_path) } catch (err: any) {
error = ""; error = err?.message ?? JSON.stringify(err);
} catch (err: any) { }
error = err?.message ?? JSON.stringify(err); })
} finally {
loaded = true;
loading_finish();
}
}; };
const delete_entry = async (entry: TrashEntry) => { const delete_entry = async (entry: TrashEntry) => {
@@ -76,122 +79,112 @@ const empty_trash = async () => {
} }
}; };
onMount(load); onMount(() => {
breadcrumbs_store.set(breadcrumbs)
load()
});
</script> </script>
<div class="trash_container">
<div class="header"> {#snippet breadcrumbs()}
<h2 class="title"><i class="icon">delete</i> Trash</h2> <i class="icon">delete</i>
<div class="actions"> <div>
<button onclick={load}> Trash bin
<i class="icon">refresh</i> {#if $loading_store === 0 && entries.length === 0}
<span>Refresh</span> (empty)
</button> {:else if entries.length !== 0}
{#if entries.length > 0} ({formatDataVolume(entries.reduce(
<button class="danger" onclick={empty_trash}> (acc, entry) => acc + entry.node.file_size, 0
<i class="icon">delete_forever</i> ), 3)})
<span>Empty trash</span>
</button>
{/if} {/if}
</div> </div>
</div> <div class="separator"></div>
<button onclick={() => window.history.back()} title="Back">
<i class="icon">arrow_back</i>
</button>
<button onclick={() => nav.navigate_up()} disabled={$nav.path.length <= 1} title="Up">
<i class="icon">north</i>
</button>
<button onclick={() => nav.reload()} title="Refresh directory listing">
<i class="icon">refresh</i>
</button>
{#if entries.length !== 0}
<button class="button button_red" onclick={empty_trash}>
<i class="icon">delete_forever</i>
<span>Delete all</span>
</button>
{/if}
{/snippet}
{#if error} {#if error}
<p class="error">{error}</p> <p class="error">{error}</p>
{:else if loaded && entries.length === 0} {:else if entries.length > 0}
<p class="empty">The trash is empty.</p> <table class="trash_table">
{:else if entries.length > 0} <thead>
<table class="trash_table"> <tr>
<thead> <td></td>
<tr> <td>Name</td>
<td></td> <td class="hide_small">Original location</td>
<td>Name</td> <td class="hide_small">Deleted at</td>
<td class="hide_small">Original location</td> <td class="hide_small">Size</td>
<td class="hide_small">Deleted</td> <td></td>
<td class="hide_small">Size</td> </tr>
<td></td> </thead>
<tbody>
{#each entries as entry (entry.node.id)}
<tr class="entry">
<td class="icon_cell">
<img
src={fs_node_icon(entry.node, 32, 32)}
class="node_icon"
alt="icon"
/>
</td>
<td class="name_cell">
<a class="entry_link" href={"/d" + fs_encode_path(entry.node.path)}>
{entry.node.name}
</a>
</td>
<td class="hide_small">
{entry.original_path ? entry.original_path.slice(0, -entry.node.name.length) : "—"}
</td>
<td class="hide_small">
{entry.deletion_date ? formatDate(entry.deletion_date) : "—"}
</td>
<td class="hide_small">
{#if entry.node.type === "file"}
{formatDataVolume(entry.node.file_size, 3)}
{/if}
</td>
<td>
<div class="row_actions">
<button title="Restore to original location" onclick={() => restore_entry(entry)}>
<i class="icon">restore_from_trash</i>
</button>
<button class="button button_red" title="Delete permanently" onclick={() => delete_entry(entry)}>
<i class="icon">delete_forever</i>
</button>
</div>
</td>
</tr> </tr>
</thead> {/each}
<tbody> </tbody>
{#each entries as entry (entry.node.id)} </table>
<tr class="entry"> {/if}
<td class="icon_cell">
<img
src={fs_node_icon(entry.node, 32, 32)}
class="node_icon"
alt="icon"
/>
</td>
<td class="name_cell">
<a class="entry_link" href={"/d" + fs_encode_path(entry.node.path)}>
{entry.node.name}
</a>
</td>
<td class="hide_small original_path">
{entry.original_path || "—"}
</td>
<td class="hide_small date_cell">
{entry.deletion_date ? formatDate(entry.deletion_date) : "—"}
</td>
<td class="hide_small size_cell">
{#if entry.node.type === "file"}
{formatDataVolume(entry.node.file_size, 3)}
{/if}
</td>
<td class="button_cell">
<div class="row_actions">
<button title="Restore to original location" onclick={() => restore_entry(entry)}>
<i class="icon">restore_from_trash</i>
</button>
<button class="danger" title="Delete permanently" onclick={() => delete_entry(entry)}>
<i class="icon">delete_forever</i>
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<style> <style>
.trash_container { .separator {
max-width: 1200px; flex: 1 1 auto;
margin: auto;
padding: 1em;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 1em;
}
.title {
display: flex;
align-items: center;
gap: 0.3em;
margin: 0;
font-size: 1.2em;
border: none;
}
.actions {
display: flex;
gap: 0.5em;
} }
.error { .error {
color: var(--danger_color, #c0392b); color: var(--danger_color, #c0392b);
} }
.empty {
color: var(--body_text_color);
opacity: 0.6;
}
.trash_table { .trash_table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
background: var(--body_background); background: var(--body_background);
border-radius: 8px;
} }
.trash_table td { .trash_table td {
padding: 0; padding: 0;
@@ -205,7 +198,6 @@ onMount(load);
} }
.icon_cell { .icon_cell {
width: 2.5em; width: 2.5em;
padding: 0.3em 0.3em 0.3em 0.5em;
} }
.node_icon { .node_icon {
display: block; display: block;
@@ -214,7 +206,6 @@ onMount(load);
object-fit: contain; object-fit: contain;
} }
.name_cell { .name_cell {
padding: 0.3em 0.5em;
word-break: break-all; word-break: break-all;
} }
.entry_link { .entry_link {
@@ -224,34 +215,10 @@ onMount(load);
.entry_link:hover { .entry_link:hover {
text-decoration: underline; text-decoration: underline;
} }
.original_path {
padding: 0.3em 0.5em;
color: var(--body_text_color);
opacity: 0.7;
font-size: 0.85em;
word-break: break-all;
}
.date_cell {
padding: 0.3em 0.5em;
white-space: nowrap;
font-size: 0.85em;
}
.size_cell {
padding: 0.3em 0.5em;
white-space: nowrap;
font-size: 0.85em;
text-align: right;
}
.button_cell {
padding: 0.3em 0.5em;
}
.row_actions { .row_actions {
display: flex; display: flex;
gap: 0.25em; gap: 0.25em;
} }
.danger {
color: var(--danger_color, #c0392b);
}
@media (max-width: 600px) { @media (max-width: 600px) {
.hide_small { .hide_small {
display: none; display: none;

View File

@@ -90,7 +90,7 @@ export const seek = (delta: number) => {
<Spinner/> <Spinner/>
</div> </div>
{:else if viewer_type === "dir" && $nav.base.name === ".Trash"} {:else if viewer_type === "dir" && $nav.base.name === ".Trash"}
<TrashBin root_path={$nav.path[$nav.path.length - 2].path}/> <TrashBin nav={nav} root_path={$nav.path[$nav.path.length - 2].path}/>
{:else if viewer_type === "dir"} {:else if viewer_type === "dir"}
<FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window} search_bar={search_bar}> <FileManager nav={nav} upload_widget={upload_widget} edit_window={edit_window} search_bar={search_bar}>
<CustomBanner path={$nav.path}/> <CustomBanner path={$nav.path}/>