Skip to content

Commit 6b0aa7d

Browse files
committed
feat: add inline action buttons for bookmarks
- Add Archive toggle button with visual feedback (diagonal recessed background, grayscale thumbnail) - Add "Show in Linkding" action that opens the bookmark's detail view - Add Delete button with 5-second double-click confirmation - Update BookmarkItem to use Kumo Tooltip and Phosphor Icons for actions - Extend bookmarkService and background script to support these new actions
1 parent 3fa96f9 commit 6b0aa7d

File tree

6 files changed

+150
-3
lines changed

6 files changed

+150
-3
lines changed

components/BookmarkItem.module.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,17 @@
88
filter: saturate(0.5);
99
}
1010
}
11+
12+
.bookmarkItem[data-archived='true'] {
13+
background-image: repeating-linear-gradient(
14+
-45deg,
15+
transparent,
16+
transparent 4px,
17+
var(--color-kumo-recessed) 4px,
18+
var(--color-kumo-recessed) 8px
19+
);
20+
21+
& img {
22+
filter: grayscale(1);
23+
}
24+
}

components/BookmarkItem.tsx

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { memo } from 'react'
2-
import { Link } from '@cloudflare/kumo'
1+
import { memo, useState, useRef, useCallback } from 'react'
2+
import { Button, Link, Tooltip, TooltipProvider } from '@cloudflare/kumo'
3+
import {
4+
TrayArrowDownIcon,
5+
TrayArrowUpIcon,
6+
ArrowSquareOutIcon,
7+
TrashIcon,
8+
} from '@phosphor-icons/react'
39
import { BookmarkContent } from './BookmarkContent'
410
import type { Bookmark } from '@/utils/types'
511
import styles from './BookmarkItem.module.css'
@@ -8,16 +14,39 @@ interface BookmarkItemProps {
814
bookmark: Bookmark
915
isDimmed: boolean
1016
onToggleUnread: (id: number, currentUnread: boolean) => Promise<void>
17+
onDelete: (id: number) => Promise<void>
18+
onToggleArchive: (id: number, currentArchived: boolean) => Promise<void>
19+
onOpenInLinkding: (bookmark: Bookmark) => void
1120
}
1221

1322
export const BookmarkItem = memo(function BookmarkItem({
1423
bookmark,
1524
isDimmed,
1625
onToggleUnread,
26+
onDelete,
27+
onToggleArchive,
28+
onOpenInLinkding,
1729
}: BookmarkItemProps) {
30+
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
31+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
32+
33+
const handleDeletePress = useCallback(() => {
34+
if (timerRef.current) clearTimeout(timerRef.current)
35+
if (isConfirmingDelete) {
36+
onDelete(bookmark.id)
37+
setIsConfirmingDelete(false)
38+
} else {
39+
setIsConfirmingDelete(true)
40+
timerRef.current = setTimeout(() => {
41+
setIsConfirmingDelete(false)
42+
}, 5000)
43+
}
44+
}, [isConfirmingDelete, onDelete, bookmark.id])
45+
1846
return (
1947
<div
2048
data-dimmed={isDimmed}
49+
data-archived={bookmark.is_archived}
2150
className={`group relative flex items-start gap-1 py-2 px-2 hover:bg-kumo-elevated transition-colors ${styles.bookmarkItem}`}
2251
style={{ contentVisibility: 'auto' }}
2352
>
@@ -51,6 +80,57 @@ export const BookmarkItem = memo(function BookmarkItem({
5180
tagClassName={styles.dimAsset}
5281
previewClassName={styles.dimAsset}
5382
/>
83+
<div className="absolute bottom-1 right-2 z-10 flex items-center opacity-0 group-hover:opacity-100 transition-opacity">
84+
<TooltipProvider>
85+
<Tooltip content={bookmark.is_archived ? 'Unarchive' : 'Archive'} asChild side="top">
86+
<Button
87+
variant="ghost"
88+
shape="square"
89+
icon={
90+
bookmark.is_archived ? (
91+
<TrayArrowUpIcon weight="bold" />
92+
) : (
93+
<TrayArrowDownIcon weight="bold" />
94+
)
95+
}
96+
aria-label={bookmark.is_archived ? 'Unarchive' : 'Archive'}
97+
onClick={e => {
98+
e.stopPropagation()
99+
onToggleArchive(bookmark.id, bookmark.is_archived)
100+
}}
101+
/>
102+
</Tooltip>
103+
<Tooltip content="Show in Linkding" asChild side="top">
104+
<Button
105+
variant="ghost"
106+
shape="square"
107+
icon={<ArrowSquareOutIcon weight="bold" />}
108+
aria-label="Show in Linkding"
109+
onClick={e => {
110+
e.stopPropagation()
111+
onOpenInLinkding(bookmark)
112+
}}
113+
/>
114+
</Tooltip>
115+
<Tooltip
116+
content={isConfirmingDelete ? 'Click again to confirm' : 'Delete'}
117+
disabled={!isConfirmingDelete}
118+
asChild
119+
side="top"
120+
>
121+
<Button
122+
variant={isConfirmingDelete ? 'destructive' : 'ghost'}
123+
shape="square"
124+
icon={<TrashIcon weight="bold" />}
125+
aria-label={isConfirmingDelete ? 'Confirm delete bookmark' : 'Delete bookmark'}
126+
onClick={e => {
127+
e.stopPropagation()
128+
handleDeletePress()
129+
}}
130+
/>
131+
</Tooltip>
132+
</TooltipProvider>
133+
</div>
54134
</div>
55135
)
56136
})

components/BookmarksInfiniteList.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ interface BookmarksInfiniteListProps {
1313
loadMoreRef: React.RefObject<HTMLDivElement | null>
1414
loadMore: () => void
1515
onToggleUnread: (id: number, current: boolean) => Promise<void>
16+
onDelete: (id: number) => Promise<void>
17+
onToggleArchive: (id: number, currentArchived: boolean) => Promise<void>
18+
onOpenInLinkding: (bookmark: Bookmark) => void
1619
}
1720

1821
export const BookmarksInfiniteList = memo(function BookmarksInfiniteList({
@@ -24,6 +27,9 @@ export const BookmarksInfiniteList = memo(function BookmarksInfiniteList({
2427
loadMoreRef,
2528
loadMore,
2629
onToggleUnread,
30+
onDelete,
31+
onToggleArchive,
32+
onOpenInLinkding,
2733
}: BookmarksInfiniteListProps) {
2834
return (
2935
<div className="flex flex-col divide-y divide-kumo-line border-b border-kumo-line">
@@ -35,6 +41,9 @@ export const BookmarksInfiniteList = memo(function BookmarksInfiniteList({
3541
bookmark={bookmark}
3642
isDimmed={unreadFilter === 'all' && !bookmark.unread}
3743
onToggleUnread={onToggleUnread}
44+
onDelete={onDelete}
45+
onToggleArchive={onToggleArchive}
46+
onOpenInLinkding={onOpenInLinkding}
3847
/>
3948
))}
4049
<div ref={loadMoreRef} className="py-4 flex flex-col items-center gap-4">

components/BookmarksList.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,20 @@ export default function BookmarksList({ variant = 'default' }: BookmarksListProp
154154
await bookmarkService.deleteBookmark(id)
155155
}, [])
156156

157+
const handleToggleArchive = useCallback(async (id: number, currentArchived: boolean) => {
158+
await bookmarkService.toggleArchive(id, currentArchived)
159+
}, [])
160+
161+
const handleOpenInLinkding = useCallback(async (bookmark: Bookmark) => {
162+
const response = await browser.runtime.sendMessage({
163+
type: 'get-server-url',
164+
})
165+
if (!response.ok || !response.server) return
166+
await browser.tabs.create({
167+
url: `${response.server}/bookmarks?details=${bookmark.id}`,
168+
})
169+
}, [])
170+
157171
if (error) {
158172
return (
159173
<div className="p-4 flex flex-col items-center gap-4">
@@ -201,6 +215,9 @@ export default function BookmarksList({ variant = 'default' }: BookmarksListProp
201215
loadMoreRef={loadMoreRef}
202216
loadMore={loadMore}
203217
onToggleUnread={handleToggleUnread}
218+
onDelete={handleDelete}
219+
onToggleArchive={handleToggleArchive}
220+
onOpenInLinkding={handleOpenInLinkding}
204221
/>
205222
</div>
206223
</div>

entrypoints/background.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,13 @@ async function processSyncQueue() {
9898
}
9999
}
100100

101-
type MessageType = 'sync-request' | 'api-request' | 'api-patch' | 'api-post' | 'api-delete'
101+
type MessageType =
102+
| 'sync-request'
103+
| 'api-request'
104+
| 'api-patch'
105+
| 'api-post'
106+
| 'api-delete'
107+
| 'get-server-url'
102108

103109
interface ApiMessage {
104110
type: MessageType
@@ -157,6 +163,11 @@ export default defineBackground(() => {
157163
browser.runtime.onMessage.addListener((message: ApiMessage, _sender, sendResponse) => {
158164
const { type, url, data, options } = message
159165

166+
if (type === 'get-server-url') {
167+
serverStorage.getValue().then(server => sendResponse({ ok: true, server }))
168+
return true
169+
}
170+
160171
if (type === 'sync-request') {
161172
processSyncQueue()
162173
.then(() => sendResponse({ ok: true }))

utils/bookmarkService.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,20 @@ export const bookmarkService = {
3535
async addBookmark(bookmark: Bookmark) {
3636
await db.bookmarks.add({ ...bookmark, _sync_status: 'synced' })
3737
},
38+
39+
async toggleArchive(id: number, currentArchived: boolean) {
40+
const newArchived = !currentArchived
41+
await db.bookmarks.update(id, {
42+
is_archived: newArchived,
43+
_sync_status: 'pending',
44+
_local_modified_at: new Date().toISOString(),
45+
})
46+
await db.sync_queue.add({
47+
action: 'update',
48+
bookmark_id: id,
49+
payload: { is_archived: newArchived },
50+
timestamp: Date.now(),
51+
})
52+
browser.runtime.sendMessage({ type: 'sync-request' })
53+
},
3854
}

0 commit comments

Comments
 (0)