Merge pull request #590 from movie-web/dev

Version 4.1.2
This commit is contained in:
mrjvs 2023-12-25 22:15:12 +01:00 committed by GitHub
commit 1e29ab3e3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
151 changed files with 3291 additions and 2107 deletions

View File

@ -21,6 +21,7 @@ module.exports = {
"dist/*", "dist/*",
"/*.js", "/*.js",
"/*.ts", "/*.ts",
"/*.mts",
"/plugins/*.ts", "/plugins/*.ts",
"/plugins/*.mjs", "/plugins/*.mjs",
"/themes/**/*.ts" "/themes/**/*.ts"
@ -61,7 +62,7 @@ module.exports = {
"no-nested-ternary": "off", "no-nested-ternary": "off",
"prefer-destructuring": "off", "prefer-destructuring": "off",
"no-param-reassign": "off", "no-param-reassign": "off",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
"react/jsx-filename-extension": [ "react/jsx-filename-extension": [
"error", "error",
{ extensions: [".js", ".tsx", ".jsx"] } { extensions: [".js", ".tsx", ".jsx"] }

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ node_modules
# production # production
/dist /dist
dev-dist dev-dist
/stats.html
# misc # misc
.DS_Store .DS_Store

View File

@ -1,6 +1,6 @@
{ {
"name": "movie-web", "name": "movie-web",
"version": "4.1.1", "version": "4.1.2",
"private": true, "private": true,
"homepage": "https://movie-web.app", "homepage": "https://movie-web.app",
"scripts": { "scripts": {
@ -26,96 +26,99 @@
] ]
}, },
"dependencies": { "dependencies": {
"@formkit/auto-animate": "^0.7.0", "@formkit/auto-animate": "^0.8.1",
"@headlessui/react": "^1.5.0", "@headlessui/react": "^1.7.17",
"@movie-web/providers": "^1.1.5", "@movie-web/providers": "^1.1.5",
"@noble/hashes": "^1.3.2", "@noble/hashes": "^1.3.3",
"@react-spring/web": "^9.7.1", "@react-spring/web": "^9.7.3",
"@scure/bip39": "^1.2.1", "@scure/bip39": "^1.2.2",
"@sozialhelden/ietf-language-tags": "^5.4.2", "@sozialhelden/ietf-language-tags": "^5.4.2",
"@types/node-forge": "^1.3.8", "@types/node-forge": "^1.3.10",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"core-js": "^3.29.1", "core-js": "^3.34.0",
"dompurify": "^3.0.1", "dompurify": "^3.0.6",
"flag-icons": "^6.11.1", "flag-icons": "^7.1.0",
"focus-trap-react": "^10.2.3", "focus-trap-react": "^10.2.3",
"fscreen": "^1.2.0", "fscreen": "^1.2.0",
"fuse.js": "^6.4.6", "fuse.js": "^7.0.0",
"hls.js": "^1.0.7", "hls.js": "^1.4.14",
"i18next": "^22.4.5", "i18next": "^23.7.11",
"immer": "^10.0.2", "immer": "^10.0.3",
"iso-639-1": "^3.1.0", "iso-639-1": "^3.1.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"million": "^2.6.4",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"ofetch": "^1.0.0", "ofetch": "^1.3.3",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-ga4": "^2.0.0", "react-ga4": "^2.1.0",
"react-google-recaptcha-v3": "^1.10.1", "react-google-recaptcha-v3": "^1.10.1",
"react-helmet-async": "^1.3.0", "react-helmet-async": "^2.0.4",
"react-i18next": "^12.1.1", "react-i18next": "^14.0.0",
"react-router-dom": "^5.2.0", "react-lazy-with-preload": "^2.2.1",
"react-router-dom": "^6.21.1",
"react-sticky-el": "^2.1.0", "react-sticky-el": "^2.1.0",
"react-turnstile": "^1.1.2", "react-turnstile": "^1.1.2",
"react-use": "^17.4.0", "react-use": "^17.4.2",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"subsrt-ts": "^2.1.1", "subsrt-ts": "^2.1.2",
"zustand": "^4.3.9" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.21.3", "@babel/core": "^7.23.6",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.23.6",
"@babel/preset-typescript": "^7.21.0", "@babel/preset-typescript": "^7.23.3",
"@types/chromecast-caf-sender": "^1.0.5", "@types/chromecast-caf-sender": "^1.0.8",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.2.1",
"@types/dompurify": "^2.4.0", "@types/dompurify": "^3.0.5",
"@types/fscreen": "^1.0.1", "@types/fscreen": "^1.0.4",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.7", "@types/lodash.throttle": "^4.1.9",
"@types/node": "^17.0.15", "@types/node": "^20.10.5",
"@types/pako": "^2.0.0", "@types/pako": "^2.0.3",
"@types/react": "^17.0.39", "@types/react": "^18.2.45",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^18.2.18",
"@types/react-helmet": "^6.1.6", "@types/react-helmet": "^6.1.11",
"@types/react-router": "^5.1.20", "@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-stickynode": "^4.0.0", "@types/react-stickynode": "^4.0.3",
"@types/react-transition-group": "^4.4.5", "@types/react-transition-group": "^4.4.10",
"@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^5.13.0", "@typescript-eslint/parser": "^6.15.0",
"@vitejs/plugin-react": "^3.1.0", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.16",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.10.0", "eslint": "^8.56.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^2.5.0", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.1.1",
"eslint-plugin-react": "7.29.4", "eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.3.0", "eslint-plugin-react-hooks": "4.6.0",
"glob": "^10.3.3", "glob": "^10.3.10",
"handlebars": "^4.7.7", "handlebars": "^4.7.8",
"jsdom": "^21.1.0", "jsdom": "^23.0.1",
"postcss": "^8.4.20", "postcss": "^8.4.32",
"postcss-rtl": "^2.0.0", "postcss-rtl": "^2.0.0",
"postcss-rtlcss": "^4.0.9", "postcss-rtlcss": "^4.0.9",
"prettier": "^2.5.1", "prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.1.7", "prettier-plugin-tailwindcss": "^0.5.9",
"tailwind-scrollbar": "^2.0.1", "rollup-plugin-visualizer": "^5.11.0",
"tailwindcss": "^3.2.4", "tailwind-scrollbar": "^3.0.5",
"tailwindcss-themer": "^3.1.0", "tailwindcss": "^3.4.0",
"type-fest": "^4.3.3", "tailwindcss-themer": "^4.0.0",
"typescript": "^4.6.4", "type-fest": "^4.8.3",
"vite": "^4.4.12", "typescript": "^5.3.3",
"vite-plugin-checker": "^0.5.6", "vite": "^5.0.10",
"vite-plugin-package-version": "^1.0.2", "vite-plugin-checker": "^0.6.2",
"vite-plugin-pwa": "^0.16.5", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-static-copy": "^0.16.0", "vite-plugin-pwa": "^0.17.4",
"vitest": "^0.28.5" "vite-plugin-static-copy": "^1.0.0",
"vitest": "^1.1.0"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {

File diff suppressed because it is too large Load Diff

View File

@ -1,71 +1,421 @@
{ {
"about": {
"description": "movie-web je webová aplikace, která vyhledává na internetu proudy médií. Cílem týmu je převážně minimalistický přístup ke konzumaci obsahu.",
"faqTitle": "Často kladené otázky",
"q1": {
"body": "movie-web nehostuje žádný obsah. Když kliknete na něco, co chcete sledovat, na internetu se vyhledá vybrané médium (Na obrazovce načítání a na kartě 'zdroje videa' můžete vidět, který zdroj používáte). Média se nikdy nenahrávají movie-webem, vše probíhá prostřednictvím tohoto vyhledávacího mechanismu.",
"title": "Kde bereme obsah?"
},
"q2": {
"body": "Není možné požádat o pořad nebo film, movie-web nespravuje žádný obsah. Veškerý obsah je prohlížen prostřednictvím zdrojů na internetu.",
"title": "Kde můžu požádat o pořad nebo film?"
},
"q3": {
"body": "Naše výsledky vyhledávání jsou založeny na The Movie Database (TMDB) a zobrazují se bez ohledu na to, zda naše zdroje skutečně obsah mají.",
"title": "Ve výsledcích vyhledávání se zobrazuje pořad nebo film, proč jej nemůžu přehrát?"
},
"title": "O movie-webu"
},
"actions": {
"copied": "Zkopírováno",
"copy": "Zkopírovat"
},
"auth": {
"createAccount": "Ještě nemáte účet? <0>Vytvořte si účet.</0>",
"deviceNameLabel": "Název zařízení",
"deviceNamePlaceholder": "Osobní telefon",
"generate": {
"description": "Vaše přístupová fráze se chová jako vaše přezdívka a heslo. Uchovejte jí v bezpečí, protože jí budete muset zadat, abyste se mohli přihlásit ke svému účtu",
"next": "Uložil jsem si moji přístupovou frázi",
"passphraseFrameLabel": "Přístupová fráze",
"title": "Vaše přístupová fráze"
},
"hasAccount": "Již máte účet? <0> Přihlaste se zde.</0>",
"login": {
"description": "Pro přihlášení ke svému účtu zadejte svou přístupovou frázi",
"deviceLengthError": "Zadejte název zařízení",
"passphraseLabel": "12slovná přístupová fráze",
"passphrasePlaceholder": "Přístupová fráze",
"submit": "Přihlásit",
"title": "Přihlaste se ke svému účtu",
"validationError": "Nesprávná nebo neúplná přístupová fráze"
},
"register": {
"information": {
"color1": "První barva profilu",
"color2": "Druhá barva profilu",
"header": "Zadejte název pro vaše zařízení a vyberte barvy a ikonu uživatele podle vašeho výběru",
"icon": "Ikona uživatele",
"next": "Další",
"title": "Informace o účtu"
}
},
"trust": {
"failed": {
"text": "Nastavili jste to správně?",
"title": "Selhalo připojení k serveru"
},
"host": "Připojujete se k <0>{{hostname}}</0> - potvrďte, že mu věříte před vytvořením účtu",
"no": "Zpět",
"title": "Věříte tomuto serveru?",
"yes": "Věřím tomuto serveru"
},
"verify": {
"description": "Zadejte prosím svou přístupovou frázi, abyste potvrdili, že jste si ji uložili, a vytvořte si účet",
"invalidData": "Data nejsou platná",
"noMatch": "Přístupová fráze neodpovídá",
"passphraseLabel": "Vaše 12slovná přístupová fráze",
"recaptchaFailed": "ReCaptcha ověření se nezdařilo",
"register": "Založit účet",
"title": "Potvrďte vaši přístupovou frázi"
}
},
"errors": {
"badge": "Rozbilo se to",
"details": "Detaily chyby",
"reloadPage": "Znovu načíst stránku",
"showError": "Ukázat detaily chyby",
"title": "Narazili jsme na chybu!"
},
"footer": {
"legal": {
"disclaimer": "Zřeknutí odpovědnosti",
"disclaimerText": "movie-web nehostuje žádné soubory, pouze odkazuje na služby třetích stran. Právní záležitosti by měly být řešeny s hostiteli souborů a poskytovateli. movie-web nenese odpovědnost za žádné mediální soubory zobrazené poskytovateli videa."
},
"links": {
"discord": "Discord",
"dmca": "DMCA",
"github": "GitHub"
},
"tagline": "Sledujte své oblíbené pořady a filmy s touto aplikací pro streamování s otevřeným zdrojovým kódem."
},
"global": { "global": {
"name": "movie-web" "name": "movie-web",
"pages": {
"about": "O nás",
"dmca": "DMCA",
"login": "Přihlásit se",
"pagetitle": "{{title}} - movie-web",
"register": "Zaregistrovat se",
"settings": "Nastavení"
}
}, },
"home": { "home": {
"search": {
"allResults": "To je vše co máme!",
"sectionTitle": "Výsledky vyhledávání",
"noResults": "Nemohli jsme nic najít!",
"failed": "Nepodařilo se najít média, zkuste to znovu!",
"loading": "Načítání...",
"placeholder": "Co si přejete sledovat?"
},
"bookmarks": { "bookmarks": {
"sectionTitle": "Záložky" "sectionTitle": "Záložky"
}, },
"continueWatching": { "continueWatching": {
"sectionTitle": "Pokračujte ve sledování" "sectionTitle": "Pokračujte ve sledování"
},
"mediaList": {
"stopEditing": "Přestat upravovat"
},
"search": {
"allResults": "To je vše co máme!",
"failed": "Nepodařilo se najít média, zkuste to znovu!",
"loading": "Načítání...",
"noResults": "Nemohli jsme nic najít!",
"placeholder": "Co si přejete sledovat?",
"sectionTitle": "Výsledky vyhledávání"
},
"titles": {
"day": {
"default": "Na co byste se chtěli dnes odpoledne dívat?"
},
"morning": {
"default": "Na co byste se chtěli dnes ráno dívat?",
"extra": [
"Slyšel jsem, že Před úsvitem je super."
]
},
"night": {
"default": "Na co byste se chtěli dnes večer dívat?",
"extra": [
"Unaven? Slyšel jsem, že Vymítač ďábla je super."
]
}
} }
}, },
"media": { "media": {
"episodeDisplay": "S{{season}} E{{episode}}",
"types": { "types": {
"movie": "Film", "movie": "Film",
"show": "Seriál" "show": "Seriál"
},
"episodeDisplay": "S{{season}} E{{episode}}"
},
"player": {
"playbackError": {
"title": "Jejda, rozbilo se to!"
},
"metadata": {
"notFound": {
"badge": "Nenalezeno",
"homeButton": "Zpátky domů",
"title": "Nemohli jsme najít Vaše média.",
"text": "Nemohli jsme najít média o které jste požádali. Buďto jsme ho nemohli najít, nebo jste manipulovali s URL."
} }
}, },
"menus": { "navigation": {
"captions": { "banner": {
"customChoice": "Nahrát titulky", "offline": "Zkontrolujte své internetové připojení"
"customizeLabel": "Upravit",
"title": "Titulky"
}, },
"sources": { "menu": {
"title": "Zdroje" "about": "O nás",
}, "donation": "Přispět",
"episodes": { "logout": "Odhlásit se",
"button": "Epizody", "register": "Synchronizovat do cloudu",
"loadingTitle": "Načítání...", "settings": "Nastavení",
"loadingList": "Načítání..." "support": "Podpořte nás"
}
},
"back": {
"default": "Zpátky domů",
"short": "Zpět"
} }
}, },
"notFound": { "notFound": {
"badge": "Nenalezeno", "badge": "Nenalezeno",
"goHome": "Zpátky domů", "goHome": "Zpátky domů",
"title": "Tuto stránku se nepodařilo najít", "message": "Dívali jsme se všude: pod koši, ve skříni, za proxy, ale nakonec jsme nemohli najít stránku, kterou hledáte.",
"message": "Dívali jsme se všude: pod koši, ve skříni, za proxy, ale nakonec jsme nemohli najít stránku, kterou hledáte." "title": "Tuto stránku se nepodařilo najít"
}, },
"navigation": { "overlays": {
"banner": { "close": "Zavřít"
"offline": "Zkontrolujte své internetové připojení" },
} "player": {
"back": {
"default": "Zpátky domů",
"short": "Zpět"
},
"casting": {
"enabled": "Odesílání do zařízení..."
},
"menus": {
"captions": {
"customChoice": "Nahrát titulky ze souboru",
"customizeLabel": "Přizpůsobit",
"offChoice": "Vypnuto",
"settings": {
"delay": "Posunutí titulků",
"fixCapitals": "Opravit velká písmena"
},
"title": "Titulky",
"unknownLanguage": "Neznámo"
},
"downloads": {
"disclaimer": "Stahování probíhá přímo u poskytovatele. movie-web nemá kontrolu nad tím, jak jsou stahování poskytovány.",
"downloadCaption": "Stáhnout titulky",
"downloadVideo": "Stáhnout video",
"hlsExplanation": "Toto médium je proud HLS, který nelze stáhnout na movie-web.",
"onAndroid": {
"1": "Na Androidu klikněte na tlačítko stahování, poté na nové stránce <bold>klepněte a podržte</bold> na videu a poté vyberte <bold>uložit</bold>.",
"shortTitle": "Stahování / Android",
"title": "Stahování na Androidu"
},
"onIos": {
"1": "Na iOS klikněte na tlačítko stahování a poté na nové stránce klikněte na <bold><ios_share /></bold> a poté na <bold>Uložit do souborů <ios_files /></bold>.",
"shortTitle": "Stahování / iOS",
"title": "Stahování na iOS"
},
"onPc": {
"1": "Na počítači klikněte na tlačítko stahování, poté na nové stránce klikněte pravým tlačítkem na video a vyberte <bold>Uložit video jako</bold>",
"shortTitle": "Stahování / počítač",
"title": "Stahování na počítači"
},
"title": "Stáhnout"
},
"episodes": {
"button": "Epizody",
"emptyState": "V této sezóně nejsou žádné epizody, vraťte se později!",
"episodeBadge": "E{{episode}}",
"loadingError": "Chyba při načítání sezóny",
"loadingList": "Načítání...",
"loadingTitle": "Načítání..."
},
"playback": {
"speedLabel": "Rychlost přehrávání",
"title": "Nastavení přehrávání"
},
"quality": {
"automaticLabel": "Automatická kvalita",
"hint": "Chcete-li získat jinou kvalitu, můžete zkusit <0>přepnout zdroj</0>.",
"iosNoQuality": "Kvůli omezením definovaným společností Apple není pro tento zdroj v iOS k dispozici výběr kvality. Chcete-li získat jinou kvalitu, můžete zkusit <0>přepnout zdroj</0>.",
"title": "Kvalita"
},
"settings": {
"captionItem": "Nastavení titulků",
"downloadItem": "Stáhnout",
"enableCaptions": "Povolit titulky",
"experienceSection": "Zážitek sledování",
"playbackItem": "Nastavení přehrávání",
"qualityItem": "Kvalita",
"sourceItem": "Zdroje videa",
"videoSection": "Nastavení videa"
},
"sources": {
"failed": {
"text": "Při pokusu o nalezení videí došlo k chybě. Zkuste prosím jiný zdroj.",
"title": "Nepodařilo se extrahovat data"
},
"noEmbeds": {
"text": "Nepodařilo se nám najít žádný vklad, zkuste prosím jiný zdroj.",
"title": "Žádné vklady"
},
"noStream": {
"text": "Tento zdroj nemá pro tento film nebo pořad žádné proudy média.",
"title": "Žádný proud média"
},
"title": "Zdroje",
"unknownOption": "Neznámý"
}
},
"metadata": {
"failed": {
"badge": "Neúspěšný",
"homeButton": "Jít domů",
"text": "Nelze načíst metadata média z TMDB. Zkontrolujte, zda není TMDB nefunkční nebo blokovaný na vašem internetovém připojení.",
"title": "Načtení metadat se nezdařilo"
},
"notFound": {
"badge": "Nenalezeno",
"homeButton": "Zpátky domů",
"text": "Nemohli jsme najít média o které jste požádali. Buď bylo odstraňeno, nebo jste manipulovali s URL.",
"title": "Nemohli jsme najít Vaše média."
}
},
"nextEpisode": {
"cancel": "Zrušit",
"next": "Další epizoda"
},
"playbackError": {
"badge": "Chyba přehrávání",
"errors": {
"errorAborted": "Načítání média bylo přerušeno uživatelem.",
"errorDecode": "Navzdory tomu, že bylo dříve určeno jako použitelné došlo při pokusu o dekódování média k chybě.",
"errorGenericMedia": "Nastala chyba neznámého média.",
"errorNetwork": "Nastala nějaká chyba síťě, která zabránila načtení média, přestože bylo předtím dostupné.",
"errorNotSupported": "Médium nebo poskytovatel média není podporovaný."
},
"homeButton": "Jít domů",
"text": "Nastala chyba při přehrávání média. Prosíme skuste to znovu.",
"title": "Video se nepodařilo přehrát!"
},
"scraping": {
"items": {
"failure": "Nastala chyba",
"notFound": "Nemá toto video",
"pending": "Ověřování videí..."
},
"notFound": {
"badge": "Nenalezeno",
"detailsButton": "Zobrazit podrobnosti",
"homeButton": "Jít domů",
"text": "Prohledali jsme naše poskytovatele a nenašli jsme média, která hledáte! Nehostujeme žádné média a nemáme žádnou kontrolu nad tím, co je k dispozici. Pro více podrobností klikněte níže na 'Zobrazit podrobnosti'.",
"title": "Nedokázali jsme to najít"
}
},
"time": {
"regular": "{{timeWatched}} / {{duration}}",
"remaining": "{{timeLeft}} zbývá • Dokončeno v {{timeFinished, datetime}}",
"shortRegular": "{{timeWatched}}",
"shortRemaining": "-{{timeLeft}}"
}
},
"screens": {
"dmca": {
"text": "Vítejte na DMCA kontaktní stránce movie-webu! Respektujeme práva duševního vlastnictví a chceme rychle řešit jakékoli problémy s autorským právem. Pokud se domníváte, že vaše dílo chráněné autorskými právy bylo na naší platformě neoprávněně použito, zašlete prosím podrobné oznámení DMCA na níže uvedený e-mail. Uveďte prosím popis materiálu chráněného autorským právem, své kontaktní údaje a prohlášení o dobré víře. Jsme odhodláni tyto záležitosti rychle vyřešit a oceňujeme vaši spolupráci při udržování movie-webu jako místa, které respektuje kreativitu a autorská práva.",
"title": "DMCA"
},
"loadingApp": "Načítání aplikace",
"loadingUser": "Načítání vášeho profilu",
"loadingUserError": {
"logout": "Odhlásit se",
"reset": "Resetovat vlastní server",
"text": "Nezdařilo se načíst váš profil",
"textWithReset": "Nezdařilo se načíst váš profil z vašeho serveru, chcete ho přepnout na výchozí server?"
},
"migration": {
"failed": "Migrace dat se nezdařila.",
"inProgress": "Počkejte prosím, migrujeme vaše data. Nemělo by to trvat dlouho."
}
},
"settings": {
"account": {
"accountDetails": {
"deviceNameLabel": "Název zařízení",
"deviceNamePlaceholder": "Osobní telefon",
"editProfile": "Upravit",
"logoutButton": "Odhlásit se"
},
"actions": {
"delete": {
"button": "Smazat účet",
"confirmButton": "Smazat účet",
"confirmDescription": "Jste si jisti, že chcete smazat váš účet? Všechny data budou ztracena!",
"confirmTitle": "Jste si jisti?",
"text": "Tato akce nejde vrátit. Všechny data budou smazána a nic nepůjde zachránit.",
"title": "Smazat účet"
},
"title": "Akce"
},
"devices": {
"deviceNameLabel": "Název zařízení",
"failed": "Načtení relací se nezdařilo",
"removeDevice": "Odstranit",
"title": "Zařízení"
},
"profile": {
"finish": "Dokončit",
"firstColor": "První barva profilu",
"secondColor": "Druhá barva profilu",
"title": "Upravit profilovou fotografii",
"userIcon": "Ikona uživatele"
},
"register": {
"cta": "Začněte",
"text": "Sdílejte průběh sledování mezi zařízeními a udržujte je synchronizovaná.",
"title": "Synchronizace do cloudu"
},
"title": "Účet"
},
"appearance": {
"activeTheme": "Aktivní",
"themes": {
"blue": "Modrá",
"default": "Výchozí",
"gray": "Šedá",
"red": "Červená",
"teal": "Modrozelená"
},
"title": "Vzhled"
},
"captions": {
"backgroundLabel": "Neprůhlednost pozadí",
"colorLabel": "Barva",
"previewQuote": "Nesmím se bát. Strach je zabiják mysli.",
"textSizeLabel": "Velikost písma",
"title": "Titulky"
},
"connections": {
"server": {
"description": "Pokud se chcete připojit k vlastnímu backendu pr ukládání dat, povolte toto a zadejte URL adresu.",
"label": "Vlastní server",
"urlLabel": "URL adresa vlastního serveru"
},
"title": "Spojení",
"workers": {
"addButton": "Přidat nového pracovníka",
"description": "Aby byla aplikace funkční, veškerá trafika prochází přes proxy. Povolte toto, pokud chcete používat svoje vlastní pracovníky.",
"emptyState": "Zatím žádní pracovníci, přidej jednoho dolů",
"label": "Použít vlastní proxy pracovníky",
"urlLabel": "URL adresy pracovníků",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Jazyk aplikace",
"languageDescription": "Jazyk použitý na celou aplikaci.",
"title": "Lokální"
},
"reset": "Resetovat",
"save": "Uložit",
"sidebar": {
"info": {
"appVersion": "Verze aplikace",
"backendUrl": "URL backendu",
"backendVersion": "Verze backendu",
"hostname": "Název hostitele",
"insecure": "nebezpečný",
"notLoggedIn": "Nejste přihlášen",
"secure": "bezpečný",
"title": "Informace o aplikaci",
"unknownVersion": "Neznámo",
"userId": "Uživatelské ID"
}
},
"unsaved": "Máte neuložené změny"
} }
} }

View File

@ -27,6 +27,7 @@
"generate": { "generate": {
"description": "Deine Passphrase dient als dein Nutzername und Passwort. Speiche sie sicher ab, damit du dich in deinem Konto anmelden kannst", "description": "Deine Passphrase dient als dein Nutzername und Passwort. Speiche sie sicher ab, damit du dich in deinem Konto anmelden kannst",
"next": "Ich habe meine Passphrase gespeichert", "next": "Ich habe meine Passphrase gespeichert",
"passphraseFrameLabel": "Passphrase",
"title": "Deine Passphrase" "title": "Deine Passphrase"
}, },
"hasAccount": "Du hast bereits einen Account? <0>Anmelden.</0>", "hasAccount": "Du hast bereits einen Account? <0>Anmelden.</0>",

View File

@ -21,17 +21,18 @@
"copy": "Copier" "copy": "Copier"
}, },
"auth": { "auth": {
"createAccount": "N'avez-vous pas encore de compte? <0>Créer un compte.</0>", "createAccount": "N'avez-vous pas encore de compte? <0>Créer un compte.</0>",
"deviceNameLabel": "Nom de l'appareil", "deviceNameLabel": "Nom de l'appareil",
"deviceNamePlaceholder": "Téléphone personnel", "deviceNamePlaceholder": "Téléphone personnel",
"generate": { "generate": {
"description": "Votre passphrase fait office de nom d'utilisateur et de mot de passe. Conservez-la précieusement, car vous devrez la saisir pour vous connecter à votre compte", "description": "Le nom d'utilisateur et le mot de passe sont obtenus à partir de votre passphrase. Vous devrez la saisir pour accéder à votre compte, alors gardez-la précieusement",
"next": "J'ai sauvegardé ma passphrase", "next": "J'ai sauvegardé ma passphrase",
"passphraseFrameLabel": "Pass phrase",
"title": "Votre passphrase" "title": "Votre passphrase"
}, },
"hasAccount": "Avez-vous déjà un compte? <0>Connectez-vous ici.</0>", "hasAccount": "Avez-vous déjà un compte? <0>Connectez-vous ici.</0>",
"login": { "login": {
"description": "Veuillez entrer votre passphrase pour vous connecter à votre compte", "description": "Veuillez fournir votre passphrase pour accéder à votre compte",
"deviceLengthError": "Veuillez saisir un nom d'appareil", "deviceLengthError": "Veuillez saisir un nom d'appareil",
"passphraseLabel": "Passphrase de 12 mots", "passphraseLabel": "Passphrase de 12 mots",
"passphrasePlaceholder": "Passphrase", "passphrasePlaceholder": "Passphrase",
@ -54,9 +55,9 @@
"text": "L'avez-vous configuré correctement ?", "text": "L'avez-vous configuré correctement ?",
"title": "Échec de la connexion au serveur" "title": "Échec de la connexion au serveur"
}, },
"host": "Vous vous connectez à <0>{{hostname}}</0> - veuillez confirmer que vous lui faites confiance avant de créer un compte.", "host": "Vous vous connectez à <0>{{hostname}}</0> - veuillez confirmer que vous lui faites confiance avant de créer un compte",
"no": "Retour", "no": "Retour",
"title": "Faites-vous confiance à ce serveur ?", "title": "Est-ce que vous avez confiance à ce serveur?",
"yes": "Je fais confiance à ce serveur" "yes": "Je fais confiance à ce serveur"
}, },
"verify": { "verify": {

View File

@ -27,6 +27,7 @@
"generate": { "generate": {
"description": "ביטוי הסיסמה שלך משמש כשם המשתמש והסיסמה שלך. אנא הקפד לשמור אותו בטוח מכיוון שתצטרך להזין אותו כדי להתחבר לחשבון שלך", "description": "ביטוי הסיסמה שלך משמש כשם המשתמש והסיסמה שלך. אנא הקפד לשמור אותו בטוח מכיוון שתצטרך להזין אותו כדי להתחבר לחשבון שלך",
"next": "אני שמרתי את משפט הסיסמה שלי", "next": "אני שמרתי את משפט הסיסמה שלי",
"passphraseFrameLabel": "ביטוי סיסמה",
"title": "משפט הסיסמה שלך" "title": "משפט הסיסמה שלך"
}, },
"hasAccount": "כבר יש לך חשבון? <0>התחבר כאן.</0>", "hasAccount": "כבר יש לך חשבון? <0>התחבר כאן.</0>",

View File

@ -22,7 +22,7 @@ export function getAuthHeaders(token: string): Record<string, string> {
export async function accountLogin( export async function accountLogin(
url: string, url: string,
id: string, id: string,
deviceName: string deviceName: string,
): Promise<LoginResponse> { ): Promise<LoginResponse> {
return ofetch<LoginResponse>("/auth/login", { return ofetch<LoginResponse>("/auth/login", {
method: "POST", method: "POST",

View File

@ -19,7 +19,7 @@ export interface BookmarkInput {
export function bookmarkMediaToInput( export function bookmarkMediaToInput(
tmdbId: string, tmdbId: string,
item: BookmarkMediaItem item: BookmarkMediaItem,
): BookmarkInput { ): BookmarkInput {
return { return {
meta: { meta: {
@ -35,7 +35,7 @@ export function bookmarkMediaToInput(
export async function addBookmark( export async function addBookmark(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
input: BookmarkInput input: BookmarkInput,
) { ) {
return ofetch<BookmarkResponse>( return ofetch<BookmarkResponse>(
`/users/${account.userId}/bookmarks/${input.tmdbId}`, `/users/${account.userId}/bookmarks/${input.tmdbId}`,
@ -44,14 +44,14 @@ export async function addBookmark(
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseURL: url,
body: input, body: input,
} },
); );
} }
export async function removeBookmark( export async function removeBookmark(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
id: string id: string,
) { ) {
return ofetch<{ tmdbId: string }>( return ofetch<{ tmdbId: string }>(
`/users/${account.userId}/bookmarks/${id}`, `/users/${account.userId}/bookmarks/${id}`,
@ -59,6 +59,6 @@ export async function removeBookmark(
method: "DELETE", method: "DELETE",
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseURL: url,
} },
); );
} }

View File

@ -41,7 +41,7 @@ export function genMnemonic(): string {
export async function signCode( export async function signCode(
code: string, code: string,
privateKey: Uint8Array privateKey: Uint8Array,
): Promise<Uint8Array> { ): Promise<Uint8Array> {
return forge.pki.ed25519.sign({ return forge.pki.ed25519.sign({
encoding: "utf8", encoding: "utf8",
@ -91,7 +91,7 @@ export async function encryptData(data: string, secret: Uint8Array) {
const cipher = forge.cipher.createCipher( const cipher = forge.cipher.createCipher(
"AES-GCM", "AES-GCM",
forge.util.createBuffer(secret) forge.util.createBuffer(secret),
); );
cipher.start({ cipher.start({
iv, iv,
@ -104,7 +104,7 @@ export async function encryptData(data: string, secret: Uint8Array) {
const tag = cipher.mode.tag; const tag = cipher.mode.tag;
return `${forge.util.encode64(iv)}.${stringBufferToBase64( return `${forge.util.encode64(iv)}.${stringBufferToBase64(
encryptedData encryptedData,
)}.${stringBufferToBase64(tag)}` as const; )}.${stringBufferToBase64(tag)}` as const;
} }
@ -115,7 +115,7 @@ export function decryptData(data: string, secret: Uint8Array) {
const decipher = forge.cipher.createDecipher( const decipher = forge.cipher.createDecipher(
"AES-GCM", "AES-GCM",
forge.util.createBuffer(secret) forge.util.createBuffer(secret),
); );
decipher.start({ decipher.start({
iv: base64ToStringBuffer(iv), iv: base64ToStringBuffer(iv),

View File

@ -9,7 +9,7 @@ import { ProgressInput } from "./progress";
export function importProgress( export function importProgress(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
progressItems: ProgressInput[] progressItems: ProgressInput[],
) { ) {
return ofetch<void>(`/users/${account.userId}/progress/import`, { return ofetch<void>(`/users/${account.userId}/progress/import`, {
method: "PUT", method: "PUT",
@ -22,7 +22,7 @@ export function importProgress(
export function importBookmarks( export function importBookmarks(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
bookmarks: BookmarkInput[] bookmarks: BookmarkInput[],
) { ) {
return ofetch<void>(`/users/${account.userId}/bookmarks`, { return ofetch<void>(`/users/${account.userId}/bookmarks`, {
method: "PUT", method: "PUT",

View File

@ -8,7 +8,7 @@ export interface ChallengeTokenResponse {
export async function getLoginChallengeToken( export async function getLoginChallengeToken(
url: string, url: string,
publicKey: string publicKey: string,
): Promise<ChallengeTokenResponse> { ): Promise<ChallengeTokenResponse> {
return ofetch<ChallengeTokenResponse>("/auth/login/start", { return ofetch<ChallengeTokenResponse>("/auth/login/start", {
method: "POST", method: "POST",
@ -35,7 +35,7 @@ export interface LoginInput {
export async function loginAccount( export async function loginAccount(
url: string, url: string,
data: LoginInput data: LoginInput,
): Promise<LoginResponse> { ): Promise<LoginResponse> {
return ofetch<LoginResponse>("/auth/login/complete", { return ofetch<LoginResponse>("/auth/login/complete", {
method: "POST", method: "POST",

View File

@ -23,7 +23,7 @@ export interface ProgressInput {
} }
export function progressUpdateItemToInput( export function progressUpdateItemToInput(
item: ProgressUpdateItem item: ProgressUpdateItem,
): ProgressInput { ): ProgressInput {
return { return {
duration: item.progress?.duration ?? 0, duration: item.progress?.duration ?? 0,
@ -44,7 +44,7 @@ export function progressUpdateItemToInput(
export function progressMediaItemToInputs( export function progressMediaItemToInputs(
tmdbId: string, tmdbId: string,
item: ProgressMediaItem item: ProgressMediaItem,
): ProgressInput[] { ): ProgressInput[] {
if (item.type === "show") { if (item.type === "show") {
return Object.entries(item.episodes).flatMap(([_, episode]) => ({ return Object.entries(item.episodes).flatMap(([_, episode]) => ({
@ -83,7 +83,7 @@ export function progressMediaItemToInputs(
export async function setProgress( export async function setProgress(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
input: ProgressInput input: ProgressInput,
) { ) {
return ofetch<ProgressResponse>( return ofetch<ProgressResponse>(
`/users/${account.userId}/progress/${input.tmdbId}`, `/users/${account.userId}/progress/${input.tmdbId}`,
@ -92,7 +92,7 @@ export async function setProgress(
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
baseURL: url, baseURL: url,
body: input, body: input,
} },
); );
} }
@ -101,7 +101,7 @@ export async function removeProgress(
account: AccountWithToken, account: AccountWithToken,
id: string, id: string,
episodeId?: string, episodeId?: string,
seasonId?: string seasonId?: string,
) { ) {
await ofetch(`/users/${account.userId}/progress/${id}`, { await ofetch(`/users/${account.userId}/progress/${id}`, {
method: "DELETE", method: "DELETE",

View File

@ -9,7 +9,7 @@ export interface ChallengeTokenResponse {
export async function getRegisterChallengeToken( export async function getRegisterChallengeToken(
url: string, url: string,
captchaToken?: string captchaToken?: string,
): Promise<ChallengeTokenResponse> { ): Promise<ChallengeTokenResponse> {
return ofetch<ChallengeTokenResponse>("/auth/register/start", { return ofetch<ChallengeTokenResponse>("/auth/register/start", {
method: "POST", method: "POST",
@ -42,7 +42,7 @@ export interface RegisterInput {
export async function registerAccount( export async function registerAccount(
url: string, url: string,
data: RegisterInput data: RegisterInput,
): Promise<RegisterResponse> { ): Promise<RegisterResponse> {
return ofetch<RegisterResponse>("/auth/register/complete", { return ofetch<RegisterResponse>("/auth/register/complete", {
method: "POST", method: "POST",

View File

@ -26,7 +26,7 @@ export async function getSessions(url: string, account: AccountWithToken) {
export async function updateSession( export async function updateSession(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
update: SessionUpdate update: SessionUpdate,
) { ) {
return ofetch<SessionResponse[]>(`/sessions/${account.sessionId}`, { return ofetch<SessionResponse[]>(`/sessions/${account.sessionId}`, {
method: "PATCH", method: "PATCH",
@ -39,7 +39,7 @@ export async function updateSession(
export async function removeSession( export async function removeSession(
url: string, url: string,
token: string, token: string,
sessionId: string sessionId: string,
) { ) {
return ofetch<SessionResponse[]>(`/sessions/${sessionId}`, { return ofetch<SessionResponse[]>(`/sessions/${sessionId}`, {
method: "DELETE", method: "DELETE",

View File

@ -18,7 +18,7 @@ export interface SettingsResponse {
export function updateSettings( export function updateSettings(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
settings: SettingsInput settings: SettingsInput,
) { ) {
return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, { return ofetch<SettingsResponse>(`/users/${account.userId}/settings`, {
method: "PUT", method: "PUT",

View File

@ -119,21 +119,21 @@ export function progressResponsesToEntries(responses: ProgressResponse[]) {
export async function getUser( export async function getUser(
url: string, url: string,
token: string token: string,
): Promise<{ user: UserResponse; session: SessionResponse }> { ): Promise<{ user: UserResponse; session: SessionResponse }> {
return ofetch<{ user: UserResponse; session: SessionResponse }>( return ofetch<{ user: UserResponse; session: SessionResponse }>(
"/users/@me", "/users/@me",
{ {
headers: getAuthHeaders(token), headers: getAuthHeaders(token),
baseURL: url, baseURL: url,
} },
); );
} }
export async function editUser( export async function editUser(
url: string, url: string,
account: AccountWithToken, account: AccountWithToken,
object: UserEdit object: UserEdit,
): Promise<{ user: UserResponse; session: SessionResponse }> { ): Promise<{ user: UserResponse; session: SessionResponse }> {
return ofetch<{ user: UserResponse; session: SessionResponse }>( return ofetch<{ user: UserResponse; session: SessionResponse }>(
`/users/${account.userId}`, `/users/${account.userId}`,
@ -142,13 +142,13 @@ export async function editUser(
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),
body: object, body: object,
baseURL: url, baseURL: url,
} },
); );
} }
export async function deleteUser( export async function deleteUser(
url: string, url: string,
account: AccountWithToken account: AccountWithToken,
): Promise<UserResponse> { ): Promise<UserResponse> {
return ofetch<UserResponse>(`/users/${account.userId}`, { return ofetch<UserResponse>(`/users/${account.userId}`, {
headers: getAuthHeaders(account.token), headers: getAuthHeaders(account.token),

View File

@ -25,7 +25,7 @@ export function mwFetch<T>(url: string, ops: P<T>[1] = {}): R<T> {
export async function singularProxiedFetch<T>( export async function singularProxiedFetch<T>(
proxyUrl: string, proxyUrl: string,
url: string, url: string,
ops: P<T>[1] = {} ops: P<T>[1] = {},
): R<T> { ): R<T> {
let combinedUrl = ops?.baseURL ?? ""; let combinedUrl = ops?.baseURL ?? "";
if ( if (

View File

@ -94,9 +94,14 @@ export async function getApiToken(): Promise<string | null> {
return apiToken; return apiToken;
} }
function parseEventInput(inp: string): any {
if (inp.length === 0) return {};
return JSON.parse(inp);
}
export async function connectServerSideEvents<T>( export async function connectServerSideEvents<T>(
url: string, url: string,
endEvents: string[] endEvents: string[],
) { ) {
const apiToken = await getApiToken(); const apiToken = await getApiToken();
@ -115,12 +120,12 @@ export async function connectServerSideEvents<T>(
endEvents.forEach((evt) => { endEvents.forEach((evt) => {
eventSource.addEventListener(evt, (e) => { eventSource.addEventListener(evt, (e) => {
eventSource.close(); eventSource.close();
promResolve(JSON.parse(e.data)); promResolve(parseEventInput(e.data));
}); });
}); });
eventSource.addEventListener("token", (e) => { eventSource.addEventListener("token", (e) => {
setApiToken(JSON.parse(e.data)); setApiToken(parseEventInput(e.data));
}); });
eventSource.addEventListener("error", (err: MessageEvent<any>) => { eventSource.addEventListener("error", (err: MessageEvent<any>) => {

View File

@ -58,7 +58,7 @@ export function scrapeSourceOutputToProviderMetric(
providerId: string, providerId: string,
embedId: string | null, embedId: string | null,
status: ProviderMetric["status"], status: ProviderMetric["status"],
err: unknown | null err: unknown | null,
): ProviderMetric { ): ProviderMetric {
const episodeId = media.episode?.tmdbId; const episodeId = media.episode?.tmdbId;
const seasonId = media.season?.tmdbId; const seasonId = media.season?.tmdbId;
@ -82,7 +82,7 @@ export function scrapeSourceOutputToProviderMetric(
export function scrapeSegmentToProviderMetric( export function scrapeSegmentToProviderMetric(
media: ScrapeMedia, media: ScrapeMedia,
providerId: string, providerId: string,
segment: ScrapingSegment segment: ScrapingSegment,
): ProviderMetric | null { ): ProviderMetric | null {
const status = segmentStatusMap[segment.status]; const status = segmentStatusMap[segment.status];
if (!status) return null; if (!status) return null;
@ -112,7 +112,7 @@ export function scrapeSegmentToProviderMetric(
export function scrapePartsToProviderMetric( export function scrapePartsToProviderMetric(
media: ScrapeMedia, media: ScrapeMedia,
order: ScrapingItems[], order: ScrapingItems[],
sources: Record<string, ScrapingSegment> sources: Record<string, ScrapingSegment>,
): ProviderMetric[] { ): ProviderMetric[] {
const output: ProviderMetric[] = []; const output: ProviderMetric[] = [];

View File

@ -14,7 +14,7 @@ const expirySeconds = 24 * 60 * 60;
* Always returns SRT * Always returns SRT
*/ */
export async function downloadCaption( export async function downloadCaption(
caption: CaptionListItem caption: CaptionListItem,
): Promise<string> { ): Promise<string> {
const cached = downloadCache.get(caption.url); const cached = downloadCache.get(caption.url);
if (cached) return cached; if (cached) return cached;

View File

@ -34,7 +34,7 @@ export interface DetailedMeta {
export function formatTMDBMetaResult( export function formatTMDBMetaResult(
details: TMDBShowData | TMDBMovieData, details: TMDBShowData | TMDBMovieData,
type: MWMediaType type: MWMediaType,
): TMDBMediaResult { ): TMDBMediaResult {
if (type === MWMediaType.MOVIE) { if (type === MWMediaType.MOVIE) {
const movie = details as TMDBMovieData; const movie = details as TMDBMovieData;
@ -68,7 +68,7 @@ export function formatTMDBMetaResult(
export async function getMetaFromId( export async function getMetaFromId(
type: MWMediaType, type: MWMediaType,
id: string, id: string,
seasonId?: string seasonId?: string,
): Promise<DetailedMeta | null> { ): Promise<DetailedMeta | null> {
const details = await getMediaDetails(id, mediaTypeToTMDB(type)); const details = await getMediaDetails(id, mediaTypeToTMDB(type));
@ -89,7 +89,7 @@ export async function getMetaFromId(
if (selectedSeason) { if (selectedSeason) {
const episodes = await getEpisodes( const episodes = await getEpisodes(
details.id.toString(), details.id.toString(),
selectedSeason.season_number selectedSeason.season_number,
); );
seasonData = { seasonData = {
@ -116,7 +116,7 @@ export async function getMetaFromId(
export async function getLegacyMetaFromId( export async function getLegacyMetaFromId(
type: MWMediaType, type: MWMediaType,
id: string, id: string,
seasonId?: string seasonId?: string,
): Promise<DetailedMeta | null> { ): Promise<DetailedMeta | null> {
const queryType = mediaTypeToJW(type); const queryType = mediaTypeToJW(type);
@ -135,15 +135,13 @@ export async function getLegacyMetaFromId(
throw err; throw err;
} }
let imdbId = data.external_ids.find( let imdbId = data.external_ids.find((v) => v.provider === "imdb_latest")
(v) => v.provider === "imdb_latest" ?.external_id;
)?.external_id;
if (!imdbId) if (!imdbId)
imdbId = data.external_ids.find((v) => v.provider === "imdb")?.external_id; imdbId = data.external_ids.find((v) => v.provider === "imdb")?.external_id;
let tmdbId = data.external_ids.find( let tmdbId = data.external_ids.find((v) => v.provider === "tmdb_latest")
(v) => v.provider === "tmdb_latest" ?.external_id;
)?.external_id;
if (!tmdbId) if (!tmdbId)
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id; tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
@ -175,7 +173,7 @@ export function isLegacyMediaType(url: string): boolean {
} }
export async function convertLegacyUrl( export async function convertLegacyUrl(
url: string url: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
if (!isLegacyUrl(url)) return undefined; if (!isLegacyUrl(url)) return undefined;
@ -191,7 +189,7 @@ export async function convertLegacyUrl(
return `/media/${TMDBIdToUrlId( return `/media/${TMDBIdToUrlId(
MWMediaType.SERIES, MWMediaType.SERIES,
details.id.toString(), details.id.toString(),
details.name details.name,
)}${suffix}`; )}${suffix}`;
} }

View File

@ -20,7 +20,7 @@ export function JWMediaToMediaType(type: string): MWMediaType {
export function formatJWMeta( export function formatJWMeta(
media: JWMediaResult, media: JWMediaResult,
season?: JWSeasonMetaResult season?: JWSeasonMetaResult,
): MWMediaMeta { ): MWMediaMeta {
const type = JWMediaToMediaType(media.object_type); const type = JWMediaToMediaType(media.object_type);
let seasons: undefined | MWSeasonMeta[]; let seasons: undefined | MWSeasonMeta[];
@ -32,7 +32,7 @@ export function formatJWMeta(
id: v.id.toString(), id: v.id.toString(),
number: v.season_number, number: v.season_number,
title: v.title, title: v.title,
}) }),
); );
} }
@ -67,7 +67,7 @@ export function JWMediaToId(media: MWMediaMeta): string {
} }
export function decodeJWId( export function decodeJWId(
paramId: string paramId: string,
): { id: string; type: MWMediaType } | null { ): { id: string; type: MWMediaType } | null {
const [prefix, type, id] = paramId.split("-", 3); const [prefix, type, id] = paramId.split("-", 3);
if (prefix !== "JW") return null; if (prefix !== "JW") return null;

View File

@ -38,7 +38,7 @@ export function TMDBMediaToMediaType(type: TMDBContentTypes): MWMediaType {
} }
export function TMDBMediaToMediaItemType( export function TMDBMediaToMediaItemType(
type: TMDBContentTypes type: TMDBContentTypes,
): MediaItem["type"] { ): MediaItem["type"] {
if (type === TMDBContentTypes.MOVIE) return "movie"; if (type === TMDBContentTypes.MOVIE) return "movie";
if (type === TMDBContentTypes.TV) return "show"; if (type === TMDBContentTypes.TV) return "show";
@ -47,7 +47,7 @@ export function TMDBMediaToMediaItemType(
export function formatTMDBMeta( export function formatTMDBMeta(
media: TMDBMediaResult, media: TMDBMediaResult,
season?: TMDBSeasonMetaResult season?: TMDBSeasonMetaResult,
): MWMediaMeta { ): MWMediaMeta {
const type = TMDBMediaToMediaType(media.object_type); const type = TMDBMediaToMediaType(media.object_type);
let seasons: undefined | MWSeasonMeta[]; let seasons: undefined | MWSeasonMeta[];
@ -59,7 +59,7 @@ export function formatTMDBMeta(
title: v.title, title: v.title,
id: v.id.toString(), id: v.id.toString(),
number: v.season_number, number: v.season_number,
}) }),
); );
} }
@ -102,7 +102,7 @@ export function formatTMDBMetaToMediaItem(media: TMDBMediaResult): MediaItem {
export function TMDBIdToUrlId( export function TMDBIdToUrlId(
type: MWMediaType, type: MWMediaType,
tmdbId: string, tmdbId: string,
title: string title: string,
) { ) {
return [ return [
"tmdb", "tmdb",
@ -120,12 +120,12 @@ export function mediaItemToId(media: MediaItem): string {
return TMDBIdToUrlId( return TMDBIdToUrlId(
mediaItemTypeToMediaType(media.type), mediaItemTypeToMediaType(media.type),
media.id, media.id,
media.title media.title,
); );
} }
export function decodeTMDBId( export function decodeTMDBId(
paramId: string paramId: string,
): { id: string; type: MWMediaType } | null { ): { id: string; type: MWMediaType } | null {
const [prefix, type, id] = paramId.split("-", 3); const [prefix, type, id] = paramId.split("-", 3);
if (prefix !== "tmdb") return null; if (prefix !== "tmdb") return null;
@ -160,7 +160,7 @@ async function get<T>(url: string, params?: object): Promise<T> {
} }
export async function multiSearch( export async function multiSearch(
query: string query: string,
): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> { ): Promise<(TMDBMovieSearchResult | TMDBShowSearchResult)[]> {
const data = await get<TMDBSearchResult>("search/multi", { const data = await get<TMDBSearchResult>("search/multi", {
query, query,
@ -172,13 +172,13 @@ export async function multiSearch(
const results = data.results.filter( const results = data.results.filter(
(r) => (r) =>
r.media_type === TMDBContentTypes.MOVIE || r.media_type === TMDBContentTypes.MOVIE ||
r.media_type === TMDBContentTypes.TV r.media_type === TMDBContentTypes.TV,
); );
return results; return results;
} }
export async function generateQuickSearchMediaUrl( export async function generateQuickSearchMediaUrl(
query: string query: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
const data = await multiSearch(query); const data = await multiSearch(query);
if (data.length === 0) return undefined; if (data.length === 0) return undefined;
@ -189,7 +189,7 @@ export async function generateQuickSearchMediaUrl(
return `/media/${TMDBIdToUrlId( return `/media/${TMDBIdToUrlId(
TMDBMediaToMediaType(result.media_type), TMDBMediaToMediaType(result.media_type),
result.id.toString(), result.id.toString(),
title title,
)}`; )}`;
} }
@ -203,7 +203,7 @@ type MediaDetailReturn<T extends TMDBContentTypes> =
export function getMediaDetails< export function getMediaDetails<
T extends TMDBContentTypes, T extends TMDBContentTypes,
TReturn = MediaDetailReturn<T> TReturn = MediaDetailReturn<T>,
>(id: string, type: T): Promise<TReturn> { >(id: string, type: T): Promise<TReturn> {
if (type === TMDBContentTypes.MOVIE) { if (type === TMDBContentTypes.MOVIE) {
return get<TReturn>(`/movie/${id}`, { append_to_response: "external_ids" }); return get<TReturn>(`/movie/${id}`, { append_to_response: "external_ids" });
@ -215,12 +215,12 @@ export function getMediaDetails<
} }
export function getMediaPoster(posterPath: string | null): string | undefined { export function getMediaPoster(posterPath: string | null): string | undefined {
if (posterPath) return `https://image.tmdb.org/t/p/w185/${posterPath}`; if (posterPath) return `https://image.tmdb.org/t/p/w342/${posterPath}`;
} }
export async function getEpisodes( export async function getEpisodes(
id: string, id: string,
season: number season: number,
): Promise<TMDBEpisodeShort[]> { ): Promise<TMDBEpisodeShort[]> {
const data = await get<TMDBSeason>(`/tv/${id}/season/${season}`); const data = await get<TMDBSeason>(`/tv/${id}/season/${season}`);
return data.episodes.map((e) => ({ return data.episodes.map((e) => ({
@ -231,7 +231,7 @@ export async function getEpisodes(
} }
export async function getMovieFromExternalId( export async function getMovieFromExternalId(
imdbId: string imdbId: string,
): Promise<string | undefined> { ): Promise<string | undefined> {
const data = await get<ExternalIdMovieSearchResult>(`/find/${imdbId}`, { const data = await get<ExternalIdMovieSearchResult>(`/find/${imdbId}`, {
external_source: "imdb_id", external_source: "imdb_id",
@ -245,7 +245,7 @@ export async function getMovieFromExternalId(
export function formatTMDBSearchResult( export function formatTMDBSearchResult(
result: TMDBMovieSearchResult | TMDBShowSearchResult, result: TMDBMovieSearchResult | TMDBShowSearchResult,
mediatype: TMDBContentTypes mediatype: TMDBContentTypes,
): TMDBMediaResult { ): TMDBMediaResult {
const type = TMDBMediaToMediaType(mediatype); const type = TMDBMediaToMediaType(mediatype);
if (type === MWMediaType.SERIES) { if (type === MWMediaType.SERIES) {

View File

@ -20,7 +20,7 @@ export function Avatar(props: AvatarProps) {
<div <div
className={classNames( className={classNames(
props.sizeClass, props.sizeClass,
"rounded-full overflow-hidden flex items-center justify-center text-white" "rounded-full overflow-hidden flex items-center justify-center text-white",
)} )}
style={{ style={{
background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`, background: `linear-gradient(to bottom right, ${props.profile.colorA}, ${props.profile.colorB})`,
@ -53,7 +53,7 @@ export function UserAvatar(props: {
auth.account && auth.account.seed auth.account && auth.account.seed
? base64ToBuffer(auth.account.seed) ? base64ToBuffer(auth.account.seed)
: null, : null,
[auth] [auth],
); );
if (!auth.account || auth.account === null) return null; if (!auth.account || auth.account === null) return null;

View File

@ -51,7 +51,7 @@ export function FlagIcon(props: FlagIconProps) {
<span <span
className={classNames( className={classNames(
"!w-8 h-6 rounded overflow-hidden bg-video-context-flagBg bg-cover bg-center block fi", "!w-8 h-6 rounded overflow-hidden bg-video-context-flagBg bg-cover bg-center block fi",
props.countryCode ? `fi-${countryCode}` : undefined props.countryCode ? `fi-${countryCode}` : undefined,
)} )}
/> />
); );

View File

@ -1,7 +1,7 @@
import classNames from "classnames"; import classNames from "classnames";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto"; import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
import { UserAvatar } from "@/components/Avatar"; import { UserAvatar } from "@/components/Avatar";
@ -21,11 +21,11 @@ function GoToLink(props: {
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
}) { }) {
const history = useHistory(); const navigate = useNavigate();
const goTo = (href: string) => { const goTo = (href: string) => {
if (href.startsWith("http")) window.open(href, "_blank"); if (href.startsWith("http")) window.open(href, "_blank");
else history.push(href); else navigate(href);
}; };
return ( return (
@ -61,7 +61,7 @@ function DropdownLink(props: {
props.highlight props.highlight
? "text-dropdown-highlight hover:text-dropdown-highlightHover" ? "text-dropdown-highlight hover:text-dropdown-highlightHover"
: "text-dropdown-text hover:text-white", : "text-dropdown-text hover:text-white",
props.className props.className,
)} )}
> >
{props.icon ? <Icon icon={props.icon} className="text-xl" /> : null} {props.icon ? <Icon icon={props.icon} className="text-xl" /> : null}
@ -88,7 +88,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
const seed = useAuthStore((s) => s.account?.seed); const seed = useAuthStore((s) => s.account?.seed);
const bufferSeed = useMemo( const bufferSeed = useMemo(
() => (seed ? base64ToBuffer(seed) : null), () => (seed ? base64ToBuffer(seed) : null),
[seed] [seed],
); );
const { logout } = useAuth(); const { logout } = useAuth();
@ -118,7 +118,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
<Icon <Icon
className={classNames( className={classNames(
"text-xl transition-transform duration-100", "text-xl transition-transform duration-100",
open ? "rotate-180" : "" open ? "rotate-180" : "",
)} )}
icon={Icons.CHEVRON_DOWN} icon={Icons.CHEVRON_DOWN}
/> />

View File

@ -1,6 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import { ReactNode, useCallback } from "react"; import { ReactNode, useCallback } from "react";
import { useHistory } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { Spinner } from "@/components/layout/Spinner"; import { Spinner } from "@/components/layout/Spinner";
@ -19,13 +19,13 @@ interface Props {
} }
export function Button(props: Props) { export function Button(props: Props) {
const history = useHistory(); const navigate = useNavigate();
const { onClick, href, loading } = props; const { onClick, href, loading } = props;
const cb = useCallback(() => { const cb = useCallback(() => {
if (loading) return; if (loading) return;
if (href) history.push(href); if (href) navigate(href);
else onClick?.(); else onClick?.();
}, [onClick, href, history, loading]); }, [onClick, href, navigate, loading]);
let colorClasses = "bg-white hover:bg-gray-200 text-black"; let colorClasses = "bg-white hover:bg-gray-200 text-black";
if (props.theme === "purple") if (props.theme === "purple")
@ -41,7 +41,7 @@ export function Button(props: Props) {
props.padding ?? "px-4 py-3", props.padding ?? "px-4 py-3",
props.className, props.className,
colorClasses, colorClasses,
props.disabled ? "cursor-not-allowed bg-opacity-60 text-opacity-60" : null props.disabled ? "cursor-not-allowed bg-opacity-60 text-opacity-60" : null,
); );
if (props.disabled) if (props.disabled)
@ -49,7 +49,7 @@ export function Button(props: Props) {
.split(" ") .split(" ")
.filter( .filter(
(className) => (className) =>
!className.startsWith("hover:") && !className.startsWith("active:") !className.startsWith("hover:") && !className.startsWith("active:"),
) )
.join(" "); .join(" ");
@ -120,7 +120,7 @@ export function ButtonPlain(props: ButtonPlainProps) {
"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",
"px-4 py-3", "px-4 py-3",
props.className, props.className,
colorClasses colorClasses,
); );
return ( return (

View File

@ -7,14 +7,14 @@ export function Toggle(props: { onClick: () => void; enabled?: boolean }) {
onClick={props.onClick} onClick={props.onClick}
className={classNames( className={classNames(
"w-11 h-6 p-1 rounded-full grid transition-colors duration-100 group/toggle tabbable", "w-11 h-6 p-1 rounded-full grid transition-colors duration-100 group/toggle tabbable",
props.enabled ? "bg-buttons-toggle" : "bg-buttons-toggleDisabled" props.enabled ? "bg-buttons-toggle" : "bg-buttons-toggleDisabled",
)} )}
> >
<div className="relative w-full h-full"> <div className="relative w-full h-full">
<div <div
className={classNames( className={classNames(
"scale-90 group-hover/toggle:scale-100 h-full aspect-square rounded-full bg-white absolute transition-all duration-100", "scale-90 group-hover/toggle:scale-100 h-full aspect-square rounded-full bg-white absolute transition-all duration-100",
props.enabled ? "left-full transform -translate-x-full" : "left-0" props.enabled ? "left-full transform -translate-x-full" : "left-0",
)} )}
/> />
</div> </div>

View File

@ -24,7 +24,7 @@ export function ColorPicker(props: {
tabIndex={0} tabIndex={0}
className={classNames( className={classNames(
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer", "w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
props.value === color ? "border-white" : "border-transparent" props.value === color ? "border-white" : "border-transparent",
)} )}
onClick={() => props.onInput(color)} onClick={() => props.onInput(color)}
style={{ backgroundColor: color }} style={{ backgroundColor: color }}

View File

@ -32,7 +32,7 @@ export function IconPicker(props: {
"w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer", "w-full h-10 rounded flex justify-center items-center text-white pointer border-2 border-opacity-10 cursor-pointer",
props.value === icon props.value === icon
? "bg-buttons-purple border-white" ? "bg-buttons-purple border-white"
: "bg-authentication-inputBg border-transparent" : "bg-authentication-inputBg border-transparent",
)} )}
onClick={() => props.onInput(icon)} onClick={() => props.onInput(icon)}
key={icon} key={icon}

View File

@ -60,5 +60,5 @@ export const SearchBarInput = forwardRef<HTMLInputElement, SearchBarProps>(
</Flare.Child> </Flare.Child>
</Flare.Base> </Flare.Base>
); );
} },
); );

View File

@ -17,7 +17,7 @@ export function BrandPill(props: {
props.backgroundClass ?? "bg-pill-background bg-opacity-50", props.backgroundClass ?? "bg-pill-background bg-opacity-50",
props.clickable props.clickable
? "transition-[transform,background-color] hover:scale-105 hover:bg-pill-backgroundHover hover:text-type-logo active:scale-95" ? "transition-[transform,background-color] hover:scale-105 hover:bg-pill-backgroundHover hover:text-type-logo active:scale-95"
: "" : "",
)} )}
> >
<Icon className="text-xl" icon={Icons.MOVIE_WEB} /> <Icon className="text-xl" icon={Icons.MOVIE_WEB} />

View File

@ -1,6 +1,6 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import type { RequireExactlyOne } from "type-fest"; import type { RequireExactlyOne } from "type-fest";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
@ -21,13 +21,13 @@ type FooterLinkProps = RequireExactlyOne<
>; >;
function FooterLink(props: FooterLinkProps) { function FooterLink(props: FooterLinkProps) {
const history = useHistory(); const navigate = useNavigate();
const navigateTo = useCallback(() => { const navigateTo = useCallback(() => {
if (!props.to) return; if (!props.to) return;
history.push(props.to); navigate(props.to);
}, [history, props.to]); }, [navigate, props.to]);
return ( return (
<a <a
@ -99,7 +99,7 @@ export function FooterView(props: {
return ( return (
<div <div
className={["flex min-h-screen flex-col", props.className || ""].join( className={["flex min-h-screen flex-col", props.className || ""].join(
" " " ",
)} )}
> >
<div style={{ flex: "1 0 auto" }}>{props.children}</div> <div style={{ flex: "1 0 auto" }}>{props.children}</div>

View File

@ -51,7 +51,7 @@ export function Navigation(props: NavigationProps) {
"fixed left-0 right-0 h-20 flex items-center", "fixed left-0 right-0 h-20 flex items-center",
props.doBackground props.doBackground
? "bg-background-main border-b border-utils-divider border-opacity-50" ? "bg-background-main border-b border-utils-divider border-opacity-50"
: null : null,
)} )}
> >
{props.doBackground ? ( {props.doBackground ? (

View File

@ -10,7 +10,7 @@ export function SettingsCard(props: {
className={classNames( className={classNames(
"w-full rounded-lg bg-settings-card-background bg-opacity-[0.15] border border-settings-card-border", "w-full rounded-lg bg-settings-card-background bg-opacity-[0.15] border border-settings-card-border",
props.paddingClass ?? "px-8 py-6", props.paddingClass ?? "px-8 py-6",
props.className props.className,
)} )}
> >
{props.children} {props.children}
@ -28,7 +28,7 @@ export function SolidSettingsCard(props: {
className={classNames( className={classNames(
"w-full rounded-lg bg-settings-card-altBackground bg-opacity-50", "w-full rounded-lg bg-settings-card-altBackground bg-opacity-50",
props.paddingClass ?? "px-8 py-6", props.paddingClass ?? "px-8 py-6",
props.className props.className,
)} )}
> >
{props.children} {props.children}

View File

@ -31,13 +31,13 @@ export function SidebarLink(props: {
"tabbable w-full px-3 py-2 flex items-center space-x-3 cursor-pointer rounded my-2", "tabbable w-full px-3 py-2 flex items-center space-x-3 cursor-pointer rounded my-2",
props.active props.active
? "bg-settings-sidebar-activeLink text-settings-sidebar-type-activated" ? "bg-settings-sidebar-activeLink text-settings-sidebar-type-activated"
: null : null,
)} )}
> >
<Icon <Icon
className={classNames( className={classNames(
"text-2xl text-settings-sidebar-type-icon", "text-2xl text-settings-sidebar-type-icon",
props.active ? "text-settings-sidebar-type-iconActivated" : null props.active ? "text-settings-sidebar-type-iconActivated" : null,
)} )}
icon={props.icon} icon={props.icon}
/> />

View File

@ -66,7 +66,7 @@ function MediaCardContent({
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-100", "relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-100",
{ {
"group-hover:rounded-lg": !closable, "group-hover:rounded-lg": !closable,
} },
)} )}
style={{ style={{
backgroundImage: media.poster ? `url(${media.poster})` : undefined, backgroundImage: media.poster ? `url(${media.poster})` : undefined,
@ -152,7 +152,7 @@ export function MediaCard(props: MediaCardProps) {
link += `/${encodeURIComponent(props.series.seasonId)}`; link += `/${encodeURIComponent(props.series.seasonId)}`;
} else { } else {
link += `/${encodeURIComponent( link += `/${encodeURIComponent(
props.series.seasonId props.series.seasonId,
)}/${encodeURIComponent(props.series.episodeId)}`; )}/${encodeURIComponent(props.series.episodeId)}`;
} }
} }
@ -164,7 +164,7 @@ export function MediaCard(props: MediaCardProps) {
tabIndex={-1} tabIndex={-1}
className={classNames( className={classNames(
"tabbable", "tabbable",
props.closable ? "hover:cursor-default" : "" props.closable ? "hover:cursor-default" : "",
)} )}
> >
{content} {content}

View File

@ -14,5 +14,5 @@ export const MediaGrid = forwardRef<HTMLDivElement, MediaGridProps>(
{props.children} {props.children}
</div> </div>
); );
} },
); );

View File

@ -32,7 +32,7 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) {
}, [progressItems, props.media]); }, [progressItems, props.media]);
const itemToDisplay = useMemo( const itemToDisplay = useMemo(
() => (item ? shouldShowProgress(item) : null), () => (item ? shouldShowProgress(item) : null),
[item] [item],
); );
const percentage = itemToDisplay?.show const percentage = itemToDisplay?.show
? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100 ? (itemToDisplay.progress.watched / itemToDisplay.progress.duration) * 100

View File

@ -50,11 +50,7 @@ export function OverlayPortal(props: {
{portalElement {portalElement
? createPortal( ? createPortal(
<Transition show={props.show} animation="none"> <Transition show={props.show} animation="none">
<FocusTrap <FocusTrap>
focusTrapOptions={{
onDeactivate: close,
}}
>
<div className="popout-wrapper fixed overflow-hidden pointer-events-auto inset-0 z-[999] select-none"> <div className="popout-wrapper fixed overflow-hidden pointer-events-auto inset-0 z-[999] select-none">
<Transition animation="fade" isChild> <Transition animation="fade" isChild>
<div <div
@ -80,7 +76,7 @@ export function OverlayPortal(props: {
</div> </div>
</FocusTrap> </FocusTrap>
</Transition>, </Transition>,
portalElement portalElement,
) )
: null} : null}
</div> </div>

View File

@ -21,7 +21,7 @@ function RouterBase(props: { id: string; children: ReactNode }) {
const router = useInternalOverlayRouter(props.id); const router = useInternalOverlayRouter(props.id);
const routeMeta = useMemo( const routeMeta = useMemo(
() => routes[router.currentRoute ?? ""], () => routes[router.currentRoute ?? ""],
[routes, router] [routes, router],
); );
const [dimensions, api] = useSpring( const [dimensions, api] = useSpring(
@ -34,7 +34,7 @@ function RouterBase(props: { id: string; children: ReactNode }) {
easing: easings.linear, easing: easings.linear,
}, },
}), }),
[] [],
); );
const currentState = useRef<null | string>(null); const currentState = useRef<null | string>(null);

View File

@ -25,11 +25,11 @@ function useCalculatePositions() {
setLeft( setLeft(
Math.min( Math.min(
buttonCenter - card.width / 2, buttonCenter - card.width / 2,
window.innerWidth - card.width - 30 window.innerWidth - card.width - 30,
) ),
); );
}, },
[] [],
); );
useEffect(() => { useEffect(() => {

View File

@ -18,7 +18,7 @@ export function Chromecast(props: ChromecastProps) {
const isVisible = (tag.getAttribute("style") ?? "").includes("inline"); const isVisible = (tag.getAttribute("style") ?? "").includes("inline");
setHidden(!isVisible); setHidden(!isVisible);
}, },
[setHidden] [setHidden],
); );
useEffect(() => { useEffect(() => {

View File

@ -54,7 +54,7 @@ function SeasonsView({
const meta = usePlayerStore((s) => s.meta); const meta = usePlayerStore((s) => s.meta);
const [loadingState, seasons] = useSeasonData( const [loadingState, seasons] = useSeasonData(
meta?.tmdbId ?? "", meta?.tmdbId ?? "",
selectedSeason selectedSeason,
); );
let content: ReactNode = null; let content: ReactNode = null;
@ -120,7 +120,7 @@ function EpisodesView({
// player already switches route after meta change // player already switches route after meta change
router.close(true); router.close(true);
}, },
[setPlayerMeta, loadingState, router, onChange] [setPlayerMeta, loadingState, router, onChange],
); );
if (!meta?.tmdbId) return null; if (!meta?.tmdbId) return null;
@ -175,7 +175,7 @@ function EpisodesView({
"p-0.5 px-2 rounded inline bg-video-context-hoverColor", "p-0.5 px-2 rounded inline bg-video-context-hoverColor",
ep.id === meta?.episode?.tmdbId ep.id === meta?.episode?.tmdbId
? "text-white bg-opacity-100" ? "text-white bg-opacity-100"
: "bg-opacity-50" : "bg-opacity-50",
)} )}
> >
{t("player.menus.episodes.episodeBadge", { {t("player.menus.episodes.episodeBadge", {
@ -226,7 +226,7 @@ function EpisodesOverlay({
setSelectedSeason(seasonId); setSelectedSeason(seasonId);
router.navigate("/episodes"); router.navigate("/episodes");
}, },
[router] [router],
); );
return ( return (

View File

@ -10,7 +10,7 @@ import { usePlayerStore } from "@/stores/player/store";
function shouldShowNextEpisodeButton( function shouldShowNextEpisodeButton(
time: number, time: number,
duration: number duration: number,
): "always" | "hover" | "none" { ): "always" | "hover" | "none" {
const percentage = time / duration; const percentage = time / duration;
const secondsFromEnd = duration - time; const secondsFromEnd = duration - time;
@ -28,7 +28,7 @@ function Button(props: {
<button <button
className={classNames( className={classNames(
"font-bold rounded h-10 w-40 scale-95 hover:scale-100 transition-all duration-200", "font-bold rounded h-10 w-40 scale-95 hover:scale-100 transition-all duration-200",
props.className props.className,
)} )}
type="button" type="button"
onClick={props.onClick} onClick={props.onClick}
@ -53,7 +53,7 @@ export function NextEpisodeButton(props: {
const showingState = shouldShowNextEpisodeButton(time, duration); const showingState = shouldShowNextEpisodeButton(time, duration);
const status = usePlayerStore((s) => s.status); const status = usePlayerStore((s) => s.status);
const setShouldStartFromBeginning = usePlayerStore( const setShouldStartFromBeginning = usePlayerStore(
(s) => s.setShouldStartFromBeginning (s) => s.setShouldStartFromBeginning,
); );
let show = false; let show = false;
@ -69,7 +69,7 @@ export function NextEpisodeButton(props: {
: "bottom-[calc(3rem+env(safe-area-inset-bottom))]"; : "bottom-[calc(3rem+env(safe-area-inset-bottom))]";
const nextEp = meta?.episodes?.find( const nextEp = meta?.episodes?.find(
(v) => v.number === (meta?.episode?.number ?? 0) + 1 (v) => v.number === (meta?.episode?.number ?? 0) + 1,
); );
const loadNextEpisode = useCallback(() => { const loadNextEpisode = useCallback(() => {

View File

@ -58,7 +58,7 @@ function ThumbnailDisplay(props: { at: number; show: boolean }) {
<p className="text-center mt-1"> <p className="text-center mt-1">
{formatSeconds( {formatSeconds(
Math.max(props.at, 0), Math.max(props.at, 0),
durationExceedsHour(props.at) durationExceedsHour(props.at),
)} )}
</p> </p>
</div> </div>
@ -79,7 +79,7 @@ function useMouseHoverPosition(barRef: RefObject<HTMLDivElement>) {
const pos = (e.pageX - rect.left) / barRef.current.offsetWidth; const pos = (e.pageX - rect.left) / barRef.current.offsetWidth;
setMousePos(pos * 100); setMousePos(pos * 100);
}, },
[setMousePos, barRef] [setMousePos, barRef],
); );
const mouseLeave = useCallback(() => { const mouseLeave = useCallback(() => {
@ -97,10 +97,10 @@ export function ProgressBar() {
const { isSeeking } = usePlayerStore((s) => s.interface); const { isSeeking } = usePlayerStore((s) => s.interface);
const commitTime = useCallback( const commitTime = useCallback(
(percentage) => { (percentage: number) => {
display?.setTime(percentage * duration); display?.setTime(percentage * duration);
}, },
[duration, display] [duration, display],
); );
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -108,7 +108,7 @@ export function ProgressBar() {
const { dragging, dragPercentage, dragMouseDown } = useProgressBar( const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
ref, ref,
commitTime commitTime,
); );
useEffect(() => { useEffect(() => {
setSeeking(dragging); setSeeking(dragging);
@ -165,8 +165,8 @@ export function ProgressBar() {
0, 0,
Math.min( Math.min(
1, 1,
dragging ? dragPercentage / 100 : time / duration dragging ? dragPercentage / 100 : time / duration,
) ),
) * 100 ) * 100
}%`, }%`,
}} }}

View File

@ -22,19 +22,19 @@ export function Time(props: { short?: boolean }) {
setTimeFormat( setTimeFormat(
timeFormat === VideoPlayerTimeFormat.REGULAR timeFormat === VideoPlayerTimeFormat.REGULAR
? VideoPlayerTimeFormat.REMAINING ? VideoPlayerTimeFormat.REMAINING
: VideoPlayerTimeFormat.REGULAR : VideoPlayerTimeFormat.REGULAR,
); );
} }
const currentTime = Math.min( const currentTime = Math.min(
Math.max(isSeeking ? draggingTime : time, 0), Math.max(isSeeking ? draggingTime : time, 0),
timeDuration timeDuration,
); );
const secondsRemaining = Math.abs(currentTime - timeDuration); const secondsRemaining = Math.abs(currentTime - timeDuration);
const timeLeft = formatSeconds( const timeLeft = formatSeconds(
secondsRemaining, secondsRemaining,
durationExceedsHour(secondsRemaining) durationExceedsHour(secondsRemaining),
); );
const timeWatched = formatSeconds(currentTime, hasHours); const timeWatched = formatSeconds(currentTime, hasHours);
const timeFinished = new Date(Date.now() + secondsRemaining * 1e3); const timeFinished = new Date(Date.now() + secondsRemaining * 1e3);

View File

@ -23,16 +23,16 @@ export function Volume(props: Props) {
const { setVolume, toggleMute } = useVolume(); const { setVolume, toggleMute } = useVolume();
const commitVolume = useCallback( const commitVolume = useCallback(
(percentage) => { (percentage: number) => {
setVolume(percentage); setVolume(percentage);
}, },
[setVolume] [setVolume],
); );
const { dragging, dragPercentage, dragMouseDown } = useProgressBar( const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
ref, ref,
commitVolume, commitVolume,
true true,
); );
const handleClick = useCallback(() => { const handleClick = useCallback(() => {

View File

@ -19,7 +19,7 @@ export function ColorOption(props: {
type="button" type="button"
className={classNames( className={classNames(
"tabbable p-1.5 bg-video-context-buttonFocus rounded transition-colors duration-100", "tabbable p-1.5 bg-video-context-buttonFocus rounded transition-colors duration-100",
props.active ? "bg-opacity-100" : "bg-opacity-0 cursor-pointer" props.active ? "bg-opacity-100" : "bg-opacity-0 cursor-pointer",
)} )}
onClick={props.onClick} onClick={props.onClick}
> >
@ -50,18 +50,18 @@ export function CaptionSetting(props: {
const currentPercentage = (props.value - props.min) / (props.max - props.min); const currentPercentage = (props.value - props.min) / (props.max - props.min);
const commit = useCallback( const commit = useCallback(
(percentage) => { (percentage: number) => {
const range = props.max - props.min; const range = props.max - props.min;
const newPercentage = Math.min(Math.max(percentage, 0), 1); const newPercentage = Math.min(Math.max(percentage, 0), 1);
props.onChange?.(props.min + range * newPercentage); props.onChange?.(props.min + range * newPercentage);
}, },
[props] [props],
); );
const { dragging, dragPercentage, dragMouseDown } = useProgressBar( const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
ref, ref,
commit, commit,
true true,
); );
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
@ -112,8 +112,8 @@ export function CaptionSetting(props: {
0, 0,
Math.min( Math.min(
1, 1,
dragging ? dragPercentage / 100 : currentPercentage dragging ? dragPercentage / 100 : currentPercentage,
) ),
) * 100 ) * 100
}%`, }%`,
}} }}
@ -141,7 +141,7 @@ export function CaptionSetting(props: {
const num = Number((e.target as HTMLInputElement).value); const num = Number((e.target as HTMLInputElement).value);
if (!Number.isNaN(num)) if (!Number.isNaN(num))
props.onChange?.( props.onChange?.(
(props.decimalsAllowed ?? 0) === 0 ? Math.round(num) : num (props.decimalsAllowed ?? 0) === 0 ? Math.round(num) : num,
); );
}} }}
ref={inputRef} ref={inputRef}
@ -163,13 +163,13 @@ export function CaptionSetting(props: {
<button <button
className={classNames( className={classNames(
inputClasses, inputClasses,
props.controlButtons ? "relative" : undefined props.controlButtons ? "relative" : undefined,
)} )}
type="button" type="button"
tabIndex={0} tabIndex={0}
> >
{textTransformer( {textTransformer(
props.value.toFixed(props.decimalsAllowed ?? 0) props.value.toFixed(props.decimalsAllowed ?? 0),
)} )}
</button> </button>
{props.controlButtons ? ( {props.controlButtons ? (
@ -180,7 +180,8 @@ export function CaptionSetting(props: {
onClick={ onClick={
() => () =>
props.onChange?.( props.onChange?.(
props.value - 1 / 10 ** (props.decimalsAllowed ?? 0) props.value -
1 / 10 ** (props.decimalsAllowed ?? 0),
) // Remove depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc. ) // Remove depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc.
} }
className={arrowButtonClasses} className={arrowButtonClasses}
@ -194,7 +195,8 @@ export function CaptionSetting(props: {
onClick={ onClick={
() => () =>
props.onChange?.( props.onChange?.(
props.value + 1 / 10 ** (props.decimalsAllowed ?? 0) props.value +
1 / 10 ** (props.decimalsAllowed ?? 0),
) // Add depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc. ) // Add depending on the decimalsAllowed. If there's 1 decimal allowed, add 0.1. For 2, add 0.01, etc.
} }
className={arrowButtonClasses} className={arrowButtonClasses}

View File

@ -127,7 +127,7 @@ export function CaptionsView({ id }: { id: string }) {
setCurrentlyDownloading(language); setCurrentlyDownloading(language);
return selectLanguage(language); return selectLanguage(language);
}, },
[selectLanguage, setCurrentlyDownloading] [selectLanguage, setCurrentlyDownloading],
); );
const content = subtitleList.map((v, i) => { const content = subtitleList.map((v, i) => {
@ -141,7 +141,7 @@ export function CaptionsView({ id }: { id: string }) {
loading={v.language === currentlyDownloading && downloadReq.loading} loading={v.language === currentlyDownloading && downloadReq.loading}
error={ error={
v.language === currentlyDownloading && downloadReq.error v.language === currentlyDownloading && downloadReq.error
? downloadReq.error ? downloadReq.error.toString()
: undefined : undefined
} }
onClick={() => startDownload(v.language)} onClick={() => startDownload(v.language)}
@ -182,3 +182,5 @@ export function CaptionsView({ id }: { id: string }) {
</> </>
); );
} }
export default CaptionsView;

View File

@ -48,7 +48,7 @@ export function DownloadView({ id }: { id: string }) {
selectedCaption selectedCaption
? convertSubtitlesToSrtDataurl(selectedCaption?.srtData) ? convertSubtitlesToSrtDataurl(selectedCaption?.srtData)
: null, : null,
[selectedCaption] [selectedCaption],
); );
if (!downloadUrl) return null; if (!downloadUrl) return null;

View File

@ -21,7 +21,7 @@ function ButtonList(props: {
"w-full px-2 py-1 rounded-md tabbable", "w-full px-2 py-1 rounded-md tabbable",
props.selected === option props.selected === option
? "bg-video-context-buttons-active text-white" ? "bg-video-context-buttons-active text-white"
: null : null,
)} )}
onClick={() => props.onClick(option)} onClick={() => props.onClick(option)}
key={option} key={option}
@ -44,7 +44,7 @@ export function PlaybackSettingsView({ id }: { id: string }) {
(v: number) => { (v: number) => {
display?.setPlaybackRate(v); display?.setPlaybackRate(v);
}, },
[display] [display],
); );
const options = [0.25, 0.5, 1, 1.5, 2]; const options = [0.25, 0.5, 1, 1.5, 2];

View File

@ -43,7 +43,7 @@ export function QualityView({ id }: { id: string }) {
const currentQuality = usePlayerStore((s) => s.currentQuality); const currentQuality = usePlayerStore((s) => s.currentQuality);
const switchQuality = usePlayerStore((s) => s.switchQuality); const switchQuality = usePlayerStore((s) => s.switchQuality);
const enableAutomaticQuality = usePlayerStore( const enableAutomaticQuality = usePlayerStore(
(s) => s.enableAutomaticQuality (s) => s.enableAutomaticQuality,
); );
const setAutomaticQuality = useQualityStore((s) => s.setAutomaticQuality); const setAutomaticQuality = useQualityStore((s) => s.setAutomaticQuality);
const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality); const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality);
@ -56,7 +56,7 @@ export function QualityView({ id }: { id: string }) {
switchQuality(q); switchQuality(q);
router.close(); router.close();
}, },
[router, switchQuality, setLastChosenQuality, setAutomaticQuality] [router, switchQuality, setLastChosenQuality, setAutomaticQuality],
); );
const changeAutomatic = useCallback(() => { const changeAutomatic = useCallback(() => {

View File

@ -17,14 +17,14 @@ export function SettingsMenu({ id }: { id: string }) {
const router = useOverlayRouter(id); const router = useOverlayRouter(id);
const currentQuality = usePlayerStore((s) => s.currentQuality); const currentQuality = usePlayerStore((s) => s.currentQuality);
const selectedCaptionLanguage = usePlayerStore( const selectedCaptionLanguage = usePlayerStore(
(s) => s.caption.selected?.language (s) => s.caption.selected?.language,
); );
const subtitlesEnabled = useSubtitleStore((s) => s.enabled); const subtitlesEnabled = useSubtitleStore((s) => s.enabled);
const currentSourceId = usePlayerStore((s) => s.sourceId); const currentSourceId = usePlayerStore((s) => s.sourceId);
const sourceName = useMemo(() => { const sourceName = useMemo(() => {
if (!currentSourceId) return "..."; if (!currentSourceId) return "...";
const source = getCachedMetadata().find( const source = getCachedMetadata().find(
(src) => src.id === currentSourceId (src) => src.id === currentSourceId,
); );
return source?.name ?? "..."; return source?.name ?? "...";
}, [currentSourceId]); }, [currentSourceId]);
@ -59,7 +59,7 @@ export function SettingsMenu({ id }: { id: string }) {
clickable clickable
onClick={() => onClick={() =>
router.navigate( router.navigate(
source?.type === "file" ? "/download" : "/download/unable" source?.type === "file" ? "/download" : "/download/unable",
) )
} }
rightSide={<Icon className="text-xl" icon={Icons.DOWNLOAD} />} rightSide={<Icon className="text-xl" icon={Icons.DOWNLOAD} />}

View File

@ -41,7 +41,7 @@ export function EmbedOption(props: {
props.routerId, props.routerId,
props.sourceId, props.sourceId,
props.url, props.url,
props.embedId props.embedId,
); );
return ( return (

View File

@ -1,17 +1,17 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
export function BackLink(props: { url: string }) { export function BackLink(props: { url: string }) {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const navigate = useNavigate();
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<button <button
type="button" type="button"
onClick={() => history.push(props.url)} onClick={() => navigate(props.url)}
className="py-1 -my-1 px-2 -mx-2 tabbable rounded-lg flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium" className="py-1 -my-1 px-2 -mx-2 tabbable rounded-lg flex items-center cursor-pointer text-type-secondary hover:text-white transition-colors duration-200 font-medium"
> >
<Icon className="mr-2" icon={Icons.ARROW_LEFT} /> <Icon className="mr-2" icon={Icons.ARROW_LEFT} />

View File

@ -8,7 +8,7 @@ export function BottomControls(props: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const setHoveringAnyControls = usePlayerStore( const setHoveringAnyControls = usePlayerStore(
(s) => s.setHoveringAnyControls (s) => s.setHoveringAnyControls,
); );
useEffect(() => { useEffect(() => {

View File

@ -21,7 +21,7 @@ export interface PlayerProps {
function useHovering(containerEl: RefObject<HTMLDivElement>) { function useHovering(containerEl: RefObject<HTMLDivElement>) {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const updateInterfaceHovering = usePlayerStore( const updateInterfaceHovering = usePlayerStore(
(s) => s.updateInterfaceHovering (s) => s.updateInterfaceHovering,
); );
const hovering = usePlayerStore((s) => s.interface.hovering); const hovering = usePlayerStore((s) => s.interface.hovering);

View File

@ -8,7 +8,7 @@ export function LeftSideControls(props: {
className?: string; className?: string;
}) { }) {
const setHoveringLeftControls = usePlayerStore( const setHoveringLeftControls = usePlayerStore(
(s) => s.setHoveringLeftControls (s) => s.setHoveringLeftControls,
); );
const mouseLeave = useCallback(() => { const mouseLeave = useCallback(() => {

View File

@ -79,15 +79,15 @@ export function SubtitleRenderer() {
const parsedCaptions = useMemo( const parsedCaptions = useMemo(
() => (srtData ? parseSubtitles(srtData, language) : []), () => (srtData ? parseSubtitles(srtData, language) : []),
[srtData, language] [srtData, language],
); );
const visibileCaptions = useMemo( const visibileCaptions = useMemo(
() => () =>
parsedCaptions.filter(({ start, end }) => parsedCaptions.filter(({ start, end }) =>
captionIsVisible(start, end, delay, videoTime) captionIsVisible(start, end, delay, videoTime),
), ),
[parsedCaptions, videoTime, delay] [parsedCaptions, videoTime, delay],
); );
return ( return (

View File

@ -11,7 +11,7 @@ export function TopControls(props: {
}) { }) {
const bannerSize = useBannerSize("player"); const bannerSize = useBannerSize("player");
const setHoveringAnyControls = usePlayerStore( const setHoveringAnyControls = usePlayerStore(
(s) => s.setHoveringAnyControls (s) => s.setHoveringAnyControls,
); );
useEffect(() => { useEffect(() => {

View File

@ -36,7 +36,7 @@ function hlsLevelToQuality(level: Level): SourceQuality | null {
function qualityToHlsLevel(quality: SourceQuality): number | null { function qualityToHlsLevel(quality: SourceQuality): number | null {
const found = Object.entries(levelConversionMap).find( const found = Object.entries(levelConversionMap).find(
(entry) => entry[1] === quality (entry) => entry[1] === quality,
); );
return found ? +found[0] : null; return found ? +found[0] : null;
} }
@ -83,7 +83,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
}); });
if (availableQuality) { if (availableQuality) {
const levelIndex = hls.levels.findIndex( const levelIndex = hls.levels.findIndex(
(v) => v.height === qualityToHlsLevel(availableQuality) (v) => v.height === qualityToHlsLevel(availableQuality),
); );
if (levelIndex !== -1) { if (levelIndex !== -1) {
hls.currentLevel = levelIndex; hls.currentLevel = levelIndex;
@ -182,10 +182,10 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
videoElement.addEventListener("canplay", () => emit("loading", false)); videoElement.addEventListener("canplay", () => emit("loading", false));
videoElement.addEventListener("waiting", () => emit("loading", true)); videoElement.addEventListener("waiting", () => emit("loading", true));
videoElement.addEventListener("volumechange", () => videoElement.addEventListener("volumechange", () =>
emit("volumechange", videoElement?.muted ? 0 : videoElement?.volume ?? 0) emit("volumechange", videoElement?.muted ? 0 : videoElement?.volume ?? 0),
); );
videoElement.addEventListener("timeupdate", () => videoElement.addEventListener("timeupdate", () =>
emit("time", videoElement?.currentTime ?? 0) emit("time", videoElement?.currentTime ?? 0),
); );
videoElement.addEventListener("loadedmetadata", () => { videoElement.addEventListener("loadedmetadata", () => {
if ( if (
@ -202,7 +202,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
if (videoElement) if (videoElement)
emit( emit(
"buffered", "buffered",
handleBuffered(videoElement.currentTime, videoElement.buffered) handleBuffered(videoElement.currentTime, videoElement.buffered),
); );
}); });
videoElement.addEventListener("webkitendfullscreen", () => { videoElement.addEventListener("webkitendfullscreen", () => {
@ -216,7 +216,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
if (e.availability === "available") { if (e.availability === "available") {
emit("canairplay", true); emit("canairplay", true);
} }
} },
); );
videoElement.addEventListener("ratechange", () => { videoElement.addEventListener("ratechange", () => {
if (videoElement) emit("playbackrate", videoElement.playbackRate); if (videoElement) emit("playbackrate", videoElement.playbackRate);
@ -368,7 +368,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
webkitPlayer.webkitSetPresentationMode( webkitPlayer.webkitSetPresentationMode(
webkitPlayer.webkitPresentationMode === "picture-in-picture" webkitPlayer.webkitPresentationMode === "picture-in-picture"
? "inline" ? "inline"
: "picture-in-picture" : "picture-in-picture",
); );
} }
if (canPictureInPicture()) { if (canPictureInPicture()) {

View File

@ -28,7 +28,7 @@ export interface ChromeCastDisplayInterfaceOptions {
*/ */
export function makeChromecastDisplayInterface( export function makeChromecastDisplayInterface(
ops: ChromeCastDisplayInterfaceOptions ops: ChromeCastDisplayInterfaceOptions,
): DisplayInterface { ): DisplayInterface {
const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>(); const { emit, on, off } = makeEmitter<DisplayInterfaceEvents>();
let isPaused = false; let isPaused = false;
@ -89,12 +89,12 @@ export function makeChromecastDisplayInterface(
}; };
ops.controller?.addEventListener( ops.controller?.addEventListener(
cast.framework.RemotePlayerEventType.ANY_CHANGE, cast.framework.RemotePlayerEventType.ANY_CHANGE,
listen listen,
); );
return () => { return () => {
ops.controller?.removeEventListener( ops.controller?.removeEventListener(
cast.framework.RemotePlayerEventType.ANY_CHANGE, cast.framework.RemotePlayerEventType.ANY_CHANGE,
listen listen,
); );
}; };
} }

View File

@ -54,7 +54,7 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
load(ops: qualityChangeOptions): void; load(ops: qualityChangeOptions): void;
changeQuality( changeQuality(
automaticQuality: boolean, automaticQuality: boolean,
preferredQuality: SourceQuality | null preferredQuality: SourceQuality | null,
): void; ): void;
processVideoElement(video: HTMLVideoElement): void; processVideoElement(video: HTMLVideoElement): void;
processContainerElement(container: HTMLElement): void; processContainerElement(container: HTMLElement): void;

View File

@ -8,7 +8,7 @@ export function useCaptions() {
const setLanguage = useSubtitleStore((s) => s.setLanguage); const setLanguage = useSubtitleStore((s) => s.setLanguage);
const enabled = useSubtitleStore((s) => s.enabled); const enabled = useSubtitleStore((s) => s.enabled);
const resetSubtitleSpecificSettings = useSubtitleStore( const resetSubtitleSpecificSettings = useSubtitleStore(
(s) => s.resetSubtitleSpecificSettings (s) => s.resetSubtitleSpecificSettings,
); );
const setCaption = usePlayerStore((s) => s.setCaption); const setCaption = usePlayerStore((s) => s.setCaption);
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage); const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
@ -27,7 +27,7 @@ export function useCaptions() {
resetSubtitleSpecificSettings(); resetSubtitleSpecificSettings();
setLanguage(language); setLanguage(language);
}, },
[setLanguage, captionList, setCaption, resetSubtitleSpecificSettings] [setLanguage, captionList, setCaption, resetSubtitleSpecificSettings],
); );
const disable = useCallback(async () => { const disable = useCallback(async () => {

View File

@ -22,7 +22,7 @@ export function useInitializeSource() {
const source = usePlayerStore((s) => s.source); const source = usePlayerStore((s) => s.source);
const sourceIdentifier = useMemo( const sourceIdentifier = useMemo(
() => (source ? JSON.stringify(source) : null), () => (source ? JSON.stringify(source) : null),
[source] [source],
); );
const { selectLastUsedLanguageIfEnabled } = useCaptions(); const { selectLastUsedLanguageIfEnabled } = useCaptions();

View File

@ -16,7 +16,7 @@ export interface Source {
function getProgress( function getProgress(
items: Record<string, ProgressMediaItem>, items: Record<string, ProgressMediaItem>,
meta: PlayerMeta | null meta: PlayerMeta | null,
): number { ): number {
const item = items[meta?.tmdbId ?? ""]; const item = items[meta?.tmdbId ?? ""];
if (!item || !meta) return 0; if (!item || !meta) return 0;
@ -38,10 +38,10 @@ export function usePlayer() {
const setSourceId = usePlayerStore((s) => s.setSourceId); const setSourceId = usePlayerStore((s) => s.setSourceId);
const status = usePlayerStore((s) => s.status); const status = usePlayerStore((s) => s.status);
const shouldStartFromBeginning = usePlayerStore( const shouldStartFromBeginning = usePlayerStore(
(s) => s.interface.shouldStartFromBeginning (s) => s.interface.shouldStartFromBeginning,
); );
const setShouldStartFromBeginning = usePlayerStore( const setShouldStartFromBeginning = usePlayerStore(
(s) => s.setShouldStartFromBeginning (s) => s.setShouldStartFromBeginning,
); );
const reset = usePlayerStore((s) => s.reset); const reset = usePlayerStore((s) => s.reset);
const meta = usePlayerStore((s) => s.meta); const meta = usePlayerStore((s) => s.meta);
@ -61,7 +61,7 @@ export function usePlayer() {
source: SourceSliceSource, source: SourceSliceSource,
captions: CaptionListItem[], captions: CaptionListItem[],
sourceId: string | null, sourceId: string | null,
startAtOverride?: number startAtOverride?: number,
) { ) {
const start = startAtOverride ?? getProgress(progressStore.items, meta); const start = startAtOverride ?? getProgress(progressStore.items, meta);
setCaption(null); setCaption(null);

View File

@ -13,14 +13,14 @@ export function usePlayerMeta() {
const { meta, setMeta } = usePlayer(); const { meta, setMeta } = usePlayer();
const scrapeMedia = useMemo( const scrapeMedia = useMemo(
() => (meta ? metaToScrapeMedia(meta) : null), () => (meta ? metaToScrapeMedia(meta) : null),
[meta] [meta],
); );
const setDirectMeta = useCallback( const setDirectMeta = useCallback(
(m: PlayerMeta) => { (m: PlayerMeta) => {
setMeta(m, playerStatus.SCRAPING); setMeta(m, playerStatus.SCRAPING);
}, },
[setMeta] [setMeta],
); );
const setPlayerMeta = useCallback( const setPlayerMeta = useCallback(
@ -65,7 +65,7 @@ export function usePlayerMeta() {
setDirectMeta(playerMeta); setDirectMeta(playerMeta);
return playerMeta; return playerMeta;
}, },
[setDirectMeta] [setDirectMeta],
); );
return { return {

View File

@ -4,12 +4,12 @@ import { usePlayerStore } from "@/stores/player/store";
export function useShouldShowControls() { export function useShouldShowControls() {
const hovering = usePlayerStore((s) => s.interface.hovering); const hovering = usePlayerStore((s) => s.interface.hovering);
const lastHoveringState = usePlayerStore( const lastHoveringState = usePlayerStore(
(s) => s.interface.lastHoveringState (s) => s.interface.lastHoveringState,
); );
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused); const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay); const hasOpenOverlay = usePlayerStore((s) => s.interface.hasOpenOverlay);
const isHoveringControls = usePlayerStore( const isHoveringControls = usePlayerStore(
(s) => s.interface.isHoveringControls (s) => s.interface.isHoveringControls,
); );
const isUsingTouch = lastHoveringState === PlayerHoverState.MOBILE_TAPPED; const isUsingTouch = lastHoveringState === PlayerHoverState.MOBILE_TAPPED;

View File

@ -24,7 +24,7 @@ export function useEmbedScraping(
routerId: string, routerId: string,
sourceId: string, sourceId: string,
url: string, url: string,
embedId: string embedId: string,
) { ) {
const setSource = usePlayerStore((s) => s.setSource); const setSource = usePlayerStore((s) => s.setSource);
const setCaption = usePlayerStore((s) => s.setCaption); const setCaption = usePlayerStore((s) => s.setCaption);
@ -43,7 +43,7 @@ export function useEmbedScraping(
const baseUrlMaker = makeProviderUrl(providerApiUrl); const baseUrlMaker = makeProviderUrl(providerApiUrl);
const conn = await connectServerSideEvents<EmbedOutput>( const conn = await connectServerSideEvents<EmbedOutput>(
baseUrlMaker.scrapeEmbed(embedId, url), baseUrlMaker.scrapeEmbed(embedId, url),
["completed", "noOutput"] ["completed", "noOutput"],
); );
result = await conn.promise(); result = await conn.promise();
} else { } else {
@ -62,7 +62,7 @@ export function useEmbedScraping(
sourceId, sourceId,
embedId, embedId,
status, status,
err err,
), ),
]); ]);
throw err; throw err;
@ -75,7 +75,7 @@ export function useEmbedScraping(
setSource( setSource(
convertRunoutputToSource({ stream: result.stream }), convertRunoutputToSource({ stream: result.stream }),
convertProviderCaption(result.stream.captions), convertProviderCaption(result.stream.captions),
progress progress,
); );
router.close(); router.close();
}, [embedId, sourceId, meta, router, report, setCaption]); }, [embedId, sourceId, meta, router, report, setCaption]);
@ -107,7 +107,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
const baseUrlMaker = makeProviderUrl(providerApiUrl); const baseUrlMaker = makeProviderUrl(providerApiUrl);
const conn = await connectServerSideEvents<SourcererOutput>( const conn = await connectServerSideEvents<SourcererOutput>(
baseUrlMaker.scrapeSource(sourceId, scrapeMedia), baseUrlMaker.scrapeSource(sourceId, scrapeMedia),
["completed", "noOutput"] ["completed", "noOutput"],
); );
result = await conn.promise(); result = await conn.promise();
} else { } else {
@ -134,7 +134,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
setSource( setSource(
convertRunoutputToSource({ stream: result.stream }), convertRunoutputToSource({ stream: result.stream }),
convertProviderCaption(result.stream.captions), convertProviderCaption(result.stream.captions),
progress progress,
); );
setSourceId(sourceId); setSourceId(sourceId);
router.close(); router.close();
@ -149,9 +149,9 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
const conn = await connectServerSideEvents<EmbedOutput>( const conn = await connectServerSideEvents<EmbedOutput>(
baseUrlMaker.scrapeEmbed( baseUrlMaker.scrapeEmbed(
result.embeds[0].embedId, result.embeds[0].embedId,
result.embeds[0].url result.embeds[0].url,
), ),
["completed", "noOutput"] ["completed", "noOutput"],
); );
embedResult = await conn.promise(); embedResult = await conn.promise();
} else { } else {
@ -170,7 +170,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
sourceId, sourceId,
result.embeds[0].embedId, result.embeds[0].embedId,
status, status,
err err,
), ),
]); ]);
throw err; throw err;
@ -181,7 +181,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
sourceId, sourceId,
result.embeds[0].embedId, result.embeds[0].embedId,
"success", "success",
null null,
), ),
]); ]);
setSourceId(sourceId); setSourceId(sourceId);
@ -189,7 +189,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
setSource( setSource(
convertRunoutputToSource({ stream: embedResult.stream }), convertRunoutputToSource({ stream: embedResult.stream }),
convertProviderCaption(embedResult.stream.captions), convertProviderCaption(embedResult.stream.captions),
progress progress,
); );
router.close(); router.close();
} }

View File

@ -102,13 +102,13 @@ export function CastingInternal() {
} }
newControlller.addEventListener( newControlller.addEventListener(
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED, cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
connectionChanged connectionChanged,
); );
return () => { return () => {
newControlller.removeEventListener( newControlller.removeEventListener(
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED, cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
connectionChanged connectionChanged,
); );
}; };
}, [available, setPlayer, setController, setInstance, setIsCasting]); }, [available, setPlayer, setController, setInstance, setIsCasting]);

View File

@ -9,7 +9,7 @@ export function SectionTitle(props: {
<h3 <h3
className={classNames( className={classNames(
"uppercase font-bold text-video-context-type-secondary text-xs pt-8 pl-1 pb-2.5 border-b border-video-context-border", "uppercase font-bold text-video-context-type-secondary text-xs pt-8 pl-1 pb-2.5 border-b border-video-context-border",
props.className props.className,
)} )}
> >
{props.children} {props.children}
@ -47,7 +47,7 @@ export function ScrollToActiveSection(props: {
scrollingContainer.current?.scrollTo( scrollingContainer.current?.scrollTo(
0, 0,
activeYPos - boxRect.height / 2 + activeLinkRect.height / 2 activeYPos - boxRect.height / 2 + activeLinkRect.height / 2,
); );
}, [props.loaded]); }, [props.loaded]);

View File

@ -75,11 +75,11 @@ export function KeyboardEvents() {
} }
if (k === "ArrowUp") if (k === "ArrowUp")
dataRef.current.setVolume( dataRef.current.setVolume(
(dataRef.current.mediaPlaying?.volume || 0) + 0.15 (dataRef.current.mediaPlaying?.volume || 0) + 0.15,
); );
if (k === "ArrowDown") if (k === "ArrowDown")
dataRef.current.setVolume( dataRef.current.setVolume(
(dataRef.current.mediaPlaying?.volume || 0) - 0.15 (dataRef.current.mediaPlaying?.volume || 0) - 0.15,
); );
if (k === "m") dataRef.current.toggleMute(); if (k === "m") dataRef.current.toggleMute();

View File

@ -15,7 +15,7 @@ export interface StatusCircleLoading extends StatusCircle {
} }
function statusIsLoading( function statusIsLoading(
props: StatusCircle | StatusCircleLoading props: StatusCircle | StatusCircleLoading,
): props is StatusCircleLoading { ): props is StatusCircleLoading {
return props.type === "loading"; return props.type === "loading";
} }
@ -25,7 +25,7 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
() => ({ () => ({
percentage: statusIsLoading(props) ? props.percentage : 0, percentage: statusIsLoading(props) ? props.percentage : 0,
}), }),
[props] [props],
); );
return ( return (

View File

@ -95,7 +95,7 @@ class ThumnbnailWorker {
0, 0,
0, 0,
this.canvasEl.width, this.canvasEl.width,
this.canvasEl.height this.canvasEl.height,
); );
const imgUrl = this.canvasEl.toDataURL(); const imgUrl = this.canvasEl.toDataURL();

View File

@ -1,5 +1,6 @@
import classNames from "classnames"; import classNames from "classnames";
import { PointerEvent, useCallback } from "react"; import { PointerEvent, useCallback } from "react";
import { useEffectOnce, useTimeoutFn } from "react-use";
import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer"; import { useShouldShowVideoElement } from "@/components/player/internals/VideoContainer";
import { PlayerHoverState } from "@/stores/player/slices/interface"; import { PlayerHoverState } from "@/stores/player/slices/interface";
@ -10,9 +11,15 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
const display = usePlayerStore((s) => s.display); const display = usePlayerStore((s) => s.display);
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused); const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
const updateInterfaceHovering = usePlayerStore( const updateInterfaceHovering = usePlayerStore(
(s) => s.updateInterfaceHovering (s) => s.updateInterfaceHovering,
); );
const hovering = usePlayerStore((s) => s.interface.hovering); const hovering = usePlayerStore((s) => s.interface.hovering);
const [_, cancel, reset] = useTimeoutFn(() => {
updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
}, 3000);
useEffectOnce(() => {
cancel();
});
const toggleFullscreen = useCallback(() => { const toggleFullscreen = useCallback(() => {
display?.toggleFullscreen(); display?.toggleFullscreen();
@ -29,11 +36,15 @@ export function VideoClickTarget(props: { showingControls: boolean }) {
} }
// toggle on other types of clicks // toggle on other types of clicks
if (hovering !== PlayerHoverState.MOBILE_TAPPED) if (hovering !== PlayerHoverState.MOBILE_TAPPED) {
updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED); updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING); reset();
} else {
updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
cancel();
}
}, },
[display, isPaused, hovering, updateInterfaceHovering] [display, isPaused, hovering, updateInterfaceHovering, reset, cancel],
); );
if (!show) return null; if (!show) return null;

View File

@ -70,7 +70,7 @@ function VideoElement() {
const language = usePlayerStore((s) => s.caption.selected?.language); const language = usePlayerStore((s) => s.caption.selected?.language);
const trackObjectUrl = useObjectUrl( const trackObjectUrl = useObjectUrl(
() => (srtData ? convertSubtitlesToObjectUrl(srtData) : null), () => (srtData ? convertSubtitlesToObjectUrl(srtData) : null),
[srtData] [srtData],
); );
// report video element to display interface // report video element to display interface

View File

@ -12,7 +12,7 @@ export function captionIsVisible(
start: number, start: number,
end: number, end: number,
delay: number, delay: number,
currentTime: number currentTime: number,
) { ) {
const delayedStart = start / 1000 + delay; const delayedStart = start / 1000 + delay;
const delayedEnd = end / 1000 + delay; const delayedEnd = end / 1000 + delay;
@ -52,7 +52,7 @@ export function convertSubtitlesToSrt(text: string): string {
export function parseSubtitles( export function parseSubtitles(
text: string, text: string,
_language?: string _language?: string,
): CaptionCueType[] { ): 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[];
@ -64,7 +64,7 @@ function stringToBase64(input: string): string {
export function convertSubtitlesToSrtDataurl(text: string): string { export function convertSubtitlesToSrtDataurl(text: string): string {
return `data:application/x-subrip;base64,${stringToBase64( return `data:application/x-subrip;base64,${stringToBase64(
convertSubtitlesToSrt(text) convertSubtitlesToSrt(text),
)}`; )}`;
} }
@ -72,12 +72,12 @@ export function convertSubtitlesToObjectUrl(text: string): string {
return URL.createObjectURL( return URL.createObjectURL(
new Blob([convertSubtitlesToVtt(text)], { new Blob([convertSubtitlesToVtt(text)], {
type: "text/vtt", type: "text/vtt",
}) }),
); );
} }
export function convertProviderCaption( export function convertProviderCaption(
captions: RunOutput["stream"]["captions"] captions: RunOutput["stream"]["captions"],
): CaptionListItem[] { ): CaptionListItem[] {
return captions.map((v) => ({ return captions.map((v) => ({
language: v.language, language: v.language,

View File

@ -18,7 +18,7 @@ const mediaErrorMap: Record<number, { name: string; key: string }> = {
}; };
export function getMediaErrorDetails( export function getMediaErrorDetails(
err: MediaError | null err: MediaError | null,
): (typeof mediaErrorMap)[number] { ): (typeof mediaErrorMap)[number] {
const item = mediaErrorMap[err?.code ?? -1]; const item = mediaErrorMap[err?.code ?? -1];
if (!item) { if (!item) {

View File

@ -36,7 +36,7 @@ export const TextInputControl = forwardRef<
onFocus, onFocus,
passwordToggleable, passwordToggleable,
}, },
ref ref,
) => { ) => {
let inputType = "text"; let inputType = "text";
const [showPassword, setShowPassword] = useState(true); const [showPassword, setShowPassword] = useState(true);
@ -81,5 +81,5 @@ export const TextInputControl = forwardRef<
} }
return input; return input;
} },
); );

View File

@ -8,7 +8,7 @@ export function Paragraph(props: {
<p <p
className={classNames( className={classNames(
"text-errors-type-secondary", "text-errors-type-secondary",
props.marginClass ?? "mt-6" props.marginClass ?? "mt-6",
)} )}
> >
{props.children} {props.children}

View File

@ -8,7 +8,7 @@ export function Title(props: {
<h2 <h2
className={classNames( className={classNames(
"text-white text-3xl font-bold text-opacity-100 mt-6", "text-white text-3xl font-bold text-opacity-100 mt-6",
props.className props.className,
)} )}
> >
{props.children} {props.children}

View File

@ -5,7 +5,7 @@ export function Divider(props: { marginClass?: string }) {
<hr <hr
className={classNames( className={classNames(
"w-full h-px border-0 bg-utils-divider bg-opacity-50", "w-full h-px border-0 bg-utils-divider bg-opacity-50",
props.marginClass ?? "my-8" props.marginClass ?? "my-8",
)} )}
/> />
); );

View File

@ -46,11 +46,11 @@ function Light(props: FlareProps) {
const halfSize = size / 2; const halfSize = size / 2;
outerRef.current.style.setProperty( outerRef.current.style.setProperty(
"--bg-x", "--bg-x",
`${(e.clientX - rect.left - halfSize).toFixed(0)}px` `${(e.clientX - rect.left - halfSize).toFixed(0)}px`,
); );
outerRef.current.style.setProperty( outerRef.current.style.setProperty(
"--bg-y", "--bg-y",
`${(e.clientY - rect.top - halfSize).toFixed(0)}px` `${(e.clientY - rect.top - halfSize).toFixed(0)}px`,
); );
} }
document.addEventListener("mousemove", mouseMove); document.addEventListener("mousemove", mouseMove);
@ -66,7 +66,7 @@ function Light(props: FlareProps) {
props.className, props.className,
{ {
"!opacity-100": props.enabled ?? false, "!opacity-100": props.enabled ?? false,
} },
)} )}
style={{ style={{
backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`, backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`,
@ -79,7 +79,7 @@ function Light(props: FlareProps) {
className={c( className={c(
"absolute inset-[1px] overflow-hidden", "absolute inset-[1px] overflow-hidden",
props.className, props.className,
props.backgroundClass props.backgroundClass,
)} )}
> >
<div <div

View File

@ -33,7 +33,7 @@ class Particle {
options: LightbarOptions = { options: LightbarOptions = {
horizontalMotion: false, horizontalMotion: false,
sizeRange: [10, 10], sizeRange: [10, 10],
} },
) { ) {
if (options.imgSrc) { if (options.imgSrc) {
this.image = new Image(); this.image = new Image();
@ -117,7 +117,7 @@ class Particle {
this.radius * 1.5, this.radius * 1.5,
this.direction, this.direction,
0, 0,
Math.PI * 2 Math.PI * 2,
); );
ctx.fillStyle = "white"; ctx.fillStyle = "white";
ctx.fill(); ctx.fill();

View File

@ -8,7 +8,7 @@ export function Ol(props: { items: React.ReactNode[] }) {
<li <li
className={classNames( className={classNames(
"grid grid-cols-[auto,1fr] gap-6", "grid grid-cols-[auto,1fr] gap-6",
i !== props.items.length - 1 ? "pb-12" : undefined i !== props.items.length - 1 ? "pb-12" : undefined,
)} )}
> >
<div className="relative z-0"> <div className="relative z-0">

View File

@ -24,7 +24,7 @@ interface Props {
function getClasses( function getClasses(
animation: TransitionAnimations, animation: TransitionAnimations,
duration: string duration: string,
): TransitionClasses { ): TransitionClasses {
if (animation === "slide-down") { if (animation === "slide-down") {
return { return {

View File

@ -67,7 +67,7 @@ export function useAuth() {
const publicKeyBase64Url = bytesToBase64Url(keys.publicKey); const publicKeyBase64Url = bytesToBase64Url(keys.publicKey);
const { challenge } = await getLoginChallengeToken( const { challenge } = await getLoginChallengeToken(
backendUrl, backendUrl,
publicKeyBase64Url publicKeyBase64Url,
); );
const signature = await signChallenge(keys, challenge); const signature = await signChallenge(keys, challenge);
const loginResult = await loginAccount(backendUrl, { const loginResult = await loginAccount(backendUrl, {
@ -83,7 +83,7 @@ export function useAuth() {
const seedBase64 = bytesToBase64(keys.seed); const seedBase64 = bytesToBase64(keys.seed);
return userDataLogin(loginResult, user.user, user.session, seedBase64); return userDataLogin(loginResult, user.user, user.session, seedBase64);
}, },
[userDataLogin, backendUrl] [userDataLogin, backendUrl],
); );
const logout = useCallback(async () => { const logout = useCallback(async () => {
@ -92,7 +92,7 @@ export function useAuth() {
await removeSession( await removeSession(
backendUrl, backendUrl,
currentAccount.token, currentAccount.token,
currentAccount.sessionId currentAccount.sessionId,
); );
} catch { } catch {
// we dont care about failing to delete session // we dont care about failing to delete session
@ -104,7 +104,7 @@ export function useAuth() {
async (registerData: RegistrationData) => { async (registerData: RegistrationData) => {
const { challenge } = await getRegisterChallengeToken( const { challenge } = await getRegisterChallengeToken(
backendUrl, backendUrl,
registerData.recaptchaToken registerData.recaptchaToken,
); );
const keys = await keysFromMnemonic(registerData.mnemonic); const keys = await keysFromMnemonic(registerData.mnemonic);
const signature = await signChallenge(keys, challenge); const signature = await signChallenge(keys, challenge);
@ -122,17 +122,17 @@ export function useAuth() {
registerResult, registerResult,
registerResult.user, registerResult.user,
registerResult.session, registerResult.session,
bytesToBase64(keys.seed) bytesToBase64(keys.seed),
); );
}, },
[backendUrl, userDataLogin] [backendUrl, userDataLogin],
); );
const importData = useCallback( const importData = useCallback(
async ( async (
account: AccountWithToken, account: AccountWithToken,
progressItems: Record<string, ProgressMediaItem>, progressItems: Record<string, ProgressMediaItem>,
bookmarks: Record<string, BookmarkMediaItem> bookmarks: Record<string, BookmarkMediaItem>,
) => { ) => {
if ( if (
Object.keys(progressItems).length === 0 && Object.keys(progressItems).length === 0 &&
@ -142,17 +142,17 @@ export function useAuth() {
} }
const progressInputs = Object.entries(progressItems).flatMap( const progressInputs = Object.entries(progressItems).flatMap(
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item) ([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
); );
const bookmarkInputs = Object.entries(bookmarks).map(([tmdbId, item]) => const bookmarkInputs = Object.entries(bookmarks).map(([tmdbId, item]) =>
bookmarkMediaToInput(tmdbId, item) bookmarkMediaToInput(tmdbId, item),
); );
await importProgress(backendUrl, account, progressInputs); await importProgress(backendUrl, account, progressInputs);
await importBookmarks(backendUrl, account, bookmarkInputs); await importBookmarks(backendUrl, account, bookmarkInputs);
}, },
[backendUrl] [backendUrl],
); );
const restore = useCallback( const restore = useCallback(
@ -180,7 +180,7 @@ export function useAuth() {
syncData(user.user, user.session, progress, bookmarks, settings); syncData(user.user, user.session, progress, bookmarks, settings);
}, },
[backendUrl, syncData, logout] [backendUrl, syncData, logout],
); );
return { return {

View File

@ -25,7 +25,7 @@ export function useAuthData() {
const setTheme = useThemeStore((s) => s.setTheme); const setTheme = useThemeStore((s) => s.setTheme);
const setAppLanguage = useLanguageStore((s) => s.setLanguage); const setAppLanguage = useLanguageStore((s) => s.setLanguage);
const importSubtitleLanguage = useSubtitleStore( const importSubtitleLanguage = useSubtitleStore(
(s) => s.importSubtitleLanguage (s) => s.importSubtitleLanguage,
); );
const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks); const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks);
@ -36,7 +36,7 @@ export function useAuthData() {
loginResponse: LoginResponse, loginResponse: LoginResponse,
user: UserResponse, user: UserResponse,
session: SessionResponse, session: SessionResponse,
seed: string seed: string,
) => { ) => {
const account = { const account = {
token: loginResponse.token, token: loginResponse.token,
@ -49,7 +49,7 @@ export function useAuthData() {
setAccount(account); setAccount(account);
return account; return account;
}, },
[setAccount] [setAccount],
); );
const logout = useCallback(async () => { const logout = useCallback(async () => {
@ -64,7 +64,7 @@ export function useAuthData() {
_session: SessionResponse, _session: SessionResponse,
progress: ProgressResponse[], progress: ProgressResponse[],
bookmarks: BookmarkResponse[], bookmarks: BookmarkResponse[],
settings: SettingsResponse settings: SettingsResponse,
) => { ) => {
replaceBookmarks(bookmarkResponsesToEntries(bookmarks)); replaceBookmarks(bookmarkResponsesToEntries(bookmarks));
replaceItems(progressResponsesToEntries(progress)); replaceItems(progressResponsesToEntries(progress));
@ -87,7 +87,7 @@ export function useAuthData() {
setAppLanguage, setAppLanguage,
importSubtitleLanguage, importSubtitleLanguage,
setTheme, setTheme,
] ],
); );
return { return {

View File

@ -17,7 +17,7 @@ export function useRouterAnchorUpdate(id: string) {
const setAnchorPoint = useOverlayStore((s) => s.setAnchorPoint); const setAnchorPoint = useOverlayStore((s) => s.setAnchorPoint);
const routerActive = useMemo( const routerActive = useMemo(
() => !!route && route.startsWith(`/${id}`), () => !!route && route.startsWith(`/${id}`),
[route, id] [route, id],
); );
const update = useCallback(() => { const update = useCallback(() => {
@ -96,7 +96,7 @@ export function useInternalOverlayRouter(id: string) {
if (route && !preventRouteClear) setRoute(null); if (route && !preventRouteClear) setRoute(null);
setTransition(null); setTransition(null);
}, },
[setRoute, route, setTransition] [setRoute, route, setTransition],
); );
const open = useCallback( const open = useCallback(
@ -104,7 +104,7 @@ export function useInternalOverlayRouter(id: string) {
setTransition(null); setTransition(null);
setRoute(joinPath(splitPath(defaultRoute, id))); setRoute(joinPath(splitPath(defaultRoute, id)));
}, },
[id, setRoute, setTransition] [id, setRoute, setTransition],
); );
const activeRoute = routerActive const activeRoute = routerActive

View File

@ -13,7 +13,7 @@ export function makePercentage(num: number) {
} }
function isClickEvent( function isClickEvent(
evt: ActivityEvent evt: ActivityEvent,
): evt is React.MouseEvent<HTMLElement> | MouseEvent { ): evt is React.MouseEvent<HTMLElement> | MouseEvent {
return ( return (
evt.type === "mousedown" || evt.type === "mousedown" ||
@ -29,7 +29,7 @@ const getEventX = (evt: ActivityEvent) => {
export function useProgressBar( export function useProgressBar(
barRef: RefObject<HTMLElement>, barRef: RefObject<HTMLElement>,
commit: (percentage: number) => void, commit: (percentage: number) => void,
commitImmediately = false commitImmediately = false,
) { ) {
const [mouseDown, setMouseDown] = useState<boolean>(false); const [mouseDown, setMouseDown] = useState<boolean>(false);
const [progress, setProgress] = useState<number>(0); const [progress, setProgress] = useState<number>(0);
@ -78,7 +78,7 @@ export function useProgressBar(
((getEventX(ev) - rect.left) / barRef.current.offsetWidth) * 100; ((getEventX(ev) - rect.left) / barRef.current.offsetWidth) * 100;
setProgress(pos); setProgress(pos);
}, },
[setProgress, barRef] [setProgress, barRef],
); );
return { return {

View File

@ -54,7 +54,7 @@ function useBaseScrape() {
.reduce<Record<string, ScrapingSegment>>((a, v) => { .reduce<Record<string, ScrapingSegment>>((a, v) => {
a[v.id] = v; a[v.id] = v;
return a; return a;
}, {}) }, {}),
); );
setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] }))); setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] })));
}, []); }, []);
@ -85,7 +85,7 @@ function useBaseScrape() {
setSources((s) => { setSources((s) => {
evt.embeds.forEach((v) => { evt.embeds.forEach((v) => {
const source = getCachedMetadata().find( const source = getCachedMetadata().find(
(src) => src.id === v.embedScraperId (src) => src.id === v.embedScraperId,
); );
if (!source) throw new Error("invalid source id"); if (!source) throw new Error("invalid source id");
const out: ScrapingSegment = { const out: ScrapingSegment = {
@ -106,7 +106,7 @@ function useBaseScrape() {
return [...s]; return [...s];
}); });
}, },
[] [],
); );
const startScrape = useCallback(() => { const startScrape = useCallback(() => {
@ -158,7 +158,7 @@ export function useScrape() {
const baseUrlMaker = makeProviderUrl(providerApiUrl); const baseUrlMaker = makeProviderUrl(providerApiUrl);
const conn = await connectServerSideEvents<RunOutput | "">( const conn = await connectServerSideEvents<RunOutput | "">(
baseUrlMaker.scrapeAll(media), baseUrlMaker.scrapeAll(media),
["completed", "noOutput"] ["completed", "noOutput"],
); );
conn.on("init", initEvent); conn.on("init", initEvent);
conn.on("start", startEvent); conn.on("start", startEvent);
@ -189,7 +189,7 @@ export function useScrape() {
discoverEmbedsEvent, discoverEmbedsEvent,
getResult, getResult,
startScrape, startScrape,
] ],
); );
return { return {
@ -204,7 +204,7 @@ export function useListCenter(
containerRef: RefObject<HTMLDivElement | null>, containerRef: RefObject<HTMLDivElement | null>,
listRef: RefObject<HTMLDivElement | null>, listRef: RefObject<HTMLDivElement | null>,
sourceOrder: ScrapingItems[], sourceOrder: ScrapingItems[],
currentSource: string | undefined currentSource: string | undefined,
) { ) {
const [renderedOnce, setRenderedOnce] = useState(false); const [renderedOnce, setRenderedOnce] = useState(false);
@ -217,7 +217,7 @@ export function useListCenter(
] as HTMLDivElement[]; ] as HTMLDivElement[];
const currentIndex = elements.findIndex( const currentIndex = elements.findIndex(
(e) => e.getAttribute("data-source-id") === currentSource (e) => e.getAttribute("data-source-id") === currentSource,
); );
const currentElement = elements[currentIndex]; const currentElement = elements[currentIndex];

View File

@ -1,12 +1,12 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useHistory, useLocation } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
export function useQueryParams() { export function useQueryParams() {
const loc = useLocation(); const loc = useLocation();
const queryParams = useMemo(() => { const queryParams = useMemo(() => {
const obj: Record<string, string> = Object.fromEntries( const obj: Record<string, string> = Object.fromEntries(
new URLSearchParams(loc.search).entries() new URLSearchParams(loc.search).entries(),
); );
return obj; return obj;
@ -16,11 +16,11 @@ export function useQueryParams() {
} }
export function useQueryParam( export function useQueryParam(
param: string param: string,
): [string | null, (a: string | null) => void] { ): [string | null, (a: string | null) => void] {
const params = useQueryParams(); const params = useQueryParams();
const location = useLocation(); const location = useLocation();
const router = useHistory(); const navigate = useNavigate();
const currentValue = params[param] ?? null; const currentValue = params[param] ?? null;
const set = useCallback( const set = useCallback(
@ -28,11 +28,11 @@ export function useQueryParam(
const parsed = new URLSearchParams(location.search); const parsed = new URLSearchParams(location.search);
if (value) parsed.set(param, value); if (value) parsed.set(param, value);
else parsed.delete(param); else parsed.delete(param);
router.push({ navigate({
search: parsed.toString(), search: parsed.toString(),
}); });
}, },
[param, location.search, router] [param, location.search, navigate],
); );
return [currentValue, set]; return [currentValue, set];

View File

@ -22,7 +22,7 @@ export function useRandomTranslation() {
return typeof keys === "string" ? keys : defaultTitle; return typeof keys === "string" ? keys : defaultTitle;
}, },
[t, seed, shouldJoke] [t, seed, shouldJoke],
); );
return { t: getRandomTranslation }; return { t: getRandomTranslation };

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { generatePath, useHistory, useParams } from "react-router-dom"; import { generatePath, useNavigate, useParams } from "react-router-dom";
function decode(query: string | null | undefined) { function decode(query: string | null | undefined) {
return query ? decodeURIComponent(query) : ""; return query ? decodeURIComponent(query) : "";
@ -8,9 +8,9 @@ function decode(query: string | null | undefined) {
export function useSearchQuery(): [ export function useSearchQuery(): [
string, string,
(inp: string, force?: boolean) => void, (inp: string, force?: boolean) => void,
() => void () => void,
] { ] {
const history = useHistory(); const navigate = useNavigate();
const params = useParams<{ query: string }>(); const params = useParams<{ query: string }>();
const [search, setSearch] = useState(decode(params.query)); const [search, setSearch] = useState(decode(params.query));
@ -22,13 +22,14 @@ export function useSearchQuery(): [
setSearch(inp); setSearch(inp);
if (!commitToUrl) return; if (!commitToUrl) return;
if (inp.length === 0) { if (inp.length === 0) {
history.replace("/"); navigate("/", { replace: true });
return; return;
} }
history.replace( navigate(
generatePath("/browse/:query", { generatePath("/browse/:query", {
query: inp, query: inp,
}) }),
{ replace: true },
); );
}; };

View File

@ -11,7 +11,7 @@ import {
import { SubtitleStyling } from "@/stores/subtitles"; import { SubtitleStyling } from "@/stores/subtitles";
export function useDerived<T>( export function useDerived<T>(
initial: T initial: T,
): [T, Dispatch<SetStateAction<T>>, () => void, boolean] { ): [T, Dispatch<SetStateAction<T>>, () => void, boolean] {
const [overwrite, setOverwrite] = useState<T | undefined>(undefined); const [overwrite, setOverwrite] = useState<T | undefined>(undefined);
useEffect(() => { useEffect(() => {
@ -19,14 +19,14 @@ export function useDerived<T>(
}, [initial]); }, [initial]);
const changed = useMemo( const changed = useMemo(
() => !isEqual(overwrite, initial) && overwrite !== undefined, () => !isEqual(overwrite, initial) && overwrite !== undefined,
[overwrite, initial] [overwrite, initial],
); );
const setter = useCallback<Dispatch<SetStateAction<T>>>( const setter = useCallback<Dispatch<SetStateAction<T>>>(
(inp) => { (inp) => {
if (!(inp instanceof Function)) setOverwrite(inp); if (!(inp instanceof Function)) setOverwrite(inp);
else setOverwrite((s) => inp(s !== undefined ? s : initial)); else setOverwrite((s) => inp(s !== undefined ? s : initial));
}, },
[initial, setOverwrite] [initial, setOverwrite],
); );
const data = overwrite === undefined ? initial : overwrite; const data = overwrite === undefined ? initial : overwrite;
@ -48,7 +48,7 @@ export function useSettingsState(
colorB: string; colorB: string;
icon: string; icon: string;
} }
| undefined | undefined,
) { ) {
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] = const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
useDerived(proxyUrls); useDerived(proxyUrls);

View File

@ -4,9 +4,9 @@ import "./stores/__old/imports";
import "@/setup/ga"; import "@/setup/ga";
import "@/assets/css/index.css"; import "@/assets/css/index.css";
import React, { Suspense, useCallback } from "react"; import { StrictMode, Suspense, useCallback } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client";
import { HelmetProvider } from "react-helmet-async"; import { HelmetProvider } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BrowserRouter, HashRouter } from "react-router-dom"; import { BrowserRouter, HashRouter } from "react-router-dom";
@ -114,7 +114,7 @@ function AuthWrapper() {
{t( {t(
isCustomUrl isCustomUrl
? "screens.loadingUserError.textWithReset" ? "screens.loadingUserError.textWithReset"
: "screens.loadingUserError.text" : "screens.loadingUserError.text",
)} )}
</ErrorScreen> </ErrorScreen>
); );
@ -141,8 +141,11 @@ function TheRouter(props: { children: ReactNode }) {
return <HashRouter>{props.children}</HashRouter>; return <HashRouter>{props.children}</HashRouter>;
} }
ReactDOM.render( const container = document.getElementById("root");
<React.StrictMode> const root = createRoot(container!);
root.render(
<StrictMode>
<ErrorBoundary> <ErrorBoundary>
<TurnstileProvider /> <TurnstileProvider />
<HelmetProvider> <HelmetProvider>
@ -158,6 +161,5 @@ ReactDOM.render(
</Suspense> </Suspense>
</HelmetProvider> </HelmetProvider>
</ErrorBoundary> </ErrorBoundary>
</React.StrictMode>, </StrictMode>,
document.getElementById("root")
); );

View File

@ -1,18 +1,18 @@
import { useHistory } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
import { LoginFormPart } from "@/pages/parts/auth/LoginFormPart"; import { LoginFormPart } from "@/pages/parts/auth/LoginFormPart";
import { PageTitle } from "@/pages/parts/util/PageTitle"; import { PageTitle } from "@/pages/parts/util/PageTitle";
export function LoginPage() { export function LoginPage() {
const history = useHistory(); const navigate = useNavigate();
return ( return (
<SubPageLayout> <SubPageLayout>
<PageTitle subpage k="global.pages.login" /> <PageTitle subpage k="global.pages.login" />
<LoginFormPart <LoginFormPart
onLogin={() => { onLogin={() => {
history.push("/"); navigate("/");
}} }}
/> />
</SubPageLayout> </SubPageLayout>

Some files were not shown because too many files have changed in this diff Show More