mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 00:05:09 +01:00
Merge branch 'dev' of github.com:JamesHawkinss/movie-web into dev
This commit is contained in:
commit
127788ef6e
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
.env.local
|
||||||
|
.github
|
||||||
|
.vscode
|
@ -1,5 +1,5 @@
|
|||||||
const a11yOff = Object.keys(require('eslint-plugin-jsx-a11y').rules)
|
const a11yOff = Object.keys(require('eslint-plugin-jsx-a11y').rules)
|
||||||
.reduce((acc, rule) => { acc[`jsx-a11y/${rule}`] = 'off'; return acc }, {})
|
.reduce((acc, rule) => { acc[`jsx-a11y/${rule}`] = 'off'; return acc }, {})
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
@ -37,6 +37,10 @@ module.exports = {
|
|||||||
"@typescript-eslint/no-shadow": ["error"],
|
"@typescript-eslint/no-shadow": ["error"],
|
||||||
"no-restricted-syntax": "off",
|
"no-restricted-syntax": "off",
|
||||||
"react/jsx-props-no-spreading": "off",
|
"react/jsx-props-no-spreading": "off",
|
||||||
|
"consistent-return": "off",
|
||||||
|
"no-continue": "off",
|
||||||
|
"no-eval": "off",
|
||||||
|
"no-await-in-loop": "off",
|
||||||
"react/jsx-filename-extension": [
|
"react/jsx-filename-extension": [
|
||||||
"error",
|
"error",
|
||||||
{ extensions: [".js", ".tsx", ".jsx"] },
|
{ extensions: [".js", ".tsx", ".jsx"] },
|
||||||
|
13
dockerfile
Normal file
13
dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM node:16.15-alpine as build
|
||||||
|
WORKDIR /app
|
||||||
|
ENV PATH /app/node_modules/.bin:$PATH
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN yarn install
|
||||||
|
COPY . ./
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# production environment
|
||||||
|
FROM nginx:stable-alpine
|
||||||
|
COPY --from=build /app/build /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
18
package.json
18
package.json
@ -5,24 +5,15 @@
|
|||||||
"homepage": "https://movie.squeezebox.dev",
|
"homepage": "https://movie.squeezebox.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.5.0",
|
"@headlessui/react": "^1.5.0",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
|
||||||
"@testing-library/react": "^11.1.0",
|
|
||||||
"@testing-library/user-event": "^12.1.10",
|
|
||||||
"@types/crypto-js": "^4.1.1",
|
|
||||||
"@types/react-router": "^5.1.18",
|
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
"json5": "^2.2.0",
|
"json5": "^2.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-helmet": "^6.1.0",
|
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^5.0.0",
|
"react-scripts": "5.0.1",
|
||||||
"react-tracked": "^1.7.6",
|
"unpacker": "^1.0.1"
|
||||||
"scheduler": "^0.20.2",
|
|
||||||
"unpacker": "^1.0.1",
|
|
||||||
"web-vitals": "^1.0.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
@ -43,11 +34,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.4.0",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/node": "^17.0.15",
|
"@types/node": "^17.0.15",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^17.0.39",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"@types/react-router": "^5.1.18",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||||
"@typescript-eslint/parser": "^5.13.0",
|
"@typescript-eslint/parser": "^5.13.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
@ -57,7 +49,7 @@
|
|||||||
"eslint-import-resolver-typescript": "^2.5.0",
|
"eslint-import-resolver-typescript": "^2.5.0",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-react": "7.28.0",
|
"eslint-plugin-react": "7.29.4",
|
||||||
"eslint-plugin-react-hooks": "4.3.0",
|
"eslint-plugin-react-hooks": "4.3.0",
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.6",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
|
@ -8,7 +8,6 @@ import { MediaView } from "./views/MediaView";
|
|||||||
import { SearchView } from "./views/SearchView";
|
import { SearchView } from "./views/SearchView";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WatchedContextProvider>
|
<WatchedContextProvider>
|
||||||
<BookmarkContextProvider>
|
<BookmarkContextProvider>
|
||||||
|
@ -7,31 +7,39 @@ import { TextInputControl } from "./text-inputs/TextInputControl";
|
|||||||
export interface SearchBarProps {
|
export interface SearchBarProps {
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onChange: (value: MWQuery) => void;
|
onChange: (value: MWQuery, force: boolean) => void;
|
||||||
|
onUnFocus: () => void;
|
||||||
value: MWQuery;
|
value: MWQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchBarInput(props: SearchBarProps) {
|
export function SearchBarInput(props: SearchBarProps) {
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
function setSearch(value: string) {
|
function setSearch(value: string) {
|
||||||
props.onChange({
|
props.onChange(
|
||||||
...props.value,
|
{
|
||||||
searchQuery: value,
|
...props.value,
|
||||||
});
|
searchQuery: value,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
function setType(type: string) {
|
function setType(type: string) {
|
||||||
props.onChange({
|
props.onChange(
|
||||||
...props.value,
|
{
|
||||||
type: type as MWMediaType,
|
...props.value,
|
||||||
});
|
type: type as MWMediaType,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex flex-col items-center gap-4 rounded-[28px] bg-denim-300 px-4 py-4 transition-colors focus-within:bg-denim-400 hover:bg-denim-400 sm:flex-row sm:py-2 sm:pl-8 sm:pr-2">
|
||||||
<TextInputControl
|
<TextInputControl
|
||||||
|
onUnFocus={props.onUnFocus}
|
||||||
onChange={(val) => setSearch(val)}
|
onChange={(val) => setSearch(val)}
|
||||||
value={props.value.searchQuery}
|
value={props.value.searchQuery}
|
||||||
className="placeholder-denim-700 w-full flex-1 bg-transparent text-white focus:outline-none"
|
className="w-full flex-1 bg-transparent text-white placeholder-denim-700 focus:outline-none"
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ export class ErrorBoundary extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.hasError) return this.props.children;
|
if (!this.state.hasError) return this.props.children as any;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen w-full flex-col items-center justify-center px-4 py-12">
|
<div className="flex min-h-screen w-full flex-col items-center justify-center px-4 py-12">
|
||||||
@ -69,7 +69,7 @@ export class ErrorBoundary extends Component<
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{this.state.error ? (
|
{this.state.error ? (
|
||||||
<div className="bg-denim-300 w-4xl mt-12 max-w-full rounded px-6 py-4">
|
<div className="w-4xl mt-12 max-w-full rounded bg-denim-300 px-6 py-4">
|
||||||
<p className="mb-1 break-words font-bold text-white">
|
<p className="mb-1 break-words font-bold text-white">
|
||||||
{this.state.error.name} - {this.state.error.description}
|
{this.state.error.name} - {this.state.error.description}
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export interface TextInputControlPropsNoLabel {
|
export interface TextInputControlPropsNoLabel {
|
||||||
onChange?: (data: string) => void;
|
onChange?: (data: string) => void;
|
||||||
|
onUnFocus?: () => void;
|
||||||
value?: string;
|
value?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -11,6 +12,7 @@ export interface TextInputControlProps extends TextInputControlPropsNoLabel {
|
|||||||
|
|
||||||
export function TextInputControl({
|
export function TextInputControl({
|
||||||
onChange,
|
onChange,
|
||||||
|
onUnFocus,
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
className,
|
className,
|
||||||
@ -23,6 +25,7 @@ export function TextInputControl({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(e) => onChange && onChange(e.target.value)}
|
onChange={(e) => onChange && onChange(e.target.value)}
|
||||||
value={value}
|
value={value}
|
||||||
|
onBlur={() => onUnFocus && onUnFocus()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { MWPortableMedia } from "providers";
|
import { MWPortableMedia } from "providers";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
export function deserializePortableMedia(media: string): MWPortableMedia {
|
export function deserializePortableMedia(media: string): MWPortableMedia {
|
||||||
return JSON.parse(atob(decodeURIComponent(media)));
|
return JSON.parse(atob(decodeURIComponent(media)));
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import { MWMediaType, MWQuery } from "providers";
|
import { MWMediaType, MWQuery } from "providers";
|
||||||
import React, { useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
|
||||||
|
|
||||||
export function useSearchQuery(): [MWQuery, (inp: Partial<MWQuery>) => void] {
|
export function useSearchQuery(): [
|
||||||
|
MWQuery,
|
||||||
|
(inp: Partial<MWQuery>, force: boolean) => void,
|
||||||
|
() => void
|
||||||
|
] {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
const { path, params } = useRouteMatch<{ type: string; query: string }>();
|
const { path, params } = useRouteMatch<{ type: string; query: string }>();
|
||||||
const [search, setSearch] = useState<MWQuery>({
|
const [search, setSearch] = useState<MWQuery>({
|
||||||
searchQuery: "",
|
searchQuery: "",
|
||||||
type: MWMediaType.MOVIE,
|
type: MWMediaType.MOVIE,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateParams = (inp: Partial<MWQuery>) => {
|
const updateParams = (inp: Partial<MWQuery>, force: boolean) => {
|
||||||
const copySearch: MWQuery = { ...search };
|
const copySearch: MWQuery = { ...search };
|
||||||
Object.assign(copySearch, inp);
|
Object.assign(copySearch, inp);
|
||||||
setSearch(copySearch);
|
setSearch(copySearch);
|
||||||
|
if (!force) return;
|
||||||
history.replace(
|
history.replace(
|
||||||
generatePath(path, {
|
generatePath(path, {
|
||||||
query:
|
query:
|
||||||
@ -23,13 +29,27 @@ export function useSearchQuery(): [MWQuery, (inp: Partial<MWQuery>) => void] {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onUnFocus = () => {
|
||||||
|
history.replace(
|
||||||
|
generatePath(path, {
|
||||||
|
query: search.searchQuery.length === 0 ? undefined : search.searchQuery,
|
||||||
|
type: search.type,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// only run on first load of the page
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (isFirstRender.current === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isFirstRender.current = false;
|
||||||
const type =
|
const type =
|
||||||
Object.values(MWMediaType).find((v) => params.type === v) ||
|
Object.values(MWMediaType).find((v) => params.type === v) ||
|
||||||
MWMediaType.MOVIE;
|
MWMediaType.MOVIE;
|
||||||
const searchQuery = params.query || "";
|
const searchQuery = params.query || "";
|
||||||
setSearch({ type, searchQuery });
|
setSearch({ type, searchQuery });
|
||||||
}, [params, setSearch]);
|
}, [setSearch, params, isFirstRender]);
|
||||||
|
|
||||||
return [search, updateParams];
|
return [search, updateParams, onUnFocus];
|
||||||
}
|
}
|
||||||
|
103
src/providers/list/xemovie/index.ts
Normal file
103
src/providers/list/xemovie/index.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
MWMediaProvider,
|
||||||
|
MWMediaType,
|
||||||
|
MWPortableMedia,
|
||||||
|
MWMediaStream,
|
||||||
|
MWQuery,
|
||||||
|
MWProviderMediaResult,
|
||||||
|
MWMediaCaption
|
||||||
|
} from "providers/types";
|
||||||
|
|
||||||
|
import { CORS_PROXY_URL } from "mw_constants";
|
||||||
|
|
||||||
|
export const xemovieScraper: MWMediaProvider = {
|
||||||
|
id: "xemovie",
|
||||||
|
enabled: true,
|
||||||
|
type: [MWMediaType.MOVIE],
|
||||||
|
displayName: "xemovie",
|
||||||
|
|
||||||
|
async getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult> {
|
||||||
|
const res = await fetch(
|
||||||
|
`${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`,
|
||||||
|
).then(d => d.text());
|
||||||
|
|
||||||
|
const DOM = new DOMParser().parseFromString(res, "text/html");
|
||||||
|
|
||||||
|
const title = DOM.querySelector(".text-primary.text-lg.font-extrabold")?.textContent || "";
|
||||||
|
const year = DOM.querySelector("div.justify-between:nth-child(3) > div:nth-child(2)")?.textContent || "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...media,
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
} as MWProviderMediaResult;
|
||||||
|
},
|
||||||
|
|
||||||
|
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
|
||||||
|
const term = query.searchQuery.toLowerCase();
|
||||||
|
|
||||||
|
const searchUrl = `${CORS_PROXY_URL}https://xemovie.co/search?q=${encodeURIComponent(term)}`;
|
||||||
|
const searchRes = await fetch(searchUrl).then((d) => d.text());
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(searchRes, "text/html");
|
||||||
|
|
||||||
|
const movieContainer = doc.querySelectorAll(".py-10")[0].querySelector(".grid");
|
||||||
|
if (!movieContainer) return [];
|
||||||
|
const movieNodes = Array.from(movieContainer.querySelectorAll("a")).filter(link => !link.className);
|
||||||
|
|
||||||
|
const results: MWProviderMediaResult[] = movieNodes.map((node) => {
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
const aElement = parent.querySelector("a");
|
||||||
|
if (!aElement) return;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: parent.querySelector("div > div > a > h6")?.textContent,
|
||||||
|
year: parent.querySelector("div.float-right")?.textContent,
|
||||||
|
mediaId: aElement.href.split('/').pop() || "",
|
||||||
|
}
|
||||||
|
}).filter((d): d is MWProviderMediaResult => !!d);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
|
||||||
|
if (media.mediaType !== MWMediaType.MOVIE) throw new Error("Incorrect type")
|
||||||
|
|
||||||
|
const url = `${CORS_PROXY_URL}https://xemovie.co/movies/${media.mediaId}/watch`;
|
||||||
|
|
||||||
|
let streamUrl = "";
|
||||||
|
const subtitles: MWMediaCaption[] = [];
|
||||||
|
|
||||||
|
const res = await fetch(url).then(d => d.text());
|
||||||
|
const scripts = Array.from(new DOMParser().parseFromString(res, "text/html").querySelectorAll("script"));
|
||||||
|
|
||||||
|
for (const script of scripts) {
|
||||||
|
if (!script.textContent) continue;
|
||||||
|
|
||||||
|
if (script.textContent.match(/https:\/\/[a-z][0-9]\.xemovie\.com/)) {
|
||||||
|
const data = JSON.parse(JSON.stringify(eval(`(${script.textContent.replace("const data = ", "").split("};")[0]}})`)));
|
||||||
|
streamUrl = data.playlist[0].file;
|
||||||
|
|
||||||
|
for (const [index, subtitleTrack] of data.playlist[0].tracks.entries()) {
|
||||||
|
const subtitleBlob = URL.createObjectURL(
|
||||||
|
await fetch(`${CORS_PROXY_URL}${subtitleTrack.file}`).then((captionRes) => captionRes.blob())
|
||||||
|
); // do this so no need for CORS errors
|
||||||
|
|
||||||
|
subtitles.push({
|
||||||
|
id: index,
|
||||||
|
url: subtitleBlob,
|
||||||
|
label: subtitleTrack.label
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamType = streamUrl.split('.').at(-1);
|
||||||
|
if (streamType !== "mp4" && streamType !== "m3u8") throw new Error("Unsupported stream type");
|
||||||
|
|
||||||
|
return { url: streamUrl, type: streamType, captions: subtitles } as MWMediaStream;
|
||||||
|
}
|
||||||
|
};
|
@ -2,11 +2,13 @@ import { theFlixScraper } from "providers/list/theflix";
|
|||||||
import { gDrivePlayerScraper } from "providers/list/gdriveplayer";
|
import { gDrivePlayerScraper } from "providers/list/gdriveplayer";
|
||||||
import { MWWrappedMediaProvider, WrapProvider } from "providers/wrapper";
|
import { MWWrappedMediaProvider, WrapProvider } from "providers/wrapper";
|
||||||
import { gomostreamScraper } from "providers/list/gomostream";
|
import { gomostreamScraper } from "providers/list/gomostream";
|
||||||
|
import { xemovieScraper } from "providers/list/xemovie";
|
||||||
|
|
||||||
export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [
|
export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [
|
||||||
WrapProvider(theFlixScraper),
|
WrapProvider(theFlixScraper),
|
||||||
WrapProvider(gDrivePlayerScraper),
|
WrapProvider(gDrivePlayerScraper),
|
||||||
WrapProvider(gomostreamScraper),
|
WrapProvider(gomostreamScraper),
|
||||||
|
WrapProvider(xemovieScraper),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const mediaProviders: MWWrappedMediaProvider[] =
|
export const mediaProviders: MWWrappedMediaProvider[] =
|
||||||
|
@ -73,7 +73,11 @@ function sortResults(
|
|||||||
providerResults: MWMassProviderOutput
|
providerResults: MWMassProviderOutput
|
||||||
): MWMassProviderOutput {
|
): MWMassProviderOutput {
|
||||||
const results: MWMassProviderOutput = { ...providerResults };
|
const results: MWMassProviderOutput = { ...providerResults };
|
||||||
const fuse = new Fuse(results.results, { threshold: 0.3, keys: ["title"] });
|
const fuse = new Fuse(results.results, {
|
||||||
|
threshold: 0.3,
|
||||||
|
keys: ["title"],
|
||||||
|
fieldNormWeight: 0.5,
|
||||||
|
});
|
||||||
results.results = fuse.search(query.searchQuery).map((v) => v.item);
|
results.results = fuse.search(query.searchQuery).map((v) => v.item);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
@ -165,7 +165,7 @@ function ExtraItems() {
|
|||||||
export function SearchView() {
|
export function SearchView() {
|
||||||
const [searching, setSearching] = useState<boolean>(false);
|
const [searching, setSearching] = useState<boolean>(false);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [search, setSearch] = useSearchQuery();
|
const [search, setSearch, setSearchUnFocus] = useSearchQuery();
|
||||||
|
|
||||||
const debouncedSearch = useDebounce<MWQuery>(search, 2000);
|
const debouncedSearch = useDebounce<MWQuery>(search, 2000);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -182,7 +182,7 @@ export function SearchView() {
|
|||||||
return (
|
return (
|
||||||
<SearchResultsView
|
<SearchResultsView
|
||||||
searchQuery={debouncedSearch}
|
searchQuery={debouncedSearch}
|
||||||
clear={() => setSearch({ searchQuery: "" })}
|
clear={() => setSearch({ searchQuery: "" }, true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return <ExtraItems />;
|
return <ExtraItems />;
|
||||||
@ -201,6 +201,7 @@ export function SearchView() {
|
|||||||
<SearchBarInput
|
<SearchBarInput
|
||||||
onChange={setSearch}
|
onChange={setSearch}
|
||||||
value={search}
|
value={search}
|
||||||
|
onUnFocus={setSearchUnFocus}
|
||||||
placeholder="What movie do you want to watch?"
|
placeholder="What movie do you want to watch?"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user