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 compact episodes view

Pas d6f132b4 7c89b2ff

+117 -9
+4 -1
src/assets/locales/en.json
··· 792 792 "logosLabel": "Image logos", 793 793 "carouselView": "Carousel view", 794 794 "carouselViewDescription": "Display your currently watching and bookmark sections as carousels instead of a grid. Disabled by default.", 795 - "carouselViewLabel": "Carousel view" 795 + "carouselViewLabel": "Carousel view", 796 + "forceCompactEpisodeView": "Force compact episode view", 797 + "forceCompactEpisodeViewDescription": "Force the episode carousel in the player to use the \"classic\" compact vertical view. Disabled by default.", 798 + "forceCompactEpisodeViewLabel": "Compact episodes" 796 799 } 797 800 }, 798 801 "connections": {
+1
src/backend/accounts/settings.ts
··· 18 18 enableDetailsModal?: boolean; 19 19 enableImageLogos?: boolean; 20 20 enableCarouselView?: boolean; 21 + forceCompactEpisodeView?: boolean; 21 22 sourceOrder?: string[]; 22 23 enableSourceOrder?: boolean; 23 24 proxyTmdb?: boolean;
+45 -6
src/components/player/atoms/Episodes.tsx
··· 17 17 import { useOverlayRouter } from "@/hooks/useOverlayRouter"; 18 18 import { PlayerMeta } from "@/stores/player/slices/source"; 19 19 import { usePlayerStore } from "@/stores/player/store"; 20 + import { usePreferencesStore } from "@/stores/preferences"; 20 21 import { useProgressStore } from "@/stores/progress"; 21 22 22 23 import { hasAired } from "../utils/aired"; ··· 122 123 const descriptionRefs = useRef<{ 123 124 [key: string]: HTMLParagraphElement | null; 124 125 }>({}); 126 + const forceCompactEpisodeView = usePreferencesStore( 127 + (s) => s.forceCompactEpisodeView, 128 + ); 125 129 126 130 const isTextTruncated = (element: HTMLElement | null) => { 127 131 if (!element) return false; ··· 248 252 content = ( 249 253 <div className="relative"> 250 254 {/* Horizontal scroll buttons */} 251 - <div className="absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block"> 255 + <div 256 + className={classNames( 257 + "absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4", 258 + forceCompactEpisodeView ? "hidden" : "hidden lg:block", 259 + )} 260 + > 252 261 <button 253 262 type="button" 254 263 className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm" ··· 261 270 <div 262 271 ref={carouselRef} 263 272 className={classNames( 264 - "flex flex-col lg:flex-row lg:overflow-x-auto space-y-3 sm:space-y-4 lg:space-y-0 lg:space-x-4 pb-4 pt-2 lg:px-12 scrollbar-hide", 265 - { "carousel-container": window.innerWidth >= 1024 }, 273 + "flex pb-4 pt-2 scrollbar-hide", 274 + { 275 + "carousel-container": 276 + window.innerWidth >= 1024 && !forceCompactEpisodeView, 277 + }, 278 + forceCompactEpisodeView 279 + ? "flex-col space-y-3" 280 + : "flex-col lg:flex-row lg:overflow-x-auto space-y-3 sm:space-y-4 lg:space-y-0 lg:space-x-4 lg:px-12 ", 266 281 )} 267 282 style={{ 268 283 scrollbarWidth: "none", ··· 289 304 return ( 290 305 <div key={ep.id} ref={isActive ? activeEpisodeRef : null}> 291 306 {/* Extra small screens - Simple vertical list with no thumbnails */} 292 - <div className="block sm:hidden w-full px-3"> 307 + <div 308 + className={classNames( 309 + "block w-full px-3", 310 + forceCompactEpisodeView ? "" : "sm:hidden", 311 + )} 312 + > 293 313 <Menu.Link 294 314 onClick={() => playEpisode(ep.id)} 295 315 active={isActive} ··· 327 347 onClick={() => playEpisode(ep.id)} 328 348 className={classNames( 329 349 "hidden sm:flex lg:hidden w-full rounded-lg overflow-hidden transition-all duration-200 relative cursor-pointer", 350 + forceCompactEpisodeView ? "!hidden" : "", 330 351 isActive 331 352 ? "bg-video-context-hoverColor/50" 332 353 : "hover:bg-video-context-hoverColor/50", ··· 430 451 onClick={() => playEpisode(ep.id)} 431 452 className={classNames( 432 453 "hidden lg:block flex-shrink-0 transition-all duration-200 relative cursor-pointer rounded-lg overflow-hidden", 454 + forceCompactEpisodeView ? "!hidden" : "", 433 455 isActive 434 456 ? "bg-video-context-hoverColor/50" 435 457 : "hover:bg-video-context-hoverColor/50", ··· 547 569 </div> 548 570 549 571 {/* Right scroll button */} 550 - <div className="absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block"> 572 + <div 573 + className={classNames( 574 + "absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4", 575 + forceCompactEpisodeView ? "hidden" : "hidden lg:block", 576 + )} 577 + > 551 578 <button 552 579 type="button" 553 580 className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm" ··· 597 624 [router], 598 625 ); 599 626 627 + const forceCompactEpisodeView = usePreferencesStore( 628 + (s) => s.forceCompactEpisodeView, 629 + ); 630 + 600 631 return ( 601 632 <Overlay id={id}> 602 633 <OverlayRouter id={id}> 603 634 <OverlayPage id={id} path="/" width={343} height={431}> 604 635 <SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} /> 605 636 </OverlayPage> 606 - <OverlayPage id={id} path="/episodes" width={0} height={375} fullWidth> 637 + <OverlayPage 638 + id={id} 639 + path="/episodes" 640 + width={343} 641 + height={ 642 + forceCompactEpisodeView || window.innerWidth < 1024 ? 431 : 375 643 + } 644 + fullWidth={!forceCompactEpisodeView} 645 + > 607 646 {selectedSeason.length > 0 ? ( 608 647 <EpisodesView 609 648 selectedSeason={selectedSeason}
+15 -1
src/hooks/useSettingsState.ts
··· 63 63 enableSkipCredits: boolean, 64 64 enableImageLogos: boolean, 65 65 enableCarouselView: boolean, 66 + forceCompactEpisodeView: boolean, 66 67 ) { 67 68 const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] = 68 69 useDerived(proxyUrls); ··· 160 161 resetEnableCarouselView, 161 162 enableCarouselViewChanged, 162 163 ] = useDerived(enableCarouselView); 164 + const [ 165 + forceCompactEpisodeViewState, 166 + setForceCompactEpisodeViewState, 167 + resetForceCompactEpisodeView, 168 + forceCompactEpisodeViewChanged, 169 + ] = useDerived(forceCompactEpisodeView); 163 170 164 171 function reset() { 165 172 resetTheme(); ··· 183 190 resetEnableSourceOrder(); 184 191 resetProxyTmdb(); 185 192 resetEnableCarouselView(); 193 + resetForceCompactEpisodeView(); 186 194 } 187 195 188 196 const changed = ··· 205 213 sourceOrderChanged || 206 214 enableSourceOrderChanged || 207 215 proxyTmdbChanged || 208 - enableCarouselViewChanged; 216 + enableCarouselViewChanged || 217 + forceCompactEpisodeViewChanged; 209 218 210 219 return { 211 220 reset, ··· 309 318 state: enableCarouselViewState, 310 319 set: setEnableCarouselViewState, 311 320 changed: enableCarouselViewChanged, 321 + }, 322 + forceCompactEpisodeView: { 323 + state: forceCompactEpisodeViewState, 324 + set: setForceCompactEpisodeViewState, 325 + changed: forceCompactEpisodeViewChanged, 312 326 }, 313 327 }; 314 328 }
+15 -1
src/pages/Settings.tsx
··· 178 178 (s) => s.setEnableCarouselView, 179 179 ); 180 180 181 + const forceCompactEpisodeView = usePreferencesStore( 182 + (s) => s.forceCompactEpisodeView, 183 + ); 184 + const setForceCompactEpisodeView = usePreferencesStore( 185 + (s) => s.setForceCompactEpisodeView, 186 + ); 187 + 181 188 const account = useAuthStore((s) => s.account); 182 189 const updateProfile = useAuthStore((s) => s.setAccountProfile); 183 190 const updateDeviceName = useAuthStore((s) => s.updateDeviceName); ··· 227 234 enableSkipCredits, 228 235 enableImageLogos, 229 236 enableCarouselView, 237 + forceCompactEpisodeView, 230 238 ); 231 239 232 240 const availableSources = useMemo(() => { ··· 282 290 state.sourceOrder.changed || 283 291 state.enableSourceOrder.changed || 284 292 state.proxyTmdb.changed || 285 - state.enableCarouselView.changed 293 + state.enableCarouselView.changed || 294 + state.forceCompactEpisodeView.changed 286 295 ) { 287 296 await updateSettings(backendUrl, account, { 288 297 applicationLanguage: state.appLanguage.state, ··· 301 310 enableSourceOrder: state.enableSourceOrder.state, 302 311 proxyTmdb: state.proxyTmdb.state, 303 312 enableCarouselView: state.enableCarouselView.state, 313 + forceCompactEpisodeView: state.forceCompactEpisodeView.state, 304 314 }); 305 315 } 306 316 if (state.deviceName.changed) { ··· 337 347 setRealDebridKey(state.realDebridKey.state); 338 348 setProxyTmdb(state.proxyTmdb.state); 339 349 setEnableCarouselView(state.enableCarouselView.state); 350 + setForceCompactEpisodeView(state.forceCompactEpisodeView.state); 340 351 341 352 if (state.profile.state) { 342 353 updateProfile(state.profile.state); ··· 378 389 setEnableSourceOrder, 379 390 setProxyTmdb, 380 391 setEnableCarouselView, 392 + setForceCompactEpisodeView, 381 393 ]); 382 394 return ( 383 395 <SubPageLayout> ··· 443 455 setEnableImageLogos={state.enableImageLogos.set} 444 456 enableCarouselView={state.enableCarouselView.state} 445 457 setEnableCarouselView={state.enableCarouselView.set} 458 + forceCompactEpisodeView={state.forceCompactEpisodeView.state} 459 + setForceCompactEpisodeView={state.forceCompactEpisodeView.set} 446 460 /> 447 461 </div> 448 462 <div id="settings-captions" className="mt-28">
+29
src/pages/parts/settings/AppearancePart.tsx
··· 216 216 217 217 enableCarouselView: boolean; 218 218 setEnableCarouselView: (v: boolean) => void; 219 + 220 + forceCompactEpisodeView: boolean; 221 + setForceCompactEpisodeView: (v: boolean) => void; 219 222 }) { 220 223 const { t } = useTranslation(); 221 224 ··· 386 389 <Toggle enabled={props.enableCarouselView} /> 387 390 <p className="flex-1 text-white font-bold"> 388 391 {t("settings.appearance.options.carouselViewLabel")} 392 + </p> 393 + </div> 394 + </div> 395 + 396 + {/* Force Compact Episode View */} 397 + <div> 398 + <p className="text-white font-bold mb-3"> 399 + {t("settings.appearance.options.forceCompactEpisodeView")} 400 + </p> 401 + <p className="max-w-[25rem] font-medium"> 402 + {t( 403 + "settings.appearance.options.forceCompactEpisodeViewDescription", 404 + )} 405 + </p> 406 + <div 407 + onClick={() => 408 + props.setForceCompactEpisodeView(!props.forceCompactEpisodeView) 409 + } 410 + className={classNames( 411 + "bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg", 412 + "cursor-pointer opacity-100 pointer-events-auto", 413 + )} 414 + > 415 + <Toggle enabled={props.forceCompactEpisodeView} /> 416 + <p className="flex-1 text-white font-bold"> 417 + {t("settings.appearance.options.forceCompactEpisodeViewLabel")} 389 418 </p> 390 419 </div> 391 420 </div>
+8
src/stores/preferences/index.tsx
··· 11 11 enableDetailsModal: boolean; 12 12 enableImageLogos: boolean; 13 13 enableCarouselView: boolean; 14 + forceCompactEpisodeView: boolean; 14 15 sourceOrder: string[]; 15 16 enableSourceOrder: boolean; 16 17 proxyTmdb: boolean; ··· 25 26 setEnableDetailsModal(v: boolean): void; 26 27 setEnableImageLogos(v: boolean): void; 27 28 setEnableCarouselView(v: boolean): void; 29 + setForceCompactEpisodeView(v: boolean): void; 28 30 setSourceOrder(v: string[]): void; 29 31 setEnableSourceOrder(v: boolean): void; 30 32 setProxyTmdb(v: boolean): void; ··· 43 45 enableDetailsModal: false, 44 46 enableImageLogos: true, 45 47 enableCarouselView: false, 48 + forceCompactEpisodeView: false, 46 49 sourceOrder: [], 47 50 enableSourceOrder: false, 48 51 proxyTmdb: false, ··· 86 89 setEnableCarouselView(v) { 87 90 set((s) => { 88 91 s.enableCarouselView = v; 92 + }); 93 + }, 94 + setForceCompactEpisodeView(v) { 95 + set((s) => { 96 + s.forceCompactEpisodeView = v; 89 97 }); 90 98 }, 91 99 setSourceOrder(v) {