mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-26 17:11:50 +01:00
commit
0ca4b3cf49
@ -43,6 +43,7 @@ module.exports = {
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"no-restricted-syntax": "off",
|
||||
"import/no-unresolved": ["error", { ignore: ["^virtual:"] }],
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"consistent-return": "off",
|
||||
"no-continue": "off",
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ node_modules
|
||||
|
||||
# production
|
||||
/dist
|
||||
dev-dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -1,5 +1,8 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"eslint.format.enable": true
|
||||
"eslint.format.enable": true,
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
@ -6,16 +6,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Because watching movies legally is boring"
|
||||
content="The place for your favourite movies & shows"
|
||||
/>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#E880C5" />
|
||||
<meta name="msapplication-TileColor" content="#E880C5" />
|
||||
<meta name="theme-color" content="#E880C5" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#120f1d" />
|
||||
<meta name="msapplication-TileColor" content="#120f1d" />
|
||||
<meta name="theme-color" content="#120f1d" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
@ -26,6 +26,7 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-stickynode": "^4.1.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-use": "^17.4.0",
|
||||
"srt-webvtt": "^2.0.0",
|
||||
"unpacker": "^1.0.1"
|
||||
},
|
||||
@ -33,6 +34,7 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"test": "vitest run",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint --ext .tsx,.ts src",
|
||||
"lint:fix": "eslint --fix --ext .tsx,.ts src",
|
||||
"lint:report": "eslint --ext .tsx,.ts --output-file eslint_report.json --format json src"
|
||||
@ -86,6 +88,8 @@
|
||||
"vite": "^4.0.1",
|
||||
"vite-plugin-checker": "^0.5.6",
|
||||
"vite-plugin-package-version": "^1.0.2",
|
||||
"vitest": "^0.28.5"
|
||||
"vite-plugin-pwa": "^0.14.4",
|
||||
"vitest": "^0.28.5",
|
||||
"workbox-window": "^6.5.4"
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
<TileColor>#120f1d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
|
1
public/ping.txt
Normal file
1
public/ping.txt
Normal file
@ -0,0 +1 @@
|
||||
pong
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "movie-web",
|
||||
"short_name": "movie-web",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#E880C5",
|
||||
"background_color": "#16171D",
|
||||
"display": "standalone",
|
||||
"start_url": "/"
|
||||
}
|
@ -35,6 +35,7 @@ const format = {
|
||||
registerProvider({
|
||||
id: "gdriveplayer",
|
||||
displayName: "gdriveplayer",
|
||||
disabled: true,
|
||||
rank: 69,
|
||||
type: [MWMediaType.MOVIE],
|
||||
|
||||
|
28
src/components/Banner.tsx
Normal file
28
src/components/Banner.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { useBanner } from "@/hooks/useBanner";
|
||||
|
||||
export function Banner(props: { children: React.ReactNode; type: "error" }) {
|
||||
const [ref] = useBanner<HTMLDivElement>("internet");
|
||||
const styles = {
|
||||
error: "bg-[#C93957] text-white",
|
||||
};
|
||||
const icons = {
|
||||
error: Icons.CIRCLE_EXCLAMATION,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div
|
||||
className={[
|
||||
styles[props.type],
|
||||
"flex items-center justify-center p-1",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon icon={icons[props.type]} />
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -34,6 +34,7 @@ export enum Icons {
|
||||
CAPTIONS = "captions",
|
||||
LINK = "link",
|
||||
CASTING = "casting",
|
||||
CIRCLE_EXCLAMATION = "circle_exclamation",
|
||||
DOWNLOAD = "download",
|
||||
}
|
||||
|
||||
@ -75,6 +76,7 @@ const iconList: Record<Icons, string> = {
|
||||
file: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>`,
|
||||
captions: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="currentColor" d="M0 96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM200 208c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48s21.5-48 48-48zm144 48c0-26.5 21.5-48 48-48c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48z"/></svg>`,
|
||||
link: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
|
||||
circle_exclamation: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
|
||||
casting: "",
|
||||
download: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
|
||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { conf } from "@/setup/config";
|
||||
import { useBannerSize } from "@/hooks/useBanner";
|
||||
import { BrandPill } from "./BrandPill";
|
||||
|
||||
export interface NavigationProps {
|
||||
@ -11,8 +12,15 @@ export interface NavigationProps {
|
||||
}
|
||||
|
||||
export function Navigation(props: NavigationProps) {
|
||||
const bannerHeight = useBannerSize();
|
||||
|
||||
return (
|
||||
<div className="fixed left-0 right-0 top-0 z-20 min-h-[150px] bg-gradient-to-b from-denim-300 via-denim-300 to-transparent sm:from-transparent">
|
||||
<div
|
||||
className="fixed left-0 right-0 top-0 z-20 min-h-[150px] bg-gradient-to-b from-denim-300 via-denim-300 to-transparent sm:from-transparent"
|
||||
style={{
|
||||
top: `${bannerHeight}px`,
|
||||
}}
|
||||
>
|
||||
<div className="fixed left-0 right-0 flex items-center justify-between py-5 px-7">
|
||||
<div
|
||||
className={`${
|
||||
|
61
src/hooks/useBanner.tsx
Normal file
61
src/hooks/useBanner.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useState,
|
||||
useMemo,
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { useMeasure } from "react-use";
|
||||
|
||||
interface BannerInstance {
|
||||
id: string;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const BannerContext = createContext<
|
||||
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>]
|
||||
>(null as any);
|
||||
|
||||
export function BannerContextProvider(props: { children: ReactNode }) {
|
||||
const [state, setState] = useState<BannerInstance[]>([]);
|
||||
const memod = useMemo<
|
||||
[BannerInstance[], Dispatch<SetStateAction<BannerInstance[]>>]
|
||||
>(() => [state, setState], [state]);
|
||||
|
||||
return (
|
||||
<BannerContext.Provider value={memod}>
|
||||
{props.children}
|
||||
</BannerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBanner<T extends Element>(id: string) {
|
||||
const [ref, { height }] = useMeasure<T>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, set] = useContext(BannerContext);
|
||||
|
||||
useEffect(() => {
|
||||
set((v) => [...v, { id, height: 0 }]);
|
||||
set((value) => {
|
||||
const v = value.find((item) => item.id === id);
|
||||
if (v) {
|
||||
v.height = height;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return () => {
|
||||
set((v) => v.filter((item) => item.id !== id));
|
||||
};
|
||||
}, [height, id, set]);
|
||||
|
||||
return [ref];
|
||||
}
|
||||
|
||||
export function useBannerSize() {
|
||||
const [val] = useContext(BannerContext);
|
||||
|
||||
return val.reduce((a, v) => a + v.height, 0);
|
||||
}
|
41
src/hooks/usePing.ts
Normal file
41
src/hooks/usePing.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function useIsOnline() {
|
||||
const [online, setOnline] = useState<boolean | null>(true);
|
||||
const ref = useRef<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
let counter = 0;
|
||||
|
||||
let abort: null | AbortController = null;
|
||||
const interval = setInterval(() => {
|
||||
// if online try once every 10 iterations intead of every iteration
|
||||
counter += 1;
|
||||
if (ref.current) {
|
||||
if (counter < 10) return;
|
||||
}
|
||||
counter = 0;
|
||||
|
||||
if (abort) abort.abort();
|
||||
abort = new AbortController();
|
||||
const signal = abort.signal;
|
||||
fetch("/ping.txt", { signal })
|
||||
.then(() => {
|
||||
setOnline(true);
|
||||
ref.current = true;
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === "AbortError") return;
|
||||
setOnline(false);
|
||||
ref.current = false;
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
if (abort) abort.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return online;
|
||||
}
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom";
|
||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||
import { ErrorBoundary } from "@/components/layout/ErrorBoundary";
|
||||
import { conf } from "@/setup/config";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
import App from "@/setup/App";
|
||||
import "@/setup/ga";
|
||||
@ -19,6 +20,11 @@ if (key) {
|
||||
(window as any).initMW(conf().PROXY_URLS, key);
|
||||
}
|
||||
initializeChromecast();
|
||||
registerSW({
|
||||
onNeedRefresh() {
|
||||
window.location.reload();
|
||||
},
|
||||
});
|
||||
|
||||
const LazyLoadedApp = React.lazy(async () => {
|
||||
await initializeStores();
|
||||
|
@ -11,34 +11,48 @@ import { DeveloperView } from "@/views/developer/DeveloperView";
|
||||
import { VideoTesterView } from "@/views/developer/VideoTesterView";
|
||||
import { ProviderTesterView } from "@/views/developer/ProviderTesterView";
|
||||
import { EmbedTesterView } from "@/views/developer/EmbedTesterView";
|
||||
import { BannerContextProvider } from "@/hooks/useBanner";
|
||||
import { Layout } from "@/setup/Layout";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<WatchedContextProvider>
|
||||
<BookmarkContextProvider>
|
||||
<Switch>
|
||||
{/* functional routes */}
|
||||
<Route exact path="/v2-migration" component={V2MigrationView} />
|
||||
<Route exact path="/">
|
||||
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
|
||||
</Route>
|
||||
<BannerContextProvider>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{/* functional routes */}
|
||||
<Route exact path="/v2-migration" component={V2MigrationView} />
|
||||
<Route exact path="/">
|
||||
<Redirect to={`/search/${MWMediaType.MOVIE}`} />
|
||||
</Route>
|
||||
|
||||
{/* pages */}
|
||||
<Route exact path="/media/:media" component={MediaView} />
|
||||
<Route
|
||||
exact
|
||||
path="/media/:media/:season/:episode"
|
||||
component={MediaView}
|
||||
/>
|
||||
<Route exact path="/search/:type/:query?" component={SearchView} />
|
||||
{/* pages */}
|
||||
<Route exact path="/media/:media" component={MediaView} />
|
||||
<Route
|
||||
exact
|
||||
path="/media/:media/:season/:episode"
|
||||
component={MediaView}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/search/:type/:query?"
|
||||
component={SearchView}
|
||||
/>
|
||||
|
||||
{/* other */}
|
||||
<Route exact path="/dev" component={DeveloperView} />
|
||||
<Route exact path="/dev/video" component={VideoTesterView} />
|
||||
<Route exact path="/dev/providers" component={ProviderTesterView} />
|
||||
<Route exact path="/dev/embeds" component={EmbedTesterView} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
{/* other */}
|
||||
<Route exact path="/dev" component={DeveloperView} />
|
||||
<Route exact path="/dev/video" component={VideoTesterView} />
|
||||
<Route
|
||||
exact
|
||||
path="/dev/providers"
|
||||
component={ProviderTesterView}
|
||||
/>
|
||||
<Route exact path="/dev/embeds" component={EmbedTesterView} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</BannerContextProvider>
|
||||
</BookmarkContextProvider>
|
||||
</WatchedContextProvider>
|
||||
);
|
||||
|
27
src/setup/Layout.tsx
Normal file
27
src/setup/Layout.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Banner } from "@/components/Banner";
|
||||
import { useBannerSize } from "@/hooks/useBanner";
|
||||
import { useIsOnline } from "@/hooks/usePing";
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function Layout(props: { children: ReactNode }) {
|
||||
const { t } = useTranslation();
|
||||
const isOnline = useIsOnline();
|
||||
const bannerSize = useBannerSize();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="fixed inset-x-0 z-[1000]">
|
||||
{!isOnline ? <Banner type="error">{t("errors.offline")}</Banner> : null}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: `${bannerSize}px`,
|
||||
}}
|
||||
className="flex min-h-screen flex-col"
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -89,5 +89,8 @@
|
||||
},
|
||||
"casting": {
|
||||
"casting": "Casting to device..."
|
||||
},
|
||||
"errors": {
|
||||
"offline": "Check your internet connection"
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,7 @@ export function VideoPlayer(props: Props) {
|
||||
<HeaderAction
|
||||
showControls={isMobile}
|
||||
onClick={props.onGoBack}
|
||||
isFullScreen
|
||||
/>
|
||||
</Transition>
|
||||
<Transition
|
||||
|
@ -5,6 +5,7 @@ import { useMeta } from "@/video/state/logic/meta";
|
||||
interface Props {
|
||||
onClick?: () => void;
|
||||
showControls?: boolean;
|
||||
isFullScreen: boolean;
|
||||
}
|
||||
|
||||
export function HeaderAction(props: Props) {
|
||||
|
@ -10,11 +10,13 @@ import { AirplayAction } from "@/video/components/actions/AirplayAction";
|
||||
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { useBannerSize } from "@/hooks/useBanner";
|
||||
|
||||
interface VideoPlayerHeaderProps {
|
||||
media?: MWMediaMeta;
|
||||
onClick?: () => void;
|
||||
showControls?: boolean;
|
||||
isFullScreen?: boolean;
|
||||
}
|
||||
|
||||
export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||
@ -25,9 +27,15 @@ export function VideoPlayerHeader(props: VideoPlayerHeaderProps) {
|
||||
: false;
|
||||
const showDivider = props.media && props.onClick;
|
||||
const { t } = useTranslation();
|
||||
const bannerHeight = useBannerSize();
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{
|
||||
paddingTop: props.isFullScreen ? `${bannerHeight}px` : undefined,
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<p className="flex items-center truncate">
|
||||
{props.onClick ? (
|
||||
|
@ -9,7 +9,7 @@ export function MediaFetchErrorView() {
|
||||
const goBack = useGoBack();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex-1">
|
||||
<div className="flex-1">
|
||||
<Helmet>
|
||||
<title>{t("media.errors.failedMeta")}</title>
|
||||
</Helmet>
|
||||
|
@ -28,7 +28,7 @@ function MediaViewLoading(props: { onGoBack(): void }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen items-center justify-center">
|
||||
<div className="relative flex flex-1 items-center justify-center">
|
||||
<Helmet>
|
||||
<title>{t("videoPlayer.loading")}</title>
|
||||
</Helmet>
|
||||
@ -62,7 +62,7 @@ function MediaViewScraping(props: MediaViewScrapingProps) {
|
||||
}, [stream, props]);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-screen items-center justify-center">
|
||||
<div className="relative flex flex-1 items-center justify-center">
|
||||
<Helmet>
|
||||
<title>{props.meta.meta.title}</title>
|
||||
</Helmet>
|
||||
|
@ -17,18 +17,18 @@ export function NotFoundWrapper(props: {
|
||||
const goBack = useGoBack();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex-1">
|
||||
<div className="relative flex flex-1 flex-col">
|
||||
<Helmet>
|
||||
<title>{t("notFound.genericTitle")}</title>
|
||||
</Helmet>
|
||||
{props.video ? (
|
||||
<div className="fixed inset-x-0 top-0 py-6 px-8">
|
||||
<div className="absolute inset-x-0 top-0 py-6 px-8">
|
||||
<VideoPlayerHeader onClick={goBack} />
|
||||
</div>
|
||||
) : (
|
||||
<Navigation />
|
||||
)}
|
||||
<div className="flex h-full flex-col items-center justify-center p-5 text-center">
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,6 +7,7 @@ import { SearchBarInput } from "@/components/SearchBar";
|
||||
import { Title } from "@/components/text/Title";
|
||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { useBannerSize } from "@/hooks/useBanner";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { SearchResultsPartial } from "./SearchResultsPartial";
|
||||
|
||||
@ -14,6 +15,7 @@ export function SearchView() {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch, setSearchUnFocus] = useSearchQuery();
|
||||
const [showBg, setShowBg] = useState(false);
|
||||
const bannerSize = useBannerSize();
|
||||
|
||||
const stickStateChanged = useCallback(
|
||||
({ status }: Sticky.Status) => setShowBg(status === Sticky.STATUS_FIXED),
|
||||
@ -36,7 +38,11 @@ export function SearchView() {
|
||||
<Title className="mx-auto max-w-xs">{t("search.title")}</Title>
|
||||
</div>
|
||||
<div className="relative z-30">
|
||||
<Sticky enabled top={16} onStateChange={stickStateChanged}>
|
||||
<Sticky
|
||||
enabled
|
||||
top={16 + bannerSize}
|
||||
onStateChange={stickStateChanged}
|
||||
>
|
||||
<SearchBarInput
|
||||
onChange={setSearch}
|
||||
value={search}
|
||||
|
@ -19,7 +19,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"types": ["vite/client"]
|
||||
"types": ["vite/client", "vite-plugin-pwa/client"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
@ -1,12 +1,60 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import loadVersion from "vite-plugin-package-version";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import checker from "vite-plugin-checker";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
injectRegister: "inline",
|
||||
workbox: {
|
||||
globIgnores: ["**ping.txt**"],
|
||||
},
|
||||
includeAssets: [
|
||||
"favicon.ico",
|
||||
"apple-touch-icon.png",
|
||||
"safari-pinned-tab.svg",
|
||||
],
|
||||
manifest: {
|
||||
name: "movie-web",
|
||||
short_name: "movie-web",
|
||||
description: "The place for your favourite movies & shows",
|
||||
theme_color: "#120f1d",
|
||||
background_color: "#120f1d",
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
{
|
||||
src: "android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "android-chrome-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "android-chrome-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
loadVersion(),
|
||||
checker({
|
||||
typescript: true, // check typescript build errors in dev server
|
||||
|
Loading…
Reference in New Issue
Block a user