Merge pull request #2 from sussy-code/dev

Merge dev for a new route
This commit is contained in:
Captain Jack Sparrow 2024-04-02 22:21:57 -04:00 committed by GitHub
commit 3d34ed5718
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 528 additions and 403 deletions

View File

@ -1,8 +1,8 @@
{
"name": "sudo-flix",
"name": "movie-web",
"version": "4.6.5",
"private": true,
"homepage": "https://sudo-flix.lol",
"homepage": "https://github.com/movie-web/movie-web",
"scripts": {
"dev": "vite",
"build": "vite build",
@ -36,7 +36,6 @@
"@scure/bip39": "^1.3.0",
"@sozialhelden/ietf-language-tags": "^5.4.2",
"@types/node-forge": "^1.3.11",
"@vercel/analytics": "^1.2.2",
"classnames": "^2.5.1",
"core-js": "^3.36.1",
"detect-browser": "^5.3.0",
@ -61,6 +60,7 @@
"react-google-recaptcha-v3": "^1.10.1",
"react-helmet-async": "^2.0.4",
"react-i18next": "^14.1.0",
"react-lazy-load-image-component": "^1.6.0",
"react-lazy-with-preload": "^2.2.1",
"react-router-dom": "^6.22.3",
"react-sticky-el": "^2.1.0",
@ -75,7 +75,6 @@
"@babel/core": "^7.24.3",
"@babel/preset-env": "^7.24.3",
"@babel/preset-typescript": "^7.24.1",
"@nuxt/eslint-config": "^0.2.0",
"@rollup/wasm-node": "^4.13.2",
"@types/chromecast-caf-sender": "^1.0.9",
"@types/crypto-js": "^4.2.2",
@ -86,9 +85,10 @@
"@types/lodash.throttle": "^4.1.9",
"@types/node": "^20.12.2",
"@types/pako": "^2.0.3",
"@types/react": "^18.2.73",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.23",
"@types/react-helmet": "^6.1.11",
"@types/react-lazy-load-image-component": "^1.6.3",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/react-stickynode": "^4.0.3",
@ -127,7 +127,8 @@
"vite-plugin-package-version": "^1.1.0",
"vite-plugin-pwa": "^0.17.5",
"vite-plugin-static-copy": "^1.0.2",
"vitest": "^1.4.0"
"vitest": "^1.4.0",
"workbox-window": "^7.0.0"
},
"pnpm": {
"overrides": {

View File

@ -42,9 +42,6 @@ dependencies:
'@types/node-forge':
specifier: ^1.3.11
version: 1.3.11
'@vercel/analytics':
specifier: ^1.2.2
version: 1.2.2(react@18.2.0)
classnames:
specifier: ^2.5.1
version: 2.5.1
@ -117,6 +114,9 @@ dependencies:
react-i18next:
specifier: ^14.1.0
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
react-lazy-load-image-component:
specifier: ^1.6.0
version: 1.6.0(react-dom@18.2.0)(react@18.2.0)
react-lazy-with-preload:
specifier: ^2.2.1
version: 2.2.1
@ -143,7 +143,7 @@ dependencies:
version: 2.1.2
zustand:
specifier: ^4.5.2
version: 4.5.2(@types/react@18.2.73)(immer@10.0.4)(react@18.2.0)
version: 4.5.2(@types/react@18.2.74)(immer@10.0.4)(react@18.2.0)
devDependencies:
'@babel/core':
@ -155,9 +155,6 @@ devDependencies:
'@babel/preset-typescript':
specifier: ^7.24.1
version: 7.24.1(@babel/core@7.24.3)
'@nuxt/eslint-config':
specifier: ^0.2.0
version: 0.2.0(eslint@8.57.0)
'@rollup/wasm-node':
specifier: ^4.13.2
version: 4.13.2
@ -189,14 +186,17 @@ devDependencies:
specifier: ^2.0.3
version: 2.0.3
'@types/react':
specifier: ^18.2.73
version: 18.2.73
specifier: ^18.2.74
version: 18.2.74
'@types/react-dom':
specifier: ^18.2.23
version: 18.2.23
'@types/react-helmet':
specifier: ^6.1.11
version: 6.1.11
'@types/react-lazy-load-image-component':
specifier: ^1.6.3
version: 1.6.3
'@types/react-router':
specifier: ^5.1.20
version: 5.1.20
@ -314,6 +314,9 @@ devDependencies:
vitest:
specifier: ^1.4.0
version: 1.4.0(@types/node@20.12.2)(jsdom@23.2.0)
workbox-window:
specifier: ^7.0.0
version: 7.0.0
packages:
@ -1939,21 +1942,6 @@ packages:
fastq: 1.17.1
dev: true
/@nuxt/eslint-config@0.2.0(eslint@8.57.0):
resolution: {integrity: sha512-NeJX8TLcnNAjQFiDs3XhP+9CHKK8jaKsP7eUyCSrQdgY7nqWe7VJx64lwzx5FTT4cW3RHMEyH+Y0qzLGYYoa/A==}
peerDependencies:
eslint: ^8.48.0
dependencies:
'@rushstack/eslint-patch': 1.10.1
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.3)
eslint: 8.57.0
eslint-plugin-vue: 9.24.0(eslint@8.57.0)
typescript: 5.4.3
transitivePeerDependencies:
- supports-color
dev: true
/@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@ -2094,10 +2082,6 @@ packages:
optionalDependencies:
fsevents: 2.3.3
/@rushstack/eslint-patch@1.10.1:
resolution: {integrity: sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==}
dev: true
/@scure/base@1.1.6:
resolution: {integrity: sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==}
dev: false
@ -2280,20 +2264,27 @@ packages:
/@types/react-dom@18.2.23:
resolution: {integrity: sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==}
dependencies:
'@types/react': 18.2.73
'@types/react': 18.2.74
dev: true
/@types/react-helmet@6.1.11:
resolution: {integrity: sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==}
dependencies:
'@types/react': 18.2.73
'@types/react': 18.2.74
dev: true
/@types/react-lazy-load-image-component@1.6.3:
resolution: {integrity: sha512-HsIsYz7yWWTh/bftdzGnijKD26JyofLRqM/RM80sxs7Gk13G83ew8R/ra2XzXuiZfjNEjAq/Va+NBHFF9ciwxA==}
dependencies:
'@types/react': 18.2.74
csstype: 3.1.3
dev: true
/@types/react-router-dom@5.3.3:
resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==}
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.73
'@types/react': 18.2.74
'@types/react-router': 5.1.20
dev: true
@ -2301,23 +2292,23 @@ packages:
resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==}
dependencies:
'@types/history': 4.7.11
'@types/react': 18.2.73
'@types/react': 18.2.74
dev: true
/@types/react-stickynode@4.0.3:
resolution: {integrity: sha512-K7YkwdhXQE4YVxIVweix4nkpdG4onm/dcnKK+qCj0vgUrNiKng+09zOfjF5AlOcC1HQkg5yxVLwp/0AzT84R0w==}
dependencies:
'@types/react': 18.2.73
'@types/react': 18.2.74
dev: true
/@types/react-transition-group@4.4.10:
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
dependencies:
'@types/react': 18.2.73
'@types/react': 18.2.74
dev: true
/@types/react@18.2.73:
resolution: {integrity: sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==}
/@types/react@18.2.74:
resolution: {integrity: sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==}
dependencies:
'@types/prop-types': 15.7.12
csstype: 3.1.3
@ -2472,21 +2463,6 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
/@vercel/analytics@1.2.2(react@18.2.0):
resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==}
peerDependencies:
next: '>= 13'
react: ^18 || ^19
peerDependenciesMeta:
next:
optional: true
react:
optional: true
dependencies:
react: 18.2.0
server-only: 0.0.1
dev: false
/@vitejs/plugin-react@4.2.1(vite@5.2.7):
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -2765,7 +2741,7 @@ packages:
postcss: '>=8.4.31'
dependencies:
browserslist: 4.23.0
caniuse-lite: 1.0.30001603
caniuse-lite: 1.0.30001605
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.0
@ -2843,6 +2819,7 @@ packages:
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: false
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
@ -2868,7 +2845,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001603
caniuse-lite: 1.0.30001605
electron-to-chromium: 1.4.723
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.23.0)
@ -2908,8 +2885,8 @@ packages:
engines: {node: '>= 6'}
dev: true
/caniuse-lite@1.0.30001603:
resolution: {integrity: sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==}
/caniuse-lite@1.0.30001605:
resolution: {integrity: sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==}
/chai@4.4.1:
resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
@ -3785,25 +3762,6 @@ packages:
string.prototype.matchall: 4.0.11
dev: true
/eslint-plugin-vue@9.24.0(eslint@8.57.0):
resolution: {integrity: sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==}
engines: {node: ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
eslint: 8.57.0
globals: 13.24.0
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 6.0.16
semver: 7.6.0
vue-eslint-parser: 9.4.2(eslint@8.57.0)
xml-name-validator: 4.0.0
transitivePeerDependencies:
- supports-color
dev: true
/eslint-scope@7.2.2:
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -4923,7 +4881,6 @@ packages:
/lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: true
/lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
@ -4940,6 +4897,10 @@ packages:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
dev: true
/lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
dev: false
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
@ -5197,6 +5158,7 @@ packages:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: false
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
@ -5710,6 +5672,18 @@ packages:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
/react-lazy-load-image-component@1.6.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-8KFkDTgjh+0+PVbH+cx0AgxLGbdTsxWMnxXzU5HEUztqewk9ufQAu8cstjZhyvtMIPsdMcPZfA0WAa7HtjQbBQ==}
peerDependencies:
react: ^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x
react-dom: ^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x
dependencies:
lodash.debounce: 4.0.8
lodash.throttle: 4.1.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-lazy-with-preload@2.2.1:
resolution: {integrity: sha512-ONSb8gizLE5jFpdHAclZ6EAAKuFX2JydnFXPPPjoUImZlLjGtKzyBS8SJgJq7CpLgsGKh9QCZdugJyEEOVC16Q==}
dev: false
@ -5944,7 +5918,7 @@ packages:
jest-worker: 26.6.2
rollup: /@rollup/wasm-node@4.13.2
serialize-javascript: 4.0.0
terser: 5.30.1
terser: 5.30.2
dev: true
/rollup-plugin-visualizer@5.12.0(@rollup/wasm-node@4.13.2):
@ -6064,10 +6038,6 @@ packages:
randombytes: 2.1.0
dev: true
/server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
dev: false
/set-cookie-parser@2.6.0:
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
dev: false
@ -6462,8 +6432,8 @@ packages:
unique-string: 2.0.0
dev: true
/terser@5.30.1:
resolution: {integrity: sha512-PJhOnRttZqqmIujxOQOMu4QuFGvh43lR7Youln3k6OJvmxwZ5FxK5rbCEh8XABRCpLf7ZnhrZuclCNCASsScnA==}
/terser@5.30.2:
resolution: {integrity: sha512-vTDjRKYKip4dOFL5VizdoxHTYDfEXPdz5t+FbxCC5Rp2s+KbEO8w5wqMDPgj7CtFKZuzq7PXv28fZoXfqqBVuw==}
engines: {node: '>=10'}
hasBin: true
dependencies:
@ -7033,24 +7003,6 @@ packages:
resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==}
dev: true
/vue-eslint-parser@9.4.2(eslint@8.57.0):
resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
engines: {node: ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: '>=6.0.0'
dependencies:
debug: 4.3.4
eslint: 8.57.0
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
espree: 9.6.1
esquery: 1.5.0
lodash: 4.17.21
semver: 7.6.0
transitivePeerDependencies:
- supports-color
dev: true
/w3c-xmlserializer@5.0.0:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
@ -7419,7 +7371,7 @@ packages:
engines: {node: '>=12.20'}
dev: true
/zustand@4.5.2(@types/react@18.2.73)(immer@10.0.4)(react@18.2.0):
/zustand@4.5.2(@types/react@18.2.74)(immer@10.0.4)(react@18.2.0):
resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==}
engines: {node: '>=12.7.0'}
peerDependencies:
@ -7434,7 +7386,7 @@ packages:
react:
optional: true
dependencies:
'@types/react': 18.2.73
'@types/react': 18.2.74
immer: 10.0.4
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)

