19 changed files with 359 additions and 325 deletions
@ -0,0 +1,152 @@
@@ -0,0 +1,152 @@
|
||||
import classNames from "classnames"; |
||||
import { useCallback, useEffect, useState } from "react"; |
||||
import { useHistory } from "react-router-dom"; |
||||
|
||||
import { UserAvatar } from "@/components/Avatar"; |
||||
import { Icon, Icons } from "@/components/Icon"; |
||||
import { Transition } from "@/components/Transition"; |
||||
import { useAuth } from "@/hooks/auth/useAuth"; |
||||
import { conf } from "@/setup/config"; |
||||
import { useAuthStore } from "@/stores/auth"; |
||||
|
||||
function Divider() { |
||||
return <hr className="border-0 w-full h-px bg-dropdown-border" />; |
||||
} |
||||
|
||||
function GoToLink(props: { |
||||
children: React.ReactNode; |
||||
href?: string; |
||||
className?: string; |
||||
onClick?: () => void; |
||||
}) { |
||||
const history = useHistory(); |
||||
|
||||
const goTo = (href: string) => { |
||||
if (href.startsWith("http")) window.open(href, "_blank"); |
||||
else history.push(href); |
||||
}; |
||||
|
||||
return ( |
||||
<a |
||||
href={props.href} |
||||
onClick={(evt) => { |
||||
evt.preventDefault(); |
||||
if (props.href) goTo(props.href); |
||||
else props.onClick?.(); |
||||
}} |
||||
className={props.className} |
||||
> |
||||
{props.children} |
||||
</a> |
||||
); |
||||
} |
||||
|
||||
function DropdownLink(props: { |
||||
children: React.ReactNode; |
||||
href?: string; |
||||
icon?: Icons; |
||||
highlight?: boolean; |
||||
className?: string; |
||||
onClick?: () => void; |
||||
}) { |
||||
return ( |
||||
<GoToLink |
||||
onClick={props.onClick} |
||||
href={props.href} |
||||
className={classNames( |
||||
"cursor-pointer flex gap-3 items-center m-4 font-medium transition-colors duration-100", |
||||
props.highlight |
||||
? "text-dropdown-highlight hover:text-dropdown-highlightHover" |
||||
: "text-dropdown-text hover:text-white", |
||||
props.className |
||||
)} |
||||
> |
||||
{props.icon ? <Icon icon={props.icon} className="text-xl" /> : null} |
||||
{props.children} |
||||
</GoToLink> |
||||
); |
||||
} |
||||
|
||||
function CircleDropdownLink(props: { icon: Icons; href: string }) { |
||||
return ( |
||||
<GoToLink |
||||
href={props.href} |
||||
className="w-11 h-11 rounded-full bg-dropdown-contentBackground text-dropdown-text hover:text-white transition-colors duration-100 flex justify-center items-center" |
||||
> |
||||
<Icon className="text-2xl" icon={props.icon} /> |
||||
</GoToLink> |
||||
); |
||||
} |
||||
|
||||
export function LinksDropdown(props: { children: React.ReactNode }) { |
||||
const [open, setOpen] = useState(false); |
||||
const userId = useAuthStore((s) => s.account?.userId); |
||||
const { logout } = useAuth(); |
||||
|
||||
useEffect(() => { |
||||
function onWindowClick(evt: MouseEvent) { |
||||
if ((evt.target as HTMLElement).closest(".is-dropdown")) return; |
||||
setOpen(false); |
||||
} |
||||
|
||||
window.addEventListener("click", onWindowClick); |
||||
return () => window.removeEventListener("click", onWindowClick); |
||||
}, []); |
||||
|
||||
const toggleOpen = useCallback(() => { |
||||
setOpen((s) => !s); |
||||
}, []); |
||||
|
||||
return ( |
||||
<div className="relative is-dropdown"> |
||||
<div className="cursor-pointer" onClick={toggleOpen}> |
||||
{props.children} |
||||
</div> |
||||
<Transition animation="slide-down" show={open}> |
||||
<div className="rounded-lg absolute w-64 bg-dropdown-altBackground top-full mt-3 right-0"> |
||||
{userId ? ( |
||||
<DropdownLink className="text-white" href="/settings"> |
||||
<UserAvatar /> |
||||
{userId} |
||||
</DropdownLink> |
||||
) : ( |
||||
<DropdownLink href="/login" icon={Icons.RISING_STAR} highlight> |
||||
Sync to cloud |
||||
</DropdownLink> |
||||
)} |
||||
<Divider /> |
||||
<DropdownLink href="/settings" icon={Icons.SETTINGS}> |
||||
Settings |
||||
</DropdownLink> |
||||
<DropdownLink href="/faq" icon={Icons.EPISODES}> |
||||
About us |
||||
</DropdownLink> |
||||
<DropdownLink href="/faq" icon={Icons.FILM}> |
||||
HELP MEEE |
||||
</DropdownLink> |
||||
{userId ? ( |
||||
<DropdownLink |
||||
className="!text-type-danger opacity-75 hover:opacity-100" |
||||
icon={Icons.LOGOUT} |
||||
onClick={logout} |
||||
> |
||||
Log out |
||||
</DropdownLink> |
||||
) : null} |
||||
<Divider /> |
||||
<div className="my-4 flex justify-center items-center gap-4"> |
||||
<CircleDropdownLink |
||||
href={conf().DISCORD_LINK} |
||||
icon={Icons.DISCORD} |
||||
/> |
||||
<CircleDropdownLink href={conf().GITHUB_LINK} icon={Icons.GITHUB} /> |
||||
<CircleDropdownLink |
||||
href={conf().DONATION_LINK} |
||||
icon={Icons.COINS} |
||||
/> |
||||
</div> |
||||
</div> |
||||
</Transition> |
||||
</div> |
||||
); |
||||
} |
@ -1,95 +1,51 @@
@@ -1,95 +1,51 @@
|
||||
import { useCallback } from "react"; |
||||
|
||||
import { |
||||
SubtitleSearchItem, |
||||
downloadSrt, |
||||
searchSubtitles, |
||||
} from "@/backend/helpers/subs"; |
||||
import { downloadCaption } from "@/backend/helpers/subs"; |
||||
import { usePlayerStore } from "@/stores/player/store"; |
||||
import { useSubtitleStore } from "@/stores/subtitles"; |
||||
import { SimpleCache } from "@/utils/cache"; |
||||
|
||||
const cacheTimeSec = 24 * 60 * 60; // 24 hours
|
||||
|
||||
const downloadCache = new SimpleCache<string, string>(); |
||||
downloadCache.setCompare((a, b) => a === b); |
||||
|
||||
const searchCache = new SimpleCache< |
||||
{ tmdbId: string; ep?: string; season?: string }, |
||||
SubtitleSearchItem[] |
||||
>(); |
||||
searchCache.setCompare( |
||||
(a, b) => a.tmdbId === b.tmdbId && a.ep === b.ep && a.season === b.season |
||||
); |
||||
|
||||
export function useCaptions() { |
||||
const setLanguage = useSubtitleStore((s) => s.setLanguage); |
||||
const enabled = useSubtitleStore((s) => s.enabled); |
||||
const setCaption = usePlayerStore((s) => s.setCaption); |
||||
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage); |
||||
const meta = usePlayerStore((s) => s.meta); |
||||
const captionList = usePlayerStore((s) => s.captionList); |
||||
|
||||
const download = useCallback( |
||||
async (subtitleId: string, language: string) => { |
||||
let srtData = downloadCache.get(subtitleId); |
||||
if (!srtData) { |
||||
srtData = await downloadSrt(subtitleId); |
||||
downloadCache.set(subtitleId, srtData, cacheTimeSec); |
||||
} |
||||
const selectLanguage = useCallback( |
||||
async (language: string) => { |
||||
const caption = captionList.find((v) => v.language === language); |
||||
if (!caption) return; |
||||
const srtData = await downloadCaption(caption); |
||||
setCaption({ |
||||
language, |
||||
language: caption.language, |
||||
srtData, |
||||
url: "", // TODO remove url
|
||||
url: caption.url, |
||||
}); |
||||
setLanguage(language); |
||||
}, |
||||
[setCaption, setLanguage] |
||||
[setLanguage, captionList, setCaption] |
||||
); |
||||
|
||||
const search = useCallback(async () => { |
||||
if (!meta) throw new Error("No meta"); |
||||
const key = { |
||||
tmdbId: meta.tmdbId, |
||||
ep: meta.episode?.tmdbId, |
||||
season: meta.season?.tmdbId, |
||||
}; |
||||
const results = searchCache.get(key); |
||||
if (results) return [...results]; |
||||
|
||||
const freshResults = await searchSubtitles(meta); |
||||
searchCache.set(key, [...freshResults], cacheTimeSec); |
||||
return freshResults; |
||||
}, [meta]); |
||||
|
||||
const disable = useCallback(async () => { |
||||
setCaption(null); |
||||
setLanguage(null); |
||||
}, [setCaption, setLanguage]); |
||||
|
||||
const downloadLastUsed = useCallback(async () => { |
||||
const selectLastUsedLanguage = useCallback(async () => { |
||||
const language = lastSelectedLanguage ?? "en"; |
||||
const searchResult = await search(); |
||||
const languageResult = searchResult.find( |
||||
(v) => v.attributes.language === language |
||||
); |
||||
if (!languageResult) return false; |
||||
await download( |
||||
languageResult.attributes.legacy_subtitle_id, |
||||
languageResult.attributes.language |
||||
); |
||||
await selectLanguage(language); |
||||
return true; |
||||
}, [lastSelectedLanguage, search, download]); |
||||
}, [lastSelectedLanguage, selectLanguage]); |
||||
|
||||
const toggleLastUsed = useCallback(async () => { |
||||
if (enabled) disable(); |
||||
else await downloadLastUsed(); |
||||
}, [downloadLastUsed, disable, enabled]); |
||||
else await selectLastUsedLanguage(); |
||||
}, [selectLastUsedLanguage, disable, enabled]); |
||||
|
||||
return { |
||||
download, |
||||
search, |
||||
selectLanguage, |
||||
disable, |
||||
downloadLastUsed, |
||||
selectLastUsedLanguage, |
||||
toggleLastUsed, |
||||
}; |
||||
} |
||||
|
Loading…
Reference in new issue