diff --git a/src/backend/accounts/bookmarks.ts b/src/backend/accounts/bookmarks.ts index c1f9d38f..bb64e712 100644 --- a/src/backend/accounts/bookmarks.ts +++ b/src/backend/accounts/bookmarks.ts @@ -10,12 +10,12 @@ export interface BookmarkMetaInput { year: number; poster?: string; type: string; - group?: string[]; } export interface BookmarkInput { tmdbId: string; meta: BookmarkMetaInput; + group?: string[]; } export function bookmarkMediaToInput( @@ -30,6 +30,7 @@ export function bookmarkMediaToInput( year: item.year ?? 0, }, tmdbId, + group: item.group, }; } diff --git a/src/backend/accounts/user.ts b/src/backend/accounts/user.ts index 10381d38..c7450bcd 100644 --- a/src/backend/accounts/user.ts +++ b/src/backend/accounts/user.ts @@ -33,8 +33,8 @@ export interface BookmarkResponse { year: number; poster?: string; type: "show" | "movie"; - group?: string[]; }; + group: string[]; updatedAt: string; } @@ -63,6 +63,7 @@ export function bookmarkResponsesToEntries(responses: BookmarkResponse[]) { const entries = responses.map((bookmark) => { const item: BookmarkMediaItem = { ...bookmark.meta, + group: bookmark.group.length > 0 ? bookmark.group : undefined, updatedAt: new Date(bookmark.updatedAt).getTime(), }; return [bookmark.tmdbId, item] as const; diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts index 9a2a24bf..9f4c29bb 100644 --- a/src/hooks/auth/useAuthData.ts +++ b/src/hooks/auth/useAuthData.ts @@ -11,6 +11,7 @@ import { } from "@/backend/accounts/user"; import { useAuthStore } from "@/stores/auth"; import { useBookmarkStore } from "@/stores/bookmarks"; +import { useGroupOrderStore } from "@/stores/groupOrder"; import { useLanguageStore } from "@/stores/language"; import { usePreferencesStore } from "@/stores/preferences"; import { useProgressStore } from "@/stores/progress"; @@ -102,7 +103,7 @@ export function useAuthData() { replaceItems(progressResponsesToEntries(progress)); if (groupOrder?.groupOrder) { - useBookmarkStore.getState().setGroupOrder(groupOrder.groupOrder); + useGroupOrderStore.getState().setGroupOrder(groupOrder.groupOrder); } if (settings.applicationLanguage) { diff --git a/src/index.tsx b/src/index.tsx index 4a531912..2f3ecee1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ import App from "@/setup/App"; import { conf } from "@/setup/config"; import { useAuthStore } from "@/stores/auth"; import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer"; +import { GroupSyncer } from "@/stores/groupOrder/GroupSyncer"; import { changeAppLanguage, useLanguageStore } from "@/stores/language"; import { ProgressSyncer } from "@/stores/progress/ProgressSyncer"; import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer"; @@ -185,6 +186,7 @@ root.render( + diff --git a/src/pages/parts/home/BookmarksCarousel.tsx b/src/pages/parts/home/BookmarksCarousel.tsx index 0e942540..1d130ddc 100644 --- a/src/pages/parts/home/BookmarksCarousel.tsx +++ b/src/pages/parts/home/BookmarksCarousel.tsx @@ -16,6 +16,7 @@ import { useIsMobile } from "@/hooks/useIsMobile"; import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons"; import { useAuthStore } from "@/stores/auth"; import { useBookmarkStore } from "@/stores/bookmarks"; +import { useGroupOrderStore } from "@/stores/groupOrder"; import { useProgressStore } from "@/stores/progress"; import { MediaItem } from "@/utils/mediaTypes"; @@ -64,8 +65,8 @@ export function BookmarksCarousel({ const account = useAuthStore((s) => s.account); // Group order editing state - const groupOrder = useBookmarkStore((s) => s.groupOrder); - const setGroupOrder = useBookmarkStore((s) => s.setGroupOrder); + const groupOrder = useGroupOrderStore((s) => s.groupOrder); + const setGroupOrder = useGroupOrderStore((s) => s.setGroupOrder); const editOrderModal = useModal("bookmark-edit-order-carousel"); const [tempGroupOrder, setTempGroupOrder] = useState([]); @@ -323,7 +324,9 @@ export function BookmarksCarousel({ // Save to backend if (backendUrl && account) { - useBookmarkStore.getState().saveGroupOrderToBackend(backendUrl, account); + useGroupOrderStore + .getState() + .saveGroupOrderToBackend(backendUrl, account); } }; diff --git a/src/pages/parts/home/BookmarksPart.tsx b/src/pages/parts/home/BookmarksPart.tsx index 7409deba..1616ce59 100644 --- a/src/pages/parts/home/BookmarksPart.tsx +++ b/src/pages/parts/home/BookmarksPart.tsx @@ -16,6 +16,7 @@ import { Heading2, Paragraph } from "@/components/utils/Text"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useAuthStore } from "@/stores/auth"; import { useBookmarkStore } from "@/stores/bookmarks"; +import { useGroupOrderStore } from "@/stores/groupOrder"; import { useProgressStore } from "@/stores/progress"; import { MediaItem } from "@/utils/mediaTypes"; @@ -42,8 +43,8 @@ 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 groupOrder = useGroupOrderStore((s) => s.groupOrder); + const setGroupOrder = useGroupOrderStore((s) => s.setGroupOrder); const removeBookmark = useBookmarkStore((s) => s.removeBookmark); const [editing, setEditing] = useState(false); const [gridRef] = useAutoAnimate(); @@ -278,7 +279,9 @@ export function BookmarksPart({ // Save to backend if (backendUrl && account) { - useBookmarkStore.getState().saveGroupOrderToBackend(backendUrl, account); + useGroupOrderStore + .getState() + .saveGroupOrderToBackend(backendUrl, account); } }; diff --git a/src/stores/bookmarks/BookmarkSyncer.tsx b/src/stores/bookmarks/BookmarkSyncer.tsx index f65d89a1..6e9c01f1 100644 --- a/src/stores/bookmarks/BookmarkSyncer.tsx +++ b/src/stores/bookmarks/BookmarkSyncer.tsx @@ -32,9 +32,9 @@ async function syncBookmarks( title: item.title ?? "", type: item.type ?? "", year: item.year ?? NaN, - group: item.group, }, tmdbId: item.tmdbId, + group: item.group, }); continue; } diff --git a/src/stores/bookmarks/index.ts b/src/stores/bookmarks/index.ts index 73cf862d..7d3ade87 100644 --- a/src/stores/bookmarks/index.ts +++ b/src/stores/bookmarks/index.ts @@ -2,9 +2,6 @@ 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 { @@ -30,20 +27,10 @@ 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; @@ -56,7 +43,6 @@ export const useBookmarkStore = create( immer((set) => ({ bookmarks: {}, updateQueue: [], - groupOrder: [], removeBookmark(id) { set((s) => { updateId += 1; @@ -135,35 +121,6 @@ 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", diff --git a/src/stores/groupOrder/GroupSyncer.tsx b/src/stores/groupOrder/GroupSyncer.tsx new file mode 100644 index 00000000..ce6ae64f --- /dev/null +++ b/src/stores/groupOrder/GroupSyncer.tsx @@ -0,0 +1,55 @@ +import { useEffect, useRef } from "react"; + +import { updateGroupOrder } from "@/backend/accounts/groupOrder"; +import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; +import { useAuthStore } from "@/stores/auth"; +import { useGroupOrderStore } from "@/stores/groupOrder"; + +const syncIntervalMs = 5 * 1000; + +export function GroupSyncer() { + const url = useBackendUrl(); + const groupOrder = useGroupOrderStore((s) => s.groupOrder); + const lastSyncedOrder = useRef([]); + const isInitialized = useRef(false); + + // Initialize lastSyncedOrder on first render + useEffect(() => { + if (!isInitialized.current) { + lastSyncedOrder.current = [...groupOrder]; + isInitialized.current = true; + } + }, [groupOrder]); + + useEffect(() => { + const interval = setInterval(() => { + (async () => { + if (!url) return; + + const user = useAuthStore.getState(); + if (!user.account) return; // not logged in, dont sync to server + + // Check if group order has changed since last sync + const currentOrder = useGroupOrderStore.getState().groupOrder; + const hasChanged = + JSON.stringify(currentOrder) !== + JSON.stringify(lastSyncedOrder.current); + + if (hasChanged) { + try { + await updateGroupOrder(url, user.account, currentOrder); + lastSyncedOrder.current = [...currentOrder]; + } catch (err) { + console.error("Failed to sync group order:", err); + } + } + })(); + }, syncIntervalMs); + + return () => { + clearInterval(interval); + }; + }, [url]); + + return null; +} diff --git a/src/stores/groupOrder/index.ts b/src/stores/groupOrder/index.ts new file mode 100644 index 00000000..c754870e --- /dev/null +++ b/src/stores/groupOrder/index.ts @@ -0,0 +1,65 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +import { getGroupOrder, updateGroupOrder } from "@/backend/accounts/groupOrder"; +import { AccountWithToken } from "@/stores/auth"; + +export interface GroupOrderStore { + groupOrder: string[]; + setGroupOrder(order: string[]): void; + saveGroupOrderToBackend( + backendUrl: string, + account: AccountWithToken, + ): Promise; + loadGroupOrderFromBackend( + backendUrl: string, + account: AccountWithToken, + ): Promise; + clear(): void; +} + +export const useGroupOrderStore = create( + persist( + immer((set) => ({ + groupOrder: [], + 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 = useGroupOrderStore.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; + }); + }, + clear() { + set((s) => { + s.groupOrder = []; + }); + }, + })), + { + name: "__MW::groupOrder", + }, + ), +);