forked from 0x2E/fusion
Fix UI (#153)
* use invalidateAll() to reduce complexity * remove useless loading placeholder * refactor: update sidebar using global states
This commit is contained in:
parent
bc8109fe39
commit
b83b868fc7
16 changed files with 110 additions and 128 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue