forked from 0x2E/fusion
feat: add bookmark
This commit is contained in:
parent
5dee1de132
commit
b43a17767d
14 changed files with 129 additions and 74 deletions
|
@ -55,7 +55,6 @@ cd build
|
||||||
|
|
||||||
## ToDo
|
## ToDo
|
||||||
|
|
||||||
- Bookmark
|
|
||||||
- PWA
|
- PWA
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
|
@ -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)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
14
frontend/src/routes/bookmarks/+page.svelte
Normal file
14
frontend/src/routes/bookmarks/+page.svelte
Normal 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} />
|
6
frontend/src/routes/bookmarks/+page.ts
Normal file
6
frontend/src/routes/bookmarks/+page.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { listItems } from '$lib/api/item';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageLoad = () => {
|
||||||
|
return listItems({ bookmark: true });
|
||||||
|
};
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
12
repo/item.go
12
repo/item.go
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue