mirror of
https://github.com/movie-web/movie-web.git
synced 2025-09-13 18:13:24 +00:00
fix all eslint issues
Co-authored-by: William Oldham <wegg7250@gmail.com>
This commit is contained in:
@@ -4,6 +4,14 @@ export interface ButtonControlProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ButtonControl({ onClick, children, className }: ButtonControlProps) {
|
||||
return <button onClick={onClick} className={className}>{children}</button>
|
||||
export function ButtonControl({
|
||||
onClick,
|
||||
children,
|
||||
className,
|
||||
}: ButtonControlProps) {
|
||||
return (
|
||||
<button onClick={onClick} className={className} type="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@@ -9,7 +9,13 @@ import React, {
|
||||
import { Backdrop, useBackdrop } from "components/layout/Backdrop";
|
||||
import { ButtonControlProps, ButtonControl } from "./ButtonControl";
|
||||
|
||||
export interface DropdownButtonProps extends ButtonControlProps {
|
||||
export interface OptionItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: Icons;
|
||||
}
|
||||
|
||||
interface DropdownButtonProps extends ButtonControlProps {
|
||||
icon: Icons;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
@@ -24,12 +30,6 @@ export interface OptionProps {
|
||||
tabIndex?: number;
|
||||
}
|
||||
|
||||
export interface OptionItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: Icons;
|
||||
}
|
||||
|
||||
function Option({ option, onClick, tabIndex }: OptionProps) {
|
||||
return (
|
||||
<div
|
||||
@@ -49,7 +49,7 @@ function Option({ option, onClick, tabIndex }: OptionProps) {
|
||||
export const DropdownButton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DropdownButtonProps
|
||||
>((props, ref) => {
|
||||
>((props: DropdownButtonProps, ref) => {
|
||||
const [setBackdrop, backdropProps, highlightedProps] = useBackdrop();
|
||||
const [delayedSelectedId, setDelayedSelectedId] = useState(
|
||||
props.selectedItem
|
||||
|
@@ -42,7 +42,7 @@ const iconList: Record<Icons, string> = {
|
||||
export function Icon(props: IconProps) {
|
||||
return (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{ __html: iconList[props.icon] }}
|
||||
dangerouslySetInnerHTML={{ __html: iconList[props.icon] }} // eslint-disable-line react/no-danger
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
|
@@ -4,7 +4,6 @@ import { DropdownButton } from "./buttons/DropdownButton";
|
||||
import { Icons } from "./Icon";
|
||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
||||
|
||||
|
||||
export interface SearchBarProps {
|
||||
buttonText?: string;
|
||||
placeholder?: string;
|
||||
@@ -30,7 +29,7 @@ export function SearchBarInput(props: SearchBarProps) {
|
||||
return (
|
||||
<div className="bg-denim-300 hover:bg-denim-400 focus-within:bg-denim-400 flex flex-col items-center gap-4 rounded-[28px] px-4 py-4 transition-colors sm:flex-row sm:py-2 sm:pl-8 sm:pr-2">
|
||||
<TextInputControl
|
||||
onChange={setSearch}
|
||||
onChange={(val) => setSearch(val)}
|
||||
value={props.value.searchQuery}
|
||||
className="placeholder-denim-700 w-full flex-1 bg-transparent text-white focus:outline-none"
|
||||
placeholder={props.placeholder}
|
||||
@@ -39,9 +38,9 @@ export function SearchBarInput(props: SearchBarProps) {
|
||||
<DropdownButton
|
||||
icon={Icons.SEARCH}
|
||||
open={dropdownOpen}
|
||||
setOpen={setDropdownOpen}
|
||||
setOpen={(val) => setDropdownOpen(val)}
|
||||
selectedItem={props.value.type}
|
||||
setSelectedItem={setType}
|
||||
setSelectedItem={(val) => setType(val)}
|
||||
options={[
|
||||
{
|
||||
id: MWMediaType.MOVIE,
|
||||
|
@@ -40,7 +40,7 @@ export function useBackdrop(): [
|
||||
}
|
||||
|
||||
export function Backdrop(props: BackdropProps) {
|
||||
const clickEvent = props.onClick || ((e: MouseEvent) => {});
|
||||
const clickEvent = props.onClick || (() => {});
|
||||
const animationEvent = props.onBackdropHide || (() => {});
|
||||
const [isVisible, setVisible, fadeProps] = useFade();
|
||||
|
||||
@@ -63,6 +63,6 @@ export function Backdrop(props: BackdropProps) {
|
||||
}`}
|
||||
{...fadeProps}
|
||||
onClick={(e) => clickEvent(e.nativeEvent)}
|
||||
/>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -14,10 +14,16 @@ interface ErrorBoundaryState {
|
||||
};
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<{}, ErrorBoundaryState> {
|
||||
state: ErrorBoundaryState = {
|
||||
hasError: false,
|
||||
};
|
||||
export class ErrorBoundary extends Component<
|
||||
Record<string, unknown>,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor() {
|
||||
super({});
|
||||
this.state = {
|
||||
hasError: false,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return {
|
||||
@@ -50,8 +56,16 @@ export class ErrorBoundary extends Component<{}, ErrorBoundaryState> {
|
||||
<IconPatch icon={Icons.WARNING} className="mb-6 text-red-400" />
|
||||
<Title>Whoops, it broke</Title>
|
||||
<p className="my-6 max-w-lg">
|
||||
The app encountered an error and wasn't able to recover, please
|
||||
report it to the <Link url={DISCORD_LINK} newTab>Discord server</Link> or on <Link url={GITHUB_LINK} newTab>GitHub</Link>.
|
||||
The app encountered an error and wasn't able to recover, please
|
||||
report it to the{" "}
|
||||
<Link url={DISCORD_LINK} newTab>
|
||||
Discord server
|
||||
</Link>{" "}
|
||||
or on{" "}
|
||||
<Link url={GITHUB_LINK} newTab>
|
||||
GitHub
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
{this.state.error ? (
|
||||
|
@@ -2,7 +2,7 @@ import { IconPatch } from "components/buttons/IconPatch";
|
||||
import { Icons } from "components/Icon";
|
||||
import { Loading } from "components/layout/Loading";
|
||||
import { MWMediaStream } from "providers";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ReactElement, useEffect, useRef, useState } from "react";
|
||||
|
||||
export interface VideoPlayerProps {
|
||||
source: MWMediaStream;
|
||||
@@ -16,7 +16,7 @@ export function SkeletonVideoPlayer(props: { error?: boolean }) {
|
||||
{props.error ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<IconPatch icon={Icons.WARNING} className="text-red-400" />
|
||||
<p className="mt-5 text-white">Couldn't get your stream</p>
|
||||
<p className="mt-5 text-white">Couldn't get your stream</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center">
|
||||
@@ -41,13 +41,16 @@ export function VideoPlayer(props: VideoPlayerProps) {
|
||||
setErrored(false);
|
||||
}, [props.source.url]);
|
||||
|
||||
let skeletonUi: null | ReactElement = null;
|
||||
if (hasErrored) {
|
||||
skeletonUi = <SkeletonVideoPlayer error />;
|
||||
} else if (isLoading) {
|
||||
skeletonUi = <SkeletonVideoPlayer />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasErrored ? (
|
||||
<SkeletonVideoPlayer error />
|
||||
) : isLoading ? (
|
||||
<SkeletonVideoPlayer />
|
||||
) : null}
|
||||
{skeletonUi}
|
||||
<video
|
||||
className={`bg-denim-500 w-full rounded-xl ${
|
||||
!showVideo ? "hidden" : ""
|
||||
|
@@ -30,6 +30,7 @@ export function useLoading<T extends (...args: any) => Promise<any>>(
|
||||
if (!isMounted.current) return resolve(undefined);
|
||||
setSuccess(true);
|
||||
resolve(v);
|
||||
return null;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (isMounted) {
|
||||
|
@@ -2,6 +2,15 @@ import { MWPortableMedia } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
export function deserializePortableMedia(media: string): MWPortableMedia {
|
||||
return JSON.parse(atob(decodeURIComponent(media)));
|
||||
}
|
||||
|
||||
export function serializePortableMedia(media: MWPortableMedia): string {
|
||||
const data = encodeURIComponent(btoa(JSON.stringify(media)));
|
||||
return data;
|
||||
}
|
||||
|
||||
export function usePortableMedia(): MWPortableMedia | undefined {
|
||||
const { media } = useParams<{ media: string }>();
|
||||
const [mediaObject, setMediaObject] = useState<MWPortableMedia | undefined>(
|
||||
@@ -19,12 +28,3 @@ export function usePortableMedia(): MWPortableMedia | undefined {
|
||||
|
||||
return mediaObject;
|
||||
}
|
||||
|
||||
export function deserializePortableMedia(media: string): MWPortableMedia {
|
||||
return JSON.parse(atob(decodeURIComponent(media)));
|
||||
}
|
||||
|
||||
export function serializePortableMedia(media: MWPortableMedia): string {
|
||||
const data = encodeURIComponent(btoa(JSON.stringify(media)));
|
||||
return data;
|
||||
}
|
||||
|
@@ -1,9 +1,5 @@
|
||||
import { getProviderFromId } from "./methods/helpers";
|
||||
import {
|
||||
MWMedia,
|
||||
MWPortableMedia,
|
||||
MWMediaStream,
|
||||
} from "./types";
|
||||
import { MWMedia, MWPortableMedia, MWMediaStream } from "./types";
|
||||
import contentCache from "./methods/contentCache";
|
||||
|
||||
export * from "./types";
|
||||
@@ -35,7 +31,7 @@ export async function convertPortableToMedia(
|
||||
if (output) return output;
|
||||
|
||||
const provider = getProviderFromId(portable.providerId);
|
||||
return await provider?.getMediaFromPortable(portable);
|
||||
return provider?.getMediaFromPortable(portable);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -47,5 +43,5 @@ export async function getStream(
|
||||
const provider = getProviderFromId(media.providerId);
|
||||
if (!provider) return undefined;
|
||||
|
||||
return await provider.getStream(media);
|
||||
return provider.getStream(media);
|
||||
}
|
||||
|
@@ -4,7 +4,8 @@ import { MWMediaType, MWPortableMedia } from "providers/types";
|
||||
const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => {
|
||||
if (media.mediaType === MWMediaType.MOVIE) {
|
||||
return `https://theflix.to/movie/${media.mediaId}?${params}`;
|
||||
} if (media.mediaType === MWMediaType.SERIES) {
|
||||
}
|
||||
if (media.mediaType === MWMediaType.SERIES) {
|
||||
return `https://theflix.to/tv-show/${media.mediaId}/season-${media.season}/episode-${media.episode}`;
|
||||
}
|
||||
|
||||
@@ -29,7 +30,7 @@ export async function getDataFromPortableSearch(
|
||||
|
||||
if (media.mediaType === MWMediaType.MOVIE) {
|
||||
return JSON.parse(node.innerHTML).props.pageProps.movie;
|
||||
} if (media.mediaType === MWMediaType.SERIES) {
|
||||
return JSON.parse(node.innerHTML).props.pageProps.selectedTv;
|
||||
}
|
||||
// must be series here
|
||||
return JSON.parse(node.innerHTML).props.pageProps.selectedTv;
|
||||
}
|
||||
|
@@ -4,10 +4,10 @@ import { MWMediaType, MWProviderMediaResult, MWQuery } from "providers";
|
||||
const getTheFlixUrl = (type: "tv-shows" | "movies", params: URLSearchParams) =>
|
||||
`https://theflix.to/${type}/trending?${params}`;
|
||||
|
||||
export async function searchTheFlix(query: MWQuery): Promise<string> {
|
||||
export function searchTheFlix(query: MWQuery): Promise<string> {
|
||||
const params = new URLSearchParams();
|
||||
params.append("search", query.searchQuery);
|
||||
return await fetch(
|
||||
return fetch(
|
||||
CORS_PROXY_URL +
|
||||
getTheFlixUrl(
|
||||
query.type === MWMediaType.MOVIE ? "movies" : "tv-shows",
|
||||
@@ -30,14 +30,11 @@ export function getDataFromSearch(page: string, limit = 10): any[] {
|
||||
|
||||
export function turnDataIntoMedia(data: any): MWProviderMediaResult {
|
||||
return {
|
||||
mediaId:
|
||||
`${data.id
|
||||
}-${
|
||||
data.name
|
||||
.replace(/[^a-z0-9]+|\s+/gim, " ")
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.toLowerCase()}`,
|
||||
mediaId: `${data.id}-${data.name
|
||||
.replace(/[^a-z0-9]+|\s+/gim, " ")
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.toLowerCase()}`,
|
||||
title: data.name,
|
||||
year: new Date(data.releaseDate).getFullYear().toString(),
|
||||
};
|
||||
|
@@ -1,20 +1,25 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { MWMassProviderOutput, MWMedia, MWQuery, convertMediaToPortable } from "providers";
|
||||
import {
|
||||
MWMassProviderOutput,
|
||||
MWMedia,
|
||||
MWQuery,
|
||||
convertMediaToPortable,
|
||||
} from "providers";
|
||||
import { SimpleCache } from "utils/cache";
|
||||
import { GetProvidersForType } from "./helpers";
|
||||
import contentCache from "./contentCache";
|
||||
|
||||
// cache
|
||||
const resultCache = new SimpleCache<MWQuery, MWMassProviderOutput>();
|
||||
resultCache.setCompare((a,b) => a.searchQuery === b.searchQuery && a.type === b.type);
|
||||
resultCache.setCompare(
|
||||
(a, b) => a.searchQuery === b.searchQuery && a.type === b.type
|
||||
);
|
||||
resultCache.initialize();
|
||||
|
||||
/*
|
||||
** actually call all providers with the search query
|
||||
*/
|
||||
async function callProviders(
|
||||
query: MWQuery
|
||||
): Promise<MWMassProviderOutput> {
|
||||
** actually call all providers with the search query
|
||||
*/
|
||||
async function callProviders(query: MWQuery): Promise<MWMassProviderOutput> {
|
||||
const allQueries = GetProvidersForType(query.type).map<
|
||||
Promise<{ media: MWMedia[]; success: boolean; id: string }>
|
||||
>(async (provider) => {
|
||||
@@ -55,33 +60,37 @@ async function callProviders(
|
||||
|
||||
output.results.forEach((result: MWMedia) => {
|
||||
contentCache.set(convertMediaToPortable(result), result, 60 * 60);
|
||||
})
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
** sort results based on query
|
||||
*/
|
||||
function sortResults(query: MWQuery, providerResults: MWMassProviderOutput): MWMassProviderOutput {
|
||||
const fuse = new Fuse(providerResults.results, { threshold: 0.3, keys: ["title"] });
|
||||
providerResults.results = fuse.search(query.searchQuery).map((v) => v.item);
|
||||
return providerResults;
|
||||
** sort results based on query
|
||||
*/
|
||||
function sortResults(
|
||||
query: MWQuery,
|
||||
providerResults: MWMassProviderOutput
|
||||
): MWMassProviderOutput {
|
||||
const results: MWMassProviderOutput = { ...providerResults };
|
||||
const fuse = new Fuse(results.results, { threshold: 0.3, keys: ["title"] });
|
||||
results.results = fuse.search(query.searchQuery).map((v) => v.item);
|
||||
return results;
|
||||
}
|
||||
|
||||
/*
|
||||
** Call search on all providers that matches query type
|
||||
*/
|
||||
export async function SearchProviders(
|
||||
query: MWQuery
|
||||
inputQuery: MWQuery
|
||||
): Promise<MWMassProviderOutput> {
|
||||
// input normalisation
|
||||
const query = { ...inputQuery };
|
||||
query.searchQuery = query.searchQuery.toLowerCase().trim();
|
||||
|
||||
// consult cache first
|
||||
let output = resultCache.get(query);
|
||||
if (!output)
|
||||
output = await callProviders(query);
|
||||
if (!output) output = await callProviders(query);
|
||||
|
||||
// sort results
|
||||
output = sortResults(query, output);
|
||||
|
@@ -23,7 +23,7 @@ export interface MWMediaMeta extends MWPortableMedia {
|
||||
year: string;
|
||||
}
|
||||
|
||||
export type MWMedia = MWMediaMeta
|
||||
export type MWMedia = MWMediaMeta;
|
||||
|
||||
export type MWProviderMediaResult = Omit<MWMedia, "mediaType" | "providerId">;
|
||||
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import { getProviderMetadata, MWMediaMeta } from "providers";
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { BookmarkStore } from "./store";
|
||||
|
||||
interface BookmarkStoreData {
|
||||
@@ -20,55 +27,77 @@ const BookmarkedContext = createContext<BookmarkStoreDataWrapper>({
|
||||
},
|
||||
});
|
||||
|
||||
function getBookmarkIndexFromMedia(
|
||||
bookmarks: MWMediaMeta[],
|
||||
media: MWMediaMeta
|
||||
): number {
|
||||
const a = bookmarks.findIndex(
|
||||
(v) =>
|
||||
v.mediaId === media.mediaId &&
|
||||
v.providerId === media.providerId &&
|
||||
v.episode === media.episode &&
|
||||
v.season === media.season
|
||||
);
|
||||
return a;
|
||||
}
|
||||
|
||||
export function BookmarkContextProvider(props: { children: ReactNode }) {
|
||||
const bookmarkLocalstorage = BookmarkStore.get();
|
||||
const [bookmarkStorage, setBookmarkStore] = useState<BookmarkStoreData>(
|
||||
bookmarkLocalstorage as BookmarkStoreData
|
||||
);
|
||||
|
||||
function setBookmarked(data: any) {
|
||||
setBookmarkStore((old) => {
|
||||
const old2 = JSON.parse(JSON.stringify(old));
|
||||
let newData = data;
|
||||
if (data.constructor === Function) {
|
||||
newData = data(old2);
|
||||
}
|
||||
bookmarkLocalstorage.save(newData);
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
|
||||
const contextValue = {
|
||||
setItemBookmark(media: MWMediaMeta, bookmarked: boolean) {
|
||||
setBookmarked((data: BookmarkStoreData) => {
|
||||
if (bookmarked) {
|
||||
const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media);
|
||||
if (itemIndex === -1) {
|
||||
const item = {
|
||||
mediaId: media.mediaId,
|
||||
mediaType: media.mediaType,
|
||||
providerId: media.providerId,
|
||||
title: media.title,
|
||||
year: media.year,
|
||||
episode: media.episode,
|
||||
season: media.season,
|
||||
};
|
||||
data.bookmarks.push(item);
|
||||
}
|
||||
} else {
|
||||
const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media);
|
||||
if (itemIndex !== -1) {
|
||||
data.bookmarks.splice(itemIndex);
|
||||
}
|
||||
const setBookmarked = useCallback(
|
||||
(data: any) => {
|
||||
setBookmarkStore((old) => {
|
||||
const old2 = JSON.parse(JSON.stringify(old));
|
||||
let newData = data;
|
||||
if (data.constructor === Function) {
|
||||
newData = data(old2);
|
||||
}
|
||||
return data;
|
||||
bookmarkLocalstorage.save(newData);
|
||||
return newData;
|
||||
});
|
||||
},
|
||||
getFilteredBookmarks() {
|
||||
return bookmarkStorage.bookmarks.filter((bookmark) => getProviderMetadata(bookmark.providerId)?.enabled);
|
||||
},
|
||||
bookmarkStore: bookmarkStorage,
|
||||
};
|
||||
[bookmarkLocalstorage, setBookmarkStore]
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
setItemBookmark(media: MWMediaMeta, bookmarked: boolean) {
|
||||
setBookmarked((data: BookmarkStoreData) => {
|
||||
if (bookmarked) {
|
||||
const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media);
|
||||
if (itemIndex === -1) {
|
||||
const item = {
|
||||
mediaId: media.mediaId,
|
||||
mediaType: media.mediaType,
|
||||
providerId: media.providerId,
|
||||
title: media.title,
|
||||
year: media.year,
|
||||
episode: media.episode,
|
||||
season: media.season,
|
||||
};
|
||||
data.bookmarks.push(item);
|
||||
}
|
||||
} else {
|
||||
const itemIndex = getBookmarkIndexFromMedia(data.bookmarks, media);
|
||||
if (itemIndex !== -1) {
|
||||
data.bookmarks.splice(itemIndex);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
});
|
||||
},
|
||||
getFilteredBookmarks() {
|
||||
return bookmarkStorage.bookmarks.filter(
|
||||
(bookmark) => getProviderMetadata(bookmark.providerId)?.enabled
|
||||
);
|
||||
},
|
||||
bookmarkStore: bookmarkStorage,
|
||||
}),
|
||||
[bookmarkStorage, setBookmarked]
|
||||
);
|
||||
|
||||
return (
|
||||
<BookmarkedContext.Provider value={contextValue}>
|
||||
@@ -81,19 +110,6 @@ export function useBookmarkContext() {
|
||||
return useContext(BookmarkedContext);
|
||||
}
|
||||
|
||||
function getBookmarkIndexFromMedia(
|
||||
bookmarks: MWMediaMeta[],
|
||||
media: MWMediaMeta
|
||||
): number {
|
||||
const a = bookmarks.findIndex((v) => (
|
||||
v.mediaId === media.mediaId &&
|
||||
v.providerId === media.providerId &&
|
||||
v.episode === media.episode &&
|
||||
v.season === media.season
|
||||
));
|
||||
return a;
|
||||
}
|
||||
|
||||
export function getIfBookmarkedFromPortable(
|
||||
bookmarks: MWMediaMeta[],
|
||||
media: MWMediaMeta
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import { MWMediaMeta, getProviderMetadata } from "providers";
|
||||
import React, { createContext, ReactNode, useContext, useState } from "react";
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { VideoProgressStore } from "./store";
|
||||
|
||||
interface WatchedStoreItem extends MWMediaMeta {
|
||||
@@ -17,6 +24,19 @@ interface WatchedStoreDataWrapper {
|
||||
watched: WatchedStoreData;
|
||||
}
|
||||
|
||||
export function getWatchedFromPortable(
|
||||
items: WatchedStoreItem[],
|
||||
media: MWMediaMeta
|
||||
): WatchedStoreItem | undefined {
|
||||
return items.find(
|
||||
(v) =>
|
||||
v.mediaId === media.mediaId &&
|
||||
v.providerId === media.providerId &&
|
||||
v.episode === media.episode &&
|
||||
v.season === media.season
|
||||
);
|
||||
}
|
||||
|
||||
const WatchedContext = createContext<WatchedStoreDataWrapper>({
|
||||
updateProgress: () => {},
|
||||
getFilteredWatched: () => [],
|
||||
@@ -32,48 +52,60 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
||||
watchedLocalstorage as WatchedStoreData
|
||||
);
|
||||
|
||||
function setWatched(data: any) {
|
||||
setWatchedReal((old) => {
|
||||
let newData = data;
|
||||
if (data.constructor === Function) {
|
||||
newData = data(old);
|
||||
}
|
||||
watchedLocalstorage.save(newData);
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
|
||||
const contextValue = {
|
||||
updateProgress(media: MWMediaMeta, progress: number, total: number): void {
|
||||
setWatched((data: WatchedStoreData) => {
|
||||
let item = getWatchedFromPortable(data.items, media);
|
||||
if (!item) {
|
||||
item = {
|
||||
mediaId: media.mediaId,
|
||||
mediaType: media.mediaType,
|
||||
providerId: media.providerId,
|
||||
title: media.title,
|
||||
year: media.year,
|
||||
percentage: 0,
|
||||
progress: 0,
|
||||
episode: media.episode,
|
||||
season: media.season,
|
||||
};
|
||||
data.items.push(item);
|
||||
const setWatched = useCallback(
|
||||
(data: any) => {
|
||||
setWatchedReal((old) => {
|
||||
let newData = data;
|
||||
if (data.constructor === Function) {
|
||||
newData = data(old);
|
||||
}
|
||||
|
||||
// update actual item
|
||||
item.progress = progress;
|
||||
item.percentage = Math.round((progress / total) * 100);
|
||||
|
||||
return data;
|
||||
watchedLocalstorage.save(newData);
|
||||
return newData;
|
||||
});
|
||||
},
|
||||
getFilteredWatched() {
|
||||
return watched.items.filter((item) => getProviderMetadata(item.providerId)?.enabled);
|
||||
},
|
||||
watched,
|
||||
};
|
||||
[setWatchedReal, watchedLocalstorage]
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
updateProgress(
|
||||
media: MWMediaMeta,
|
||||
progress: number,
|
||||
total: number
|
||||
): void {
|
||||
setWatched((data: WatchedStoreData) => {
|
||||
let item = getWatchedFromPortable(data.items, media);
|
||||
if (!item) {
|
||||
item = {
|
||||
mediaId: media.mediaId,
|
||||
mediaType: media.mediaType,
|
||||
providerId: media.providerId,
|
||||
title: media.title,
|
||||
year: media.year,
|
||||
percentage: 0,
|
||||
progress: 0,
|
||||
episode: media.episode,
|
||||
season: media.season,
|
||||
};
|
||||
data.items.push(item);
|
||||
}
|
||||
|
||||
// update actual item
|
||||
item.progress = progress;
|
||||
item.percentage = Math.round((progress / total) * 100);
|
||||
|
||||
return data;
|
||||
});
|
||||
},
|
||||
getFilteredWatched() {
|
||||
return watched.items.filter(
|
||||
(item) => getProviderMetadata(item.providerId)?.enabled
|
||||
);
|
||||
},
|
||||
watched,
|
||||
}),
|
||||
[watched, setWatched]
|
||||
);
|
||||
|
||||
return (
|
||||
<WatchedContext.Provider value={contextValue}>
|
||||
@@ -85,15 +117,3 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
|
||||
export function useWatchedContext() {
|
||||
return useContext(WatchedContext);
|
||||
}
|
||||
|
||||
export function getWatchedFromPortable(
|
||||
items: WatchedStoreItem[],
|
||||
media: MWMediaMeta
|
||||
): WatchedStoreItem | undefined {
|
||||
return items.find((v) => (
|
||||
v.mediaId === media.mediaId &&
|
||||
v.providerId === media.providerId &&
|
||||
v.episode === media.episode &&
|
||||
v.season === media.season
|
||||
));
|
||||
}
|
||||
|
@@ -11,7 +11,8 @@ function buildStoreObject(d: any) {
|
||||
id: d.storageString,
|
||||
};
|
||||
|
||||
function update(this: any, obj: any) {
|
||||
function update(this: any, obj2: any) {
|
||||
let obj = obj2;
|
||||
if (!obj) throw new Error("object to update is not an object");
|
||||
|
||||
// repeat until object fully updated
|
||||
@@ -53,54 +54,54 @@ function buildStoreObject(d: any) {
|
||||
function get(this: any) {
|
||||
// get from storage api
|
||||
const store = this;
|
||||
let data: any = localStorage.getItem(this.id);
|
||||
let gottenData: any = localStorage.getItem(this.id);
|
||||
|
||||
// parse json if item exists
|
||||
if (data) {
|
||||
if (gottenData) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
if (!data.constructor) {
|
||||
gottenData = JSON.parse(gottenData);
|
||||
if (!gottenData.constructor) {
|
||||
console.error(
|
||||
`Storage item for store ${this.id} has not constructor`
|
||||
);
|
||||
throw new Error("storage item has no constructor");
|
||||
}
|
||||
if (data.constructor !== Object) {
|
||||
if (gottenData.constructor !== Object) {
|
||||
console.error(`Storage item for store ${this.id} is not an object`);
|
||||
throw new Error("storage item is not an object");
|
||||
}
|
||||
} catch (_) {
|
||||
// if errored, set to null so it generates new one, see below
|
||||
console.error(`Failed to parse storage item for store ${this.id}`);
|
||||
data = null;
|
||||
gottenData = null;
|
||||
}
|
||||
}
|
||||
|
||||
// if item doesnt exist, generate from version init
|
||||
if (!data) {
|
||||
data = this.versions[this.currentVersion.toString()].init();
|
||||
if (!gottenData) {
|
||||
gottenData = this.versions[this.currentVersion.toString()].init();
|
||||
}
|
||||
|
||||
// update the data if needed
|
||||
data = this.update(data);
|
||||
gottenData = this.update(gottenData);
|
||||
|
||||
// add a save object to return value
|
||||
data.save = function save(newData: any) {
|
||||
const dataToStore = newData || data;
|
||||
gottenData.save = function save(newData: any) {
|
||||
const dataToStore = newData || gottenData;
|
||||
localStorage.setItem(store.id, JSON.stringify(dataToStore));
|
||||
};
|
||||
|
||||
// add instance helpers
|
||||
Object.entries(d.instanceHelpers).forEach(([name, helper]: any) => {
|
||||
if (data[name] !== undefined)
|
||||
if (gottenData[name] !== undefined)
|
||||
throw new Error(
|
||||
`helper name: ${name} on instance of store ${this.id} is reserved`
|
||||
);
|
||||
data[name] = helper.bind(data);
|
||||
gottenData[name] = helper.bind(gottenData);
|
||||
});
|
||||
|
||||
// return data
|
||||
return data;
|
||||
return gottenData;
|
||||
}
|
||||
|
||||
// add functions to store
|
||||
@@ -158,7 +159,7 @@ export function versionedStoreBuilder(): any {
|
||||
? (data: any) => {
|
||||
// update function, and increment version
|
||||
migrate(data);
|
||||
data["--version"] = version;
|
||||
data["--version"] = version; // eslint-disable-line no-param-reassign
|
||||
return data;
|
||||
}
|
||||
: undefined,
|
||||
@@ -176,7 +177,8 @@ export function versionedStoreBuilder(): any {
|
||||
|
||||
registerHelper({ name, helper, type }: any) {
|
||||
// type
|
||||
if (!type) type = "instance";
|
||||
let helperType: string = type;
|
||||
if (!helperType) helperType = "instance";
|
||||
|
||||
// input checking
|
||||
if (!name || name.constructor !== String) {
|
||||
@@ -185,14 +187,14 @@ export function versionedStoreBuilder(): any {
|
||||
if (!helper || helper.constructor !== Function) {
|
||||
throw new Error("helper function is not a function");
|
||||
}
|
||||
if (!["instance", "static"].includes(type)) {
|
||||
if (!["instance", "static"].includes(helperType)) {
|
||||
throw new Error("helper type must be either 'instance' or 'static'");
|
||||
}
|
||||
|
||||
// register helper
|
||||
if (type === "instance")
|
||||
if (helperType === "instance")
|
||||
this._data.instanceHelpers[name as string] = helper;
|
||||
else if (type === "static")
|
||||
else if (helperType === "static")
|
||||
this._data.staticHelpers[name as string] = helper;
|
||||
|
||||
return this;
|
||||
|
@@ -17,7 +17,7 @@ import {
|
||||
getProviderFromId,
|
||||
MWMediaProvider,
|
||||
} from "providers";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { ReactElement, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
getIfBookmarkedFromPortable,
|
||||
@@ -57,7 +57,7 @@ function StyledMediaView(props: StyledMediaViewProps) {
|
||||
<>
|
||||
<VideoPlayer
|
||||
source={props.stream}
|
||||
onProgress={updateProgress}
|
||||
onProgress={(e) => updateProgress(e)}
|
||||
startAt={startAtTime}
|
||||
/>
|
||||
<Paper className="mt-5">
|
||||
@@ -110,11 +110,13 @@ function MediaViewContent(props: { portable: MWPortableMedia }) {
|
||||
const mediaPortable = props.portable;
|
||||
const [streamUrl, setStreamUrl] = useState<MWMediaStream | undefined>();
|
||||
const [media, setMedia] = useState<MWMedia | undefined>();
|
||||
const [fetchAllData, loading, error] = useLoading((mediaPortable) => {
|
||||
const streamPromise = getStream(mediaPortable);
|
||||
const mediaPromise = convertPortableToMedia(mediaPortable);
|
||||
return Promise.all([streamPromise, mediaPromise]);
|
||||
});
|
||||
const [fetchAllData, loading, error] = useLoading(
|
||||
(portable: MWPortableMedia) => {
|
||||
const streamPromise = getStream(portable);
|
||||
const mediaPromise = convertPortableToMedia(portable);
|
||||
return Promise.all([streamPromise, mediaPromise]);
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -127,7 +129,7 @@ function MediaViewContent(props: { portable: MWPortableMedia }) {
|
||||
})();
|
||||
}, [mediaPortable, setStreamUrl, fetchAllData]);
|
||||
|
||||
let content: ReactNode;
|
||||
let content: ReactElement | null = null;
|
||||
if (loading) content = <LoadingMediaView />;
|
||||
else if (error) content = <LoadingMediaView error />;
|
||||
else if (mediaPortable && media && streamUrl)
|
||||
@@ -141,7 +143,7 @@ function MediaViewContent(props: { portable: MWPortableMedia }) {
|
||||
/>
|
||||
);
|
||||
|
||||
return <>{content}</>;
|
||||
return content;
|
||||
}
|
||||
|
||||
export function MediaView() {
|
||||
@@ -152,11 +154,11 @@ export function MediaView() {
|
||||
<div className="flex min-h-screen w-full">
|
||||
<Navigation>
|
||||
<ArrowLink
|
||||
onClick={() => {
|
||||
onClick={() =>
|
||||
reactHistory.action !== "POP"
|
||||
? reactHistory.goBack()
|
||||
: reactHistory.push("/");
|
||||
}}
|
||||
: reactHistory.push("/")
|
||||
}
|
||||
direction="left"
|
||||
linkText="Go back"
|
||||
/>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { WatchedMediaCard } from "components/media/WatchedMediaCard";
|
||||
import { SearchBarInput } from "components/SearchBar";
|
||||
import { MWMassProviderOutput, MWQuery, SearchProviders } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ThinContainer } from "components/layout/ThinContainer";
|
||||
import { SectionHeading } from "components/layout/SectionHeading";
|
||||
import { Icons } from "components/Icon";
|
||||
@@ -78,9 +78,9 @@ function SearchResultsView({
|
||||
|
||||
useEffect(() => {
|
||||
async function runSearch(query: MWQuery) {
|
||||
const results = await runSearchQuery(query);
|
||||
if (!results) return;
|
||||
setResults(results);
|
||||
const searchResults = await runSearchQuery(query);
|
||||
if (!searchResults) return;
|
||||
setResults(searchResults);
|
||||
}
|
||||
|
||||
if (searchQuery.searchQuery !== "") runSearch(searchQuery);
|
||||
@@ -123,53 +123,6 @@ function SearchResultsView({
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchView() {
|
||||
const [searching, setSearching] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [search, setSearch] = useSearchQuery();
|
||||
|
||||
const debouncedSearch = useDebounce<MWQuery>(search, 2000);
|
||||
useEffect(() => {
|
||||
setSearching(search.searchQuery !== "");
|
||||
setLoading(search.searchQuery !== "");
|
||||
}, [search]);
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [debouncedSearch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navigation />
|
||||
<ThinContainer>
|
||||
{/* input section */}
|
||||
<div className="mt-44 space-y-16 text-center">
|
||||
<div className="space-y-4">
|
||||
<Tagline>Because watching legally is boring</Tagline>
|
||||
<Title>What movie do you want to watch?</Title>
|
||||
</div>
|
||||
<SearchBarInput
|
||||
onChange={setSearch}
|
||||
value={search}
|
||||
placeholder="What movie do you want to watch?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* results view */}
|
||||
{loading ? (
|
||||
<SearchLoading />
|
||||
) : searching ? (
|
||||
<SearchResultsView
|
||||
searchQuery={debouncedSearch}
|
||||
clear={() => setSearch({ searchQuery: "" })}
|
||||
/>
|
||||
) : (
|
||||
<ExtraItems />
|
||||
)}
|
||||
</ThinContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExtraItems() {
|
||||
const { getFilteredBookmarks } = useBookmarkContext();
|
||||
const { getFilteredWatched } = useWatchedContext();
|
||||
@@ -207,3 +160,53 @@ function ExtraItems() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchView() {
|
||||
const [searching, setSearching] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [search, setSearch] = useSearchQuery();
|
||||
|
||||
const debouncedSearch = useDebounce<MWQuery>(search, 2000);
|
||||
useEffect(() => {
|
||||
setSearching(search.searchQuery !== "");
|
||||
setLoading(search.searchQuery !== "");
|
||||
}, [search]);
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [debouncedSearch]);
|
||||
|
||||
const resultView = useMemo(() => {
|
||||
if (loading) return <SearchLoading />;
|
||||
if (searching)
|
||||
return (
|
||||
<SearchResultsView
|
||||
searchQuery={debouncedSearch}
|
||||
clear={() => setSearch({ searchQuery: "" })}
|
||||
/>
|
||||
);
|
||||
return <ExtraItems />;
|
||||
}, [loading, searching, debouncedSearch, setSearch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navigation />
|
||||
<ThinContainer>
|
||||
{/* input section */}
|
||||
<div className="mt-44 space-y-16 text-center">
|
||||
<div className="space-y-4">
|
||||
<Tagline>Because watching legally is boring</Tagline>
|
||||
<Title>What movie do you want to watch?</Title>
|
||||
</div>
|
||||
<SearchBarInput
|
||||
onChange={setSearch}
|
||||
value={search}
|
||||
placeholder="What movie do you want to watch?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* results view */}
|
||||
{resultView}
|
||||
</ThinContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user