-
-
-
-
-
-
-
- {props.children}
-
-
-
{
- setShowModal(true);
- }}
- />
-
-
-
-
-
-
+ <>
+
- setShowModal(false)} />
-
+
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+
{
+ setShowModal(true);
+ }}
+ />
+
+
+
+
+
+
+
+
+
setShowModal(false)} />
+
+ >
);
}
diff --git a/src/components/text-inputs/TextInputControl.tsx b/src/components/text-inputs/TextInputControl.tsx
index a6d18994..c3b42616 100644
--- a/src/components/text-inputs/TextInputControl.tsx
+++ b/src/components/text-inputs/TextInputControl.tsx
@@ -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 = (
onChange && onChange(e.target.value)}
value={value}
onBlur={() => onUnFocus && onUnFocus()}
+ onFocus={() => onFocus?.()}
/>
);
diff --git a/src/components/utils/Flare.css b/src/components/utils/Flare.css
new file mode 100644
index 00000000..1c17cda8
--- /dev/null
+++ b/src/components/utils/Flare.css
@@ -0,0 +1,7 @@
+.flare-enabled .flare-light {
+ opacity: 1 !important;
+}
+
+.hover\:flare-enabled:hover .flare-light {
+ opacity: 1 !important;
+}
diff --git a/src/components/utils/Flare.tsx b/src/components/utils/Flare.tsx
index 680c3812..86ea0bfe 100644
--- a/src/components/utils/Flare.tsx
+++ b/src/components/utils/Flare.tsx
@@ -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
{props.children}
;
+}
+
+function Child(props: { className?: string; children?: ReactNode }) {
+ return
{props.children}
;
+}
+
+function Light(props: FlareProps) {
const outerRef = useRef
(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) {
);
}
+
+export const Flare = {
+ Base,
+ Light,
+ Child,
+};
diff --git a/src/components/utils/Lightbar.css b/src/components/utils/Lightbar.css
new file mode 100644
index 00000000..9cf845d4
--- /dev/null
+++ b/src/components/utils/Lightbar.css
@@ -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;
+}
diff --git a/src/components/utils/Lightbar.tsx b/src/components/utils/Lightbar.tsx
new file mode 100644
index 00000000..604f7cba
--- /dev/null
+++ b/src/components/utils/Lightbar.tsx
@@ -0,0 +1,9 @@
+import "./Lightbar.css";
+
+export function Lightbar(props: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/src/setup/App.tsx b/src/setup/App.tsx
index 6bb6b957..b3a49685 100644
--- a/src/setup/App.tsx
+++ b/src/setup/App.tsx
@@ -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() {
{/* functional routes */}
-
diff --git a/src/setup/index.css b/src/setup/index.css
index 259aaa61..168ce2ea 100644
--- a/src/setup/index.css
+++ b/src/setup/index.css
@@ -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],
@@ -198,4 +199,4 @@ input[type=range].styled-slider.slider-progress::-ms-fill-lower {
::-webkit-scrollbar {
/* For some reason the styles don't get applied without the width */
width: 13px;
-}
\ No newline at end of file
+}
diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json
index f78adc46..e11e67de 100644
--- a/src/setup/locales/en/translation.json
+++ b/src/setup/locales/en/translation.json
@@ -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",
diff --git a/src/views/HomePage.tsx b/src/views/HomePage.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/views/SearchPart.tsx b/src/views/SearchPart.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/views/other/v2Migration.tsx b/src/views/other/v2Migration.tsx
deleted file mode 100644
index d0b05e42..00000000
--- a/src/views/other/v2Migration.tsx
+++ /dev/null
@@ -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 ;
-}
-
-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;
-}
diff --git a/src/views/parts/home/BookmarksPart.tsx b/src/views/parts/home/BookmarksPart.tsx
new file mode 100644
index 00000000..7c8dfeb6
--- /dev/null
+++ b/src/views/parts/home/BookmarksPart.tsx
@@ -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();
+ 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 (
+
+
+
+
+
+ {bookmarksSorted.map((v) => (
+ setItemBookmark(v, false)}
+ />
+ ))}
+
+
+ );
+}
diff --git a/src/views/parts/home/WatchingPart.tsx b/src/views/parts/home/WatchingPart.tsx
new file mode 100644
index 00000000..c9281dd5
--- /dev/null
+++ b/src/views/parts/home/WatchingPart.tsx
@@ -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();
+
+ const bookmarks = getFilteredBookmarks();
+ const watchedItems = getFilteredWatched().filter(
+ (v) => !getIfBookmarkedFromPortable(bookmarks, v.item.meta)
+ );
+
+ if (watchedItems.length === 0) return null;
+
+ return (
+
+
+
+
+
+ {watchedItems.map((v) => (
+ removeProgress(v.item.meta.id)}
+ />
+ ))}
+
+
+ );
+}
diff --git a/src/views/search/HomeView.tsx b/src/views/search/HomeView.tsx
index 42868cc6..5c80eff1 100644
--- a/src/views/search/HomeView.tsx
+++ b/src/views/search/HomeView.tsx
@@ -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 (
-
-
-
-
-
-
- {t("v3.newDomain")}
-
-
-
-
-
- {t("v3.newSiteTitle")}
-
-
-
-
-
-
-
-
{t("v3.tireless")}
-
-
-
-
-
-
- );
-}
-
export function HomeView() {
return (
-
-
diff --git a/src/views/search/SearchView.tsx b/src/views/search/SearchView.tsx
index 01aec28a..17df6663 100644
--- a/src/views/search/SearchView.tsx
+++ b/src/views/search/SearchView.tsx
@@ -27,16 +27,13 @@ export function SearchView() {
return (
+
{t("global.name")}
-
-
{t("search.title")}
diff --git a/tailwind.config.js b/tailwind.config.js
index 0733f489..7fd185a2 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -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"
}
}
}