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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Key features include:

- Group, bookmark, search, automatic feed sniffing, OPML file import/export
- Support for RSS, Atom, and JSON feed types
- Responsive, light/dark mode, PWA
- Responsive, light/dark mode, PWA, Keyboard shortcut support
- Lightweight and self-hosted friendly
- Built with Golang and SQLite, deploy with a single binary
- Pre-built Docker image
Expand Down Expand Up @@ -86,3 +86,4 @@ For example:
- Front-end is built with: [Sveltekit](https://github.com/sveltejs/kit), [daisyUI](https://github.com/saadeghi/daisyui)
- Back-end is built with: [Echo](https://github.com/labstack/echo), [GORM](https://github.com/go-gorm/gorm)
- Parsing feed with [gofeed](https://github.com/mmcdole/gofeed)
- Logo by [Icons8](https://icons8.com/icon/FeQbTvGTsiN5/news)
15 changes: 8 additions & 7 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
"@sveltejs/kit": "^2.20.2",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.15",
"@tailwindcss/vite": "^4.0.17",
"@types/eslint": "^9.6.1",
"@types/node": "^22.13.11",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@types/node": "^22.13.14",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"clsx": "^2.1.1",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
Expand All @@ -33,13 +33,14 @@
"svelte-check": "^4.1.5",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.15",
"tailwindcss": "^4.0.17",
"typescript": "^5.8.2",
"typescript-eslint": "^8.27.0",
"vite": "^6.2.2"
"typescript-eslint": "^8.28.0",
"vite": "^6.2.3"
},
"type": "module",
"dependencies": {
"@github/hotkey": "^3.1.1",
"daisyui": "^5.0.9",
"dompurify": "^3.2.4",
"ky": "^1.7.5"
Expand Down
333 changes: 173 additions & 160 deletions frontend/pnpm-lock.yaml

Large diffs are not rendered by default.

19 changes: 8 additions & 11 deletions frontend/src/lib/components/ActionSearch.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
import { Search } from 'lucide-svelte';
</script>

<label class="input input-sm lg:w-80">
<button
onclick={async () => {
await goto('/search');
}}
class="input input-sm lg:w-80"
>
<Search class="size-4 opacity-50" />
<input
type="search"
placeholder={t('item.search.placeholder')}
onclick={async () => {
await goto('/search');
}}
readonly
class="cursor-pointer"
/>
</label>
<input type="search" placeholder={t('item.search.placeholder')} readonly class="cursor-pointer" />
</button>
43 changes: 34 additions & 9 deletions frontend/src/lib/components/ItemActionBookmark.svelte
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
<script lang="ts">
<script module>
import { updateBookmark } from '$lib/api/item';
import type { Item } from '$lib/api/model';
import { t } from '$lib/i18n';
import { BookmarkIcon, BookmarkXIcon } from 'lucide-svelte';
import { toast } from 'svelte-sonner';

let { item = $bindable<Item>() } = $props();

async function toggleBookmark(e: Event) {
e.preventDefault();

export async function toggleBookmark(item: Item) {
try {
await updateBookmark(item.id, !item.bookmark);
item.bookmark = !item.bookmark;
} catch (e) {
toast.error((e as Error).message);
}
}
</script>

<script lang="ts">
import { t } from '$lib/i18n';
import { BookmarkIcon, BookmarkXIcon } from 'lucide-svelte';
import { activateShortcut, deactivateShortcut, shortcuts } from './ShortcutHelpModal.svelte';

let { item = $bindable<Item>(), enableShortcut = false } = $props();

let Icon = $derived(item.bookmark ? BookmarkXIcon : BookmarkIcon);
let tooltip = $derived(
item.bookmark ? t('item.remove_from_bookmark') : t('item.add_to_bookmark')
);

let el = $state<HTMLElement>();
$effect(() => {
if (!el) return;

if (enableShortcut) {
activateShortcut(el, shortcuts.toggleBookmark.keys);
} else {
deactivateShortcut(el);
}
});

function handleClick(e: Event) {
e.preventDefault();

toggleBookmark(item);
}
</script>

<div class="tooltip tooltip-bottom" data-tip={tooltip}>
<button onclick={toggleBookmark} aria-label={tooltip} class="btn btn-ghost btn-square">
<button
onclick={handleClick}
aria-label={tooltip}
bind:this={el}
class="btn btn-ghost btn-square"
>
<Icon class="size-4" />
</button>
</div>
8 changes: 7 additions & 1 deletion frontend/src/lib/components/ItemActionMarkAllasRead.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { t } from '$lib/i18n';
import { CheckCheck } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import { shortcut, shortcuts } from './ShortcutHelpModal.svelte';

type Props =
| {
Expand Down Expand Up @@ -39,7 +40,12 @@
class="tooltip tooltip-bottom"
data-tip={props.disabled ? undefined : t('item.mark_all_as_read')}
>
<button disabled={props.disabled} onclick={handleMarkAllAsRead} class="btn btn-ghost btn-square">
<button
disabled={props.disabled}
onclick={handleMarkAllAsRead}
use:shortcut={shortcuts.markAllasread.keys}
class="btn btn-ghost btn-square"
>
<CheckCheck class="size-4" />
</button>
</div>
37 changes: 28 additions & 9 deletions frontend/src/lib/components/ItemActionUnread.svelte
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
<script lang="ts">
import { updateUnread } from '$lib/api/item';
<script module>
import type { Item } from '$lib/api/model';
import { t } from '$lib/i18n';
import { CheckIcon, UndoIcon } from 'lucide-svelte';
import { updateUnread } from '$lib/api/item';
import { toast } from 'svelte-sonner';

let { item = $bindable<Item>() } = $props();

async function toggleUnread(e: Event) {
e.preventDefault();
export async function toggleUnread(item: Item) {
try {
await updateUnread([item.id], !item.unread);
item.unread = !item.unread;
} catch (e) {
toast.error((e as Error).message);
}
}
</script>

<script lang="ts">
import { t } from '$lib/i18n';
import { CheckIcon, UndoIcon } from 'lucide-svelte';
import { activateShortcut, deactivateShortcut, shortcuts } from './ShortcutHelpModal.svelte';

let { item = $bindable<Item>(), enableShortcut = false } = $props();

let Icon = $derived(item.unread ? CheckIcon : UndoIcon);
let tooltip = $derived(item.unread ? t('item.mark_as_read') : t('item.mark_as_unread'));

let el = $state<HTMLElement>();
$effect(() => {
if (!el) return;

if (enableShortcut) {
activateShortcut(el, shortcuts.toggleUnread.keys);
} else {
deactivateShortcut(el);
}
});

function handleClick(e: Event) {
e.preventDefault();
toggleUnread(item);
}
</script>

<div class="tooltip tooltip-bottom" data-tip={tooltip}>
<button onclick={toggleUnread} class="btn btn-ghost btn-square">
<button onclick={handleClick} bind:this={el} class="btn btn-ghost btn-square">
<Icon class="size-4" />
</button>
</div>
17 changes: 15 additions & 2 deletions frontend/src/lib/components/ItemActionVisitLink.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,29 @@
import type { Item } from '$lib/api/model';
import { t } from '$lib/i18n';
import { ExternalLink } from 'lucide-svelte';
import { activateShortcut, deactivateShortcut, shortcuts } from './ShortcutHelpModal.svelte';

interface Props {
item: Item;
enableShortcut?: boolean;
}

let { item }: Props = $props();
let { item, enableShortcut }: Props = $props();

let el = $state<HTMLElement>();
$effect(() => {
if (!el) return;

if (enableShortcut) {
activateShortcut(el, shortcuts.viewOriginal.keys);
} else {
deactivateShortcut(el);
}
});
</script>

<div class="tooltip tooltip-bottom" data-tip={t('item.visit_the_original')}>
<a href={item.link} target="_blank" class="btn btn-ghost btn-square">
<a href={item.link} target="_blank" bind:this={el} class="btn btn-ghost btn-square">
<ExternalLink class="size-4" />
</a>
</div>
51 changes: 44 additions & 7 deletions frontend/src/lib/components/ItemList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import ItemActionUnread from './ItemActionUnread.svelte';
import ItemActionVisitLink from './ItemActionVisitLink.svelte';
import Pagination from './Pagination.svelte';
import { shortcut, shortcuts } from './ShortcutHelpModal.svelte';

interface Props {
data: Promise<{
Expand Down Expand Up @@ -59,6 +60,31 @@
applyFilterToURL(url, filter);
await goto(url, { invalidate: ['page:' + page.url.pathname] });
}

let selectedItemIndex = $state(-1);
$effect(() => {
if (items) {
selectedItemIndex = -1;
}
});
function moveItem(direction: 'prev' | 'next') {
if (items.length === 0) return;

if (direction === 'prev') {
selectedItemIndex -= 1;
if (selectedItemIndex < 0) {
selectedItemIndex = items.length - 1;
}
} else {
selectedItemIndex += 1;
selectedItemIndex %= items.length;
}

const el = document.getElementById(`item-${selectedItemIndex}`);
if (el) {
el.focus();
}
}
</script>

<div>
Expand All @@ -71,12 +97,23 @@
<div class="skeleton h-10 w-full rounded"></div>
</div>
{:else}
<!-- shortcut -->
<div class="hidden">
<button onclick={() => moveItem('next')} use:shortcut={shortcuts.nextItem.keys}
>{shortcuts.nextItem.desc}</button
>
<button onclick={() => moveItem('prev')} use:shortcut={shortcuts.prevItem.keys}
>{shortcuts.prevItem.desc}</button
>
</div>

<ul data-sveltekit-preload-data="hover">
{#each items as item, i}
<li class="group rounded-md">
<li class="rounded-md">
<a
id={'item-' + i}
href={'/items/' + item.id}
class="hover:bg-base-200 relative flex w-full flex-col items-center justify-between space-y-1 space-x-2 rounded-md px-2 py-2 transition-colors md:flex-row"
class="group hover:bg-base-200 relative flex w-full flex-col items-center justify-between space-y-1 space-x-2 rounded-md px-2 py-2 transition-colors focus:ring-2 md:flex-row"
>
<div class="flex w-full md:w-[80%] md:shrink-0">
<h2
Expand All @@ -87,7 +124,7 @@
</div>
<div class="flex w-full md:grow">
<div
class="text-base-content/60 flex w-full justify-between gap-2 text-xs font-normal group-hover:hidden"
class="text-base-content/60 flex w-full justify-between gap-2 text-xs font-normal group-hover:hidden group-focus:hidden"
>
<div class="flex grow items-center space-x-2 overflow-x-hidden">
<div class="avatar">
Expand All @@ -105,11 +142,11 @@
</div>
</div>
<div
class="invisible absolute right-1 w-fit justify-end gap-2 md:group-hover:visible md:group-hover:flex"
class="invisible absolute right-1 w-fit justify-end gap-2 md:group-hover:visible md:group-hover:flex md:group-focus:visible md:group-focus:flex"
>
<ItemActionUnread bind:item={items[i]} />
<ItemActionBookmark bind:item={items[i]} />
<ItemActionVisitLink {item} />
<ItemActionUnread bind:item={items[i]} enableShortcut={i === selectedItemIndex} />
<ItemActionBookmark bind:item={items[i]} enableShortcut={i === selectedItemIndex} />
<ItemActionVisitLink {item} enableShortcut={i === selectedItemIndex} />
</div>
</a>
</li>
Expand Down
Loading