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.

media grids

+136 -112
+1
package.json
··· 42 42 ] 43 43 }, 44 44 "devDependencies": { 45 + "@tailwindcss/line-clamp": "^0.4.2", 45 46 "@types/crypto-js": "^4.1.1", 46 47 "@types/node": "^17.0.15", 47 48 "@types/react": "^17.0.39",
+1 -1
src/components/SearchBar.tsx
··· 37 37 } 38 38 39 39 return ( 40 - <div className="relative flex flex-col rounded-[28px] bg-denim-300 transition-colors focus-within:bg-denim-400 hover:bg-denim-400 sm:flex-row sm:items-center"> 40 + <div className="relative flex flex-col rounded-[28px] bg-denim-400 transition-colors focus-within:bg-denim-400 hover:bg-denim-500 sm:flex-row sm:items-center"> 41 41 <div className="pointer-events-none absolute left-5 top-0 bottom-0 flex max-h-14 items-center"> 42 42 <Icon icon={Icons.SEARCH} /> 43 43 </div>
+1 -5
src/components/buttons/DropdownButton.tsx
··· 6 6 } from "react"; 7 7 import { Icon, Icons } from "@/components/Icon"; 8 8 9 - import { 10 - Backdrop, 11 - BackdropContainer, 12 - useBackdrop, 13 - } from "@/components/layout/Backdrop"; 9 + import { BackdropContainer, useBackdrop } from "@/components/layout/Backdrop"; 14 10 import { ButtonControlProps, ButtonControl } from "./ButtonControl"; 15 11 16 12 export interface OptionItem {
+2 -2
src/components/buttons/IconPatch.tsx
··· 12 12 return ( 13 13 <div className={props.className || undefined} onClick={props.onClick}> 14 14 <div 15 - className={`flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent bg-denim-300 transition-[color,transform,border-color] duration-75 ${ 15 + className={`flex h-12 w-12 items-center justify-center rounded-full border-2 border-transparent bg-denim-500 transition-[color,transform,border-color] duration-75 ${ 16 16 props.clickable 17 - ? "cursor-pointer hover:scale-110 hover:bg-denim-400 hover:text-white active:scale-125" 17 + ? "cursor-pointer hover:scale-110 hover:bg-denim-600 hover:text-white active:scale-125" 18 18 : "" 19 19 } ${props.active ? "border-bink-600 bg-bink-100 text-bink-600" : ""}`} 20 20 >
+4 -4
src/components/layout/Backdrop.tsx
··· 40 40 return [setBackdrop, backdropProps, highlightedProps]; 41 41 } 42 42 43 - export function Backdrop(props: BackdropProps) { 43 + function Backdrop(props: BackdropProps) { 44 44 const clickEvent = props.onClick || (() => {}); 45 45 const animationEvent = props.onBackdropHide || (() => {}); 46 46 const [isVisible, setVisible, fadeProps] = useFade(); ··· 59 59 60 60 return ( 61 61 <div 62 - className={`fixed left-0 right-0 top-0 h-screen w-screen bg-black bg-opacity-50 opacity-100 transition-opacity ${ 62 + className={`pointer-events-auto fixed left-0 right-0 top-0 h-screen w-screen bg-black bg-opacity-50 opacity-100 transition-opacity ${ 63 63 !isVisible ? "opacity-0" : "" 64 64 }`} 65 65 {...fadeProps} ··· 99 99 return ( 100 100 <div ref={root}> 101 101 {createPortal( 102 - <div className="absolute top-0 left-0 z-[999]"> 102 + <div className="pointer-events-none fixed top-0 left-0 z-[999]"> 103 103 <Backdrop active={props.active} {...props} /> 104 - <div ref={copy} className="absolute"> 104 + <div ref={copy} className="pointer-events-auto absolute"> 105 105 {props.children} 106 106 </div> 107 107 </div>,
+5 -4
src/components/layout/BrandPill.tsx
··· 6 6 7 7 return ( 8 8 <div 9 - className={`flex items-center space-x-2 rounded-full bg-bink-100 bg-opacity-50 px-4 py-2 text-bink-600 ${props.clickable 10 - ? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-200 hover:text-bink-700 active:scale-95" 9 + className={`flex items-center space-x-2 rounded-full bg-bink-300 bg-opacity-50 px-4 py-2 text-bink-600 ${ 10 + props.clickable 11 + ? "transition-[transform,background-color] hover:scale-105 hover:bg-bink-400 hover:text-bink-700 active:scale-95" 11 12 : "" 12 - }`} 13 + }`} 13 14 > 14 15 <Icon className="text-xl" icon={Icons.MOVIE_WEB} /> 15 - <span className="font-semibold text-white">{t('global.name')}</span> 16 + <span className="font-semibold text-white">{t("global.name")}</span> 16 17 </div> 17 18 ); 18 19 }
+18
src/components/layout/WideContainer.tsx
··· 1 + import { ReactNode } from "react"; 2 + 3 + interface WideContainerProps { 4 + classNames?: string; 5 + children?: ReactNode; 6 + } 7 + 8 + export function WideContainer(props: WideContainerProps) { 9 + return ( 10 + <div 11 + className={`mx-auto w-[700px] max-w-full px-8 sm:px-4 ${ 12 + props.classNames || "" 13 + }`} 14 + > 15 + {props.children} 16 + </div> 17 + ); 18 + }
+27 -51
src/components/media/MediaCard.tsx
··· 5 5 MWMediaMeta, 6 6 MWMediaType, 7 7 } from "@/providers"; 8 - import { Icon, Icons } from "@/components/Icon"; 9 8 import { serializePortableMedia } from "@/hooks/usePortableMedia"; 10 9 import { DotList } from "@/components/text/DotList"; 11 10 12 11 export interface MediaCardProps { 13 12 media: MWMediaMeta; 13 + // eslint-disable-next-line react/no-unused-prop-types 14 14 watchedPercentage: number; 15 15 linkable?: boolean; 16 16 series?: boolean; 17 17 } 18 18 19 - function MediaCardContent({ 20 - media, 21 - linkable, 22 - watchedPercentage, 23 - series, 24 - }: MediaCardProps) { 19 + // TODO add progress back 20 + 21 + function MediaCardContent({ media, series, linkable }: MediaCardProps) { 25 22 const provider = getProviderFromId(media.providerId); 26 23 27 24 if (!provider) { ··· 29 26 } 30 27 31 28 return ( 32 - <article 33 - className={`group relative mb-4 flex overflow-hidden rounded bg-denim-300 py-4 px-5 ${ 34 - linkable ? "hover:bg-denim-400" : "" 29 + <div 30 + className={`group -m-3 mb-2 rounded-xl bg-denim-300 bg-opacity-0 transition-colors duration-100 ${ 31 + linkable ? "hover:bg-opacity-100" : "" 35 32 }`} 36 33 > 37 - {/* progress background */} 38 - {watchedPercentage > 0 ? ( 39 - <div className="absolute top-0 left-0 right-0 bottom-0"> 40 - <div 41 - className="relative h-full bg-bink-300 bg-opacity-30" 42 - style={{ 43 - width: `${watchedPercentage}%`, 44 - }} 45 - > 46 - <div className="absolute right-0 top-0 bottom-0 ml-auto w-40 bg-gradient-to-l from-bink-400 to-transparent opacity-40" /> 47 - </div> 48 - </div> 49 - ) : null} 50 - 51 - <div className="relative flex flex-1"> 52 - {/* card content */} 53 - <div className="flex-1"> 54 - <h1 className="mb-1 font-bold text-white"> 55 - {media.title} 56 - {series && media.seasonId && media.episodeId ? ( 57 - <span className="ml-2 text-xs text-denim-700"> 58 - S{media.seasonId} E{media.episodeId} 59 - </span> 60 - ) : null} 61 - </h1> 62 - <DotList 63 - className="text-xs" 64 - content={[provider.displayName, media.mediaType, media.year]} 65 - /> 66 - </div> 67 - 68 - {/* hoverable chevron */} 69 - <div 70 - className={`flex translate-x-3 items-center justify-end text-xl text-white opacity-0 transition-[opacity,transform] ${ 71 - linkable ? "group-hover:translate-x-0 group-hover:opacity-100" : "" 72 - }`} 73 - > 74 - <Icon icon={Icons.CHEVRON_RIGHT} /> 75 - </div> 76 - </div> 77 - </article> 34 + <article 35 + className={`relative mb-2 p-3 transition-transform duration-100 ${ 36 + linkable ? "group-hover:scale-95" : "" 37 + }`} 38 + > 39 + <div className="mb-4 aspect-[2/3] w-full rounded-xl bg-denim-500" /> 40 + <h1 className="mb-1 max-h-[4.5rem] text-ellipsis break-words font-bold text-white line-clamp-3"> 41 + <span>{media.title}</span> 42 + {series && media.seasonId && media.episodeId ? ( 43 + <span className="ml-2 text-xs text-denim-700"> 44 + S{media.seasonId} E{media.episodeId} 45 + </span> 46 + ) : null} 47 + </h1> 48 + <DotList 49 + className="text-xs" 50 + content={[provider.displayName, media.mediaType, media.year]} 51 + /> 52 + </article> 53 + </div> 78 54 ); 79 55 } 80 56
+11
src/components/media/MediaGrid.tsx
··· 1 + interface MediaGridProps { 2 + children?: React.ReactNode; 3 + } 4 + 5 + export function MediaGrid(props: MediaGridProps) { 6 + return ( 7 + <div className="grid grid-cols-2 gap-6 sm:grid-cols-3"> 8 + {props.children} 9 + </div> 10 + ); 11 + }
+6 -1
src/components/text/Title.tsx
··· 1 1 export interface TitleProps { 2 2 children?: React.ReactNode; 3 + className?: string; 3 4 } 4 5 5 6 export function Title(props: TitleProps) { 6 7 return ( 7 - <h1 className="mx-auto max-w-xs text-2xl font-bold text-white sm:text-3xl md:text-4xl"> 8 + <h1 9 + className={`text-2xl font-bold text-white sm:text-3xl md:text-4xl ${ 10 + props.className ?? "" 11 + }`} 12 + > 8 13 {props.children} 9 14 </h1> 10 15 );
+18 -10
src/views/search/HomeView.tsx
··· 1 1 import { Icons } from "@/components/Icon"; 2 2 import { SectionHeading } from "@/components/layout/SectionHeading"; 3 + import { MediaGrid } from "@/components/media/MediaGrid"; 3 4 import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; 4 5 import { 5 6 getIfBookmarkedFromPortable, ··· 20 21 title={t("search.bookmarks") || "Bookmarks"} 21 22 icon={Icons.BOOKMARK} 22 23 > 23 - {bookmarks.map((v) => ( 24 - <WatchedMediaCard key={[v.mediaId, v.providerId].join("|")} media={v} /> 25 - ))} 24 + <MediaGrid> 25 + {bookmarks.map((v) => ( 26 + <WatchedMediaCard 27 + key={[v.mediaId, v.providerId].join("|")} 28 + media={v} 29 + /> 30 + ))} 31 + </MediaGrid> 26 32 </SectionHeading> 27 33 ); 28 34 } ··· 44 50 title={t("search.continueWatching") || "Continue Watching"} 45 51 icon={Icons.CLOCK} 46 52 > 47 - {watchedItems.map((v) => ( 48 - <WatchedMediaCard 49 - key={[v.mediaId, v.providerId].join("|")} 50 - media={v} 51 - series 52 - /> 53 - ))} 53 + <MediaGrid> 54 + {watchedItems.map((v) => ( 55 + <WatchedMediaCard 56 + key={[v.mediaId, v.providerId].join("|")} 57 + media={v} 58 + series 59 + /> 60 + ))} 61 + </MediaGrid> 54 62 </SectionHeading> 55 63 ); 56 64 }
+1 -1
src/views/search/SearchLoadingView.tsx
··· 5 5 const { t } = useTranslation(); 6 6 return ( 7 7 <Loading 8 - className="my-24" 8 + className="mt-40" 9 9 text={t("search.loading") || "Fetching your favourite shows..."} 10 10 /> 11 11 );
+10 -7
src/views/search/SearchResultsView.tsx
··· 1 1 import { IconPatch } from "@/components/buttons/IconPatch"; 2 2 import { Icons } from "@/components/Icon"; 3 3 import { SectionHeading } from "@/components/layout/SectionHeading"; 4 + import { MediaGrid } from "@/components/media/MediaGrid"; 4 5 import { WatchedMediaCard } from "@/components/media/WatchedMediaCard"; 5 6 import { useLoading } from "@/hooks/useLoading"; 6 7 import { MWMassProviderOutput, MWQuery, SearchProviders } from "@/providers"; ··· 19 20 const icon: Icons = allFailed ? Icons.WARNING : Icons.EYE_SLASH; 20 21 21 22 return ( 22 - <div className="my-24 flex flex-col items-center justify-center space-y-3 text-center"> 23 + <div className="mt-40 flex flex-col items-center justify-center space-y-3 text-center"> 23 24 <IconPatch 24 25 icon={icon} 25 26 className={`text-xl ${allFailed ? "text-red-400" : "text-bink-600"}`} ··· 83 84 title={t("search.headingTitle") || "Search results"} 84 85 icon={Icons.SEARCH} 85 86 > 86 - {results.results.map((v) => ( 87 - <WatchedMediaCard 88 - key={[v.mediaId, v.providerId].join("|")} 89 - media={v} 90 - /> 91 - ))} 87 + <MediaGrid> 88 + {results.results.map((v) => ( 89 + <WatchedMediaCard 90 + key={[v.mediaId, v.providerId].join("|")} 91 + media={v} 92 + /> 93 + ))} 94 + </MediaGrid> 92 95 </SectionHeading> 93 96 ) : null} 94 97
+6 -6
src/views/search/SearchView.tsx
··· 5 5 import Sticky from "react-stickynode"; 6 6 import { Title } from "@/components/text/Title"; 7 7 import { useSearchQuery } from "@/hooks/useSearchQuery"; 8 + import { WideContainer } from "@/components/layout/WideContainer"; 8 9 import { useTranslation } from "react-i18next"; 9 - 10 10 import { SearchResultsPartial } from "./SearchResultsPartial"; 11 11 12 12 export function SearchView() { ··· 21 21 22 22 return ( 23 23 <> 24 - <div className="relative z-10"> 24 + <div className="relative z-10 mb-24"> 25 25 <Navigation bg={showBg} /> 26 26 <ThinContainer> 27 27 <div className="mt-44 space-y-16 text-center"> 28 28 <div className="absolute left-0 bottom-0 right-0 flex h-0 justify-center"> 29 - <div className="absolute bottom-4 h-[100vh] w-[300vh] rounded-[100%] bg-[#211D30]" /> 29 + <div className="absolute bottom-4 h-[100vh] w-[300vh] rounded-[100%] bg-denim-300" /> 30 30 </div> 31 31 <div className="relative z-20"> 32 32 <div className="mb-16"> 33 - <Title>{t("search.title")}</Title> 33 + <Title className="mx-auto max-w-xs">{t("search.title")}</Title> 34 34 </div> 35 35 <Sticky enabled top={16} onStateChange={stickStateChanged}> 36 36 <SearchBarInput ··· 46 46 </div> 47 47 </ThinContainer> 48 48 </div> 49 - <ThinContainer> 49 + <WideContainer> 50 50 <SearchResultsPartial search={search} /> 51 - </ThinContainer> 51 + </WideContainer> 52 52 </> 53 53 ); 54 54 }
+13 -13
tailwind.config.js
··· 12 12 "bink-500": "#8D66B5", 13 13 "bink-600": "#A87FD1", 14 14 "bink-700": "#CD97D6", 15 - "denim-100": "#131119", 16 - "denim-200": "#1E1A29", 17 - "denim-300": "#282336", 18 - "denim-400": "#322D43", 19 - "denim-500": "#433D55", 20 - "denim-600": "#5A5370", 21 - "denim-700": "#817998", 15 + "denim-100": "#120F1D", 16 + "denim-200": "#191526", 17 + "denim-300": "#211D30", 18 + "denim-400": "#2B263D", 19 + "denim-500": "#38334A", 20 + "denim-600": "#504B64", 21 + "denim-700": "#7A758F" 22 22 }, 23 23 24 24 /* fonts */ 25 25 fontFamily: { 26 - "open-sans": "'Open Sans'", 26 + "open-sans": "'Open Sans'" 27 27 }, 28 28 29 29 /* animations */ 30 30 keyframes: { 31 31 "loading-pin": { 32 32 "0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" }, 33 - "20%": { height: "1em", "background-color": "white" }, 34 - }, 33 + "20%": { height: "1em", "background-color": "white" } 34 + } 35 35 }, 36 - animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }, 37 - }, 36 + animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" } 37 + } 38 38 }, 39 - plugins: [require("tailwind-scrollbar")], 39 + plugins: [require("tailwind-scrollbar"), require("@tailwindcss/line-clamp")] 40 40 };
+12 -7
yarn.lock
··· 254 254 "@swc/core-win32-ia32-msvc" "1.3.22" 255 255 "@swc/core-win32-x64-msvc" "1.3.22" 256 256 257 + "@tailwindcss/line-clamp@^0.4.2": 258 + "integrity" "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==" 259 + "resolved" "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz" 260 + "version" "0.4.2" 261 + 257 262 "@tootallnate/once@2": 258 263 "version" "2.0.0" 259 264 ··· 1942 1947 "version" "1.1.4" 1943 1948 1944 1949 "json5@^1.0.1": 1945 - "integrity" "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==" 1946 - "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz" 1947 - "version" "1.0.1" 1950 + "integrity" "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==" 1951 + "resolved" "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" 1952 + "version" "1.0.2" 1948 1953 dependencies: 1949 1954 "minimist" "^1.2.0" 1950 1955 1951 1956 "json5@^2.2.0": 1952 - "integrity" "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" 1953 - "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" 1954 - "version" "2.2.1" 1957 + "integrity" "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" 1958 + "resolved" "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" 1959 + "version" "2.2.3" 1955 1960 1956 1961 "jsonparse@^1.3.1": 1957 1962 "version" "1.3.1" ··· 3225 3230 "resolved" "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-2.0.1.tgz" 3226 3231 "version" "2.0.1" 3227 3232 3228 - "tailwindcss@^3.2.4", "tailwindcss@3.x": 3233 + "tailwindcss@^3.2.4", "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@3.x": 3229 3234 "integrity" "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==" 3230 3235 "resolved" "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz" 3231 3236 "version" "3.2.4"