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
9 changes: 4 additions & 5 deletions frontend/src/lib/components/ItemActionMarkAllasRead.svelte
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/lib/components/ItemActionUnread.svelte
Original file line number Diff line number Diff line change
@@ -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);
}
Expand All @@ -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';

Expand Down
133 changes: 58 additions & 75 deletions frontend/src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,34 +29,24 @@
} 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) => ({
...feed,
indexInList: curIndexInList++
}))
});
openGroups[group.id] = false;
});
return groupFeeds;
});
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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>

Expand Down
15 changes: 15 additions & 0 deletions frontend/src/lib/state.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 2 additions & 2 deletions frontend/src/routes/(authed)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>
Expand Down
30 changes: 15 additions & 15 deletions frontend/src/routes/(authed)/+layout.ts
Original file line number Diff line number Diff line change
@@ -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 {};
};
4 changes: 1 addition & 3 deletions frontend/src/routes/(authed)/+page.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/routes/(authed)/all/+page.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/routes/(authed)/bookmarks/+page.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/routes/(authed)/feeds/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/routes/(authed)/feeds/[id]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/routes/(authed)/feeds/[id]/ActionMenu.svelte
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/routes/(authed)/groups/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/routes/(authed)/groups/[id]/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading