19 changed files with 324 additions and 312 deletions
@ -0,0 +1,7 @@ |
|||||||
|
.flare-enabled .flare-light { |
||||||
|
opacity: 1 !important; |
||||||
|
} |
||||||
|
|
||||||
|
.hover\:flare-enabled:hover .flare-light { |
||||||
|
opacity: 1 !important; |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
import "./Lightbar.css"; |
||||||
|
|
||||||
|
export function Lightbar(props: { className?: string }) { |
||||||
|
return ( |
||||||
|
<div className={props.className}> |
||||||
|
<div className="lightbar" /> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -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; |
|
||||||
} |
|
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue