correct languages + settings page styling

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-27 21:51:14 +02:00
parent 1cbf9f3c45
commit 9ff603f87c
18 changed files with 301 additions and 62 deletions

View file

@ -8,6 +8,7 @@
"@headlessui/react": "^1.5.0",
"@movie-web/providers": "^1.0.2",
"@react-spring/web": "^9.7.1",
"@sozialhelden/ietf-language-tags": "^5.4.2",
"classnames": "^2.3.2",
"core-js": "^3.29.1",
"dompurify": "^3.0.1",

View file

@ -17,6 +17,9 @@ dependencies:
'@react-spring/web':
specifier: ^9.7.1
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:
specifier: ^2.3.2
version: 2.3.2
@ -1956,6 +1959,13 @@ packages:
rollup: 2.79.1
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:
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
dependencies:
@ -4456,6 +4466,10 @@ packages:
p-locate: 5.0.0
dev: true
/lodash.compact@3.0.1:
resolution: {integrity: sha512-2ozeiPi+5eBXW1CLtzjk8XQFhQOEMwwfxblqeq6EGyTxZJ1bPATqilY0e6g2SLQpP4KuMeuioBhEnWz5Pr7ICQ==}
dev: false
/lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: true
@ -5986,7 +6000,6 @@ packages:
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
/ufo@1.3.0:
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}

View file

@ -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> {
// 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

View file

@ -8,7 +8,7 @@ interface Props {
icon?: Icons;
onClick?: () => void;
children?: ReactNode;
theme?: "white" | "purple" | "secondary";
theme?: "white" | "purple" | "secondary" | "danger";
padding?: string;
className?: string;
href?: string;
@ -26,6 +26,8 @@ export function Button(props: Props) {
if (props.theme === "secondary")
colorClasses =
"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(
"cursor-pointer inline-flex items-center justify-center rounded-lg font-medium transition-[transform,background-color] duration-100 active:scale-105 md:px-8",

View file

@ -1,9 +1,11 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { Link } from "react-router-dom";
import { IconPatch } from "@/components/buttons/IconPatch";
import { Icons } from "@/components/Icon";
import { Lightbar } from "@/components/utils/Lightbar";
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
import { conf } from "@/setup/config";
import { useBannerSize } from "@/stores/banner";
@ -13,10 +15,12 @@ export interface NavigationProps {
children?: ReactNode;
bg?: boolean;
noLightbar?: boolean;
doBackground?: boolean;
}
export function Navigation(props: NavigationProps) {
const bannerHeight = useBannerSize();
return (
<>
{!props.noLightbar ? (
@ -37,7 +41,17 @@ export function Navigation(props: NavigationProps) {
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
className={`${
props.bg ? "opacity-100" : "opacity-0"

View file

@ -3,16 +3,13 @@ import { ReactNode, useRef, useState } from "react";
import { useAsync, useAsyncFn } from "react-use";
import { convert } from "subsrt-ts";
import {
SubtitleSearchItem,
languageIdToName,
subtitleTypeList,
} from "@/backend/helpers/subs";
import { SubtitleSearchItem, subtitleTypeList } from "@/backend/helpers/subs";
import { FlagIcon } from "@/components/FlagIcon";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu";
import { Input } from "@/components/player/internals/ContextMenu/Input";
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
import { getLanguageFromIETF } from "@/components/player/utils/language";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store";
import { useSubtitleStore } from "@/stores/subtitles";
@ -35,6 +32,7 @@ export function CaptionOption(props: {
he: "il",
ze: "cn",
ar: "sa",
ja: "jp",
};
let countryCode =
(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.value) {
const subs = req.value.map((v) => {
const languageName = languageIdToName(v.attributes.language) ?? "unknown";
const languageName =
getLanguageFromIETF(v.attributes.language) ?? "unknown";
return {
...v,
languageName,

View file

@ -1,10 +1,10 @@
import { useMemo } from "react";
import { languageIdToName } from "@/backend/helpers/subs";
import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon";
import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu";
import { getLanguageFromIETF } from "@/components/player/utils/language";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { usePlayerStore } from "@/stores/player/store";
import { qualityToString } from "@/stores/player/utils/qualities";
@ -26,7 +26,7 @@ export function SettingsMenu({ id }: { id: string }) {
const { toggleLastUsed } = useCaptions();
const selectedLanguagePretty = selectedCaptionLanguage
? languageIdToName(selectedCaptionLanguage) ?? "unknown"
? getLanguageFromIETF(selectedCaptionLanguage) ?? "unknown"
: undefined;
const source = usePlayerStore((s) => s.source);

View file

@ -68,13 +68,14 @@ export function CaptionCue({
export function SubtitleRenderer() {
const videoTime = usePlayerStore((s) => s.progress.time);
const srtData = usePlayerStore((s) => s.caption.selected?.srtData);
const language = usePlayerStore((s) => s.caption.selected?.language);
const styling = useSubtitleStore((s) => s.styling);
const overrideCasing = useSubtitleStore((s) => s.overrideCasing);
const delay = useSubtitleStore((s) => s.delay);
const parsedCaptions = useMemo(
() => (srtData ? parseSubtitles(srtData) : []),
[srtData]
() => (srtData ? parseSubtitles(srtData, language) : []),
[srtData, language]
);
const visibileCaptions = useMemo(

View file

@ -47,7 +47,10 @@ export function convertSubtitlesToSrt(text: string): string {
return srt;
}
export function parseSubtitles(text: string): CaptionCueType[] {
export function parseSubtitles(
text: string,
_language?: string
): CaptionCueType[] {
const vtt = convertSubtitlesToVtt(text);
return parse(vtt).filter((cue) => cue.type === "caption") as CaptionCueType[];
}

View 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}`;
}

View file

@ -75,4 +75,4 @@
100% {
transform: rotate(180deg) translateZ(0px) translateY(400px) scaleX(1);
}
}
}

View file

@ -1,3 +1,4 @@
import classNames from "classnames";
import { useEffect, useRef } from "react";
import "./Lightbar.css";
@ -161,7 +162,12 @@ function ParticlesCanvas() {
export function Lightbar(props: { className?: string }) {
return (
<div className={props.className}>
<div
className={classNames(
"grid grid-cols-[100%] w-full overflow-x-hidden",
props.className
)}
>
<div className="lightbar">
<ParticlesCanvas />
<div className="lightbar-visual" />

View file

@ -1,13 +1,17 @@
interface TextProps {
className?: string;
children: React.ReactNode;
border?: boolean;
}
const borderClass = "pb-4 border-b border-utils-divider border-opacity-50";
export function Heading1(props: TextProps) {
return (
<h1
className={[
"text-5xl font-bold text-white mb-9",
props.border ? borderClass : null,
props.className ?? "",
].join(" ")}
>
@ -21,6 +25,21 @@ export function Heading2(props: TextProps) {
<h2
className={[
"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 ?? "",
].join(" ")}
>
@ -34,6 +53,7 @@ export function Paragraph(props: TextProps) {
<p
className={[
"text-type-text my-9 font-medium",
props.border ? borderClass : null,
props.className ?? "",
].join(" ")}
>

View file

@ -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 { WideContainer } from "@/components/layout/WideContainer";
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 { 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 (
<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
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}
/>
<span>{props.children}</span>
</div>
</a>
);
}
function SettingsSidebar() {
// eslint-disable-next-line no-restricted-globals
const hostname = location.hostname;
const rem = 16;
return (
<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">
<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>
<Divider />
<SidebarSection title="App information">
@ -52,7 +89,7 @@ function SettingsSidebar() {
<span className="text-right">{hostname}</span>
</div>
</SidebarSection>
</div>
</Sticky>
</div>
);
}
@ -62,17 +99,118 @@ function SettingsLayout(props: { children: React.ReactNode }) {
<WideContainer ultraWide>
<div className="grid grid-cols-[260px,1fr] gap-12">
<SettingsSidebar />
{props.children}
<div className="space-y-16">{props.children}</div>
</div>
</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() {
return (
<SubPageLayout>
<SettingsLayout>
<Heading1>Setting</Heading1>
<AccountSection />
<DevicesSection />
<ActionsSection />
</SettingsLayout>
</SubPageLayout>
);

View file

@ -1,12 +1,24 @@
import classNames from "classnames";
import { FooterView } from "@/components/layout/Footer";
import { Navigation } from "@/components/layout/Navigation";
export function BlurEllipsis() {
export function BlurEllipsis(props: { positionClass?: string }) {
return (
<>
{/* 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 className="absolute top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB 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-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 />
{/* Main page */}
<FooterView>
<Navigation noLightbar />
<Navigation doBackground noLightbar />
<div className="mt-40">{props.children}</div>
</FooterView>
</div>

View file

@ -69,7 +69,9 @@ module.exports = {
// Buttons
buttons: {
toggle: "#8D44D6",
toggleDisabled: "#202836"
toggleDisabled: "#202836",
danger: "#792131",
dangerHover: "#8a293b"
},
// only used for body colors/textures
@ -111,12 +113,21 @@ module.exports = {
settings: {
sidebar: {
activeLink: "#171728",
type: {
secondary: "#4B395F",
inactive: "#8D68A9",
icon: "#926CAD",
iconActivated: "#6942A8",
activated: "#CBA1E8"
}
},
card: {
border: "#2A243E",
background: "#29243D",
altBackground: "#29243D"
}
},

View file

@ -17,7 +17,10 @@
"jsx": "react-jsx",
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"@sozialhelden/ietf-language-tags": [
"../node_modules/@sozialhelden/ietf-language-tags/dist/cjs"
]
},
"types": ["vite/client", "vite-plugin-pwa/vanillajs"]
},

View file

@ -14,10 +14,12 @@ export default defineConfig(({ mode }) => {
handlebars({
vars: {
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,
env,
},
env
}
}),
react({
babel: {
@ -29,23 +31,23 @@ export default defineConfig(({ mode }) => {
modules: false,
useBuiltIns: "entry",
corejs: {
version: "3.29",
},
},
],
],
},
version: "3.29"
}
}
]
]
}
}),
VitePWA({
disable: process.env.VITE_PWA_ENABLED !== "yes",
registerType: "autoUpdate",
workbox: {
globIgnores: ["**ping.txt**"],
globIgnores: ["**ping.txt**"]
},
includeAssets: [
"favicon.ico",
"apple-touch-icon.png",
"safari-pinned-tab.svg",
"safari-pinned-tab.svg"
],
manifest: {
name: "movie-web",
@ -61,53 +63,57 @@ export default defineConfig(({ mode }) => {
src: "android-chrome-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
purpose: "any"
},
{
src: "android-chrome-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
purpose: "any"
},
{
src: "android-chrome-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
purpose: "maskable"
},
{
src: "android-chrome-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
purpose: "maskable"
}
]
}
}),
loadVersion(),
checker({
overlay: {
position: "tr",
position: "tr"
},
typescript: true, // check typescript build errors in dev server
eslint: {
// check lint errors in dev server
lintCommand: "eslint --ext .tsx,.ts src",
dev: {
logLevel: ["error"],
},
},
}),
logLevel: ["error"]
}
}
})
],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
"@sozialhelden/ietf-language-tags": path.resolve(
__dirname,
"./node_modules/@sozialhelden/ietf-language-tags/dist/cjs"
)
}
},
test: {
environment: "jsdom",
},
environment: "jsdom"
}
};
});