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 the ability to disable sources and embeds

Pas f0a35f64 40eb846a

+243 -13
+4
src/backend/accounts/settings.ts
··· 21 21 forceCompactEpisodeView?: boolean; 22 22 sourceOrder?: string[]; 23 23 enableSourceOrder?: boolean; 24 + disabledSources?: string[]; 24 25 embedOrder?: string[]; 25 26 enableEmbedOrder?: boolean; 27 + disabledEmbeds?: string[]; 26 28 proxyTmdb?: boolean; 27 29 enableLowPerformanceMode?: boolean; 28 30 enableNativeSubtitles?: boolean; ··· 48 50 enableCarouselView?: boolean; 49 51 sourceOrder?: string[]; 50 52 enableSourceOrder?: boolean; 53 + disabledSources?: string[]; 51 54 embedOrder?: string[]; 52 55 enableEmbedOrder?: boolean; 56 + disabledEmbeds?: string[]; 53 57 proxyTmdb?: boolean; 54 58 enableLowPerformanceMode?: boolean; 55 59 enableNativeSubtitles?: boolean;
+139
src/components/form/SortableListWithToggles.tsx
··· 1 + import { 2 + DndContext, 3 + DragEndEvent, 4 + KeyboardSensor, 5 + MouseSensor, 6 + TouchSensor, 7 + closestCenter, 8 + useSensor, 9 + useSensors, 10 + } from "@dnd-kit/core"; 11 + import { 12 + restrictToParentElement, 13 + restrictToVerticalAxis, 14 + } from "@dnd-kit/modifiers"; 15 + import { 16 + SortableContext, 17 + arrayMove, 18 + sortableKeyboardCoordinates, 19 + useSortable, 20 + verticalListSortingStrategy, 21 + } from "@dnd-kit/sortable"; 22 + import { CSS } from "@dnd-kit/utilities"; 23 + import classNames from "classnames"; 24 + 25 + import { Toggle } from "../buttons/Toggle"; 26 + import { Icon, Icons } from "../Icon"; 27 + 28 + export interface ToggleableItem { 29 + id: string; 30 + name: string; 31 + disabled?: boolean; 32 + enabled?: boolean; 33 + } 34 + 35 + function SortableItemWithToggle(props: { 36 + item: ToggleableItem; 37 + onToggle: (id: string) => void; 38 + }) { 39 + const { attributes, listeners, setNodeRef, transform, transition } = 40 + useSortable({ id: props.item.id }); 41 + 42 + const style = { 43 + transform: CSS.Transform.toString(transform), 44 + transition, 45 + }; 46 + 47 + const handleToggleClick = (e: React.MouseEvent) => { 48 + e.stopPropagation(); 49 + e.preventDefault(); 50 + props.onToggle(props.item.id); 51 + }; 52 + 53 + return ( 54 + <div 55 + ref={setNodeRef} 56 + style={style} 57 + {...attributes} 58 + className={classNames( 59 + "bg-dropdown-background hover:bg-dropdown-hoverBackground select-none space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg", 60 + props.item.disabled && "opacity-50", 61 + )} 62 + > 63 + <span className="flex-1 text-white font-bold">{props.item.name}</span> 64 + {props.item.disabled && <Icon icon={Icons.UNPLUG} />} 65 + <div 66 + onClick={handleToggleClick} 67 + className="cursor-pointer flex-shrink-0" 68 + onMouseDown={(e) => e.stopPropagation()} 69 + onTouchStart={(e) => e.stopPropagation()} 70 + > 71 + <Toggle enabled={props.item.enabled ?? true} /> 72 + </div> 73 + <div 74 + {...listeners} 75 + className={classNames( 76 + "cursor-grab touch-manipulation flex-shrink-0", 77 + transform ? "cursor-grabbing" : "cursor-grab", 78 + )} 79 + > 80 + <Icon icon={Icons.MENU} /> 81 + </div> 82 + </div> 83 + ); 84 + } 85 + 86 + export function SortableListWithToggles(props: { 87 + items: ToggleableItem[]; 88 + setItems: (items: ToggleableItem[]) => void; 89 + onToggle: (id: string) => void; 90 + }) { 91 + const sensors = useSensors( 92 + useSensor(TouchSensor, { 93 + activationConstraint: { 94 + delay: 75, 95 + tolerance: 1, 96 + }, 97 + }), 98 + useSensor(MouseSensor), 99 + useSensor(KeyboardSensor, { 100 + coordinateGetter: sortableKeyboardCoordinates, 101 + }), 102 + ); 103 + 104 + const handleDragEnd = (event: DragEndEvent) => { 105 + const { active, over } = event; 106 + if (!over) return; 107 + if (active.id !== over.id) { 108 + const currentItems = props.items; 109 + const oldIndex = currentItems.findIndex((item) => item.id === active.id); 110 + const newIndex = currentItems.findIndex((item) => item.id === over.id); 111 + const newItems = arrayMove(currentItems, oldIndex, newIndex); 112 + props.setItems(newItems); 113 + } 114 + }; 115 + 116 + return ( 117 + <DndContext 118 + sensors={sensors} 119 + collisionDetection={closestCenter} 120 + onDragEnd={handleDragEnd} 121 + modifiers={[restrictToVerticalAxis, restrictToParentElement]} 122 + > 123 + <SortableContext 124 + items={props.items} 125 + strategy={verticalListSortingStrategy} 126 + > 127 + <div className="flex flex-col gap-2"> 128 + {props.items.map((item) => ( 129 + <SortableItemWithToggle 130 + key={item.id} 131 + item={item} 132 + onToggle={props.onToggle} 133 + /> 134 + ))} 135 + </div> 136 + </SortableContext> 137 + </DndContext> 138 + ); 139 + }
+4 -2
src/components/player/atoms/settings/SourceSelectingView.tsx
··· 144 144 const currentSourceId = usePlayerStore((s) => s.sourceId); 145 145 const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); 146 146 const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); 147 + const disabledSources = usePreferencesStore((s) => s.disabledSources); 147 148 148 149 const sources = useMemo(() => { 149 150 if (!metaType) return []; 150 151 const allSources = getCachedMetadata() 151 152 .filter((v) => v.type === "source") 152 - .filter((v) => v.mediaTypes?.includes(metaType)); 153 + .filter((v) => v.mediaTypes?.includes(metaType)) 154 + .filter((v) => !disabledSources.includes(v.id)); 153 155 154 156 if (!enableSourceOrder || preferredSourceOrder.length === 0) { 155 157 return allSources; ··· 172 174 orderedSources.push(...remainingSources); 173 175 174 176 return orderedSources; 175 - }, [metaType, preferredSourceOrder, enableSourceOrder]); 177 + }, [metaType, preferredSourceOrder, enableSourceOrder, disabledSources]); 176 178 177 179 return ( 178 180 <>
+7
src/hooks/auth/useAuthData.ts
··· 54 54 const setEnableSourceOrder = usePreferencesStore( 55 55 (s) => s.setEnableSourceOrder, 56 56 ); 57 + const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources); 58 + 57 59 const setProxyTmdb = usePreferencesStore((s) => s.setProxyTmdb); 58 60 59 61 const setEnableLowPerformanceMode = usePreferencesStore( ··· 179 181 setEnableSourceOrder(settings.enableSourceOrder); 180 182 } 181 183 184 + if (settings.disabledSources !== undefined) { 185 + setDisabledSources(settings.disabledSources); 186 + } 187 + 182 188 if (settings.proxyTmdb !== undefined) { 183 189 setProxyTmdb(settings.proxyTmdb); 184 190 } ··· 224 230 setEnableCarouselView, 225 231 setSourceOrder, 226 232 setEnableSourceOrder, 233 + setDisabledSources, 227 234 setProxyTmdb, 228 235 setFebboxKey, 229 236 setEnableLowPerformanceMode,
+19 -5
src/hooks/useProviderScrape.tsx
··· 155 155 156 156 const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); 157 157 const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); 158 + const disabledSources = usePreferencesStore((s) => s.disabledSources); 158 159 const preferredEmbedOrder = usePreferencesStore((s) => s.embedOrder); 159 160 const enableEmbedOrder = usePreferencesStore((s) => s.enableEmbedOrder); 161 + const disabledEmbeds = usePreferencesStore((s) => s.disabledEmbeds); 160 162 161 163 const startScraping = useCallback( 162 164 async (media: ScrapeMedia) => { 165 + // Filter out disabled sources from the source order 166 + const filteredSourceOrder = enableSourceOrder 167 + ? preferredSourceOrder.filter((id) => !disabledSources.includes(id)) 168 + : undefined; 169 + 170 + // Filter out disabled embeds from the embed order 171 + const filteredEmbedOrder = enableEmbedOrder 172 + ? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id)) 173 + : undefined; 174 + 163 175 const providerApiUrl = getLoadbalancedProviderApiUrl(); 164 176 if (providerApiUrl && !isExtensionActiveCached()) { 165 177 startScrape(); ··· 167 179 const conn = await connectServerSideEvents<RunOutput | "">( 168 180 baseUrlMaker.scrapeAll( 169 181 media, 170 - enableSourceOrder ? preferredSourceOrder : undefined, 171 - enableEmbedOrder ? preferredEmbedOrder : undefined, 182 + filteredSourceOrder, 183 + filteredEmbedOrder, 172 184 ), 173 185 ["completed", "noOutput"], 174 186 ); ··· 187 199 const providers = getProviders(); 188 200 const output = await providers.runAll({ 189 201 media, 190 - // Only pass sourceOrder if enableSourceOrder is true 191 - sourceOrder: enableSourceOrder ? preferredSourceOrder : undefined, 202 + // Only pass sourceOrder if enableSourceOrder is true, and filter out disabled sources 203 + sourceOrder: filteredSourceOrder, 192 204 // Only pass embedOrder if enableEmbedOrder is true 193 - embedOrder: enableEmbedOrder ? preferredEmbedOrder : undefined, 205 + embedOrder: filteredEmbedOrder, 194 206 events: { 195 207 init: initEvent, 196 208 start: startEvent, ··· 211 223 startScrape, 212 224 preferredSourceOrder, 213 225 enableSourceOrder, 226 + disabledSources, 214 227 preferredEmbedOrder, 215 228 enableEmbedOrder, 229 + disabledEmbeds, 216 230 ], 217 231 ); 218 232
+14
src/hooks/useSettingsState.ts
··· 59 59 enableDetailsModal: boolean, 60 60 sourceOrder: string[], 61 61 enableSourceOrder: boolean, 62 + disabledSources: string[], 62 63 proxyTmdb: boolean, 63 64 enableSkipCredits: boolean, 64 65 enableImageLogos: boolean, ··· 158 159 resetEnableSourceOrder, 159 160 enableSourceOrderChanged, 160 161 ] = useDerived(enableSourceOrder); 162 + const [ 163 + disabledSourcesState, 164 + setDisabledSourcesState, 165 + resetDisabledSources, 166 + disabledSourcesChanged, 167 + ] = useDerived(disabledSources); 161 168 const [proxyTmdbState, setProxyTmdbState, resetProxyTmdb, proxyTmdbChanged] = 162 169 useDerived(proxyTmdb); 163 170 const [ ··· 223 230 resetEnableImageLogos(); 224 231 resetSourceOrder(); 225 232 resetEnableSourceOrder(); 233 + resetDisabledSources(); 226 234 resetProxyTmdb(); 227 235 resetEnableCarouselView(); 228 236 resetForceCompactEpisodeView(); ··· 252 260 enableImageLogosChanged || 253 261 sourceOrderChanged || 254 262 enableSourceOrderChanged || 263 + disabledSourcesChanged || 255 264 proxyTmdbChanged || 256 265 enableCarouselViewChanged || 257 266 forceCompactEpisodeViewChanged || ··· 358 367 state: proxyTmdbState, 359 368 set: setProxyTmdbState, 360 369 changed: proxyTmdbChanged, 370 + }, 371 + disabledSources: { 372 + state: disabledSourcesState, 373 + set: setDisabledSourcesState, 374 + changed: disabledSourcesChanged, 361 375 }, 362 376 enableCarouselView: { 363 377 state: enableCarouselViewState,
+10
src/pages/Settings.tsx
··· 156 156 (s) => s.setEnableSourceOrder, 157 157 ); 158 158 159 + const disabledSources = usePreferencesStore((s) => s.disabledSources); 160 + const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources); 161 + 159 162 const enableDiscover = usePreferencesStore((s) => s.enableDiscover); 160 163 const setEnableDiscover = usePreferencesStore((s) => s.setEnableDiscover); 161 164 ··· 259 262 enableDetailsModal, 260 263 sourceOrder, 261 264 enableSourceOrder, 265 + disabledSources, 262 266 proxyTmdb, 263 267 enableSkipCredits, 264 268 enableImageLogos, ··· 323 327 state.enableImageLogos.changed || 324 328 state.sourceOrder.changed || 325 329 state.enableSourceOrder.changed || 330 + state.disabledSources.changed || 326 331 state.proxyTmdb.changed || 327 332 state.enableCarouselView.changed || 328 333 state.forceCompactEpisodeView.changed || ··· 346 351 enableImageLogos: state.enableImageLogos.state, 347 352 sourceOrder: state.sourceOrder.state, 348 353 enableSourceOrder: state.enableSourceOrder.state, 354 + disabledSources: state.disabledSources.state, 349 355 proxyTmdb: state.proxyTmdb.state, 350 356 enableCarouselView: state.enableCarouselView.state, 351 357 forceCompactEpisodeView: state.forceCompactEpisodeView.state, ··· 381 387 setEnableImageLogos(state.enableImageLogos.state); 382 388 setSourceOrder(state.sourceOrder.state); 383 389 setEnableSourceOrder(state.enableSourceOrder.state); 390 + setDisabledSources(state.disabledSources.state); 384 391 setAppLanguage(state.appLanguage.state); 385 392 setTheme(state.theme.state); 386 393 setSubStyling(state.subtitleStyling.state); ··· 427 434 setEnableImageLogos, 428 435 setSourceOrder, 429 436 setEnableSourceOrder, 437 + setDisabledSources, 430 438 setAppLanguage, 431 439 setTheme, 432 440 setSubStyling, ··· 491 499 setSourceOrder={state.sourceOrder.set} 492 500 enableSourceOrder={state.enableSourceOrder.state} 493 501 setenableSourceOrder={state.enableSourceOrder.set} 502 + disabledSources={state.disabledSources.state} 503 + setDisabledSources={state.disabledSources.set} 494 504 enableLowPerformanceMode={state.enableLowPerformanceMode.state} 495 505 setEnableLowPerformanceMode={state.enableLowPerformanceMode.set} 496 506 enableHoldToBoost={state.enableHoldToBoost.state}
+15 -3
src/pages/parts/admin/EmbedOrderPart.tsx
··· 5 5 import { getAllProviders, getProviders } from "@/backend/providers/providers"; 6 6 import { Button } from "@/components/buttons/Button"; 7 7 import { Toggle } from "@/components/buttons/Toggle"; 8 - import { SortableList } from "@/components/form/SortableList"; 8 + import { SortableListWithToggles } from "@/components/form/SortableListWithToggles"; 9 9 import { Heading2 } from "@/components/utils/Text"; 10 10 import { usePreferencesStore } from "@/stores/preferences"; 11 11 ··· 17 17 const setEmbedOrder = usePreferencesStore((s) => s.setEmbedOrder); 18 18 const enableEmbedOrder = usePreferencesStore((s) => s.enableEmbedOrder); 19 19 const setEnableEmbedOrder = usePreferencesStore((s) => s.setEnableEmbedOrder); 20 + const disabledEmbeds = usePreferencesStore((s) => s.disabledEmbeds); 21 + const setDisabledEmbeds = usePreferencesStore((s) => s.setDisabledEmbeds); 20 22 21 23 const allEmbeds = getAllProviders().listEmbeds(); 22 24 ··· 29 31 id: e.id, 30 32 name: e.name || e.id, 31 33 disabled: !currentDeviceEmbeds.find((embed) => embed.id === e.id), 34 + enabled: !disabledEmbeds.includes(e.id), 32 35 })); 33 36 } 34 37 ··· 37 40 id, 38 41 name: allEmbeds.find((e) => e.id === id)?.name || id, 39 42 disabled: !currentDeviceEmbeds.find((e) => e.id === id), 43 + enabled: !disabledEmbeds.includes(id), 40 44 })); 41 - }, [embedOrder, allEmbeds]); 45 + }, [embedOrder, allEmbeds, disabledEmbeds]); 46 + 47 + const handleEmbedToggle = (embedId: string) => { 48 + const newDisabledEmbeds = disabledEmbeds.includes(embedId) 49 + ? disabledEmbeds.filter((id) => id !== embedId) 50 + : [...disabledEmbeds, embedId]; 51 + setDisabledEmbeds(newDisabledEmbeds); 52 + }; 42 53 43 54 return ( 44 55 <div className="space-y-6"> ··· 72 83 73 84 {enableEmbedOrder && ( 74 85 <div className="w-full flex flex-col gap-4"> 75 - <SortableList 86 + <SortableListWithToggles 76 87 items={embedItems} 77 88 setItems={(items) => setEmbedOrder(items.map((item) => item.id))} 89 + onToggle={handleEmbedToggle} 78 90 /> 79 91 <Button 80 92 className="max-w-[25rem]"
+1
src/pages/parts/auth/VerifyPassphrasePart.tsx
··· 52 52 enableCarouselView: store.enableCarouselView, 53 53 sourceOrder: store.sourceOrder, 54 54 enableSourceOrder: store.enableSourceOrder, 55 + disabledSources: store.disabledSources, 55 56 proxyTmdb: store.proxyTmdb, 56 57 febboxKey: store.febboxKey, 57 58 enableLowPerformanceMode: store.enableLowPerformanceMode,
+14 -3
src/pages/parts/settings/PreferencesPart.tsx
··· 8 8 import { Toggle } from "@/components/buttons/Toggle"; 9 9 import { FlagIcon } from "@/components/FlagIcon"; 10 10 import { Dropdown } from "@/components/form/Dropdown"; 11 - import { SortableList } from "@/components/form/SortableList"; 11 + import { SortableListWithToggles } from "@/components/form/SortableListWithToggles"; 12 12 import { Heading1 } from "@/components/utils/Text"; 13 13 import { appLanguageOptions } from "@/setup/i18n"; 14 14 import { isAutoplayAllowed } from "@/utils/autoplay"; ··· 27 27 setSourceOrder: (v: string[]) => void; 28 28 enableSourceOrder: boolean; 29 29 setenableSourceOrder: (v: boolean) => void; 30 + disabledSources: string[]; 31 + setDisabledSources: (v: string[]) => void; 30 32 enableLowPerformanceMode: boolean; 31 33 setEnableLowPerformanceMode: (v: boolean) => void; 32 34 enableHoldToBoost: boolean; ··· 61 63 id, 62 64 name: allSources.find((s) => s.id === id)?.name || id, 63 65 disabled: !currentDeviceSources.find((s) => s.id === id), 66 + enabled: !props.disabledSources.includes(id), 64 67 })); 65 - }, [props.sourceOrder, allSources]); 68 + }, [props.sourceOrder, props.disabledSources, allSources]); 66 69 67 70 const navigate = useNavigate(); 68 71 ··· 75 78 props.setEnableThumbnails(false); 76 79 props.setEnableAutoplay(false); 77 80 } 81 + }; 82 + 83 + const handleSourceToggle = (sourceId: string) => { 84 + const newDisabledSources = props.disabledSources.includes(sourceId) 85 + ? props.disabledSources.filter((id) => id !== sourceId) 86 + : [...props.disabledSources, sourceId]; 87 + props.setDisabledSources(newDisabledSources); 78 88 }; 79 89 80 90 return ( ··· 294 304 295 305 {props.enableSourceOrder && ( 296 306 <div className="w-full flex flex-col gap-4"> 297 - <SortableList 307 + <SortableListWithToggles 298 308 items={sourceItems} 299 309 setItems={(items) => 300 310 props.setSourceOrder(items.map((item) => item.id)) 301 311 } 312 + onToggle={handleSourceToggle} 302 313 /> 303 314 <Button 304 315 className="max-w-[25rem]"
+16
src/stores/preferences/index.tsx
··· 14 14 forceCompactEpisodeView: boolean; 15 15 sourceOrder: string[]; 16 16 enableSourceOrder: boolean; 17 + disabledSources: string[]; 17 18 embedOrder: string[]; 18 19 enableEmbedOrder: boolean; 20 + disabledEmbeds: string[]; 19 21 proxyTmdb: boolean; 20 22 febboxKey: string | null; 21 23 realDebridKey: string | null; ··· 37 39 setForceCompactEpisodeView(v: boolean): void; 38 40 setSourceOrder(v: string[]): void; 39 41 setEnableSourceOrder(v: boolean): void; 42 + setDisabledSources(v: string[]): void; 40 43 setEmbedOrder(v: string[]): void; 41 44 setEnableEmbedOrder(v: boolean): void; 45 + setDisabledEmbeds(v: string[]): void; 42 46 setProxyTmdb(v: boolean): void; 43 47 setFebboxKey(v: string | null): void; 44 48 setRealDebridKey(v: string | null): void; ··· 64 68 forceCompactEpisodeView: false, 65 69 sourceOrder: [], 66 70 enableSourceOrder: false, 71 + disabledSources: [], 67 72 embedOrder: [], 68 73 enableEmbedOrder: false, 74 + disabledEmbeds: [], 69 75 proxyTmdb: false, 70 76 febboxKey: null, 71 77 realDebridKey: null, ··· 130 136 s.enableSourceOrder = v; 131 137 }); 132 138 }, 139 + setDisabledSources(v) { 140 + set((s) => { 141 + s.disabledSources = v; 142 + }); 143 + }, 133 144 setEmbedOrder(v) { 134 145 set((s) => { 135 146 s.embedOrder = v; ··· 138 149 setEnableEmbedOrder(v) { 139 150 set((s) => { 140 151 s.enableEmbedOrder = v; 152 + }); 153 + }, 154 + setDisabledEmbeds(v) { 155 + set((s) => { 156 + s.disabledEmbeds = v; 141 157 }); 142 158 }, 143 159 setProxyTmdb(v) {