1
0
Fork 0
forked from 0x2E/fusion

feat: add bookmark

This commit is contained in:
rook1e 2024-03-12 01:51:46 +08:00
parent 5dee1de132
commit b43a17767d
No known key found for this signature in database
GPG key ID: C63289D731719BC0
14 changed files with 129 additions and 74 deletions

View file

@ -55,7 +55,6 @@ cd build
## ToDo ## ToDo
- Bookmark
- PWA - PWA
## Credits ## Credits

View file

@ -32,6 +32,9 @@ func Run() {
if conf.Debug { if conf.Debug {
r.Debug = true r.Debug = true
r.Use(middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) { r.Use(middleware.BodyDump(func(c echo.Context, reqBody, resBody []byte) {
if len(resBody) > 500 {
resBody = append(resBody[:500], []byte("...")...)
}
r.Logger.Debugf("req: %s\nresp: %s\n", reqBody, resBody) r.Logger.Debugf("req: %s\nresp: %s\n", reqBody, resBody)
})) }))
} }

View file

@ -7,6 +7,7 @@ type listOptions = {
keyword?: string; keyword?: string;
feed_id?: number; feed_id?: number;
unread?: boolean; unread?: boolean;
bookmark?: boolean;
}; };
export async function listItems(options?: listOptions) { export async function listItems(options?: listOptions) {
@ -21,10 +22,14 @@ export async function getItem(id: number) {
return api.get('items/' + id).json<Item>(); return api.get('items/' + id).json<Item>();
} }
export async function updateItem(id: number, unread: boolean) { export async function updateItem(
id: number,
data: {
unread?: boolean;
bookmark?: boolean;
}
) {
return api.patch('items/' + id, { return api.patch('items/' + id, {
json: { json: data
unread: unread
}
}); });
} }

View file

@ -20,5 +20,6 @@ export type Item = {
content: string; content: string;
pub_date: Date; pub_date: Date;
unread: boolean; unread: boolean;
bookmark: boolean;
feed: { id: number; name: string }; feed: { id: number; name: string };
}; };

View file

@ -1,5 +1,11 @@
<script lang="ts"> <script lang="ts">
import { CheckIcon, ExternalLinkIcon, UndoIcon } from 'lucide-svelte'; import {
BookmarkIcon,
BookmarkXIcon,
CheckIcon,
ExternalLinkIcon,
UndoIcon
} from 'lucide-svelte';
import type { ComponentType } from 'svelte'; import type { ComponentType } from 'svelte';
import type { Icon } from 'lucide-svelte'; import type { Icon } from 'lucide-svelte';
import * as Tooltip from '$lib/components/ui/tooltip'; import * as Tooltip from '$lib/components/ui/tooltip';
@ -12,48 +18,54 @@
id: number; id: number;
link: string; link: string;
unread: boolean; unread: boolean;
bookmark: boolean;
}; };
function getActions( function getActions(
unread: boolean unread: boolean,
bookmark: boolean
): { icon: ComponentType<Icon>; tooltip: string; handler: (e: Event) => void }[] { ): { icon: ComponentType<Icon>; tooltip: string; handler: (e: Event) => void }[] {
const list = [ const visitOriginalAction = {
// { icon: BookmarkIcon, tooltip: 'Save to Bookmark', handler: handleSaveToBookmark }, icon: ExternalLinkIcon,
{ icon: ExternalLinkIcon, tooltip: 'Visit Original Link', handler: handleExternalLink } tooltip: 'Visit Original Link',
]; handler: handleExternalLink
};
const unreadAction = unread const unreadAction = unread
? { icon: CheckIcon, tooltip: 'Mark as Read', handler: handleMarkAsRead } ? { icon: CheckIcon, tooltip: 'Mark as Read', handler: handleToggleUnread }
: { icon: UndoIcon, tooltip: 'Mark as Unread', handler: handleMarkAsUnread }; : { icon: UndoIcon, tooltip: 'Mark as Unread', handler: handleToggleUnread };
list.unshift(unreadAction); const bookmarkAction = bookmark
return list; ? { icon: BookmarkXIcon, tooltip: 'Cancel Bookmark', handler: handleToggleBookmark }
} : { icon: BookmarkIcon, tooltip: 'Add to Bookmark', handler: handleToggleBookmark };
$: actions = getActions(data.unread);
async function handleMarkAsRead(e: Event) { return [unreadAction, bookmarkAction, visitOriginalAction];
}
$: actions = getActions(data.unread, data.bookmark);
async function handleToggleUnread(e: Event) {
e.preventDefault(); e.preventDefault();
try { try {
await updateItem(data.id, false); await updateItem(data.id, { unread: !data.unread });
invalidateAll();
} catch (e) { } catch (e) {
toast.error((e as Error).message); toast.error((e as Error).message);
} }
invalidateAll();
}
async function handleMarkAsUnread(e: Event) {
e.preventDefault();
try {
await updateItem(data.id, true);
} catch (e) {
toast.error((e as Error).message);
}
invalidateAll();
} }
function handleExternalLink(e: Event) { function handleExternalLink(e: Event) {
e.preventDefault(); e.preventDefault();
handleMarkAsRead(e); handleToggleUnread(e);
window.open(data.link, '_target'); window.open(data.link, '_target');
} }
async function handleToggleBookmark(e: Event) {
e.preventDefault();
try {
await updateItem(data.id, { bookmark: !data.bookmark });
invalidateAll();
} catch (e) {
toast.error((e as Error).message);
}
}
</script> </script>
<div> <div>

