Files
flotilla/src/app/components/BookmarkSidebar.svelte
T

248 lines
7.4 KiB
Svelte

<script lang="ts">
import {type TrustedEvent} from "@welshman/util"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
import Folder from "@assets/icons/folder.svg?dataurl"
import Add from "@assets/icons/add.svg?dataurl"
type BookmarkList = {
key: string
label: string
count: number
event?: TrustedEvent
}
type Props = {
lists: BookmarkList[]
selectedKey: string
totalCount: number
onOpenManager: () => void
onSelect: (key: string) => void
onRename: (key: string, label: string, nextLabel: string) => Promise<void>
onDelete: (key: string, label: string) => Promise<void>
}
const {lists, selectedKey, totalCount, onOpenManager, onSelect, onRename, onDelete}: Props =
$props()
let menuOpen = $state(false)
let menuX = $state(0)
let menuY = $state(0)
let menuListKey = $state("")
let menuListLabel = $state("")
let listDialogMode: "rename" | "delete" | undefined = $state()
let dialogListKey = $state("")
let dialogListLabel = $state("")
let nextListLabel = $state("")
let dialogPending = $state(false)
const closeMenu = () => {
menuOpen = false
}
const openListMenu = (event: MouseEvent, key: string, label: string) => {
event.preventDefault()
menuOpen = true
menuX = event.clientX
menuY = event.clientY
menuListKey = key
menuListLabel = label
}
const openMenuList = () => {
closeMenu()
onSelect(menuListKey)
}
const openRenameDialog = () => {
closeMenu()
listDialogMode = "rename"
dialogListKey = menuListKey
dialogListLabel = menuListLabel
nextListLabel = menuListLabel
}
const openDeleteDialog = () => {
closeMenu()
listDialogMode = "delete"
dialogListKey = menuListKey
dialogListLabel = menuListLabel
nextListLabel = menuListLabel
}
const closeDialog = (force = false) => {
if (!force && dialogPending) {
return
}
listDialogMode = undefined
dialogListKey = ""
dialogListLabel = ""
nextListLabel = ""
}
const submitRename = async () => {
if (listDialogMode !== "rename") {
return
}
dialogPending = true
try {
await onRename(dialogListKey, dialogListLabel, nextListLabel)
closeDialog(true)
} finally {
dialogPending = false
}
}
const submitDelete = async () => {
if (listDialogMode !== "delete") {
return
}
dialogPending = true
try {
await onDelete(dialogListKey, dialogListLabel)
closeDialog(true)
} finally {
dialogPending = false
}
}
const handleWindowKeydown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeMenu()
closeDialog()
}
}
</script>
<svelte:window onclick={closeMenu} onkeydown={handleWindowKeydown} />
<SecondaryNav class="relative">
<SecondaryNavSection>
<SecondaryNavHeader>
<span class="flex items-center gap-2 uppercase tracking-wide">
<Icon icon={Bookmark} />
Bookmarks
</span>
<span class="badge badge-sm badge-neutral">{totalCount}</span>
</SecondaryNavHeader>
<div
class="flex items-center justify-between px-1 pt-1 text-xs uppercase tracking-wide opacity-70">
<span>My Lists</span>
<Button class="btn btn-ghost btn-xs btn-square" onclick={onOpenManager}>
<Icon size={3.5} icon={Add} />
</Button>
</div>
</SecondaryNavSection>
<div class="col-2 gap-2 overflow-y-auto px-2 pb-2">
{#each lists as list (list.key)}
<div
role="button"
tabindex="-1"
oncontextmenu={event => openListMenu(event, list.key, list.label)}>
<Link href={`/bookmarks?list=${encodeURIComponent(list.key)}`}>
<div
class={`card2 card2-sm bg-alt col-2 gap-1 transition-colors hover:bg-base-100 ${selectedKey === list.key ? "bg-base-100" : ""}`}>
<div class="flex items-center justify-between gap-2">
<p class="truncate font-medium">
<span class="inline-flex items-center gap-2">
<Icon size={4} icon={list.key.startsWith("10003:") ? Bookmark : Folder} />
{list.label}
</span>
</p>
<span class="badge badge-sm badge-neutral">{list.count}</span>
</div>
</div>
</Link>
</div>
{:else}
<div class="card2 card2-sm bg-alt text-sm opacity-70">No lists found yet.</div>
{/each}
</div>
{#if menuOpen}
<div class="fixed inset-0 z-popover" role="presentation">
<div
class="menu rounded-box bg-base-100 shadow-xl"
role="menu"
style={`position: fixed; left: ${menuX}px; top: ${menuY}px; min-width: 11rem;`}>
<li>
<Button class="justify-start" onclick={openMenuList}>Open List</Button>
</li>
{#if menuListKey !== selectedKey}
<li>
<Button class="justify-start" onclick={openRenameDialog}>Rename List</Button>
</li>
<li>
<Button class="justify-start text-error" onclick={openDeleteDialog}>Delete List</Button>
</li>
{/if}
</div>
</div>
{/if}
{#if listDialogMode}
<div class="fixed inset-0 z-modal bg-black/45 p-4 md:p-6">
<button
type="button"
class="absolute inset-0"
aria-label="Close dialog"
onclick={() => closeDialog()}></button>
<div class="center relative h-full w-full">
<div
class="card2 bg-base-100 w-full max-w-md gap-4 p-5 shadow-2xl"
role="dialog"
aria-modal="true"
tabindex="-1">
{#if listDialogMode === "rename"}
<div class="col-2 gap-1">
<h3 class="text-lg font-semibold">Rename List</h3>
<p class="text-sm opacity-70">Choose a new name for "{dialogListLabel}".</p>
</div>
<label class="input input-bordered flex items-center gap-2">
<Icon icon={Folder} />
<input bind:value={nextListLabel} class="grow" type="text" placeholder="List name" />
</label>
<div class="flex items-center justify-end gap-2">
<Button class="btn btn-ghost" onclick={() => closeDialog()} disabled={dialogPending}
>Cancel</Button>
<Button
class="btn btn-neutral"
onclick={submitRename}
disabled={dialogPending || !nextListLabel.trim()}>
Save
</Button>
</div>
{:else}
<div class="col-2 gap-1">
<h3 class="text-lg font-semibold">Delete List?</h3>
<p class="text-sm opacity-70">
This will remove "{dialogListLabel}" and cannot be undone.
</p>
</div>
<div class="flex items-center justify-end gap-2">
<Button class="btn btn-ghost" onclick={() => closeDialog()} disabled={dialogPending}
>Cancel</Button>
<Button class="btn btn-error" onclick={submitDelete} disabled={dialogPending}
>Delete</Button>
</div>
{/if}
</div>
</div>
</div>
{/if}
</SecondaryNav>