"use client"; import React, { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import { ActionButton, ActionButtonWithTooltip, } from "@/components/ui/action-button"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import { useToast } from "@/components/ui/use-toast"; import useBulkActionsStore from "@/lib/bulkActions"; import { useTranslation } from "@/lib/i18n/client"; import { CheckCheck, FileDown, Hash, Link, List, Pencil, RotateCw, Trash2, X, } from "lucide-react"; import { useDeleteBookmark, useRecrawlBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; import { limitConcurrency } from "@karakeep/shared/concurrency"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import BulkManageListsModal from "./bookmarks/BulkManageListsModal"; import BulkTagModal from "./bookmarks/BulkTagModal"; import { ArchivedActionIcon, FavouritedActionIcon } from "./bookmarks/icons"; const MAX_CONCURRENT_BULK_ACTIONS = 50; export default function BulkBookmarksAction() { const { t } = useTranslation(); const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore(); const setIsBulkEditEnabled = useBulkActionsStore( (state) => state.setIsBulkEditEnabled, ); const selectAllBookmarks = useBulkActionsStore((state) => state.selectAll); const unSelectAllBookmarks = useBulkActionsStore( (state) => state.unSelectAll, ); const isEverythingSelected = useBulkActionsStore( (state) => state.isEverythingSelected, ); const { toast } = useToast(); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [manageListsModal, setManageListsModalOpen] = useState(false); const [bulkTagModal, setBulkTagModalOpen] = useState(false); const pathname = usePathname(); const [currentPathname, setCurrentPathname] = useState(""); // Reset bulk edit state when the route changes useEffect(() => { if (pathname !== currentPathname) { setCurrentPathname(pathname); setIsBulkEditEnabled(false); } }, [pathname, currentPathname]); const onError = () => { toast({ variant: "destructive", title: "Something went wrong", description: "There was a problem with your request.", }); }; const deleteBookmarkMutator = useDeleteBookmark({ onSuccess: () => { setIsBulkEditEnabled(false); }, onError, }); const updateBookmarkMutator = useUpdateBookmark({ onSuccess: () => { setIsBulkEditEnabled(false); }, onError, }); const recrawlBookmarkMutator = useRecrawlBookmark({ onSuccess: () => { setIsBulkEditEnabled(false); }, onError, }); interface UpdateBookmarkProps { favourited?: boolean; archived?: boolean; } const recrawlBookmarks = async (archiveFullPage: boolean) => { const links = selectedBookmarks.filter( (item) => item.content.type === BookmarkTypes.LINK, ); await Promise.all( limitConcurrency( links.map( (item) => () => recrawlBookmarkMutator.mutateAsync({ bookmarkId: item.id, archiveFullPage, }), ), MAX_CONCURRENT_BULK_ACTIONS, ), ); toast({ description: `${links.length} bookmarks will be ${archiveFullPage ? "re-crawled and archived!" : "refreshed!"}`, }); }; function isClipboardAvailable() { if (typeof window === "undefined") { return false; } return window && window.navigator && window.navigator.clipboard; } const copyLinks = async () => { if (!isClipboardAvailable()) { toast({ description: `Copying is only available over https`, }); return; } const copyString = selectedBookmarks .map((item) => { return item.content.type === BookmarkTypes.LINK && item.content.url; }) .filter(Boolean) .join("\n"); await navigator.clipboard.writeText(copyString); toast({ description: `Added ${selectedBookmarks.length} bookmark links into the clipboard!`, }); }; const updateBookmarks = async ({ favourited, archived, }: UpdateBookmarkProps) => { await Promise.all( limitConcurrency( selectedBookmarks.map( (item) => () => updateBookmarkMutator.mutateAsync({ bookmarkId: item.id, favourited, archived, }), ), MAX_CONCURRENT_BULK_ACTIONS, ), ); toast({ description: `${selectedBookmarks.length} bookmarks have been updated!`, }); }; const deleteBookmarks = async () => { await Promise.all( limitConcurrency( selectedBookmarks.map( (item) => () => deleteBookmarkMutator.mutateAsync({ bookmarkId: item.id }), ), MAX_CONCURRENT_BULK_ACTIONS, ), ); toast({ description: `${selectedBookmarks.length} bookmarks have been deleted!`, }); setIsDeleteDialogOpen(false); }; const alreadyFavourited = selectedBookmarks.length && selectedBookmarks.every((item) => item.favourited === true); const alreadyArchived = selectedBookmarks.length && selectedBookmarks.every((item) => item.archived === true); const actionList = [ { name: isClipboardAvailable() ? t("actions.copy_link") : "Copying is only available over https", icon: , action: () => copyLinks(), isPending: false, hidden: !isBulkEditEnabled, }, { name: t("actions.add_to_list"), icon: , action: () => setManageListsModalOpen(true), isPending: false, hidden: !isBulkEditEnabled, }, { name: t("actions.edit_tags"), icon: , action: () => setBulkTagModalOpen(true), isPending: false, hidden: !isBulkEditEnabled, }, { name: alreadyFavourited ? t("actions.unfavorite") : t("actions.favorite"), icon: , action: () => updateBookmarks({ favourited: !alreadyFavourited }), isPending: updateBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: alreadyArchived ? t("actions.unarchive") : t("actions.archive"), icon: , action: () => updateBookmarks({ archived: !alreadyArchived }), isPending: updateBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: t("actions.download_full_page_archive"), icon: , action: () => recrawlBookmarks(true), isPending: recrawlBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: t("actions.refresh"), icon: , action: () => recrawlBookmarks(false), isPending: recrawlBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { name: t("actions.delete"), icon: , action: () => setIsDeleteDialogOpen(true), hidden: !isBulkEditEnabled, }, { name: isEverythingSelected() ? t("actions.unselect_all") : t("actions.select_all"), icon: (

( {selectedBookmarks.length} )

), action: () => isEverythingSelected() ? unSelectAllBookmarks() : selectAllBookmarks(), alwaysEnable: true, hidden: !isBulkEditEnabled, }, { name: t("actions.close_bulk_edit"), icon: , action: () => setIsBulkEditEnabled(false), alwaysEnable: true, hidden: !isBulkEditEnabled, }, { name: t("actions.bulk_edit"), icon: , action: () => setIsBulkEditEnabled(true), alwaysEnable: true, hidden: isBulkEditEnabled, }, ]; return (
Are you sure you want to delete these bookmarks?

} actionButton={() => ( deleteBookmarks()} > {t("actions.delete")} )} /> b.id)} open={manageListsModal} setOpen={setManageListsModalOpen} /> b.id)} open={bulkTagModal} setOpen={setBulkTagModalOpen} />
{actionList.map( ({ name, icon: Icon, action, isPending, hidden, alwaysEnable }) => ( {Icon} ), )}
); }