diff --git a/package.json b/package.json index c99f2b19..67d409e9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04c85400..3c4daccd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/src/backend/helpers/subs.ts b/src/backend/helpers/subs.ts index 630cf75b..37800546 100644 --- a/src/backend/helpers/subs.ts +++ b/src/backend/helpers/subs.ts @@ -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 { // 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 diff --git a/src/components/Button.tsx b/src/components/Button.tsx index af75a9c4..5237806b 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -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", diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 632ba92a..3076a274 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -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`, }} > -
+
+ {props.doBackground ? ( + + ) : null}
errored!

; 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, diff --git a/src/components/player/atoms/settings/SettingsMenu.tsx b/src/components/player/atoms/settings/SettingsMenu.tsx index 1ff5f9c2..5ecd8aec 100644 --- a/src/components/player/atoms/settings/SettingsMenu.tsx +++ b/src/components/player/atoms/settings/SettingsMenu.tsx @@ -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); diff --git a/src/components/player/base/SubtitleView.tsx b/src/components/player/base/SubtitleView.tsx index 3f030e6a..d1bf037f 100644 --- a/src/components/player/base/SubtitleView.tsx +++ b/src/components/player/base/SubtitleView.tsx @@ -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( diff --git a/src/components/player/utils/captions.ts b/src/components/player/utils/captions.ts index 4204ad0b..2ba822f9 100644 --- a/src/components/player/utils/captions.ts +++ b/src/components/player/utils/captions.ts @@ -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[]; } diff --git a/src/components/player/utils/language.ts b/src/components/player/utils/language.ts new file mode 100644 index 00000000..bf31e786 --- /dev/null +++ b/src/components/player/utils/language.ts @@ -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}`; +} diff --git a/src/components/utils/Lightbar.css b/src/components/utils/Lightbar.css index 0850c1f0..7fe7572b 100644 --- a/src/components/utils/Lightbar.css +++ b/src/components/utils/Lightbar.css @@ -75,4 +75,4 @@ 100% { transform: rotate(180deg) translateZ(0px) translateY(400px) scaleX(1); } -} \ No newline at end of file +} diff --git a/src/components/utils/Lightbar.tsx b/src/components/utils/Lightbar.tsx index a01b725e..0d30b5ad 100644 --- a/src/components/utils/Lightbar.tsx +++ b/src/components/utils/Lightbar.tsx @@ -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 ( -
+
diff --git a/src/components/utils/Text.tsx b/src/components/utils/Text.tsx index 7c24364b..3151fb09 100644 --- a/src/components/utils/Text.tsx +++ b/src/components/utils/Text.tsx @@ -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 (

@@ -21,6 +25,21 @@ export function Heading2(props: TextProps) {

+ {props.children} +

+ ); +} + +export function Heading3(props: TextProps) { + return ( +

@@ -34,6 +53,7 @@ export function Paragraph(props: TextProps) {

diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 845d5d4f..27f92201 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -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 ( -

+ ); } function SettingsSidebar() { // eslint-disable-next-line no-restricted-globals const hostname = location.hostname; + const rem = 16; return (
-
+ - Account + {/* I looked over at my bookshelf to come up with these links */} + A war in my name! + + TANSTAAFL + + We all float down here + My skin is not my own @@ -52,7 +89,7 @@ function SettingsSidebar() { {hostname}
-
+

); } @@ -62,17 +99,118 @@ function SettingsLayout(props: { children: React.ReactNode }) {
- {props.children} +
{props.children}
); } +function SecondaryLabel(props: { children: React.ReactNode }) { + return

{props.children}

; +} + +function Card(props: { + children: React.ReactNode; + className?: string; + paddingClass?: string; +}) { + return ( +
+ {props.children} +
+ ); +} + +function AltCard(props: { + children: React.ReactNode; + className?: string; + paddingClass?: string; +}) { + return ( +
+ {props.children} +
+ ); +} + +function AccountSection() { + return ( +
+ Account + Beep beep +
+ ); +} + +function DevicesSection() { + const devices = [ + "Jip's iPhone", + "Muad'Dib's Nintendo Switch", + "Oppenheimer's old-ass phone", + ]; + return ( +
+ + Devices + +
+ {devices.map((deviceName) => ( + +
+ Device name +

{deviceName}

+
+ +
+ ))} +
+
+ ); +} + +function ActionsSection() { + return ( +
+ Actions + +
+ Delete account +

+ This action is irreversible. All data will be deleted and nothing + can be recovered. +

+
+
+ +
+
+
+ ); +} + export function SettingsPage() { return ( - Setting + + + ); diff --git a/src/pages/layouts/SubPageLayout.tsx b/src/pages/layouts/SubPageLayout.tsx index 0b12de6a..9e3c66f4 100644 --- a/src/pages/layouts/SubPageLayout.tsx +++ b/src/pages/layouts/SubPageLayout.tsx @@ -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 */} -
-
+
+
); } @@ -23,7 +35,7 @@ export function SubPageLayout(props: { children: React.ReactNode }) { {/* Main page */} - +
{props.children}
diff --git a/tailwind.config.js b/tailwind.config.js index dbefe882..ef997936 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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" } }, diff --git a/tsconfig.json b/tsconfig.json index 54cc7b88..2048c3ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] }, diff --git a/vite.config.ts b/vite.config.ts index bf8e7e60..35e32efa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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" + } }; });