mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 01:42:25 +00:00
add edit group and edit bookmarks modals
This commit is contained in:
parent
2ad6b8b942
commit
1b073006f4
11 changed files with 1029 additions and 0 deletions
|
|
@ -297,7 +297,27 @@
|
|||
"description": "Drag and drop to reorder your bookmark groups",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save"
|
||||
},
|
||||
"editGroup": {
|
||||
"title": "Edit Group",
|
||||
"description": "Edit the name and icon of your bookmark group",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"affectsBookmarks": "This will affect {{count}} bookmark(s)",
|
||||
"nameLabel": "Group name",
|
||||
"namePlaceholder": "Enter a name for your group"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Bookmark",
|
||||
"description": "Edit the details for this bookmark",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"groupsLabel": "Groups",
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Enter a title for your bookmark",
|
||||
"yearLabel": "Year",
|
||||
"yearPlaceholder": "Enter a year for your bookmark"
|
||||
}
|
||||
},
|
||||
"continueWatching": {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ export interface MediaCardProps {
|
|||
onClose?: () => void;
|
||||
onShowDetails?: (media: MediaItem) => void;
|
||||
forceSkeleton?: boolean;
|
||||
editable?: boolean;
|
||||
onEdit?: () => void;
|
||||
}
|
||||
|
||||
function checkReleased(media: MediaItem): boolean {
|
||||
|
|
@ -119,6 +121,8 @@ function MediaCardContent({
|
|||
onClose,
|
||||
onShowDetails,
|
||||
forceSkeleton,
|
||||
editable,
|
||||
onEdit,
|
||||
}: MediaCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
|
||||
|
|
@ -288,6 +292,24 @@ function MediaCardContent({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{editable && closable && (
|
||||
<div className="absolute bottom-0 translate-y-1 right-1">
|
||||
<button
|
||||
className="media-more-button p-2"
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onEdit?.();
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="text-xs font-semibold text-type-secondary"
|
||||
icon={Icons.EDIT}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Flare.Child>
|
||||
</Flare.Base>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ export interface WatchedMediaCardProps {
|
|||
closable?: boolean;
|
||||
onClose?: () => void;
|
||||
onShowDetails?: (media: MediaItem) => void;
|
||||
editable?: boolean;
|
||||
onEdit?: () => void;
|
||||
}
|
||||
|
||||
export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
||||
|
|
@ -51,6 +53,8 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) {
|
|||
onClose={props.onClose}
|
||||
closable={props.closable}
|
||||
onShowDetails={props.onShowDetails}
|
||||
editable={props.editable}
|
||||
onEdit={props.onEdit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
167
src/components/overlays/EditBookmarkModal.tsx
Normal file
167
src/components/overlays/EditBookmarkModal.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { GroupDropdown } from "@/components/form/GroupDropdown";
|
||||
import { Modal, ModalCard } from "@/components/overlays/Modal";
|
||||
import { UserIcons } from "@/components/UserIcon";
|
||||
import { Heading2, Paragraph } from "@/components/utils/Text";
|
||||
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks";
|
||||
|
||||
interface EditBookmarkModalProps {
|
||||
id: string;
|
||||
isShown: boolean;
|
||||
bookmarkId: string | null;
|
||||
onCancel: () => void;
|
||||
onSave: (bookmarkId: string, changes: Partial<BookmarkMediaItem>) => void;
|
||||
}
|
||||
|
||||
export function EditBookmarkModal({
|
||||
id,
|
||||
isShown,
|
||||
bookmarkId,
|
||||
onCancel,
|
||||
onSave,
|
||||
}: EditBookmarkModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
|
||||
const [title, setTitle] = useState("");
|
||||
const [year, setYear] = useState<number | undefined>();
|
||||
const [groups, setGroups] = useState<string[]>([]);
|
||||
|
||||
// Get all available groups from all bookmarks
|
||||
const allGroups = useMemo(() => {
|
||||
const groupSet = new Set<string>();
|
||||
Object.values(bookmarks).forEach((bookmark) => {
|
||||
if (bookmark.group) {
|
||||
bookmark.group.forEach((group) => groupSet.add(group));
|
||||
}
|
||||
});
|
||||
return Array.from(groupSet);
|
||||
}, [bookmarks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bookmarkId && bookmarks[bookmarkId]) {
|
||||
const bookmark = bookmarks[bookmarkId];
|
||||
setTitle(bookmark.title);
|
||||
setYear(bookmark.year);
|
||||
setGroups(bookmark.group || []);
|
||||
} else {
|
||||
setTitle("");
|
||||
setYear(undefined);
|
||||
setGroups([]);
|
||||
}
|
||||
}, [bookmarkId, bookmarks]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!bookmarkId) return;
|
||||
|
||||
const changes: Partial<BookmarkMediaItem> = {};
|
||||
|
||||
if (title !== bookmarks[bookmarkId]?.title) {
|
||||
changes.title = title;
|
||||
}
|
||||
|
||||
if (year !== bookmarks[bookmarkId]?.year) {
|
||||
changes.year = year;
|
||||
}
|
||||
|
||||
const currentGroups = bookmarks[bookmarkId]?.group || [];
|
||||
if (
|
||||
JSON.stringify(groups.sort()) !== JSON.stringify(currentGroups.sort())
|
||||
) {
|
||||
changes.group = groups;
|
||||
}
|
||||
|
||||
if (Object.keys(changes).length > 0) {
|
||||
onSave(bookmarkId, changes);
|
||||
}
|
||||
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const handleCreateGroup = (groupString: string, _icon: UserIcons) => {
|
||||
if (!groups.includes(groupString)) {
|
||||
setGroups([...groups, groupString]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveGroup = (groupToRemove?: string) => {
|
||||
if (groupToRemove) {
|
||||
setGroups(groups.filter((group) => group !== groupToRemove));
|
||||
} else {
|
||||
setGroups([]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isShown || !bookmarkId) return null;
|
||||
|
||||
return (
|
||||
<Modal id={id}>
|
||||
<ModalCard>
|
||||
<Heading2 className="!my-0">{t("home.bookmarks.edit.title")}</Heading2>
|
||||
<Paragraph className="mt-4">
|
||||
{t("home.bookmarks.edit.description")}
|
||||
</Paragraph>
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t("home.bookmarks.edit.titleLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={t("home.bookmarks.edit.titlePlaceholder")}
|
||||
className="w-full px-3 py-2 bg-background-main border border-background-secondary rounded-lg text-white text-sm placeholder:text-type-secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Year */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t("home.bookmarks.edit.yearLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={year || ""}
|
||||
onChange={(e) =>
|
||||
setYear(
|
||||
e.target.value ? parseInt(e.target.value, 10) : undefined,
|
||||
)
|
||||
}
|
||||
placeholder={t("home.bookmarks.edit.yearPlaceholder")}
|
||||
className="w-full px-3 py-2 bg-background-main border border-background-secondary rounded-lg text-white text-sm placeholder:text-type-secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Groups */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
{t("home.bookmarks.edit.groupsLabel")}
|
||||
</label>
|
||||
<GroupDropdown
|
||||
groups={allGroups}
|
||||
currentGroups={groups}
|
||||
onSelectGroups={setGroups}
|
||||
onCreateGroup={handleCreateGroup}
|
||||
onRemoveGroup={handleRemoveGroup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mt-6 justify-end">
|
||||
<Button theme="secondary" onClick={onCancel}>
|
||||
{t("home.bookmarks.edit.cancel")}
|
||||
</Button>
|
||||
<Button theme="purple" onClick={handleSave}>
|
||||
{t("home.bookmarks.edit.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCard>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
175
src/components/overlays/EditGroupModal.tsx
Normal file
175
src/components/overlays/EditGroupModal.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Modal, ModalCard } from "@/components/overlays/Modal";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
import { Heading2, Paragraph } from "@/components/utils/Text";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import {
|
||||
createGroupString,
|
||||
findBookmarksByGroup,
|
||||
parseGroupString,
|
||||
} from "@/utils/bookmarkModifications";
|
||||
|
||||
const userIconList = Object.values(UserIcons);
|
||||
|
||||
interface EditGroupModalProps {
|
||||
id: string;
|
||||
isShown: boolean;
|
||||
groupName: string | null;
|
||||
onCancel: () => void;
|
||||
onSave: (oldGroupName: string, newGroupName: string) => void;
|
||||
}
|
||||
|
||||
export function EditGroupModal({
|
||||
id,
|
||||
isShown,
|
||||
groupName,
|
||||
onCancel,
|
||||
onSave,
|
||||
}: EditGroupModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const [newGroupIcon, setNewGroupIcon] = useState<UserIcons>(
|
||||
UserIcons.BOOKMARK,
|
||||
);
|
||||
const [affectedBookmarks, setAffectedBookmarks] = useState<string[]>([]);
|
||||
|
||||
const getIconFromKey = (iconKey: string): UserIcons => {
|
||||
const key = iconKey.toUpperCase() as keyof typeof UserIcons;
|
||||
return UserIcons[key] || UserIcons.BOOKMARK;
|
||||
};
|
||||
|
||||
const getIconKey = (icon: UserIcons): string => {
|
||||
const entry = Object.entries(UserIcons).find(([, value]) => value === icon);
|
||||
return entry ? entry[0] : "BOOKMARK";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (groupName) {
|
||||
const { icon, name } = parseGroupString(groupName);
|
||||
setNewGroupName(name);
|
||||
setNewGroupIcon(getIconFromKey(icon || "BOOKMARK"));
|
||||
setAffectedBookmarks(findBookmarksByGroup(bookmarks, groupName));
|
||||
} else {
|
||||
setNewGroupName("");
|
||||
setNewGroupIcon(UserIcons.BOOKMARK);
|
||||
setAffectedBookmarks([]);
|
||||
}
|
||||
}, [groupName, bookmarks]);
|
||||
|
||||
const handleSave = () => {
|
||||
if (!groupName || !newGroupName.trim()) return;
|
||||
|
||||
const iconKey = getIconKey(newGroupIcon);
|
||||
const newGroupString = createGroupString(iconKey, newGroupName.trim());
|
||||
|
||||
if (newGroupString !== groupName) {
|
||||
onSave(groupName, newGroupString);
|
||||
}
|
||||
|
||||
onCancel();
|
||||
};
|
||||
|
||||
if (!isShown || !groupName) return null;
|
||||
|
||||
const { icon: currentIcon, name: currentName } = parseGroupString(groupName);
|
||||
const currentIconKey = currentIcon.toUpperCase() as keyof typeof UserIcons;
|
||||
const currentIconComponent = UserIcons[currentIconKey] || UserIcons.BOOKMARK;
|
||||
|
||||
return (
|
||||
<Modal id={id}>
|
||||
<ModalCard>
|
||||
<Heading2 className="!my-0">
|
||||
{t("home.bookmarks.groups.editGroup.title")}
|
||||
</Heading2>
|
||||
<Paragraph className="mt-4">
|
||||
{t("home.bookmarks.groups.editGroup.description")}
|
||||
</Paragraph>
|
||||
|
||||
{/* Current Group Info */}
|
||||
<div className="mt-4 p-3 bg-background-main rounded">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<UserIcon icon={currentIconComponent} className="w-5 h-5" />
|
||||
<span className="font-medium">{currentName}</span>
|
||||
</div>
|
||||
<p className="text-sm text-type-secondary">
|
||||
{t("home.bookmarks.groups.editGroup.affectsBookmarks", {
|
||||
count: affectedBookmarks.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mt-6">
|
||||
{/* New Group Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
{t("home.bookmarks.groups.editGroup.nameLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newGroupName}
|
||||
onChange={(e) => setNewGroupName(e.target.value)}
|
||||
placeholder={t("home.bookmarks.groups.editGroup.namePlaceholder")}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-background-main border border-border rounded text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
{newGroupName.trim().length > 0 && (
|
||||
<div className="flex items-center gap-2 flex-wrap pt-4 w-full justify-center">
|
||||
{userIconList.map((icon) => (
|
||||
<button
|
||||
type="button"
|
||||
key={icon}
|
||||
className={`rounded p-1 border-2 ${
|
||||
newGroupIcon === icon
|
||||
? "border-type-link bg-mediaCard-hoverBackground"
|
||||
: "border-transparent hover:border-background-secondary"
|
||||
}`}
|
||||
onClick={() => setNewGroupIcon(icon)}
|
||||
>
|
||||
<span className="w-5 h-5 flex items-center justify-center">
|
||||
<UserIcon
|
||||
icon={icon}
|
||||
className={`w-full h-full ${
|
||||
newGroupIcon === icon ? "text-type-link" : ""
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mt-6 justify-end">
|
||||
<Button theme="secondary" onClick={onCancel}>
|
||||
{t("home.bookmarks.groups.editGroup.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="purple"
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
!newGroupName.trim() ||
|
||||
createGroupString(
|
||||
getIconKey(newGroupIcon),
|
||||
newGroupName.trim(),
|
||||
) === groupName
|
||||
}
|
||||
>
|
||||
{t("home.bookmarks.groups.editGroup.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCard>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,6 +8,8 @@ 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";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
|
|
@ -94,6 +96,18 @@ export function BookmarksCarousel({
|
|||
const backendUrl = useBackendUrl();
|
||||
const account = useAuthStore((s) => s.account);
|
||||
|
||||
// Editing modals
|
||||
const editBookmarkModal = useModal("bookmark-edit-carousel");
|
||||
const editGroupModal = useModal("bookmark-edit-group-carousel");
|
||||
const [editingBookmarkId, setEditingBookmarkId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [editingGroupName, setEditingGroupName] = useState<string | null>(null);
|
||||
const modifyBookmarks = useBookmarkStore((s) => s.modifyBookmarks);
|
||||
const modifyBookmarksByGroup = useBookmarkStore(
|
||||
(s) => s.modifyBookmarksByGroup,
|
||||
);
|
||||
|
||||
// Group order editing state
|
||||
const groupOrder = useGroupOrderStore((s) => s.groupOrder);
|
||||
const setGroupOrder = useGroupOrderStore((s) => s.setGroupOrder);
|
||||
|
|
@ -328,6 +342,38 @@ export function BookmarksCarousel({
|
|||
}
|
||||
};
|
||||
|
||||
const handleEditBookmark = (bookmarkId: string) => {
|
||||
setEditingBookmarkId(bookmarkId);
|
||||
editBookmarkModal.show();
|
||||
};
|
||||
|
||||
const handleSaveBookmark = (bookmarkId: string, changes: any) => {
|
||||
modifyBookmarks([bookmarkId], changes);
|
||||
editBookmarkModal.hide();
|
||||
setEditingBookmarkId(null);
|
||||
};
|
||||
|
||||
const handleEditGroup = (groupName: string) => {
|
||||
setEditingGroupName(groupName);
|
||||
editGroupModal.show();
|
||||
};
|
||||
|
||||
const handleSaveGroup = (oldGroupName: string, newGroupName: string) => {
|
||||
modifyBookmarksByGroup({ oldGroupName, newGroupName });
|
||||
editGroupModal.hide();
|
||||
setEditingGroupName(null);
|
||||
};
|
||||
|
||||
const handleCancelEditBookmark = () => {
|
||||
editBookmarkModal.hide();
|
||||
setEditingBookmarkId(null);
|
||||
};
|
||||
|
||||
const handleCancelEditGroup = () => {
|
||||
editGroupModal.hide();
|
||||
setEditingGroupName(null);
|
||||
};
|
||||
|
||||
const categorySlug = "bookmarks";
|
||||
const SKELETON_COUNT = 10;
|
||||
|
||||
|
|
@ -360,6 +406,17 @@ export function BookmarksCarousel({
|
|||
secondaryText={t("home.bookmarks.groups.reorder.done")}
|
||||
/>
|
||||
)}
|
||||
{editing && section.group && (
|
||||
<EditButtonWithText
|
||||
editing={editing}
|
||||
onEdit={() => handleEditGroup(section.group!)}
|
||||
id="edit-group-button"
|
||||
text={t("home.bookmarks.groups.editGroup.title")}
|
||||
secondaryText={t(
|
||||
"home.bookmarks.groups.editGroup.cancel",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
|
|
@ -394,6 +451,8 @@ export function BookmarksCarousel({
|
|||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
editable={editing}
|
||||
onEdit={() => handleEditBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -467,6 +526,8 @@ export function BookmarksCarousel({
|
|||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
editable={editing}
|
||||
onEdit={() => handleEditBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
|
@ -506,6 +567,24 @@ export function BookmarksCarousel({
|
|||
setTempGroupOrder(newOrder);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Edit Bookmark Modal */}
|
||||
<EditBookmarkModal
|
||||
id={editBookmarkModal.id}
|
||||
isShown={editBookmarkModal.isShown}
|
||||
bookmarkId={editingBookmarkId}
|
||||
onCancel={handleCancelEditBookmark}
|
||||
onSave={handleSaveBookmark}
|
||||
/>
|
||||
|
||||
{/* Edit Group Modal */}
|
||||
<EditGroupModal
|
||||
id={editGroupModal.id}
|
||||
isShown={editGroupModal.isShown}
|
||||
groupName={editingGroupName}
|
||||
onCancel={handleCancelEditGroup}
|
||||
onSave={handleSaveGroup}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ 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";
|
||||
|
|
@ -46,9 +48,19 @@ export function BookmarksPart({
|
|||
const [editing, setEditing] = useState(false);
|
||||
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const editOrderModal = useModal("bookmark-edit-order");
|
||||
const editBookmarkModal = useModal("bookmark-edit");
|
||||
const editGroupModal = useModal("bookmark-edit-group");
|
||||
const [tempGroupOrder, setTempGroupOrder] = useState<string[]>([]);
|
||||
const [editingBookmarkId, setEditingBookmarkId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [editingGroupName, setEditingGroupName] = useState<string | null>(null);
|
||||
const backendUrl = useBackendUrl();
|
||||
const account = useAuthStore((s) => s.account);
|
||||
const modifyBookmarks = useBookmarkStore((s) => s.modifyBookmarks);
|
||||
const modifyBookmarksByGroup = useBookmarkStore(
|
||||
(s) => s.modifyBookmarksByGroup,
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
let output: MediaItem[] = [];
|
||||
|
|
@ -248,6 +260,38 @@ export function BookmarksPart({
|
|||
}
|
||||
};
|
||||
|
||||
const handleEditBookmark = (bookmarkId: string) => {
|
||||
setEditingBookmarkId(bookmarkId);
|
||||
editBookmarkModal.show();
|
||||
};
|
||||
|
||||
const handleSaveBookmark = (bookmarkId: string, changes: any) => {
|
||||
modifyBookmarks([bookmarkId], changes);
|
||||
editBookmarkModal.hide();
|
||||
setEditingBookmarkId(null);
|
||||
};
|
||||
|
||||
const handleEditGroup = (groupName: string) => {
|
||||
setEditingGroupName(groupName);
|
||||
editGroupModal.show();
|
||||
};
|
||||
|
||||
const handleSaveGroup = (oldGroupName: string, newGroupName: string) => {
|
||||
modifyBookmarksByGroup({ oldGroupName, newGroupName });
|
||||
editGroupModal.hide();
|
||||
setEditingGroupName(null);
|
||||
};
|
||||
|
||||
const handleCancelEditBookmark = () => {
|
||||
editBookmarkModal.hide();
|
||||
setEditingBookmarkId(null);
|
||||
};
|
||||
|
||||
const handleCancelEditGroup = () => {
|
||||
editGroupModal.hide();
|
||||
setEditingGroupName(null);
|
||||
};
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
|
|
@ -276,6 +320,17 @@ export function BookmarksPart({
|
|||
secondaryText={t("home.bookmarks.groups.reorder.done")}
|
||||
/>
|
||||
)}
|
||||
{editing && section.group && (
|
||||
<EditButtonWithText
|
||||
editing={editing}
|
||||
onEdit={() => handleEditGroup(section.group!)}
|
||||
id="edit-group-button"
|
||||
text={t("home.bookmarks.groups.editGroup.title")}
|
||||
secondaryText={t(
|
||||
"home.bookmarks.groups.editGroup.cancel",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
|
|
@ -290,12 +345,15 @@ export function BookmarksPart({
|
|||
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>
|
||||
))}
|
||||
|
|
@ -333,12 +391,15 @@ export function BookmarksPart({
|
|||
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>
|
||||
))}
|
||||
|
|
@ -359,6 +420,24 @@ export function BookmarksPart({
|
|||
setTempGroupOrder(newOrder);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Edit Bookmark Modal */}
|
||||
<EditBookmarkModal
|
||||
id={editBookmarkModal.id}
|
||||
isShown={editBookmarkModal.isShown}
|
||||
bookmarkId={editingBookmarkId}
|
||||
onCancel={handleCancelEditBookmark}
|
||||
onSave={handleSaveBookmark}
|
||||
/>
|
||||
|
||||
{/* Edit Group Modal */}
|
||||
<EditGroupModal
|
||||
id={editGroupModal.id}
|
||||
isShown={editGroupModal.isShown}
|
||||
groupName={editingGroupName}
|
||||
onCancel={handleCancelEditGroup}
|
||||
onSave={handleSaveGroup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@ import { persist } from "zustand/middleware";
|
|||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||
import {
|
||||
BookmarkModificationOptions,
|
||||
BookmarkModificationResult,
|
||||
BulkGroupModificationOptions,
|
||||
modifyBookmarks,
|
||||
modifyBookmarksByGroup,
|
||||
} from "@/utils/bookmarkModifications";
|
||||
|
||||
export interface BookmarkMediaItem {
|
||||
title: string;
|
||||
|
|
@ -40,6 +47,13 @@ export interface BookmarkStore {
|
|||
): void;
|
||||
isEpisodeFavorited(showId: string, episodeId: string): boolean;
|
||||
getFavoriteEpisodes(showId: string): string[];
|
||||
modifyBookmarks(
|
||||
bookmarkIds: string[],
|
||||
options: BookmarkModificationOptions,
|
||||
): BookmarkModificationResult;
|
||||
modifyBookmarksByGroup(
|
||||
options: BulkGroupModificationOptions,
|
||||
): BookmarkModificationResult;
|
||||
clear(): void;
|
||||
clearUpdateQueue(): void;
|
||||
removeUpdateItem(id: string): void;
|
||||
|
|
@ -186,6 +200,83 @@ export const useBookmarkStore = create(
|
|||
const bookmark = useBookmarkStore.getState().bookmarks[showId];
|
||||
return bookmark?.favoriteEpisodes ?? [];
|
||||
},
|
||||
modifyBookmarks(
|
||||
bookmarkIds: string[],
|
||||
options: BookmarkModificationOptions,
|
||||
): BookmarkModificationResult {
|
||||
let result: BookmarkModificationResult = {
|
||||
modifiedIds: [],
|
||||
hasChanges: false,
|
||||
};
|
||||
|
||||
set((s) => {
|
||||
const { modifiedBookmarks, result: modificationResult } =
|
||||
modifyBookmarks(s.bookmarks, bookmarkIds, options);
|
||||
s.bookmarks = modifiedBookmarks;
|
||||
result = modificationResult;
|
||||
|
||||
// Add to update queue for modified bookmarks
|
||||
if (result.hasChanges) {
|
||||
result.modifiedIds.forEach((bookmarkId) => {
|
||||
const bookmark = s.bookmarks[bookmarkId];
|
||||
if (bookmark) {
|
||||
updateId += 1;
|
||||
s.updateQueue.push({
|
||||
id: updateId.toString(),
|
||||
action: "add",
|
||||
tmdbId: bookmarkId,
|
||||
title: bookmark.title,
|
||||
year: bookmark.year,
|
||||
poster: bookmark.poster,
|
||||
type: bookmark.type,
|
||||
group: bookmark.group,
|
||||
favoriteEpisodes: bookmark.favoriteEpisodes,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
modifyBookmarksByGroup(
|
||||
options: BulkGroupModificationOptions,
|
||||
): BookmarkModificationResult {
|
||||
let result: BookmarkModificationResult = {
|
||||
modifiedIds: [],
|
||||
hasChanges: false,
|
||||
};
|
||||
|
||||
set((s) => {
|
||||
const { modifiedBookmarks, result: modificationResult } =
|
||||
modifyBookmarksByGroup(s.bookmarks, options);
|
||||
s.bookmarks = modifiedBookmarks;
|
||||
result = modificationResult;
|
||||
|
||||
// Add to update queue for modified bookmarks
|
||||
if (result.hasChanges) {
|
||||
result.modifiedIds.forEach((bookmarkId) => {
|
||||
const bookmark = s.bookmarks[bookmarkId];
|
||||
if (bookmark) {
|
||||
updateId += 1;
|
||||
s.updateQueue.push({
|
||||
id: updateId.toString(),
|
||||
action: "add",
|
||||
tmdbId: bookmarkId,
|
||||
title: bookmark.title,
|
||||
year: bookmark.year,
|
||||
poster: bookmark.poster,
|
||||
type: bookmark.type,
|
||||
group: bookmark.group,
|
||||
favoriteEpisodes: bookmark.favoriteEpisodes,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: "__MW::bookmarks",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ import { persist } from "zustand/middleware";
|
|||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||
import {
|
||||
ProgressModificationOptions,
|
||||
ProgressModificationResult,
|
||||
modifyProgressItems,
|
||||
} from "@/utils/progressModifications";
|
||||
|
||||
export { getProgressPercentage } from "./utils";
|
||||
|
||||
|
|
@ -63,6 +68,10 @@ export interface ProgressStore {
|
|||
updateItem(ops: UpdateItemOptions): void;
|
||||
removeItem(id: string): void;
|
||||
replaceItems(items: Record<string, ProgressMediaItem>): void;
|
||||
modifyProgressItems(
|
||||
progressIds: string[],
|
||||
options: ProgressModificationOptions,
|
||||
): ProgressModificationResult;
|
||||
clear(): void;
|
||||
clearUpdateQueue(): void;
|
||||
removeUpdateItem(id: string): void;
|
||||
|
|
@ -175,6 +184,44 @@ export const useProgressStore = create(
|
|||
s.updateQueue = [...s.updateQueue.filter((v) => v.id !== id)];
|
||||
});
|
||||
},
|
||||
modifyProgressItems(
|
||||
progressIds: string[],
|
||||
options: ProgressModificationOptions,
|
||||
): ProgressModificationResult {
|
||||
let result: ProgressModificationResult = {
|
||||
modifiedIds: [],
|
||||
hasChanges: false,
|
||||
};
|
||||
|
||||
set((s) => {
|
||||
const { modifiedProgressItems, result: modificationResult } =
|
||||
modifyProgressItems(s.items, progressIds, options);
|
||||
s.items = modifiedProgressItems;
|
||||
result = modificationResult;
|
||||
|
||||
// Add to update queue for modified progress items
|
||||
if (result.hasChanges) {
|
||||
result.modifiedIds.forEach((progressId) => {
|
||||
const progressItem = s.items[progressId];
|
||||
if (progressItem) {
|
||||
updateId += 1;
|
||||
s.updateQueue.push({
|
||||
id: updateId.toString(),
|
||||
action: "upsert",
|
||||
tmdbId: progressId,
|
||||
title: progressItem.title,
|
||||
year: progressItem.year,
|
||||
poster: progressItem.poster,
|
||||
type: progressItem.type,
|
||||
progress: progressItem.progress,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: "__MW::progress",
|
||||
|
|
|
|||
242
src/utils/bookmarkModifications.ts
Normal file
242
src/utils/bookmarkModifications.ts
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
import { BookmarkMediaItem } from "@/stores/bookmarks";
|
||||
|
||||
/**
|
||||
* Options for modifying bookmark properties
|
||||
*/
|
||||
export interface BookmarkModificationOptions {
|
||||
/** Update the title of the bookmark */
|
||||
title?: string;
|
||||
/** Update the year of the bookmark */
|
||||
year?: number;
|
||||
/** Update the poster URL of the bookmark */
|
||||
poster?: string;
|
||||
/** Update the groups array (replaces existing groups) */
|
||||
groups?: string[];
|
||||
/** Add groups to existing groups (doesn't remove existing ones) */
|
||||
addGroups?: string[];
|
||||
/** Remove specific groups from the bookmark */
|
||||
removeGroups?: string[];
|
||||
/** Update favorite episodes */
|
||||
favoriteEpisodes?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a bookmark modification operation
|
||||
*/
|
||||
export interface BookmarkModificationResult {
|
||||
/** IDs of bookmarks that were modified */
|
||||
modifiedIds: string[];
|
||||
/** Whether any bookmarks were actually changed */
|
||||
hasChanges: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a single bookmark item with the provided options
|
||||
*/
|
||||
export function modifyBookmark(
|
||||
bookmark: BookmarkMediaItem,
|
||||
options: BookmarkModificationOptions,
|
||||
): BookmarkMediaItem {
|
||||
const modified = { ...bookmark, updatedAt: Date.now() };
|
||||
|
||||
if (options.title !== undefined) {
|
||||
modified.title = options.title;
|
||||
}
|
||||
|
||||
if (options.year !== undefined) {
|
||||
modified.year = options.year;
|
||||
}
|
||||
|
||||
if (options.poster !== undefined) {
|
||||
modified.poster = options.poster;
|
||||
}
|
||||
|
||||
if (options.groups !== undefined) {
|
||||
modified.group = options.groups;
|
||||
}
|
||||
|
||||
if (options.addGroups && options.addGroups.length > 0) {
|
||||
const currentGroups = modified.group || [];
|
||||
const newGroups = [...currentGroups];
|
||||
options.addGroups.forEach((group) => {
|
||||
if (!newGroups.includes(group)) {
|
||||
newGroups.push(group);
|
||||
}
|
||||
});
|
||||
modified.group = newGroups;
|
||||
}
|
||||
|
||||
if (options.removeGroups && options.removeGroups.length > 0) {
|
||||
const currentGroups = modified.group || [];
|
||||
modified.group = currentGroups.filter(
|
||||
(group) => !options.removeGroups!.includes(group),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.favoriteEpisodes !== undefined) {
|
||||
modified.favoriteEpisodes = options.favoriteEpisodes;
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies multiple bookmarks by their IDs
|
||||
*/
|
||||
export function modifyBookmarks(
|
||||
bookmarks: Record<string, BookmarkMediaItem>,
|
||||
bookmarkIds: string[],
|
||||
options: BookmarkModificationOptions,
|
||||
): {
|
||||
modifiedBookmarks: Record<string, BookmarkMediaItem>;
|
||||
result: BookmarkModificationResult;
|
||||
} {
|
||||
const modifiedBookmarks = { ...bookmarks };
|
||||
const modifiedIds: string[] = [];
|
||||
let hasChanges = false;
|
||||
|
||||
bookmarkIds.forEach((id) => {
|
||||
const original = modifiedBookmarks[id];
|
||||
if (original) {
|
||||
const modified = modifyBookmark(original, options);
|
||||
modifiedBookmarks[id] = modified;
|
||||
modifiedIds.push(id);
|
||||
|
||||
// Check if anything actually changed
|
||||
if (!hasChanges) {
|
||||
hasChanges = Object.keys(options).some((key) => {
|
||||
const optionKey = key as keyof BookmarkModificationOptions;
|
||||
if (optionKey === "addGroups" || optionKey === "removeGroups")
|
||||
return true;
|
||||
|
||||
const optionValue = options[optionKey];
|
||||
const currentValue = modified[optionKey as keyof BookmarkMediaItem];
|
||||
|
||||
if (Array.isArray(optionValue) && Array.isArray(currentValue)) {
|
||||
return (
|
||||
optionValue.length !== currentValue.length ||
|
||||
!optionValue.every((val) => currentValue.includes(val))
|
||||
);
|
||||
}
|
||||
|
||||
return optionValue !== currentValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
modifiedBookmarks,
|
||||
result: { modifiedIds, hasChanges: hasChanges && modifiedIds.length > 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for bulk group modifications
|
||||
*/
|
||||
export interface BulkGroupModificationOptions {
|
||||
/** The old group name to replace */
|
||||
oldGroupName: string;
|
||||
/** The new group name */
|
||||
newGroupName: string;
|
||||
/** Whether to only modify bookmarks that have this as their only group */
|
||||
onlyIfExclusive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies all bookmarks that contain a specific group name
|
||||
*/
|
||||
export function modifyBookmarksByGroup(
|
||||
bookmarks: Record<string, BookmarkMediaItem>,
|
||||
options: BulkGroupModificationOptions,
|
||||
): {
|
||||
modifiedBookmarks: Record<string, BookmarkMediaItem>;
|
||||
result: BookmarkModificationResult;
|
||||
} {
|
||||
const modifiedBookmarks = { ...bookmarks };
|
||||
const modifiedIds: string[] = [];
|
||||
|
||||
Object.entries(bookmarks).forEach(([id, bookmark]) => {
|
||||
if (bookmark.group && bookmark.group.includes(options.oldGroupName)) {
|
||||
// Check if we should only modify exclusive groups
|
||||
if (options.onlyIfExclusive && bookmark.group.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newGroups = bookmark.group.map((group) =>
|
||||
group === options.oldGroupName ? options.newGroupName : group,
|
||||
);
|
||||
|
||||
modifiedBookmarks[id] = {
|
||||
...bookmark,
|
||||
group: newGroups,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
modifiedIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
modifiedBookmarks,
|
||||
result: { modifiedIds, hasChanges: modifiedIds.length > 0 },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all bookmarks that belong to a specific group
|
||||
*/
|
||||
export function findBookmarksByGroup(
|
||||
bookmarks: Record<string, BookmarkMediaItem>,
|
||||
groupName: string,
|
||||
): string[] {
|
||||
return Object.entries(bookmarks)
|
||||
.filter(([, bookmark]) => bookmark.group?.includes(groupName))
|
||||
.map(([id]) => id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all unique group names from bookmarks
|
||||
*/
|
||||
export function getAllGroupNames(
|
||||
bookmarks: Record<string, BookmarkMediaItem>,
|
||||
): string[] {
|
||||
const groups = new Set<string>();
|
||||
Object.values(bookmarks).forEach((bookmark) => {
|
||||
if (bookmark.group) {
|
||||
bookmark.group.forEach((group) => groups.add(group));
|
||||
}
|
||||
});
|
||||
return Array.from(groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a group name format
|
||||
*/
|
||||
export function isValidGroupName(groupName: string): boolean {
|
||||
// Group names should be non-empty and not contain only whitespace
|
||||
return groupName.trim().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a group string to extract icon and name components
|
||||
*/
|
||||
export function parseGroupString(group: string): {
|
||||
icon: string;
|
||||
name: string;
|
||||
} {
|
||||
const match = group.match(/^\[([a-zA-Z0-9_]+)\](.*)$/);
|
||||
if (match) {
|
||||
return { icon: match[1], name: match[2].trim() };
|
||||
}
|
||||
return { icon: "", name: group };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted group string from icon and name
|
||||
*/
|
||||
export function createGroupString(icon: string, name: string): string {
|
||||
if (icon && name) {
|
||||
return `[${icon}]${name}`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
103
src/utils/progressModifications.ts
Normal file
103
src/utils/progressModifications.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { ProgressItem, ProgressMediaItem } from "@/stores/progress";
|
||||
|
||||
/**
|
||||
* Options for modifying progress item properties
|
||||
*/
|
||||
export interface ProgressModificationOptions {
|
||||
/** Update the title of the progress item */
|
||||
title?: string;
|
||||
/** Update the year of the progress item */
|
||||
year?: number;
|
||||
/** Update the poster URL of the progress item */
|
||||
poster?: string;
|
||||
/** Update the overall progress for movies or shows */
|
||||
progress?: ProgressItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a progress modification operation
|
||||
*/
|
||||
export interface ProgressModificationResult {
|
||||
/** IDs of progress items that were modified */
|
||||
modifiedIds: string[];
|
||||
/** Whether any progress items were actually changed */
|
||||
hasChanges: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a single progress item with the provided options
|
||||
*/
|
||||
export function modifyProgressItem(
|
||||
progressItem: ProgressMediaItem,
|
||||
options: ProgressModificationOptions,
|
||||
): ProgressMediaItem {
|
||||
const modified = { ...progressItem, updatedAt: Date.now() };
|
||||
|
||||
if (options.title !== undefined) {
|
||||
modified.title = options.title;
|
||||
}
|
||||
|
||||
if (options.year !== undefined) {
|
||||
modified.year = options.year;
|
||||
}
|
||||
|
||||
if (options.poster !== undefined) {
|
||||
modified.poster = options.poster;
|
||||
}
|
||||
|
||||
if (options.progress !== undefined) {
|
||||
modified.progress = { ...options.progress };
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies multiple progress items by their IDs
|
||||
*/
|
||||
export function modifyProgressItems(
|
||||
progressItems: Record<string, ProgressMediaItem>,
|
||||
progressIds: string[],
|
||||
options: ProgressModificationOptions,
|
||||
): {
|
||||
modifiedProgressItems: Record<string, ProgressMediaItem>;
|
||||
result: ProgressModificationResult;
|
||||
} {
|
||||
const modifiedProgressItems = { ...progressItems };
|
||||
const modifiedIds: string[] = [];
|
||||
let hasChanges = false;
|
||||
|
||||
progressIds.forEach((id) => {
|
||||
const original = modifiedProgressItems[id];
|
||||
if (original) {
|
||||
const modified = modifyProgressItem(original, options);
|
||||
modifiedProgressItems[id] = modified;
|
||||
modifiedIds.push(id);
|
||||
|
||||
// Check if anything actually changed
|
||||
if (!hasChanges) {
|
||||
hasChanges = Object.keys(options).some((key) => {
|
||||
const optionKey = key as keyof ProgressModificationOptions;
|
||||
const optionValue = options[optionKey];
|
||||
const currentValue = modified[optionKey as keyof ProgressMediaItem];
|
||||
|
||||
if (optionKey === "progress" && optionValue && currentValue) {
|
||||
return (
|
||||
(optionValue as ProgressItem).watched !==
|
||||
(currentValue as ProgressItem).watched ||
|
||||
(optionValue as ProgressItem).duration !==
|
||||
(currentValue as ProgressItem).duration
|
||||
);
|
||||
}
|
||||
|
||||
return optionValue !== currentValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
modifiedProgressItems,
|
||||
result: { modifiedIds, hasChanges: hasChanges && modifiedIds.length > 0 },
|
||||
};
|
||||
}
|
||||
Loading…
Reference in a new issue