mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 00:46:07 +00:00
correct languages + settings page styling
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
1cbf9f3c45
commit
9ff603f87c
|
@ -8,6 +8,7 @@
|
||||||
"@headlessui/react": "^1.5.0",
|
"@headlessui/react": "^1.5.0",
|
||||||
"@movie-web/providers": "^1.0.2",
|
"@movie-web/providers": "^1.0.2",
|
||||||
"@react-spring/web": "^9.7.1",
|
"@react-spring/web": "^9.7.1",
|
||||||
|
"@sozialhelden/ietf-language-tags": "^5.4.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"core-js": "^3.29.1",
|
"core-js": "^3.29.1",
|
||||||
"dompurify": "^3.0.1",
|
"dompurify": "^3.0.1",
|
||||||
|
|
|
@ -17,6 +17,9 @@ dependencies:
|
||||||
'@react-spring/web':
|
'@react-spring/web':
|
||||||
specifier: ^9.7.1
|
specifier: ^9.7.1
|
||||||
version: 9.7.3(react-dom@17.0.2)(react@17.0.2)
|
version: 9.7.3(react-dom@17.0.2)(react@17.0.2)
|
||||||
|
'@sozialhelden/ietf-language-tags':
|
||||||
|
specifier: ^5.4.2
|
||||||
|
version: 5.4.2
|
||||||
classnames:
|
classnames:
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
|
@ -1956,6 +1959,13 @@ packages:
|
||||||
rollup: 2.79.1
|
rollup: 2.79.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@sozialhelden/ietf-language-tags@5.4.2:
|
||||||
|
resolution: {integrity: sha512-aCN7bVOfX9sBN0EHyWJT14H8bx+VYBo8tdcynai35wgoxKMfVtgEECkQ1gs8nEL6GHGes8lPIfo6AjIch44N3w==}
|
||||||
|
dependencies:
|
||||||
|
lodash.compact: 3.0.1
|
||||||
|
typescript: 4.9.5
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@surma/rollup-plugin-off-main-thread@2.2.3:
|
/@surma/rollup-plugin-off-main-thread@2.2.3:
|
||||||
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -4456,6 +4466,10 @@ packages:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.compact@3.0.1:
|
||||||
|
resolution: {integrity: sha512-2ozeiPi+5eBXW1CLtzjk8XQFhQOEMwwfxblqeq6EGyTxZJ1bPATqilY0e6g2SLQpP4KuMeuioBhEnWz5Pr7ICQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.debounce@4.0.8:
|
/lodash.debounce@4.0.8:
|
||||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -5986,7 +6000,6 @@ packages:
|
||||||
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
||||||
engines: {node: '>=4.2.0'}
|
engines: {node: '>=4.2.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ufo@1.3.0:
|
/ufo@1.3.0:
|
||||||
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}
|
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}
|
||||||
|
|
|
@ -95,10 +95,6 @@ export async function searchSubtitles(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function languageIdToName(langId: string): string | null {
|
|
||||||
return languageMap[langId]?.nativeName ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function downloadSrt(legacySubId: string): Promise<string> {
|
export async function downloadSrt(legacySubId: string): Promise<string> {
|
||||||
// TODO there is cloudflare protection so this may not always work. what to do about that?
|
// TODO there is cloudflare protection so this may not always work. what to do about that?
|
||||||
// TODO also there is ratelimit on the page itself
|
// TODO also there is ratelimit on the page itself
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface Props {
|
||||||
icon?: Icons;
|
icon?: Icons;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
theme?: "white" | "purple" | "secondary";
|
theme?: "white" | "purple" | "secondary" | "danger";
|
||||||
padding?: string;
|
padding?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
|
@ -26,6 +26,8 @@ export function Button(props: Props) {
|
||||||
if (props.theme === "secondary")
|
if (props.theme === "secondary")
|
||||||
colorClasses =
|
colorClasses =
|
||||||
"bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white";
|
"bg-video-buttons-cancel hover:bg-video-buttons-cancelHover transition-colors duration-100 text-white";
|
||||||
|
if (props.theme === "danger")
|
||||||
|
colorClasses = "bg-buttons-danger hover:bg-buttons-dangerHover text-white";
|
||||||
|
|
||||||
let classes = classNames(
|
let classes = classNames(
|
||||||
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
|
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import classNames from "classnames";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { IconPatch } from "@/components/buttons/IconPatch";
|
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { Lightbar } from "@/components/utils/Lightbar";
|
import { Lightbar } from "@/components/utils/Lightbar";
|
||||||
|
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { useBannerSize } from "@/stores/banner";
|
import { useBannerSize } from "@/stores/banner";
|
||||||
|
|
||||||
|
@ -13,10 +15,12 @@ export interface NavigationProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
bg?: boolean;
|
bg?: boolean;
|
||||||
noLightbar?: boolean;
|
noLightbar?: boolean;
|
||||||
|
doBackground?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Navigation(props: NavigationProps) {
|
export function Navigation(props: NavigationProps) {
|
||||||
const bannerHeight = useBannerSize();
|
const bannerHeight = useBannerSize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!props.noLightbar ? (
|
{!props.noLightbar ? (
|
||||||
|
@ -37,7 +41,17 @@ export function Navigation(props: NavigationProps) {
|
||||||
top: `${bannerHeight}px`,
|
top: `${bannerHeight}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="fixed left-0 right-0 flex items-center">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"fixed left-0 right-0 flex items-center",
|
||||||
|
props.doBackground
|
||||||
|
? "bg-background-main border-b border-utils-divider border-opacity-50 overflow-hidden"
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.doBackground ? (
|
||||||
|
<BlurEllipsis positionClass="absolute" />
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
props.bg ? "opacity-100" : "opacity-0"
|
props.bg ? "opacity-100" : "opacity-0"
|
||||||
|
|
|
@ -3,16 +3,13 @@ import { ReactNode, useRef, useState } from "react";
|
||||||
import { useAsync, useAsyncFn } from "react-use";
|
import { useAsync, useAsyncFn } from "react-use";
|
||||||
import { convert } from "subsrt-ts";
|
import { convert } from "subsrt-ts";
|
||||||
|
|
||||||
import {
|
import { SubtitleSearchItem, subtitleTypeList } from "@/backend/helpers/subs";
|
||||||
SubtitleSearchItem,
|
|
||||||
languageIdToName,
|
|
||||||
subtitleTypeList,
|
|
||||||
} from "@/backend/helpers/subs";
|
|
||||||
import { FlagIcon } from "@/components/FlagIcon";
|
import { FlagIcon } from "@/components/FlagIcon";
|
||||||
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
import { Input } from "@/components/player/internals/ContextMenu/Input";
|
||||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||||
|
import { getLanguageFromIETF } from "@/components/player/utils/language";
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { useSubtitleStore } from "@/stores/subtitles";
|
import { useSubtitleStore } from "@/stores/subtitles";
|
||||||
|
@ -35,6 +32,7 @@ export function CaptionOption(props: {
|
||||||
he: "il",
|
he: "il",
|
||||||
ze: "cn",
|
ze: "cn",
|
||||||
ar: "sa",
|
ar: "sa",
|
||||||
|
ja: "jp",
|
||||||
};
|
};
|
||||||
let countryCode =
|
let countryCode =
|
||||||
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
|
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
|
||||||
|
@ -155,7 +153,8 @@ export function CaptionsView({ id }: { id: string }) {
|
||||||
else if (req.error) content = <p>errored!</p>;
|
else if (req.error) content = <p>errored!</p>;
|
||||||
else if (req.value) {
|
else if (req.value) {
|
||||||
const subs = req.value.map((v) => {
|
const subs = req.value.map((v) => {
|
||||||
const languageName = languageIdToName(v.attributes.language) ?? "unknown";
|
const languageName =
|
||||||
|
getLanguageFromIETF(v.attributes.language) ?? "unknown";
|
||||||
return {
|
return {
|
||||||
...v,
|
...v,
|
||||||
languageName,
|
languageName,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { languageIdToName } from "@/backend/helpers/subs";
|
|
||||||
import { Toggle } from "@/components/buttons/Toggle";
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
import { useCaptions } from "@/components/player/hooks/useCaptions";
|
||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
|
import { getLanguageFromIETF } from "@/components/player/utils/language";
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { qualityToString } from "@/stores/player/utils/qualities";
|
import { qualityToString } from "@/stores/player/utils/qualities";
|
||||||
|
@ -26,7 +26,7 @@ export function SettingsMenu({ id }: { id: string }) {
|
||||||
const { toggleLastUsed } = useCaptions();
|
const { toggleLastUsed } = useCaptions();
|
||||||
|
|
||||||
const selectedLanguagePretty = selectedCaptionLanguage
|
const selectedLanguagePretty = selectedCaptionLanguage
|
||||||
? languageIdToName(selectedCaptionLanguage) ?? "unknown"
|
? getLanguageFromIETF(selectedCaptionLanguage) ?? "unknown"
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const source = usePlayerStore((s) => s.source);
|
const source = usePlayerStore((s) => s.source);
|
||||||
|
|
|
@ -68,13 +68,14 @@ export function CaptionCue({
|
||||||
export function SubtitleRenderer() {
|
export function SubtitleRenderer() {
|
||||||
const videoTime = usePlayerStore((s) => s.progress.time);
|
const videoTime = usePlayerStore((s) => s.progress.time);
|
||||||
const srtData = usePlayerStore((s) => s.caption.selected?.srtData);
|
const srtData = usePlayerStore((s) => s.caption.selected?.srtData);
|
||||||
|
const language = usePlayerStore((s) => s.caption.selected?.language);
|
||||||
const styling = useSubtitleStore((s) => s.styling);
|
const styling = useSubtitleStore((s) => s.styling);
|
||||||
const overrideCasing = useSubtitleStore((s) => s.overrideCasing);
|
const overrideCasing = useSubtitleStore((s) => s.overrideCasing);
|
||||||
const delay = useSubtitleStore((s) => s.delay);
|
const delay = useSubtitleStore((s) => s.delay);
|
||||||
|
|
||||||
const parsedCaptions = useMemo(
|
const parsedCaptions = useMemo(
|
||||||
() => (srtData ? parseSubtitles(srtData) : []),
|
() => (srtData ? parseSubtitles(srtData, language) : []),
|
||||||
[srtData]
|
[srtData, language]
|
||||||
);
|
);
|
||||||
|
|
||||||
const visibileCaptions = useMemo(
|
const visibileCaptions = useMemo(
|
||||||
|
|
|
@ -47,7 +47,10 @@ export function convertSubtitlesToSrt(text: string): string {
|
||||||
return srt;
|
return srt;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSubtitles(text: string): CaptionCueType[] {
|
export function parseSubtitles(
|
||||||
|
text: string,
|
||||||
|
_language?: string
|
||||||
|
): CaptionCueType[] {
|
||||||
const vtt = convertSubtitlesToVtt(text);
|
const vtt = convertSubtitlesToVtt(text);
|
||||||
return parse(vtt).filter((cue) => cue.type === "caption") as CaptionCueType[];
|
return parse(vtt).filter((cue) => cue.type === "caption") as CaptionCueType[];
|
||||||
}
|
}
|
||||||
|
|
14
src/components/player/utils/language.ts
Normal file
14
src/components/player/utils/language.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { getTag } from "@sozialhelden/ietf-language-tags";
|
||||||
|
|
||||||
|
export function getLanguageFromIETF(ietf: string): string | null {
|
||||||
|
const tag = getTag(ietf, true);
|
||||||
|
|
||||||
|
const lang = tag?.language?.Description?.[0] ?? null;
|
||||||
|
if (!lang) return null;
|
||||||
|
|
||||||
|
const region = tag?.region?.Description?.[0] ?? null;
|
||||||
|
let regionText = "";
|
||||||
|
if (region) regionText = ` (${region})`;
|
||||||
|
|
||||||
|
return `${lang}${regionText}`;
|
||||||
|
}
|
|
@ -75,4 +75,4 @@
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(180deg) translateZ(0px) translateY(400px) scaleX(1);
|
transform: rotate(180deg) translateZ(0px) translateY(400px) scaleX(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import classNames from "classnames";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import "./Lightbar.css";
|
import "./Lightbar.css";
|
||||||
|
|
||||||
|
@ -161,7 +162,12 @@ function ParticlesCanvas() {
|
||||||
|
|
||||||
export function Lightbar(props: { className?: string }) {
|
export function Lightbar(props: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className={props.className}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"grid grid-cols-[100%] w-full overflow-x-hidden",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="lightbar">
|
<div className="lightbar">
|
||||||
<ParticlesCanvas />
|
<ParticlesCanvas />
|
||||||
<div className="lightbar-visual" />
|
<div className="lightbar-visual" />
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
interface TextProps {
|
interface TextProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
border?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const borderClass = "pb-4 border-b border-utils-divider border-opacity-50";
|
||||||
|
|
||||||
export function Heading1(props: TextProps) {
|
export function Heading1(props: TextProps) {
|
||||||
return (
|
return (
|
||||||
<h1
|
<h1
|
||||||
className={[
|
className={[
|
||||||
"text-5xl font-bold text-white mb-9",
|
"text-5xl font-bold text-white mb-9",
|
||||||
|
props.border ? borderClass : null,
|
||||||
props.className ?? "",
|
props.className ?? "",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
|
@ -21,6 +25,21 @@ export function Heading2(props: TextProps) {
|
||||||
<h2
|
<h2
|
||||||
className={[
|
className={[
|
||||||
"text-3xl font-bold text-white mt-20 mb-9",
|
"text-3xl font-bold text-white mt-20 mb-9",
|
||||||
|
props.border ? borderClass : null,
|
||||||
|
props.className ?? "",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Heading3(props: TextProps) {
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
className={[
|
||||||
|
"text-xl font-bold text-white mb-3",
|
||||||
|
props.border ? borderClass : null,
|
||||||
props.className ?? "",
|
props.className ?? "",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
|
@ -34,6 +53,7 @@ export function Paragraph(props: TextProps) {
|
||||||
<p
|
<p
|
||||||
className={[
|
className={[
|
||||||
"text-type-text my-9 font-medium",
|
"text-type-text my-9 font-medium",
|
||||||
|
props.border ? borderClass : null,
|
||||||
props.className ?? "",
|
props.className ?? "",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import Sticky from "react-stickynode";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { WideContainer } from "@/components/layout/WideContainer";
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
import { Divider } from "@/components/utils/Divider";
|
import { Divider } from "@/components/utils/Divider";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1, Heading2, Heading3 } from "@/components/utils/Text";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||||
|
@ -19,27 +24,59 @@ function SidebarSection(props: { title: string; children: React.ReactNode }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarLink(props: { children: React.ReactNode; icon: Icons }) {
|
function SidebarLink(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
icon: Icons;
|
||||||
|
active?: boolean;
|
||||||
|
}) {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const goToPage = (link: string) => {
|
||||||
|
history.push(link);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-2 py-1 flex items-center space-x-3">
|
<a
|
||||||
|
onClick={() => goToPage("/settings")}
|
||||||
|
className={classNames(
|
||||||
|
"w-full px-3 py-2 flex items-center space-x-3 cursor-pointer rounded my-2",
|
||||||
|
props.active
|
||||||
|
? "bg-settings-sidebar-activeLink text-settings-sidebar-type-activated"
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className="text-2xl text-settings-sidebar-type-icon"
|
className={classNames(
|
||||||
|
"text-2xl text-settings-sidebar-type-icon",
|
||||||
|
props.active ? "text-settings-sidebar-type-iconActivated" : null
|
||||||
|
)}
|
||||||
icon={props.icon}
|
icon={props.icon}
|
||||||
/>
|
/>
|
||||||
<span>{props.children}</span>
|
<span>{props.children}</span>
|
||||||
</div>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsSidebar() {
|
function SettingsSidebar() {
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const hostname = location.hostname;
|
const hostname = location.hostname;
|
||||||
|
const rem = 16;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="sticky top-24 text-settings-sidebar-type-inactive">
|
<Sticky
|
||||||
|
enabled
|
||||||
|
top={10 * rem} // 10rem
|
||||||
|
className="text-settings-sidebar-type-inactive"
|
||||||
|
>
|
||||||
<SidebarSection title="Settings">
|
<SidebarSection title="Settings">
|
||||||
<SidebarLink icon={Icons.WAND}>Account</SidebarLink>
|
{/* I looked over at my bookshelf to come up with these links */}
|
||||||
|
<SidebarLink icon={Icons.WAND}>A war in my name!</SidebarLink>
|
||||||
|
<SidebarLink active icon={Icons.COMPRESS}>
|
||||||
|
TANSTAAFL
|
||||||
|
</SidebarLink>
|
||||||
|
<SidebarLink icon={Icons.AIRPLAY}>We all float down here</SidebarLink>
|
||||||
|
<SidebarLink icon={Icons.BOOKMARK}>My skin is not my own</SidebarLink>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
<Divider />
|
<Divider />
|
||||||
<SidebarSection title="App information">
|
<SidebarSection title="App information">
|
||||||
|
@ -52,7 +89,7 @@ function SettingsSidebar() {
|
||||||
<span className="text-right">{hostname}</span>
|
<span className="text-right">{hostname}</span>
|
||||||
</div>
|
</div>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</div>
|
</Sticky>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -62,17 +99,118 @@ function SettingsLayout(props: { children: React.ReactNode }) {
|
||||||
<WideContainer ultraWide>
|
<WideContainer ultraWide>
|
||||||
<div className="grid grid-cols-[260px,1fr] gap-12">
|
<div className="grid grid-cols-[260px,1fr] gap-12">
|
||||||
<SettingsSidebar />
|
<SettingsSidebar />
|
||||||
{props.children}
|
<div className="space-y-16">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
</WideContainer>
|
</WideContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SecondaryLabel(props: { children: React.ReactNode }) {
|
||||||
|
return <p className="text-type-text">{props.children}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
paddingClass?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"w-full rounded-lg bg-settings-card-background bg-opacity-[0.15] border border-settings-card-border",
|
||||||
|
props.paddingClass ?? "px-8 py-6",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AltCard(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
paddingClass?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"w-full rounded-lg bg-settings-card-altBackground bg-opacity-50",
|
||||||
|
props.paddingClass ?? "px-8 py-6",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountSection() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading1 border>Account</Heading1>
|
||||||
|
<Card>Beep beep</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DevicesSection() {
|
||||||
|
const devices = [
|
||||||
|
"Jip's iPhone",
|
||||||
|
"Muad'Dib's Nintendo Switch",
|
||||||
|
"Oppenheimer's old-ass phone",
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading2 border className="mt-0 mb-9">
|
||||||
|
Devices
|
||||||
|
</Heading2>
|
||||||
|
<div className="space-y-5">
|
||||||
|
{devices.map((deviceName) => (
|
||||||
|
<Card
|
||||||
|
className="flex justify-between items-center"
|
||||||
|
paddingClass="px-6 py-4"
|
||||||
|
key={deviceName}
|
||||||
|
>
|
||||||
|
<div className="font-medium">
|
||||||
|
<SecondaryLabel>Device name</SecondaryLabel>
|
||||||
|
<p className="text-white">{deviceName}</p>
|
||||||
|
</div>
|
||||||
|
<Button theme="danger">Remove</Button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActionsSection() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Heading2 border>Actions</Heading2>
|
||||||
|
<AltCard paddingClass="px-6 py-12" className="grid grid-cols-2 gap-12">
|
||||||
|
<div>
|
||||||
|
<Heading3>Delete account</Heading3>
|
||||||
|
<p className="text-type-text">
|
||||||
|
This action is irreversible. All data will be deleted and nothing
|
||||||
|
can be recovered.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end items-center">
|
||||||
|
<Button theme="danger">Delete account</Button>
|
||||||
|
</div>
|
||||||
|
</AltCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<Heading1>Setting</Heading1>
|
<AccountSection />
|
||||||
|
<DevicesSection />
|
||||||
|
<ActionsSection />
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { FooterView } from "@/components/layout/Footer";
|
import { FooterView } from "@/components/layout/Footer";
|
||||||
import { Navigation } from "@/components/layout/Navigation";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
|
|
||||||
export function BlurEllipsis() {
|
export function BlurEllipsis(props: { positionClass?: string }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Blur elipsis */}
|
{/* Blur elipsis */}
|
||||||
<div className="absolute top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25" />
|
<div
|
||||||
<div className="absolute top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25" />
|
className={classNames(
|
||||||
|
props.positionClass ?? "fixed",
|
||||||
|
"top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
props.positionClass ?? "fixed",
|
||||||
|
"top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +35,7 @@ export function SubPageLayout(props: { children: React.ReactNode }) {
|
||||||
<BlurEllipsis />
|
<BlurEllipsis />
|
||||||
{/* Main page */}
|
{/* Main page */}
|
||||||
<FooterView>
|
<FooterView>
|
||||||
<Navigation noLightbar />
|
<Navigation doBackground noLightbar />
|
||||||
<div className="mt-40">{props.children}</div>
|
<div className="mt-40">{props.children}</div>
|
||||||
</FooterView>
|
</FooterView>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -69,7 +69,9 @@ module.exports = {
|
||||||
// Buttons
|
// Buttons
|
||||||
buttons: {
|
buttons: {
|
||||||
toggle: "#8D44D6",
|
toggle: "#8D44D6",
|
||||||
toggleDisabled: "#202836"
|
toggleDisabled: "#202836",
|
||||||
|
danger: "#792131",
|
||||||
|
dangerHover: "#8a293b"
|
||||||
},
|
},
|
||||||
|
|
||||||
// only used for body colors/textures
|
// only used for body colors/textures
|
||||||
|
@ -111,12 +113,21 @@ module.exports = {
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
sidebar: {
|
sidebar: {
|
||||||
|
activeLink: "#171728",
|
||||||
|
|
||||||
type: {
|
type: {
|
||||||
secondary: "#4B395F",
|
secondary: "#4B395F",
|
||||||
inactive: "#8D68A9",
|
inactive: "#8D68A9",
|
||||||
icon: "#926CAD",
|
icon: "#926CAD",
|
||||||
|
iconActivated: "#6942A8",
|
||||||
activated: "#CBA1E8"
|
activated: "#CBA1E8"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
card: {
|
||||||
|
border: "#2A243E",
|
||||||
|
background: "#29243D",
|
||||||
|
altBackground: "#29243D"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,10 @@
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
|
"@sozialhelden/ietf-language-tags": [
|
||||||
|
"../node_modules/@sozialhelden/ietf-language-tags/dist/cjs"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"types": ["vite/client", "vite-plugin-pwa/vanillajs"]
|
"types": ["vite/client", "vite-plugin-pwa/vanillajs"]
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,10 +14,12 @@ export default defineConfig(({ mode }) => {
|
||||||
handlebars({
|
handlebars({
|
||||||
vars: {
|
vars: {
|
||||||
opensearchEnabled: env.VITE_OPENSEARCH_ENABLED === "true",
|
opensearchEnabled: env.VITE_OPENSEARCH_ENABLED === "true",
|
||||||
routeDomain: env.VITE_APP_DOMAIN + (env.VITE_NORMAL_ROUTER !== 'true' ? "/#" : ""),
|
routeDomain:
|
||||||
|
env.VITE_APP_DOMAIN +
|
||||||
|
(env.VITE_NORMAL_ROUTER !== "true" ? "/#" : ""),
|
||||||
domain: env.VITE_APP_DOMAIN,
|
domain: env.VITE_APP_DOMAIN,
|
||||||
env,
|
env
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
react({
|
react({
|
||||||
babel: {
|
babel: {
|
||||||
|
@ -29,23 +31,23 @@ export default defineConfig(({ mode }) => {
|
||||||
modules: false,
|
modules: false,
|
||||||
useBuiltIns: "entry",
|
useBuiltIns: "entry",
|
||||||
corejs: {
|
corejs: {
|
||||||
version: "3.29",
|
version: "3.29"
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
disable: process.env.VITE_PWA_ENABLED !== "yes",
|
disable: process.env.VITE_PWA_ENABLED !== "yes",
|
||||||
registerType: "autoUpdate",
|
registerType: "autoUpdate",
|
||||||
workbox: {
|
workbox: {
|
||||||
globIgnores: ["**ping.txt**"],
|
globIgnores: ["**ping.txt**"]
|
||||||
},
|
},
|
||||||
includeAssets: [
|
includeAssets: [
|
||||||
"favicon.ico",
|
"favicon.ico",
|
||||||
"apple-touch-icon.png",
|
"apple-touch-icon.png",
|
||||||
"safari-pinned-tab.svg",
|
"safari-pinned-tab.svg"
|
||||||
],
|
],
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "movie-web",
|
name: "movie-web",
|
||||||
|
@ -61,53 +63,57 @@ export default defineConfig(({ mode }) => {
|
||||||
src: "android-chrome-192x192.png",
|
src: "android-chrome-192x192.png",
|
||||||
sizes: "192x192",
|
sizes: "192x192",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "any",
|
purpose: "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "android-chrome-512x512.png",
|
src: "android-chrome-512x512.png",
|
||||||
sizes: "512x512",
|
sizes: "512x512",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "any",
|
purpose: "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "android-chrome-192x192.png",
|
src: "android-chrome-192x192.png",
|
||||||
sizes: "192x192",
|
sizes: "192x192",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "maskable",
|
purpose: "maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "android-chrome-512x512.png",
|
src: "android-chrome-512x512.png",
|
||||||
sizes: "512x512",
|
sizes: "512x512",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "maskable",
|
purpose: "maskable"
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
}),
|
}),
|
||||||
loadVersion(),
|
loadVersion(),
|
||||||
checker({
|
checker({
|
||||||
overlay: {
|
overlay: {
|
||||||
position: "tr",
|
position: "tr"
|
||||||
},
|
},
|
||||||
typescript: true, // check typescript build errors in dev server
|
typescript: true, // check typescript build errors in dev server
|
||||||
eslint: {
|
eslint: {
|
||||||
// check lint errors in dev server
|
// check lint errors in dev server
|
||||||
lintCommand: "eslint --ext .tsx,.ts src",
|
lintCommand: "eslint --ext .tsx,.ts src",
|
||||||
dev: {
|
dev: {
|
||||||
logLevel: ["error"],
|
logLevel: ["error"]
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
"@sozialhelden/ietf-language-tags": path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"./node_modules/@sozialhelden/ietf-language-tags/dist/cjs"
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
test: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom"
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue