mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-14 04:00:32 +00:00
add dedicated bookmarks page
This commit is contained in:
parent
2aece97408
commit
9cccb2637c
4 changed files with 548 additions and 45 deletions
|
|
@ -242,6 +242,7 @@
|
|||
"home": {
|
||||
"bookmarks": {
|
||||
"sectionTitle": "Bookmarks",
|
||||
"showAll": "Show all",
|
||||
"groups": {
|
||||
"dropdown": {
|
||||
"placeholderButton": "Add to group",
|
||||
|
|
|
|||
454
src/pages/bookmarks/AllBookmarks.tsx
Normal file
454
src/pages/bookmarks/AllBookmarks.tsx
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { EditButton } from "@/components/buttons/EditButton";
|
||||
import { EditButtonWithText } from "@/components/buttons/EditButtonWithText";
|
||||
import { Item, SortableList } from "@/components/form/SortableList";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { DetailsModal } from "@/components/overlays/details/DetailsModal";
|
||||
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
import { Heading1, Heading2, Paragraph } from "@/components/utils/Text";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import { useRandomTranslation } from "@/hooks/useRandomTranslation";
|
||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useGroupOrderStore } from "@/stores/groupOrder";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
function parseGroupString(group: string): { icon: UserIcons; name: string } {
|
||||
const match = group.match(/^\[([a-zA-Z0-9_]+)\](.*)$/);
|
||||
if (match) {
|
||||
const iconKey = match[1].toUpperCase() as keyof typeof UserIcons;
|
||||
const icon = UserIcons[iconKey] || UserIcons.BOOKMARK;
|
||||
const name = match[2].trim();
|
||||
return { icon, name };
|
||||
}
|
||||
return { icon: UserIcons.BOOKMARK, name: group };
|
||||
}
|
||||
|
||||
interface AllBookmarksProps {
|
||||
onShowDetails?: (media: MediaItem) => void;
|
||||
}
|
||||
|
||||
export function AllBookmarks({ onShowDetails }: AllBookmarksProps) {
|
||||
const { t } = useTranslation();
|
||||
const { t: randomT } = useRandomTranslation();
|
||||
const emptyText = randomT(`home.search.empty`);
|
||||
const navigate = useNavigate();
|
||||
const progressItems = useProgressStore((s) => s.items);
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
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<HTMLDivElement>();
|
||||
const editOrderModal = useModal("bookmark-edit-order-all");
|
||||
const [tempGroupOrder, setTempGroupOrder] = useState<string[]>([]);
|
||||
const backendUrl = useBackendUrl();
|
||||
const account = useAuthStore((s) => s.account);
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const { showModal } = useOverlayStack();
|
||||
|
||||
const handleShowDetails = async (media: MediaItem) => {
|
||||
if (onShowDetails) {
|
||||
onShowDetails(media);
|
||||
} else {
|
||||
setDetailsData({
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
showModal("details");
|
||||
}
|
||||
};
|
||||
|
||||
const items = useMemo(() => {
|
||||
let output: MediaItem[] = [];
|
||||
Object.entries(bookmarks).forEach((entry) => {
|
||||
output.push({
|
||||
id: entry[0],
|
||||
...entry[1],
|
||||
});
|
||||
});
|
||||
output = output.sort((a, b) => {
|
||||
const bookmarkA = bookmarks[a.id];
|
||||
const bookmarkB = bookmarks[b.id];
|
||||
const progressA = progressItems[a.id];
|
||||
const progressB = progressItems[b.id];
|
||||
|
||||
const dateA = Math.max(bookmarkA.updatedAt, progressA?.updatedAt ?? 0);
|
||||
const dateB = Math.max(bookmarkB.updatedAt, progressB?.updatedAt ?? 0);
|
||||
|
||||
return dateB - dateA;
|
||||
});
|
||||
return output;
|
||||
}, [bookmarks, progressItems]);
|
||||
|
||||
const { groupedItems, regularItems } = useMemo(() => {
|
||||
const grouped: Record<string, MediaItem[]> = {};
|
||||
const regular: MediaItem[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
const bookmark = bookmarks[item.id];
|
||||
if (Array.isArray(bookmark?.group)) {
|
||||
bookmark.group.forEach((groupName) => {
|
||||
if (!grouped[groupName]) {
|
||||
grouped[groupName] = [];
|
||||
}
|
||||
grouped[groupName].push(item);
|
||||
});
|
||||
} else {
|
||||
regular.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort items within each group by date
|
||||
Object.keys(grouped).forEach((group) => {
|
||||
grouped[group].sort((a, b) => {
|
||||
const bookmarkA = bookmarks[a.id];
|
||||
const bookmarkB = bookmarks[b.id];
|
||||
const progressA = progressItems[a.id];
|
||||
const progressB = progressItems[b.id];
|
||||
|
||||
const dateA = Math.max(bookmarkA.updatedAt, progressA?.updatedAt ?? 0);
|
||||
const dateB = Math.max(bookmarkB.updatedAt, progressB?.updatedAt ?? 0);
|
||||
|
||||
return dateB - dateA;
|
||||
});
|
||||
});
|
||||
|
||||
return { groupedItems: grouped, regularItems: regular };
|
||||
}, [items, bookmarks, progressItems]);
|
||||
|
||||
// group sorting
|
||||
const allGroups = useMemo(() => {
|
||||
const groups = new Set<string>();
|
||||
|
||||
Object.values(bookmarks).forEach((bookmark) => {
|
||||
if (Array.isArray(bookmark.group)) {
|
||||
bookmark.group.forEach((group) => groups.add(group));
|
||||
}
|
||||
});
|
||||
|
||||
groups.add("bookmarks");
|
||||
|
||||
return Array.from(groups);
|
||||
}, [bookmarks]);
|
||||
|
||||
const sortableItems = useMemo(() => {
|
||||
const currentOrder = editOrderModal.isShown ? tempGroupOrder : groupOrder;
|
||||
|
||||
if (currentOrder.length === 0) {
|
||||
return allGroups.map((group) => {
|
||||
const { name } = parseGroupString(group);
|
||||
return {
|
||||
id: group,
|
||||
name: group === "bookmarks" ? t("home.bookmarks.sectionTitle") : name,
|
||||
} as Item;
|
||||
});
|
||||
}
|
||||
|
||||
const orderMap = new Map(
|
||||
currentOrder.map((group, index) => [group, index]),
|
||||
);
|
||||
const sortedGroups = allGroups.sort((groupA, groupB) => {
|
||||
const orderA = orderMap.has(groupA)
|
||||
? orderMap.get(groupA)!
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const orderB = orderMap.has(groupB)
|
||||
? orderMap.get(groupB)!
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
return sortedGroups.map((group) => {
|
||||
const { name } = parseGroupString(group);
|
||||
return {
|
||||
id: group,
|
||||
name: group === "bookmarks" ? t("home.bookmarks.sectionTitle") : name,
|
||||
} as Item;
|
||||
});
|
||||
}, [allGroups, t, editOrderModal.isShown, tempGroupOrder, groupOrder]);
|
||||
|
||||
const sortedSections = useMemo(() => {
|
||||
const sections: Array<{
|
||||
type: "grouped" | "regular";
|
||||
group?: string;
|
||||
items: MediaItem[];
|
||||
}> = [];
|
||||
|
||||
const allSections = new Map<string, MediaItem[]>();
|
||||
|
||||
Object.entries(groupedItems).forEach(([group, groupItems]) => {
|
||||
allSections.set(group, groupItems);
|
||||
});
|
||||
|
||||
if (regularItems.length > 0) {
|
||||
allSections.set("bookmarks", regularItems);
|
||||
}
|
||||
|
||||
if (groupOrder.length === 0) {
|
||||
allSections.forEach((sectionItems, group) => {
|
||||
if (group === "bookmarks") {
|
||||
sections.push({ type: "regular", items: sectionItems });
|
||||
} else {
|
||||
sections.push({ type: "grouped", group, items: sectionItems });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const orderMap = new Map(
|
||||
groupOrder.map((group, index) => [group, index]),
|
||||
);
|
||||
|
||||
Array.from(allSections.entries())
|
||||
.sort(([groupA], [groupB]) => {
|
||||
const orderA = orderMap.has(groupA)
|
||||
? orderMap.get(groupA)!
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const orderB = orderMap.has(groupB)
|
||||
? orderMap.get(groupB)!
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
return orderA - orderB;
|
||||
})
|
||||
.forEach(([group, sectionItems]) => {
|
||||
if (group === "bookmarks") {
|
||||
sections.push({ type: "regular", items: sectionItems });
|
||||
} else {
|
||||
sections.push({ type: "grouped", group, items: sectionItems });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
}, [groupedItems, regularItems, groupOrder]);
|
||||
|
||||
const handleEditGroupOrder = () => {
|
||||
// Initialize with current order or default order
|
||||
if (groupOrder.length === 0) {
|
||||
const defaultOrder = allGroups.map((group) => group);
|
||||
setTempGroupOrder(defaultOrder);
|
||||
} else {
|
||||
setTempGroupOrder([...groupOrder]);
|
||||
}
|
||||
editOrderModal.show();
|
||||
};
|
||||
|
||||
const handleReorderClick = () => {
|
||||
handleEditGroupOrder();
|
||||
// Keep editing state active by setting it to true
|
||||
setEditing(true);
|
||||
};
|
||||
|
||||
const handleCancelOrder = () => {
|
||||
editOrderModal.hide();
|
||||
};
|
||||
|
||||
const handleSaveOrderClick = () => {
|
||||
setGroupOrder(tempGroupOrder);
|
||||
editOrderModal.hide();
|
||||
|
||||
// Save to backend
|
||||
if (backendUrl && account) {
|
||||
useGroupOrderStore
|
||||
.getState()
|
||||
.saveGroupOrderToBackend(backendUrl, account);
|
||||
}
|
||||
};
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<WideContainer>
|
||||
<div className="flex flex-col items-center justify-center translate-y-1/2">
|
||||
<p className="text-[18.5px] pb-3">{emptyText}</p>
|
||||
<Button
|
||||
theme="purple"
|
||||
onClick={() => navigate("/")}
|
||||
className="mt-4"
|
||||
>
|
||||
{t("notFound.goHome")}
|
||||
</Button>
|
||||
</div>
|
||||
</WideContainer>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<WideContainer>
|
||||
<div className="flex items-center justify-between gap-8">
|
||||
<Heading1 className="text-2xl font-bold text-white">
|
||||
{t("home.bookmarks.sectionTitle")}
|
||||
</Heading1>
|
||||
<div className="flex items-center gap-2">
|
||||
{editing && allGroups.length > 1 && (
|
||||
<EditButtonWithText
|
||||
editing={editing}
|
||||
onEdit={handleReorderClick}
|
||||
id="edit-group-order-button-all"
|
||||
text={t("home.bookmarks.groups.reorder.button")}
|
||||
secondaryText={t("home.bookmarks.groups.reorder.done")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 pb-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/")}
|
||||
className="flex items-center text-white hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<Icon icon={Icons.ARROW_LEFT} className="text-xl" />
|
||||
<span className="ml-2">{t("discover.page.back")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`relative ${editOrderModal.isShown ? "pointer-events-none" : ""}`}
|
||||
>
|
||||
{/* Grouped Bookmarks */}
|
||||
{sortedSections.map((section) => {
|
||||
if (section.type === "grouped") {
|
||||
const { icon, name } = parseGroupString(section.group || "");
|
||||
return (
|
||||
<div key={section.group || "bookmarks"} className="mb-6">
|
||||
<SectionHeading
|
||||
title={name}
|
||||
customIcon={
|
||||
<span className="w-6 h-6 flex items-center justify-center">
|
||||
<UserIcon icon={icon} className="w-full h-full" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{editing && allGroups.length > 1 && (
|
||||
<EditButtonWithText
|
||||
editing={editing}
|
||||
onEdit={handleReorderClick}
|
||||
id="edit-group-order-button"
|
||||
text={t("home.bookmarks.groups.reorder.button")}
|
||||
secondaryText={t(
|
||||
"home.bookmarks.groups.reorder.done",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id={`edit-button-bookmark-${section.group}`}
|
||||
/>
|
||||
</div>
|
||||
</SectionHeading>
|
||||
<MediaGrid>
|
||||
{section.items.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
>
|
||||
<WatchedMediaCard
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(v.id)}
|
||||
onShowDetails={handleShowDetails}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</div>
|
||||
);
|
||||
} // regular items
|
||||
return (
|
||||
<div key="regular-bookmarks" className="mb-6">
|
||||
<SectionHeading
|
||||
title={t("home.bookmarks.sectionTitle")}
|
||||
icon={Icons.BOOKMARK}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{editing && allGroups.length > 1 && (
|
||||
<EditButtonWithText
|
||||
editing={editing}
|
||||
onEdit={handleReorderClick}
|
||||
id="edit-group-order-button"
|
||||
text={t("home.bookmarks.groups.reorder.button")}
|
||||
secondaryText={t("home.bookmarks.groups.reorder.done")}
|
||||
/>
|
||||
)}
|
||||
<EditButton
|
||||
editing={editing}
|
||||
onEdit={setEditing}
|
||||
id="edit-button-bookmark"
|
||||
/>
|
||||
</div>
|
||||
</SectionHeading>
|
||||
<MediaGrid ref={gridRef}>
|
||||
{section.items.map((v) => (
|
||||
<div
|
||||
key={v.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
>
|
||||
<WatchedMediaCard
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(v.id)}
|
||||
onShowDetails={handleShowDetails}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Edit Order Modal */}
|
||||
<Modal id={editOrderModal.id}>
|
||||
<ModalCard>
|
||||
<Heading2 className="!mt-0">
|
||||
{t("home.bookmarks.groups.reorder.title")}
|
||||
</Heading2>
|
||||
<Paragraph>
|
||||
{t("home.bookmarks.groups.reorder.description")}
|
||||
</Paragraph>
|
||||
<div className="mt-6">
|
||||
<SortableList
|
||||
items={sortableItems}
|
||||
setItems={(newItems) => {
|
||||
const newOrder = newItems.map((item) => item.id);
|
||||
setTempGroupOrder(newOrder);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-4 mt-6 justify-end">
|
||||
<Button theme="secondary" onClick={handleCancelOrder}>
|
||||
{t("home.bookmarks.groups.reorder.cancel")}
|
||||
</Button>
|
||||
<Button theme="purple" onClick={handleSaveOrderClick}>
|
||||
{t("home.bookmarks.groups.reorder.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCard>
|
||||
</Modal>
|
||||
|
||||
{detailsData && <DetailsModal id="details" data={detailsData} />}
|
||||
</WideContainer>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { EditButton } from "@/components/buttons/EditButton";
|
||||
import { EditButtonWithText } from "@/components/buttons/EditButtonWithText";
|
||||
import { Item, SortableList } from "@/components/form/SortableList";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
import { Heading2, Paragraph } from "@/components/utils/Text";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
|
|
@ -39,6 +41,7 @@ interface BookmarksCarouselProps {
|
|||
}
|
||||
|
||||
const LONG_PRESS_DURATION = 500; // 0.5 seconds
|
||||
const MAX_ITEMS_PER_SECTION = 20; // Limit items per section
|
||||
|
||||
function MediaCardSkeleton() {
|
||||
return (
|
||||
|
|
@ -51,6 +54,36 @@ function MediaCardSkeleton() {
|
|||
);
|
||||
}
|
||||
|
||||
function MoreBookmarksCard() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto">
|
||||
<Link to="/bookmarks" className="block">
|
||||
<Flare.Base className="group -m-[0.705em] h-[20rem] hover:scale-95 transition-all rounded-xl bg-background-main duration-300 hover:bg-mediaCard-hoverBackground tabbable">
|
||||
<Flare.Light
|
||||
flareSize={300}
|
||||
cssColorVar="--colors-mediaCard-hoverAccent"
|
||||
backgroundClass="bg-mediaCard-hoverBackground duration-100"
|
||||
className="rounded-xl bg-background-main group-hover:opacity-100"
|
||||
/>
|
||||
<Flare.Child className="pointer-events-auto h-[20rem] relative mb-2 p-[0.4em] transition-transform duration-300">
|
||||
<div className="flex absolute inset-0 flex-col items-center justify-center">
|
||||
<Icon
|
||||
icon={Icons.ARROW_RIGHT}
|
||||
className="text-4xl mb-2 transition-transform duration-300"
|
||||
/>
|
||||
<span className="text-sm text-center px-2">
|
||||
{t("home.bookmarks.showAll")}
|
||||
</span>
|
||||
</div>
|
||||
</Flare.Child>
|
||||
</Flare.Base>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BookmarksCarousel({
|
||||
carouselRefs,
|
||||
onShowDetails,
|
||||
|
|
@ -396,7 +429,7 @@ export function BookmarksCarousel({
|
|||
if (section.type === "grouped") {
|
||||
const { icon, name } = parseGroupString(section.group || "");
|
||||
return (
|
||||
<div key={section.group || "bookmarks"}>
|
||||
<div key={section.group}>
|
||||
<SectionHeading
|
||||
title={name}
|
||||
customIcon={
|
||||
|
|
@ -432,28 +465,34 @@ export function BookmarksCarousel({
|
|||
>
|
||||
<div className="md:w-12" />
|
||||
|
||||
{section.items.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
{section.items
|
||||
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||
.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
key={media.id}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{section.items.length > MAX_ITEMS_PER_SECTION && (
|
||||
<MoreBookmarksCard />
|
||||
)}
|
||||
|
||||
<div className="md:w-12" />
|
||||
</div>
|
||||
|
|
@ -472,7 +511,7 @@ export function BookmarksCarousel({
|
|||
return (
|
||||
<div key="regular-bookmarks">
|
||||
<SectionHeading
|
||||
title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
|
||||
title={t("home.bookmarks.sectionTitle")}
|
||||
icon={Icons.BOOKMARK}
|
||||
className="ml-4 md:ml-12 mt-2 -mb-5"
|
||||
>
|
||||
|
|
@ -503,34 +542,40 @@ export function BookmarksCarousel({
|
|||
<div className="md:w-12" />
|
||||
|
||||
{section.items.length > 0
|
||||
? section.items.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
? section.items
|
||||
.slice(0, MAX_ITEMS_PER_SECTION)
|
||||
.map((media) => (
|
||||
<div
|
||||
key={media.id}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
style={{ userSelect: "none" }}
|
||||
onContextMenu={(
|
||||
e: React.MouseEvent<HTMLDivElement>,
|
||||
) => e.preventDefault()}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||
>
|
||||
<WatchedMediaCard
|
||||
key={media.id}
|
||||
media={media}
|
||||
onShowDetails={onShowDetails}
|
||||
closable={editing}
|
||||
onClose={() => removeBookmark(media.id)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: Array.from({ length: SKELETON_COUNT }).map(() => (
|
||||
<MediaCardSkeleton
|
||||
key={`skeleton-${categorySlug}-${Math.random().toString(36).substring(7)}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
{section.items.length > MAX_ITEMS_PER_SECTION && (
|
||||
<MoreBookmarksCard />
|
||||
)}
|
||||
|
||||
<div className="md:w-12" />
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
|
|||
import { useOnlineListener } from "@/hooks/usePing";
|
||||
import { AboutPage } from "@/pages/About";
|
||||
import { AdminPage } from "@/pages/admin/AdminPage";
|
||||
import { AllBookmarks } from "@/pages/bookmarks/AllBookmarks";
|
||||
import VideoTesterView from "@/pages/developer/VideoTesterView";
|
||||
import { DiscoverMore } from "@/pages/discover/AllMovieLists";
|
||||
import { Discover } from "@/pages/discover/Discover";
|
||||
|
|
@ -182,6 +183,8 @@ function App() {
|
|||
/>
|
||||
<Route path="/discover/more/:category" element={<MoreContent />} />
|
||||
<Route path="/discover/all" element={<DiscoverMore />} />
|
||||
{/* Bookmarks page */}
|
||||
<Route path="/bookmarks" element={<AllBookmarks />} />
|
||||
{/* Settings page */}
|
||||
<Route
|
||||
path="/settings"
|
||||
|
|
|
|||
Loading…
Reference in a new issue