View file

@ -60,7 +60,9 @@
</div> </div>
<div class="w-full hidden group-hover:inline-flex justify-end"> <div class="w-full hidden group-hover:inline-flex justify-end">
<ItemAction data={{ id: item.id, link: item.link, unread: item.unread }} /> <ItemAction
data={{ id: item.id, link: item.link, unread: item.unread, bookmark: item.bookmark }}
/>
</div> </div>
</div> </div>
</Button> </Button>

View file

@ -9,6 +9,7 @@
} }
let links: link[] = [ let links: link[] = [
{ label: 'Unread', url: '/' }, { label: 'Unread', url: '/' },
{ label: 'Bookmark', url: '/bookmarks' },
{ label: 'All', url: '/all' }, { label: 'All', url: '/all' },
{ label: 'Feeds', url: '/feeds' } { label: 'Feeds', url: '/feeds' }
]; ];
@ -31,7 +32,7 @@
<nav class="block w-full sm:mt-3 mb-6"> <nav class="block w-full sm:mt-3 mb-6">
<div <div
class="flex justify-around items-center w-full sm:max-w-sm mx-auto px-6 py-4 sm:rounded-2xl shadow-md sm:border bg-background" class="flex justify-around items-center w-full sm:max-w-[600px] mx-auto px-6 py-4 sm:rounded-2xl shadow-md sm:border bg-background"
> >
<div class="flex items-center"> <div class="flex items-center">
<img src="/favicon.png" alt="logo" class="w-10" /> <img src="/favicon.png" alt="logo" class="w-10" />

View file

@ -0,0 +1,14 @@
<script lang="ts">
import type { PageData } from './$types';
import ItemList from '$lib/components/ItemList.svelte';
import PageHead from '$lib/components/PageHead.svelte';
export let data: PageData;
</script>
<svelte:head>
<title>Bookmark</title>
</svelte:head>
<PageHead title="Bookmark" />
<ItemList data={data.items} />

View file

@ -0,0 +1,6 @@
import { listItems } from '$lib/api/item';
import type { PageLoad } from './$types';
export const load: PageLoad = () => {
return listItems({ bookmark: true });
};

View file

@ -57,7 +57,7 @@
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{data.feed.name} / {moment(data.pub_date).format('lll')} {data.feed.name} / {moment(data.pub_date).format('lll')}
</p> </p>
<ItemAction data={{ id: data.id, link: data.link, unread: data.unread }} /> <ItemAction data={{ id: data.id, link: data.link, unread: data.unread, bookmark: data.bookmark }} />
<article class="mt-6 prose dark:prose-invert prose-lg max-w-full"> <article class="mt-6 prose dark:prose-invert prose-lg max-w-full">
<!-- eslint-disable-next-line svelte/no-at-html-tags --> <!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html data.content} {@html data.content}

View file

@ -12,12 +12,13 @@ type Item struct {
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt soft_delete.DeletedAt DeletedAt soft_delete.DeletedAt
Title *string `gorm:"title;not null;index"` Title *string `gorm:"title;not null;index"`
GUID *string `gorm:"guid,index"` GUID *string `gorm:"guid,index"`
Link *string `gorm:"link,index"` Link *string `gorm:"link,index"`
Content *string `gorm:"content"` Content *string `gorm:"content"`
PubDate *time.Time `gorm:"pub_date"` PubDate *time.Time `gorm:"pub_date"`
Unread *bool `gorm:"unread;default:true"` Unread *bool `gorm:"unread;default:true;index"`
Bookmark *bool `gorm:"bookmark;default:false;index"`
FeedID uint `gorm:"feed_id;index"` FeedID uint `gorm:"feed_id;index"`
Feed Feed Feed Feed

View file

@ -18,9 +18,10 @@ type Item struct {
} }
type ItemFilter struct { type ItemFilter struct {
Keyword *string Keyword *string
FeedID *uint FeedID *uint
Unread *bool Unread *bool
Bookmark *bool
} }
func (i Item) List(filter ItemFilter, offset, count *int) ([]*model.Item, int, error) { func (i Item) List(filter ItemFilter, offset, count *int) ([]*model.Item, int, error) {
@ -35,7 +36,10 @@ func (i Item) List(filter ItemFilter, offset, count *int) ([]*model.Item, int, e
db = db.Where("feed_id = ?", *filter.FeedID) db = db.Where("feed_id = ?", *filter.FeedID)
} }
if filter.Unread != nil { if filter.Unread != nil {
db = db.Where("unread = true") db = db.Where("unread = ?", *filter.Unread)
}
if filter.Bookmark != nil {
db = db.Where("bookmark = ?", *filter.Bookmark)
} }
err := db.Count(&total).Error err := db.Count(&total).Error
if err != nil { if err != nil {

View file

@ -26,9 +26,10 @@ func NewItem(repo ItemRepo) *Item {
func (i Item) List(req *ReqItemList) (*RespItemList, error) { func (i Item) List(req *ReqItemList) (*RespItemList, error) {
filter := repo.ItemFilter{ filter := repo.ItemFilter{
Keyword: req.Keyword, Keyword: req.Keyword,
FeedID: req.FeedID, FeedID: req.FeedID,
Unread: req.Unread, Unread: req.Unread,
Bookmark: req.Bookmark,
} }
data, total, err := i.repo.List(filter, req.Offset, req.Count) data, total, err := i.repo.List(filter, req.Offset, req.Count)
if err != nil { if err != nil {
@ -38,12 +39,13 @@ func (i Item) List(req *ReqItemList) (*RespItemList, error) {
items := make([]*ItemForm, 0, len(data)) items := make([]*ItemForm, 0, len(data))
for _, v := range data { for _, v := range data {
items = append(items, &ItemForm{ items = append(items, &ItemForm{
ID: v.ID, ID: v.ID,
GUID: v.GUID, GUID: v.GUID,
Title: v.Title, Title: v.Title,
Link: v.Link, Link: v.Link,
PubDate: v.PubDate, PubDate: v.PubDate,
Unread: v.Unread, Unread: v.Unread,
Bookmark: v.Bookmark,
Feed: ItemFeed{ Feed: ItemFeed{
ID: v.Feed.ID, ID: v.Feed.ID,
Name: v.Feed.Name, Name: v.Feed.Name,
@ -63,13 +65,14 @@ func (i Item) Get(req *ReqItemGet) (*RespItemGet, error) {
} }
return &RespItemGet{ return &RespItemGet{
ID: data.ID, ID: data.ID,
GUID: data.GUID, GUID: data.GUID,
Title: data.Title, Title: data.Title,
Link: data.Link, Link: data.Link,
Content: data.Content, Content: data.Content,
PubDate: data.PubDate, PubDate: data.PubDate,
Unread: data.Unread, Unread: data.Unread,
Bookmark: data.Bookmark,
Feed: ItemFeed{ Feed: ItemFeed{
ID: data.Feed.ID, ID: data.Feed.ID,
Name: data.Feed.Name, Name: data.Feed.Name,
@ -79,7 +82,8 @@ func (i Item) Get(req *ReqItemGet) (*RespItemGet, error) {
func (i Item) Update(req *ReqItemUpdate) error { func (i Item) Update(req *ReqItemUpdate) error {
return i.repo.Update(req.ID, &model.Item{ return i.repo.Update(req.ID, &model.Item{
Unread: req.Unread, Unread: req.Unread,
Bookmark: req.Bookmark,
}) })
} }

View file

@ -7,21 +7,23 @@ type ItemFeed struct {
Name *string `json:"name"` Name *string `json:"name"`
} }
type ItemForm struct { type ItemForm struct {
ID uint `json:"id"` ID uint `json:"id"`
Title *string `json:"title"` Title *string `json:"title"`
Link *string `json:"link"` Link *string `json:"link"`
GUID *string `json:"guid"` GUID *string `json:"guid"`
Content *string `json:"content"` Content *string `json:"content"`
PubDate *time.Time `json:"pub_date"` PubDate *time.Time `json:"pub_date"`
Unread *bool `json:"unread"` Unread *bool `json:"unread"`
Feed ItemFeed `json:"feed"` Bookmark *bool `json:"bookmark"`
Feed ItemFeed `json:"feed"`
} }
type ReqItemList struct { type ReqItemList struct {
Paginate Paginate
Keyword *string `query:"keyword"` Keyword *string `query:"keyword"`
FeedID *uint `query:"feed_id"` FeedID *uint `query:"feed_id"`
Unread *bool `query:"unread"` Unread *bool `query:"unread"`
Bookmark *bool `query:"bookmark"`
} }
type RespItemList struct { type RespItemList struct {
@ -36,8 +38,9 @@ type ReqItemGet struct {
type RespItemGet ItemForm type RespItemGet ItemForm
type ReqItemUpdate struct { type ReqItemUpdate struct {
ID uint `param:"id" validate:"required"` ID uint `param:"id" validate:"required"`
Unread *bool `json:"unread"` Unread *bool `json:"unread"`
Bookmark *bool `json:"bookmark"`
} }
type ReqItemDelete struct { type ReqItemDelete struct {