mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
add support for multiple groups
This commit is contained in:
parent
3a6b0fb2f2
commit
fe8533251c
7 changed files with 113 additions and 77 deletions
|
|
@ -10,7 +10,7 @@ export interface BookmarkMetaInput {
|
|||
year: number;
|
||||
poster?: string;
|
||||
type: string;
|
||||
group?: string;
|
||||
group?: string[];
|
||||
}
|
||||
|
||||
export interface BookmarkInput {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import { UserIcon, UserIcons } from "@/components/UserIcon";
|
|||
|
||||
interface GroupDropdownProps {
|
||||
groups: string[];
|
||||
currentGroup?: string;
|
||||
onSelectGroup: (group: string) => void;
|
||||
currentGroups: string[];
|
||||
onSelectGroups: (groups: string[]) => void;
|
||||
onCreateGroup: (group: string, icon: UserIcons) => void;
|
||||
onRemoveGroup: () => void;
|
||||
onRemoveGroup: (groupToRemove?: string) => void;
|
||||
}
|
||||
|
||||
const userIconList = Object.values(UserIcons);
|
||||
|
|
@ -26,8 +26,8 @@ function parseGroupString(group: string): { icon: UserIcons; name: string } {
|
|||
|
||||
export function GroupDropdown({
|
||||
groups,
|
||||
currentGroup,
|
||||
onSelectGroup,
|
||||
currentGroups,
|
||||
onSelectGroups,
|
||||
onCreateGroup,
|
||||
onRemoveGroup,
|
||||
}: GroupDropdownProps) {
|
||||
|
|
@ -36,11 +36,14 @@ export function GroupDropdown({
|
|||
const [showInput, setShowInput] = useState(false);
|
||||
const [selectedIcon, setSelectedIcon] = useState<UserIcons>(userIconList[0]);
|
||||
|
||||
const handleSelect = (group: string) => {
|
||||
setOpen(false);
|
||||
setShowInput(false);
|
||||
setNewGroup("");
|
||||
onSelectGroup(group);
|
||||
const handleToggleGroup = (group: string) => {
|
||||
let newGroups;
|
||||
if (currentGroups.includes(group)) {
|
||||
newGroups = currentGroups.filter((g) => g !== group);
|
||||
} else {
|
||||
newGroups = [...currentGroups, group];
|
||||
}
|
||||
onSelectGroups(newGroups);
|
||||
};
|
||||
|
||||
const handleCreate = (group: string, icon: UserIcons) => {
|
||||
|
|
@ -59,18 +62,21 @@ export function GroupDropdown({
|
|||
className="w-full px-3 py-2 text-xs bg-gray-700/50 border border-gray-600 rounded-lg text-white flex justify-between items-center"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
{currentGroup ? (
|
||||
(() => {
|
||||
const { icon, name } = parseGroupString(currentGroup);
|
||||
return (
|
||||
<span className="flex items-center gap-2 font-semibold text-purple-400">
|
||||
<span className="w-6 h-6 flex items-center justify-center">
|
||||
<UserIcon icon={icon} className="inline-block" />
|
||||
{currentGroups.length > 0 ? (
|
||||
<span className="flex flex-wrap gap-1 items-center">
|
||||
{currentGroups.map((group) => {
|
||||
const { icon, name } = parseGroupString(group);
|
||||
return (
|
||||
<span
|
||||
key={group}
|
||||
className="flex items-center gap-1 bg-purple-900/30 px-2 py-1 rounded text-purple-300 text-xs"
|
||||
>
|
||||
<UserIcon icon={icon} className="inline-block w-4 h-4" />
|
||||
{name}
|
||||
</span>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
})()
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-white/70">Add to group</span>
|
||||
)}
|
||||
|
|
@ -82,24 +88,23 @@ export function GroupDropdown({
|
|||
</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="absolute z-50 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg py-1 text-xs">
|
||||
<div className="absolute z-[150] mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg py-1 text-xs">
|
||||
{groups.length === 0 && !showInput && (
|
||||
<div className="px-4 py-2 text-gray-400">No groups</div>
|
||||
)}
|
||||
{groups.map((group) => {
|
||||
const { icon, name } = parseGroupString(group);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
<label
|
||||
key={group}
|
||||
className={`w-full text-left px-4 py-2 hover:bg-purple-700/30 rounded-md flex items-center gap-2 ${
|
||||
currentGroup === group
|
||||
? "text-purple-400 font-semibold"
|
||||
: "text-white"
|
||||
}`}
|
||||
onClick={() => handleSelect(group)}
|
||||
disabled={currentGroup === group}
|
||||
className="flex items-center gap-2 px-4 py-2 hover:bg-purple-700/30 rounded-md cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={currentGroups.includes(group)}
|
||||
onChange={() => handleToggleGroup(group)}
|
||||
className="accent-purple-400"
|
||||
/>
|
||||
<span className="w-5 h-5 flex items-center justify-center mr-2">
|
||||
<UserIcon
|
||||
icon={icon}
|
||||
|
|
@ -107,7 +112,7 @@ export function GroupDropdown({
|
|||
/>
|
||||
</span>
|
||||
{name}
|
||||
</button>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col gap-2 px-4 py-2">
|
||||
|
|
@ -159,17 +164,36 @@ export function GroupDropdown({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{currentGroup && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full text-left px-4 pt-3 pb-2 text-red-400 hover:bg-red-700/30 border-t border-gray-700"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onRemoveGroup();
|
||||
}}
|
||||
>
|
||||
Remove from group
|
||||
</button>
|
||||
{currentGroups.length > 0 && (
|
||||
<div className="border-t border-gray-700 pt-2 px-4">
|
||||
<div className="text-xs text-red-400 mb-1">
|
||||
Remove from group:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{currentGroups.map((group) => {
|
||||
const { icon, name } = parseGroupString(group);
|
||||
return (
|
||||
<button
|
||||
key={group}
|
||||
type="button"
|
||||
className="flex items-center gap-1 px-2 py-1 rounded bg-red-900/30 text-red-300 text-xs hover:bg-red-700/30"
|
||||
onClick={() => onRemoveGroup(group)}
|
||||
>
|
||||
<UserIcon icon={icon} className="inline-block w-4 h-4" />
|
||||
{name}
|
||||
<span className="ml-1">×</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 text-xs text-red-400 underline"
|
||||
onClick={() => onRemoveGroup()}
|
||||
>
|
||||
Remove all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import { IconPatch } from "../buttons/IconPatch";
|
|||
|
||||
interface MediaBookmarkProps {
|
||||
media: MediaItem;
|
||||
group?: string;
|
||||
group?: string[];
|
||||
}
|
||||
|
||||
export function MediaBookmarkButton({ media, group }: MediaBookmarkProps) {
|
||||
const addBookmark = useBookmarkStore((s) => s.addBookmark);
|
||||
const addBookmarkWithGroup = useBookmarkStore((s) => s.addBookmarkWithGroup);
|
||||
const addBookmarkWithGroups = useBookmarkStore(
|
||||
(s) => s.addBookmarkWithGroups,
|
||||
);
|
||||
const removeBookmark = useBookmarkStore((s) => s.removeBookmark);
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
const meta: PlayerMeta | undefined = useMemo(() => {
|
||||
|
|
@ -33,13 +35,13 @@ export function MediaBookmarkButton({ media, group }: MediaBookmarkProps) {
|
|||
const toggleBookmark = useCallback(() => {
|
||||
if (!meta) return;
|
||||
if (isBookmarked) removeBookmark(meta.tmdbId);
|
||||
else if (group) addBookmarkWithGroup(meta, group);
|
||||
else if (group && group.length > 0) addBookmarkWithGroups(meta, group);
|
||||
else addBookmark(meta);
|
||||
}, [
|
||||
isBookmarked,
|
||||
meta,
|
||||
addBookmark,
|
||||
addBookmarkWithGroup,
|
||||
addBookmarkWithGroups,
|
||||
removeBookmark,
|
||||
group,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -30,21 +30,22 @@ export function DetailsBody({
|
|||
const [releaseInfo, setReleaseInfo] = useState<TraktReleaseResponse | null>(
|
||||
null,
|
||||
);
|
||||
const addBookmarkWithGroup = useBookmarkStore((s) => s.addBookmarkWithGroup);
|
||||
const removeBookmark = useBookmarkStore((s) => s.removeBookmark);
|
||||
const addBookmark = useBookmarkStore((s) => s.addBookmark);
|
||||
const addBookmarkWithGroups = useBookmarkStore(
|
||||
(s) => s.addBookmarkWithGroups,
|
||||
);
|
||||
|
||||
const bookmarks = useBookmarkStore((s) => s.bookmarks);
|
||||
const currentGroup = bookmarks[data.id?.toString() ?? ""]?.group;
|
||||
const currentGroups = bookmarks[data.id?.toString() ?? ""]?.group || [];
|
||||
|
||||
const allGroups = Array.from(
|
||||
new Set(
|
||||
Object.values(bookmarks)
|
||||
.map((b) => b.group)
|
||||
.flatMap((b) => b.group || [])
|
||||
.filter(Boolean),
|
||||
),
|
||||
) as string[];
|
||||
|
||||
const handleSelectGroup = (group: string) => {
|
||||
const handleSelectGroups = (groups: string[]) => {
|
||||
if (!data.id) return;
|
||||
const meta = {
|
||||
tmdbId: data.id.toString(),
|
||||
|
|
@ -55,14 +56,14 @@ export function DetailsBody({
|
|||
: 0,
|
||||
poster: data.posterUrl,
|
||||
};
|
||||
addBookmarkWithGroup(meta, group);
|
||||
addBookmarkWithGroups(meta, groups);
|
||||
};
|
||||
|
||||
const handleCreateGroup = (group: string) => {
|
||||
handleSelectGroup(group);
|
||||
handleSelectGroups([...currentGroups, group]);
|
||||
};
|
||||
|
||||
const handleRemoveGroup = () => {
|
||||
const handleRemoveGroup = (groupToRemove?: string) => {
|
||||
if (!data.id) return;
|
||||
const meta = {
|
||||
tmdbId: data.id.toString(),
|
||||
|
|
@ -73,8 +74,13 @@ export function DetailsBody({
|
|||
: 0,
|
||||
poster: data.posterUrl,
|
||||
};
|
||||
removeBookmark(data.id.toString());
|
||||
addBookmark(meta);
|
||||
if (groupToRemove) {
|
||||
const newGroups = currentGroups.filter((g) => g !== groupToRemove);
|
||||
addBookmarkWithGroups(meta, newGroups);
|
||||
} else {
|
||||
// Remove all groups
|
||||
addBookmarkWithGroups(meta, []);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -266,8 +272,8 @@ export function DetailsBody({
|
|||
{/* Group Dropdown */}
|
||||
<GroupDropdown
|
||||
groups={allGroups}
|
||||
currentGroup={currentGroup}
|
||||
onSelectGroup={handleSelectGroup}
|
||||
currentGroups={currentGroups}
|
||||
onSelectGroups={handleSelectGroups}
|
||||
onCreateGroup={handleCreateGroup}
|
||||
onRemoveGroup={handleRemoveGroup}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -91,11 +91,13 @@ export function BookmarksCarousel({
|
|||
|
||||
items.forEach((item) => {
|
||||
const bookmark = bookmarks[item.id];
|
||||
if (bookmark?.group) {
|
||||
if (!grouped[bookmark.group]) {
|
||||
grouped[bookmark.group] = [];
|
||||
}
|
||||
grouped[bookmark.group].push(item);
|
||||
if (Array.isArray(bookmark?.group)) {
|
||||
bookmark.group.forEach((groupName) => {
|
||||
if (!grouped[groupName]) {
|
||||
grouped[groupName] = [];
|
||||
}
|
||||
grouped[groupName].push(item);
|
||||
});
|
||||
} else {
|
||||
regular.push(item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,11 +69,13 @@ export function BookmarksPart({
|
|||
|
||||
items.forEach((item) => {
|
||||
const bookmark = bookmarks[item.id];
|
||||
if (bookmark?.group) {
|
||||
if (!grouped[bookmark.group]) {
|
||||
grouped[bookmark.group] = [];
|
||||
}
|
||||
grouped[bookmark.group].push(item);
|
||||
if (Array.isArray(bookmark?.group)) {
|
||||
bookmark.group.forEach((groupName) => {
|
||||
if (!grouped[groupName]) {
|
||||
grouped[groupName] = [];
|
||||
}
|
||||
grouped[groupName].push(item);
|
||||
});
|
||||
} else {
|
||||
regular.push(item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface BookmarkMediaItem {
|
|||
poster?: string;
|
||||
type: "show" | "movie";
|
||||
updatedAt: number;
|
||||
group?: string;
|
||||
group?: string[];
|
||||
}
|
||||
|
||||
export interface BookmarkUpdateItem {
|
||||
|
|
@ -20,7 +20,7 @@ export interface BookmarkUpdateItem {
|
|||
id: string;
|
||||
poster?: string;
|
||||
type?: "show" | "movie";
|
||||
group?: string;
|
||||
group?: string[];
|
||||
action: "delete" | "add";
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export interface BookmarkStore {
|
|||
bookmarks: Record<string, BookmarkMediaItem>;
|
||||
updateQueue: BookmarkUpdateItem[];
|
||||
addBookmark(meta: PlayerMeta): void;
|
||||
addBookmarkWithGroup(meta: PlayerMeta, group?: string): void;
|
||||
addBookmarkWithGroups(meta: PlayerMeta, groups?: string[]): void;
|
||||
removeBookmark(id: string): void;
|
||||
replaceBookmarks(items: Record<string, BookmarkMediaItem>): void;
|
||||
clear(): void;
|
||||
|
|
@ -77,7 +77,7 @@ export const useBookmarkStore = create(
|
|||
};
|
||||
});
|
||||
},
|
||||
addBookmarkWithGroup(meta, group) {
|
||||
addBookmarkWithGroups(meta, groups) {
|
||||
set((s) => {
|
||||
updateId += 1;
|
||||
s.updateQueue.push({
|
||||
|
|
@ -88,7 +88,7 @@ export const useBookmarkStore = create(
|
|||
title: meta.title,
|
||||
year: meta.releaseYear,
|
||||
poster: meta.poster,
|
||||
group,
|
||||
group: groups,
|
||||
});
|
||||
|
||||
s.bookmarks[meta.tmdbId] = {
|
||||
|
|
@ -97,7 +97,7 @@ export const useBookmarkStore = create(
|
|||
year: meta.releaseYear,
|
||||
poster: meta.poster,
|
||||
updatedAt: Date.now(),
|
||||
group,
|
||||
group: groups,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue