1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

Merge pull request #511 from movie-web/even-more-v4-stuff

Even more v4 stuff
This commit is contained in:
mrjvs 2023-12-07 02:03:38 +01:00 committed by GitHub
commit dec658b049
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 193 additions and 94 deletions

View file

@ -6,7 +6,7 @@
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.7.0", "@formkit/auto-animate": "^0.7.0",
"@headlessui/react": "^1.5.0", "@headlessui/react": "^1.5.0",
"@movie-web/providers": "^1.1.2", "@movie-web/providers": "^1.1.3",
"@noble/hashes": "^1.3.2", "@noble/hashes": "^1.3.2",
"@react-spring/web": "^9.7.1", "@react-spring/web": "^9.7.1",
"@scure/bip39": "^1.2.1", "@scure/bip39": "^1.2.1",

View file

@ -18,8 +18,8 @@ dependencies:
specifier: ^1.5.0 specifier: ^1.5.0
version: 1.7.17(react-dom@17.0.2)(react@17.0.2) version: 1.7.17(react-dom@17.0.2)(react@17.0.2)
'@movie-web/providers': '@movie-web/providers':
specifier: ^1.1.2 specifier: ^1.1.3
version: 1.1.2 version: 1.1.3
'@noble/hashes': '@noble/hashes':
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2 version: 1.3.2
@ -1889,8 +1889,8 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
dev: true dev: true
/@movie-web/providers@1.1.2: /@movie-web/providers@1.1.3:
resolution: {integrity: sha512-ZPSHBoz9WFLc6bWnRAXpefE+Vf8GNJ4xuWv5gu+uNg7dNBIMCnPqeuABlNIGxpEi68Go7zYlyx6nH/GQItgweA==} resolution: {integrity: sha512-6oxRqoZLVWQJHkJJaS1ZqDV7/LATYJ2EY0RKHhQUho3eFP5SpcdAvElllvvaRaomVFix8ftYYuy+NHWTbFox0g==}
dependencies: dependencies:
cheerio: 1.0.0-rc.12 cheerio: 1.0.0-rc.12
crypto-js: 4.2.0 crypto-js: 4.2.0

View file

@ -2,6 +2,8 @@
"auth": { "auth": {
"deviceNameLabel": "Device name", "deviceNameLabel": "Device name",
"deviceNamePlaceholder": "Personal phone", "deviceNamePlaceholder": "Personal phone",
"hasAccount": "Already have an account? <0>Login here.</0>",
"createAccount": "Dont have an account yet? <0>Create an account.</0>",
"register": { "register": {
"information": { "information": {
"title": "Account information", "title": "Account information",
@ -218,9 +220,18 @@
"stopEditing": "Stop editing" "stopEditing": "Stop editing"
}, },
"titles": { "titles": {
"morning": ["Morning title"], "morning": {
"day": ["Day title"], "default": "What would you like to watch this morning?",
"night": ["Night title"] "extra": ["I hear Before Sunrise is good"]
},
"day": {
"default": "What would you like to watch this afternoon?",
"extra": []
},
"night": {
"default": "What would you like to watch tonight?",
"extra": ["Tired? I hear The Excorcist is good."]
}
}, },
"search": { "search": {
"loading": "Loading...", "loading": "Loading...",
@ -369,15 +380,21 @@
} }
} }
}, },
"faq": { "about": {
"title": "About us", "title": "About movie-web",
"description": "movie-web is a web application that searches the internet for streams. The team aims for a mostly minimalistic approach to consuming content.",
"faqTitle": "Common questions",
"q1": { "q1": {
"title": "1", "title": "Where does the content come from?",
"body": "Body of 1" "body": "movie-web does not host any content. When you click on something to watch, the internet is searched for the selected media (On the loading screen and in the 'video sources' tab you can see which source you're using). Media never gets uploaded by movie-web, everything is through this searching mechanism."
}, },
"how": { "q2": {
"title": "1", "title": "Where can I request a show or movie?",
"body": "Body of 1" "body": "It's not possible to request a show or movie, movie-web does not manage any content. All content is viewed through sources on the internet."
},
"q3": {
"title": "The search results display the show or movie, why can't I play it?",
"body": "Our search results are powered by The Movie Database (TMDB) and display regardless of whether our sources actually have the content."
} }
}, },
"footer": { "footer": {

View file

@ -53,17 +53,6 @@
"reloadPage": "Reload the page", "reloadPage": "Reload the page",
"title": "That be an error, Captain" "title": "That be an error, Captain"
}, },
"faq": {
"how": {
"body": "Body of 1",
"title": "1"
},
"q1": {
"body": "Body of 1",
"title": "1"
},
"title": "About us"
},
"footer": { "footer": {
"legal": { "legal": {
"disclaimer": "Disclaimer", "disclaimer": "Disclaimer",
@ -104,17 +93,6 @@
"noResults": "We couldn't find anythin', arrr!", "noResults": "We couldn't find anythin', arrr!",
"placeholder": "What do ye want to watch?", "placeholder": "What do ye want to watch?",
"sectionTitle": "Searchin' results" "sectionTitle": "Searchin' results"
},
"titles": {
"day": [
"Day title"
],
"morning": [
"Morning title"
],
"night": [
"Night title"
]
} }
}, },
"media": { "media": {

View file

@ -19,6 +19,7 @@ export interface ProgressInput {
episodeId?: string; episodeId?: string;
seasonNumber?: number; seasonNumber?: number;
episodeNumber?: number; episodeNumber?: number;
updatedAt?: string;
} }
export function progressUpdateItemToInput( export function progressUpdateItemToInput(
@ -60,6 +61,7 @@ export function progressMediaItemToInputs(
seasonId: episode.seasonId, seasonId: episode.seasonId,
episodeNumber: episode.number, episodeNumber: episode.number,
seasonNumber: item.seasons[episode.seasonId].number, seasonNumber: item.seasons[episode.seasonId].number,
updatedAt: new Date(episode.updatedAt).toISOString(),
})); }));
} }
return [ return [
@ -67,6 +69,7 @@ export function progressMediaItemToInputs(
duration: item.progress?.duration ?? 0, duration: item.progress?.duration ?? 0,
watched: item.progress?.watched ?? 0, watched: item.progress?.watched ?? 0,
tmdbId, tmdbId,
updatedAt: new Date(item.updatedAt).toISOString(),
meta: { meta: {
title: item.title ?? "", title: item.title ?? "",
type: item.type ?? "", type: item.type ?? "",

View file

@ -139,12 +139,9 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
<DropdownLink href="/settings" icon={Icons.SETTINGS}> <DropdownLink href="/settings" icon={Icons.SETTINGS}>
{t("navigation.menu.settings")} {t("navigation.menu.settings")}
</DropdownLink> </DropdownLink>
<DropdownLink href="/faq" icon={Icons.EPISODES}> <DropdownLink href="/about" icon={Icons.EPISODES}>
{t("navigation.menu.about")} {t("navigation.menu.about")}
</DropdownLink> </DropdownLink>
<DropdownLink href="/faq" icon={Icons.FILM}>
{t("navigation.menu.support")}
</DropdownLink>
{deviceName ? ( {deviceName ? (
<DropdownLink <DropdownLink
className="!text-type-danger opacity-75 hover:opacity-100" className="!text-type-danger opacity-75 hover:opacity-100"

View file

@ -3,11 +3,11 @@ import { memo } from "react";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
export enum UserIcons { export enum UserIcons {
SEARCH = "search", USER_GROUP = "userGroup",
BOOKMARK = "bookmark", COUCH = "couch",
CLOCK = "clock", MOBILE = "mobile",
EYE_SLASH = "eyeSlash", TICKET = "ticket",
USER = "user", HANDCUFFS = "handcuffs",
} }
export interface UserIconProps { export interface UserIconProps {
@ -16,16 +16,16 @@ export interface UserIconProps {
} }
const iconList: Record<UserIcons, string> = { const iconList: Record<UserIcons, string> = {
search: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z"/></svg>`, userGroup: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3zM609.3 512H471.4c5.4-9.4 8.6-20.3 8.6-32v-8c0-60.7-27.1-115.2-69.8-151.8c2.4-.1 4.7-.2 7.1-.2h61.4C567.8 320 640 392.2 640 481.3c0 17-13.8 30.7-30.7 30.7zM432 256c-31 0-59-12.6-79.3-32.9C372.4 196.5 384 163.6 384 128c0-26.8-6.6-52.1-18.3-74.3C384.3 40.1 407.2 32 432 32c61.9 0 112 50.1 112 112s-50.1 112-112 112z"/></svg>`,
bookmark: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M384 48V512l-192-112L0 512V48C0 21.5 21.5 0 48 0h288C362.5 0 384 21.5 384 48z"/></svg>`, couch: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M64 160C64 89.3 121.3 32 192 32H448c70.7 0 128 57.3 128 128v33.6c-36.5 7.4-64 39.7-64 78.4v48H128V272c0-38.7-27.5-71-64-78.4V160zM544 272c0-20.9 13.4-38.7 32-45.3c5-1.8 10.4-2.7 16-2.7c26.5 0 48 21.5 48 48V448c0 17.7-14.3 32-32 32H576c-17.7 0-32-14.3-32-32H96c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32V272c0-26.5 21.5-48 48-48c5.6 0 11 1 16 2.7c18.6 6.6 32 24.4 32 45.3v48 32h32H512h32V320 272z"/></svg>`,
clock: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512zM232 256C232 264 236 271.5 242.7 275.1L338.7 339.1C349.7 347.3 364.6 344.3 371.1 333.3C379.3 322.3 376.3 307.4 365.3 300L280 243.2V120C280 106.7 269.3 96 255.1 96C242.7 96 231.1 106.7 231.1 120L232 256z"/></svg>`, mobile: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M16 64C16 28.7 44.7 0 80 0H304c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H80c-35.3 0-64-28.7-64-64V64zM144 448c0 8.8 7.2 16 16 16h64c8.8 0 16-7.2 16-16s-7.2-16-16-16H160c-8.8 0-16 7.2-16 16zM304 64H80V384H304V64z"/></svg>`,
eyeSlash: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="currentColor" d="M150.7 92.77C195 58.27 251.8 32 320 32C400.8 32 465.5 68.84 512.6 112.6C559.4 156 590.7 207.1 605.5 243.7C608.8 251.6 608.8 260.4 605.5 268.3C592.1 300.6 565.2 346.1 525.6 386.7L630.8 469.1C641.2 477.3 643.1 492.4 634.9 502.8C626.7 513.2 611.6 515.1 601.2 506.9L9.196 42.89C-1.236 34.71-3.065 19.63 5.112 9.196C13.29-1.236 28.37-3.065 38.81 5.112L150.7 92.77zM223.1 149.5L313.4 220.3C317.6 211.8 320 202.2 320 191.1C320 180.5 316.1 169.7 311.6 160.4C314.4 160.1 317.2 159.1 320 159.1C373 159.1 416 202.1 416 255.1C416 269.7 413.1 282.7 407.1 294.5L446.6 324.7C457.7 304.3 464 280.9 464 255.1C464 176.5 399.5 111.1 320 111.1C282.7 111.1 248.6 126.2 223.1 149.5zM320 480C239.2 480 174.5 443.2 127.4 399.4C80.62 355.1 49.34 304 34.46 268.3C31.18 260.4 31.18 251.6 34.46 243.7C44 220.8 60.29 191.2 83.09 161.5L177.4 235.8C176.5 242.4 176 249.1 176 255.1C176 335.5 240.5 400 320 400C338.7 400 356.6 396.4 373 389.9L446.2 447.5C409.9 467.1 367.8 480 320 480H320z"/></svg>`, ticket: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M64 64C28.7 64 0 92.7 0 128v64c0 8.8 7.4 15.7 15.7 18.6C34.5 217.1 48 235 48 256s-13.5 38.9-32.3 45.4C7.4 304.3 0 311.2 0 320v64c0 35.3 28.7 64 64 64H512c35.3 0 64-28.7 64-64V320c0-8.8-7.4-15.7-15.7-18.6C541.5 294.9 528 277 528 256s13.5-38.9 32.3-45.4c8.3-2.9 15.7-9.8 15.7-18.6V128c0-35.3-28.7-64-64-64H64zm64 112l0 160c0 8.8 7.2 16 16 16H432c8.8 0 16-7.2 16-16V176c0-8.8-7.2-16-16-16H144c-8.8 0-16 7.2-16 16zM96 160c0-17.7 14.3-32 32-32H448c17.7 0 32 14.3 32 32V352c0 17.7-14.3 32-32 32H128c-17.7 0-32-14.3-32-32V160z"/></svg>`,
user: `<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-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`, handcuffs: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M240 32a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM192 48a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm-32 80c17.7 0 32 14.3 32 32h8c13.3 0 24 10.7 24 24v16c0 1.7-.2 3.4-.5 5.1C280.3 229.6 320 286.2 320 352c0 88.4-71.6 160-160 160S0 440.4 0 352c0-65.8 39.7-122.4 96.5-146.9c-.4-1.6-.5-3.3-.5-5.1V184c0-13.3 10.7-24 24-24h8c0-17.7 14.3-32 32-32zm0 320a96 96 0 1 0 0-192 96 96 0 1 0 0 192zm192-96c0-25.9-5.1-50.5-14.4-73.1c16.9-32.9 44.8-59.1 78.9-73.9c-.4-1.6-.5-3.3-.5-5.1V184c0-13.3 10.7-24 24-24h8c0-17.7 14.3-32 32-32s32 14.3 32 32h8c13.3 0 24 10.7 24 24v16c0 1.7-.2 3.4-.5 5.1C600.3 229.6 640 286.2 640 352c0 88.4-71.6 160-160 160c-62 0-115.8-35.3-142.4-86.9c9.3-22.5 14.4-47.2 14.4-73.1zm224 0a96 96 0 1 0 -192 0 96 96 0 1 0 192 0zM368 0a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm80 48a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>`,
}; };
export const UserIcon = memo((props: UserIconProps) => { export const UserIcon = memo((props: UserIconProps) => {
const icon = iconList[props.icon]; const icon = iconList[props.icon];
if (!icon) return <Icon icon={Icons.X} />; if (!icon) return <Icon className={props.className} icon={Icons.X} />;
return ( return (
<span <span
dangerouslySetInnerHTML={{ __html: icon }} // eslint-disable-line react/no-danger dangerouslySetInnerHTML={{ __html: icon }} // eslint-disable-line react/no-danger

View file

@ -2,7 +2,7 @@ import classNames from "classnames";
import { Icon, Icons } from "../Icon"; import { Icon, Icons } from "../Icon";
const colors = ["#2E65CF", "#7652DD", "#CF2E68", "#C2CF2E", "#2ECFA8"]; const colors = ["#0A54FF", "#CF2E68", "#F9DD7F", "#7652DD", "#2ECFA8"];
export const initialColor = colors[0]; export const initialColor = colors[0];
export function ColorPicker(props: { export function ColorPicker(props: {

View file

@ -3,11 +3,11 @@ import classNames from "classnames";
import { UserIcon, UserIcons } from "../UserIcon"; import { UserIcon, UserIcons } from "../UserIcon";
const icons = [ const icons = [
UserIcons.USER, UserIcons.USER_GROUP,
UserIcons.BOOKMARK, UserIcons.COUCH,
UserIcons.CLOCK, UserIcons.MOBILE,
UserIcons.EYE_SLASH, UserIcons.TICKET,
UserIcons.SEARCH, UserIcons.HANDCUFFS,
]; ];
export const initialIcon = icons[0]; export const initialIcon = icons[0];

View file

@ -1,3 +1,5 @@
import classNames from "classnames";
export function LargeCard(props: { export function LargeCard(props: {
children: React.ReactNode; children: React.ReactNode;
top?: React.ReactNode; top?: React.ReactNode;
@ -36,10 +38,19 @@ export function LargeCardText(props: {
); );
} }
export function LargeCardButtons(props: { children: React.ReactNode }) { export function LargeCardButtons(props: {
children: React.ReactNode;
splitAlign?: boolean;
}) {
return ( return (
<div className="flex justify-center mt-12"> <div className="mt-12">
<div className="mx-auto inline-grid grid-cols-1 gap-3 justify-center items-center"> <div
className={classNames("mx-auto", {
"flex flex-row-reverse justify-between items-center":
props.splitAlign,
"flex max-w-xs flex-col-reverse gap-3": !props.splitAlign,
})}
>
{props.children} {props.children}
</div> </div>
</div> </div>

View file

@ -7,6 +7,9 @@ import { useSubtitleStore } from "@/stores/subtitles";
export function useCaptions() { export function useCaptions() {
const setLanguage = useSubtitleStore((s) => s.setLanguage); const setLanguage = useSubtitleStore((s) => s.setLanguage);
const enabled = useSubtitleStore((s) => s.enabled); const enabled = useSubtitleStore((s) => s.enabled);
const resetSubtitleSpecificSettings = useSubtitleStore(
(s) => s.resetSubtitleSpecificSettings
);
const setCaption = usePlayerStore((s) => s.setCaption); const setCaption = usePlayerStore((s) => s.setCaption);
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage); const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const captionList = usePlayerStore((s) => s.captionList); const captionList = usePlayerStore((s) => s.captionList);
@ -21,9 +24,10 @@ export function useCaptions() {
srtData, srtData,
url: caption.url, url: caption.url,
}); });
resetSubtitleSpecificSettings();
setLanguage(language); setLanguage(language);
}, },
[setLanguage, captionList, setCaption] [setLanguage, captionList, setCaption, resetSubtitleSpecificSettings]
); );
const disable = useCallback(async () => { const disable = useCallback(async () => {

View file

@ -0,0 +1,23 @@
import { ReactNode } from "react";
import { Link as LinkRouter } from "react-router-dom";
export function MwLink(props: {
children?: ReactNode;
to?: string;
url?: string;
onClick?: () => void;
}) {
const isExternal = !!props.url;
const isInternal = !!props.to;
const content = (
<span className="group mt-1 cursor-pointer font-bold text-type-link hover:text-type-linkHover active:scale-95">
{props.children}
</span>
);
if (isExternal) return <a href={props.url}>{content}</a>;
if (isInternal) return <LinkRouter to={props.to ?? ""}>{content}</LinkRouter>;
return (
<span onClick={() => props.onClick && props.onClick()}>{content}</span>
);
}

View file

@ -8,7 +8,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
<li <li
className={classNames( className={classNames(
"grid grid-cols-[auto,1fr] gap-6", "grid grid-cols-[auto,1fr] gap-6",
i !== props.items.length - 1 ? "pb-6" : undefined i !== props.items.length - 1 ? "pb-12" : undefined
)} )}
> >
<div className="relative z-0"> <div className="relative z-0">
@ -17,7 +17,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
</div> </div>
{i !== props.items.length - 1 ? ( {i !== props.items.length - 1 ? (
<div <div
className="h-full w-px absolute top-6 left-1/2 transform -translate-x-1/2" className="h-[calc(100%+1.5rem)] w-px absolute top-6 left-1/2 transform -translate-x-1/2"
style={{ style={{
backgroundImage: backgroundImage:
"linear-gradient(to bottom, transparent 5px, #1F1F29 5px, #1F1F29 10px)", "linear-gradient(to bottom, transparent 5px, #1F1F29 5px, #1F1F29 10px)",

View file

@ -1,21 +1,28 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
// 10% chance of getting a joke title
const shouldGiveJokeTitle = () => Math.floor(Math.random() * 10) === 0;
export function useRandomTranslation() { export function useRandomTranslation() {
const { t } = useTranslation(); const { t } = useTranslation();
const shouldJoke = useMemo(() => shouldGiveJokeTitle(), []);
const seed = useMemo(() => Math.random(), []); const seed = useMemo(() => Math.random(), []);
const getRandomTranslation = useCallback( const getRandomTranslation = useCallback(
(key: string) => { (key: string): string => {
const res = t(key, { returnObjects: true }); const defaultTitle = t(`${key}.default`) ?? "";
if (!shouldJoke) return defaultTitle;
if (Array.isArray(res)) { const keys = t(`${key}.extra`, { returnObjects: true });
return res[Math.floor(seed * res.length)]; if (Array.isArray(keys)) {
if (keys.length === 0) return defaultTitle;
return keys[Math.floor(seed * keys.length)];
} }
return res; return typeof keys === "string" ? keys : defaultTitle;
}, },
[t, seed] [t, seed, shouldJoke]
); );
return { t: getRandomTranslation }; return { t: getRandomTranslation };

View file

@ -1,3 +1,4 @@
import "@/setup/pwa";
import "core-js/stable"; import "core-js/stable";
import "./stores/__old/imports"; import "./stores/__old/imports";
import "@/setup/ga"; import "@/setup/ga";
@ -10,7 +11,6 @@ import { HelmetProvider } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BrowserRouter, HashRouter } from "react-router-dom"; import { BrowserRouter, HashRouter } from "react-router-dom";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
import { registerSW } from "virtual:pwa-register";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
@ -40,9 +40,6 @@ if (key) {
(window as any).initMW(conf().PROXY_URLS, key); (window as any).initMW(conf().PROXY_URLS, key);
} }
initializeChromecast(); initializeChromecast();
registerSW({
immediate: true,
});
function LoadingScreen(props: { type: "user" | "lazy" }) { function LoadingScreen(props: { type: "user" | "lazy" }) {
const mapping = { const mapping = {

View file

@ -22,14 +22,22 @@ export function AboutPage() {
<SubPageLayout> <SubPageLayout>
<PageTitle subpage k="global.pages.about" /> <PageTitle subpage k="global.pages.about" />
<ThinContainer> <ThinContainer>
<Heading1>{t("faq.title")}</Heading1> <Heading1>{t("about.title")}</Heading1>
<Paragraph>{t("about.description")}</Paragraph>
<Heading2>{t("about.faqTitle")}</Heading2>
<Ol <Ol
items={[ items={[
<Question title={t("faq.q1.title")}>{t("faq.q1.body")}</Question>, <Question title={t("about.q1.title")}>
{t("about.q1.body")}
</Question>,
<Question title={t("about.q2.title")}>
{t("about.q2.body")}
</Question>,
<Question title={t("about.q3.title")}>
{t("about.q3.body")}
</Question>,
]} ]}
/> />
<Heading2>{t("faq.how.title")}</Heading2>
<Paragraph>{t("faq.how.body")}</Paragraph>
</ThinContainer> </ThinContainer>
</SubPageLayout> </SubPageLayout>
); );

View file

@ -2,8 +2,8 @@ import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { ColorPicker } from "@/components/form/ColorPicker"; import { ColorPicker, initialColor } from "@/components/form/ColorPicker";
import { IconPicker } from "@/components/form/IconPicker"; import { IconPicker, initialIcon } from "@/components/form/IconPicker";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { import {
LargeCard, LargeCard,
@ -28,9 +28,9 @@ interface AccountCreatePartProps {
export function AccountCreatePart(props: AccountCreatePartProps) { export function AccountCreatePart(props: AccountCreatePartProps) {
const [device, setDevice] = useState(""); const [device, setDevice] = useState("");
const [colorA, setColorA] = useState("#2E65CF"); const [colorA, setColorA] = useState(initialColor);
const [colorB, setColorB] = useState("#2E65CF"); const [colorB, setColorB] = useState(initialColor);
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER); const [userIcon, setUserIcon] = useState<UserIcons>(initialIcon);
const { t } = useTranslation(); const { t } = useTranslation();
const [hasDeviceError, setHasDeviceError] = useState(false); const [hasDeviceError, setHasDeviceError] = useState(false);

View file

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { verifyValidMnemonic } from "@/backend/accounts/crypto"; import { verifyValidMnemonic } from "@/backend/accounts/crypto";
@ -10,6 +10,7 @@ import {
LargeCardButtons, LargeCardButtons,
LargeCardText, LargeCardText,
} from "@/components/layout/LargeCard"; } from "@/components/layout/LargeCard";
import { MwLink } from "@/components/text/Link";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { useAuth } from "@/hooks/auth/useAuth"; import { useAuth } from "@/hooks/auth/useAuth";
import { useBookmarkStore } from "@/stores/bookmarks"; import { useBookmarkStore } from "@/stores/bookmarks";
@ -88,6 +89,11 @@ export function LoginFormPart(props: LoginFormPartProps) {
{t("auth.login.submit")} {t("auth.login.submit")}
</Button> </Button>
</LargeCardButtons> </LargeCardButtons>
<p className="text-center mt-6">
<Trans i18nKey="auth.createAccount">
<MwLink to="/register">.</MwLink>
</Trans>
</p>
</LargeCard> </LargeCard>
); );
} }

View file

@ -12,6 +12,7 @@ import {
LargeCardText, LargeCardText,
} from "@/components/layout/LargeCard"; } from "@/components/layout/LargeCard";
import { Loading } from "@/components/layout/Loading"; import { Loading } from "@/components/layout/Loading";
import { MwLink } from "@/components/text/Link";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl"; import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
@ -60,16 +61,21 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
{cardContent} {cardContent}
</div> </div>
<LargeCardButtons> <LargeCardButtons>
<Button theme="secondary" onClick={() => history.push("/")}>
{t("auth.trust.no")}
</Button>
<Button <Button
theme="purple" theme="purple"
onClick={() => result.value && props.onNext?.(result.value)} onClick={() => result.value && props.onNext?.(result.value)}
> >
{t("auth.trust.yes")} {t("auth.trust.yes")}
</Button> </Button>
<Button theme="secondary" onClick={() => history.push("/")}>
{t("auth.trust.no")}
</Button>
</LargeCardButtons> </LargeCardButtons>
<p className="text-center mt-6">
<Trans i18nKey="auth.hasAccount">
<MwLink to="/login">.</MwLink>
</Trans>
</p>
</LargeCard> </LargeCard>
); );
} }

View file

@ -1,4 +1,5 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Sticky from "react-sticky-el"; import Sticky from "react-sticky-el";
import { SearchBarInput } from "@/components/form/SearchBar"; import { SearchBarInput } from "@/components/form/SearchBar";
@ -15,7 +16,8 @@ export interface HeroPartProps {
} }
export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
const { t } = useRandomTranslation(); const { t: randomT } = useRandomTranslation();
const { t } = useTranslation();
const [search, setSearch, setSearchUnFocus] = searchParams; const [search, setSearch, setSearchUnFocus] = searchParams;
const [, setShowBg] = useState(false); const [, setShowBg] = useState(false);
const bannerSize = useBannerSize(); const bannerSize = useBannerSize();
@ -32,7 +34,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
if (hour < 12) time = "morning"; if (hour < 12) time = "morning";
else if (hour < 19) time = "day"; else if (hour < 19) time = "day";
const title = t(`home.titles.${time}`); const title = randomT(`home.titles.${time}`);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useSlashFocus(inputRef); useSlashFocus(inputRef);
@ -41,7 +43,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
<ThinContainer> <ThinContainer>
<div className="mt-44 space-y-16 text-center"> <div className="mt-44 space-y-16 text-center">
<div className="relative z-10 mb-16"> <div className="relative z-10 mb-16">
<HeroTitle className="mx-auto max-w-xs">{title}</HeroTitle> <HeroTitle className="mx-auto max-w-md">{title}</HeroTitle>
</div> </div>
<div className="relative h-20 z-30"> <div className="relative h-20 z-30">
<Sticky <Sticky
@ -56,7 +58,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
onChange={setSearch} onChange={setSearch}
value={search} value={search}
onUnFocus={setSearchUnFocus} onUnFocus={setSearchUnFocus}
placeholder={t("home.search.placeholder")} placeholder={t("home.search.placeholder") ?? ""}
/> />
</Sticky> </Sticky>
</div> </div>

View file

@ -102,9 +102,12 @@ export function PlayerPart(props: PlayerPartProps) {
<Player.Pip /> <Player.Pip />
<Player.Airplay /> <Player.Airplay />
<Player.Chromecast /> <Player.Chromecast />
<Player.Settings />
</> </>
) : null} ) : null}
{status === playerStatus.PLAYBACK_ERROR ||
status === playerStatus.PLAYING ? (
<Player.Settings />
) : null}
<Player.Fullscreen /> <Player.Fullscreen />
</div> </div>
</div> </div>

View file

@ -92,7 +92,7 @@ function App() {
<Route exact path={["/browse/:query?", "/"]} component={HomePage} /> <Route exact path={["/browse/:query?", "/"]} component={HomePage} />
<Route exact path="/register" component={RegisterPage} /> <Route exact path="/register" component={RegisterPage} />
<Route exact path="/login" component={LoginPage} /> <Route exact path="/login" component={LoginPage} />
<Route exact path="/faq" component={AboutPage} /> <Route exact path="/about" component={AboutPage} />
{shouldHaveDmcaPage() ? ( {shouldHaveDmcaPage() ? (
<Route exact path="/dmca" component={DmcaPage} /> <Route exact path="/dmca" component={DmcaPage} />

27
src/setup/pwa.ts Normal file
View file

@ -0,0 +1,27 @@
import { registerSW } from "virtual:pwa-register";
const intervalMS = 60 * 60 * 1000;
registerSW({
immediate: true,
onRegisteredSW(swUrl, r) {
if (!r) return;
setInterval(async () => {
if (!(!r.installing && navigator)) return;
if ("connection" in navigator && !navigator.onLine) return;
const resp = await fetch(swUrl, {
cache: "no-store",
headers: {
cache: "no-store",
"cache-control": "no-cache",
},
});
if (resp?.status === 200) {
await r.update();
}
}, intervalMS);
},
});

View file

@ -34,6 +34,7 @@ export interface SubtitleStore {
setOverrideCasing(enabled: boolean): void; setOverrideCasing(enabled: boolean): void;
setDelay(delay: number): void; setDelay(delay: number): void;
importSubtitleLanguage(lang: string | null): void; importSubtitleLanguage(lang: string | null): void;
resetSubtitleSpecificSettings(): void;
} }
export const useSubtitleStore = create( export const useSubtitleStore = create(
@ -51,6 +52,12 @@ export const useSubtitleStore = create(
backgroundOpacity: 0.5, backgroundOpacity: 0.5,
size: 1, size: 1,
}, },
resetSubtitleSpecificSettings() {
set((s) => {
s.delay = 0;
s.overrideCasing = false;
});
},
updateStyling(newStyling) { updateStyling(newStyling) {
set((s) => { set((s) => {
if (newStyling.backgroundOpacity !== undefined) if (newStyling.backgroundOpacity !== undefined)

View file

@ -1,4 +1,5 @@
import fscreen from "fscreen"; import fscreen from "fscreen";
import Hls from "hls.js";
export const isSafari = /^((?!chrome|android).)*safari/i.test( export const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent navigator.userAgent
@ -48,5 +49,6 @@ export function canWebkitPictureInPicture(): boolean {
} }
export function canPlayHlsNatively(video: HTMLVideoElement): boolean { export function canPlayHlsNatively(video: HTMLVideoElement): boolean {
if (Hls.isSupported()) return false; // no need to play natively
return !!video.canPlayType("application/vnd.apple.mpegurl"); return !!video.canPlayType("application/vnd.apple.mpegurl");
} }

View file

@ -63,7 +63,7 @@ export const defaultTheme = {
secondary: "#64647B", secondary: "#64647B",
danger: "#F46E6E", danger: "#F46E6E",
link: "#A87FD1", link: "#A87FD1",
linkHover: "#A87FD1", linkHover: "#ba8fe6",
}, },
// search bar // search bar

View file

@ -42,6 +42,7 @@ export default defineConfig(({ mode }) => {
disable: process.env.VITE_PWA_ENABLED !== "yes", disable: process.env.VITE_PWA_ENABLED !== "yes",
registerType: "autoUpdate", registerType: "autoUpdate",
workbox: { workbox: {
maximumFileSizeToCacheInBytes: 4000000, // 4mb
globIgnores: ["**ping.txt**"] globIgnores: ["**ping.txt**"]
}, },
includeAssets: [ includeAssets: [