1
0
Fork 1
mirror of https://github.com/0x2E/fusion.git synced 2025-06-08 05:27:15 +09:00
* use invalidateAll() to reduce complexity

* remove useless loading placeholder

* refactor: update sidebar using global states
This commit is contained in:
Yuan 2025-04-26 15:34:51 +08:00 committed by GitHub
parent bc8109fe39
commit b83b868fc7
Signed by: github
GPG key ID: B5690EEEBB952194
16 changed files with 110 additions and 128 deletions

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import { listItems, parseURLtoFilter, updateUnread } from '$lib/api/item';
import { invalidateAll } from '$app/navigation';
import { listItems, updateUnread } from '$lib/api/item';
import type { Item } from '$lib/api/model';
import { t } from '$lib/i18n';
import { CheckCheck } from 'lucide-svelte';
@ -29,7 +28,7 @@
const ids = props.items.map((v) => v.id);
await updateUnread(ids, false);
toast.success(t('state.success'));
invalidate('page:' + page.url.pathname);
invalidateAll();
} catch (e) {
toast.error((e as Error).message);
}
@ -57,7 +56,7 @@
await updateUnread(ids, false);
}
toast.success(t('state.success'));
invalidate('page:' + page.url.pathname);
invalidateAll();
} catch (e) {
toast.error((e as Error).message);
}

View file

