UI overhaul, import fixes, fix user store

This commit is contained in:
2026-07-30 17:07:46 +02:00
parent 773149169b
commit 4b8bd11a4c
75 changed files with 1106 additions and 744 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction, type FileActionHandler } from "./FileManagerLib";

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import { fs_mkdir } from "lib/FilesystemAPI.svelte";
import { fs_mkdir } from "lib/FilesystemAPI";
import Button from "layout/Button.svelte";
import type { FSNavigator } from "filesystem/FSNavigator";
import { loading_finish, loading_start } from "lib/Loading";

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_rename, fs_trash, FSNode } from "lib/FilesystemAPI.svelte"
import { fs_rename, fs_trash, FSNode } from "lib/FilesystemAPI"
import { onMount } from "svelte"
import CreateDirectory from "./CreateDirectory.svelte"
import ListView from "./ListView.svelte"
@@ -305,7 +305,7 @@ onMount(() => {
shadow: "var(--highlight_color) 0 0 10px 2px inset",
}}
>
<div class="width_container">
<div class="width_container bubble">
{#if mode === "viewing"}
<div class="toolbar">
<div class="toolbar_left">
@@ -430,11 +430,9 @@ onMount(() => {
position: sticky;
top: 0;
display: block;
width: 100%;
margin: auto;
margin-top: 0;
padding: 0;
background: var(--body_background);
/* backdrop-filter: blur(4px); */
}
.toolbar {
display: flex;

View File

@@ -4,7 +4,7 @@ import type { FSNavigator } from "filesystem/FSNavigator";
import Button from "layout/Button.svelte";
import Dialog from "layout/Dialog.svelte";
import { bookmark_add, bookmark_del, bookmarks_store, is_bookmark } from "lib/Bookmarks";
import { fs_trash, type FSNode } from "lib/FilesystemAPI.svelte";
import { fs_trash, type FSNode } from "lib/FilesystemAPI";
import { loading_finish, loading_start } from "lib/Loading";
import { tick } from "svelte";

View File

@@ -5,7 +5,7 @@ import GalleryView from "./GalleryView.svelte"
import CompactView from "./CompactView.svelte"
import Modal from "util/Modal.svelte";
import { FSNavigator, path_link } from "filesystem/FSNavigator";
import { fs_encode_path, type FSNode } from "lib/FilesystemAPI.svelte";
import { fs_encode_path, type FSNode } from "lib/FilesystemAPI";
import { FileAction, type FileActionHandler } from "./FileManagerLib";
let nav = $state(new FSNavigator(false))

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { fs_node_icon, fs_node_type, fs_encode_path } from "lib/FilesystemAPI.svelte";
import { fs_node_icon, fs_node_type, fs_encode_path } from "lib/FilesystemAPI";
import type { FSNavigator } from "filesystem/FSNavigator";
import { FileAction, type FileActionHandler } from "./FileManagerLib";

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { formatDataVolume } from "util/Formatting";
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI.svelte"
import { fs_encode_path, fs_node_icon } from "lib/FilesystemAPI"
import type { FSNavigator } from "filesystem/FSNavigator";
import SortButton from "layout/SortButton.svelte";
import { FileAction, type FileActionHandler } from "./FileManagerLib";

View File

@@ -0,0 +1,260 @@
<script lang="ts">
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";
import { loading_finish, loading_start } from "lib/Loading";
import { onMount } from "svelte";
let { root_path = "/me" }: { root_path?: string } = $props();
let entries: TrashEntry[] = $state([]);
let error: string = $state("");
let loaded = $state(false);
const load = async () => {
loaded = false;
entries = [];
try {
loading_start();
entries = await fs_get_trash(root_path)
error = "";
} catch (err: any) {
error = err?.message ?? JSON.stringify(err);
} finally {
loaded = true;
loading_finish();
}
};
const delete_entry = async (entry: TrashEntry) => {
if (!confirm(`Permanently delete "${entry.node.name}"?`)) return;
try {
loading_start();
await fs_check_response(
await fetch(fs_path_url(entry.node.path) + "?trash", { method: "DELETE" })
);
entries = entries.filter(e => e.node.id !== entry.node.id);
} catch (err: any) {
alert(err?.message ?? JSON.stringify(err));
} finally {
loading_finish();
}
};
const restore_entry = async (entry: TrashEntry) => {
if (!entry.original_path) {
alert("Original path is not known, cannot restore this file.");
return;
}
try {
loading_start();
const form = new FormData();
form.append("action", "rename");
form.append("target", "/me" + entry.original_path);
await fs_check_response(
await fetch(fs_path_url(entry.node.path), { method: "POST", body: form })
);
entries = entries.filter(e => e.node.id !== entry.node.id);
} catch (err: any) {
alert(err?.message ?? JSON.stringify(err));
} finally {
loading_finish();
}
};
const empty_trash = async () => {
if (!confirm("Permanently delete all items in the trash?")) return;
try {
loading_start();
await fs_check_response(
await fetch(fs_path_url(root_path) + "?empty_trash", { method: "DELETE" })
);
entries = [];
} catch (err: any) {
alert(err?.message ?? JSON.stringify(err));
} finally {
loading_finish();
}
};
onMount(load);
</script>
<div class="trash_container">
<div class="header">
<h2 class="title"><i class="icon">delete</i> Trash</h2>
<div class="actions">
<button onclick={load}>
<i class="icon">refresh</i>
<span>Refresh</span>
</button>
{#if entries.length > 0}
<button class="danger" onclick={empty_trash}>
<i class="icon">delete_forever</i>
<span>Empty trash</span>
</button>
{/if}
</div>
</div>
{#if error}
<p class="error">{error}</p>
{:else if loaded && entries.length === 0}
<p class="empty">The trash is empty.</p>
{:else if entries.length > 0}
<table class="trash_table">
<thead>
<tr>
<td></td>
<td>Name</td>
<td class="hide_small">Original location</td>
<td class="hide_small">Deleted</td>
<td class="hide_small">Size</td>
<td></td>
</tr>
</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 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>
.trash_container {
max-width: 1200px;
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 {
color: var(--danger_color, #c0392b);
}
.empty {
color: var(--body_text_color);
opacity: 0.6;
}
.trash_table {
width: 100%;
border-collapse: collapse;
background: var(--body_background);
}
.trash_table td {
padding: 0;
}
.entry {
cursor: default;
border-top: 1px solid var(--separator);
}
.entry:hover {
background: var(--input_hover_background);
}
.icon_cell {
width: 2.5em;
padding: 0.3em 0.3em 0.3em 0.5em;
}
.node_icon {
display: block;
width: 2em;
height: 2em;
object-fit: contain;
}
.name_cell {
padding: 0.3em 0.5em;
word-break: break-all;
}
.entry_link {
color: var(--body_text_color);
text-decoration: none;
}
.entry_link:hover {
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 {
display: flex;
gap: 0.25em;
}
.danger {
color: var(--danger_color, #c0392b);
}
@media (max-width: 600px) {
.hide_small {
display: none;
}
}
</style>