Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 33 additions & 39 deletions frontend/src/lib/components/favorites/FavoritesView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
import ViewHeader from '$lib/components/shared/ViewHeader.svelte';
import {applyWallpaperOnly} from '$lib/actions/themeActions';
import {getIsApplying} from '$lib/stores/theme.svelte';
import type {favorites as favoritesNs} from '../../../../wailsjs/go/models';

type Favorite = favoritesNs.Favorite;
import {
getFavorites,
getFavoritesError,
refreshFavorites,
toggleFavorite,
type Favorite,
} from '$lib/stores/favorites.svelte';

let favorites = $state<Favorite[]>([]);
let favorites = $derived(getFavorites());
let loadError = $derived(getFavoritesError());
let isLoading = $state(true);
let filterTag = $state<string>('');
let previewIndex = $state(-1);
Expand All @@ -45,17 +50,13 @@
loadFavorites();
});

// Re-sync with the backend on every mount so favourites added via the
// CLI/IPC while this tab was closed show up.
async function loadFavorites() {
isLoading = true;
try {
const {GetFavorites} = await import(
'../../../../wailsjs/go/main/App'
);
const result = await GetFavorites();
favorites = Array.isArray(result) ? result : [];
await refreshFavorites();
loadThumbnails();
} catch {
favorites = [];
} finally {
isLoading = false;
}
Expand Down Expand Up @@ -103,12 +104,11 @@

async function handleRemove(fav: Favorite) {
try {
const {ToggleFavorite} = await import(
'../../../../wailsjs/go/main/App'
);
await ToggleFavorite(fav.path, fav.type ?? '', {});
favorites = favorites.filter(f => f.path !== fav.path);
} catch {}
await toggleFavorite(fav.path, fav.type ?? '');
} catch (err) {
console.error('ToggleFavorite failed', err);
showToast('Could not update favorites');
}
}

async function handleAddExtra(fav: Favorite) {
Expand Down Expand Up @@ -199,9 +199,22 @@
</ViewHeader>

<div class="flex-1 overflow-y-auto p-3">
{#if loadError}
<div
class="border-border bg-bg-surface text-fg-primary mb-3 flex items-center justify-between gap-3 border p-3 text-xs"
role="alert"
>
<span>{loadError}</span>
<button
class="text-accent shrink-0 px-2 py-1"
onclick={loadFavorites}
disabled={isLoading}>Retry</button
>
</div>
{/if}
{#if isLoading}
<LoadingState message="Loading favorites…" />
{:else if filtered.length === 0}
{:else if filtered.length === 0 && !loadError}
{#if filterTag}
<EmptyState
title="No favorites with this label"
Expand Down Expand Up @@ -260,11 +273,13 @@
path={fav.path}
name={fav.data?.name || fav.data?.id || 'Wallpaper'}
isAdded={getAdditionalImages().includes(fav.path)}
isFavorited={true}
applying={getIsApplying()}
onuse={() => handleSelect(fav)}
onwallpaperonly={() => applyWallpaperOnly(fav.path)}
onpreview={() => handlePreview(i)}
onaddextra={() => handleAddExtra(fav)}
onfavorite={() => handleRemove(fav)}
>
{#snippet thumb()}
{#if getCachedThumbnail(fav.path)}
Expand All @@ -279,27 +294,6 @@
>
{/if}
{/snippet}
{#snippet topRight()}
<button
class="absolute right-1.5 top-1.5 z-10 flex h-7 w-7 items-center justify-center opacity-100"
onclick={() => handleRemove(fav)}
aria-label="Remove from favorites"
>
<svg
class="text-destructive h-4 w-4"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
></path>
</svg>
</button>
{/snippet}
</WallpaperTile>
{/each}
</div>
Expand Down
21 changes: 20 additions & 1 deletion frontend/src/lib/components/local/LocalBrowser.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
loadFullImage,
getCachedFullImage,
} from '$lib/stores/imagecache.svelte';
import {isFavorite, toggleFavorite} from '$lib/stores/favorites.svelte';
import LazyImage from '$lib/components/shared/LazyImage.svelte';
import WallpaperTile from '$lib/components/shared/WallpaperTile.svelte';
import ImagePreview from '$lib/components/shared/ImagePreview.svelte';
Expand Down Expand Up @@ -89,7 +90,9 @@
} catch (err) {
wallpapers = [];
loadError =
err instanceof Error ? err.message : 'Wallpaper folder unavailable';
err instanceof Error
? err.message
: 'Wallpaper folder unavailable';
} finally {
isLoading = false;
}
Expand Down Expand Up @@ -186,6 +189,20 @@
showToast('Added to additional images');
}

async function handleFavorite(wp: Wallpaper) {
try {
const nowFavorited = await toggleFavorite(wp.path, 'local', {
name: wp.name,
});
showToast(
nowFavorited ? 'Added to favorites' : 'Removed from favorites'
);
} catch (err) {
console.error('ToggleFavorite failed', err);
showToast('Could not update favorites');
}
}

async function handlePreview(index: number) {
const wp = filtered[index];
const cached = getCachedFullImage(wp.path);
Expand Down Expand Up @@ -371,11 +388,13 @@
path={wp.path}
name={wp.name}
isAdded={getAdditionalImages().includes(wp.path)}
isFavorited={isFavorite(wp.path)}
applying={getIsApplying()}
onuse={() => selectWallpaper(wp.path)}
onwallpaperonly={() => applyWallpaperOnly(wp.path)}
onpreview={() => handlePreview(i)}
onaddextra={() => handleAddExtra(wp.path)}
onfavorite={() => handleFavorite(wp)}
>
{#snippet thumb()}
<LazyImage path={wp.path} alt={wp.name} />
Expand Down
48 changes: 39 additions & 9 deletions frontend/src/lib/components/shared/WallpaperTile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,33 @@

// Shared card for the Local and Favorites wallpaper grids. The thumbnail
// source differs per view (lazy-loaded vs preloaded cache), so it arrives
// as a `thumb` snippet; `topRight` is an optional slot for view-specific
// actions (e.g. the favourites remove button). Clicking the thumbnail or
// the Use button selects the wallpaper.
// as a `thumb` snippet. The favourite heart renders only when `onfavorite`
// is supplied. Clicking the thumbnail or the Use button selects the
// wallpaper.
let {
path,
name,
isAdded = false,
isFavorited = false,
applying = false,
onuse,
onwallpaperonly,
onpreview,
onaddextra,
onfavorite,
thumb,
topRight,
}: {
path: string;
name: string;
isAdded?: boolean;
isFavorited?: boolean;
applying?: boolean;
onuse: () => void;
onwallpaperonly: () => void;
onpreview: () => void;
onaddextra: () => void;
onfavorite?: () => void;
thumb: Snippet;
topRight?: Snippet;
} = $props();
</script>

Expand All @@ -51,7 +53,7 @@
class="absolute left-1.5 top-1.5 z-10 flex h-7 w-7 items-center justify-center transition-all duration-150
{isAdded
? 'opacity-100'
: 'opacity-0 hover:!opacity-100 group-hover:opacity-60'}"
: 'opacity-0 hover:!opacity-100 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-60'}"
onclick={e => {
e.stopPropagation();
onaddextra();
Expand All @@ -73,12 +75,40 @@
</svg>
</button>

{#if topRight}
{@render topRight()}
{#if onfavorite}
<button
class="absolute right-1.5 top-1.5 z-10 flex h-7 w-7 items-center justify-center transition-all duration-150
{isFavorited
? 'opacity-100'
: 'opacity-0 hover:!opacity-100 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-60'}"
onclick={e => {
e.stopPropagation();
onfavorite();
}}
aria-label={isFavorited
? 'Remove from favorites'
: 'Add to favorites'}
>
<svg
class="h-4 w-4 {isFavorited
? 'text-destructive'
: 'text-white'}"
viewBox="0 0 24 24"
fill={isFavorited ? 'currentColor' : 'none'}
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
></path>
</svg>
</button>
{/if}

<div
class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-2 bg-black/60 opacity-0 transition-opacity duration-150 group-hover:opacity-100"
class="pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-2 bg-black/60 opacity-0 transition-opacity duration-150 group-focus-within:opacity-100 group-hover:opacity-100"
>
<button
class="bg-accent hover:bg-accent-hover text-accent-fg pointer-events-auto min-w-[7rem] px-4 py-1.5 text-[11px] font-medium transition-colors"
Expand Down
45 changes: 14 additions & 31 deletions frontend/src/lib/components/wallhaven/WallpaperCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,29 @@
import {getIsApplying} from '$lib/stores/theme.svelte';
import {openURL} from '$lib/utils/browser';
import {observeIntersection} from '$lib/utils/intersection';
import {isFavorite, toggleFavorite} from '$lib/stores/favorites.svelte';

let {wallpaper, onpreview}: {wallpaper: any; onpreview: () => void} =
$props();
let isDownloading = $state(false);
let isFavorited = $state(false);
let favoriteKey = $derived(wallpaper.path || wallpaper.id);
let isFavorited = $derived(isFavorite(favoriteKey));
let cardEl = $state<HTMLDivElement | null>(null);
let inView = $state(false);
let favoriteChecked = false;

// Gate <img> and the IsFavorite IPC call on viewport so scrolled-past
// cards release decoded-image memory and never-visible cards skip IPC.
// Gate <img> on viewport so scrolled-past cards release decoded-image
// memory.
$effect(() => {
if (!cardEl) return;
return observeIntersection(
cardEl,
entry => {
inView = entry.isIntersecting;
if (inView && !favoriteChecked) {
favoriteChecked = true;
checkFavorite();
}
},
{rootMargin: '600px 0px'}
);
});

async function checkFavorite() {
try {
const {IsFavorite} = await import(
'../../../../wailsjs/go/main/App'
);
isFavorited = await IsFavorite(wallpaper.path || wallpaper.id);
} catch {}
}

async function handleUse() {
isDownloading = true;
try {
Expand All @@ -64,20 +52,15 @@
async function handleFavorite(event: MouseEvent) {
event.stopPropagation();
try {
const {ToggleFavorite} = await import(
'../../../../wailsjs/go/main/App'
);
const result = await ToggleFavorite(
wallpaper.path || wallpaper.id,
'wallhaven',
{
id: wallpaper.id,
thumbUrl: wallpaper.thumbs?.small,
resolution: wallpaper.resolution,
}
);
isFavorited = result;
} catch {}
await toggleFavorite(favoriteKey, 'wallhaven', {
id: wallpaper.id,
thumbUrl: wallpaper.thumbs?.small,
resolution: wallpaper.resolution,
});
} catch (err) {
console.error('ToggleFavorite failed', err);
showToast('Could not update favorites');
}
}

async function handleAddExtra(event: MouseEvent) {
Expand Down
Loading
Loading