@ -1,12 +1,15 @@
<script module>
import type { Item } from '$lib/api/model';
import { updateUnread } from '$lib/api/item';
import type { Item } from '$lib/api/model';
import { toast } from 'svelte-sonner';
export async function toggleUnread(item: Item) {
try {
await updateUnread([item.id], !item.unread);
item.unread = !item.unread;
// we don't refresh the page using invalideAll() because we want to keep the
// modified item in the list rather than be filtered out
updateUnreadCount(item.feed.id, item.unread ? 1 : -1);
} catch (e) {
toast.error((e as Error).message);
}
@ -15,6 +18,7 @@
<script lang="ts">
import { t } from '$lib/i18n';
import { updateUnreadCount } from '$lib/state.svelte';
import { CheckIcon, UndoIcon } from 'lucide-svelte';
import { activateShortcut, deactivateShortcut, shortcuts } from './ShortcutHelpModal.svelte';

View file

@ -3,8 +3,9 @@
import { page } from '$app/state';
import { getFavicon } from '$lib/api/favicon';
import { logout } from '$lib/api/login';
import type { Feed, Group } from '$lib/api/model';
import type { Feed } from '$lib/api/model';
import { t } from '$lib/i18n';
import { globalState } from '$lib/state.svelte';
import {
BookmarkCheck,
ChevronDown,
@ -28,26 +29,17 @@
} from './ShortcutHelpModal.svelte';
import ThemeController from './ThemeController.svelte';
interface Props {
feeds: Promise<Feed[]>;
groups: Promise<Group[]>;
}
let { feeds, groups }: Props = $props();
// State to track open groups
let openGroups = $state<Record<number, boolean>>({});
let feedList = $derived.by(async () => {
const [feedsData, groupsData] = await Promise.all([feeds, groups]);
let groupList = $derived.by(() => {
const groupFeeds: { id: number; name: string; feeds: (Feed & { indexInList: number })[] }[] =
[];
let curIndexInList = 0;
groupsData.forEach((group) => {
globalState.groups.forEach((group) => {
groupFeeds.push({
id: group.id,
name: group.name,
feeds: feedsData
feeds: globalState.feeds
.filter((feed) => feed.group.id === group.id)
.sort((a, b) => a.name.localeCompare(b.name))
.map((feed) => ({
@ -55,7 +47,6 @@
indexInList: curIndexInList++
}))
});
openGroups[group.id] = false;
});
return groupFeeds;
});
@ -115,15 +106,8 @@
}
let selectedFeedIndex = $state(-1);
let selectedFeedGroupId = $state(-1);
$effect(() => {
feeds.then(() => {
selectedFeedIndex = -1;
selectedFeedGroupId = -1;
});
});
async function moveFeed(direction: 'prev' | 'next') {
const feedList = await feeds;
const feedList = globalState.feeds;
if (feedList.length === 0) return;
@ -200,60 +184,59 @@
<ul class="menu w-full">
<li class="menu-title text-xs">{t('common.feeds')}</li>
{#await feedList}
<div class="skeleton bg-base-300 h-10"></div>
{:then groupData}
{#each groupData as group (group.id)}
{@const isOpen = openGroups[group.id]}
<li>
<div class="relative flex items-center pl-10">
<button
class="btn btn-ghost btn-sm btn-square absolute top-0 left-1"
onclick={(event) => {
event.preventDefault();
openGroups[group.id] = !isOpen;
}}
>
{#if isOpen}
<ChevronDown class="size-4" />
{:else}
<ChevronRight class="size-4" />
{/if}
</button>
<a href="/groups/{group.id}" class="line-clamp-1 grow text-left">
{group.name}
</a>
</div>
<ul class:hidden={!isOpen}>
{#each group.feeds as feed}
{@const textColor = feed.suspended
? 'text-neutral-content/60'
: feed.failure
? 'text-error'
: ''}
<li>
<a
id="sidebar-feed-{feed.indexInList}"
data-group-id={group.id}
href="/feeds/{feed.id}"
class={`${isHighlight('/feeds/' + feed.id) ? 'menu-active' : ''} focus:ring-2`}
>
<div class="avatar">
<div class="size-4 rounded-full">
<img src={getFavicon(feed.link)} alt={feed.name} loading="lazy" />
</div>
{#each groupList as group}
{@const isOpen = openGroups[group.id]}
<li class="p-0">
<div class="gap-0 p-0">
<button
class="btn btn-ghost btn-sm btn-square"
onclick={(event) => {
event.preventDefault();
openGroups[group.id] = !isOpen;
}}
>
{#if isOpen}
<ChevronDown class="size-4" />
{:else}
<ChevronRight class="size-4" />
{/if}
</button>
<a
href="/groups/{group.id}"
class="line-clamp-1 block h-full grow place-content-center text-left"
>
{group.name}
</a>
</div>
<ul class:hidden={!isOpen}>
{#each group.feeds as feed}
{@const textColor = feed.suspended
? 'text-neutral-content/60'
: feed.failure
? 'text-error'
: ''}
<li>
<a
id="sidebar-feed-{feed.indexInList}"
data-group-id={group.id}
href="/feeds/{feed.id}"
class={`${isHighlight('/feeds/' + feed.id) ? 'menu-active' : ''} focus:ring-2`}
>
<div class="avatar">
<div class="size-4 rounded-full">
<img src={getFavicon(feed.link)} alt={feed.name} loading="lazy" />
</div>
<span class={`line-clamp-1 grow ${textColor}`}>{feed.name}</span>
{#if feed.unread_count > 0}
<span class="text-base-content/60 text-xs">{feed.unread_count}</span>
{/if}
</a>
</li>
{/each}
</ul>
</li>
{/each}
{/await}
</div>
<span class={`line-clamp-1 grow ${textColor}`}>{feed.name}</span>
{#if feed.unread_count > 0}
<span class="text-base-content/60 text-xs">{feed.unread_count}</span>
{/if}
</a>
</li>
{/each}
</ul>
</li>
{/each}
</ul>
</div>

View file

@ -4,3 +4,18 @@ export const globalState = $state({
groups: [] as Group[],
feeds: [] as Feed[]
});
export function setGlobalFeeds(feeds: Feed[]) {
globalState.feeds = feeds;
}
export function setGlobalGroups(groups: Group[]) {
globalState.groups = groups;
}
export function updateUnreadCount(feedId: number, change: number) {
const feed = globalState.feeds.find((f) => f.id === feedId);
if (feed) {
feed.unread_count = Math.max(0, (feed.unread_count || 0) + change);
}
}

View file

@ -4,7 +4,7 @@
import ShortcutHelpModal from '$lib/components/ShortcutHelpModal.svelte';
import Sidebar from '$lib/components/Sidebar.svelte';
let { children, data } = $props();
let { children } = $props();
let showSidebar = $state(false);
beforeNavigate(() => {
showSidebar = false;
@ -29,7 +29,7 @@
<div
class="text-base-content bg-base-200 z-50 h-full min-h-full w-[80%] overflow-x-hidden px-2 py-4 lg:w-72"
>
<Sidebar feeds={data.feeds} groups={data.groups} />
<Sidebar />
</div>
</div>
</div>

View file

@ -1,20 +1,20 @@
import { listFeeds } from '$lib/api/feed';
import { allGroups } from '$lib/api/group';
import { globalState } from '$lib/state.svelte';
import { setGlobalFeeds, setGlobalGroups } from '$lib/state.svelte';
import type { LayoutLoad } from './$types';
export const load: LayoutLoad = async () => {
const feeds = listFeeds().then((feeds) => {
globalState.feeds = feeds;
return feeds;
});
const groups = allGroups().then((groups) => {
groups.sort((a, b) => a.id - b.id);
globalState.groups = groups;
return groups;
});
return {
feeds,
groups
};
export const load: LayoutLoad = async ({ depends }) => {
depends('app:feeds', 'app:groups');
await Promise.all([
allGroups().then((groups) => {
groups.sort((a, b) => a.id - b.id);
setGlobalGroups(groups);
}),
listFeeds().then((feeds) => {
setGlobalFeeds(feeds);
})
]);
return {};
};

View file

@ -1,9 +1,7 @@
import { listItems, parseURLtoFilter } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ depends, url }) => {
depends(`page:${url.pathname}`);
export const load: PageLoad = async ({ url }) => {
const filter = parseURLtoFilter(url.searchParams, {
unread: true,
bookmark: undefined,

View file

@ -1,9 +1,7 @@
import { listItems, parseURLtoFilter } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, depends }) => {
depends(`page:${url.pathname}`);
export const load: PageLoad = async ({ url }) => {
const filter = parseURLtoFilter(url.searchParams, {
unread: undefined,
bookmark: undefined,

View file

@ -1,9 +1,7 @@
import { listItems, parseURLtoFilter } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, depends }) => {
depends(`page:${url.pathname}`);
export const load: PageLoad = async ({ url }) => {
const filter = parseURLtoFilter(url.searchParams, {
unread: undefined,
bookmark: true,

View file

@ -15,9 +15,7 @@
{/await}
</svelte:head>
{#await data.feed}
Loading...
{:then feed}
{#await data.feed then feed}
<PageNavHeader showSearch={true}>
{#await data.items then items}
<ItemActionMarkAllasRead items={items.items} />

View file

@ -4,9 +4,7 @@ import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ depends, url, params }) => {
depends(`page:${url.pathname}`);
export const load: PageLoad = async ({ url, params }) => {
const id = parseInt(params.id);
const feed = getFeed(id);
const filter = parseURLtoFilter(url.searchParams, {

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { goto, invalidate, invalidateAll } from '$app/navigation';
import { page } from '$app/state';
import { goto, invalidateAll } from '$app/navigation';
import { deleteFeed, updateFeed, type FeedUpdateForm } from '$lib/api/feed';
import type { Feed } from '$lib/api/model';
import { t } from '$lib/i18n';
@ -41,7 +40,7 @@
suspended: !feed.suspended
});
toast.success(t('state.success'));
invalidate('page:' + page.url.pathname);
invalidateAll();
} catch (e) {
toast.error((e as Error).message);
}

View file

@ -14,9 +14,7 @@
{/await}
</svelte:head>
{#await data.group}
Loading...
{:then group}
{#await data.group then group}
<PageNavHeader showSearch={true}>
{#await data.items then items}
<ItemActionMarkAllasRead items={items.items} />

View file

@ -5,9 +5,7 @@ import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = async ({ depends, url, params }) => {
depends(`page:${url.pathname}`);
export const load: PageLoad = async ({ url, params }) => {
const id = parseInt(params.id);
const group = allGroups().then((groups) => {
const group = groups.find((g) => g.id === id);

View file

@ -4,9 +4,7 @@ import type { PageLoad } from './$types';
export const prerender = false;
export const load: PageLoad = ({ depends, url, params }) => {
depends(`page:${url.pathname}`);
export const load: PageLoad = ({ params }) => {
const id = parseInt(params.id);
if (id < 1) {
error(404, 'wrong id');

View file

@ -1,9 +1,7 @@
import { listItems, parseURLtoFilter } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url, depends }) => {
depends(`page:${url.pathname}`);
export const load: PageLoad = async ({ url }) => {
const filter = parseURLtoFilter(url.searchParams);
return {
filter,