mirror of
https://github.com/0x2E/fusion.git
synced 2025-06-08 05:27:15 +09:00
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">
|
<script lang="ts">
|
||||||
import { invalidate } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { listItems, updateUnread } from '$lib/api/item';
|
||||||
import { listItems, parseURLtoFilter, updateUnread } from '$lib/api/item';
|
|
||||||
import type { Item } from '$lib/api/model';
|
import type { Item } from '$lib/api/model';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import { CheckCheck } from 'lucide-svelte';
|
import { CheckCheck } from 'lucide-svelte';
|
||||||
|
@ -29,7 +28,7 @@
|
||||||
const ids = props.items.map((v) => v.id);
|
const ids = props.items.map((v) => v.id);
|
||||||
await updateUnread(ids, false);
|
await updateUnread(ids, false);
|
||||||
toast.success(t('state.success'));
|
toast.success(t('state.success'));
|
||||||
invalidate('page:' + page.url.pathname);
|
invalidateAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error((e as Error).message);
|
toast.error((e as Error).message);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +56,7 @@
|
||||||
await updateUnread(ids, false);
|
await updateUnread(ids, false);
|
||||||
}
|
}
|
||||||
toast.success(t('state.success'));
|
toast.success(t('state.success'));
|
||||||
invalidate('page:' + page.url.pathname);
|
invalidateAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error((e as Error).message);
|
toast.error((e as Error).message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<script module>
|
<script module>
|
||||||
import type { Item } from '$lib/api/model';
|
|
||||||
import { updateUnread } from '$lib/api/item';
|
import { updateUnread } from '$lib/api/item';
|
||||||
|
import type { Item } from '$lib/api/model';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
export async function toggleUnread(item: Item) {
|
export async function toggleUnread(item: Item) {
|
||||||
try {
|
try {
|
||||||
await updateUnread([item.id], !item.unread);
|
await updateUnread([item.id], !item.unread);
|
||||||
item.unread = !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) {
|
} catch (e) {
|
||||||
toast.error((e as Error).message);
|
toast.error((e as Error).message);
|
||||||
}
|
}
|
||||||
|
@ -15,6 +18,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
|
import { updateUnreadCount } from '$lib/state.svelte';
|
||||||
import { CheckIcon, UndoIcon } from 'lucide-svelte';
|
import { CheckIcon, UndoIcon } from 'lucide-svelte';
|
||||||
import { activateShortcut, deactivateShortcut, shortcuts } from './ShortcutHelpModal.svelte';
|
import { activateShortcut, deactivateShortcut, shortcuts } from './ShortcutHelpModal.svelte';
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { getFavicon } from '$lib/api/favicon';
|
import { getFavicon } from '$lib/api/favicon';
|
||||||
import { logout } from '$lib/api/login';
|
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 { t } from '$lib/i18n';
|
||||||
|
import { globalState } from '$lib/state.svelte';
|
||||||
import {
|
import {
|
||||||
BookmarkCheck,
|
BookmarkCheck,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
@ -28,26 +29,17 @@
|
||||||
} from './ShortcutHelpModal.svelte';
|
} from './ShortcutHelpModal.svelte';
|
||||||
import ThemeController from './ThemeController.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 openGroups = $state<Record<number, boolean>>({});
|
||||||
|
|
||||||
let feedList = $derived.by(async () => {
|
let groupList = $derived.by(() => {
|
||||||
const [feedsData, groupsData] = await Promise.all([feeds, groups]);
|
|
||||||
const groupFeeds: { id: number; name: string; feeds: (Feed & { indexInList: number })[] }[] =
|
const groupFeeds: { id: number; name: string; feeds: (Feed & { indexInList: number })[] }[] =
|
||||||
[];
|
[];
|
||||||
let curIndexInList = 0;
|
let curIndexInList = 0;
|
||||||
groupsData.forEach((group) => {
|
globalState.groups.forEach((group) => {
|
||||||
groupFeeds.push({
|
groupFeeds.push({
|
||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
feeds: feedsData
|
feeds: globalState.feeds
|
||||||
.filter((feed) => feed.group.id === group.id)
|
.filter((feed) => feed.group.id === group.id)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((feed) => ({
|
.map((feed) => ({
|
||||||
|
@ -55,7 +47,6 @@
|
||||||
indexInList: curIndexInList++
|
indexInList: curIndexInList++
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
openGroups[group.id] = false;
|
|
||||||
});
|
});
|
||||||
return groupFeeds;
|
return groupFeeds;
|
||||||
});
|
});
|
||||||
|
@ -115,15 +106,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedFeedIndex = $state(-1);
|
let selectedFeedIndex = $state(-1);
|
||||||
let selectedFeedGroupId = $state(-1);
|
|
||||||
$effect(() => {
|
|
||||||
feeds.then(() => {
|
|
||||||
selectedFeedIndex = -1;
|
|
||||||
selectedFeedGroupId = -1;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
async function moveFeed(direction: 'prev' | 'next') {
|
async function moveFeed(direction: 'prev' | 'next') {
|
||||||
const feedList = await feeds;
|
const feedList = globalState.feeds;
|
||||||
|
|
||||||
if (feedList.length === 0) return;
|
if (feedList.length === 0) return;
|
||||||
|
|
||||||
|
@ -200,60 +184,59 @@
|
||||||
|
|
||||||
<ul class="menu w-full">
|
<ul class="menu w-full">
|
||||||
<li class="menu-title text-xs">{t('common.feeds')}</li>
|
<li class="menu-title text-xs">{t('common.feeds')}</li>
|
||||||
{#await feedList}
|
{#each groupList as group}
|
||||||
<div class="skeleton bg-base-300 h-10"></div>
|
{@const isOpen = openGroups[group.id]}
|
||||||
{:then groupData}
|
<li class="p-0">
|
||||||
{#each groupData as group (group.id)}
|
<div class="gap-0 p-0">
|
||||||
{@const isOpen = openGroups[group.id]}
|
<button
|
||||||
<li>
|
class="btn btn-ghost btn-sm btn-square"
|
||||||
<div class="relative flex items-center pl-10">
|
onclick={(event) => {
|
||||||
<button
|
event.preventDefault();
|
||||||
class="btn btn-ghost btn-sm btn-square absolute top-0 left-1"
|
openGroups[group.id] = !isOpen;
|
||||||
onclick={(event) => {
|
}}
|
||||||
event.preventDefault();
|
>
|
||||||
openGroups[group.id] = !isOpen;
|
{#if isOpen}
|
||||||
}}
|
<ChevronDown class="size-4" />
|
||||||
>
|
{:else}
|
||||||
{#if isOpen}
|
<ChevronRight class="size-4" />
|
||||||
<ChevronDown class="size-4" />
|
{/if}
|
||||||
{:else}
|
</button>
|
||||||
<ChevronRight class="size-4" />
|
<a
|
||||||
{/if}
|
href="/groups/{group.id}"
|
||||||
</button>
|
class="line-clamp-1 block h-full grow place-content-center text-left"
|
||||||
<a href="/groups/{group.id}" class="line-clamp-1 grow text-left">
|
>
|
||||||
{group.name}
|
{group.name}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ul class:hidden={!isOpen}>
|
<ul class:hidden={!isOpen}>
|
||||||
{#each group.feeds as feed}
|
{#each group.feeds as feed}
|
||||||
{@const textColor = feed.suspended
|
{@const textColor = feed.suspended
|
||||||
? 'text-neutral-content/60'
|
? 'text-neutral-content/60'
|
||||||
: feed.failure
|
: feed.failure
|
||||||
? 'text-error'
|
? 'text-error'
|
||||||
: ''}
|
: ''}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
id="sidebar-feed-{feed.indexInList}"
|
id="sidebar-feed-{feed.indexInList}"
|
||||||
data-group-id={group.id}
|
data-group-id={group.id}
|
||||||
href="/feeds/{feed.id}"
|
href="/feeds/{feed.id}"
|
||||||
class={`${isHighlight('/feeds/' + feed.id) ? 'menu-active' : ''} focus:ring-2`}
|
class={`${isHighlight('/feeds/' + feed.id) ? 'menu-active' : ''} focus:ring-2`}
|
||||||
>
|
>
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
<div class="size-4 rounded-full">
|
<div class="size-4 rounded-full">
|
||||||
<img src={getFavicon(feed.link)} alt={feed.name} loading="lazy" />
|
<img src={getFavicon(feed.link)} alt={feed.name} loading="lazy" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span class={`line-clamp-1 grow ${textColor}`}>{feed.name}</span>
|
</div>
|
||||||
{#if feed.unread_count > 0}
|
<span class={`line-clamp-1 grow ${textColor}`}>{feed.name}</span>
|
||||||
<span class="text-base-content/60 text-xs">{feed.unread_count}</span>
|
{#if feed.unread_count > 0}
|
||||||
{/if}
|
<span class="text-base-content/60 text-xs">{feed.unread_count}</span>
|
||||||
</a>
|
{/if}
|
||||||
</li>
|
</a>
|
||||||
{/each}
|
</li>
|
||||||
</ul>
|
{/each}
|
||||||
</li>
|
</ul>
|
||||||
{/each}
|
</li>
|
||||||
{/await}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,3 +4,18 @@ export const globalState = $state({
|
||||||
groups: [] as Group[],
|
groups: [] as Group[],
|
||||||
feeds: [] as Feed[]
|
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 ShortcutHelpModal from '$lib/components/ShortcutHelpModal.svelte';
|
||||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||||
|
|
||||||
let { children, data } = $props();
|
let { children } = $props();
|
||||||
let showSidebar = $state(false);
|
let showSidebar = $state(false);
|
||||||
beforeNavigate(() => {
|
beforeNavigate(() => {
|
||||||
showSidebar = false;
|
showSidebar = false;
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<div
|
<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"
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { listFeeds } from '$lib/api/feed';
|
import { listFeeds } from '$lib/api/feed';
|
||||||
import { allGroups } from '$lib/api/group';
|
import { allGroups } from '$lib/api/group';
|
||||||
import { globalState } from '$lib/state.svelte';
|
import { setGlobalFeeds, setGlobalGroups } from '$lib/state.svelte';
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
export const load: LayoutLoad = async () => {
|
export const load: LayoutLoad = async ({ depends }) => {
|
||||||
const feeds = listFeeds().then((feeds) => {
|
depends('app:feeds', 'app:groups');
|
||||||
globalState.feeds = feeds;
|
|
||||||
return feeds;
|
await Promise.all([
|
||||||
});
|
allGroups().then((groups) => {
|
||||||
const groups = allGroups().then((groups) => {
|
groups.sort((a, b) => a.id - b.id);
|
||||||
groups.sort((a, b) => a.id - b.id);
|
setGlobalGroups(groups);
|
||||||
globalState.groups = groups;
|
}),
|
||||||
return groups;
|
listFeeds().then((feeds) => {
|
||||||
});
|
setGlobalFeeds(feeds);
|
||||||
return {
|
})
|
||||||
feeds,
|
]);
|
||||||
groups
|
|
||||||
};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageLoad = async ({ depends, url }) => {
|
export const load: PageLoad = async ({ url }) => {
|
||||||
depends(`page:${url.pathname}`);
|
|
||||||
|
|
||||||
const filter = parseURLtoFilter(url.searchParams, {
|
const filter = parseURLtoFilter(url.searchParams, {
|
||||||
unread: true,
|
unread: true,
|
||||||
bookmark: undefined,
|
bookmark: undefined,
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageLoad = async ({ url, depends }) => {
|
export const load: PageLoad = async ({ url }) => {
|
||||||
depends(`page:${url.pathname}`);
|
|
||||||
|
|
||||||
const filter = parseURLtoFilter(url.searchParams, {
|
const filter = parseURLtoFilter(url.searchParams, {
|
||||||
unread: undefined,
|
unread: undefined,
|
||||||
bookmark: undefined,
|
bookmark: undefined,
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageLoad = async ({ url, depends }) => {
|
export const load: PageLoad = async ({ url }) => {
|
||||||
depends(`page:${url.pathname}`);
|
|
||||||
|
|
||||||
const filter = parseURLtoFilter(url.searchParams, {
|
const filter = parseURLtoFilter(url.searchParams, {
|
||||||
unread: undefined,
|
unread: undefined,
|
||||||
bookmark: true,
|
bookmark: true,
|
||||||
|
|
|
@ -15,9 +15,7 @@
|
||||||
{/await}
|
{/await}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#await data.feed}
|
{#await data.feed then feed}
|
||||||
Loading...
|
|
||||||
{:then feed}
|
|
||||||
<PageNavHeader showSearch={true}>
|
<PageNavHeader showSearch={true}>
|
||||||
{#await data.items then items}
|
{#await data.items then items}
|
||||||
<ItemActionMarkAllasRead items={items.items} />
|
<ItemActionMarkAllasRead items={items.items} />
|
||||||
|
|
|
@ -4,9 +4,7 @@ import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
export const load: PageLoad = async ({ depends, url, params }) => {
|
export const load: PageLoad = async ({ url, params }) => {
|
||||||
depends(`page:${url.pathname}`);
|
|
||||||
|
|
||||||
const id = parseInt(params.id);
|
const id = parseInt(params.id);
|
||||||
const feed = getFeed(id);
|
const feed = getFeed(id);
|
||||||
const filter = parseURLtoFilter(url.searchParams, {
|
const filter = parseURLtoFilter(url.searchParams, {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto, invalidate, invalidateAll } from '$app/navigation';
|
import { goto, invalidateAll } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
|
||||||
import { deleteFeed, updateFeed, type FeedUpdateForm } from '$lib/api/feed';
|
import { deleteFeed, updateFeed, type FeedUpdateForm } from '$lib/api/feed';
|
||||||
import type { Feed } from '$lib/api/model';
|
import type { Feed } from '$lib/api/model';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
|
@ -41,7 +40,7 @@
|
||||||
suspended: !feed.suspended
|
suspended: !feed.suspended
|
||||||
});
|
});
|
||||||
toast.success(t('state.success'));
|
toast.success(t('state.success'));
|
||||||
invalidate('page:' + page.url.pathname);
|
invalidateAll();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error((e as Error).message);
|
toast.error((e as Error).message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,7 @@
|
||||||
{/await}
|
{/await}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#await data.group}
|
{#await data.group then group}
|
||||||
Loading...
|
|
||||||
{:then group}
|
|
||||||
<PageNavHeader showSearch={true}>
|
<PageNavHeader showSearch={true}>
|
||||||
{#await data.items then items}
|
{#await data.items then items}
|
||||||
<ItemActionMarkAllasRead items={items.items} />
|
<ItemActionMarkAllasRead items={items.items} />
|
||||||
|
|
|
@ -5,9 +5,7 @@ import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
export const load: PageLoad = async ({ depends, url, params }) => {
|
export const load: PageLoad = async ({ url, params }) => {
|
||||||
depends(`page:${url.pathname}`);
|
|
||||||
|
|
||||||
const id = parseInt(params.id);
|
const id = parseInt(params.id);
|
||||||
const group = allGroups().then((groups) => {
|
const group = allGroups().then((groups) => {
|
||||||
const group = groups.find((g) => g.id === id);
|
const group = groups.find((g) => g.id === id);
|
||||||
|
|
|
@ -4,9 +4,7 @@ import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
export const load: PageLoad = ({ depends, url, params }) => {
|
export const load: PageLoad = ({ params }) => {
|
||||||
depends(`page:${url.pathname}`);
|
|
||||||
|
|
||||||
const id = parseInt(params.id);
|
const id = parseInt(params.id);
|
||||||
if (id < 1) {
|
if (id < 1) {
|
||||||
error(404, 'wrong id');
|
error(404, 'wrong id');
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
import { listItems, parseURLtoFilter } from '$lib/api/item';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageLoad = async ({ url, depends }) => {
|
export const load: PageLoad = async ({ url }) => {
|
||||||
depends(`page:${url.pathname}`);
|
|
||||||
|
|
||||||
const filter = parseURLtoFilter(url.searchParams);
|
const filter = parseURLtoFilter(url.searchParams);
|
||||||
return {
|
return {
|
||||||
filter,
|
filter,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue