mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +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 { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
|
@ -7,7 +9,6 @@ import { EditButtonWithText } from "@/components/buttons/EditButtonWithText";
|
|||
import { Item } from "@/components/form/SortableList";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { EditBookmarkModal } from "@/components/overlays/EditBookmarkModal";
|
||||
import { EditGroupModal } from "@/components/overlays/EditGroupModal";
|
||||
import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal";
|
||||
|
|
@ -15,6 +16,10 @@ import { useModal } from "@/components/overlays/Modal";
|
|||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import {
|
||||
SortableMediaCard,
|
||||
useBookmarkDragAndDrop,
|
||||
} from "@/hooks/useBookmarkDragAndDrop";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
|
@ -181,6 +186,14 @@ export function BookmarksCarousel({
|
|||
return { groupedItems: grouped, regularItems: regular };
|
||||
}, [items, bookmarks, progressItems]);
|
||||
|
||||
// Drag and drop hook
|
||||
const { sensors, orderedItems, orderedGroupedItems, handleDragEnd } =
|
||||
useBookmarkDragAndDrop({
|
||||
editing,
|
||||
items,
|
||||
groupedItems,
|
||||
});
|
||||
|
||||
// group sorting
|
||||
const allGroups = useMemo(() => {
|
||||
const groups = new Set<string>();
|
||||
|
|
@ -435,27 +448,50 @@ export function BookmarksCarousel({
|
|||
>
|
||||
<div className="md:w-12" />
|
||||
|
||||
{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"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
key={media.id}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
editable={editing}
|
||||
onEdit={() => handleEditBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(e) => handleDragEnd(e, section.group)}
|
||||
>
|
||||
<SortableContext
|
||||
items={
|
||||
editing && orderedGroupedItems[section.group || ""]
|
||||
? orderedGroupedItems[section.group || ""]
|
||||
.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 && orderedGroupedItems[section.group || ""]
|
||||
? orderedGroupedItems[section.group || ""]
|
||||
: 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 && (
|
||||
<MoreBookmarksCard />
|
||||
|
|
@ -509,33 +545,71 @@ export function BookmarksCarousel({
|
|||
>
|
||||
<div className="md:w-12" />
|
||||
|
||||
{section.items.length > 0
|
||||
? 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"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
{section.items.length > 0 ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(e) => handleDragEnd(e)}
|
||||
>
|
||||
<SortableContext
|
||||
items={
|
||||
editing
|
||||
? orderedItems
|
||||
.filter((item) => {
|
||||
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}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
editable={editing}
|
||||
onEdit={() => handleEditBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: Array.from({ length: SKELETON_COUNT }).map(() => (
|
||||
<MediaCardSkeleton
|
||||
key={`skeleton-${categorySlug}-${Math.random().toString(36).substring(7)}`}
|
||||
/>
|
||||
))}
|
||||
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>
|
||||
) : (
|
||||
Array.from({ length: SKELETON_COUNT }).map(() => (
|
||||
<MediaCardSkeleton
|
||||
key={`skeleton-${categorySlug}-${Math.random().toString(36).substring(7)}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{section.items.length > MAX_ITEMS_PER_SECTION && (
|
||||
<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 { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -8,13 +10,16 @@ import { Item } 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 { EditBookmarkModal } from "@/components/overlays/EditBookmarkModal";
|
||||
import { EditGroupModal } from "@/components/overlays/EditGroupModal";
|
||||
import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import {
|
||||
SortableMediaCard,
|
||||
useBookmarkDragAndDrop,
|
||||
} from "@/hooks/useBookmarkDragAndDrop";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useGroupOrderStore } from "@/stores/groupOrder";
|
||||
|
|
@ -120,6 +125,14 @@ export function BookmarksPart({
|
|||
return { groupedItems: grouped, regularItems: regular };
|
||||
}, [items, bookmarks, progressItems]);
|
||||
|
||||
// Drag and drop hook
|
||||
const { sensors, orderedItems, orderedGroupedItems, handleDragEnd } =
|
||||
useBookmarkDragAndDrop({
|
||||
editing,
|
||||
items,
|
||||
groupedItems,
|
||||
});
|
||||
|
||||
// group sorting
|
||||
const allGroups = useMemo(() => {
|
||||
const groups = new Set<string>();
|
||||
|
|
@ -338,26 +351,47 @@ export function BookmarksPart({
|
|||
/>
|
||||
</div>
|
||||
</SectionHeading>
|
||||
<MediaGrid>
|
||||
{section.items.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
className="relative group"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(v.id)}
|
||||
onShowDetails={onShowDetails}
|
||||
editable={editing}
|
||||
onEdit={() => handleEditBookmark(v.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</MediaGrid>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(e) => handleDragEnd(e, section.group)}
|
||||
>
|
||||
<SortableContext
|
||||
items={
|
||||
editing && orderedGroupedItems[section.group || ""]
|
||||
? orderedGroupedItems[section.group || ""].map(
|
||||
(item) => item.id,
|
||||
)
|
||||
: section.items.map((item) => item.id)
|
||||
}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<MediaGrid>
|
||||
{(editing && orderedGroupedItems[section.group || ""]
|
||||
? orderedGroupedItems[section.group || ""]
|
||||
: 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>
|
||||
);
|
||||
} // regular items
|
||||
|
|
@ -384,26 +418,59 @@ export function BookmarksPart({
|
|||
/>
|
||||
</div>
|
||||
</SectionHeading>
|
||||
<MediaGrid ref={gridRef}>
|
||||
{section.items.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
className="relative group"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(v.id)}
|
||||
onShowDetails={onShowDetails}
|
||||
editable={editing}
|
||||
onEdit={() => handleEditBookmark(v.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</MediaGrid>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={(e) => handleDragEnd(e)}
|
||||
>
|
||||
<SortableContext
|
||||
items={
|
||||
editing
|
||||
? orderedItems
|
||||
.filter((item) => {
|
||||
const bookmark = bookmarks[item.id];
|
||||
return (
|
||||
!Array.isArray(bookmark?.group) ||
|
||||
bookmark.group.length === 0
|
||||
);
|
||||
})
|
||||
.map((item) => item.id)
|
||||
: section.items.map((item) => item.id)
|
||||
}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export interface BookmarkStore {
|
|||
modifyBookmarksByGroup(
|
||||
options: BulkGroupModificationOptions,
|
||||
): BookmarkModificationResult;
|
||||
updateBookmarkOrder(bookmarkIds: string[]): void;
|
||||
clear(): void;
|
||||
clearUpdateQueue(): void;
|
||||
removeUpdateItem(id: string): void;
|
||||
|
|
@ -277,6 +278,21 @@ export const useBookmarkStore = create(
|
|||
|
||||
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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue