mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-10 22:05:06 +01:00
Lightbar
This commit is contained in:
parent
eb57f1958f
commit
1fde44076a
@ -1,4 +1,8 @@
|
||||
import c from "classnames";
|
||||
import { useState } from "react";
|
||||
|
||||
import { MWQuery } from "@/backend/metadata/types/mw";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
|
||||
import { Icon, Icons } from "./Icon";
|
||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
||||
@ -11,6 +15,8 @@ export interface SearchBarProps {
|
||||
}
|
||||
|
||||
export function SearchBarInput(props: SearchBarProps) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
function setSearch(value: string) {
|
||||
props.onChange(
|
||||
{
|
||||
@ -22,18 +28,42 @@ export function SearchBarInput(props: SearchBarProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="pointer-events-none absolute bottom-0 left-5 top-0 flex max-h-14 items-center">
|
||||
<Icon icon={Icons.SEARCH} />
|
||||
</div>
|
||||
|
||||
<TextInputControl
|
||||
onUnFocus={props.onUnFocus}
|
||||
onChange={(val) => setSearch(val)}
|
||||
value={props.value.searchQuery}
|
||||
className="w-full flex-1 bg-transparent px-4 py-4 pl-12 text-white placeholder-denim-700 focus:outline-none sm:py-4 sm:pr-2"
|
||||
placeholder={props.placeholder}
|
||||
<Flare.Base
|
||||
className={c({
|
||||
"hover:flare-enabled group relative flex flex-col rounded-[28px] transition-colors sm:flex-row sm:items-center":
|
||||
true,
|
||||
"bg-search-background": !focused,
|
||||
"bg-search-focused": focused,
|
||||
})}
|
||||
>
|
||||
<Flare.Light
|
||||
flareSize={400}
|
||||
enabled={focused}
|
||||
className="rounded-[28px]"
|
||||
backgroundClass={c({
|
||||
"transition-colors": true,
|
||||
"bg-search-background": !focused,
|
||||
"bg-search-focused": focused,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Flare.Child className="flex flex-1 flex-col">
|
||||
<div className="pointer-events-none absolute bottom-0 left-5 top-0 flex max-h-14 items-center text-search-icon">
|
||||
<Icon icon={Icons.SEARCH} />
|
||||
</div>
|
||||
|
||||
<TextInputControl
|
||||
onUnFocus={() => {
|
||||
setFocused(false);
|
||||
props.onUnFocus();
|
||||
}}
|
||||
onFocus={() => setFocused(true)}
|
||||
onChange={(val) => setSearch(val)}
|
||||
value={props.value.searchQuery}
|
||||
className="text-search-text w-full flex-1 bg-transparent px-4 py-4 pl-12 placeholder-search-placeholder focus:outline-none sm:py-4 sm:pr-2"
|
||||
placeholder={props.placeholder}
|
||||
/>
|
||||
</Flare.Child>
|
||||
</Flare.Base>
|
||||
);
|
||||
}
|
||||
|
@ -22,6 +22,15 @@ function FooterLink(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function Dmca() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<FooterLink icon={Icons.DRAGON} href="https://youtu.be/-WOonkg_ZCo">
|
||||
{t("footer.links.dmca")}
|
||||
</FooterLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -33,29 +42,27 @@ export function Footer() {
|
||||
<BrandPill />
|
||||
</div>
|
||||
<p className="mt-4 lg:max-w-[400px]">{t("footer.tagline")}</p>
|
||||
<div className="mt-8 space-x-[2rem]">
|
||||
<FooterLink icon={Icons.GITHUB} href="https://github.com/movie-web">
|
||||
{t("footer.links.github")}
|
||||
</FooterLink>
|
||||
<FooterLink
|
||||
icon={Icons.DISCORD}
|
||||
href="https://github.com/movie-web"
|
||||
>
|
||||
{t("footer.links.github")}
|
||||
</FooterLink>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:text-right">
|
||||
<h3 className="font-semibold text-type-emphasis">
|
||||
{t("footer.legal.disclaimer")}
|
||||
</h3>
|
||||
<p className="mt-3">{t("footer.legal.disclaimerText")}</p>
|
||||
<div className="mt-8">
|
||||
<FooterLink icon={Icons.DRAGON} href="https://youtu.be/-WOonkg_ZCo">
|
||||
{t("footer.links.dmca")}
|
||||
</FooterLink>
|
||||
</div>
|
||||
<div className="space-x-[2rem]">
|
||||
<FooterLink icon={Icons.GITHUB} href="https://github.com/movie-web">
|
||||
{t("footer.links.github")}
|
||||
</FooterLink>
|
||||
<FooterLink icon={Icons.DISCORD} href="https://discord.movie-web.app">
|
||||
{t("footer.links.discord")}
|
||||
</FooterLink>
|
||||
<div className="inline md:hidden">
|
||||
<Dmca />
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden items-center justify-end md:flex">
|
||||
<Dmca />
|
||||
</div>
|
||||
</WideContainer>
|
||||
</footer>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
|
||||
|
||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { Lightbar } from "@/components/utils/Lightbar";
|
||||
import { useBannerSize } from "@/hooks/useBanner";
|
||||
import { conf } from "@/setup/config";
|
||||
import SettingsModal from "@/views/SettingsModal";
|
||||
@ -18,60 +19,67 @@ export function Navigation(props: NavigationProps) {
|
||||
const bannerHeight = useBannerSize();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
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"
|
||||
style={{
|
||||
top: `${bannerHeight}px`,
|
||||
}}
|
||||
>
|
||||
<div className="fixed left-0 right-0 flex items-center justify-between px-7 py-5">
|
||||
<div
|
||||
className={`${
|
||||
props.bg ? "opacity-100" : "opacity-0"
|
||||
} absolute inset-0 block bg-denim-100 transition-opacity duration-300`}
|
||||
>
|
||||
<div className="pointer-events-none absolute -bottom-24 h-24 w-full bg-gradient-to-b from-denim-100 to-transparent" />
|
||||
</div>
|
||||
<div className="relative flex w-full items-center justify-center sm:w-fit">
|
||||
<div className="mr-auto sm:mr-6">
|
||||
<Link to="/">
|
||||
<BrandPill clickable />
|
||||
</Link>
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
props.children ? "hidden sm:flex" : "flex"
|
||||
} relative flex-row gap-4`}
|
||||
>
|
||||
<IconPatch
|
||||
className="text-2xl text-white"
|
||||
icon={Icons.GEAR}
|
||||
clickable
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
/>
|
||||
<a
|
||||
href={conf().DISCORD_LINK}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-2xl text-white"
|
||||
>
|
||||
<IconPatch icon={Icons.DISCORD} clickable />
|
||||
</a>
|
||||
<a
|
||||
href={conf().GITHUB_LINK}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-2xl text-white"
|
||||
>
|
||||
<IconPatch icon={Icons.GITHUB} clickable />
|
||||
</a>
|
||||
<>
|
||||
<div className="absolute inset-x-0 top-0 flex h-[88px] items-center justify-center">
|
||||
<div className="absolute inset-x-0 flex items-center">
|
||||
<Lightbar />
|
||||
</div>
|
||||
</div>
|
||||
<SettingsModal show={showModal} onClose={() => setShowModal(false)} />
|
||||
</div>
|
||||
<div
|
||||
className="fixed left-0 right-0 top-0 min-h-[150px] bg-gradient-to-b from-background-main via-background-main to-transparent sm:from-transparent"
|
||||
style={{
|
||||
top: `${bannerHeight}px`,
|
||||
}}
|
||||
>
|
||||
<div className="fixed left-0 right-0 flex items-center justify-between px-7 py-5">
|
||||
<div
|
||||
className={`${
|
||||
props.bg ? "opacity-100" : "opacity-0"
|
||||
} absolute inset-0 block bg-background-main transition-opacity duration-300`}
|
||||
>
|
||||
<div className="pointer-events-none absolute -bottom-24 h-24 w-full bg-gradient-to-b from-background-main to-transparent" />
|
||||
</div>
|
||||
<div className="relative flex w-full items-center justify-center sm:w-fit">
|
||||
<div className="mr-auto sm:mr-6">
|
||||
<Link to="/">
|
||||
<BrandPill clickable />
|
||||
</Link>
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
props.children ? "hidden sm:flex" : "flex"
|
||||
} relative flex-row gap-4`}
|
||||
>
|
||||
<IconPatch
|
||||
className="text-2xl text-white"
|
||||
icon={Icons.GEAR}
|
||||
clickable
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
}}
|
||||
/>
|
||||
<a
|
||||
href={conf().DISCORD_LINK}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-2xl text-white"
|
||||
>
|
||||
<IconPatch icon={Icons.DISCORD} clickable />
|
||||
</a>
|
||||
<a
|
||||
href={conf().GITHUB_LINK}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-2xl text-white"
|
||||
>
|
||||
<IconPatch icon={Icons.GITHUB} clickable />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsModal show={showModal} onClose={() => setShowModal(false)} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
export interface TextInputControlPropsNoLabel {
|
||||
onChange?: (data: string) => void;
|
||||
onUnFocus?: () => void;
|
||||
onFocus?: () => void;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
@ -17,6 +18,7 @@ export function TextInputControl({
|
||||
label,
|
||||
className,
|
||||
placeholder,
|
||||
onFocus,
|
||||
}: TextInputControlProps) {
|
||||
const input = (
|
||||
<input
|
||||
@ -26,6 +28,7 @@ export function TextInputControl({
|
||||
onChange={(e) => onChange && onChange(e.target.value)}
|
||||
value={value}
|
||||
onBlur={() => onUnFocus && onUnFocus()}
|
||||
onFocus={() => onFocus?.()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
7
src/components/utils/Flare.css
Normal file
7
src/components/utils/Flare.css
Normal file
@ -0,0 +1,7 @@
|
||||
.flare-enabled .flare-light {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.hover\:flare-enabled:hover .flare-light {
|
||||
opacity: 1 !important;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import c from "classnames";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import "./Flare.css";
|
||||
|
||||
export interface FlareProps {
|
||||
className?: string;
|
||||
@ -12,7 +13,15 @@ export interface FlareProps {
|
||||
const SIZE_DEFAULT = 200;
|
||||
const CSS_VAR_DEFAULT = "--colors-global-accentA";
|
||||
|
||||
export function Flare(props: FlareProps) {
|
||||
function Base(props: { className?: string; children?: ReactNode }) {
|
||||
return <div className={c(props.className, "relative")}>{props.children}</div>;
|
||||
}
|
||||
|
||||
function Child(props: { className?: string; children?: ReactNode }) {
|
||||
return <div className={c(props.className, "relative")}>{props.children}</div>;
|
||||
}
|
||||
|
||||
function Light(props: FlareProps) {
|
||||
const outerRef = useRef<HTMLDivElement>(null);
|
||||
const size = props.flareSize ?? SIZE_DEFAULT;
|
||||
const cssVar = props.cssColorVar ?? CSS_VAR_DEFAULT;
|
||||
@ -20,13 +29,14 @@ export function Flare(props: FlareProps) {
|
||||
useEffect(() => {
|
||||
function mouseMove(e: MouseEvent) {
|
||||
if (!outerRef.current) return;
|
||||
const rect = outerRef.current.getBoundingClientRect();
|
||||
outerRef.current.style.setProperty(
|
||||
"--bg-x",
|
||||
`${(e.clientX - size / 2).toFixed(0)}px`
|
||||
`${(e.clientX - rect.left - size / 2).toFixed(0)}px`
|
||||
);
|
||||
outerRef.current.style.setProperty(
|
||||
"--bg-y",
|
||||
`${(e.clientY - size / 2).toFixed(0)}px`
|
||||
`${(e.clientY - rect.top - size / 2).toFixed(0)}px`
|
||||
);
|
||||
}
|
||||
document.addEventListener("mousemove", mouseMove);
|
||||
@ -38,10 +48,10 @@ export function Flare(props: FlareProps) {
|
||||
<div
|
||||
ref={outerRef}
|
||||
className={c(
|
||||
"overflow-hidden, pointer-events-none absolute inset-0 hidden",
|
||||
"flare-light pointer-events-none absolute inset-0 overflow-hidden opacity-0 transition-opacity duration-[400ms]",
|
||||
props.className,
|
||||
{
|
||||
"!block": props.enabled ?? false,
|
||||
"!opacity-100": props.enabled ?? false,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
@ -49,7 +59,7 @@ export function Flare(props: FlareProps) {
|
||||
backgroundPosition: `var(--bg-x) var(--bg-y)`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundAttachment: "fixed",
|
||||
backgroundSize: "200px 200px",
|
||||
backgroundSize: `${size.toFixed(0)}px ${size.toFixed(0)}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@ -60,16 +70,22 @@ export function Flare(props: FlareProps) {
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 opacity-5"
|
||||
className="absolute inset-0 opacity-10"
|
||||
style={{
|
||||
background: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`,
|
||||
backgroundPosition: `var(--bg-x) var(--bg-y)`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundAttachment: "fixed",
|
||||
backgroundSize: "200px 200px",
|
||||
backgroundSize: `${size.toFixed(0)}px ${size.toFixed(0)}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Flare = {
|
||||
Base,
|
||||
Light,
|
||||
Child,
|
||||
};
|
||||
|
22
src/components/utils/Lightbar.css
Normal file
22
src/components/utils/Lightbar.css
Normal file
@ -0,0 +1,22 @@
|
||||
.lightbar {
|
||||
position: absolute;
|
||||
left: -25vw;
|
||||
top: 0;
|
||||
width: 150vw;
|
||||
height: 800px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
--top: theme('colors.background.main');
|
||||
--bottom: theme('colors.lightBar.light');
|
||||
--first: conic-gradient(from 90deg at 80% 50%,var(--top),var(--bottom));
|
||||
--second: conic-gradient(from 270deg at 20% 50%,var(--bottom),var(--top));
|
||||
mask-image: radial-gradient(100% 50% at center center, black, transparent);
|
||||
background-image: var(--first), var(--second);
|
||||
background-position-x: 1%, 99%;
|
||||
background-position-y: 0%, 0%;
|
||||
background-size: 50% 100%, 50% 100%;
|
||||
opacity: 1;
|
||||
transform: rotate(180deg) translateZ(0px) translateY(400px);
|
||||
transform-origin: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
9
src/components/utils/Lightbar.tsx
Normal file
9
src/components/utils/Lightbar.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import "./Lightbar.css";
|
||||
|
||||
export function Lightbar(props: { className?: string }) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className="lightbar" />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -17,7 +17,6 @@ import { SettingsProvider } from "@/state/settings";
|
||||
import { WatchedContextProvider } from "@/state/watched";
|
||||
import { MediaView } from "@/views/media/MediaView";
|
||||
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
||||
import { V2MigrationView } from "@/views/other/v2Migration";
|
||||
import { SearchView } from "@/views/search/SearchView";
|
||||
|
||||
function LegacyUrlView({ children }: { children: ReactElement }) {
|
||||
@ -62,7 +61,6 @@ function App() {
|
||||
<Layout>
|
||||
<Switch>
|
||||
{/* functional routes */}
|
||||
<Route exact path="/v2-migration" component={V2MigrationView} />
|
||||
<Route exact path="/s/:query">
|
||||
<QuickSearch />
|
||||
</Route>
|
||||
|
@ -4,9 +4,10 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply bg-denim-100 font-open-sans text-denim-700 overflow-x-hidden;
|
||||
@apply bg-background-main font-open-sans text-denim-700 overflow-x-hidden;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
html[data-full],
|
||||
|
@ -10,7 +10,7 @@
|
||||
"headingTitle": "Search results",
|
||||
"bookmarks": "Bookmarks",
|
||||
"continueWatching": "Continue Watching",
|
||||
"title": "What do you want to watch?",
|
||||
"title": "What to watch tonight?",
|
||||
"placeholder": "What do you want to watch?"
|
||||
},
|
||||
"media": {
|
||||
@ -136,7 +136,8 @@
|
||||
"tagline": "Watch your favorite shows and movies with this open source streaming app.",
|
||||
"links": {
|
||||
"github": "GitHub",
|
||||
"dmca": "DMCA"
|
||||
"dmca": "DMCA",
|
||||
"discord": "Discord"
|
||||
},
|
||||
"legal": {
|
||||
"disclaimer": "Disclaimer",
|
||||
|
0
src/views/HomePage.tsx
Normal file
0
src/views/HomePage.tsx
Normal file
0
src/views/SearchPart.tsx
Normal file
0
src/views/SearchPart.tsx
Normal file
@ -1,107 +0,0 @@
|
||||
import pako from "pako";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||
import { conf } from "@/setup/config";
|
||||
|
||||
function fromBinary(str: string): Uint8Array {
|
||||
const result = new Uint8Array(str.length);
|
||||
[...str].forEach((char, i) => {
|
||||
result[i] = char.charCodeAt(0);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function importV2Data({ data, time }: { data: any; time: Date }) {
|
||||
const savedTime = localStorage.getItem("mw-migration-date");
|
||||
if (savedTime) {
|
||||
if (new Date(savedTime) >= time) {
|
||||
// has already migrated this or something newer, skip
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// restore migration data
|
||||
if (data.bookmarks)
|
||||
localStorage.setItem("mw-bookmarks", JSON.stringify(data.bookmarks));
|
||||
if (data.videoProgress)
|
||||
localStorage.setItem("video-progress", JSON.stringify(data.videoProgress));
|
||||
|
||||
localStorage.setItem("mw-migration-date", time.toISOString());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function EmbedMigration() {
|
||||
let hasReceivedMigrationData = false;
|
||||
|
||||
const onMessage = (e: any) => {
|
||||
const data = e.data;
|
||||
if (data && data.isMigrationData && !hasReceivedMigrationData) {
|
||||
hasReceivedMigrationData = true;
|
||||
const didImport = importV2Data({
|
||||
data: data.data,
|
||||
time: data.date,
|
||||
});
|
||||
if (didImport) window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", onMessage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", onMessage);
|
||||
};
|
||||
});
|
||||
|
||||
return <iframe src="https://movie.squeezebox.dev" hidden />;
|
||||
}
|
||||
|
||||
export function V2MigrationView() {
|
||||
const [done, setDone] = useState(false);
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search ?? "");
|
||||
if (!params.has("m-time") || !params.has("m-data")) {
|
||||
// migration params missing, just redirect
|
||||
setDone(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(
|
||||
pako.inflate(fromBinary(atob(params.get("m-data") as string)), {
|
||||
to: "string",
|
||||
})
|
||||
);
|
||||
const timeOfMigration = new Date(params.get("m-time") as string);
|
||||
|
||||
importV2Data({
|
||||
data,
|
||||
time: timeOfMigration,
|
||||
});
|
||||
|
||||
// finished
|
||||
setDone(true);
|
||||
}, []);
|
||||
|
||||
// redirect when done
|
||||
useEffect(() => {
|
||||
if (!done) return;
|
||||
const newUrl = new URL(window.location.href);
|
||||
|
||||
const newParams = [] as string[];
|
||||
newUrl.searchParams.forEach((_, key) => newParams.push(key));
|
||||
newParams.forEach((v) => newUrl.searchParams.delete(v));
|
||||
newUrl.searchParams.append("migrated", "1");
|
||||
|
||||
// hash router compatibility
|
||||
newUrl.hash = conf().NORMAL_ROUTER ? "" : `/search/${MWMediaType.MOVIE}`;
|
||||
newUrl.pathname = conf().NORMAL_ROUTER
|
||||
? `/search/${MWMediaType.MOVIE}`
|
||||
: "";
|
||||
|
||||
window.location.href = newUrl.toString();
|
||||
}, [done]);
|
||||
|
||||
return null;
|
||||
}
|
58
src/views/parts/home/BookmarksPart.tsx
Normal file
58
src/views/parts/home/BookmarksPart.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { EditButton } from "@/components/buttons/EditButton";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { useBookmarkContext } from "@/state/bookmark";
|
||||
import { useWatchedContext } from "@/state/watched";
|
||||
|
||||
export function BookmarksPart() {
|
||||
const { t } = useTranslation();
|
||||
const { getFilteredBookmarks, setItemBookmark } = useBookmarkContext();
|
||||
const bookmarks = getFilteredBookmarks();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const { watched } = useWatchedContext();
|
||||
|
||||
const bookmarksSorted = useMemo(() => {
|
||||
return bookmarks
|
||||
.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
watched: watched.items
|
||||
.sort((a, b) => b.watchedAt - a.watchedAt)
|
||||
.find((watchedItem) => watchedItem.item.meta.id === v.id),
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
(a, b) => (b.watched?.watchedAt || 0) - (a.watched?.watchedAt || 0)
|
||||
);
|
||||
}, [watched.items, bookmarks]);
|
||||
|
||||
if (bookmarks.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeading
|
||||
title={t("search.bookmarks") || "Bookmarks"}
|
||||
icon={Icons.BOOKMARK}
|
||||
>
|
||||
<EditButton editing={editing} onEdit={setEditing} />
|
||||
</SectionHeading>
|
||||
<MediaGrid ref={gridRef}>
|
||||
{bookmarksSorted.map((v) => (
|
||||
<WatchedMediaCard
|
||||
key={v.id}
|
||||
media={v}
|
||||
closable={editing}
|
||||
onClose={() => setItemBookmark(v, false)}
|
||||
/>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
45
src/views/parts/home/WatchingPart.tsx
Normal file
45
src/views/parts/home/WatchingPart.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
getIfBookmarkedFromPortable,
|
||||
useBookmarkContext,
|
||||
} from "@/state/bookmark";
|
||||
import { useWatchedContext } from "@/state/watched";
|
||||
|
||||
function Watched() {
|
||||
const { t } = useTranslation();
|
||||
const { getFilteredBookmarks } = useBookmarkContext();
|
||||
const { getFilteredWatched, removeProgress } = useWatchedContext();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [gridRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
const bookmarks = getFilteredBookmarks();
|
||||
const watchedItems = getFilteredWatched().filter(
|
||||
(v) => !getIfBookmarkedFromPortable(bookmarks, v.item.meta)
|
||||
);
|
||||
|
||||
if (watchedItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionHeading
|
||||
title={t("search.continueWatching") || "Continue Watching"}
|
||||
icon={Icons.CLOCK}
|
||||
>
|
||||
<EditButton editing={editing} onEdit={setEditing} />
|
||||
</SectionHeading>
|
||||
<MediaGrid ref={gridRef}>
|
||||
{watchedItems.map((v) => (
|
||||
<WatchedMediaCard
|
||||
key={v.item.meta.id}
|
||||
media={v.item.meta}
|
||||
closable={editing}
|
||||
onClose={() => removeProgress(v.item.meta.id)}
|
||||
/>
|
||||
))}
|
||||
</MediaGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,12 +1,9 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { EditButton } from "@/components/buttons/EditButton";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { Modal, ModalCard } from "@/components/layout/Modal";
|
||||
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
@ -16,8 +13,6 @@ import {
|
||||
} from "@/state/bookmark";
|
||||
import { useWatchedContext } from "@/state/watched";
|
||||
|
||||
import { EmbedMigration } from "../other/v2Migration";
|
||||
|
||||
function Bookmarks() {
|
||||
const { t } = useTranslation();
|
||||
const { getFilteredBookmarks, setItemBookmark } = useBookmarkContext();
|
||||
@ -101,106 +96,9 @@ function Watched() {
|
||||
);
|
||||
}
|
||||
|
||||
function NewDomainModal() {
|
||||
const [show, setShow] = useState(
|
||||
new URLSearchParams(window.location.search).get("migrated") === "1" ||
|
||||
localStorage.getItem("mw-show-domain-modal") === "true"
|
||||
);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
localStorage.setItem("mw-show-domain-modal", "false");
|
||||
setShow(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const newParams = new URLSearchParams(history.location.search);
|
||||
newParams.delete("migrated");
|
||||
if (newParams.get("migrated") === "1")
|
||||
localStorage.setItem("mw-show-domain-modal", "true");
|
||||
history.replace({
|
||||
search: newParams.toString(),
|
||||
});
|
||||
}, [history]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setLoaded(true);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
// If you see this bit of code, don't snitch!
|
||||
// We need to urge users to update their bookmarks and usage,
|
||||
// so we're putting a fake deadline that's only 2 weeks away.
|
||||
const day = 1e3 * 60 * 60 * 24;
|
||||
const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
const firstVisitToSite = new Date(
|
||||
localStorage.getItem("firstVisitToSite") || Date.now()
|
||||
);
|
||||
localStorage.setItem("firstVisitToSite", firstVisitToSite.toISOString());
|
||||
const fakeEndResult = new Date(firstVisitToSite.getTime() + 14 * day);
|
||||
const endDateString = `${fakeEndResult.getDate()} ${
|
||||
months[fakeEndResult.getMonth()]
|
||||
} ${fakeEndResult.getFullYear()}`;
|
||||
|
||||
return (
|
||||
<Modal show={show && loaded}>
|
||||
<ModalCard>
|
||||
<div className="mb-12">
|
||||
<div
|
||||
className="absolute left-0 top-0 h-[300px] w-full -translate-y-1/2 opacity-50"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(ellipse 70% 9rem, #7831C1 0%, transparent 100%)`,
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex items-center justify-center">
|
||||
<div className="rounded-full bg-bink-200 px-12 py-4 text-center text-sm font-bold text-white md:text-xl">
|
||||
{t("v3.newDomain")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-white">
|
||||
{t("v3.newSiteTitle")}
|
||||
</h2>
|
||||
<p className="leading-7">
|
||||
<Trans i18nKey="v3.newDomainText" values={{ date: endDateString }}>
|
||||
<span className="text-slate-300" />
|
||||
<span className="font-bold text-white" />
|
||||
</Trans>
|
||||
</p>
|
||||
<p>{t("v3.tireless")}</p>
|
||||
</div>
|
||||
<div className="mb-6 mt-16 flex items-center justify-center">
|
||||
<Button icon={Icons.PLAY} onClick={() => closeModal()}>
|
||||
{t("v3.leaveAnnouncement")}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCard>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function HomeView() {
|
||||
return (
|
||||
<div>
|
||||
<EmbedMigration />
|
||||
<NewDomainModal />
|
||||
<Bookmarks />
|
||||
<Watched />
|
||||
</div>
|
||||
|
@ -27,16 +27,13 @@ export function SearchView() {
|
||||
|
||||
return (
|
||||
<FooterView>
|
||||
<Navigation bg={showBg} />
|
||||
<div className="relative z-10 mb-16 sm:mb-24">
|
||||
<Helmet>
|
||||
<title>{t("global.name")}</title>
|
||||
</Helmet>
|
||||
<Navigation bg={showBg} />
|
||||
<ThinContainer>
|
||||
<div className="mt-44 space-y-16 text-center">
|
||||
<div className="absolute bottom-0 left-0 right-0 flex h-0 justify-center">
|
||||
<div className="absolute bottom-4 h-[100vh] w-[3000px] rounded-[100%] bg-denim-300 md:w-[200vw]" />
|
||||
</div>
|
||||
<div className="relative z-10 mb-16">
|
||||
<Title className="mx-auto max-w-xs">{t("search.title")}</Title>
|
||||
</div>
|
||||
|
@ -50,20 +50,39 @@ module.exports = {
|
||||
defaultTheme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// meta data for the theme itself
|
||||
global: {
|
||||
accentA: "#505DBD",
|
||||
accentB: "#3440A1"
|
||||
},
|
||||
|
||||
// light bar
|
||||
lightBar: {
|
||||
light: "#2A2A71"
|
||||
},
|
||||
|
||||
// only used for body colors/textures
|
||||
background: {
|
||||
main: "#0A0A10",
|
||||
accentA: "#6E3B80",
|
||||
accentB: "#1F1F50"
|
||||
},
|
||||
global: {
|
||||
accentA: "#505DBD",
|
||||
accentB: "#3440A1"
|
||||
},
|
||||
|
||||
// typography
|
||||
type: {
|
||||
emphasis: "#FFFFFF",
|
||||
text: "#73739D",
|
||||
dimmed: "#926CAD",
|
||||
divider: "#353549"
|
||||
divider: "#262632"
|
||||
},
|
||||
|
||||
// search bar
|
||||
search: {
|
||||
background: "#1E1E33",
|
||||
focused: "#24243C",
|
||||
placeholder: "#4A4A71",
|
||||
icon: "#545476",
|
||||
text: "#FFFFFF"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user