From 65ea4c509177ed666c7bc2e5b367d7e22d54367d Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:41:49 -0600 Subject: [PATCH] add edit group order --- src/assets/locales/en.json | 19 +- src/backend/accounts/groupOrder.ts | 29 ++ src/components/buttons/EditButtonWithText.tsx | 43 ++ src/components/form/GroupDropdown.tsx | 15 +- src/hooks/auth/useAuth.ts | 13 +- src/hooks/auth/useAuthData.ts | 5 + src/pages/parts/home/BookmarksCarousel.tsx | 423 +++++++++++++----- src/pages/parts/home/BookmarksPart.tsx | 302 ++++++++++--- src/stores/bookmarks/index.ts | 43 ++ 9 files changed, 722 insertions(+), 170 deletions(-) create mode 100644 src/backend/accounts/groupOrder.ts create mode 100644 src/components/buttons/EditButtonWithText.tsx diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index e3e324ec..a8d5c598 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -240,7 +240,24 @@ }, "home": { "bookmarks": { - "sectionTitle": "Bookmarks" + "sectionTitle": "Bookmarks", + "groups": { + "dropdown": { + "placeholderButton": "Add to group", + "empty": "No groups yet", + "addButton": "Add", + "removeFromGroup": "Remove from group", + "removeAll": "Remove all" + }, + "reorder": { + "button": "Reorder", + "done": "Done", + "title": "Edit Group Order", + "description": "Drag and drop to reorder your bookmark groups.", + "cancel": "Cancel", + "save": "Save" + } + } }, "continueWatching": { "sectionTitle": "Continue Watching..." diff --git a/src/backend/accounts/groupOrder.ts b/src/backend/accounts/groupOrder.ts new file mode 100644 index 00000000..a502a5d6 --- /dev/null +++ b/src/backend/accounts/groupOrder.ts @@ -0,0 +1,29 @@ +import { ofetch } from "ofetch"; + +import { getAuthHeaders } from "@/backend/accounts/auth"; +import { AccountWithToken } from "@/stores/auth"; + +export interface GroupOrderResponse { + groupOrder: string[]; +} + +export function updateGroupOrder( + url: string, + account: AccountWithToken, + groupOrder: string[], +) { + return ofetch(`/users/${account.userId}/group-order`, { + method: "PUT", + body: groupOrder, + baseURL: url, + headers: getAuthHeaders(account.token), + }); +} + +export function getGroupOrder(url: string, account: AccountWithToken) { + return ofetch(`/users/${account.userId}/group-order`, { + method: "GET", + baseURL: url, + headers: getAuthHeaders(account.token), + }); +} diff --git a/src/components/buttons/EditButtonWithText.tsx b/src/components/buttons/EditButtonWithText.tsx new file mode 100644 index 00000000..860120e4 --- /dev/null +++ b/src/components/buttons/EditButtonWithText.tsx @@ -0,0 +1,43 @@ +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { useCallback, useRef } from "react"; +import { useTranslation } from "react-i18next"; + +export interface EditButtonWithTextProps { + editing: boolean; + onEdit?: (editing: boolean) => void; + id?: string; + text: string; + secondaryText?: string; +} + +export function EditButtonWithText(props: EditButtonWithTextProps) { + const { t } = useTranslation(); + const [parent] = useAutoAnimate(); + const buttonRef = useRef(null); + + const onClick = useCallback(() => { + props.onEdit?.(!props.editing); + }, [props]); + + return ( + + ); +} diff --git a/src/components/form/GroupDropdown.tsx b/src/components/form/GroupDropdown.tsx index 83181907..59cff194 100644 --- a/src/components/form/GroupDropdown.tsx +++ b/src/components/form/GroupDropdown.tsx @@ -1,3 +1,4 @@ +import { t } from "i18next"; import React, { useState } from "react"; import { Icon, Icons } from "@/components/Icon"; @@ -78,7 +79,9 @@ export function GroupDropdown({ })} ) : ( - Add to group + + {t("home.bookmarks.groups.dropdown.placeholderButton")} + )} {groups.length === 0 && !showInput && ( -
No groups
+
+ {t("home.bookmarks.groups.dropdown.empty")} +
)} {groups.map((group) => { const { icon, name } = parseGroupString(group); @@ -137,7 +142,7 @@ export function GroupDropdown({ disabled={!newGroup.trim()} style={{ flexShrink: 0 }} > - Add + {t("home.bookmarks.groups.dropdown.addButton")} {newGroup.trim().length > 0 && ( @@ -167,7 +172,7 @@ export function GroupDropdown({ {currentGroups.length > 0 && (
- Remove from group: + {t("home.bookmarks.groups.dropdown.removeFromGroup")}
{currentGroups.map((group) => { @@ -190,7 +195,7 @@ export function GroupDropdown({ className="ml-2 text-xs text-red-400 underline" onClick={() => onRemoveGroup()} > - Remove all + {t("home.bookmarks.groups.dropdown.removeAll")}
diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts index 3ce49163..0bd48018 100644 --- a/src/hooks/auth/useAuth.ts +++ b/src/hooks/auth/useAuth.ts @@ -9,6 +9,7 @@ import { keysFromMnemonic, signChallenge, } from "@/backend/accounts/crypto"; +import { getGroupOrder } from "@/backend/accounts/groupOrder"; import { importBookmarks, importProgress } from "@/backend/accounts/import"; import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login"; import { progressMediaItemToInputs } from "@/backend/accounts/progress"; @@ -180,13 +181,21 @@ export function useAuth() { throw err; } - const [bookmarks, progress, settings] = await Promise.all([ + const [bookmarks, progress, settings, groupOrder] = await Promise.all([ getBookmarks(backendUrl, account), getProgress(backendUrl, account), getSettings(backendUrl, account), + getGroupOrder(backendUrl, account), ]); - syncData(user.user, user.session, progress, bookmarks, settings); + syncData( + user.user, + user.session, + progress, + bookmarks, + settings, + groupOrder, + ); }, [backendUrl, syncData, logout], ); diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts index b1278168..9a2a24bf 100644 --- a/src/hooks/auth/useAuthData.ts +++ b/src/hooks/auth/useAuthData.ts @@ -96,10 +96,15 @@ export function useAuthData() { progress: ProgressResponse[], bookmarks: BookmarkResponse[], settings: SettingsResponse, + groupOrder: { groupOrder: string[] }, ) => { replaceBookmarks(bookmarkResponsesToEntries(bookmarks)); replaceItems(progressResponsesToEntries(progress)); + if (groupOrder?.groupOrder) { + useBookmarkStore.getState().setGroupOrder(groupOrder.groupOrder); + } + if (settings.applicationLanguage) { setAppLanguage(settings.applicationLanguage); } diff --git a/src/pages/parts/home/BookmarksCarousel.tsx b/src/pages/parts/home/BookmarksCarousel.tsx index 3f6f5b23..0e942540 100644 --- a/src/pages/parts/home/BookmarksCarousel.tsx +++ b/src/pages/parts/home/BookmarksCarousel.tsx @@ -1,13 +1,20 @@ import React, { useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { Button } from "@/components/buttons/Button"; import { EditButton } from "@/components/buttons/EditButton"; +import { EditButtonWithText } from "@/components/buttons/EditButtonWithText"; +import { Item, SortableList } from "@/components/form/SortableList"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; +import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; import { UserIcon, UserIcons } from "@/components/UserIcon"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useIsMobile } from "@/hooks/useIsMobile"; import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons"; +import { useAuthStore } from "@/stores/auth"; import { useBookmarkStore } from "@/stores/bookmarks"; import { useProgressStore } from "@/stores/progress"; import { MediaItem } from "@/utils/mediaTypes"; @@ -53,6 +60,14 @@ export function BookmarksCarousel({ const [editing, setEditing] = useState(false); const removeBookmark = useBookmarkStore((s) => s.removeBookmark); const pressTimerRef = useRef(null); + const backendUrl = useBackendUrl(); + const account = useAuthStore((s) => s.account); + + // Group order editing state + const groupOrder = useBookmarkStore((s) => s.groupOrder); + const setGroupOrder = useBookmarkStore((s) => s.setGroupOrder); + const editOrderModal = useModal("bookmark-edit-order-carousel"); + const [tempGroupOrder, setTempGroupOrder] = useState([]); const { isMobile } = useIsMobile(); @@ -121,6 +136,116 @@ export function BookmarksCarousel({ return { groupedItems: grouped, regularItems: regular }; }, [items, bookmarks, progressItems]); + // group sorting + const allGroups = useMemo(() => { + const groups = new Set(); + + Object.values(bookmarks).forEach((bookmark) => { + if (Array.isArray(bookmark.group)) { + bookmark.group.forEach((group) => groups.add(group)); + } + }); + + groups.add("bookmarks"); + + return Array.from(groups); + }, [bookmarks]); + + const sortableItems = useMemo(() => { + const currentOrder = editOrderModal.isShown ? tempGroupOrder : groupOrder; + + if (currentOrder.length === 0) { + return allGroups.map((group) => { + const { name } = parseGroupString(group); + return { + id: group, + name: group === "bookmarks" ? t("home.bookmarks.sectionTitle") : name, + } as Item; + }); + } + + const orderMap = new Map( + currentOrder.map((group, index) => [group, index]), + ); + const sortedGroups = allGroups.sort((groupA, groupB) => { + const orderA = orderMap.has(groupA) + ? orderMap.get(groupA)! + : Number.MAX_SAFE_INTEGER; + const orderB = orderMap.has(groupB) + ? orderMap.get(groupB)! + : Number.MAX_SAFE_INTEGER; + return orderA - orderB; + }); + + return sortedGroups.map((group) => { + const { name } = parseGroupString(group); + return { + id: group, + name: group === "bookmarks" ? t("home.bookmarks.sectionTitle") : name, + } as Item; + }); + }, [allGroups, t, editOrderModal.isShown, tempGroupOrder, groupOrder]); + + // Create a unified list of sections including both grouped and regular bookmarks + const sortedSections = useMemo(() => { + const sections: Array<{ + type: "grouped" | "regular"; + group?: string; + items: MediaItem[]; + }> = []; + + // Create a combined map of all sections (grouped + regular) + const allSections = new Map(); + + // Add grouped sections + Object.entries(groupedItems).forEach(([group, groupItems]) => { + allSections.set(group, groupItems); + }); + + // Add regular bookmarks as "bookmarks" group + if (regularItems.length > 0) { + allSections.set("bookmarks", regularItems); + } + + // Sort sections based on group order + if (groupOrder.length === 0) { + // No order set, use default order + allSections.forEach((sectionItems, group) => { + if (group === "bookmarks") { + sections.push({ type: "regular", items: sectionItems }); + } else { + sections.push({ type: "grouped", group, items: sectionItems }); + } + }); + } else { + // Use the saved order + const orderMap = new Map( + groupOrder.map((group, index) => [group, index]), + ); + + Array.from(allSections.entries()) + .sort(([groupA], [groupB]) => { + const orderA = orderMap.has(groupA) + ? orderMap.get(groupA)! + : Number.MAX_SAFE_INTEGER; + const orderB = orderMap.has(groupB) + ? orderMap.get(groupB)! + : Number.MAX_SAFE_INTEGER; + return orderA - orderB; + }) + .forEach(([group, sectionItems]) => { + if (group === "bookmarks") { + sections.push({ type: "regular", items: sectionItems }); + } else { + sections.push({ type: "grouped", group, items: sectionItems }); + } + }); + } + + return sections; + }, [groupedItems, regularItems, groupOrder]); + // kill me + const handleWheel = (e: React.WheelEvent) => { if (isScrolling) return; isScrolling = true; @@ -171,6 +296,37 @@ export function BookmarksCarousel({ } }; + const handleEditGroupOrder = () => { + // Initialize with current order or default order + if (groupOrder.length === 0) { + const defaultOrder = allGroups.map((group) => group); + setTempGroupOrder(defaultOrder); + } else { + setTempGroupOrder([...groupOrder]); + } + editOrderModal.show(); + }; + + const handleReorderClick = () => { + handleEditGroupOrder(); + // Keep editing state active by setting it to true + setEditing(true); + }; + + const handleCancelOrder = () => { + editOrderModal.hide(); + }; + + const handleSaveOrderClick = () => { + setGroupOrder(tempGroupOrder); + editOrderModal.hide(); + + // Save to backend + if (backendUrl && account) { + useBookmarkStore.getState().saveGroupOrderToBackend(backendUrl, account); + } + }; + const categorySlug = "bookmarks"; const SKELETON_COUNT = 10; @@ -179,104 +335,49 @@ export function BookmarksCarousel({ return ( <> {/* Grouped Bookmarks Carousels */} - {Object.entries(groupedItems).map(([group, groupItems]) => { - const { icon, name } = parseGroupString(group); - return ( -
- - - - } - className="ml-4 md:ml-12 mt-2 -mb-5" - > -
- -
-
-
-
{ - carouselRefs.current[group] = el; - }} - onWheel={handleWheel} + {sortedSections.map((section) => { + if (section.type === "grouped") { + const { icon, name } = parseGroupString(section.group || ""); + return ( +
+ + + + } + className="ml-4 md:ml-12 mt-2 -mb-5" > -
- - {groupItems.map((media) => ( -
) => - e.preventDefault() - } - onTouchStart={handleTouchStart} - onTouchEnd={handleTouchEnd} - onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} - className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto" - > - removeBookmark(media.id)} +
+ {editing && allGroups.length > 1 && ( + -
- ))} + )} + +
+ +
+
{ + carouselRefs.current[section.group || "bookmarks"] = el; + }} + onWheel={handleWheel} + > +
-
-
- - {!isMobile && ( - - )} -
-
- ); - })} - - {/* Regular Bookmarks Carousel */} - {regularItems.length > 0 && ( - <> - -
- -
-
-
-
{ - carouselRefs.current[categorySlug] = el; - }} - onWheel={handleWheel} - > -
- - {regularItems.length > 0 - ? regularItems.map((media) => ( + {section.items.map((media) => (
removeBookmark(media.id)} />
- )) - : Array.from({ length: SKELETON_COUNT }).map(() => ( - ))} -
-
+
+
- {!isMobile && ( - - )} + {!isMobile && ( + + )} +
+
+ ); + } // regular items + return ( +
+ +
+ {editing && allGroups.length > 1 && ( + + )} + +
+
+
+
{ + carouselRefs.current[categorySlug] = el; + }} + onWheel={handleWheel} + > +
+ + {section.items.length > 0 + ? section.items.map((media) => ( +
) => + e.preventDefault() + } + onTouchStart={handleTouchStart} + onTouchEnd={handleTouchEnd} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} + className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto" + > + removeBookmark(media.id)} + /> +
+ )) + : Array.from({ length: SKELETON_COUNT }).map(() => ( + + ))} + +
+
+ + {!isMobile && ( + + )} +
- - )} + ); + })} + + {/* Edit Order Modal */} + + + + {t("home.bookmarks.groups.reorder.title")} + + + {t("home.bookmarks.groups.reorder.description")} + +
+ { + const newOrder = newItems.map((item) => item.id); + setTempGroupOrder(newOrder); + }} + /> +
+
+ + +
+
+
); } diff --git a/src/pages/parts/home/BookmarksPart.tsx b/src/pages/parts/home/BookmarksPart.tsx index de4602b7..7409deba 100644 --- a/src/pages/parts/home/BookmarksPart.tsx +++ b/src/pages/parts/home/BookmarksPart.tsx @@ -2,12 +2,19 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { Button } from "@/components/buttons/Button"; import { EditButton } from "@/components/buttons/EditButton"; +import { EditButtonWithText } from "@/components/buttons/EditButtonWithText"; +import { Item, SortableList } from "@/components/form/SortableList"; import { Icons } from "@/components/Icon"; import { SectionHeading } from "@/components/layout/SectionHeading"; import { MediaGrid } from "@/components/media/MediaGrid"; import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; +import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; import { UserIcon, UserIcons } from "@/components/UserIcon"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; +import { useAuthStore } from "@/stores/auth"; import { useBookmarkStore } from "@/stores/bookmarks"; import { useProgressStore } from "@/stores/progress"; import { MediaItem } from "@/utils/mediaTypes"; @@ -35,9 +42,15 @@ export function BookmarksPart({ const { t } = useTranslation(); const progressItems = useProgressStore((s) => s.items); const bookmarks = useBookmarkStore((s) => s.bookmarks); + const groupOrder = useBookmarkStore((s) => s.groupOrder); + const setGroupOrder = useBookmarkStore((s) => s.setGroupOrder); const removeBookmark = useBookmarkStore((s) => s.removeBookmark); const [editing, setEditing] = useState(false); const [gridRef] = useAutoAnimate(); + const editOrderModal = useModal("bookmark-edit-order"); + const [tempGroupOrder, setTempGroupOrder] = useState([]); + const backendUrl = useBackendUrl(); + const account = useAuthStore((s) => s.account); const pressTimerRef = useRef(null); @@ -99,6 +112,109 @@ export function BookmarksPart({ return { groupedItems: grouped, regularItems: regular }; }, [items, bookmarks, progressItems]); + // group sorting + const allGroups = useMemo(() => { + const groups = new Set(); + + Object.values(bookmarks).forEach((bookmark) => { + if (Array.isArray(bookmark.group)) { + bookmark.group.forEach((group) => groups.add(group)); + } + }); + + groups.add("bookmarks"); + + return Array.from(groups); + }, [bookmarks]); + + const sortableItems = useMemo(() => { + const currentOrder = editOrderModal.isShown ? tempGroupOrder : groupOrder; + + if (currentOrder.length === 0) { + return allGroups.map((group) => { + const { name } = parseGroupString(group); + return { + id: group, + name: group === "bookmarks" ? t("home.bookmarks.sectionTitle") : name, + } as Item; + }); + } + + const orderMap = new Map( + currentOrder.map((group, index) => [group, index]), + ); + const sortedGroups = allGroups.sort((groupA, groupB) => { + const orderA = orderMap.has(groupA) + ? orderMap.get(groupA)! + : Number.MAX_SAFE_INTEGER; + const orderB = orderMap.has(groupB) + ? orderMap.get(groupB)! + : Number.MAX_SAFE_INTEGER; + return orderA - orderB; + }); + + return sortedGroups.map((group) => { + const { name } = parseGroupString(group); + return { + id: group, + name: group === "bookmarks" ? t("home.bookmarks.sectionTitle") : name, + } as Item; + }); + }, [allGroups, t, editOrderModal.isShown, tempGroupOrder, groupOrder]); + + const sortedSections = useMemo(() => { + const sections: Array<{ + type: "grouped" | "regular"; + group?: string; + items: MediaItem[]; + }> = []; + + const allSections = new Map(); + + Object.entries(groupedItems).forEach(([group, groupItems]) => { + allSections.set(group, groupItems); + }); + + if (regularItems.length > 0) { + allSections.set("bookmarks", regularItems); + } + + if (groupOrder.length === 0) { + allSections.forEach((sectionItems, group) => { + if (group === "bookmarks") { + sections.push({ type: "regular", items: sectionItems }); + } else { + sections.push({ type: "grouped", group, items: sectionItems }); + } + }); + } else { + const orderMap = new Map( + groupOrder.map((group, index) => [group, index]), + ); + + Array.from(allSections.entries()) + .sort(([groupA], [groupB]) => { + const orderA = orderMap.has(groupA) + ? orderMap.get(groupA)! + : Number.MAX_SAFE_INTEGER; + const orderB = orderMap.has(groupB) + ? orderMap.get(groupB)! + : Number.MAX_SAFE_INTEGER; + return orderA - orderB; + }) + .forEach(([group, sectionItems]) => { + if (group === "bookmarks") { + sections.push({ type: "regular", items: sectionItems }); + } else { + sections.push({ type: "grouped", group, items: sectionItems }); + } + }); + } + + return sections; + }, [groupedItems, regularItems, groupOrder]); + // kill me + useEffect(() => { onItemsChange(items.length > 0); }, [items, onItemsChange]); @@ -135,31 +251,122 @@ export function BookmarksPart({ } }; + const handleEditGroupOrder = () => { + // Initialize with current order or default order + if (groupOrder.length === 0) { + const defaultOrder = allGroups.map((group) => group); + setTempGroupOrder(defaultOrder); + } else { + setTempGroupOrder([...groupOrder]); + } + editOrderModal.show(); + }; + + const handleReorderClick = () => { + handleEditGroupOrder(); + // Keep editing state active by setting it to true + setEditing(true); + }; + + const handleCancelOrder = () => { + editOrderModal.hide(); + }; + + const handleSaveOrderClick = () => { + setGroupOrder(tempGroupOrder); + editOrderModal.hide(); + + // Save to backend + if (backendUrl && account) { + useBookmarkStore.getState().saveGroupOrderToBackend(backendUrl, account); + } + }; + if (items.length === 0) return null; return (
{/* Grouped Bookmarks */} - {Object.entries(groupedItems).map(([group, groupItems]) => { - const { icon, name } = parseGroupString(group); + {sortedSections.map((section) => { + if (section.type === "grouped") { + const { icon, name } = parseGroupString(section.group || ""); + return ( +
+ + + + } + > +
+ {editing && allGroups.length > 1 && ( + + )} + +
+
+ + {section.items.map((v) => ( +
) => + e.preventDefault() + } + onTouchStart={handleTouchStart} + onTouchEnd={handleTouchEnd} + onMouseDown={handleMouseDown} + onMouseUp={handleMouseUp} + > + removeBookmark(v.id)} + onShowDetails={onShowDetails} + /> +
+ ))} +
+
+ ); + } // regular items return ( -
+
- - - } + title={t("home.bookmarks.sectionTitle")} + icon={Icons.BOOKMARK} > - +
+ {editing && allGroups.length > 1 && ( + + )} + +
- - {groupItems.map((v) => ( + + {section.items.map((v) => (
0 && ( -
- - + + + {t("home.bookmarks.groups.reorder.title")} + + + {t("home.bookmarks.groups.reorder.description")} + +
+ { + const newOrder = newItems.map((item) => item.id); + setTempGroupOrder(newOrder); + }} /> - - - {regularItems.map((v) => ( -
) => - e.preventDefault() - } - onTouchStart={handleTouchStart} - onTouchEnd={handleTouchEnd} - onMouseDown={handleMouseDown} - onMouseUp={handleMouseUp} - > - removeBookmark(v.id)} - onShowDetails={onShowDetails} - /> -
- ))} -
-
- )} +
+
+ + +
+ +
); } diff --git a/src/stores/bookmarks/index.ts b/src/stores/bookmarks/index.ts index 7d3ade87..73cf862d 100644 --- a/src/stores/bookmarks/index.ts +++ b/src/stores/bookmarks/index.ts @@ -2,6 +2,9 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; +import { getGroupOrder, updateGroupOrder } from "@/backend/accounts/groupOrder"; +import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; +import { AccountWithToken, useAuthStore } from "@/stores/auth"; import { PlayerMeta } from "@/stores/player/slices/source"; export interface BookmarkMediaItem { @@ -27,10 +30,20 @@ export interface BookmarkUpdateItem { export interface BookmarkStore { bookmarks: Record; updateQueue: BookmarkUpdateItem[]; + groupOrder: string[]; addBookmark(meta: PlayerMeta): void; addBookmarkWithGroups(meta: PlayerMeta, groups?: string[]): void; removeBookmark(id: string): void; replaceBookmarks(items: Record): void; + setGroupOrder(order: string[]): void; + saveGroupOrderToBackend( + backendUrl: string, + account: AccountWithToken, + ): Promise; + loadGroupOrderFromBackend( + backendUrl: string, + account: AccountWithToken, + ): Promise; clear(): void; clearUpdateQueue(): void; removeUpdateItem(id: string): void; @@ -43,6 +56,7 @@ export const useBookmarkStore = create( immer((set) => ({ bookmarks: {}, updateQueue: [], + groupOrder: [], removeBookmark(id) { set((s) => { updateId += 1; @@ -121,6 +135,35 @@ export const useBookmarkStore = create( s.updateQueue = [...s.updateQueue.filter((v) => v.id !== id)]; }); }, + setGroupOrder(order: string[]) { + set((s) => { + s.groupOrder = order; + }); + }, + async saveGroupOrderToBackend( + backendUrl: string, + account: AccountWithToken, + ) { + if (!account || !backendUrl) { + throw new Error("No authenticated account or backend URL"); + } + + const currentState = useBookmarkStore.getState(); + await updateGroupOrder(backendUrl, account, currentState.groupOrder); + }, + async loadGroupOrderFromBackend( + backendUrl: string, + account: AccountWithToken, + ) { + if (!account || !backendUrl) { + throw new Error("No authenticated account or backend URL"); + } + + const response = await getGroupOrder(backendUrl, account); + set((s) => { + s.groupOrder = response.groupOrder; + }); + }, })), { name: "__MW::bookmarks",