20
public/config.js Normal file
View File

@ -0,0 +1,20 @@
window.__CONFIG__ = {
// The URL for the CORS proxy, the URL must NOT end with a slash!
// If not specified, the onboarding will not allow a "default setup". The user will have to use the extension or set up a proxy themselves
VITE_CORS_PROXY_URL: "https://sudo-proxy.up.railway.app",
// The READ API key to access TMDB
VITE_TMDB_READ_API_KEY: "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJhZTljNGE2ZDE1ZDFiODZiNzdlMWQyYmI5ZGY0MzdmYyIsInN1YiI6IjY1YjNmMWI0NTk0Yzk0MDE2MzNkZDBjNSIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.kAX7TkbKuJkNty6IsjcCLnoENFicVZn6d6DkLQsy3p8",
// The DMCA email displayed in the footer, null to hide the DMCA link
VITE_DMCA_EMAIL: null,
// Whether to disable hash-based routing, leave this as false if you don't know what this is
VITE_NORMAL_ROUTER: true,
// The backend URL to communicate with
VITE_BACKEND_URL: "https://backend.sudo-flix.lol",
// A comma separated list of disallowed IDs in the case of a DMCA claim - in the format "series-<id>" and "movie-<id>"
VITE_DISALLOWED_IDS: "",
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" fill="#fff" viewBox="0 -960 960 960" width="24"><path d="M300-240q25 0 42.5-17.5T360-300q0-25-17.5-42.5T300-360q-25 0-42.5 17.5T240-300q0 25 17.5 42.5T300-240Zm0-360q25 0 42.5-17.5T360-660q0-25-17.5-42.5T300-720q-25 0-42.5 17.5T240-660q0 25 17.5 42.5T300-600Zm180 180q25 0 42.5-17.5T540-480q0-25-17.5-42.5T480-540q-25 0-42.5 17.5T420-480q0 25 17.5 42.5T480-420Zm180 180q25 0 42.5-17.5T720-300q0-25-17.5-42.5T660-360q-25 0-42.5 17.5T600-300q0 25 17.5 42.5T660-240Zm0-360q25 0 42.5-17.5T720-660q0-25-17.5-42.5T660-720q-25 0-42.5 17.5T600-660q0 25 17.5 42.5T660-600ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Z"/></svg>

After

Width:  |  Height:  |  Size: 744 B

View File

@ -109,8 +109,7 @@
"pages": {
"about": "About",
"dmca": "DMCA",
"topSources": "Top Sources",
"topFlix": "Top Flix",
"discover": "Discover",
"support": "Support",
"login": "Login",
"onboarding": "Setup",

View File

@ -152,7 +152,7 @@ const headers = {
Authorization: `Bearer ${apiKey}`,
};
async function get<T>(url: string, params?: object): Promise<T> {
export async function get<T>(url: string, params?: object): Promise<T> {
if (!apiKey) throw new Error("TMDB API key not set");
const res = await proxiedFetch<any>(encodeURI(url), {

View File

@ -80,7 +80,7 @@ export function AboutPage() {
className="py-px mt-8 box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center"
onClick={() => navigate("/flix")}
>
Top Flix
Discover
</Button>
<Button
className="py-px mt-8 box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center"

View File

@ -1,14 +1,65 @@
import classNames from "classnames";
import { ReactNode, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; // Import Link from react-router-dom
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { LazyLoadImage } from "react-lazy-load-image-component";
import { useNavigate } from "react-router-dom";
import { ThiccContainer } from "@/components/layout/ThinContainer";
import { Divider } from "@/components/utils/Divider";
import { Heading1, Paragraph } from "@/components/utils/Text";
import { BACKEND_URL } from "@/setup/constants";
import "react-lazy-load-image-component/src/effects/blur.css";
import { ThinContainer } from "@/components/layout/ThinContainer";
import { WideContainer } from "@/components/layout/WideContainer";
import { HomeLayout } from "@/pages/layouts/HomeLayout";
import { conf } from "@/setup/config";
import { SubPageLayout } from "./layouts/SubPageLayout";
import { PageTitle } from "./parts/util/PageTitle";
import { get } from "../backend/metadata/tmdb";
import { Icon, Icons } from "../components/Icon";
const pagesToFetch = 5;
// Define the Media type
interface Media {
id: number;
poster_path: string;
title?: string;
name?: string;
}
// Update the Movie and TVShow interfaces to extend the Media interface
interface Movie extends Media {
title: string;
}
interface TVShow extends Media {
name: string;
}
// Define the Genre type
interface Genre {
id: number;
name: string;
}
// Define the Category type
interface Category {
name: string;
endpoint: string;
}
// Define the categories
const categories: Category[] = [
{
name: "Now Playing",
endpoint: "/movie/now_playing?language=en-US",
},
{
name: "Popular",
endpoint: "/movie/popular?language=en-US",
},
{
name: "Top Rated",
endpoint: "/movie/top_rated?language=en-US",
},
];
export function Button(props: {
className: string;
@ -31,297 +82,401 @@ export function Button(props: {
);
}
function isShowOrMovie(tmdbFullId: string): "series" | "movie" | "unknown" {
if (tmdbFullId.includes("show-")) {
return "series";
}
if (tmdbFullId.includes("movie-")) {
return "movie";
}
return "unknown";
}
function directLinkToContent(tmdbFullId: string) {
if (isShowOrMovie(tmdbFullId) === "series") {
return `/media/tmdb-tv-${tmdbFullId.split("-")[1]}`;
}
if (isShowOrMovie(tmdbFullId) === "movie") {
return `/media/tmdb-movie-${tmdbFullId.split("-")[1]}`;
}
return null;
}
function ConfigValue(props: {
name: string;
type: string;
id: string;
children?: ReactNode;
}) {
const navigate = useNavigate();
const link = directLinkToContent(props.id);
return (
<>
<div className="flex">
<p className="flex-1 font-bold text-white pr-5 pl-3">
{link ? (
<p
onClick={() => navigate(link)}
className="transition duration-200 hover:underline cursor-pointer"
>
{props.name}
</p>
) : (
<p>{props.name}</p>
)}
</p>
<p className="pr-3 cursor-default">{props.children}</p>
</div>
<p className="pr-5 pl-3 cursor-default">
{props.type.charAt(0).toUpperCase() + props.type.slice(1)}
</p>
<Divider marginClass="my-3" />
</>
);
}
async function getRecentPlayedItems() {
const response = await fetch(`${BACKEND_URL}/metrics`);
const text = await response.text();
const regex =
/mw_media_watch_count{tmdb_full_id="([^"]+)",provider_id="([^"]+)",title="([^"]+)",success="([^"]+)"} (\d+)/g;
let match;
const loop = true;
const items: { [key: string]: any } = {};
while (loop) {
match = regex.exec(text);
if (match === null) break;
const [_, tmdbFullId, providerId, title, success, count] = match;
if (items[tmdbFullId]) {
items[tmdbFullId].count += parseInt(count, 10);
} else {
items[tmdbFullId] = {
tmdbFullId,
providerId,
title,
success: success === "true",
count: parseInt(count, 10),
};
}
}
if (Object.keys(items).length > 0) {
return Object.values(items);
}
throw new Error("RECENT_PLAYED_ITEMS not found");
}
async function getTotalViews() {
const response = await fetch(`${BACKEND_URL}/metrics`);
const text = await response.text();
// Add up all mw_media_watch_count entries
const regex = /mw_media_watch_count{[^}]*} (\d+)/g;
let totalViews = 0;
let match = regex.exec(text);
while (match !== null) {
totalViews += parseInt(match[1], 10);
match = regex.exec(text);
}
if (totalViews > 0) {
return totalViews.toString();
}
throw new Error("TOTAL_VIEWS not found");
}
function getProcessStartTime(): Promise<string> {
return fetch(`${BACKEND_URL}/metrics`)
.then((response) => response.text())
.then((text) => {
const regex = /process_start_time_seconds (\d+)/;
const match = text.match(regex);
if (match) {
const parsedNum = parseInt(match[1], 10);
const date = new Date(parsedNum * 1000);
return date.toISOString();
}
throw new Error("PROCESS_START_TIME_SECONDS not found");
});
}
async function getTimeSinceProcessStart(): Promise<string> {
const processStartTime = await getProcessStartTime();
const currentTime = new Date();
const timeDifference =
currentTime.getTime() - new Date(processStartTime).getTime();
const hours = Math.floor(timeDifference / (1000 * 60 * 60));
const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000);
const days = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
if (days > 0) {
if (days === 1) {
return `${days} day`;
}
return `${days} days`;
}
if (hours > 0) {
return `${hours} hours`;
}
if (minutes > 0) {
return `${minutes} minutes`;
}
return `${seconds} seconds`;
}
export function TopFlix() {
const [recentPlayedItems, setRecentPlayedItems] = useState<any[]>([]);
const [totalViews, setTotalViews] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 10;
const maxItemsToShow = 100; // Maximum items to show
const maxPageCount = Math.ceil(maxItemsToShow / itemsPerPage); // Calculate max page count based on maxItemsToShow
const [timeSinceProcessStart, setTimeSinceProcessStart] = useState<
string | null
>(null);
const { t } = useTranslation();
const [showBg] = useState<boolean>(false);
const [genres, setGenres] = useState<Genre[]>([]);
const [randomMovie, setRandomMovie] = useState<Movie | null>(null); // Add this line
const [genreMovies, setGenreMovies] = useState<{
[genreId: number]: Movie[];
}>({});
const [countdown, setCountdown] = useState<number | null>(null);
const navigate = useNavigate();
useEffect(() => {
getRecentPlayedItems()
.then((items) => {
// Limit the items to the first 100 to ensure we don't exceed the max page count
const limitedItems = items
.slice(0, maxItemsToShow)
.filter(
(item, index, self) =>
index ===
self.findIndex((t2) => t2.tmdbFullId === item.tmdbFullId),
);
setRecentPlayedItems(limitedItems);
})
.catch((error) => {
console.error("Error fetching recent played items:", error);
});
getTotalViews()
.then((views) => {
setTotalViews(parseInt(views, 10).toLocaleString());
})
.catch((error) => {
console.error("Error fetching total views:", error);
});
}, []);
// Add a new state variable for the category movies
const [categoryMovies, setCategoryMovies] = useState<{
[categoryName: string]: Movie[];
}>({});
useEffect(() => {
getTimeSinceProcessStart()
.then((time) => {
setTimeSinceProcessStart(time);
})
.catch((error) => {
console.error("Error fetching time since process start:", error);
const fetchMoviesForCategory = async (category: Category) => {
try {
const movies: any[] = [];
for (let page = 1; page <= pagesToFetch; page += 1) {
const data = await get<any>(category.endpoint, {
api_key: conf().TMDB_READ_API_KEY,
language: "en-US",
page: page.toString(),
});
}, []);
function getItemsForCurrentPage() {
const sortedItems = recentPlayedItems.sort((a, b) => b.count - a.count);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return sortedItems.slice(startIndex, endIndex).map((item, index) => ({
...item,
rank: startIndex + index + 1,
movies.push(...data.results);
}
setCategoryMovies((prevCategoryMovies) => ({
...prevCategoryMovies,
[category.name]: movies,
}));
}
return (
<SubPageLayout>
<ThiccContainer>
<PageTitle subpage k="global.pages.topFlix" />
<div className="mt-8 w-full px-8 cursor-default">
<Heading1>Top flix</Heading1>
<Paragraph className="mb-6">
The top 100 most-watched movies on sudo-flix.lol, sourced directly
from the most recent sudo-backend deployment. The backend is
redeployed frequently which may result in low numbers being shown
here.
</Paragraph>
<div className="mt-2 w-full">
<div className="flex justify-center">
<div className="bg-buttons-secondary rounded-xl scale-95 py-3 px-5 mb-2">
<p className="font-bold text-buttons-secondaryText">
Server Lifetime: {timeSinceProcessStart}
</p>
</div>
<div className="bg-buttons-secondary rounded-xl scale-95 py-3 px-5 mb-2">
<p className="font-bold text-buttons-secondaryText">
Overall Views: {totalViews}
</p>
</div>
</div>
<div className="flex justify-center">
<Button
className="py-px w-60 box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center inline-block"
onClick={() => navigate("/flix/sources")}
>
Most used providers
</Button>
</div>
</div>
</div>
<div className="pl-6 pr-6">
<Divider marginClass="my-3" />
{getItemsForCurrentPage().map((item) => {
return (
<ConfigValue
key={item.tmdbFullId}
type={isShowOrMovie(item.tmdbFullId)}
id={item.tmdbFullId}
name={item.title}
>
{`${
item.providerId.charAt(0).toUpperCase() +
item.providerId.slice(1)
}`}{" "}
<strong>-</strong> {`Views: `}
<strong>{parseInt(item.count, 10).toLocaleString()}</strong>
</ConfigValue>
} catch (error) {
console.error(
`Error fetching movies for category ${category.name}:`,
error,
);
})}
</div>
<div
style={{ display: "flex", justifyContent: "space-between" }}
className="mt-5 w-full px-8"
>
<Button
className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center"
onClick={() =>
setCurrentPage(currentPage === 1 ? maxPageCount : currentPage - 1)
}
>
Previous page
</Button>
<div
style={{ display: "flex", alignItems: "center", cursor: "default" }}
>
{currentPage}/{maxPageCount}
</div>
<Button
className="py-px box-content bg-buttons-secondary hover:bg-buttons-secondaryHover bg-opacity-90 text-buttons-secondaryText justify-center items-center"
onClick={() =>
setCurrentPage(currentPage === maxPageCount ? 1 : currentPage + 1)
};
categories.forEach(fetchMoviesForCategory);
}, []);
// Add a new state variable for the TV show genres
const [tvGenres, setTVGenres] = useState<Genre[]>([]);
// Add a new state variable for the TV shows
const [tvShowGenres, setTVShowGenres] = useState<{
[genreId: number]: TVShow[];
}>({});
// Fetch TV show genres
useEffect(() => {
const fetchTVGenres = async () => {
try {
const data = await get<any>("/genre/tv/list", {
api_key: conf().TMDB_READ_API_KEY,
language: "en-US",
});
// Shuffle the array of genres
for (let i = data.genres.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[data.genres[i], data.genres[j]] = [data.genres[j], data.genres[i]];
}
// Fetch only the first 5 TV show genres
setTVGenres(data.genres.slice(0, 5));
} catch (error) {
console.error("Error fetching TV show genres:", error);
}
};
fetchTVGenres();
}, []);
// Fetch TV shows for each genre
useEffect(() => {
const fetchTVShowsForGenre = async (genreId: number) => {
try {
const tvShows: any[] = [];
for (let page = 1; page <= 5; page += 1) {
// Fetch only 5 pages
const data = await get<any>("/discover/tv", {
api_key: conf().TMDB_READ_API_KEY,
with_genres: genreId.toString(),
language: "en-US",
page: page.toString(),
});
tvShows.push(...data.results);
}
setTVShowGenres((prevTVShowGenres) => ({
...prevTVShowGenres,
[genreId]: tvShows,
}));
} catch (error) {
console.error(`Error fetching TV shows for genre ${genreId}:`, error);
}
};
tvGenres.forEach((genre) => fetchTVShowsForGenre(genre.id));
}, [tvGenres]);
// Move the hooks outside of the renderMovies function
const carouselRef = useRef<HTMLDivElement>(null);
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const gradientRef = useRef<HTMLDivElement>(null);
// Update the scrollCarousel function to use the new ref map
function scrollCarousel(categorySlug: string, direction: string) {
const carousel = carouselRefs.current[categorySlug];
if (carousel) {
const movieElements = carousel.getElementsByTagName("a");
if (movieElements.length > 0) {
const movieWidth = movieElements[0].offsetWidth;
const visibleMovies = Math.floor(carousel.offsetWidth / movieWidth);
const scrollAmount = movieWidth * visibleMovies;
if (direction === "left") {
carousel.scrollBy({ left: -scrollAmount, behavior: "smooth" });
} else {
carousel.scrollBy({ left: scrollAmount, behavior: "smooth" });
}
}
}
}
const [movieWidth, setMovieWidth] = useState(
window.innerWidth < 600 ? "150px" : "200px",
);
useEffect(() => {
const handleResize = () => {
setMovieWidth(window.innerWidth < 600 ? "150px" : "200px");
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
useEffect(() => {
if (carouselRef.current && gradientRef.current) {
const carouselHeight = carouselRef.current.getBoundingClientRect().height;
gradientRef.current.style.top = `${carouselHeight}px`;
gradientRef.current.style.bottom = `${carouselHeight}px`;
}
}, [movieWidth]); // Added movieWidth to the dependency array
function renderMovies(medias: Media[], category: string, isTVShow = false) {
const categorySlug = category.toLowerCase().replace(/ /g, "-"); // Convert the category to a slug
const displayCategory =
category === "Now Playing"
? "In Cinemas"
: category.includes("Movie")
? `${category}s`
: isTVShow
? `${category} Programmes`
: `${category} Movies`;
return (
<div className="relative overflow-hidden mt-4 rounded-xl">
<h2 className="text-2xl font-bold text-white sm:text-3xl md:text-2xl mx-auto pl-2">
{displayCategory}
</h2>
<div
id={`carousel-${categorySlug}`}
className="flex whitespace-nowrap overflow-auto scroll-snap-x-mandatory mb-4 mt-4"
ref={(el) => {
carouselRefs.current[categorySlug] = el;
}}
style={{ overflowX: "hidden" }}
>
Next page
</Button>
{medias.slice(0, 20).map((media) => (
<a
key={media.id}
href={`media/tmdb-${isTVShow ? "tv" : "movie"}-${media.id}-${
isTVShow ? media.name : media.title
}`}
rel="noopener noreferrer"
className="block text-center relative overflow-hidden transition-transform transform hover:scale-95 mr-4"
style={{ flex: `0 0 ${movieWidth}` }} // Set a fixed width for each movie
>
<LazyLoadImage
src={`https://image.tmdb.org/t/p/w500${media.poster_path}`}
alt={isTVShow ? media.name : media.title}
className="rounded-xl"
effect="blur"
style={{
width: "100%",
height: "100%",
transition: "opacity 0.3s, transform 0.3s",
}}
/>
</a>
))}
</div>
</ThiccContainer>
</SubPageLayout>
<button
type="button" // Added type attribute with value "button"
title="Back"
className="absolute top-1/2 transform -translate-y-1/2 z-10 left-2"
onClick={() => scrollCarousel(categorySlug, "left")}
>
<div className="cursor-pointer text-white flex justify-center items-center h-10 w-10 rounded-full bg-search-hoverBackground active:scale-110 transition-[transform,background-color] duration-200">
<Icon icon={Icons.ARROW_LEFT} />
</div>
</button>
<button
type="button" // Added type attribute with value "button"
title="Next"
className="absolute top-1/2 right-2 transform -translate-y-1/2 z-10"
onClick={() => scrollCarousel(categorySlug, "right")}
>
<div className="cursor-pointer text-white flex justify-center items-center h-10 w-10 rounded-full bg-search-hoverBackground active:scale-110 transition-[transform,background-color] duration-200">
<Icon icon={Icons.ARROW_RIGHT} />
</div>
</button>
</div>
);
}
const handleRandomMovieClick = () => {
const allMovies = Object.values(genreMovies).flat(); // Flatten all movie arrays
const uniqueTitles = new Set<string>(); // Use a Set to store unique titles
allMovies.forEach((movie) => uniqueTitles.add(movie.title)); // Add each title to the Set
const uniqueTitlesArray = Array.from(uniqueTitles); // Convert the Set back to an array
const randomIndex = Math.floor(Math.random() * uniqueTitlesArray.length);
const selectedMovie = allMovies.find(
(movie) => movie.title === uniqueTitlesArray[randomIndex],
);
if (selectedMovie) {
setRandomMovie(selectedMovie);
setCountdown(5);
// Schedule navigation after 5 seconds
setTimeout(() => {
navigate(
`/media/tmdb-movie-${selectedMovie.id}-${selectedMovie.title}`,
);
}, 5000);
}
};
// Fetch Movie genres
useEffect(() => {
const fetchGenres = async () => {
try {
const data = await get<any>("/genre/movie/list", {
api_key: conf().TMDB_READ_API_KEY,
language: "en-US",
});
// Shuffle the array of genres
for (let i = data.genres.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[data.genres[i], data.genres[j]] = [data.genres[j], data.genres[i]];
}
// Fetch only the first 5 genres
setGenres(data.genres.slice(0, 5));
} catch (error) {
console.error("Error fetching genres:", error);
}
};
fetchGenres();
}, []);
// Fetch movies for each genre
useEffect(() => {
const fetchMoviesForGenre = async (genreId: number) => {
try {
const movies: any[] = [];
for (let page = 1; page <= 5; page += 1) {
// Fetch only 5 pages
const data = await get<any>("/discover/movie", {
api_key: conf().TMDB_READ_API_KEY,
with_genres: genreId.toString(),
language: "en-US",
page: page.toString(),
});
movies.push(...data.results);
}
setGenreMovies((prevGenreMovies) => ({
...prevGenreMovies,
[genreId]: movies,
}));
} catch (error) {
console.error(`Error fetching movies for genre ${genreId}:`, error);
}
};
genres.forEach((genre) => fetchMoviesForGenre(genre.id));
}, [genres]);
useEffect(() => {
let countdownInterval: NodeJS.Timeout;
if (countdown !== null && countdown > 0) {
countdownInterval = setInterval(() => {
setCountdown((prevCountdown) =>
prevCountdown !== null ? prevCountdown - 1 : prevCountdown,
);
}, 1000);
}
return () => {
clearInterval(countdownInterval);
};
}, [countdown]);
return (
<HomeLayout showBg={showBg}>
<div className="mb-16 sm:mb-2">
<PageTitle subpage k="global.pages.discover" />
<ThinContainer>
<div className="mt-44 space-y-16 text-center">
<div className="relative z-10 mb-16">
<h1 className="text-4xl font-bold text-white">
{t("global.pages.discover")}
</h1>
</div>
</div>
</ThinContainer>
</div>
<WideContainer>
<>
<div className="flex items-center justify-center mb-6">
<button
type="button"
className="flex items-center space-x-2 rounded-full px-4 text-white py-2 bg-pill-background bg-opacity-50 hover:bg-pill-backgroundHover transition-[background,transform] duration-100 hover:scale-105"
onClick={handleRandomMovieClick}
disabled={countdown !== null && countdown > 0} // Disable the button during the countdown
>
<span className="flex items-center space-x-2">
{countdown !== null && countdown > 0
? `Playing in ${countdown} seconds`
: "Watch Something New"}
<img
src="/lightbar-images/dice.svg"
alt="Small Image"
style={{
width: "20px", // Adjust the width as needed
height: "20px", // Adjust the height as needed
marginLeft: "10px", // Add margin-right
}}
/>
</span>
</button>
</div>
{randomMovie && (
<div className="mt-4 mb-4 text-center">
<p>Now Playing {randomMovie.title}</p>
{/* You can add additional details or play functionality here */}
</div>
)}
<div className="flex flex-col">
{categories.map((category) => (
<div
key={category.name}
id={`carousel-${category.name
.toLowerCase()
.replace(/ /g, "-")}`}
className="mt-8"
>
{renderMovies(
categoryMovies[category.name] || [],
category.name,
)}
</div>
))}
{genres.map((genre) => (
<div
key={genre.id}
id={`carousel-${genre.name.toLowerCase().replace(/ /g, "-")}`}
className="mt-8"
>
{renderMovies(genreMovies[genre.id] || [], genre.name)}
</div>
))}
{tvGenres.map((genre) => (
<div
key={genre.id}
id={`carousel-${genre.name.toLowerCase().replace(/ /g, "-")}`}
className="mt-8"
>
{renderMovies(tvShowGenres[genre.id] || [], genre.name, true)}
</div>
))}
</div>
</>
</WideContainer>
</HomeLayout>
);
}

View File

@ -99,7 +99,7 @@ export function CaptionsPart(props: {
onChange={(v) =>
props.setStyling({ ...props.styling, backgroundBlur: v / 100 })
}
value={props.styling.backgroundBlur * 100}
value={props.styling.backgroundBlur * 1}
textTransformer={(s) => `${s}%`}
/>
<CaptionSetting

View File

@ -26,7 +26,6 @@ import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy";
import { RegisterPage } from "@/pages/Register";
import { SupportPage } from "@/pages/Support";
import { TopFlix } from "@/pages/TopFlix";
import { TopSources } from "@/pages/TopSources";
import { Layout } from "@/setup/Layout";
import { useHistoryListener } from "@/stores/history";
import { LanguageProvider } from "@/stores/language";
@ -153,7 +152,6 @@ function App() {
<Route path="/support" element={<SupportPage />} />
{/* Top flix page */}
<Route path="/flix" element={<TopFlix />} />
<Route path="/flix/sources" element={<TopSources />} />
{/* Settings page */}
<Route
path="/settings"

View File

@ -26,7 +26,6 @@ import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy";
import { RegisterPage } from "@/pages/Register";
import { SupportPage } from "@/pages/Support";
import { TopFlix } from "@/pages/TopFlix";
import { TopSources } from "@/pages/TopSources";
import { Layout } from "@/setup/Layout";
import { useHistoryListener } from "@/stores/history";
import { LanguageProvider } from "@/stores/language";
@ -87,7 +86,6 @@ function QueryView() {
return null;
}
// Duplicate of @/src/setup/App.tsx
function App() {
useHistoryListener();
useOnlineListener();
@ -154,7 +152,6 @@ function App() {
<Route path="/support" element={<SupportPage />} />
{/* Top flix page */}
<Route path="/flix" element={<TopFlix />} />
<Route path="/flix/sources" element={<TopSources />} />
{/* Settings page */}
<Route
path="/settings"
@ -182,3 +179,5 @@ function App() {
</Layout>
);
}
export default App;