diff --git a/.vscode/settings.json b/.vscode/settings.json index 279011fe..741f0c0a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "eslint.format.enable": true, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" } -} +} \ No newline at end of file diff --git a/public/skull.svg b/public/skull.svg new file mode 100644 index 00000000..02c4741b --- /dev/null +++ b/public/skull.svg @@ -0,0 +1 @@ + diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index aff10ea4..8745a459 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -6,6 +6,7 @@ import { Icon, Icons } from "@/components/Icon"; export interface OptionItem { id: string; name: string; + leftIcon?: React.ReactNode; } interface DropdownProps { @@ -20,12 +21,17 @@ export function Dropdown(props: DropdownProps) { {({ open }) => ( <> - - {props.selectedItem.name} + + + {props.selectedItem.leftIcon + ? props.selectedItem.leftIcon + : null} + {props.selectedItem.name} + @@ -37,17 +43,18 @@ export function Dropdown(props: DropdownProps) { leaveFrom="opacity-100" leaveTo="opacity-0" > - + {props.options.map((opt) => ( - `relative cursor-default select-none py-2 pl-10 pr-4 ${ + `flex gap-4 items-center relative cursor-default select-none py-3 pl-4 pr-4 ${ active ? "bg-denim-400 text-bink-700" : "text-white" }` } key={opt.id} value={opt} > + {opt.leftIcon ? opt.leftIcon : null} {opt.name} ))} diff --git a/src/components/FlagIcon.tsx b/src/components/FlagIcon.tsx index fcfd1531..2a4d2ce5 100644 --- a/src/components/FlagIcon.tsx +++ b/src/components/FlagIcon.tsx @@ -6,11 +6,40 @@ export interface FlagIconProps { } export function FlagIcon(props: FlagIconProps) { + // Country code overrides + const countryOverrides: Record = { + en: "gb", + cs: "cz", + el: "gr", + fa: "ir", + ko: "kr", + he: "il", + ze: "cn", + ar: "sa", + ja: "jp", + bs: "ba", + vi: "vn", + zh: "cn", + sl: "si", + }; + + let countryCode = + (props.countryCode || "")?.split("-").pop()?.toLowerCase() || ""; + if (countryOverrides[countryCode]) + countryCode = countryOverrides[countryCode]; + + if (countryCode === "pirate") + return ( +
+ +
+ ); + return ( ); diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index 6eab1a65..ccf2c519 100644 --- a/src/components/player/atoms/settings/CaptionsView.tsx +++ b/src/components/player/atoms/settings/CaptionsView.tsx @@ -13,6 +13,7 @@ import { getLanguageFromIETF } from "@/components/player/utils/language"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { usePlayerStore } from "@/stores/player/store"; import { useSubtitleStore } from "@/stores/subtitles"; +import { sortLangCodes } from "@/utils/sortLangCodes"; export function CaptionOption(props: { countryCode?: string; @@ -22,24 +23,6 @@ export function CaptionOption(props: { onClick?: () => void; error?: React.ReactNode; }) { - // Country code overrides - const countryOverrides: Record = { - en: "gb", - cs: "cz", - el: "gr", - fa: "ir", - ko: "kr", - he: "il", - ze: "cn", - ar: "sa", - ja: "jp", - bs: "ba", - }; - let countryCode = - (props.countryCode || "")?.split("-").pop()?.toLowerCase() || ""; - if (countryOverrides[countryCode]) - countryCode = countryOverrides[countryCode]; - return ( - + {props.children} @@ -64,19 +47,12 @@ function searchSubs( subs: (SubtitleSearchItem & { languageName: string })[], searchQuery: string ) { - const languagesOrder = ["en", "hi", "fr", "de", "nl", "pt"].reverse(); // Reverse is neccesary, not sure why - + const sorted = sortLangCodes(subs.map((t) => t.attributes.language)); let results = subs.sort((a, b) => { - if ( - languagesOrder.indexOf(b.attributes.language) !== -1 || - languagesOrder.indexOf(a.attributes.language) !== -1 - ) - return ( - languagesOrder.indexOf(b.attributes.language) - - languagesOrder.indexOf(a.attributes.language) - ); - - return a.languageName.localeCompare(b.languageName); + return ( + sorted.indexOf(a.attributes.language) - + sorted.indexOf(b.attributes.language) + ); }); if (searchQuery.trim().length > 0) { @@ -152,7 +128,7 @@ export function CaptionsView({ id }: { id: string }) { if (req.loading) content =

loading...

; else if (req.error) content =

errored!

; else if (req.value) { - const subs = req.value.map((v) => { + const subs = req.value.filter(Boolean).map((v) => { const languageName = getLanguageFromIETF(v.attributes.language) ?? "unknown"; return { diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 28a98b4a..3d35fd31 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -18,6 +18,7 @@ import { AccountWithToken, useAuthStore } from "@/stores/auth"; import { useThemeStore } from "@/stores/theme"; import { SubPageLayout } from "./layouts/SubPageLayout"; +import { LocalePart } from "./settings/LocalePart"; function SettingsLayout(props: { children: React.ReactNode }) { const { isMobile } = useIsMobile(); @@ -79,6 +80,9 @@ export function SettingsPage() { )} +
+ +
diff --git a/src/pages/settings/AccountActionsPart.tsx b/src/pages/settings/AccountActionsPart.tsx index 086d8cd4..61166ccc 100644 --- a/src/pages/settings/AccountActionsPart.tsx +++ b/src/pages/settings/AccountActionsPart.tsx @@ -14,6 +14,8 @@ export function AccountActionsPart() { const { logout } = useAuthData(); const [deleteResult, deleteExec] = useAsyncFn(async () => { if (!account) return; + // eslint-disable-next-line no-restricted-globals + if (!confirm("You sure bro?")) return; await deleteUser(url, account); logout(); }, [logout, account, url]); diff --git a/src/pages/settings/LocalePart.tsx b/src/pages/settings/LocalePart.tsx new file mode 100644 index 00000000..b1a1951e --- /dev/null +++ b/src/pages/settings/LocalePart.tsx @@ -0,0 +1,36 @@ +import { Dropdown } from "@/components/Dropdown"; +import { FlagIcon } from "@/components/FlagIcon"; +import { Heading1 } from "@/components/utils/Text"; +import { appLanguageOptions } from "@/setup/i18n"; +import { useLanguageStore } from "@/stores/language"; +import { sortLangCodes } from "@/utils/sortLangCodes"; + +export function LocalePart() { + const sorted = sortLangCodes(appLanguageOptions.map((t) => t.id)); + const { language, setLanguage } = useLanguageStore(); + + const options = appLanguageOptions + .sort((a, b) => sorted.indexOf(a.id) - sorted.indexOf(b.id)) + .map((opt) => ({ + id: opt.id, + name: `${opt.englishName} — ${opt.nativeName}`, + leftIcon: , + })); + + const selected = options.find((t) => t.id === language); + + return ( +
+ Locale +

Application language

+

+ Language applied to the entire application. +

+ setLanguage(opt.id)} + /> +
+ ); +} diff --git a/src/pages/settings/SidebarPart.tsx b/src/pages/settings/SidebarPart.tsx index 3757fb1c..3cc73049 100644 --- a/src/pages/settings/SidebarPart.tsx +++ b/src/pages/settings/SidebarPart.tsx @@ -18,6 +18,7 @@ export function SidebarPart() { const settingLinks = [ { text: "Account", id: "settings-account", icon: Icons.USER }, + { text: "Locale", id: "settings-locale", icon: Icons.LINK }, { text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB }, { text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS }, ]; @@ -35,10 +36,10 @@ export function SidebarPart() { const visible = !( Math.floor( - 100 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100 + 50 - ((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100 ) < percentageVisible || Math.floor( - 100 - ((rect.bottom - windowHeight) / rect.height) * 100 + 50 - ((rect.bottom - windowHeight) / rect.height) * 100 ) < percentageVisible ); @@ -80,6 +81,7 @@ export function SidebarPart() { icon={v.icon} active={v.id === activeLink} onClick={() => scrollTo(v.id)} + key={v.id} > {v.text} diff --git a/src/stores/language/index.ts b/src/stores/language/index.ts index 9ff91476..052127bc 100644 --- a/src/stores/language/index.ts +++ b/src/stores/language/index.ts @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { create } from "zustand"; +import { persist } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; import i18n from "@/setup/i18n"; @@ -10,14 +11,17 @@ export interface LanguageStore { } export const useLanguageStore = create( - immer((set) => ({ - language: "en", - setLanguage(v) { - set((s) => { - s.language = v; - }); - }, - })) + persist( + immer((set) => ({ + language: "en", + setLanguage(v) { + set((s) => { + s.language = v; + }); + }, + })), + { name: "__MW::locale" } + ) ); export function useLanguageListener() { diff --git a/src/utils/sortLangCodes.ts b/src/utils/sortLangCodes.ts new file mode 100644 index 00000000..57999e92 --- /dev/null +++ b/src/utils/sortLangCodes.ts @@ -0,0 +1,12 @@ +export function sortLangCodes(langCodes: string[]) { + const languagesOrder = ["en", "hi", "fr", "de", "nl", "pt"].reverse(); // Reverse is neccesary, not sure why + + const results = langCodes.sort((a, b) => { + if (languagesOrder.indexOf(b) !== -1 || languagesOrder.indexOf(a) !== -1) + return languagesOrder.indexOf(b) - languagesOrder.indexOf(a); + + return a.localeCompare(b); + }); + + return results; +} diff --git a/themes/default.ts b/themes/default.ts index 7dffe5d4..90b1f98d 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -4,23 +4,23 @@ export const defaultTheme = { themePreview: { primary: "#505DBD", secondary: "#73739D", - ghost: "white" + ghost: "white", }, // Branding pill: { - background: "#1C1C36" + background: "#1C1C36", }, // meta data for the theme itself global: { accentA: "#505DBD", - accentB: "#3440A1" + accentB: "#3440A1", }, // light bar lightBar: { - light: "#2A2A71" + light: "#2A2A71", }, // Buttons @@ -39,14 +39,14 @@ export const defaultTheme = { purple: "#6b298a", purpleHover: "#7f35a1", cancel: "#252533", - cancelHover: "#3C3C4A" + cancelHover: "#3C3C4A", }, // only used for body colors/textures background: { main: "#0A0A10", accentA: "#6E3B80", - accentB: "#1F1F50" + accentB: "#1F1F50", }, // typography @@ -55,7 +55,7 @@ export const defaultTheme = { text: "#73739D", dimmed: "#926CAD", divider: "#262632", - secondary: "#64647B" + secondary: "#64647B", }, // search bar @@ -64,7 +64,7 @@ export const defaultTheme = { focused: "#24243C", placeholder: "#4A4A71", icon: "#545476", - text: "#FFFFFF" + text: "#FFFFFF", }, // media cards @@ -76,13 +76,18 @@ export const defaultTheme = { barColor: "#4B4B63", barFillColor: "#BA7FD6", badge: "#151522", - badgeText: "#5F5F7A" + badgeText: "#5F5F7A", }, // Large card largeCard: { background: "#171728", - icon: "#6741A5" + icon: "#6741A5", + }, + + // Dropdown + dropdown: { + background: "#171728", }, // Passphrase @@ -92,7 +97,7 @@ export const defaultTheme = { wordBackground: "#171728", copyText: "#58587A", copyTextHover: "#8888AA", - errorText: "#DB3D62" + errorText: "#DB3D62", }, // Settings page @@ -105,19 +110,19 @@ export const defaultTheme = { inactive: "#8D68A9", icon: "#926CAD", iconActivated: "#6942A8", - activated: "#CBA1E8" - } + activated: "#CBA1E8", + }, }, card: { border: "#2A243E", background: "#29243D", - altBackground: "#29243D" - } + altBackground: "#29243D", + }, }, utils: { - divider: "#353549" + divider: "#353549", }, // Error page @@ -126,20 +131,20 @@ export const defaultTheme = { border: "#252534", type: { - secondary: "#62627D" - } + secondary: "#62627D", + }, }, // About page about: { circle: "#262632", - circleText: "#9A9AC3" + circleText: "#9A9AC3", }, progress: { background: "#8787A8", preloaded: "#8787A8", - filled: "#A75FC9" + filled: "#A75FC9", }, // video player @@ -151,11 +156,11 @@ export const defaultTheme = { error: "#E44F4F", success: "#40B44B", loading: "#B759D8", - noresult: "#64647B" + noresult: "#64647B", }, audio: { - set: "#A75FC9" + set: "#A75FC9", }, context: { @@ -175,16 +180,16 @@ export const defaultTheme = { buttons: { list: "#161C26", - active: "#0D1317" + active: "#0D1317", }, type: { main: "#617A8A", secondary: "#374A56", - accent: "#A570FA" - } - } - } - } - } -} + accent: "#A570FA", + }, + }, + }, + }, + }, +};