svelte refactor

This commit is contained in:
Bradley Nelson
2026-06-17 22:30:56 -06:00
parent 89e14f8f9e
commit e6279da5ce
26 changed files with 497 additions and 386 deletions
+16 -15
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { errorMessage, errorToast } from '$lib/utils/errors';
import {
clearPluginLogs,
createUser,
@@ -112,7 +113,7 @@
try {
dashboard = await getDashboard();
} catch (e) {
dashboardError = e instanceof Error ? e.message : String(e);
dashboardError = errorMessage(e);
}
}
@@ -141,7 +142,7 @@
try {
smtpResult = await sendSmtpTest(smtpTo.trim());
} catch (e) {
smtpResult = { success: false, message: e instanceof Error ? e.message : String(e) };
smtpResult = { success: false, message: errorMessage(e) };
} finally {
smtpSending = false;
}
@@ -157,7 +158,7 @@
try {
oidc = await getOidcSettings();
} catch (e) {
oidcMsg = { text: e instanceof Error ? e.message : String(e), ok: false };
oidcMsg = { text: errorMessage(e), ok: false };
}
}
async function runOidcTest() {
@@ -185,7 +186,7 @@
});
oidcMsg = { text: t('admin.settings_saved_ok', 'Settings saved.'), ok: true };
} catch (e) {
oidcMsg = { text: e instanceof Error ? e.message : String(e), ok: false };
oidcMsg = { text: errorMessage(e), ok: false };
} finally {
oidcSaving = false;
}
@@ -246,7 +247,7 @@
pathStyle: storage.s3_force_path_style ?? false
};
} catch (e) {
storageMsg = { text: e instanceof Error ? e.message : String(e), ok: false };
storageMsg = { text: errorMessage(e), ok: false };
}
}
function applyPreset() {
@@ -275,7 +276,7 @@
storageMsg = { text: t('admin.storage_saved', 'Storage settings saved.'), ok: true };
await loadStorage();
} catch (e) {
storageMsg = { text: e instanceof Error ? e.message : String(e), ok: false };
storageMsg = { text: errorMessage(e), ok: false };
} finally {
storageBusy = false;
}
@@ -299,7 +300,7 @@
};
}
} catch (e) {
storageMsg = { text: e instanceof Error ? e.message : String(e), ok: false };
storageMsg = { text: errorMessage(e), ok: false };
} finally {
storageBusy = false;
}
@@ -347,7 +348,7 @@
try {
verifyResult = await verifyMigration(100);
} catch (e) {
verifyError = e instanceof Error ? e.message : String(e);
verifyError = errorMessage(e);
} finally {
verifying = false;
}
@@ -505,7 +506,7 @@
});
retentionMsg = t('admin.plugins_retention_saved', 'Retention saved.');
} catch (e) {
retentionMsg = e instanceof Error ? e.message : String(e);
retentionMsg = errorMessage(e);
}
}
async function purgeLogs() {
@@ -541,7 +542,7 @@
};
await loadPlugins();
} catch (err) {
installMsg = { ok: false, text: err instanceof Error ? err.message : String(err) };
installMsg = { ok: false, text: errorMessage(err) };
} finally {
installing = false;
input.value = '';
@@ -609,7 +610,7 @@
users = page.users;
total = page.total;
} catch (e) {
usersError = e instanceof Error ? e.message : String(e);
usersError = errorMessage(e);
}
}
@@ -620,12 +621,12 @@
pluginsAvailable = res.available;
plugins = res.plugins;
} catch (e) {
pluginsError = e instanceof Error ? e.message : String(e);
pluginsError = errorMessage(e);
}
}
function reportError(e: unknown) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
async function copyText(text: string) {
@@ -720,7 +721,7 @@
resetModal = null;
ui.notify(t('admin.password_reset', 'Password reset'), 'success');
} catch (err) {
resetError = err instanceof Error ? err.message : String(err);
resetError = errorMessage(err);
} finally {
resetting = false;
}
@@ -776,7 +777,7 @@
};
await loadUsers();
} catch (err) {
createError = err instanceof Error ? err.message : String(err);
createError = errorMessage(err);
} finally {
creating = false;
}
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { errorMessage } from '$lib/utils/errors';
import { page } from '$app/state';
import { onMount } from 'svelte';
import {
@@ -37,7 +38,7 @@
return t('device.lookup_failed', 'Failed to verify code. Please try again.');
}
}
return err instanceof Error ? err.message : String(err);
return errorMessage(err);
}
async function lookup(e?: SubmitEvent) {
@@ -83,7 +84,7 @@
await decideDevice(code, action);
step = action === 'approve' ? 'approved' : 'denied';
} catch (err) {
errorText = err instanceof Error ? err.message : String(err);
errorText = errorMessage(err);
step = 'error';
} finally {
busy = false;
+18 -34
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import { useOwnerCache } from '$lib/composables/useOwnerCache.svelte';
import { errorToast } from '$lib/utils/errors';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import {
@@ -14,7 +17,6 @@
import { renameFile, deleteFile } from '$lib/api/endpoints/files';
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
import type { FileItem } from '$lib/api/types';
import Icon from '$lib/icons/Icon.svelte';
import FileViewer from '$lib/components/FileViewer.svelte';
import MoveDialog from '$lib/components/MoveDialog.svelte';
import ShareDialog from '$lib/components/ShareDialog.svelte';
@@ -25,7 +27,6 @@
} from '$lib/components/ResourceList.svelte';
import { confirmDialog, promptDialog } from '$lib/stores/dialogs.svelte';
import { t } from '$lib/i18n/index.svelte';
import { ui } from '$lib/stores/ui.svelte';
let raw = $state<FavoritesResourceItem[]>([]);
let cursor = $state<string | undefined>(undefined);
@@ -33,7 +34,7 @@
let error = $state<string | null>(null);
let groupBy = $state('');
let reversed = $state(false);
let ownerNames = $state<Record<string, string>>({});
const owners = useOwnerCache(resolveOwnerName);
const byId = $derived(new Map(raw.map((it) => [it.resource.id, it])));
@@ -50,7 +51,7 @@
size: isFile ? (it.resource as FileItem).size : null,
date: it.favorited_at,
ownerId,
ownerName: ownerId ? (ownerNames[ownerId] ?? null) : null,
ownerName: owners.name(ownerId),
isFavorite: true,
category: isFile ? it.resource.category : 'Folder',
modifiedAt: it.resource.modified_at
@@ -65,7 +66,7 @@
label: t('groupby.owner', 'Owner'),
orderBy: 'owner',
bucketOf: (e) => e.ownerId ?? null,
labelOf: (id) => ownerNames[id] ?? id
labelOf: (id) => owners.label(id)
},
{
key: 'type',
@@ -94,19 +95,6 @@
}
];
async function resolveOwners(items: FavoritesResourceItem[]) {
const ids = [
...new Set(items.map((i) => i.resource.owner_id).filter((id): id is string => !!id))
];
await Promise.all(
ids.map(async (id) => {
if (ownerNames[id]) return;
const name = await resolveOwnerName(id);
ownerNames = { ...ownerNames, [id]: name };
})
);
}
async function load(reset = false, orderBy = 'name', rev = reversed) {
loading = true;
error = null;
@@ -119,7 +107,7 @@
});
raw = reset ? page.items : [...raw, ...page.items];
cursor = page.next_cursor;
void resolveOwners(page.items);
void owners.resolve(page.items.map((i) => i.resource.owner_id));
} catch (e) {
console.error('favorites: load error', e);
error = t('errors_loadFailed', 'Failed to load items');
@@ -152,7 +140,7 @@
await removeFavorite(entry.kind, entry.id);
raw = raw.filter((i) => i.resource.id !== entry.id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -175,7 +163,7 @@
else await renameFolder(entry.id, name);
await load(true, orderByForGroup());
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -192,7 +180,7 @@
else await deleteFolder(entry.id);
raw = raw.filter((i) => i.resource.id !== entry.id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -268,7 +256,7 @@
raw = raw.filter((i) => !removed.has(i.resource.id));
selectedIds = new Set();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -302,22 +290,18 @@
onselectionchange={(ids) => (selectedIds = ids)}
>
{#snippet batchToolbar()}
<button class="btn btn-secondary" onclick={batchDownload}>
<Icon name="download" />
{t('common.download', 'Download')}
</button>
<button
class="btn btn-secondary"
<Button icon="download" onclick={batchDownload}>{t('common.download', 'Download')}</Button>
<Button
icon="arrows-alt"
onclick={() => {
moveTarget = null;
moveItems = batchTargets();
moveOpen = true;
}}><Icon name="arrows-alt" /> {t('files.move', 'Move')}</button
}}>{t('files.move', 'Move')}</Button
>
<Button variant="danger" icon="trash" onclick={batchDelete}
>{t('common.delete', 'Delete')}</Button
>
<button class="btn btn-danger" onclick={batchDelete}>
<Icon name="trash" />
{t('common.delete', 'Delete')}
</button>
{/snippet}
</ResourceList>
@@ -1,4 +1,7 @@
<script lang="ts">
import SkeletonList from '$lib/components/SkeletonList.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import { errorMessage, errorToast } from '$lib/utils/errors';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import Icon from '$lib/icons/Icon.svelte';
@@ -121,7 +124,7 @@
if (isFav) await removeFavorite(kind, id);
else await addFavorite(kind, id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
await loadBadges();
}
}
@@ -213,7 +216,7 @@
await createFolder(name, currentId);
await load();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -259,7 +262,7 @@
ui.finishProgress(nid, done, 'success');
await load();
} catch (err) {
ui.finishProgress(nid, err instanceof Error ? err.message : String(err), 'error');
ui.finishProgress(nid, errorMessage(err), 'error');
} finally {
uploading = false;
}
@@ -292,7 +295,7 @@
else await renameFolder(id, name);
await load();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -309,7 +312,7 @@
else await deleteFolder(id);
await load();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -433,7 +436,7 @@
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -460,7 +463,7 @@
clearSelection();
void loadBadges();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -523,7 +526,7 @@
if (folder) await deleteFolder(id);
else await deleteFile(id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
clearSelection();
@@ -568,7 +571,7 @@
clearSelection();
await load();
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
}
}
@@ -644,7 +647,7 @@
else window.open(url, '_blank');
} catch (e) {
win?.close();
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -663,7 +666,7 @@
try {
playlists = await listPlaylists();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
return;
}
const existing = playlists.map((p) => p.name).join(', ');
@@ -686,7 +689,7 @@
await addTracks(playlist.id, [file.id]);
ui.notify(t('music.added_to_playlist', 'Added to playlist'), 'success');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -745,7 +748,7 @@
ui.notify(t('files.uploaded', 'Upload complete'), 'success');
await load();
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
} finally {
uploading = false;
input.value = '';
@@ -1076,36 +1079,14 @@
</div>
{#if error}
<div class="empty-state"><p>{error}</p></div>
<EmptyState title={error} error />
{:else if showSkeleton && isEmpty}
<div class="files-container">
<div
class={viewClass === 'files-grid-view'
? 'files-grid-view files-skeleton'
: 'files-skeleton'}
>
{#each SKELETON as i (i)}
{#if filesStore.viewMode === 'grid'}
<div class="skeleton-card">
<div class="skeleton skeleton-thumb"></div>
<div class="skeleton skeleton-line skeleton-line--medium"></div>
<div class="skeleton skeleton-line skeleton-line--short"></div>
</div>
{:else}
<div class="skeleton-row">
<div class="skeleton skeleton-icon"></div>
<div class="skeleton skeleton-line skeleton-line--medium"></div>
<div class="skeleton skeleton-line skeleton-line--short"></div>
</div>
{/if}
{/each}
</div>
</div>
<SkeletonList count={SKELETON.length} />
{:else if isEmpty}
<div class="empty-state">
<p>{t('files.empty_title', 'This folder is empty')}</p>
<p>{t('files.empty_hint', 'Drop files here or use the Upload button to add files.')}</p>
</div>
<EmptyState
title={t('files.empty_title', 'This folder is empty')}
hint={t('files.empty_hint', 'Drop files here or use the Upload button to add files.')}
/>
{:else}
<div class="files-container">
<div class={viewClass}>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { errorMessage, errorToast } from '$lib/utils/errors';
import { onMount } from 'svelte';
import {
addGroupMember,
@@ -54,7 +55,7 @@
groups = page.items;
total = page.total;
} catch (e) {
error = e instanceof Error ? e.message : String(e);
error = errorMessage(e);
} finally {
loading = false;
}
@@ -74,7 +75,7 @@
}
function report(e: unknown) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
/** Localised "(N members)" label. Project i18n has no plural rules, so we
+21 -25
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { useSelection } from '$lib/composables/useSelection.svelte';
import { errorMessage, errorToast } from '$lib/utils/errors';
import { onMount } from 'svelte';
import { fileInlineUrl } from '$lib/api/endpoints/files';
import {
@@ -67,7 +69,7 @@
playlists = await listPlaylists();
if (!current && playlists.length > 0) await select(playlists[0]);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
error = errorMessage(e);
} finally {
loading = false;
}
@@ -79,7 +81,7 @@
try {
tracks = await listTracks(p.id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -95,7 +97,7 @@
await select(p);
ui.notify(t('music.created', { name: p.name }, 'Created “{{name}}”.'), 'success');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -112,7 +114,7 @@
current.name = name;
playlists = playlists.map((p) => (p.id === current!.id ? { ...p, name } : p));
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -131,7 +133,7 @@
p.id === current!.id ? { ...p, description: desc || null } : p
);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -152,7 +154,7 @@
}
ui.notify(t('music.deleted', { name: p.name }, 'Deleted “{{name}}”.'), 'success');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -163,7 +165,7 @@
tracks = tracks.filter((x) => x.id !== track.id);
ui.notify(t('music.track_removed', 'Track removed.'), 'success');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -181,7 +183,7 @@
'success'
);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -207,7 +209,7 @@
);
ui.notify(t('music.reordered', 'Playlist reordered.'), 'success');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
await select(current);
}
}
@@ -380,7 +382,7 @@
);
ui.notify(t('music.cover_updated', 'Cover updated.'), 'success');
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
}
}
@@ -406,7 +408,7 @@
try {
shares = await listShares(current.id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
} finally {
sharesLoading = false;
}
@@ -420,7 +422,7 @@
await loadShares();
ui.notify(t('music.share_added', 'Shared.'), 'success');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
async function onRemoveShare(userId: string) {
@@ -429,7 +431,7 @@
await removeShare(current.id, userId);
await loadShares();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -438,7 +440,7 @@
let addOpen = $state(false);
let addQuery = $state('');
let addResults = $state<FileItem[]>([]);
let addSelected = $state<Set<string>>(new Set());
const addSelected = useSelection();
let addSearching = $state(false);
let addDebounce: ReturnType<typeof setTimeout> | null = null;
@@ -457,7 +459,7 @@
AUDIO_TYPES.some((e) => f.name.toLowerCase().endsWith(`.${e}`))
);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
addResults = [];
} finally {
addSearching = false;
@@ -471,25 +473,19 @@
addOpen = true;
addQuery = '';
addResults = [];
addSelected = new Set();
addSelected.clear();
void runAddSearch(''); // show all audio files immediately
}
function toggleAdd(id: string) {
const n = new Set(addSelected);
if (n.has(id)) n.delete(id);
else n.add(id);
addSelected = n;
}
async function confirmAdd() {
if (!current || addSelected.size === 0) return;
const count = addSelected.size;
try {
await addTracks(current.id, [...addSelected]);
await addTracks(current.id, addSelected.values());
addOpen = false;
tracks = await listTracks(current.id);
ui.notify(t('music.tracks_added', { n: count }, 'Added {{n}} track(s).'), 'success');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -968,7 +964,7 @@
<input
type="checkbox"
checked={addSelected.has(f.id)}
onchange={() => toggleAdd(f.id)}
onchange={() => addSelected.toggle(f.id)}
/>
<Icon name="file-audio" />
<span class="music-picker-name" title={f.name}>{f.name}</span>
+22 -59
View File
@@ -1,4 +1,8 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
import { useSelection } from '$lib/composables/useSelection.svelte';
import { errorMessage, errorToast } from '$lib/utils/errors';
import { onMount } from 'svelte';
import {
batchTrash,
@@ -25,7 +29,7 @@
type GroupMode = 'day' | 'month' | 'year';
const GROUP_KEY = 'oxicloud-photos-group';
let groupMode = $state<GroupMode>('month');
let selected = $state<Set<string>>(new Set());
const selected = useSelection();
let lightbox = $state(-1); // index into `items`, -1 = closed
/** Client-generated video frame thumbnails (file id → data/URL). */
@@ -98,7 +102,7 @@
cursor = page.nextCursor;
if (!page.nextCursor) exhausted = true;
} catch (e) {
error = e instanceof Error ? e.message : String(e);
error = errorMessage(e);
exhausted = true;
} finally {
loading = false;
@@ -111,21 +115,14 @@
if (typeof localStorage !== 'undefined') localStorage.setItem(GROUP_KEY, m);
}
function toggle(id: string) {
const n = new Set(selected);
if (n.has(id)) n.delete(id);
else n.add(id);
selected = n;
}
/** A plain tile click toggles selection once anything is selected, else opens the lightbox. */
function onTileClick(p: FileItem) {
if (selected.size > 0) toggle(p.id);
if (selected.size > 0) selected.toggle(p.id);
else openLightbox(p);
}
function downloadSelected() {
for (const id of selected) {
for (const id of selected.ids) {
const a = document.createElement('a');
a.href = fileDownloadUrl(id);
a.download = '';
@@ -136,7 +133,7 @@
}
async function trashSelected() {
const ids = [...selected];
const ids = selected.values();
const ok = await confirmDialog({
title: t('photos.delete', 'Delete photos'),
message: t('photos.confirm_delete', { n: ids.length }, 'Move {{n}} photos to trash?'),
@@ -148,9 +145,7 @@
const trashed = await batchTrash(ids);
if (trashed.size > 0) {
items = items.filter((p) => !trashed.has(p.id));
const n = new Set(selected);
for (const id of trashed) n.delete(id);
selected = n;
for (const id of trashed) selected.delete(id);
}
if (trashed.size < ids.length) {
ui.notify(
@@ -165,7 +160,7 @@
ui.notify(t('photos.trashed', { n: trashed.size }, '{{n}} moved to trash.'), 'success');
}
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -345,7 +340,7 @@
await addFavorite('file', lbItem.id);
lbFavorited = !lbFavorited;
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -369,7 +364,7 @@
lightbox = Math.min(at, items.length - 1);
}
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -422,13 +417,9 @@
<div class="batch-bar">
<span>{t('files.selected_count', { n: selected.size }, '{{n}} selected')}</span>
<div class="batch-bar__actions">
<button class="btn btn-secondary" onclick={downloadSelected}
>{t('common.download', 'Download')}</button
>
<button class="btn btn-secondary" onclick={() => (selected = new Set())}
>{t('common.clear', 'Clear')}</button
>
<button class="btn btn-danger" onclick={trashSelected}>{t('common.delete', 'Delete')}</button>
<Button onclick={downloadSelected}>{t('common.download', 'Download')}</Button>
<Button onclick={() => selected.clear()}>{t('common.clear', 'Clear')}</Button>
<Button variant="danger" onclick={trashSelected}>{t('common.delete', 'Delete')}</Button>
</div>
</div>
{/if}
@@ -436,13 +427,11 @@
{#if error}
<p class="status status--error" role="alert">{error}</p>
{:else if items.length === 0 && exhausted}
<div class="empty-state">
<Icon name="images" class="empty-state__icon" />
<p class="empty-state__title">{t('photos.empty', 'No photos yet.')}</p>
<p class="empty-state__hint">
{t('photos.empty_hint', 'Photos and videos you upload will appear here, grouped by date.')}
</p>
</div>
<EmptyState
icon="images"
title={t('photos.empty', 'No photos yet.')}
hint={t('photos.empty_hint', 'Photos and videos you upload will appear here, grouped by date.')}
/>
{:else}
{#each groups as group (group.key)}
<h2 class="photos-group">
@@ -473,7 +462,7 @@
class="photos__check"
class:on={selected.has(photo.id)}
aria-label={t('common.select', 'Select')}
onclick={() => toggle(photo.id)}
onclick={() => selected.toggle(photo.id)}
>
<Icon name="check" />
</button>
@@ -730,32 +719,6 @@
color: var(--color-danger-text);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
text-align: center;
padding: 4rem 1rem;
color: var(--color-text-muted);
}
.empty-state :global(.empty-state__icon) {
font-size: 3rem;
color: var(--color-text-muted);
}
.empty-state__title {
margin: 0;
font-size: 1.1rem;
color: var(--color-text-heading);
}
.empty-state__hint {
margin: 0;
max-width: 28rem;
}
.sentinel {
height: 1px;
}
+10 -26
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { errorToast } from '$lib/utils/errors';
import { relativeTimeAgo } from '$lib/utils/time';
import { onMount } from 'svelte';
import {
changePassword,
@@ -78,26 +80,8 @@
const autoPasswords = $derived(appPasswords.filter((p) => isAutoAppPassword(p)));
/** Relative time (e.g. "3 days ago"); "Never" when absent. */
function timeAgo(value: string | null | undefined): string {
if (!value) return t('profile.never', 'Never');
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
const diffSec = Math.round((date.getTime() - Date.now()) / 1000);
const abs = Math.abs(diffSec);
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
['year', 31536000],
['month', 2592000],
['week', 604800],
['day', 86400],
['hour', 3600],
['minute', 60]
];
for (const [unit, secs] of units) {
if (abs >= secs) return rtf.format(Math.round(diffSec / secs), unit);
}
return rtf.format(diffSec, 'second');
}
const timeAgo = (value: string | null | undefined): string =>
relativeTimeAgo(value, { empty: t('profile.never', 'Never'), invalidAsString: true });
function hydrate() {
const u = session.user;
@@ -139,7 +123,7 @@
if (patch.preferred_locale) await setLocale(patch.preferred_locale as Locale);
ui.notify(t('profile.saved', 'Profile saved'), 'success');
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
} finally {
savingProfile = false;
}
@@ -164,7 +148,7 @@
currentPw = newPw = confirmPw = '';
ui.notify(t('profile.password_updated', 'Password updated'), 'success');
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
} finally {
savingPassword = false;
}
@@ -196,7 +180,7 @@
} catch (err) {
uploadedDataUrl = null;
avatarPreview = null;
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
} finally {
input.value = '';
}
@@ -210,7 +194,7 @@
avatarImgFailed = false;
closeAvatarEdit();
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
} finally {
avatarBusy = false;
}
@@ -251,7 +235,7 @@
newLabel = '';
await loadAppPasswords();
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
} finally {
creatingPw = false;
}
@@ -270,7 +254,7 @@
generated = null;
await loadAppPasswords();
} catch (err) {
ui.notify(err instanceof Error ? err.message : String(err), 'error');
errorToast(err);
}
}
+20 -39
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import { useOwnerCache } from '$lib/composables/useOwnerCache.svelte';
import { errorToast } from '$lib/utils/errors';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { clearRecent, fetchRecentPage, type RecentResourceItem } from '$lib/api/endpoints/recent';
@@ -14,7 +17,6 @@
import { fileDownloadUrl, renameFile, deleteFile } from '$lib/api/endpoints/files';
import { renameFolder, deleteFolder } from '$lib/api/endpoints/folders';
import type { FileItem, ItemType } from '$lib/api/types';
import Icon from '$lib/icons/Icon.svelte';
import FileViewer from '$lib/components/FileViewer.svelte';
import MoveDialog from '$lib/components/MoveDialog.svelte';
import ShareDialog from '$lib/components/ShareDialog.svelte';
@@ -25,7 +27,6 @@
} from '$lib/components/ResourceList.svelte';
import { confirmDialog, promptDialog } from '$lib/stores/dialogs.svelte';
import { t } from '$lib/i18n/index.svelte';
import { ui } from '$lib/stores/ui.svelte';
let raw = $state<RecentResourceItem[]>([]);
let cursor = $state<string | undefined>(undefined);
@@ -33,7 +34,7 @@
let error = $state<string | null>(null);
let groupBy = $state('');
let reversed = $state(false);
let ownerNames = $state<Record<string, string>>({});
const owners = useOwnerCache(resolveOwnerName);
let favoriteIds = $state<Set<string>>(new Set());
const byId = $derived(new Map(raw.map((it) => [it.resource.id, it])));
@@ -51,7 +52,7 @@
size: isFile ? (it.resource as FileItem).size : null,
date: it.accessed_at,
ownerId,
ownerName: ownerId ? (ownerNames[ownerId] ?? null) : null,
ownerName: owners.name(ownerId),
isFavorite: favoriteIds.has(it.resource.id),
category: isFile ? it.resource.category : 'Folder',
modifiedAt: it.resource.modified_at
@@ -66,7 +67,7 @@
label: t('groupby.owner', 'Owner'),
orderBy: 'owner',
bucketOf: (e) => e.ownerId ?? null,
labelOf: (id) => ownerNames[id] ?? id
labelOf: (id) => owners.label(id)
},
{
key: 'type',
@@ -95,19 +96,6 @@
}
];
async function resolveOwners(items: RecentResourceItem[]) {
const ids = [
...new Set(items.map((i) => i.resource.owner_id).filter((id): id is string => !!id))
];
await Promise.all(
ids.map(async (id) => {
if (ownerNames[id]) return;
const name = await resolveOwnerName(id);
ownerNames = { ...ownerNames, [id]: name };
})
);
}
async function loadFavoriteIds() {
try {
const favs = await fetchFavoritesPage({ resourceTypes: ['file', 'folder'] });
@@ -130,7 +118,7 @@
});
raw = reset ? page.items : [...raw, ...page.items];
cursor = page.next_cursor;
void resolveOwners(page.items);
void owners.resolve(page.items.map((i) => i.resource.owner_id));
} catch (e) {
console.error('recent: load error', e);
error = t('errors_loadFailed', 'Failed to load items');
@@ -172,7 +160,7 @@
favoriteIds = isFav
? new Set([...favoriteIds, entry.id])
: new Set([...favoriteIds].filter((id) => id !== entry.id));
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -188,7 +176,7 @@
raw = [];
cursor = undefined;
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -211,7 +199,7 @@
else await renameFolder(entry.id, name);
await load(true, orderByForGroup());
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -228,7 +216,7 @@
else await deleteFolder(entry.id);
raw = raw.filter((i) => i.resource.id !== entry.id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -304,7 +292,7 @@
raw = raw.filter((i) => !removed.has(i.resource.id));
selectedIds = new Set();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -342,29 +330,22 @@
>
{#snippet toolbar()}
{#if entries.length > 0}
<button class="btn btn-secondary" onclick={clearAll}>
<Icon name="broom" />
{t('recent.clear', 'Clear recent')}
</button>
<Button icon="broom" onclick={clearAll}>{t('recent.clear', 'Clear recent')}</Button>
{/if}
{/snippet}
{#snippet batchToolbar()}
<button class="btn btn-secondary" onclick={batchDownload}>
<Icon name="download" />
{t('common.download', 'Download')}
</button>
<button
class="btn btn-secondary"
<Button icon="download" onclick={batchDownload}>{t('common.download', 'Download')}</Button>
<Button
icon="arrows-alt"
onclick={() => {
moveTarget = null;
moveItems = batchTargets();
moveOpen = true;
}}><Icon name="arrows-alt" /> {t('files.move', 'Move')}</button
}}>{t('files.move', 'Move')}</Button
>
<Button variant="danger" icon="trash" onclick={batchDelete}
>{t('common.delete', 'Delete')}</Button
>
<button class="btn btn-danger" onclick={batchDelete}>
<Icon name="trash" />
{t('common.delete', 'Delete')}
</button>
{/snippet}
</ResourceList>
+6 -21
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import EmptyState from '$lib/components/EmptyState.svelte';
import { errorMessage } from '$lib/utils/errors';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { searchFiles } from '$lib/api/endpoints/search';
@@ -142,7 +144,7 @@
modifiedAfter: dateBound(dateFilter)
});
} catch (e) {
error = e instanceof Error ? e.message : String(e);
error = errorMessage(e);
} finally {
loading = false;
}
@@ -239,16 +241,11 @@
</h2>
</div>
{:else if error}
<div class="empty-state"><p>{error}</p></div>
<EmptyState title={error} error />
{:else if !query}
<div class="empty-state">
<p>{t('search.prompt', 'Type a query in the search bar above.')}</p>
</div>
<EmptyState title={t('search.prompt', 'Type a query in the search bar above.')} />
{:else if isEmpty}
<div class="empty-state search-empty">
<Icon name="search" />
<p>{t('search.no_results', 'No results found for this search')}</p>
</div>
<EmptyState icon="search" title={t('search.no_results', 'No results found for this search')} />
{:else if results}
<div class="files-container">
<div class="files-list-view" style="--files-list-columns: minmax(200px, 2fr) 1fr 110px 140px">
@@ -383,16 +380,4 @@
font-weight: var(--weight-medium);
color: var(--color-text);
}
.search-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
}
.search-empty :global(.oxi-icon) {
font-size: var(--text-3xl);
color: var(--color-text-faint);
}
</style>
@@ -1,4 +1,5 @@
<script lang="ts">
import { errorMessage } from '$lib/utils/errors';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { fetchSharedWithMe, type IncomingGrantItem } from '$lib/api/endpoints/grants';
@@ -38,7 +39,7 @@
raw = reset ? page.items : [...raw, ...page.items];
cursor = page.next_cursor;
} catch (e) {
error = e instanceof Error ? e.message : String(e);
error = errorMessage(e);
} finally {
loading = false;
}
+17 -35
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import EmptyState from '$lib/components/EmptyState.svelte';
import { errorMessage, errorToast } from '$lib/utils/errors';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import {
@@ -152,7 +154,7 @@
raw = reset ? page.items : [...raw, ...page.items];
cursor = page.next_cursor;
} catch (e) {
error = e instanceof Error ? e.message : String(e);
error = errorMessage(e);
} finally {
loading = false;
}
@@ -232,7 +234,7 @@
g.role = role;
raw = [...raw];
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -248,7 +250,7 @@
g.expires_at = iso;
raw = [...raw];
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -257,7 +259,7 @@
try {
summarize((await notifyGrantRecipient(g.grant_id)).outcomes);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -267,7 +269,7 @@
await revokeGrant(g.grant_id);
dropGrant(g.grant_id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -278,7 +280,7 @@
if (await copyShareLink(share.url)) ui.notify(t('share.copied', 'Link copied'), 'success');
else ui.notify(t('share.copy_failed', 'Could not copy link'), 'error');
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -288,7 +290,7 @@
g.expires_at = expiryToIso(date || null);
raw = [...raw];
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -311,7 +313,7 @@
'success'
);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -321,7 +323,7 @@
await deleteShare(g.subject_id);
dropGrant(g.grant_id);
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -363,18 +365,13 @@
</div>
{#if error}
<div class="empty-state">
<Icon name="exclamation-circle" class="empty-state-icon empty-state-icon--error" />
<p>{error}</p>
</div>
<EmptyState icon="exclamation-circle" title={error} error />
{:else if isEmpty}
<div class="empty-state">
<Icon name="share-alt" class="empty-state-icon" />
<p>{t('myshares.emptyStateTitle', "You haven't shared anything yet")}</p>
<p class="empty-state__hint">
{t('myshares.emptyStateDesc', 'Items you share with others will appear here')}
</p>
</div>
<EmptyState
icon="share-alt"
title={t('myshares.emptyStateTitle', "You haven't shared anything yet")}
hint={t('myshares.emptyStateDesc', 'Items you share with others will appear here')}
/>
{:else}
<div class="ms-lanes">
{#each lanes as lane (lane.key)}
@@ -832,19 +829,4 @@
.ms-more {
margin: var(--space-3) auto 0;
}
.empty-state__hint {
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.empty-state :global(.empty-state-icon) {
font-size: var(--text-5xl);
color: var(--color-text-faint);
margin-bottom: var(--space-2);
}
.empty-state :global(.empty-state-icon--error) {
color: var(--color-danger-text);
}
</style>
+4 -3
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { errorToast } from '$lib/utils/errors';
import { onMount } from 'svelte';
import {
deleteTrashItem,
@@ -110,7 +111,7 @@
ui.notify(t('trash.restored', 'Restored'), 'success');
await reloadFromTop();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -126,7 +127,7 @@
await deleteTrashItem(entry.id);
await reloadFromTop();
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}
@@ -143,7 +144,7 @@
raw = [];
cursor = undefined;
} catch (e) {
ui.notify(e instanceof Error ? e.message : String(e), 'error');
errorToast(e);
}
}