mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 07:02:21 +00:00
add drag and drop bookmark reordering
This commit is contained in:
parent
1b073006f4
commit
c90e77ddf3
4 changed files with 447 additions and 89 deletions
201
src/hooks/useBookmarkDragAndDrop.tsx
Normal file
201
src/hooks/useBookmarkDragAndDrop.tsx
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import {
|
||||||
|
DragEndEvent,
|
||||||
|
KeyboardSensor,
|
||||||
|
MouseSensor,
|
||||||
|
TouchSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
useSortable,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||||
|
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||||
|
import { MediaItem } from "@/utils/mediaTypes";
|
||||||
|
|
||||||
|
interface SortableMediaCardProps {
|
||||||
|
media: MediaItem;
|
||||||
|
closable?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
onShowDetails?: (media: MediaItem) => void;
|
||||||
|
editable?: boolean;
|
||||||
|
onEdit?: () => void;
|
||||||
|
isEditing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableMediaCard({
|
||||||
|
media,
|
||||||
|
closable,
|
||||||
|
onClose,
|
||||||
|
onShowDetails,
|
||||||
|
editable,
|
||||||
|
onEdit,
|
||||||
|
isEditing,
|
||||||
|
}: SortableMediaCardProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: media.id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...(isEditing ? { ...attributes, ...listeners } : {})}
|
||||||
|
className={isEditing ? "cursor-grab active:cursor-grabbing" : ""}
|
||||||
|
>
|
||||||
|
<WatchedMediaCard
|
||||||
|
media={media}
|
||||||
|
closable={closable}
|
||||||
|
onClose={onClose}
|
||||||
|
onShowDetails={onShowDetails}
|
||||||
|
editable={editable}
|
||||||
|
onEdit={onEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseBookmarkDragAndDropProps {
|
||||||
|
editing: boolean;
|
||||||
|
items: MediaItem[];
|
||||||
|
groupedItems: Record<string, MediaItem[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBookmarkDragAndDrop({
|
||||||
|
editing,
|
||||||
|
items,
|
||||||
|
groupedItems,
|
||||||
|
}: UseBookmarkDragAndDropProps) {
|
||||||
|
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||||
|
const updateBookmarkOrder = useBookmarkStore((s) => s.updateBookmarkOrder);
|
||||||
|
|
||||||
|
// Drag and drop sensors
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(TouchSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
delay: 75,
|
||||||
|
tolerance: 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
useSensor(MouseSensor),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track order during editing
|
||||||
|
const [orderedItems, setOrderedItems] = useState<MediaItem[]>([]);
|
||||||
|
const [orderedGroupedItems, setOrderedGroupedItems] = useState<
|
||||||
|
Record<string, MediaItem[]>
|
||||||
|
>({});
|
||||||
|
const isApplyingOrderRef = useRef(false);
|
||||||
|
|
||||||
|
// Initialize ordered items when entering edit mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) {
|
||||||
|
setOrderedItems([...items]);
|
||||||
|
setOrderedGroupedItems({ ...groupedItems });
|
||||||
|
isApplyingOrderRef.current = false;
|
||||||
|
}
|
||||||
|
}, [editing, items, groupedItems]);
|
||||||
|
|
||||||
|
// Apply order when exiting edit mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editing && orderedItems.length > 0 && !isApplyingOrderRef.current) {
|
||||||
|
isApplyingOrderRef.current = true;
|
||||||
|
|
||||||
|
// Apply order for regular items
|
||||||
|
const regularOrder = orderedItems
|
||||||
|
.filter((item) => {
|
||||||
|
const bookmark = bookmarks[item.id];
|
||||||
|
return !Array.isArray(bookmark?.group) || bookmark.group.length === 0;
|
||||||
|
})
|
||||||
|
.map((item) => item.id);
|
||||||
|
if (regularOrder.length > 0) {
|
||||||
|
updateBookmarkOrder(regularOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply order for grouped items
|
||||||
|
Object.entries(orderedGroupedItems).forEach(
|
||||||
|
([_groupName, groupItems]) => {
|
||||||
|
const groupOrderIds = groupItems.map((item) => item.id);
|
||||||
|
if (groupOrderIds.length > 0) {
|
||||||
|
updateBookmarkOrder(groupOrderIds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset ordered items after a short delay to allow state updates to complete
|
||||||
|
setTimeout(() => {
|
||||||
|
setOrderedItems([]);
|
||||||
|
setOrderedGroupedItems({});
|
||||||
|
isApplyingOrderRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
editing,
|
||||||
|
orderedItems,
|
||||||
|
orderedGroupedItems,
|
||||||
|
bookmarks,
|
||||||
|
updateBookmarkOrder,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent, groupName?: string) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (!over || active.id === over.id) return;
|
||||||
|
|
||||||
|
if (groupName) {
|
||||||
|
// Handle grouped items
|
||||||
|
const currentItems = orderedGroupedItems[groupName] || [];
|
||||||
|
const oldIndex = currentItems.findIndex((item) => item.id === active.id);
|
||||||
|
const newIndex = currentItems.findIndex((item) => item.id === over.id);
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1) {
|
||||||
|
const newItems = arrayMove(currentItems, oldIndex, newIndex);
|
||||||
|
setOrderedGroupedItems({
|
||||||
|
...orderedGroupedItems,
|
||||||
|
[groupName]: newItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle regular items
|
||||||
|
const currentItems = orderedItems.filter((item) => {
|
||||||
|
const bookmark = bookmarks[item.id];
|
||||||
|
return !Array.isArray(bookmark?.group) || bookmark.group.length === 0;
|
||||||
|
});
|
||||||
|
const oldIndex = currentItems.findIndex((item) => item.id === active.id);
|
||||||
|
const newIndex = currentItems.findIndex((item) => item.id === over.id);
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1) {
|
||||||
|
const newItems = arrayMove(currentItems, oldIndex, newIndex);
|
||||||
|
// Update orderedItems with the new order
|
||||||
|
const otherItems = orderedItems.filter((item) => {
|
||||||
|
const bookmark = bookmarks[item.id];
|
||||||
|
return Array.isArray(bookmark?.group) && bookmark.group.length > 0;
|
||||||
|
});
|
||||||
|
setOrderedItems([...newItems, ...otherItems]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sensors,
|
||||||
|
orderedItems,
|
||||||
|
orderedGroupedItems,
|
||||||
|
handleDragEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { DndContext, closestCenter } from "@dnd-kit/core";
|
||||||
|
import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
@ -7,7 +9,6 @@ import { EditButtonWithText } from "@/components/buttons/EditButtonWithText";
|
||||||
import { Item } from "@/components/form/SortableList";
|
import { Item } from "@/components/form/SortableList";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
|
||||||
import { EditBookmarkModal } from "@/components/overlays/EditBookmarkModal";
|
import { EditBookmarkModal } from "@/components/overlays/EditBookmarkModal";
|
||||||
import { EditGroupModal } from "@/components/overlays/EditGroupModal";
|
import { EditGroupModal } from "@/components/overlays/EditGroupModal";
|
||||||
import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal";
|
import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal";
|
||||||
|
|
@ -15,6 +16,10 @@ import { useModal } from "@/components/overlays/Modal";
|
||||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||||
import { Flare } from "@/components/utils/Flare";
|
import { Flare } from "@/components/utils/Flare";
|
||||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||||
|
import {
|
||||||
|
SortableMediaCard,
|
||||||
|
useBookmarkDragAndDrop,
|
||||||
|
} from "@/hooks/useBookmarkDragAndDrop";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
@ -181,6 +186,14 @@ export function BookmarksCarousel({
|
||||||
return { groupedItems: grouped, regularItems: regular };
|
return { groupedItems: grouped, regularItems: regular };
|
||||||
}, [items, bookmarks, progressItems]);
|
}, [items, bookmarks, progressItems]);
|
||||||
|
|
||||||
|
// Drag and drop hook
|
||||||
|
const { sensors, orderedItems, orderedGroupedItems, handleDragEnd } =
|
||||||
|
useBookmarkDragAndDrop({
|
||||||
|
editing,
|
||||||
|
items,
|
||||||
|
groupedItems,
|
||||||
|
});
|
||||||
|
|
||||||
// group sorting
|
// group sorting
|
||||||
const allGroups = useMemo(() => {
|
const allGroups = useMemo(() => {
|
||||||
const groups = new Set<string>();
|
const groups = new Set<string>();
|
||||||
|
|
@ -435,27 +448,50 @@ export function BookmarksCarousel({
|
||||||
>
|
>
|
||||||
<div className="md:w-12" />
|
<div className="md:w-12" />
|
||||||
|
|
||||||
{section.items
|
<DndContext
|
||||||
.slice(0, MAX_ITEMS_PER_SECTION)
|
sensors={sensors}
|
||||||
.map((media) => (
|
collisionDetection={closestCenter}
|
||||||
<div
|
onDragEnd={(e) => handleDragEnd(e, section.group)}
|
||||||
key={media.id}
|
>
|
||||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
<SortableContext
|
||||||
e.preventDefault()
|
items={
|
||||||
}
|
editing && orderedGroupedItems[section.group || ""]
|
||||||
className="relative mt-4 group cursor-pointer rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
? orderedGroupedItems[section.group || ""]
|
||||||
>
|
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||||
<WatchedMediaCard
|
.map((item) => item.id)
|
||||||
key={media.id}
|
: section.items
|
||||||
media={media}
|
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||||
onShowDetails={onShowDetails}
|
.map((item) => item.id)
|
||||||
closable={editing}
|
}
|
||||||
onClose={() => removeBookmark(media.id)}
|
strategy={rectSortingStrategy}
|
||||||
editable={editing}
|
>
|
||||||
onEdit={() => handleEditBookmark(media.id)}
|
{(editing && orderedGroupedItems[section.group || ""]
|
||||||
/>
|
? orderedGroupedItems[section.group || ""]
|
||||||
</div>
|
: section.items
|
||||||
))}
|
)
|
||||||
|
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||||
|
.map((media) => (
|
||||||
|
<div
|
||||||
|
key={media.id}
|
||||||
|
onContextMenu={(
|
||||||
|
e: React.MouseEvent<HTMLDivElement>,
|
||||||
|
) => e.preventDefault()}
|
||||||
|
className="relative mt-4 group cursor-pointer rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||||
|
>
|
||||||
|
<SortableMediaCard
|
||||||
|
key={media.id}
|
||||||
|
media={media}
|
||||||
|
onShowDetails={onShowDetails}
|
||||||
|
closable={editing}
|
||||||
|
onClose={() => removeBookmark(media.id)}
|
||||||
|
editable={editing}
|
||||||
|
onEdit={() => handleEditBookmark(media.id)}
|
||||||
|
isEditing={editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
|
||||||
{section.items.length > MAX_ITEMS_PER_SECTION && (
|
{section.items.length > MAX_ITEMS_PER_SECTION && (
|
||||||
<MoreBookmarksCard />
|
<MoreBookmarksCard />
|
||||||
|
|
@ -509,33 +545,71 @@ export function BookmarksCarousel({
|
||||||
>
|
>
|
||||||
<div className="md:w-12" />
|
<div className="md:w-12" />
|
||||||
|
|
||||||
{section.items.length > 0
|
{section.items.length > 0 ? (
|
||||||
? section.items
|
<DndContext
|
||||||
.slice(0, MAX_ITEMS_PER_SECTION)
|
sensors={sensors}
|
||||||
.map((media) => (
|
collisionDetection={closestCenter}
|
||||||
<div
|
onDragEnd={(e) => handleDragEnd(e)}
|
||||||
key={media.id}
|
>
|
||||||
onContextMenu={(
|
<SortableContext
|
||||||
e: React.MouseEvent<HTMLDivElement>,
|
items={
|
||||||
) => e.preventDefault()}
|
editing
|
||||||
className="relative mt-4 group cursor-pointer rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
? orderedItems
|
||||||
>
|
.filter((item) => {
|
||||||
<WatchedMediaCard
|
const bookmark = bookmarks[item.id];
|
||||||
|
return (
|
||||||
|
!Array.isArray(bookmark?.group) ||
|
||||||
|
bookmark.group.length === 0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||||
|
.map((item) => item.id)
|
||||||
|
: section.items
|
||||||
|
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||||
|
.map((item) => item.id)
|
||||||
|
}
|
||||||
|
strategy={rectSortingStrategy}
|
||||||
|
>
|
||||||
|
{(editing
|
||||||
|
? orderedItems.filter((item) => {
|
||||||
|
const bookmark = bookmarks[item.id];
|
||||||
|
return (
|
||||||
|
!Array.isArray(bookmark?.group) ||
|
||||||
|
bookmark.group.length === 0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: section.items
|
||||||
|
)
|
||||||
|
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||||
|
.map((media) => (
|
||||||
|
<div
|
||||||
key={media.id}
|
key={media.id}
|
||||||
media={media}
|
onContextMenu={(
|
||||||
onShowDetails={onShowDetails}
|
e: React.MouseEvent<HTMLDivElement>,
|
||||||
closable={editing}
|
) => e.preventDefault()}
|
||||||
onClose={() => removeBookmark(media.id)}
|
className="relative mt-4 group cursor-pointer rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||||
editable={editing}
|
>
|
||||||
onEdit={() => handleEditBookmark(media.id)}
|
<SortableMediaCard
|
||||||
/>
|
key={media.id}
|
||||||
</div>
|
media={media}
|
||||||
))
|
onShowDetails={onShowDetails}
|
||||||
: Array.from({ length: SKELETON_COUNT }).map(() => (
|
closable={editing}
|
||||||
<MediaCardSkeleton
|
onClose={() => removeBookmark(media.id)}
|
||||||
key={`skeleton-${categorySlug}-${Math.random().toString(36).substring(7)}`}
|
editable={editing}
|
||||||
/>
|
onEdit={() => handleEditBookmark(media.id)}
|
||||||
))}
|
isEditing={editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
) : (
|
||||||
|
Array.from({ length: SKELETON_COUNT }).map(() => (
|
||||||
|
<MediaCardSkeleton
|
||||||
|
key={`skeleton-${categorySlug}-${Math.random().toString(36).substring(7)}`}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
{section.items.length > MAX_ITEMS_PER_SECTION && (
|
{section.items.length > MAX_ITEMS_PER_SECTION && (
|
||||||
<MoreBookmarksCard />
|
<MoreBookmarksCard />
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { DndContext, closestCenter } from "@dnd-kit/core";
|
||||||
|
import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable";
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
@ -8,13 +10,16 @@ import { Item } from "@/components/form/SortableList";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
|
||||||
import { EditBookmarkModal } from "@/components/overlays/EditBookmarkModal";
|
import { EditBookmarkModal } from "@/components/overlays/EditBookmarkModal";
|
||||||
import { EditGroupModal } from "@/components/overlays/EditGroupModal";
|
import { EditGroupModal } from "@/components/overlays/EditGroupModal";
|
||||||
import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal";
|
import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal";
|
||||||
import { useModal } from "@/components/overlays/Modal";
|
import { useModal } from "@/components/overlays/Modal";
|
||||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||||
|
import {
|
||||||
|
SortableMediaCard,
|
||||||
|
useBookmarkDragAndDrop,
|
||||||
|
} from "@/hooks/useBookmarkDragAndDrop";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||||
import { useGroupOrderStore } from "@/stores/groupOrder";
|
import { useGroupOrderStore } from "@/stores/groupOrder";
|
||||||
|
|
@ -120,6 +125,14 @@ export function BookmarksPart({
|
||||||
return { groupedItems: grouped, regularItems: regular };
|
return { groupedItems: grouped, regularItems: regular };
|
||||||
}, [items, bookmarks, progressItems]);
|
}, [items, bookmarks, progressItems]);
|
||||||
|
|
||||||
|
// Drag and drop hook
|
||||||
|
const { sensors, orderedItems, orderedGroupedItems, handleDragEnd } =
|
||||||
|
useBookmarkDragAndDrop({
|
||||||
|
editing,
|
||||||
|
items,
|
||||||
|
groupedItems,
|
||||||
|
});
|
||||||
|
|
||||||
// group sorting
|
// group sorting
|
||||||
const allGroups = useMemo(() => {
|
const allGroups = useMemo(() => {
|
||||||
const groups = new Set<string>();
|
const groups = new Set<string>();
|
||||||
|
|
@ -338,26 +351,47 @@ export function BookmarksPart({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
<MediaGrid>
|
<DndContext
|
||||||
{section.items.map((v) => (
|
sensors={sensors}
|
||||||
<div
|
collisionDetection={closestCenter}
|
||||||
key={v.id}
|
onDragEnd={(e) => handleDragEnd(e, section.group)}
|
||||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
>
|
||||||
e.preventDefault()
|
<SortableContext
|
||||||
}
|
items={
|
||||||
className="relative group"
|
editing && orderedGroupedItems[section.group || ""]
|
||||||
>
|
? orderedGroupedItems[section.group || ""].map(
|
||||||
<WatchedMediaCard
|
(item) => item.id,
|
||||||
media={v}
|
)
|
||||||
closable={editing}
|
: section.items.map((item) => item.id)
|
||||||
onClose={() => removeBookmark(v.id)}
|
}
|
||||||
onShowDetails={onShowDetails}
|
strategy={rectSortingStrategy}
|
||||||
editable={editing}
|
>
|
||||||
onEdit={() => handleEditBookmark(v.id)}
|
<MediaGrid>
|
||||||
/>
|
{(editing && orderedGroupedItems[section.group || ""]
|
||||||
</div>
|
? orderedGroupedItems[section.group || ""]
|
||||||
))}
|
: section.items
|
||||||
</MediaGrid>
|
).map((v) => (
|
||||||
|
<div
|
||||||
|
key={v.id}
|
||||||
|
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
className="relative group"
|
||||||
|
>
|
||||||
|
<SortableMediaCard
|
||||||
|
media={v}
|
||||||
|
closable={editing}
|
||||||
|
onClose={() => removeBookmark(v.id)}
|
||||||
|
onShowDetails={onShowDetails}
|
||||||
|
editable={editing}
|
||||||
|
onEdit={() => handleEditBookmark(v.id)}
|
||||||
|
isEditing={editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</MediaGrid>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} // regular items
|
} // regular items
|
||||||
|
|
@ -384,26 +418,59 @@ export function BookmarksPart({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
<MediaGrid ref={gridRef}>
|
<DndContext
|
||||||
{section.items.map((v) => (
|
sensors={sensors}
|
||||||
<div
|
collisionDetection={closestCenter}
|
||||||
key={v.id}
|
onDragEnd={(e) => handleDragEnd(e)}
|
||||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
>
|
||||||
e.preventDefault()
|
<SortableContext
|
||||||
}
|
items={
|
||||||
className="relative group"
|
editing
|
||||||
>
|
? orderedItems
|
||||||
<WatchedMediaCard
|
.filter((item) => {
|
||||||
media={v}
|
const bookmark = bookmarks[item.id];
|
||||||
closable={editing}
|
return (
|
||||||
onClose={() => removeBookmark(v.id)}
|
!Array.isArray(bookmark?.group) ||
|
||||||
onShowDetails={onShowDetails}
|
bookmark.group.length === 0
|
||||||
editable={editing}
|
);
|
||||||
onEdit={() => handleEditBookmark(v.id)}
|
})
|
||||||
/>
|
.map((item) => item.id)
|
||||||
</div>
|
: section.items.map((item) => item.id)
|
||||||
))}
|
}
|
||||||
</MediaGrid>
|
strategy={rectSortingStrategy}
|
||||||
|
>
|
||||||
|
<MediaGrid ref={gridRef}>
|
||||||
|
{(editing
|
||||||
|
? orderedItems.filter((item) => {
|
||||||
|
const bookmark = bookmarks[item.id];
|
||||||
|
return (
|
||||||
|
!Array.isArray(bookmark?.group) ||
|
||||||
|
bookmark.group.length === 0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: section.items
|
||||||
|
).map((v) => (
|
||||||
|
<div
|
||||||
|
key={v.id}
|
||||||
|
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
className="relative group"
|
||||||
|
>
|
||||||
|
<SortableMediaCard
|
||||||
|
media={v}
|
||||||
|
closable={editing}
|
||||||
|
onClose={() => removeBookmark(v.id)}
|
||||||
|
onShowDetails={onShowDetails}
|
||||||
|
editable={editing}
|
||||||
|
onEdit={() => handleEditBookmark(v.id)}
|
||||||
|
isEditing={editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</MediaGrid>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export interface BookmarkStore {
|
||||||
modifyBookmarksByGroup(
|
modifyBookmarksByGroup(
|
||||||
options: BulkGroupModificationOptions,
|
options: BulkGroupModificationOptions,
|
||||||
): BookmarkModificationResult;
|
): BookmarkModificationResult;
|
||||||
|
updateBookmarkOrder(bookmarkIds: string[]): void;
|
||||||
clear(): void;
|
clear(): void;
|
||||||
clearUpdateQueue(): void;
|
clearUpdateQueue(): void;
|
||||||
removeUpdateItem(id: string): void;
|
removeUpdateItem(id: string): void;
|
||||||
|
|
@ -277,6 +278,21 @@ export const useBookmarkStore = create(
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
updateBookmarkOrder(bookmarkIds: string[]) {
|
||||||
|
set((s) => {
|
||||||
|
const baseTime = Date.now();
|
||||||
|
bookmarkIds.forEach((bookmarkId, index) => {
|
||||||
|
const bookmark = s.bookmarks[bookmarkId];
|
||||||
|
if (bookmark) {
|
||||||
|
// Update timestamp to reflect order (earlier items have higher timestamps)
|
||||||
|
// This ensures they appear first when sorted by date descending
|
||||||
|
// Note: We don't add to update queue here to avoid quota errors.
|
||||||
|
// Order is persisted locally via updatedAt timestamps.
|
||||||
|
bookmark.updatedAt = baseTime - index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: "__MW::bookmarks",
|
name: "__MW::bookmarks",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue