diff --git a/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx b/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx index b2abc1c71..b671049b8 100644 --- a/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx +++ b/src/equicordplugins/channelTabs/components/BookmarkContainer.tsx @@ -5,14 +5,14 @@ */ import { BaseText } from "@components/BaseText"; -import { BasicChannelTabsProps, Bookmark, BookmarkFolder, BookmarkProps, isBookmarkFolder, isTabSelected, navigateToBookmark, openedTabs, settings, switchChannel, useBookmarks } from "@equicordplugins/channelTabs/util"; +import { BasicChannelTabsProps, Bookmark, BookmarkFolder, BookmarkProps, getDiscordFolderIcon, isBookmarkFolder, isTabSelected, navigateToBookmark, openedTabs, settings, switchChannel, UseBookmarkMethods, useBookmarks } from "@equicordplugins/channelTabs/util"; import { CircleQuestionIcon, DiscoveryIcon, EnvelopeIcon, FriendsIcon, NitroIcon, QuestIcon, ShopIcon } from "@equicordplugins/channelTabs/util/icons"; import { classNameFactory } from "@utils/css"; import { getGuildAcronym, getIntlMessage } from "@utils/discord"; import { classes } from "@utils/misc"; import { closeModal, openModal } from "@utils/modal"; import { findComponentByCodeLazy } from "@webpack"; -import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore, useState } from "@webpack/common"; +import { Avatar, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, React, ReadStateStore, ReadStateUtils, SelectedChannelStore, SelectedGuildStore, TextInput, Tooltip, useDrag, useDrop, useEffect, useRef, UserStore, useState } from "@webpack/common"; import { NotificationDot } from "./ChannelTab"; import { BookmarkContextMenu, EditModal } from "./ContextMenus"; @@ -44,15 +44,28 @@ function FolderIcon({ fill }: { fill: string; }) { } function BookmarkIcon({ bookmark }: { bookmark: Bookmark | BookmarkFolder; }) { - if (isBookmarkFolder(bookmark)) return ( - - - - ); + if (isBookmarkFolder(bookmark)) { + const IconComponent = getDiscordFolderIcon(bookmark.iconName); + + const fill = bookmark.iconName === "CircleShieldIcon" ? "var(--background-base-low)" : bookmark.iconColor; + const customIconSize = 16; + + return IconComponent + ? + : + + ; + } // Handle special synthetic pages const { channelId } = bookmark; @@ -205,19 +218,22 @@ function BookmarkFolderOpenMenu(props: BookmarkProps) { ); } -function FolderBookmarkItem({ bookmark, folderIndex, bookmarkIndex, methods, isCurrentChannel }: { +function FolderBookmarkItem({ bookmark, bookmarks, folderIndex, bookmarkIndex, methods, isCurrentChannel, searchActive }: { bookmark: Bookmark; + bookmarks: (Bookmark | BookmarkFolder)[]; folderIndex: number; bookmarkIndex: number; - methods: any; + methods: UseBookmarkMethods; isCurrentChannel?: boolean; + searchActive?: boolean; }) { const { bookmarkNotificationDot } = settings.use(["bookmarkNotificationDot"]); const ref = useRef(null); // drag implementation for folder items - const [, drag] = useDrag(() => ({ + const [{ isDragging }, drag] = useDrag(() => ({ type: "vc_Bookmark", + canDrag: () => !searchActive, item: () => ({ index: bookmarkIndex, // index within folder folderIndex: folderIndex, // the folder its in @@ -227,17 +243,68 @@ function FolderBookmarkItem({ bookmark, folderIndex, bookmarkIndex, methods, isC collect: monitor => ({ isDragging: !!monitor.isDragging() }), - }), [bookmark, folderIndex, bookmarkIndex]); + }), [bookmark, folderIndex, bookmarkIndex, searchActive]); - // combine drag ref - drag(ref); + const [dropSide, setDropSide] = useState<"before" | "after" | null>(null); + + const [{ isOver: isReorderOver, canDrop: canReorderDrop }, drop] = useDrop(() => ({ + accept: "vc_Bookmark", + canDrop: item => !searchActive && item.isFromFolder && item.folderIndex === folderIndex, + hover: (item, monitor) => { + if (!ref.current || searchActive || !item.isFromFolder || item.folderIndex !== folderIndex) return; + + const dragIndex = item.index; + const hoverIndex = bookmarkIndex; + if (dragIndex === hoverIndex) { + setDropSide(null); + return; + } + + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2; + const clientOffset = monitor.getClientOffset(); + if (!clientOffset) return; + + const hoverClientX = clientOffset.x - hoverBoundingRect.left; + const nextDropSide = hoverClientX < hoverMiddleX ? "before" : "after"; + setDropSide(nextDropSide); + + if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX + || dragIndex > hoverIndex && hoverClientX > hoverMiddleX) { + return; + } + + const folder = bookmarks[folderIndex]; + if (!isBookmarkFolder(folder)) return; + + const newBookmarks = [...folder.bookmarks]; + const moved = newBookmarks.splice(dragIndex, 1)[0]; + newBookmarks.splice(hoverIndex, 0, moved); + methods.editBookmark(folderIndex, { bookmarks: newBookmarks }); + item.index = hoverIndex; + }, + collect: monitor => ({ + isOver: monitor.isOver({ shallow: true }), + canDrop: monitor.canDrop() + }) + }), [bookmarkIndex, bookmarks, folderIndex, methods, searchActive]); + + useEffect(() => { + if (!isReorderOver && dropSide) setDropSide(null); + }, [dropSide, isReorderOver]); + + drag(drop(ref)); return (
navigateToBookmark(bookmark)} onContextMenu={e => { @@ -258,7 +325,9 @@ function FolderBookmarkItem({ bookmark, folderIndex, bookmarkIndex, methods, isC modalKey={key} bookmark={bookmark} onSave={name => { - const folder = methods.bookmarks[folderIndex] as BookmarkFolder; + const folder = bookmarks[folderIndex]; + if (!isBookmarkFolder(folder)) return; + const newBookmarks = [...folder.bookmarks]; newBookmarks[bookmarkIndex].name = name; methods.editBookmark(folderIndex, { bookmarks: newBookmarks }); @@ -307,16 +376,17 @@ function FolderBookmarkItem({ bookmark, folderIndex, bookmarkIndex, methods, isC ); } -function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?: () => void; isCurrentChannel?: boolean; }) { - const { bookmarks, index, methods, isExpanded, onToggleFolder, isCurrentChannel } = props; +function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?: () => void; isCurrentChannel?: boolean; searchActive?: boolean; }) { + const { bookmarks, index, methods, isExpanded, onToggleFolder, isCurrentChannel, searchActive } = props; const bookmark = bookmarks[index]; const { bookmarkNotificationDot } = settings.use(["bookmarkNotificationDot"]); const ref = useRef(null); + const [dropSide, setDropSide] = useState<"before" | "after" | null>(null); - const [, drag] = useDrag(() => ({ + const [{ isDragging }, drag] = useDrag(() => ({ type: "vc_Bookmark", - canDrag: () => !isBookmarkFolder(bookmark), // Don't allow dragging folders + canDrag: () => !isBookmarkFolder(bookmark) && !searchActive, item: () => { // find current index dynamically instead of using stale closure const currentIndex = bookmarks.findIndex(b => @@ -334,22 +404,30 @@ function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder? collect: monitor => ({ isDragging: !!monitor.isDragging() }), - }), [bookmarks, bookmark, index]); + }), [bookmarks, bookmark, index, searchActive]); - const [, drop] = useDrop(() => ({ + const [{ isOver: isReorderOver, canDrop: canReorderDrop }, drop] = useDrop(() => ({ accept: "vc_Bookmark", + canDrop: item => !searchActive && !item.isFromFolder, hover: (item, monitor) => { - if (!ref.current) return; + if (!ref.current || searchActive || item.isFromFolder) return; const dragIndex = item.index; const hoverIndex = index; - if (dragIndex === hoverIndex) return; + if (dragIndex === hoverIndex) { + setDropSide(null); + return; + } - const hoverBoundingRect = ref.current?.getBoundingClientRect(); - const hoverMiddleX = - (hoverBoundingRect.right - hoverBoundingRect.left) / 2; + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2; const clientOffset = monitor.getClientOffset(); + if (!clientOffset) return; + const hoverClientX = clientOffset.x - hoverBoundingRect.left; + const nextDropSide = hoverClientX < hoverMiddleX ? "before" : "after"; + setDropSide(nextDropSide); + if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX || dragIndex > hoverIndex && hoverClientX > hoverMiddleX) { return; @@ -358,7 +436,15 @@ function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder? methods.moveDraggedBookmarks(dragIndex, hoverIndex); item.index = hoverIndex; }, - }), []); + collect: monitor => ({ + isOver: monitor.isOver({ shallow: true }), + canDrop: monitor.canDrop() + }), + }), [index, methods, searchActive]); + + useEffect(() => { + if (!isReorderOver && dropSide) setDropSide(null); + }, [dropSide, isReorderOver]); // separate drop zone for folders to accept tabs and bookmarks (prevents duplicates) const [{ isOver: isFolderDropTarget }, folderDrop] = useDrop(() => ({ @@ -416,9 +502,14 @@ function Bookmark(props: BookmarkProps & { isExpanded?: boolean; onToggleFolder?
{ @@ -485,6 +576,9 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI const { guildId, channelId, userId } = props; const [bookmarks, methods] = useBookmarks(userId); const [expandedFolders, setExpandedFolders] = useState>(new Set()); + const [isSearchOpen, setIsSearchOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const searchInputRef = useRef(null); // make setting reactive to changes const { bookmarksIndependentFromTabs } = settings.use(["bookmarksIndependentFromTabs"]); @@ -498,6 +592,14 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI activeTab.channelId === currentChannelId && (activeTab.guildId || "@me") === currentGuildId; + const normalizedSearchQuery = searchQuery.trim().toLowerCase(); + const searchActive = normalizedSearchQuery.length > 0; + + useEffect(() => { + if (!isSearchOpen) return; + searchInputRef.current?.focus(); + }, [isSearchOpen]); + const toggleFolder = (folderIndex: number) => { setExpandedFolders(prev => { const next = new Set(prev); @@ -523,8 +625,9 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI const [{ isOver }, dropRef] = useDrop(() => ({ accept: ["vc_ChannelTab", "vc_Bookmark"], + canDrop: () => !searchActive, drop: (item: any, monitor) => { - if (!bookmarks) return; + if (!bookmarks || searchActive) return; // check if a nested drop zone (like a folder) already handled this if (monitor.didDrop()) { @@ -557,7 +660,9 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI collect: monitor => ({ isOver: monitor.isOver({ shallow: true }) }) - }), [bookmarks, methods]); + }), [bookmarks, methods, searchActive]); + + const matchesSearch = (value?: string) => value?.toLowerCase().includes(normalizedSearchQuery) ?? false; return (
@@ -570,52 +675,121 @@ export default function BookmarkContainer(props: BasicChannelTabsProps & { userI } {Array.isArray(bookmarks) && bookmarks.length > 0 && bookmarks.flatMap((bookmark, i) => { - const items = [ + if (!isBookmarkFolder(bookmark)) { + if (searchActive && !matchesSearch(bookmark.name)) return []; + + return [ + + ]; + } + + const folderMatches = searchActive ? matchesSearch(bookmark.name) : false; + const visibleFolderBookmarks = bookmark.bookmarks + .map((childBookmark, childIndex) => ({ childBookmark, childIndex })) + .filter(({ childBookmark }) => !searchActive || folderMatches || matchesSearch(childBookmark.name)); + + if (searchActive && !folderMatches && !visibleFolderBookmarks.length) return []; + + const isExpanded = searchActive + ? visibleFolderBookmarks.length > 0 + : expandedFolders.has(i); + + const folderItems = [ toggleFolder(i)} - isCurrentChannel={ - bookmarksIndependentFromTabs && - !channelMatchesActiveTab && ( - !isBookmarkFolder(bookmark) && - bookmark.channelId === currentChannelId && - (bookmark.guildId || "@me") === currentGuildId - ) - } + searchActive={searchActive} + isExpanded={isExpanded} + onToggleFolder={searchActive ? undefined : () => toggleFolder(i)} + isCurrentChannel={false} /> ]; - // if folder is expanded, render its bookmarks - if (isBookmarkFolder(bookmark) && expandedFolders.has(i)) { - bookmark.bookmarks.forEach((childBookmark, childIndex) => { - items.push( + if (isExpanded) { + visibleFolderBookmarks.forEach(({ childBookmark, childIndex }) => { + folderItems.push( ); }); } - return items; + return folderItems; }) } +
+
+ { + if (!searchQuery.trim()) setIsSearchOpen(false); + }} + onKeyDown={e => { + if (e.key !== "Escape") return; + if (searchQuery.trim()) { + setSearchQuery(""); + } else { + setIsSearchOpen(false); + } + }} + /> +
+ + {p => } + +
+ {p => + ))} +
+ + + + + + ); +} + +function FolderAppearanceFields({ + name, + setName, + color, + setColor, + iconName, + setIconName, + placeholder = "Folder" +}: { + name: string, + setName: (value: string) => void, + color: string, + setColor: (value: string) => void, + iconName?: string, + setIconName: (value?: string) => void, + placeholder?: string; +}) { + return <> + Folder Name + + Folder Color +
+ setColor(intToColor(value))} + showEyeDropper={false} + /> +
+ Folder Icon + + + {iconName && } + ; +} + export function EditModal({ modalProps, modalKey, bookmark, onSave }: { modalProps: ModalProps, modalKey: string, bookmark: Bookmark | BookmarkFolder, - onSave: (name: string, color: string) => void; + onSave: (name: string, color: string, iconName?: string) => void; }) { const [name, setName] = useState(bookmark.name); - const [color, setColor] = useState(isBookmarkFolder(bookmark) ? bookmark.iconColor : undefined); + const [color, setColor] = useState(isBookmarkFolder(bookmark) ? normalizeFolderColor(bookmark.iconColor) : bookmarkFolderColors.Black); + const [iconName, setIconName] = useState(isBookmarkFolder(bookmark) ? bookmark.iconName : undefined); const placeholder = bookmarkPlaceholderName(bookmark); return ( @@ -53,31 +279,28 @@ export function EditModal({ modalProps, modalKey, bookmark, onSave }: { Edit Bookmark - Bookmark Name - setName(v)} - /> - {isBookmarkFolder(bookmark) && <> - Folder Color -