mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
Merge pull request #511 from movie-web/even-more-v4-stuff
Even more v4 stuff
This commit is contained in:
commit
dec658b049
27 changed files with 193 additions and 94 deletions
|
@ -6,7 +6,7 @@
|
|||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.7.0",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@movie-web/providers": "^1.1.2",
|
||||
"@movie-web/providers": "^1.1.3",
|
||||
"@noble/hashes": "^1.3.2",
|
||||
"@react-spring/web": "^9.7.1",
|
||||
"@scure/bip39": "^1.2.1",
|
||||
|
|
|
@ -18,8 +18,8 @@ dependencies:
|
|||
specifier: ^1.5.0
|
||||
version: 1.7.17(react-dom@17.0.2)(react@17.0.2)
|
||||
'@movie-web/providers':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3
|
||||
'@noble/hashes':
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2
|
||||
|
@ -1889,8 +1889,8 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@movie-web/providers@1.1.2:
|
||||
resolution: {integrity: sha512-ZPSHBoz9WFLc6bWnRAXpefE+Vf8GNJ4xuWv5gu+uNg7dNBIMCnPqeuABlNIGxpEi68Go7zYlyx6nH/GQItgweA==}
|
||||
/@movie-web/providers@1.1.3:
|
||||
resolution: {integrity: sha512-6oxRqoZLVWQJHkJJaS1ZqDV7/LATYJ2EY0RKHhQUho3eFP5SpcdAvElllvvaRaomVFix8ftYYuy+NHWTbFox0g==}
|
||||
dependencies:
|
||||
cheerio: 1.0.0-rc.12
|
||||
crypto-js: 4.2.0
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
"auth": {
|
||||
"deviceNameLabel": "Device name",
|
||||
"deviceNamePlaceholder": "Personal phone",
|
||||
"hasAccount": "Already have an account? <0>Login here.</0>",
|
||||
"createAccount": "Dont have an account yet? <0>Create an account.</0>",
|
||||
"register": {
|
||||
"information": {
|
||||
"title": "Account information",
|
||||
|
@ -218,9 +220,18 @@
|
|||
"stopEditing": "Stop editing"
|
||||
},
|
||||
"titles": {
|
||||
"morning": ["Morning title"],
|
||||
"day": ["Day title"],
|
||||
"night": ["Night title"]
|
||||
"morning": {
|
||||
"default": "What would you like to watch this morning?",
|
||||
"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": {
|
||||
"loading": "Loading...",
|
||||
|
@ -369,15 +380,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"title": "About us",
|
||||
"about": {
|
||||
"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": {
|
||||
"title": "1",
|
||||
"body": "Body of 1"
|
||||
"title": "Where does the content come from?",
|
||||
"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": {
|
||||
"title": "1",
|
||||
"body": "Body of 1"
|
||||
"q2": {
|
||||
"title": "Where can I request a show or movie?",
|
||||
"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": {
|
||||
|
|
|
@ -53,17 +53,6 @@
|
|||
"reloadPage": "Reload the page",
|
||||
"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": {
|
||||
"legal": {
|
||||
"disclaimer": "Disclaimer",
|
||||
|
@ -104,17 +93,6 @@
|
|||
"noResults": "We couldn't find anythin', arrr!",
|
||||
"placeholder": "What do ye want to watch?",
|
||||
"sectionTitle": "Searchin' results"
|
||||
},
|
||||
"titles": {
|
||||
"day": [
|
||||
"Day title"
|
||||
],
|
||||
"morning": [
|
||||
"Morning title"
|
||||
],
|
||||
"night": [
|
||||
"Night title"
|
||||
]
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface ProgressInput {
|
|||
episodeId?: string;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export function progressUpdateItemToInput(
|
||||
|
@ -60,6 +61,7 @@ export function progressMediaItemToInputs(
|
|||
seasonId: episode.seasonId,
|
||||
episodeNumber: episode.number,
|
||||
seasonNumber: item.seasons[episode.seasonId].number,
|
||||
updatedAt: new Date(episode.updatedAt).toISOString(),
|
||||
}));
|
||||
}
|
||||
return [
|
||||
|
@ -67,6 +69,7 @@ export function progressMediaItemToInputs(
|
|||
duration: item.progress?.duration ?? 0,
|
||||
watched: item.progress?.watched ?? 0,
|
||||
tmdbId,
|
||||
updatedAt: new Date(item.updatedAt).toISOString(),
|
||||
meta: {
|
||||
title: item.title ?? "",
|
||||
type: item.type ?? "",
|
||||
|
|
|
@ -139,12 +139,9 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
|||
<DropdownLink href="/settings" icon={Icons.SETTINGS}>
|
||||
{t("navigation.menu.settings")}
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/faq" icon={Icons.EPISODES}>
|
||||
<DropdownLink href="/about" icon={Icons.EPISODES}>
|
||||
{t("navigation.menu.about")}
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/faq" icon={Icons.FILM}>
|
||||
{t("navigation.menu.support")}
|
||||
</DropdownLink>
|
||||
{deviceName ? (
|
||||
<DropdownLink
|
||||
className="!text-type-danger opacity-75 hover:opacity-100"
|
||||
|
|
|
@ -3,11 +3,11 @@ import { memo } from "react";
|
|||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
||||
export enum UserIcons {
|
||||
SEARCH = "search",
|
||||
BOOKMARK = "bookmark",
|
||||
CLOCK = "clock",
|
||||
EYE_SLASH = "eyeSlash",
|
||||
USER = "user",
|
||||
USER_GROUP = "userGroup",
|
||||
COUCH = "couch",
|
||||
MOBILE = "mobile",
|
||||
TICKET = "ticket",
|
||||
HANDCUFFS = "handcuffs",
|
||||
}
|
||||
|
||||
export interface UserIconProps {
|
||||
|
@ -16,16 +16,16 @@ export interface UserIconProps {
|
|||
}
|
||||
|
||||
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>`,
|
||||
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>`,
|
||||
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>`,
|
||||
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>`,
|
||||
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>`,
|
||||
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>`,
|
||||
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>`,
|
||||
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>`,
|
||||
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>`,
|
||||
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) => {
|
||||
const icon = iconList[props.icon];
|
||||
if (!icon) return <Icon icon={Icons.X} />;
|
||||
if (!icon) return <Icon className={props.className} icon={Icons.X} />;
|
||||
return (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{ __html: icon }} // eslint-disable-line react/no-danger
|
||||
|
|
|
@ -2,7 +2,7 @@ import classNames from "classnames";
|
|||
|
||||
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 function ColorPicker(props: {
|
||||
|
|
|
@ -3,11 +3,11 @@ import classNames from "classnames";
|
|||
import { UserIcon, UserIcons } from "../UserIcon";
|
||||
|
||||
const icons = [
|
||||
UserIcons.USER,
|
||||
UserIcons.BOOKMARK,
|
||||
UserIcons.CLOCK,
|
||||
UserIcons.EYE_SLASH,
|
||||
UserIcons.SEARCH,
|
||||
UserIcons.USER_GROUP,
|
||||
UserIcons.COUCH,
|
||||
UserIcons.MOBILE,
|
||||
UserIcons.TICKET,
|
||||
UserIcons.HANDCUFFS,
|
||||
];
|
||||
export const initialIcon = icons[0];
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import classNames from "classnames";
|
||||
|
||||
export function LargeCard(props: {
|
||||
children: 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 (
|
||||
<div className="flex justify-center mt-12">
|
||||
<div className="mx-auto inline-grid grid-cols-1 gap-3 justify-center items-center">
|
||||
<div className="mt-12">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,9 @@ import { useSubtitleStore } from "@/stores/subtitles";
|
|||
export function useCaptions() {
|
||||
const setLanguage = useSubtitleStore((s) => s.setLanguage);
|
||||
const enabled = useSubtitleStore((s) => s.enabled);
|
||||
const resetSubtitleSpecificSettings = useSubtitleStore(
|
||||
(s) => s.resetSubtitleSpecificSettings
|
||||
);
|
||||
const setCaption = usePlayerStore((s) => s.setCaption);
|
||||
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
|
||||
const captionList = usePlayerStore((s) => s.captionList);
|
||||
|
@ -21,9 +24,10 @@ export function useCaptions() {
|
|||
srtData,
|
||||
url: caption.url,
|
||||
});
|
||||
resetSubtitleSpecificSettings();
|
||||
setLanguage(language);
|
||||
},
|
||||
[setLanguage, captionList, setCaption]
|
||||
[setLanguage, captionList, setCaption, resetSubtitleSpecificSettings]
|
||||
);
|
||||
|
||||
const disable = useCallback(async () => {
|
||||
|
|
23
src/components/text/Link.tsx
Normal file
23
src/components/text/Link.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -8,7 +8,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
|
|||
<li
|
||||
className={classNames(
|
||||
"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">
|
||||
|
@ -17,7 +17,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
|
|||
</div>
|
||||
{i !== props.items.length - 1 ? (
|
||||
<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={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to bottom, transparent 5px, #1F1F29 5px, #1F1F29 10px)",
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// 10% chance of getting a joke title
|
||||
const shouldGiveJokeTitle = () => Math.floor(Math.random() * 10) === 0;
|
||||
|
||||
export function useRandomTranslation() {
|
||||
const { t } = useTranslation();
|
||||
const shouldJoke = useMemo(() => shouldGiveJokeTitle(), []);
|
||||
const seed = useMemo(() => Math.random(), []);
|
||||
|
||||
const getRandomTranslation = useCallback(
|
||||
(key: string) => {
|
||||
const res = t(key, { returnObjects: true });
|
||||
(key: string): string => {
|
||||
const defaultTitle = t(`${key}.default`) ?? "";
|
||||
if (!shouldJoke) return defaultTitle;
|
||||
|
||||
if (Array.isArray(res)) {
|
||||
return res[Math.floor(seed * res.length)];
|
||||
const keys = t(`${key}.extra`, { returnObjects: true });
|
||||
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 };
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import "@/setup/pwa";
|
||||
import "core-js/stable";
|
||||
import "./stores/__old/imports";
|
||||
import "@/setup/ga";
|
||||
|
@ -10,7 +11,6 @@ import { HelmetProvider } from "react-helmet-async";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||
import { useAsync } from "react-use";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
@ -40,9 +40,6 @@ if (key) {
|
|||
(window as any).initMW(conf().PROXY_URLS, key);
|
||||
}
|
||||
initializeChromecast();
|
||||
registerSW({
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
function LoadingScreen(props: { type: "user" | "lazy" }) {
|
||||
const mapping = {
|
||||
|
|
|
@ -22,14 +22,22 @@ export function AboutPage() {
|
|||
<SubPageLayout>
|
||||
<PageTitle subpage k="global.pages.about" />
|
||||
<ThinContainer>
|
||||
<Heading1>{t("faq.title")}</Heading1>
|
||||
<Heading1>{t("about.title")}</Heading1>
|
||||
<Paragraph>{t("about.description")}</Paragraph>
|
||||
<Heading2>{t("about.faqTitle")}</Heading2>
|
||||
<Ol
|
||||
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>
|
||||
</SubPageLayout>
|
||||
);
|
||||
|
|
|
@ -2,8 +2,8 @@ import { useCallback, useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||
import { IconPicker } from "@/components/form/IconPicker";
|
||||
import { ColorPicker, initialColor } from "@/components/form/ColorPicker";
|
||||
import { IconPicker, initialIcon } from "@/components/form/IconPicker";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import {
|
||||
LargeCard,
|
||||
|
@ -28,9 +28,9 @@ interface AccountCreatePartProps {
|
|||
|
||||
export function AccountCreatePart(props: AccountCreatePartProps) {
|
||||
const [device, setDevice] = useState("");
|
||||
const [colorA, setColorA] = useState("#2E65CF");
|
||||
const [colorB, setColorB] = useState("#2E65CF");
|
||||
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
|
||||
const [colorA, setColorA] = useState(initialColor);
|
||||
const [colorB, setColorB] = useState(initialColor);
|
||||
const [userIcon, setUserIcon] = useState<UserIcons>(initialIcon);
|
||||
const { t } = useTranslation();
|
||||
const [hasDeviceError, setHasDeviceError] = useState(false);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
||||
|
@ -10,6 +10,7 @@ import {
|
|||
LargeCardButtons,
|
||||
LargeCardText,
|
||||
} from "@/components/layout/LargeCard";
|
||||
import { MwLink } from "@/components/text/Link";
|
||||
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
|
||||
import { useAuth } from "@/hooks/auth/useAuth";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
|
@ -88,6 +89,11 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||
{t("auth.login.submit")}
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
<p className="text-center mt-6">
|
||||
<Trans i18nKey="auth.createAccount">
|
||||
<MwLink to="/register">.</MwLink>
|
||||
</Trans>
|
||||
</p>
|
||||
</LargeCard>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
LargeCardText,
|
||||
} from "@/components/layout/LargeCard";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import { MwLink } from "@/components/text/Link";
|
||||
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
||||
import { conf } from "@/setup/config";
|
||||
|
||||
|
@ -60,16 +61,21 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
|
|||
{cardContent}
|
||||
</div>
|
||||
<LargeCardButtons>
|
||||
<Button theme="secondary" onClick={() => history.push("/")}>
|
||||
{t("auth.trust.no")}
|
||||
</Button>
|
||||
<Button
|
||||
theme="purple"
|
||||
onClick={() => result.value && props.onNext?.(result.value)}
|
||||
>
|
||||
{t("auth.trust.yes")}
|
||||
</Button>
|
||||
<Button theme="secondary" onClick={() => history.push("/")}>
|
||||
{t("auth.trust.no")}
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
<p className="text-center mt-6">
|
||||
<Trans i18nKey="auth.hasAccount">
|
||||
<MwLink to="/login">.</MwLink>
|
||||
</Trans>
|
||||
</p>
|
||||
</LargeCard>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Sticky from "react-sticky-el";
|
||||
|
||||
import { SearchBarInput } from "@/components/form/SearchBar";
|
||||
|
@ -15,7 +16,8 @@ export interface HeroPartProps {
|
|||
}
|
||||
|
||||
export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
||||
const { t } = useRandomTranslation();
|
||||
const { t: randomT } = useRandomTranslation();
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch, setSearchUnFocus] = searchParams;
|
||||
const [, setShowBg] = useState(false);
|
||||
const bannerSize = useBannerSize();
|
||||
|
@ -32,7 +34,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||
if (hour < 12) time = "morning";
|
||||
else if (hour < 19) time = "day";
|
||||
|
||||
const title = t(`home.titles.${time}`);
|
||||
const title = randomT(`home.titles.${time}`);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useSlashFocus(inputRef);
|
||||
|
@ -41,7 +43,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||
<ThinContainer>
|
||||
<div className="mt-44 space-y-16 text-center">
|
||||
<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 className="relative h-20 z-30">
|
||||
<Sticky
|
||||
|
@ -56,7 +58,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||
onChange={setSearch}
|
||||
value={search}
|
||||
onUnFocus={setSearchUnFocus}
|
||||
placeholder={t("home.search.placeholder")}
|
||||
placeholder={t("home.search.placeholder") ?? ""}
|
||||
/>
|
||||
</Sticky>
|
||||
</div>
|
||||
|
|
|
@ -102,9 +102,12 @@ export function PlayerPart(props: PlayerPartProps) {
|
|||
<Player.Pip />
|
||||
<Player.Airplay />
|
||||
<Player.Chromecast />
|
||||
<Player.Settings />
|
||||
</>
|
||||
) : null}
|
||||
{status === playerStatus.PLAYBACK_ERROR ||
|
||||
status === playerStatus.PLAYING ? (
|
||||
<Player.Settings />
|
||||
) : null}
|
||||
<Player.Fullscreen />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -92,7 +92,7 @@ function App() {
|
|||
<Route exact path={["/browse/:query?", "/"]} component={HomePage} />
|
||||
<Route exact path="/register" component={RegisterPage} />
|
||||
<Route exact path="/login" component={LoginPage} />
|
||||
<Route exact path="/faq" component={AboutPage} />
|
||||
<Route exact path="/about" component={AboutPage} />
|
||||
|
||||
{shouldHaveDmcaPage() ? (
|
||||
<Route exact path="/dmca" component={DmcaPage} />
|
||||
|
|
27
src/setup/pwa.ts
Normal file
27
src/setup/pwa.ts
Normal 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);
|
||||
},
|
||||
});
|
|
@ -34,6 +34,7 @@ export interface SubtitleStore {
|
|||
setOverrideCasing(enabled: boolean): void;
|
||||
setDelay(delay: number): void;
|
||||
importSubtitleLanguage(lang: string | null): void;
|
||||
resetSubtitleSpecificSettings(): void;
|
||||
}
|
||||
|
||||
export const useSubtitleStore = create(
|
||||
|
@ -51,6 +52,12 @@ export const useSubtitleStore = create(
|
|||
backgroundOpacity: 0.5,
|
||||
size: 1,
|
||||
},
|
||||
resetSubtitleSpecificSettings() {
|
||||
set((s) => {
|
||||
s.delay = 0;
|
||||
s.overrideCasing = false;
|
||||
});
|
||||
},
|
||||
updateStyling(newStyling) {
|
||||
set((s) => {
|
||||
if (newStyling.backgroundOpacity !== undefined)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import fscreen from "fscreen";
|
||||
import Hls from "hls.js";
|
||||
|
||||
export const isSafari = /^((?!chrome|android).)*safari/i.test(
|
||||
navigator.userAgent
|
||||
|
@ -48,5 +49,6 @@ export function canWebkitPictureInPicture(): boolean {
|
|||
}
|
||||
|
||||
export function canPlayHlsNatively(video: HTMLVideoElement): boolean {
|
||||
if (Hls.isSupported()) return false; // no need to play natively
|
||||
return !!video.canPlayType("application/vnd.apple.mpegurl");
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ export const defaultTheme = {
|
|||
secondary: "#64647B",
|
||||
danger: "#F46E6E",
|
||||
link: "#A87FD1",
|
||||
linkHover: "#A87FD1",
|
||||
linkHover: "#ba8fe6",
|
||||
},
|
||||
|
||||
// search bar
|
||||
|
|
|
@ -42,6 +42,7 @@ export default defineConfig(({ mode }) => {
|
|||
disable: process.env.VITE_PWA_ENABLED !== "yes",
|
||||
registerType: "autoUpdate",
|
||||
workbox: {
|
||||
maximumFileSizeToCacheInBytes: 4000000, // 4mb
|
||||
globIgnores: ["**ping.txt**"]
|
||||
},
|
||||
includeAssets: [
|
||||
|
|
Loading…
Reference in a new issue