pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add support for multiple groups

Pas fe853325 3a6b0fb2

+113 -77
+1 -1
src/backend/accounts/bookmarks.ts
··· 10 10 year: number; 11 11 poster?: string; 12 12 type: string; 13 - group?: string; 13 + group?: string[]; 14 14 } 15 15 16 16 export interface BookmarkInput {
+67 -43
src/components/form/GroupDropdown.tsx
··· 5 5 6 6 interface GroupDropdownProps { 7 7 groups: string[]; 8 - currentGroup?: string; 9 - onSelectGroup: (group: string) => void; 8 + currentGroups: string[]; 9 + onSelectGroups: (groups: string[]) => void; 10 10 onCreateGroup: (group: string, icon: UserIcons) => void; 11 - onRemoveGroup: () => void; 11 + onRemoveGroup: (groupToRemove?: string) => void; 12 12 } 13 13 14 14 const userIconList = Object.values(UserIcons); ··· 26 26 27 27 export function GroupDropdown({ 28 28 groups, 29 - currentGroup, 30 - onSelectGroup, 29 + currentGroups, 30 + onSelectGroups, 31 31 onCreateGroup, 32 32 onRemoveGroup, 33 33 }: GroupDropdownProps) { ··· 36 36 const [showInput, setShowInput] = useState(false); 37 37 const [selectedIcon, setSelectedIcon] = useState<UserIcons>(userIconList[0]); 38 38 39 - const handleSelect = (group: string) => { 40 - setOpen(false); 41 - setShowInput(false); 42 - setNewGroup(""); 43 - onSelectGroup(group); 39 + const handleToggleGroup = (group: string) => { 40 + let newGroups; 41 + if (currentGroups.includes(group)) { 42 + newGroups = currentGroups.filter((g) => g !== group); 43 + } else { 44 + newGroups = [...currentGroups, group]; 45 + } 46 + onSelectGroups(newGroups); 44 47 }; 45 48 46 49 const handleCreate = (group: string, icon: UserIcons) => { ··· 59 62 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" 60 63 onClick={() => setOpen((v) => !v)} 61 64 > 62 - {currentGroup ? ( 63 - (() => { 64 - const { icon, name } = parseGroupString(currentGroup); 65 - return ( 66 - <span className="flex items-center gap-2 font-semibold text-purple-400"> 67 - <span className="w-6 h-6 flex items-center justify-center"> 68 - <UserIcon icon={icon} className="inline-block" /> 65 + {currentGroups.length > 0 ? ( 66 + <span className="flex flex-wrap gap-1 items-center"> 67 + {currentGroups.map((group) => { 68 + const { icon, name } = parseGroupString(group); 69 + return ( 70 + <span 71 + key={group} 72 + className="flex items-center gap-1 bg-purple-900/30 px-2 py-1 rounded text-purple-300 text-xs" 73 + > 74 + <UserIcon icon={icon} className="inline-block w-4 h-4" /> 75 + {name} 69 76 </span> 70 - {name} 71 - </span> 72 - ); 73 - })() 77 + ); 78 + })} 79 + </span> 74 80 ) : ( 75 81 <span className="text-white/70">Add to group</span> 76 82 )} ··· 82 88 </span> 83 89 </button> 84 90 {open && ( 85 - <div className="absolute z-50 mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg py-1 text-xs"> 91 + <div className="absolute z-[150] mt-1 w-full bg-gray-800 border border-gray-700 rounded-lg shadow-lg py-1 text-xs"> 86 92 {groups.length === 0 && !showInput && ( 87 93 <div className="px-4 py-2 text-gray-400">No groups</div> 88 94 )} 89 95 {groups.map((group) => { 90 96 const { icon, name } = parseGroupString(group); 91 97 return ( 92 - <button 93 - type="button" 98 + <label 94 99 key={group} 95 - className={`w-full text-left px-4 py-2 hover:bg-purple-700/30 rounded-md flex items-center gap-2 ${ 96 - currentGroup === group 97 - ? "text-purple-400 font-semibold" 98 - : "text-white" 99 - }`} 100 - onClick={() => handleSelect(group)} 101 - disabled={currentGroup === group} 100 + className="flex items-center gap-2 px-4 py-2 hover:bg-purple-700/30 rounded-md cursor-pointer" 102 101 > 102 + <input 103 + type="checkbox" 104 + checked={currentGroups.includes(group)} 105 + onChange={() => handleToggleGroup(group)} 106 + className="accent-purple-400" 107 + /> 103 108 <span className="w-5 h-5 flex items-center justify-center mr-2"> 104 109 <UserIcon 105 110 icon={icon} ··· 107 112 /> 108 113 </span> 109 114 {name} 110 - </button> 115 + </label> 111 116 ); 112 117 })} 113 118 <div className="flex flex-col gap-2 px-4 py-2"> ··· 159 164 </div> 160 165 )} 161 166 </div> 162 - {currentGroup && ( 163 - <button 164 - type="button" 165 - className="w-full text-left px-4 pt-3 pb-2 text-red-400 hover:bg-red-700/30 border-t border-gray-700" 166 - onClick={() => { 167 - setOpen(false); 168 - onRemoveGroup(); 169 - }} 170 - > 171 - Remove from group 172 - </button> 167 + {currentGroups.length > 0 && ( 168 + <div className="border-t border-gray-700 pt-2 px-4"> 169 + <div className="text-xs text-red-400 mb-1"> 170 + Remove from group: 171 + </div> 172 + <div className="flex flex-wrap gap-2"> 173 + {currentGroups.map((group) => { 174 + const { icon, name } = parseGroupString(group); 175 + return ( 176 + <button 177 + key={group} 178 + type="button" 179 + 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" 180 + onClick={() => onRemoveGroup(group)} 181 + > 182 + <UserIcon icon={icon} className="inline-block w-4 h-4" /> 183 + {name} 184 + <span className="ml-1">&times;</span> 185 + </button> 186 + ); 187 + })} 188 + <button 189 + type="button" 190 + className="ml-2 text-xs text-red-400 underline" 191 + onClick={() => onRemoveGroup()} 192 + > 193 + Remove all 194 + </button> 195 + </div> 196 + </div> 173 197 )} 174 198 </div> 175 199 )}
+6 -4
src/components/media/MediaBookmark.tsx
··· 9 9 10 10 interface MediaBookmarkProps { 11 11 media: MediaItem; 12 - group?: string; 12 + group?: string[]; 13 13 } 14 14 15 15 export function MediaBookmarkButton({ media, group }: MediaBookmarkProps) { 16 16 const addBookmark = useBookmarkStore((s) => s.addBookmark); 17 - const addBookmarkWithGroup = useBookmarkStore((s) => s.addBookmarkWithGroup); 17 + const addBookmarkWithGroups = useBookmarkStore( 18 + (s) => s.addBookmarkWithGroups, 19 + ); 18 20 const removeBookmark = useBookmarkStore((s) => s.removeBookmark); 19 21 const bookmarks = useBookmarkStore((s) => s.bookmarks); 20 22 const meta: PlayerMeta | undefined = useMemo(() => { ··· 33 35 const toggleBookmark = useCallback(() => { 34 36 if (!meta) return; 35 37 if (isBookmarked) removeBookmark(meta.tmdbId); 36 - else if (group) addBookmarkWithGroup(meta, group); 38 + else if (group && group.length > 0) addBookmarkWithGroups(meta, group); 37 39 else addBookmark(meta); 38 40 }, [ 39 41 isBookmarked, 40 42 meta, 41 43 addBookmark, 42 - addBookmarkWithGroup, 44 + addBookmarkWithGroups, 43 45 removeBookmark, 44 46 group, 45 47 ]);
+19 -13
src/components/overlays/details/DetailsBody.tsx
··· 30 30 const [releaseInfo, setReleaseInfo] = useState<TraktReleaseResponse | null>( 31 31 null, 32 32 ); 33 - const addBookmarkWithGroup = useBookmarkStore((s) => s.addBookmarkWithGroup); 34 - const removeBookmark = useBookmarkStore((s) => s.removeBookmark); 35 - const addBookmark = useBookmarkStore((s) => s.addBookmark); 33 + const addBookmarkWithGroups = useBookmarkStore( 34 + (s) => s.addBookmarkWithGroups, 35 + ); 36 + 36 37 const bookmarks = useBookmarkStore((s) => s.bookmarks); 37 - const currentGroup = bookmarks[data.id?.toString() ?? ""]?.group; 38 + const currentGroups = bookmarks[data.id?.toString() ?? ""]?.group || []; 38 39 39 40 const allGroups = Array.from( 40 41 new Set( 41 42 Object.values(bookmarks) 42 - .map((b) => b.group) 43 + .flatMap((b) => b.group || []) 43 44 .filter(Boolean), 44 45 ), 45 46 ) as string[]; 46 47 47 - const handleSelectGroup = (group: string) => { 48 + const handleSelectGroups = (groups: string[]) => { 48 49 if (!data.id) return; 49 50 const meta = { 50 51 tmdbId: data.id.toString(), ··· 55 56 : 0, 56 57 poster: data.posterUrl, 57 58 }; 58 - addBookmarkWithGroup(meta, group); 59 + addBookmarkWithGroups(meta, groups); 59 60 }; 60 61 61 62 const handleCreateGroup = (group: string) => { 62 - handleSelectGroup(group); 63 + handleSelectGroups([...currentGroups, group]); 63 64 }; 64 65 65 - const handleRemoveGroup = () => { 66 + const handleRemoveGroup = (groupToRemove?: string) => { 66 67 if (!data.id) return; 67 68 const meta = { 68 69 tmdbId: data.id.toString(), ··· 73 74 : 0, 74 75 poster: data.posterUrl, 75 76 }; 76 - removeBookmark(data.id.toString()); 77 - addBookmark(meta); 77 + if (groupToRemove) { 78 + const newGroups = currentGroups.filter((g) => g !== groupToRemove); 79 + addBookmarkWithGroups(meta, newGroups); 80 + } else { 81 + // Remove all groups 82 + addBookmarkWithGroups(meta, []); 83 + } 78 84 }; 79 85 80 86 useEffect(() => { ··· 266 272 {/* Group Dropdown */} 267 273 <GroupDropdown 268 274 groups={allGroups} 269 - currentGroup={currentGroup} 270 - onSelectGroup={handleSelectGroup} 275 + currentGroups={currentGroups} 276 + onSelectGroups={handleSelectGroups} 271 277 onCreateGroup={handleCreateGroup} 272 278 onRemoveGroup={handleRemoveGroup} 273 279 />
+7 -5
src/pages/parts/home/BookmarksCarousel.tsx
··· 91 91 92 92 items.forEach((item) => { 93 93 const bookmark = bookmarks[item.id]; 94 - if (bookmark?.group) { 95 - if (!grouped[bookmark.group]) { 96 - grouped[bookmark.group] = []; 97 - } 98 - grouped[bookmark.group].push(item); 94 + if (Array.isArray(bookmark?.group)) { 95 + bookmark.group.forEach((groupName) => { 96 + if (!grouped[groupName]) { 97 + grouped[groupName] = []; 98 + } 99 + grouped[groupName].push(item); 100 + }); 99 101 } else { 100 102 regular.push(item); 101 103 }
+7 -5
src/pages/parts/home/BookmarksPart.tsx
··· 69 69 70 70 items.forEach((item) => { 71 71 const bookmark = bookmarks[item.id]; 72 - if (bookmark?.group) { 73 - if (!grouped[bookmark.group]) { 74 - grouped[bookmark.group] = []; 75 - } 76 - grouped[bookmark.group].push(item); 72 + if (Array.isArray(bookmark?.group)) { 73 + bookmark.group.forEach((groupName) => { 74 + if (!grouped[groupName]) { 75 + grouped[groupName] = []; 76 + } 77 + grouped[groupName].push(item); 78 + }); 77 79 } else { 78 80 regular.push(item); 79 81 }
+6 -6
src/stores/bookmarks/index.ts
··· 10 10 poster?: string; 11 11 type: "show" | "movie"; 12 12 updatedAt: number; 13 - group?: string; 13 + group?: string[]; 14 14 } 15 15 16 16 export interface BookmarkUpdateItem { ··· 20 20 id: string; 21 21 poster?: string; 22 22 type?: "show" | "movie"; 23 - group?: string; 23 + group?: string[]; 24 24 action: "delete" | "add"; 25 25 } 26 26 ··· 28 28 bookmarks: Record<string, BookmarkMediaItem>; 29 29 updateQueue: BookmarkUpdateItem[]; 30 30 addBookmark(meta: PlayerMeta): void; 31 - addBookmarkWithGroup(meta: PlayerMeta, group?: string): void; 31 + addBookmarkWithGroups(meta: PlayerMeta, groups?: string[]): void; 32 32 removeBookmark(id: string): void; 33 33 replaceBookmarks(items: Record<string, BookmarkMediaItem>): void; 34 34 clear(): void; ··· 77 77 }; 78 78 }); 79 79 }, 80 - addBookmarkWithGroup(meta, group) { 80 + addBookmarkWithGroups(meta, groups) { 81 81 set((s) => { 82 82 updateId += 1; 83 83 s.updateQueue.push({ ··· 88 88 title: meta.title, 89 89 year: meta.releaseYear, 90 90 poster: meta.poster, 91 - group, 91 + group: groups, 92 92 }); 93 93 94 94 s.bookmarks[meta.tmdbId] = { ··· 97 97 year: meta.releaseYear, 98 98 poster: meta.poster, 99 99 updatedAt: Date.now(), 100 - group, 100 + group: groups, 101 101 }; 102 102 }); 103 103 },