Browse Source

Merge pull request #929 from movie-web/dev

Version 4.5.0
pull/1017/head 4.5.0
William Oldham 1 year ago committed by GitHub
parent
commit
1acefd75fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .github/CODE_OF_CONDUCT.md
  2. 7
      .github/CONTRIBUTING.md
  3. 9
      .github/SECURITY.md
  4. 4
      Dockerfile
  5. 6
      README.md
  6. 7
      package.json
  7. 53
      pnpm-lock.yaml
  8. 5
      public/config.js
  9. 17
      src/assets/locales/ar.json
  10. 13
      src/assets/locales/bg.json
  11. 17
      src/assets/locales/bn.json
  12. 13
      src/assets/locales/ca.json
  13. 17
      src/assets/locales/cs.json
  14. 13
      src/assets/locales/de.json
  15. 13
      src/assets/locales/el.json
  16. 12
      src/assets/locales/en.json
  17. 13
      src/assets/locales/es.json
  18. 13
      src/assets/locales/et.json
  19. 13
      src/assets/locales/fa.json
  20. 13
      src/assets/locales/fi-FI.json
  21. 9
      src/assets/locales/fr.json
  22. 13
      src/assets/locales/gl.json
  23. 13
      src/assets/locales/gu.json
  24. 17
      src/assets/locales/he.json
  25. 13
      src/assets/locales/hi.json
  26. 13
      src/assets/locales/id.json
  27. 13
      src/assets/locales/is-IS.json
  28. 13
      src/assets/locales/it.json
  29. 5
      src/assets/locales/ja.json
  30. 13
      src/assets/locales/ko.json
  31. 13
      src/assets/locales/lv.json
  32. 13
      src/assets/locales/minion.json
  33. 13
      src/assets/locales/ne.json
  34. 13
      src/assets/locales/nl.json
  35. 13
      src/assets/locales/pa.json
  36. 5
      src/assets/locales/pirate.json
  37. 13
      src/assets/locales/pl.json
  38. 13
      src/assets/locales/pt-BR.json
  39. 13
      src/assets/locales/pt-PT.json
  40. 13
      src/assets/locales/ro.json
  41. 9
      src/assets/locales/ru.json
  42. 9
      src/assets/locales/sl.json
  43. 13
      src/assets/locales/sv.json
  44. 13
      src/assets/locales/th.json
  45. 17
      src/assets/locales/tok.json
  46. 13
      src/assets/locales/tr.json
  47. 13
      src/assets/locales/uk.json
  48. 5
      src/assets/locales/vi.json
  49. 17
      src/assets/locales/zh-Hant.json
  50. 17
      src/assets/locales/zh.json
  51. 7
      src/backend/helpers/report.ts
  52. 20
      src/backend/helpers/subs.ts
  53. 10
      src/components/buttons/Button.tsx
  54. 16
      src/components/player/atoms/Volume.tsx
  55. 6
      src/hooks/auth/useAuth.ts
  56. 2
      src/hooks/auth/useBackendUrl.ts
  57. 7
      src/hooks/useSettingsState.ts
  58. 34
      src/pages/Settings.tsx
  59. 4
      src/pages/layouts/SubPageLayout.tsx
  60. 8
      src/pages/onboarding/Onboarding.tsx
  61. 2
      src/pages/onboarding/OnboardingExtension.tsx
  62. 2
      src/pages/onboarding/OnboardingProxy.tsx
  63. 14
      src/pages/parts/admin/BackendTestPart.tsx
  64. 8
      src/pages/parts/admin/TMDBTestPart.tsx
  65. 20
      src/pages/parts/admin/WorkerTestPart.tsx
  66. 3
      src/pages/parts/auth/LoginFormPart.tsx
  67. 22
      src/pages/parts/auth/TrustBackendPart.tsx
  68. 5
      src/pages/parts/auth/VerifyPassphrasePart.tsx
  69. 6
      src/pages/parts/home/HeroPart.tsx
  70. 2
      src/pages/parts/settings/AccountActionsPart.tsx
  71. 4
      src/pages/parts/settings/ConnectionsPart.tsx
  72. 1
      src/pages/parts/settings/DeviceListPart.tsx
  73. 7
      src/pages/parts/settings/SidebarPart.tsx
  74. 27
      src/pages/parts/settings/ThemePart.tsx
  75. 6
      src/setup/constants.ts
  76. 2
      src/setup/ga.ts
  77. 1
      src/stores/bookmarks/BookmarkSyncer.tsx
  78. 1
      src/stores/progress/ProgressSyncer.tsx
  79. 1
      src/stores/subtitles/SettingsSyncer.tsx
  80. 21
      src/stores/theme/index.tsx

2
.github/CODE_OF_CONDUCT.md

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
codeofconduct@movie-web.app. our [Discord](https://movie-web.github.io/links/discord).
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

7
.github/CONTRIBUTING.md

@ -1,6 +1,6 @@
# Contributing Guidelines for movie-web # Contributing Guidelines for movie-web
Thank you for investing your time in contributing to our project! Your contribution will be reflected on [movie-web.app](https://movie-web.app). Thank you for investing your time in contributing to our project! Your contribution will be reflected on all of the community hosted instances that are on the latest version.
Please read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. Please read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable.
@ -33,7 +33,7 @@ There are two places where to request features or report bugs:
### Discord Server ### Discord Server
If you do not have a GitHub account or want to discuss a feature or bug with us before making an issue, you can join our Discord server. If you do not have a GitHub account or want to discuss a feature or bug with us before making an issue, you can join our Discord server.
<a href="https://discord.movie-web.app"><img src="https://discord.com/api/guilds/871713465100816424/widget.png?style=banner2" alt="Discord Server"></a> <a href="https://movie-web.github.io/links/discord"><img src="https://discord.com/api/guilds/871713465100816424/widget.png?style=banner2" alt="Discord Server"></a>
### GitHub Issues ### GitHub Issues
To make a GitHub issue for movie-web, please visit the [new issue page](https://github.com/movie-web/movie-web/issues/new/choose) where you can pick either the "Bug Report" or "Feature Request" template. To make a GitHub issue for movie-web, please visit the [new issue page](https://github.com/movie-web/movie-web/issues/new/choose) where you can pick either the "Bug Report" or "Feature Request" template.
@ -85,7 +85,8 @@ Here are some tips to make sure that your pull requests are :pinched_fingers: fi
### Language Contributions ### Language Contributions
Language contributions help movie-web massively, allowing people worldwide to use our app! Language contributions help movie-web massively, allowing people worldwide to use our app!
We use weblate for crowdsourcing our translations. [Click here to go to our translation tool.](https://weblate.movie-web.app/projects/movie-web/website/) We use Weblate for crowdsourcing our translations. [Click here to go to our translation tool.](https://movie-web.github.io/links/weblate)
1. First make sure you make an account. (click the link above) 1. First make sure you make an account. (click the link above)
2. Click the language you want to help translate, if it's not listed you can click the plus top left to add a new language. 2. Click the language you want to help translate, if it's not listed you can click the plus top left to add a new language.

9
.github/SECURITY.md

@ -2,12 +2,9 @@
## Supported Versions ## Supported Versions
The movie-web maintainers only support the latest version of movie-web published at https://movie-web.app. The latest version of movie-web is the only version that is supported, as it is the only version that is being actively developed.
Support is not provided for any forks or mirrors of movie-web.
## Reporting a Vulnerability ## Reporting a Vulnerability
There are two ways you can contact the movie-web maintainers to report a vulnerability: You can contact the movie-web maintainers to report a vulnerability:
- Email [security@movie-web.app](mailto:security@movie-web.app) - Report the vulnerability in the [movie-web Discord server](https://movie-web.github.io/links/discord)
- Report the vulnerability in the [movie-web Discord server](https://discord.movie-web.app)

4
Dockerfile

@ -8,6 +8,10 @@ COPY package.json ./
COPY pnpm-lock.yaml ./ COPY pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
ARG PWA_ENABLED="false"
ENV VITE_PWA_ENABLED=${PWA_ENABLED}
COPY . ./ COPY . ./
RUN pnpm run build RUN pnpm run build

6
README.md

@ -4,13 +4,13 @@
<p align="center"> <p align="center">
<img src="https://skillicons.dev/icons?i=react,vite,ts" /> <img src="https://skillicons.dev/icons?i=react,vite,ts" />
<br/> <br/>
<a href="https://discord.movie-web.app"><kbd>🔵 discord</kbd></a> <a href="https://movie-web.app"><kbd>🟢 website</kbd></a> <a href="https://movie-web.github.io/links/discord"><kbd>🔵 discord</kbd></a> <a href="https://movie-web.github.io/docs"><kbd>🟢 docs</kbd></a>
</p> </p>
<br/><br/> <br/><br/>
# ⚡What is movie-web? # ⚡What is movie-web?
movie-web is a web app for watching movies easily. Check it out at <a href="https://movie-web.app"><kbd>movie-web.app</kbd></a>. movie-web is a web app for watching movies easily.
This service works by displaying video files from third-party providers inside an intuitive and aesthetic user interface. This service works by displaying video files from third-party providers inside an intuitive and aesthetic user interface.
@ -57,7 +57,7 @@ pnpm build
A simple guide has been written to assist in hosting your own instance of movie-web. Check it out below A simple guide has been written to assist in hosting your own instance of movie-web. Check it out below
|[Selfhosting guide](https://docs.movie-web.app)| |[Selfhosting guide](https://movie-web.github.io/docs)|
|---| |---|
## 🤝 Thanks to all Contributors ## 🤝 Thanks to all Contributors

7
package.json

@ -1,8 +1,8 @@
{ {
"name": "movie-web", "name": "movie-web",
"version": "4.4.2", "version": "4.5.0",
"private": true, "private": true,
"homepage": "https://movie-web.app", "homepage": "https://github.com/movie-web/movie-web",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
@ -123,7 +123,8 @@
"vite-plugin-package-version": "^1.1.0", "vite-plugin-package-version": "^1.1.0",
"vite-plugin-pwa": "^0.17.4", "vite-plugin-pwa": "^0.17.4",
"vite-plugin-static-copy": "^1.0.0", "vite-plugin-static-copy": "^1.0.0",
"vitest": "^1.1.0" "vitest": "^1.1.0",
"workbox-window": "^7.0.0"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {

53
pnpm-lock.yaml

@ -268,7 +268,7 @@ devDependencies:
version: 0.5.9(prettier@3.1.1) version: 0.5.9(prettier@3.1.1)
rollup-plugin-visualizer: rollup-plugin-visualizer:
specifier: ^5.11.0 specifier: ^5.11.0
version: 5.11.0(@rollup/wasm-node@4.10.0) version: 5.11.0(@rollup/wasm-node@4.12.0)
tailwind-scrollbar: tailwind-scrollbar:
specifier: ^3.0.5 specifier: ^3.0.5
version: 3.0.5(tailwindcss@3.4.0) version: 3.0.5(tailwindcss@3.4.0)
@ -302,6 +302,9 @@ devDependencies:
vitest: vitest:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0(@types/node@20.10.5)(jsdom@23.0.1) version: 1.1.0(@types/node@20.10.5)(jsdom@23.0.1)
workbox-window:
specifier: ^7.0.0
version: 7.0.0
packages: packages:
@ -2059,7 +2062,7 @@ packages:
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
dev: false dev: false
/@rollup/plugin-babel@5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.10.0): /@rollup/plugin-babel@5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.12.0):
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
peerDependencies: peerDependencies:
@ -2072,36 +2075,36 @@ packages:
dependencies: dependencies:
'@babel/core': 7.23.6 '@babel/core': 7.23.6
'@babel/helper-module-imports': 7.22.15 '@babel/helper-module-imports': 7.22.15
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.10.0) '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.12.0)
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
dev: true dev: true
/@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.10.0): /@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.12.0):
resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
peerDependencies: peerDependencies:
rollup: npm:@rollup/wasm-node rollup: npm:@rollup/wasm-node
dependencies: dependencies:
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.10.0) '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.12.0)
'@types/resolve': 1.17.1 '@types/resolve': 1.17.1
builtin-modules: 3.3.0 builtin-modules: 3.3.0
deepmerge: 4.3.1 deepmerge: 4.3.1
is-module: 1.0.0 is-module: 1.0.0
resolve: 1.22.4 resolve: 1.22.4
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
dev: true dev: true
/@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.10.0): /@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.12.0):
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
peerDependencies: peerDependencies:
rollup: npm:@rollup/wasm-node rollup: npm:@rollup/wasm-node
dependencies: dependencies:
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.10.0) '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.12.0)
magic-string: 0.25.9 magic-string: 0.25.9
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
dev: true dev: true
/@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.10.0): /@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.12.0):
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
peerDependencies: peerDependencies:
@ -2110,11 +2113,11 @@ packages:
'@types/estree': 0.0.39 '@types/estree': 0.0.39
estree-walker: 1.0.1 estree-walker: 1.0.1
picomatch: 2.3.1 picomatch: 2.3.1
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
dev: true dev: true
/@rollup/wasm-node@4.10.0: /@rollup/wasm-node@4.12.0:
resolution: {integrity: sha512-wH/ih4T/iP2PUyTrkyioZqDoFY/gmu63LPLTOM5Q21gSB/D3Ejw3UBpUOMLt86fIbN3mV+wL45MyA71XAj1ytg==} resolution: {integrity: sha512-sqy3+YvV/uWX6bPZOR5PlEdH6xyMPXoelllRQ/uZ13tzy9f4pXZTbajnoWN8IHHXwTNKPiLzsePLiDEVmkxMNw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
@ -5098,7 +5101,7 @@ packages:
'@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.6) '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.6)
'@babel/types': 7.23.6 '@babel/types': 7.23.6
kleur: 4.1.5 kleur: 4.1.5
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
unplugin: 1.5.1 unplugin: 1.5.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -6026,7 +6029,7 @@ packages:
glob: 7.2.3 glob: 7.2.3
dev: true dev: true
/rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.10.0): /rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.12.0):
resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser
peerDependencies: peerDependencies:
@ -6034,12 +6037,12 @@ packages:
dependencies: dependencies:
'@babel/code-frame': 7.23.5 '@babel/code-frame': 7.23.5
jest-worker: 26.6.2 jest-worker: 26.6.2
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
serialize-javascript: 4.0.0 serialize-javascript: 4.0.0
terser: 5.19.3 terser: 5.19.3
dev: true dev: true
/rollup-plugin-visualizer@5.11.0(@rollup/wasm-node@4.10.0): /rollup-plugin-visualizer@5.11.0(@rollup/wasm-node@4.12.0):
resolution: {integrity: sha512-exM0Ms2SN3AgTzMeW7y46neZQcyLY7eKwWAop1ZoRTCZwyrIRdMMJ6JjToAJbML77X/9N8ZEpmXG4Z/Clb9k8g==} resolution: {integrity: sha512-exM0Ms2SN3AgTzMeW7y46neZQcyLY7eKwWAop1ZoRTCZwyrIRdMMJ6JjToAJbML77X/9N8ZEpmXG4Z/Clb9k8g==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@ -6051,7 +6054,7 @@ packages:
dependencies: dependencies:
open: 8.4.2 open: 8.4.2
picomatch: 2.3.1 picomatch: 2.3.1
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
source-map: 0.7.4 source-map: 0.7.4
yargs: 17.7.2 yargs: 17.7.2
dev: true dev: true
@ -7037,7 +7040,7 @@ packages:
'@types/node': 20.10.5 '@types/node': 20.10.5
esbuild: 0.19.10 esbuild: 0.19.10
postcss: 8.4.32 postcss: 8.4.32
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
@ -7299,9 +7302,9 @@ packages:
'@babel/core': 7.23.6 '@babel/core': 7.23.6
'@babel/preset-env': 7.23.6(@babel/core@7.23.6) '@babel/preset-env': 7.23.6(@babel/core@7.23.6)
'@babel/runtime': 7.23.6 '@babel/runtime': 7.23.6
'@rollup/plugin-babel': 5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.10.0) '@rollup/plugin-babel': 5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.12.0)
'@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.10.0) '@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.12.0)
'@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.10.0) '@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.12.0)
'@surma/rollup-plugin-off-main-thread': 2.2.3 '@surma/rollup-plugin-off-main-thread': 2.2.3
ajv: 8.12.0 ajv: 8.12.0
common-tags: 1.8.2 common-tags: 1.8.2
@ -7310,8 +7313,8 @@ packages:
glob: 7.2.3 glob: 7.2.3
lodash: 4.17.21 lodash: 4.17.21
pretty-bytes: 5.6.0 pretty-bytes: 5.6.0
rollup: /@rollup/wasm-node@4.10.0 rollup: /@rollup/wasm-node@4.12.0
rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.10.0) rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.12.0)
source-map: 0.8.0-beta.0 source-map: 0.8.0-beta.0
stringify-object: 3.3.0 stringify-object: 3.3.0
strip-comments: 2.0.1 strip-comments: 2.0.1

5
public/config.js

@ -1,6 +1,7 @@
window.__CONFIG__ = { window.__CONFIG__ = {
// The URL for the CORS proxy, the URL must NOT end with a slash! // The URL for the CORS proxy, the URL must NOT end with a slash!
VITE_CORS_PROXY_URL: "CHANGEME", // 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: "",
// The READ API key to access TMDB // The READ API key to access TMDB
VITE_TMDB_READ_API_KEY: "CHANGEME", VITE_TMDB_READ_API_KEY: "CHANGEME",
@ -11,7 +12,7 @@ window.__CONFIG__ = {
// Whether to disable hash-based routing, leave this as false if you don't know what this is // Whether to disable hash-based routing, leave this as false if you don't know what this is
VITE_NORMAL_ROUTER: false, VITE_NORMAL_ROUTER: false,
// The backend URL to communicate with, defaults to the movie-web hosted one at backend.movie-web.app // The backend URL to communicate with
VITE_BACKEND_URL: null, VITE_BACKEND_URL: null,
// A comma separated list of disallowed IDs in the case of a DMCA claim - in the format "series-<id>" and "movie-<id>" // A comma separated list of disallowed IDs in the case of a DMCA claim - in the format "series-<id>" and "movie-<id>"

17
src/assets/locales/ar.json

@ -116,27 +116,24 @@
"failed": "تعذر العثور على الوسائط، حاول مجددا!", "failed": "تعذر العثور على الوسائط، حاول مجددا!",
"loading": "جار التحميل...", "loading": "جار التحميل...",
"noResults": "لم نتمكن من العثور على أي شيء!", "noResults": "لم نتمكن من العثور على أي شيء!",
"placeholder": "ماذا تريد أن تشاهد؟", "placeholder": {
"default": "ماذا تريد أن تشاهد؟",
"extra": []
},
"sectionTitle": "نتائج البحث" "sectionTitle": "نتائج البحث"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "ماذا تريد أن تشاهد في هذه الظهيرة؟", "default": "ماذا تريد أن تشاهد في هذه الظهيرة؟",
"extra": [ "extra": ["متشوق للمغامرة؟ قد يكون Jurassic Park خيارًا مثاليًا لك."]
"متشوق للمغامرة؟ قد يكون Jurassic Park خيارًا مثاليًا لك."
]
}, },
"morning": { "morning": {
"default": "ماذا تريد أن تشاهد في هذا الصباح؟", "default": "ماذا تريد أن تشاهد في هذا الصباح؟",
"extra": [ "extra": ["سمعت أن فلم \"Before Sunrise\" جيد"]
"سمعت أن فلم \"Before Sunrise\" جيد"
]
}, },
"night": { "night": {
"default": "ماذا تريد أن تشاهد في هذه الليلة؟", "default": "ماذا تريد أن تشاهد في هذه الليلة؟",
"extra": [ "extra": [ُرهَق؟ سمعت أن فيلم \"The Exorcist\" جيد."]
ُرهَق؟ سمعت أن فيلم \"The Exorcist\" جيد."
]
} }
} }
}, },

13
src/assets/locales/bg.json

@ -115,7 +115,10 @@
"failed": "Неуспешно намиране на медия, опитайте отново!", "failed": "Неуспешно намиране на медия, опитайте отново!",
"loading": "Зареждане...", "loading": "Зареждане...",
"noResults": "Не успяхме да намерим нищо!", "noResults": "Не успяхме да намерим нищо!",
"placeholder": "Какво искате да гледате?", "placeholder": {
"default": "Какво искате да гледате?",
"extra": []
},
"sectionTitle": "Резултати от търсенето" "sectionTitle": "Резултати от търсенето"
}, },
"titles": { "titles": {
@ -127,15 +130,11 @@
}, },
"morning": { "morning": {
"default": "Какво бихте искали да гледате тази сутрин?", "default": "Какво бихте искали да гледате тази сутрин?",
"extra": [ "extra": ["Чух, че Before Sunrise е добър"]
"Чух, че Before Sunrise е добър"
]
}, },
"night": { "night": {
"default": "Какво бихте искали да гледате тази вечер?", "default": "Какво бихте искали да гледате тази вечер?",
"extra": [ "extra": ["Изморен? Чух, че Екзорсистът е добър."]
"Изморен? Чух, че Екзорсистът е добър."
]
} }
} }
}, },

17
src/assets/locales/bn.json

@ -115,27 +115,24 @@
"failed": "মিিযরথ, আবর চ করন!", "failed": "মিিযরথ, আবর চ করন!",
"loading": "লড হচ..।", "loading": "লড হচ..।",
"noResults": "আমরিই খইনি!", "noResults": "আমরিই খইনি!",
"placeholder": "আপনিিখতন?", "placeholder": {
"default": "আপনিিখতন?",
"extra": []
},
"sectionTitle": "অনসনন ফলফল" "sectionTitle": "অনসনন ফলফল"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "আপনি এই বিিখতন?", "default": "আপনি এই বিিখতন?",
"extra": [ "extra": ["দহসিক বধ করছন? জিক পক নিত পছনদ হত।"]
"দহসিক বধ করছন? জিক পক নিত পছনদ হত।"
]
}, },
"morning": { "morning": {
"default": "আপনি এই সকিখতন?", "default": "আপনি এই সকিখতন?",
"extra": [ "extra": ["শিদযর আগ"]
"শিদযর আগ"
]
}, },
"night": { "night": {
"default": "আপনি আজ রিখতন?", "default": "আপনি আজ রিখতন?",
"extra": [ "extra": ["কত? আমিিয একসরসিট ভল।"]
"কত? আমিিয একসরসিট ভল।"
]
} }
} }
}, },

13
src/assets/locales/ca.json

@ -115,7 +115,10 @@
"failed": "No s'ha pogut trobar cap contingut, torneu-ho a provar!", "failed": "No s'ha pogut trobar cap contingut, torneu-ho a provar!",
"loading": "S'està carregant…", "loading": "S'està carregant…",
"noResults": "No hem pogut trobar res!", "noResults": "No hem pogut trobar res!",
"placeholder": "Què voleu mirar?", "placeholder": {
"default": "Què voleu mirar?",
"extra": []
},
"sectionTitle": "Resultats de la cerca" "sectionTitle": "Resultats de la cerca"
}, },
"titles": { "titles": {
@ -127,15 +130,11 @@
}, },
"morning": { "morning": {
"default": "Què us agradaria mirar aquest matí?", "default": "Què us agradaria mirar aquest matí?",
"extra": [ "extra": ["He sentit que «Abans de l'alba» és bona"]
"He sentit que «Abans de l'alba» és bona"
]
}, },
"night": { "night": {
"default": "Què us agradaria mirar aquesta nit?", "default": "Què us agradaria mirar aquesta nit?",
"extra": [ "extra": ["Esteu cansat? He sentit que «L'exorcista» és bona."]
"Esteu cansat? He sentit que «L'exorcista» és bona."
]
} }
} }
}, },

17
src/assets/locales/cs.json

@ -116,27 +116,24 @@
"failed": "Nepodařilo se najít média, zkuste to znovu!", "failed": "Nepodařilo se najít média, zkuste to znovu!",
"loading": "Načítání...", "loading": "Načítání...",
"noResults": "Nemohli jsme nic najít!", "noResults": "Nemohli jsme nic najít!",
"placeholder": "Co si přejete sledovat?", "placeholder": {
"default": "Co si přejete sledovat?",
"extra": []
},
"sectionTitle": "Výsledky vyhledávání" "sectionTitle": "Výsledky vyhledávání"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "Na co byste se chtěli dnes odpoledne dívat?", "default": "Na co byste se chtěli dnes odpoledne dívat?",
"extra": [ "extra": ["Chceš zažít dobrodružství? Jurský Park je pro tebe."]
"Chceš zažít dobrodružství? Jurský Park je pro tebe."
]
}, },
"morning": { "morning": {
"default": "Na co byste se chtěli dnes ráno dívat?", "default": "Na co byste se chtěli dnes ráno dívat?",
"extra": [ "extra": ["Slyšel jsem, že Před úsvitem je super."]
"Slyšel jsem, že Před úsvitem je super."
]
}, },
"night": { "night": {
"default": "Na co byste se chtěli dnes večer dívat?", "default": "Na co byste se chtěli dnes večer dívat?",
"extra": [ "extra": ["Unaven? Slyšel jsem, že Vymítač ďábla je super."]
"Unaven? Slyšel jsem, že Vymítač ďábla je super."
]
} }
} }
}, },

13
src/assets/locales/de.json

@ -116,7 +116,10 @@
"failed": "Das Medium wurde nicht gefunden, bitte versuchen Sie es erneut!", "failed": "Das Medium wurde nicht gefunden, bitte versuchen Sie es erneut!",
"loading": "Wird geladen...", "loading": "Wird geladen...",
"noResults": "Wir haben nichts gefunden!", "noResults": "Wir haben nichts gefunden!",
"placeholder": "Was möchtest du schauen?", "placeholder": {
"default": "Was möchtest du schauen?",
"extra": []
},
"sectionTitle": "Suchergebnisse" "sectionTitle": "Suchergebnisse"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Was würdest du diesen Morgen gerne schauen?", "default": "Was würdest du diesen Morgen gerne schauen?",
"extra": [ "extra": ["Before Sunrise soll gut sein"]
"Before Sunrise soll gut sein"
]
}, },
"night": { "night": {
"default": "Was möchtest du diesen Abend gerne schauen?", "default": "Was möchtest du diesen Abend gerne schauen?",
"extra": [ "extra": ["Müde? Ich hab gehört The Exorcist soll gut sein."]
"Müde? Ich hab gehört The Exorcist soll gut sein."
]
} }
} }
}, },

13
src/assets/locales/el.json

@ -115,7 +115,10 @@
"failed": "Απέτυχε η εύρεση πολυμέσων, δοκιμάστε ξανά!", "failed": "Απέτυχε η εύρεση πολυμέσων, δοκιμάστε ξανά!",
"loading": "Φόρτωση...", "loading": "Φόρτωση...",
"noResults": "Δεν μπορέσαμε να βρούμε τίποτα!", "noResults": "Δεν μπορέσαμε να βρούμε τίποτα!",
"placeholder": "Τι θέλετε να παρακολουθήσετε;", "placeholder": {
"default": "Τι θέλετε να παρακολουθήσετε;",
"extra": []
},
"sectionTitle": "Αποτελέσματα αναζήτησης" "sectionTitle": "Αποτελέσματα αναζήτησης"
}, },
"titles": { "titles": {
@ -127,15 +130,11 @@
}, },
"morning": { "morning": {
"default": "Τι θα θέλατε να παρακολουθήσετε σήμερα το πρωί;", "default": "Τι θα θέλατε να παρακολουθήσετε σήμερα το πρωί;",
"extra": [ "extra": ["Έχω ακούσει ότι το Before Sunrise είναι καλό"]
"Έχω ακούσει ότι το Before Sunrise είναι καλό"
]
}, },
"night": { "night": {
"default": "Τι θα θέλατε να παρακολουθήσετε απόψε;", "default": "Τι θα θέλατε να παρακολουθήσετε απόψε;",
"extra": [ "extra": ["Κούραση; Έχω ακούσει ότι ο Εξορκιστής είναι καλός."]
"Κούραση; Έχω ακούσει ότι ο Εξορκιστής είναι καλός."
]
} }
} }
}, },

12
src/assets/locales/en.json

@ -55,6 +55,8 @@
"text": "Did you configure it correctly?", "text": "Did you configure it correctly?",
"title": "Failed to reach server" "title": "Failed to reach server"
}, },
"noHostTitle": "Server not configured!",
"noHost": "The server has not been configured, therefore you cannot create an account",
"host": "You are connecting to <0>{{hostname}}</0> - please confirm you trust it before making an account", "host": "You are connecting to <0>{{hostname}}</0> - please confirm you trust it before making an account",
"no": "Go back", "no": "Go back",
"title": "Do you trust this server?", "title": "Do you trust this server?",
@ -116,7 +118,15 @@
"failed": "Failed to find media, try again!", "failed": "Failed to find media, try again!",
"loading": "Loading...", "loading": "Loading...",
"noResults": "We couldn't find anything!", "noResults": "We couldn't find anything!",
"placeholder": "What do you want to watch?", "placeholder": {
"default": "What do you want to watch?",
"extra": [
"What do you want to explore?",
"What's on your watchlist?",
"What's your favorite movie?",
"What's your favorite series?"
]
},
"sectionTitle": "Search results" "sectionTitle": "Search results"
}, },
"titles": { "titles": {

13
src/assets/locales/es.json

@ -116,7 +116,10 @@
"failed": "¡Error al encontrar contenido, inténtalo de nuevo!", "failed": "¡Error al encontrar contenido, inténtalo de nuevo!",
"loading": "Cargando...", "loading": "Cargando...",
"noResults": "¡No pudimos encontrar nada!", "noResults": "¡No pudimos encontrar nada!",
"placeholder": "¿Qué te gustaría ver?", "placeholder": {
"default": "¿Qué te gustaría ver?",
"extra": []
},
"sectionTitle": "Resultados de búsqueda" "sectionTitle": "Resultados de búsqueda"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "¿Qué te gustaría ver esta mañana?", "default": "¿Qué te gustaría ver esta mañana?",
"extra": [ "extra": ["Escuché que “Antes del amanecer” es buena"]
"Escuché que “Antes del amanecer” es buena"
]
}, },
"night": { "night": {
"default": "¿Qué te gustaría ver esta noche?", "default": "¿Qué te gustaría ver esta noche?",
"extra": [ "extra": ["¿Cansado? Escuché que “El Exorcista” es buena."]
"¿Cansado? Escuché que “El Exorcista” es buena."
]
} }
} }
}, },

13
src/assets/locales/et.json

@ -116,7 +116,10 @@
"failed": "Meedia leidmine ebaõnnestus, proovige uuesti!", "failed": "Meedia leidmine ebaõnnestus, proovige uuesti!",
"loading": "Laadimine....", "loading": "Laadimine....",
"noResults": "Me ei leidnud midagi!", "noResults": "Me ei leidnud midagi!",
"placeholder": "Mida tahate vaadata?", "placeholder": {
"default": "Mida tahate vaadata?",
"extra": []
},
"sectionTitle": "Otsingutulemused" "sectionTitle": "Otsingutulemused"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Mida te soovite täna hommikul vaadata?", "default": "Mida te soovite täna hommikul vaadata?",
"extra": [ "extra": ["Ma kuulsin, et Before Sunrise on hea"]
"Ma kuulsin, et Before Sunrise on hea"
]
}, },
"night": { "night": {
"default": "Mida te soovite täna õhtul vaadata?", "default": "Mida te soovite täna õhtul vaadata?",
"extra": [ "extra": ["Väsinud? Olen kuulnud, et The Exorcist on hea."]
"Väsinud? Olen kuulnud, et The Exorcist on hea."
]
} }
} }
}, },

13
src/assets/locales/fa.json

@ -116,7 +116,10 @@
"failed": "چیزی پیدا نشد، دوباره تلاش کنید!", "failed": "چیزی پیدا نشد، دوباره تلاش کنید!",
"loading": "در حال بارگذاری...", "loading": "در حال بارگذاری...",
"noResults": "چیزی پیدا نکردیم!", "noResults": "چیزی پیدا نکردیم!",
"placeholder": "چه میخواهید تماشا کنید؟", "placeholder": {
"default": "چه میخواهید تماشا کنید؟",
"extra": []
},
"sectionTitle": "نتایج جستجو" "sectionTitle": "نتایج جستجو"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "دوست دارید امروز صبح چه چیزی تماشا کنید؟", "default": "دوست دارید امروز صبح چه چیزی تماشا کنید؟",
"extra": [ "extra": ["شنیدم فیلم \"پیش از طلوع\" عالیه"]
"شنیدم فیلم \"پیش از طلوع\" عالیه"
]
}, },
"night": { "night": {
"default": "دوست دارید امشب چه چیزی تماشا کنید؟", "default": "دوست دارید امشب چه چیزی تماشا کنید؟",
"extra": [ "extra": ["خسته اید؟ شنیده ام که \"جن گیر\" فیلم خوبی است."]
"خسته اید؟ شنیده ام که \"جن گیر\" فیلم خوبی است."
]
} }
} }
}, },

13
src/assets/locales/fi-FI.json

@ -116,7 +116,10 @@
"failed": "Mediaa ei löytynyt, yritä uudelleen!", "failed": "Mediaa ei löytynyt, yritä uudelleen!",
"loading": "Ladataan...", "loading": "Ladataan...",
"noResults": "Emme löytäneet mitään!", "noResults": "Emme löytäneet mitään!",
"placeholder": "Mitä haluat katsoa?", "placeholder": {
"default": "Mitä haluat katsoa?",
"extra": []
},
"sectionTitle": "Hakutulokset" "sectionTitle": "Hakutulokset"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Mitä haluaisit katsoa tänä aamuna?", "default": "Mitä haluaisit katsoa tänä aamuna?",
"extra": [ "extra": ["Kuulen, että Rakkautta ennen aamua (Before Sunrise) on hyvä"]
"Kuulen, että Rakkautta ennen aamua (Before Sunrise) on hyvä"
]
}, },
"night": { "night": {
"default": "Mitä haluaisit katsoa tänä iltana?", "default": "Mitä haluaisit katsoa tänä iltana?",
"extra": [ "extra": ["Väsynyt? Kuulin, että Manaaja (The Exorcist) on hyvä."]
"Väsynyt? Kuulin, että Manaaja (The Exorcist) on hyvä."
]
} }
} }
}, },

9
src/assets/locales/fr.json

@ -116,7 +116,10 @@
"failed": "Le média n'a pas été trouvé, veuillez réessayez!", "failed": "Le média n'a pas été trouvé, veuillez réessayez!",
"loading": "Chargement...", "loading": "Chargement...",
"noResults": "Nous n'avons rien trouvé!", "noResults": "Nous n'avons rien trouvé!",
"placeholder": "Que voulez-vous voir?", "placeholder": {
"default": "Que voulez-vous voir?",
"extra": []
},
"sectionTitle": "Résultats de la recherche" "sectionTitle": "Résultats de la recherche"
}, },
"titles": { "titles": {
@ -128,9 +131,7 @@
}, },
"morning": { "morning": {
"default": "Que voulez-vous regarder ce matin ?", "default": "Que voulez-vous regarder ce matin ?",
"extra": [ "extra": ["Les films, c'est comme les voyages : ça nous ouvre l'esprit"]
"Les films, c'est comme les voyages : ça nous ouvre l'esprit"
]
}, },
"night": { "night": {
"default": "Que voulez-vous regarder ce soir ?", "default": "Que voulez-vous regarder ce soir ?",

13
src/assets/locales/gl.json

@ -116,7 +116,10 @@
"failed": "Error ao encontrar contido... intentao de novo!", "failed": "Error ao encontrar contido... intentao de novo!",
"loading": "Cargando...", "loading": "Cargando...",
"noResults": "Non atopamos nada!", "noResults": "Non atopamos nada!",
"placeholder": "Que che gustaría ver?", "placeholder": {
"default": "Que che gustaría ver?",
"extra": []
},
"sectionTitle": "Resultados da busca" "sectionTitle": "Resultados da busca"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Que che gustaría ver esta mañá?", "default": "Que che gustaría ver esta mañá?",
"extra": [ "extra": ["Escoitei que “Antes del amanecer” é boa"]
"Escoitei que “Antes del amanecer” é boa"
]
}, },
"night": { "night": {
"default": "Que che gustaría ver esta noite?", "default": "Que che gustaría ver esta noite?",
"extra": [ "extra": ["Canso? Escoitei que “El Exorcista” é boa."]
"Canso? Escoitei que “El Exorcista” é boa."
]
} }
} }
}, },

13
src/assets/locales/gu.json

@ -116,21 +116,20 @@
"failed": "મિધવિફળ, ફરરયસ કર!", "failed": "મિધવિફળ, ફરરયસ કર!",
"loading": "લડ થય છ...", "loading": "લડ થય છ...",
"noResults": "અમઈપણ શ શક નથ!", "noResults": "અમઈપણ શ શક નથ!",
"placeholder": "તમ?", "placeholder": {
"default": "તમ?",
"extra": []
},
"sectionTitle": "શધ પરિ" "sectionTitle": "શધ પરિ"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "તમ આ બપ?", "default": "તમ આ બપ?",
"extra": [ "extra": ["સહસિક લ? જિક પક યય પસદગઈ શક."]
"સહસિક લ? જિક પક યય પસદગઈ શક."
]
}, },
"morning": { "morning": {
"default": "તમ આ સવ?", "default": "તમ આ સવ?",
"extra": [ "extra": ["હભળ Before Sunrise સ"]
"હભળ Before Sunrise સ"
]
}, },
"night": { "night": {
"default": "તમ આજ?", "default": "તમ આજ?",

17
src/assets/locales/he.json

@ -116,27 +116,24 @@
"failed": "לא הצלחנו למצוא מדיה, נסה שוב!", "failed": "לא הצלחנו למצוא מדיה, נסה שוב!",
"loading": "טוען...", "loading": "טוען...",
"noResults": "לא יכולנו למצוא כלום!", "noResults": "לא יכולנו למצוא כלום!",
"placeholder": "במה תרצה לצפות?", "placeholder": {
"default": "במה תרצה לצפות?",
"extra": []
},
"sectionTitle": "תוצאות חיפוש" "sectionTitle": "תוצאות חיפוש"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "במה תרצה לצפות באחר צהריים זה?", "default": "במה תרצה לצפות באחר צהריים זה?",
"extra": [ "extra": ["מרגיש הרפתקני? פארק היורה עשוי להיות הבחירה המושלמת."]
"מרגיש הרפתקני? פארק היורה עשוי להיות הבחירה המושלמת."
]
}, },
"morning": { "morning": {
"default": "במה תרצה לצפות הבוקר?", "default": "במה תרצה לצפות הבוקר?",
"extra": [ "extra": ["שמעתי שלפני הזריחה זה סרט טוב"]
"שמעתי שלפני הזריחה זה סרט טוב"
]
}, },
"night": { "night": {
"default": "במה תרצה לצפות הלילה?", "default": "במה תרצה לצפות הלילה?",
"extra": [ "extra": ["רוצה לישון? הפיג'מות היא בחירה מצויינת."]
"רוצה לישון? הפיג'מות היא בחירה מצויינת."
]
} }
} }
}, },

13
src/assets/locales/hi.json

@ -116,7 +116,10 @@
"failed": "मिढनिफल, परयस कर!", "failed": "मिढनिफल, परयस कर!",
"loading": "लड ह रह..।", "loading": "लड ह रह..।",
"noResults": "हमछ नहि!", "noResults": "हमछ नहि!",
"placeholder": "कखनहत?", "placeholder": {
"default": "कखनहत?",
"extra": []
},
"sectionTitle": "खज क परिम" "sectionTitle": "खज क परिम"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "आप आज सबह कखन?", "default": "आप आज सबह कखन?",
"extra": [ "extra": ["मदय स पहलक ह"]
"मदय स पहलक ह"
]
}, },
"night": { "night": {
"default": "आप आज रत कखन?", "default": "आप आज रत कखन?",
"extra": [ "extra": ["थकए ह? म एकरसिट अच।"]
"थकए ह? म एकरसिट अच।"
]
} }
} }
}, },

13
src/assets/locales/id.json

@ -115,7 +115,10 @@
"failed": "Gagal menemukan media, coba lagi!", "failed": "Gagal menemukan media, coba lagi!",
"loading": "Memuat...", "loading": "Memuat...",
"noResults": "Kami tidak dapat menemukan apapun!", "noResults": "Kami tidak dapat menemukan apapun!",
"placeholder": "Apa yang ingin anda tonton?", "placeholder": {
"default": "Apa yang ingin anda tonton?",
"extra": []
},
"sectionTitle": "Hasil pencarian" "sectionTitle": "Hasil pencarian"
}, },
"titles": { "titles": {
@ -127,15 +130,11 @@
}, },
"morning": { "morning": {
"default": "Apa yang ingin anda tonton pagi ini?", "default": "Apa yang ingin anda tonton pagi ini?",
"extra": [ "extra": ["Kayaknya film Before Sunrise bagus deh"]
"Kayaknya film Before Sunrise bagus deh"
]
}, },
"night": { "night": {
"default": "Apa yang ingin anda tonton malam ini?", "default": "Apa yang ingin anda tonton malam ini?",
"extra": [ "extra": ["Capek? Katanya The Exocist rekomended."]
"Capek? Katanya The Exocist rekomended."
]
} }
} }
}, },

13
src/assets/locales/is-IS.json

@ -108,7 +108,10 @@
"failed": "Mostókst að finna miðil, reyndu aftur!", "failed": "Mostókst að finna miðil, reyndu aftur!",
"loading": "Hlaðið...", "loading": "Hlaðið...",
"noResults": "Við gátum ekki fundið neitt!", "noResults": "Við gátum ekki fundið neitt!",
"placeholder": "Hvað viltu horfa á?", "placeholder": {
"default": "Hvað viltu horfa á?",
"extra": []
},
"sectionTitle": "Leitar niðurstöður" "sectionTitle": "Leitar niðurstöður"
}, },
"titles": { "titles": {
@ -120,15 +123,11 @@
}, },
"morning": { "morning": {
"default": "Hvað myndirðu vilja horfa á þessum morgni?", "default": "Hvað myndirðu vilja horfa á þessum morgni?",
"extra": [ "extra": ["Ég heyrði að Before Sunrise sé góð"]
"Ég heyrði að Before Sunrise sé góð"
]
}, },
"night": { "night": {
"default": "Hvað myndirðu vilja horfa á í nótt?", "default": "Hvað myndirðu vilja horfa á í nótt?",
"extra": [ "extra": ["Þreytt? Ég heyrði að The Exorcist sé góð."]
"Þreytt? Ég heyrði að The Exorcist sé góð."
]
} }
} }
}, },

13
src/assets/locales/it.json

@ -116,7 +116,10 @@
"failed": "Impossibile trovare i media, riprova!", "failed": "Impossibile trovare i media, riprova!",
"loading": "Caricamento...", "loading": "Caricamento...",
"noResults": "Non abbiamo trovato nulla!", "noResults": "Non abbiamo trovato nulla!",
"placeholder": "Cosa vuoi guardare?", "placeholder": {
"default": "Cosa vuoi guardare?",
"extra": []
},
"sectionTitle": "Risultati della ricerca" "sectionTitle": "Risultati della ricerca"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Cosa vorresti guardare questa mattina?", "default": "Cosa vorresti guardare questa mattina?",
"extra": [ "extra": ["Ho sentito che «Prima Dell'alba» è buono"]
"Ho sentito che «Prima Dell'alba» è buono"
]
}, },
"night": { "night": {
"default": "Cosa vorresti guardare questa sera?", "default": "Cosa vorresti guardare questa sera?",
"extra": [ "extra": ["Stanco? Ho sentito che L'esorciccio è buono."]
"Stanco? Ho sentito che L'esorciccio è buono."
]
} }
} }
}, },

5
src/assets/locales/ja.json

@ -76,7 +76,10 @@
"allResults": "それがすべてです!", "allResults": "それがすべてです!",
"loading": "読み込み中...", "loading": "読み込み中...",
"noResults": "見つかりませんでした!", "noResults": "見つかりませんでした!",
"placeholder": "どんな映画を見たい?", "placeholder": {
"default": "どんな映画を見たい?",
"extra": []
},
"sectionTitle": "検索結果" "sectionTitle": "検索結果"
}, },
"titles": { "titles": {

13
src/assets/locales/ko.json

@ -116,7 +116,10 @@
"failed": "미디어 검색에 실패하였습니다, 다시 시도해주세요!", "failed": "미디어 검색에 실패하였습니다, 다시 시도해주세요!",
"loading": "로딩...", "loading": "로딩...",
"noResults": "검색결과가 없습니다!", "noResults": "검색결과가 없습니다!",
"placeholder": "무엇을 보고 싶으신가요?", "placeholder": {
"default": "무엇을 보고 싶으신가요?",
"extra": []
},
"sectionTitle": "검색 결과" "sectionTitle": "검색 결과"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "오늘 아침에 무엇을 보고 싶으신가요?", "default": "오늘 아침에 무엇을 보고 싶으신가요?",
"extra": [ "extra": ["Before Sunrise가 좋다고 들었어요"]
"Before Sunrise가 좋다고 들었어요"
]
}, },
"night": { "night": {
"default": "오늘 밤에 무엇을 보고 싶으신가요?", "default": "오늘 밤에 무엇을 보고 싶으신가요?",
"extra": [ "extra": ["피곤하신가요? The Exorcist가 좋다고 들었어요."]
"피곤하신가요? The Exorcist가 좋다고 들었어요."
]
} }
} }
}, },

13
src/assets/locales/lv.json

@ -114,7 +114,10 @@
"failed": "Neizdevās atrast multividi. Mēģiniet vēlreiz!", "failed": "Neizdevās atrast multividi. Mēģiniet vēlreiz!",
"loading": "Lādejas...", "loading": "Lādejas...",
"noResults": "Mēs nevarējām neko atrast!", "noResults": "Mēs nevarējām neko atrast!",
"placeholder": "Ko tu gribi skatīties?", "placeholder": {
"default": "Ko tu gribi skatīties?",
"extra": []
},
"sectionTitle": "Meklējuma rezultāti" "sectionTitle": "Meklējuma rezultāti"
}, },
"titles": { "titles": {
@ -123,15 +126,11 @@
}, },
"morning": { "morning": {
"default": "Ko tu gribētu šorīt noskatīties?", "default": "Ko tu gribētu šorīt noskatīties?",
"extra": [ "extra": ["Es dzirdu, ka Pirms saullēkta ir labs"]
"Es dzirdu, ka Pirms saullēkta ir labs"
]
}, },
"night": { "night": {
"default": "Ko tu gribētu šovakar skatīties?", "default": "Ko tu gribētu šovakar skatīties?",
"extra": [ "extra": ["Noguris? Es dzirdu, ka Exorcist ir labs."]
"Noguris? Es dzirdu, ka Exorcist ir labs."
]
} }
} }
}, },

13
src/assets/locales/minion.json

@ -115,7 +115,10 @@
"failed": "Failed to banana banana, try again!", "failed": "Failed to banana banana, try again!",
"loading": "Loading...", "loading": "Loading...",
"noResults": "We couldn't banana anything!", "noResults": "We couldn't banana anything!",
"placeholder": "Banana do you want to banana?", "placeholder": {
"default": "Banana do you want to banana?",
"extra": []
},
"sectionTitle": "Banana results" "sectionTitle": "Banana results"
}, },
"titles": { "titles": {
@ -127,15 +130,11 @@
}, },
"morning": { "morning": {
"default": "What would you like to banana this banana?", "default": "What would you like to banana this banana?",
"extra": [ "extra": ["Banana! I hear Banana Sunrise is banana"]
"Banana! I hear Banana Sunrise is banana"
]
}, },
"night": { "night": {
"default": "What would you like to banana banana?", "default": "What would you like to banana banana?",
"extra": [ "extra": ["Banana? I hear The Banana is banana."]
"Banana? I hear The Banana is banana."
]
} }
} }
}, },

13
src/assets/locales/ne.json

@ -115,7 +115,10 @@
"failed": "मििन असफल भय, फिरयस गर!", "failed": "मििन असफल भय, फिरयस गर!",
"loading": "लड गर...", "loading": "लड गर...",
"noResults": "हिन सक!", "noResults": "हिन सक!",
"placeholder": "तपन चहनछ?", "placeholder": {
"default": "तपन चहनछ?",
"extra": []
},
"sectionTitle": "खज परिमहर" "sectionTitle": "खज परिमहर"
}, },
"titles": { "titles": {
@ -127,15 +130,11 @@
}, },
"morning": { "morning": {
"default": "तपई आज बिन कन चहनछ?", "default": "तपई आज बिन कन चहनछ?",
"extra": [ "extra": ["Before Sunrise र छ भन"]
"Before Sunrise र छ भन"
]
}, },
"night": { "night": {
"default": "तप आज रन चहनछ?", "default": "तप आज रन चहनछ?",
"extra": [ "extra": ["थकित? म The Exorcist र छ।"]
"थकित? म The Exorcist र छ।"
]
} }
} }
}, },

13
src/assets/locales/nl.json

@ -116,7 +116,10 @@
"failed": "Het is niet gelukt de media te laden, probeer het nog eens!", "failed": "Het is niet gelukt de media te laden, probeer het nog eens!",
"loading": "Aan het zoeken...", "loading": "Aan het zoeken...",
"noResults": "We konden helaas niets vinden!", "noResults": "We konden helaas niets vinden!",
"placeholder": "Wat wil je graag kijken?", "placeholder": {
"default": "Wat wil je graag kijken?",
"extra": []
},
"sectionTitle": "Zoekresultaten" "sectionTitle": "Zoekresultaten"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Waar wil je deze ochtend naar kijken?", "default": "Waar wil je deze ochtend naar kijken?",
"extra": [ "extra": ["Ik hoor dat Before Sunrise goed is"]
"Ik hoor dat Before Sunrise goed is"
]
}, },
"night": { "night": {
"default": "Wat wil je vanavond bekijken?", "default": "Wat wil je vanavond bekijken?",
"extra": [ "extra": ["Moe? Ik hoor dat The Exorcist goed is."]
"Moe? Ik hoor dat The Exorcist goed is."
]
} }
} }
}, },

13
src/assets/locales/pa.json

@ -116,7 +116,10 @@
"failed": "ਮਆ ਲਭਣ ਵਿਚ ਅਸਫਲ, ਦਿ ਕਰ!", "failed": "ਮਆ ਲਭਣ ਵਿਚ ਅਸਫਲ, ਦਿ ਕਰ!",
"loading": "ਲਡ ਕਿ...", "loading": "ਲਡ ਕਿ...",
"noResults": "ਅਸਝ ਵ ਨਹਭ ਸਕ!", "noResults": "ਅਸਝ ਵ ਨਹਭ ਸਕ!",
"placeholder": "ਤਖਣ?", "placeholder": {
"default": "ਤਖਣ?",
"extra": []
},
"sectionTitle": "ਖਜ ਨਤ" "sectionTitle": "ਖਜ ਨਤ"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "ਤਜ ਸਵਖਣ?", "default": "ਤਜ ਸਵਖਣ?",
"extra": [ "extra": ["ਮਣਦਿਰਜ ਚੜਹਨ ਤ ਪਹਿ"]
"ਮਣਦਿਰਜ ਚੜਹਨ ਤ ਪਹਿ"
]
}, },
"night": { "night": {
"default": "ਤਜ ਰਤ ਕਖਣ?", "default": "ਤਜ ਰਤ ਕਖਣ?",
"extra": [ "extra": ["ਥਕ ਗਏ? ਮਿਆ ਹਿ Exorcist ਚ."]
"ਥਕ ਗਏ? ਮਿਆ ਹਿ Exorcist ਚ."
]
} }
} }
}, },

5
src/assets/locales/pirate.json

@ -116,7 +116,10 @@
"failed": "Arrrr failed to find media, try again!", "failed": "Arrrr failed to find media, try again!",
"loading": "Hold yer horses, me heartie!", "loading": "Hold yer horses, me heartie!",
"noResults": "We couldn't find anythin', arrr!", "noResults": "We couldn't find anythin', arrr!",
"placeholder": "What do ye want to watch?", "placeholder": {
"default": "What do ye want to watch?",
"extra": []
},
"sectionTitle": "Searchin' results" "sectionTitle": "Searchin' results"
}, },
"titles": { "titles": {

13
src/assets/locales/pl.json

@ -116,7 +116,10 @@
"failed": "Nie udało się znaleźć mediów, Spróbuj ponownie!", "failed": "Nie udało się znaleźć mediów, Spróbuj ponownie!",
"loading": "Wczytywanie...", "loading": "Wczytywanie...",
"noResults": "Nie mogliśmy niczego znaleźć!", "noResults": "Nie mogliśmy niczego znaleźć!",
"placeholder": "Co chciałbyś obejrzeć?", "placeholder": {
"default": "Co chciałbyś obejrzeć?",
"extra": []
},
"sectionTitle": "Wyniki wyszukiwania" "sectionTitle": "Wyniki wyszukiwania"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Co chciałbyś obejrzeć dziś rano?", "default": "Co chciałbyś obejrzeć dziś rano?",
"extra": [ "extra": ["Słyszałem że „Przed wschodem słońca” jest dobre"]
"Słyszałem że „Przed wschodem słońca” jest dobre"
]
}, },
"night": { "night": {
"default": "Co chciałbyś obejrzeć dziś wieczorem?", "default": "Co chciałbyś obejrzeć dziś wieczorem?",
"extra": [ "extra": ["Zmęczony? Słyszałem że „Egzorcysta” jest dobry."]
"Zmęczony? Słyszałem że „Egzorcysta” jest dobry."
]
} }
} }
}, },

13
src/assets/locales/pt-BR.json

@ -116,7 +116,10 @@
"failed": "Falha ao encontrar mídia, tente novamente!", "failed": "Falha ao encontrar mídia, tente novamente!",
"loading": "Carregando...", "loading": "Carregando...",
"noResults": "Não conseguimos encontrar nada!", "noResults": "Não conseguimos encontrar nada!",
"placeholder": "O que você quer assistir?", "placeholder": {
"default": "O que você quer assistir?",
"extra": []
},
"sectionTitle": "Resultados da pesquisa" "sectionTitle": "Resultados da pesquisa"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "O que você gostaria de assistir esta manhã?", "default": "O que você gostaria de assistir esta manhã?",
"extra": [ "extra": ["Ouvi dizer que Antes do Amanhecer é bom"]
"Ouvi dizer que Antes do Amanhecer é bom"
]
}, },
"night": { "night": {
"default": "O que você gostaria de assistir esta noite?", "default": "O que você gostaria de assistir esta noite?",
"extra": [ "extra": ["Cansado? Ouvi dizer que O Exorcista é bom."]
"Cansado? Ouvi dizer que O Exorcista é bom."
]
} }
} }
}, },

13
src/assets/locales/pt-PT.json

@ -115,7 +115,10 @@
"failed": "Falha ao encontrar mídia, tente novamente!", "failed": "Falha ao encontrar mídia, tente novamente!",
"loading": "A carregar...", "loading": "A carregar...",
"noResults": "Não conseguimos encontrar nada!", "noResults": "Não conseguimos encontrar nada!",
"placeholder": "O que deseja assistir?", "placeholder": {
"default": "O que deseja assistir?",
"extra": []
},
"sectionTitle": "Resultados da pesquisa" "sectionTitle": "Resultados da pesquisa"
}, },
"titles": { "titles": {
@ -127,15 +130,11 @@
}, },
"morning": { "morning": {
"default": "O que gostaria de assistir esta manhã?", "default": "O que gostaria de assistir esta manhã?",
"extra": [ "extra": ["Dizem que Antes do Amanhecer é bom"]
"Dizem que Antes do Amanhecer é bom"
]
}, },
"night": { "night": {
"default": "O que gostaria de assistir esta noite?", "default": "O que gostaria de assistir esta noite?",
"extra": [ "extra": ["Cansado? Dizem que O Exorcista é bom."]
"Cansado? Dizem que O Exorcista é bom."
]
} }
} }
}, },

13
src/assets/locales/ro.json

@ -114,7 +114,10 @@
"failed": "Găsire media eșuată, încearcă din nou!", "failed": "Găsire media eșuată, încearcă din nou!",
"loading": "Se încarcă...", "loading": "Se încarcă...",
"noResults": "Nu am putut găsi nimic!", "noResults": "Nu am putut găsi nimic!",
"placeholder": "La ce dorești să te uiți?", "placeholder": {
"default": "La ce dorești să te uiți?",
"extra": []
},
"sectionTitle": "Rezultate de căutare" "sectionTitle": "Rezultate de căutare"
}, },
"titles": { "titles": {
@ -126,15 +129,11 @@
}, },
"morning": { "morning": {
"default": "La ce dorești să te in uiți dimineață aceasta?", "default": "La ce dorești să te in uiți dimineață aceasta?",
"extra": [ "extra": ["Aud că Before Sunrise este bun"]
"Aud că Before Sunrise este bun"
]
}, },
"night": { "night": {
"default": "La ce dorești să te uiți în astă seară?", "default": "La ce dorești să te uiți în astă seară?",
"extra": [ "extra": ["Obosit? Aud că The Exorcist is good."]
"Obosit? Aud că The Exorcist is good."
]
} }
} }
}, },

9
src/assets/locales/ru.json

@ -116,7 +116,10 @@
"failed": "Не удалось найти медиафайл, попробуйте снова!", "failed": "Не удалось найти медиафайл, попробуйте снова!",
"loading": "Загрузка...", "loading": "Загрузка...",
"noResults": "Мы ничего не нашли!", "noResults": "Мы ничего не нашли!",
"placeholder": "Что вы хотите посмотреть?", "placeholder": {
"default": "Что вы хотите посмотреть?",
"extra": []
},
"sectionTitle": "Результаты поиска" "sectionTitle": "Результаты поиска"
}, },
"titles": { "titles": {
@ -128,9 +131,7 @@
}, },
"morning": { "morning": {
"default": "Что бы вы хотели посмотреть этим утром?", "default": "Что бы вы хотели посмотреть этим утром?",
"extra": [ "extra": ["Слышали, что «Перед рассветом» – отличный фильм"]
"Слышали, что «Перед рассветом» – отличный фильм"
]
}, },
"night": { "night": {
"default": "Что бы вы хотели посмотреть этим вечером?", "default": "Что бы вы хотели посмотреть этим вечером?",

9
src/assets/locales/sl.json

@ -115,7 +115,10 @@
"failed": "Ni uspelo najti medija, prosim poskusite znova!", "failed": "Ni uspelo najti medija, prosim poskusite znova!",
"loading": "Nalaganje...", "loading": "Nalaganje...",
"noResults": "Vsebin nismo našli!", "noResults": "Vsebin nismo našli!",
"placeholder": "Kaj si želite gledati?", "placeholder": {
"default": "Kaj si želite gledati?",
"extra": []
},
"sectionTitle": "Rezultati iskanja" "sectionTitle": "Rezultati iskanja"
}, },
"titles": { "titles": {
@ -127,9 +130,7 @@
}, },
"morning": { "morning": {
"default": "Kateri film ali serijo bi si želeli ogledati ob tem jutru?", "default": "Kateri film ali serijo bi si želeli ogledati ob tem jutru?",
"extra": [ "extra": ["Slišala sem, da je film \"Pred sončnim vzhodom\" odličen"]
"Slišala sem, da je film \"Pred sončnim vzhodom\" odličen"
]
}, },
"night": { "night": {
"default": "Kateri film ali serijo bi si želeli ogledati nocoj?", "default": "Kateri film ali serijo bi si želeli ogledati nocoj?",

13
src/assets/locales/sv.json

@ -114,7 +114,10 @@
"failed": "Misslyckades med att hitta media, försök igen!", "failed": "Misslyckades med att hitta media, försök igen!",
"loading": "Laddar...", "loading": "Laddar...",
"noResults": "Vi kunde inte hitta någonting!", "noResults": "Vi kunde inte hitta någonting!",
"placeholder": "Vad vill du titta på?", "placeholder": {
"default": "Vad vill du titta på?",
"extra": []
},
"sectionTitle": "Sökresultat" "sectionTitle": "Sökresultat"
}, },
"titles": { "titles": {
@ -123,15 +126,11 @@
}, },
"morning": { "morning": {
"default": "Vad vill du titta på den här morgonen?", "default": "Vad vill du titta på den här morgonen?",
"extra": [ "extra": ["Jag hör att Before Sunrise är bra"]
"Jag hör att Before Sunrise är bra"
]
}, },
"night": { "night": {
"default": "Vad vill du titta på ikväll?", "default": "Vad vill du titta på ikväll?",
"extra": [ "extra": ["Trött? Jag hör att The Exorcist är bra."]
"Trött? Jag hör att The Exorcist är bra."
]
} }
} }
}, },

13
src/assets/locales/th.json

@ -115,7 +115,10 @@
"failed": "ไมพบสอน ลองอกครง!", "failed": "ไมพบสอน ลองอกครง!",
"loading": "กำลงโหลด..", "loading": "กำลงโหลด..",
"noResults": "เราไมพบอะไรเลย!", "noResults": "เราไมพบอะไรเลย!",
"placeholder": "คณอยากดอะไรคะ?", "placeholder": {
"default": "คณอยากดอะไรคะ?",
"extra": []
},
"sectionTitle": "ผลการคนหา" "sectionTitle": "ผลการคนหา"
}, },
"titles": { "titles": {
@ -124,15 +127,11 @@
}, },
"morning": { "morning": {
"default": "คณอยากดอะไรเชาน?", "default": "คณอยากดอะไรเชาน?",
"extra": [ "extra": ["ฉนไดนมาวาเรอง Before Sunrise สนก"]
"ฉนไดนมาวาเรอง Before Sunrise สนก"
]
}, },
"night": { "night": {
"default": "คณอยากดเรองอะไรในชวงคำ?", "default": "คณอยากดเรองอะไรในชวงคำ?",
"extra": [ "extra": ["เหนอยมย? ฉนไดนมาวา The Exorcist นนด"]
"เหนอยมย? ฉนไดนมาวา The Exorcist นนด"
]
} }
} }
}, },

17
src/assets/locales/tok.json

@ -115,27 +115,24 @@
"failed": "lukin li pakala a! o alasa sin", "failed": "lukin li pakala a! o alasa sin",
"loading": "alasa...", "loading": "alasa...",
"noResults": "ijo li lon ala a!", "noResults": "ijo li lon ala a!",
"placeholder": "sina wile lukin e seme?", "placeholder": {
"default": "sina wile lukin e seme?",
"extra": []
},
"sectionTitle": "mi lukin e ni:" "sectionTitle": "mi lukin e ni:"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "tenpo suno ni la sina wile lukin e seme?", "default": "tenpo suno ni la sina wile lukin e seme?",
"extra": [ "extra": ["sina pilin alasa la o lukin e sitelen Jurassic Park"]
"sina pilin alasa la o lukin e sitelen Jurassic Park"
]
}, },
"morning": { "morning": {
"default": "tenpo sin ni la sina wile lukin e seme?", "default": "tenpo sin ni la sina wile lukin e seme?",
"extra": [ "extra": ["ken la sitelen Before Sunrise li pona"]
"ken la sitelen Before Sunrise li pona"
]
}, },
"night": { "night": {
"default": "tenpo pimeja ni la sina wile lukin e seme?", "default": "tenpo pimeja ni la sina wile lukin e seme?",
"extra": [ "extra": ["sina pilin lape anu seme? o alasa lukin e sitelen Exorcist"]
"sina pilin lape anu seme? o alasa lukin e sitelen Exorcist"
]
} }
} }
}, },

13
src/assets/locales/tr.json

@ -116,7 +116,10 @@
"failed": "Medya bulunamadı, tekrar deneyin!", "failed": "Medya bulunamadı, tekrar deneyin!",
"loading": "Yükleniyor...", "loading": "Yükleniyor...",
"noResults": "Hiçbir şey bulamadık!", "noResults": "Hiçbir şey bulamadık!",
"placeholder": "Ne izlemek istersiniz?", "placeholder": {
"default": "Ne izlemek istersiniz?",
"extra": []
},
"sectionTitle": "Arama sonuçları" "sectionTitle": "Arama sonuçları"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Bu sabah ne izlemek istersiniz?", "default": "Bu sabah ne izlemek istersiniz?",
"extra": [ "extra": ["Before Sunrise'a iyi diyorlar"]
"Before Sunrise'a iyi diyorlar"
]
}, },
"night": { "night": {
"default": "Bu akşam ne izlemek istersiniz?", "default": "Bu akşam ne izlemek istersiniz?",
"extra": [ "extra": ["Yoruldun mu? The Exorcist'e iyi diyorlar."]
"Yoruldun mu? The Exorcist'e iyi diyorlar."
]
} }
} }
}, },

13
src/assets/locales/uk.json

@ -116,7 +116,10 @@
"failed": "Не вдалося знайти медіафайли, повторіть спробу!", "failed": "Не вдалося знайти медіафайли, повторіть спробу!",
"loading": "Завантаження...", "loading": "Завантаження...",
"noResults": "Ми не змогли знайти нічого!", "noResults": "Ми не змогли знайти нічого!",
"placeholder": "Що ви хочете подивитися?", "placeholder": {
"default": "Що ви хочете подивитися?",
"extra": []
},
"sectionTitle": "Результати пошуку" "sectionTitle": "Результати пошуку"
}, },
"titles": { "titles": {
@ -128,15 +131,11 @@
}, },
"morning": { "morning": {
"default": "Що б ви хотіли подивитися сьогодні вранці?", "default": "Що б ви хотіли подивитися сьогодні вранці?",
"extra": [ "extra": ["Я чув, що \"Перед сходом сонця\" гарний"]
"Я чув, що \"Перед сходом сонця\" гарний"
]
}, },
"night": { "night": {
"default": "Що б ви хотіли подивитися сьогодні ввечері?", "default": "Що б ви хотіли подивитися сьогодні ввечері?",
"extra": [ "extra": ["Втомився? Я чув, що \"Екзорцист\" хороший."]
"Втомився? Я чув, що \"Екзорцист\" хороший."
]
} }
} }
}, },

5
src/assets/locales/vi.json

@ -116,7 +116,10 @@
"failed": "Không thể tìm thấy nội dung, hãy thử lại!", "failed": "Không thể tìm thấy nội dung, hãy thử lại!",
"loading": "Đang tải...", "loading": "Đang tải...",
"noResults": "Chúng tôi không thể tìm thấy gì!", "noResults": "Chúng tôi không thể tìm thấy gì!",
"placeholder": "Bạn muốn xem gì?", "placeholder": {
"default": "Bạn muốn xem gì?",
"extra": []
},
"sectionTitle": "Kết quả tìm kiếm" "sectionTitle": "Kết quả tìm kiếm"
}, },
"titles": { "titles": {

17
src/assets/locales/zh-Hant.json

@ -116,27 +116,24 @@
"failed": "未能找到媒體,請重試!", "failed": "未能找到媒體,請重試!",
"loading": "載入中...", "loading": "載入中...",
"noResults": "我们找不到任何结果!", "noResults": "我们找不到任何结果!",
"placeholder": "您想看什麼?", "placeholder": {
"default": "您想看什麼?",
"extra": []
},
"sectionTitle": "搜索結果" "sectionTitle": "搜索結果"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "您今天下午想看什麼?", "default": "您今天下午想看什麼?",
"extra": [ "extra": ["想要來場冒險嗎?《侏羅紀公園》可能是完美選擇。"]
"想要來場冒險嗎?《侏羅紀公園》可能是完美選擇。"
]
}, },
"morning": { "morning": {
"default": "您今天早上想看什麼?", "default": "您今天早上想看什麼?",
"extra": [ "extra": ["我聽說《情留半天》不錯"]
"我聽說《情留半天》不錯"
]
}, },
"night": { "night": {
"default": "您今晚想看什麼?", "default": "您今晚想看什麼?",
"extra": [ "extra": ["疲倦了嗎?我聽說《驅魔人》不錯。"]
"疲倦了嗎?我聽說《驅魔人》不錯。"
]
} }
} }
}, },

17
src/assets/locales/zh.json

@ -116,27 +116,24 @@
"failed": "查找媒体失败,请重试!", "failed": "查找媒体失败,请重试!",
"loading": "载入中……", "loading": "载入中……",
"noResults": "我们找不到任何结果!", "noResults": "我们找不到任何结果!",
"placeholder": "您想看些什么?", "placeholder": {
"default": "您想看些什么?",
"extra": []
},
"sectionTitle": "搜索结果" "sectionTitle": "搜索结果"
}, },
"titles": { "titles": {
"day": { "day": {
"default": "您今天下午想看什么?", "default": "您今天下午想看什么?",
"extra": [ "extra": ["想要来场冒险?《侏罗纪公园》可能是最佳选项。"]
"想要来场冒险?《侏罗纪公园》可能是最佳选项。"
]
}, },
"morning": { "morning": {
"default": "您今早想看什么?", "default": "您今早想看什么?",
"extra": [ "extra": ["我听说《爱在黎明破晓前》不错"]
"我听说《爱在黎明破晓前》不错"
]
}, },
"night": { "night": {
"default": "您今晚想看什么?", "default": "您今晚想看什么?",
"extra": [ "extra": ["累了?我听说《驱魔人》不错。"]
"累了?我听说《驱魔人》不错。"
]
} }
} }
}, },

7
src/backend/helpers/report.ts

@ -5,13 +5,14 @@ import { useCallback } from "react";
import { isExtensionActiveCached } from "@/backend/extension/messaging"; import { isExtensionActiveCached } from "@/backend/extension/messaging";
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape"; import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
import { BACKEND_URL } from "@/setup/constants";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { PlayerMeta } from "@/stores/player/slices/source"; import { PlayerMeta } from "@/stores/player/slices/source";
// for anybody who cares - these are anonymous metrics. // for anybody who cares - these are anonymous metrics.
// They are just used for figuring out if providers are broken or not // They are just used for figuring out if providers are broken or not
const metricsEndpoint = "https://backend.movie-web.app/metrics/providers"; const metricsEndpoint = `${BACKEND_URL}/metrics/providers`;
const captchaMetricsEndpoint = "https://backend.movie-web.app/metrics/captcha"; const captchaMetricsEndpoint = `${BACKEND_URL}/metrics/captcha`;
const batchId = () => nanoid(32); const batchId = () => nanoid(32);
export type ProviderMetric = { export type ProviderMetric = {
@ -44,6 +45,7 @@ function getStackTrace(error: Error, lines: number) {
} }
export async function reportProviders(items: ProviderMetric[]): Promise<void> { export async function reportProviders(items: ProviderMetric[]): Promise<void> {
if (!BACKEND_URL) return;
return ofetch(metricsEndpoint, { return ofetch(metricsEndpoint, {
method: "POST", method: "POST",
body: { body: {
@ -156,6 +158,7 @@ export function useReportProviders() {
} }
export function reportCaptchaSolve(success: boolean) { export function reportCaptchaSolve(success: boolean) {
if (!BACKEND_URL) return;
ofetch(captchaMetricsEndpoint, { ofetch(captchaMetricsEndpoint, {
method: "POST", method: "POST",
body: { body: {

20
src/backend/helpers/subs.ts

@ -5,6 +5,11 @@ import { convertSubtitlesToSrt } from "@/components/player/utils/captions";
import { CaptionListItem } from "@/stores/player/slices/source"; import { CaptionListItem } from "@/stores/player/slices/source";
import { SimpleCache } from "@/utils/cache"; import { SimpleCache } from "@/utils/cache";
import {
isExtensionActiveCached,
sendExtensionRequest,
} from "../extension/messaging";
export const subtitleTypeList = list().map((type) => `.${type}`); export const subtitleTypeList = list().map((type) => `.${type}`);
const downloadCache = new SimpleCache<string, string>(); const downloadCache = new SimpleCache<string, string>();
downloadCache.setCompare((a, b) => a === b); downloadCache.setCompare((a, b) => a === b);
@ -21,7 +26,22 @@ export async function downloadCaption(
let data: string | undefined; let data: string | undefined;
if (caption.needsProxy) { if (caption.needsProxy) {
if (isExtensionActiveCached()) {
const extensionResponse = await sendExtensionRequest({
url: caption.url,
method: "GET",
});
if (
!extensionResponse?.success ||
typeof extensionResponse.response.body !== "string"
) {
throw new Error("failed to get caption data from extension");
}
data = extensionResponse.response.body;
} else {
data = await proxiedFetch<string>(caption.url, { responseType: "text" }); data = await proxiedFetch<string>(caption.url, { responseType: "text" });
}
} else { } else {
data = await fetch(caption.url).then((v) => v.text()); data = await fetch(caption.url).then((v) => v.text());
} }

10
src/components/buttons/Button.tsx

@ -1,6 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { ReactNode, useCallback } from "react"; import { ReactNode, useCallback } from "react";
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";
@ -21,7 +20,6 @@ interface Props {
} }
export function Button(props: Props) { export function Button(props: Props) {
const navigate = useNavigate();
const { onClick, href, loading } = props; const { onClick, href, loading } = props;
const cb = useCallback( const cb = useCallback(
( (
@ -31,10 +29,12 @@ export function Button(props: Props) {
>, >,
) => { ) => {
if (loading) return; if (loading) return;
if (href && !onClick) navigate(href); if (href && !onClick) {
else onClick?.(event); event.preventDefault();
window.open(href, "_blank", "noreferrer");
} else onClick?.(event);
}, },
[onClick, href, navigate, loading], [onClick, href, loading],
); );
let colorClasses = "bg-white hover:bg-gray-200 text-black"; let colorClasses = "bg-white hover:bg-gray-200 text-black";

16
src/components/player/atoms/Volume.tsx

@ -47,8 +47,22 @@ export function Volume(props: Props) {
if (dragging) percentage = makePercentage(dragPercentage); if (dragging) percentage = makePercentage(dragPercentage);
const percentageString = makePercentageString(percentage); const percentageString = makePercentageString(percentage);
const handleWheel = useCallback(
(event: React.WheelEvent<HTMLDivElement>) => {
event.preventDefault();
let newVolume = volume - event.deltaY / 1000;
newVolume = Math.max(0, Math.min(newVolume, 1));
setVolume(newVolume);
},
[volume, setVolume],
);
return ( return (
<div className={props.className} onMouseEnter={handleMouseEnter}> <div
className={props.className}
onMouseEnter={handleMouseEnter}
onWheel={handleWheel}
>
<div className="pointer-events-auto flex cursor-pointer items-center py-0"> <div className="pointer-events-auto flex cursor-pointer items-center py-0">
<div className="px-4 text-2xl text-white" onClick={handleClick}> <div className="px-4 text-2xl text-white" onClick={handleClick}>
<Icon icon={percentage > 0 ? Icons.VOLUME : Icons.VOLUME_X} /> <Icon icon={percentage > 0 ? Icons.VOLUME : Icons.VOLUME_X} />

6
src/hooks/auth/useAuth.ts

@ -63,6 +63,7 @@ export function useAuth() {
const login = useCallback( const login = useCallback(
async (loginData: LoginData) => { async (loginData: LoginData) => {
if (!backendUrl) return;
const keys = await keysFromMnemonic(loginData.mnemonic); const keys = await keysFromMnemonic(loginData.mnemonic);
const publicKeyBase64Url = bytesToBase64Url(keys.publicKey); const publicKeyBase64Url = bytesToBase64Url(keys.publicKey);
const { challenge } = await getLoginChallengeToken( const { challenge } = await getLoginChallengeToken(
@ -87,7 +88,7 @@ export function useAuth() {
); );
const logout = useCallback(async () => { const logout = useCallback(async () => {
if (!currentAccount) return; if (!currentAccount || !backendUrl) return;
try { try {
await removeSession( await removeSession(
backendUrl, backendUrl,
@ -102,6 +103,7 @@ export function useAuth() {
const register = useCallback( const register = useCallback(
async (registerData: RegistrationData) => { async (registerData: RegistrationData) => {
if (!backendUrl) return;
const { challenge } = await getRegisterChallengeToken( const { challenge } = await getRegisterChallengeToken(
backendUrl, backendUrl,
registerData.recaptchaToken, registerData.recaptchaToken,
@ -134,6 +136,7 @@ export function useAuth() {
progressItems: Record<string, ProgressMediaItem>, progressItems: Record<string, ProgressMediaItem>,
bookmarks: Record<string, BookmarkMediaItem>, bookmarks: Record<string, BookmarkMediaItem>,
) => { ) => {
if (!backendUrl) return;
if ( if (
Object.keys(progressItems).length === 0 && Object.keys(progressItems).length === 0 &&
Object.keys(bookmarks).length === 0 Object.keys(bookmarks).length === 0
@ -159,6 +162,7 @@ export function useAuth() {
const restore = useCallback( const restore = useCallback(
async (account: AccountWithToken) => { async (account: AccountWithToken) => {
if (!backendUrl) return;
let user: { user: UserResponse; session: SessionResponse }; let user: { user: UserResponse; session: SessionResponse };
try { try {
user = await getUser(backendUrl, account.token); user = await getUser(backendUrl, account.token);

2
src/hooks/auth/useBackendUrl.ts

@ -1,7 +1,7 @@
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
export function useBackendUrl() { export function useBackendUrl(): string | undefined {
const backendUrl = useAuthStore((s) => s.backendUrl); const backendUrl = useAuthStore((s) => s.backendUrl);
return backendUrl ?? conf().BACKEND_URL; return backendUrl ?? conf().BACKEND_URL;
} }

7
src/hooks/useSettingsState.ts

@ -9,6 +9,7 @@ import {
} from "react"; } from "react";
import { SubtitleStyling } from "@/stores/subtitles"; import { SubtitleStyling } from "@/stores/subtitles";
import { usePreviewThemeStore } from "@/stores/theme";
export function useDerived<T>( export function useDerived<T>(
initial: T, initial: T,
@ -56,6 +57,11 @@ export function useSettingsState(
const [backendUrlState, setBackendUrl, resetBackendUrl, backendUrlChanged] = const [backendUrlState, setBackendUrl, resetBackendUrl, backendUrlChanged] =
useDerived(backendUrl); useDerived(backendUrl);
const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme); const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme);
const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme);
const resetPreviewTheme = useCallback(
() => setPreviewTheme(theme),
[setPreviewTheme, theme],
);
const [ const [
appLanguageState, appLanguageState,
setAppLanguage, setAppLanguage,
@ -81,6 +87,7 @@ export function useSettingsState(
function reset() { function reset() {
resetTheme(); resetTheme();
resetPreviewTheme();
resetAppLanguage(); resetAppLanguage();
resetSubStyling(); resetSubStyling();
resetProxyUrls(); resetProxyUrls();

34
src/pages/Settings.tsx

@ -1,5 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
@ -33,7 +33,7 @@ import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { useLanguageStore } from "@/stores/language"; import { useLanguageStore } from "@/stores/language";
import { usePreferencesStore } from "@/stores/preferences"; import { usePreferencesStore } from "@/stores/preferences";
import { useSubtitleStore } from "@/stores/subtitles"; import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme"; import { usePreviewThemeStore, useThemeStore } from "@/stores/theme";
import { SubPageLayout } from "./layouts/SubPageLayout"; import { SubPageLayout } from "./layouts/SubPageLayout";
import { PreferencesPart } from "./parts/settings/PreferencesPart"; import { PreferencesPart } from "./parts/settings/PreferencesPart";
@ -70,6 +70,7 @@ export function AccountSettings(props: {
const url = useBackendUrl(); const url = useBackendUrl();
const { account } = props; const { account } = props;
const [sessionsResult, execSessions] = useAsyncFn(() => { const [sessionsResult, execSessions] = useAsyncFn(() => {
if (!url) return Promise.resolve([]);
return getSessions(url, account); return getSessions(url, account);
}, [account, url]); }, [account, url]);
useEffect(() => { useEffect(() => {
@ -103,6 +104,8 @@ export function SettingsPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const activeTheme = useThemeStore((s) => s.theme); const activeTheme = useThemeStore((s) => s.theme);
const setTheme = useThemeStore((s) => s.setTheme); const setTheme = useThemeStore((s) => s.setTheme);
const previewTheme = usePreviewThemeStore((s) => s.previewTheme);
const setPreviewTheme = usePreviewThemeStore((s) => s.setPreviewTheme);
const appLanguage = useLanguageStore((s) => s.language); const appLanguage = useLanguageStore((s) => s.language);
const setAppLanguage = useLanguageStore((s) => s.setLanguage); const setAppLanguage = useLanguageStore((s) => s.setLanguage);
@ -143,8 +146,27 @@ export function SettingsPage() {
enableThumbnails, enableThumbnails,
); );
useEffect(() => {
setPreviewTheme(activeTheme ?? "default");
}, [setPreviewTheme, activeTheme]);
useEffect(() => {
// Clear preview theme on unmount
return () => {
setPreviewTheme(null);
};
}, [setPreviewTheme]);
const setThemeWithPreview = useCallback(
(theme: string) => {
state.theme.set(theme === "default" ? null : theme);
setPreviewTheme(theme);
},
[state.theme, setPreviewTheme],
);
const saveChanges = useCallback(async () => { const saveChanges = useCallback(async () => {
if (account) { if (account && backendUrl) {
if ( if (
state.appLanguage.changed || state.appLanguage.changed ||
state.theme.changed || state.theme.changed ||
@ -241,7 +263,11 @@ export function SettingsPage() {
/> />
</div> </div>
<div id="settings-appearance" className="mt-48"> <div id="settings-appearance" className="mt-48">
<ThemePart active={state.theme.state} setTheme={state.theme.set} /> <ThemePart
active={previewTheme ?? "default"}
inUse={activeTheme ?? "default"}
setTheme={setThemeWithPreview}
/>
</div> </div>
<div id="settings-captions" className="mt-48"> <div id="settings-captions" className="mt-48">
<CaptionsPart <CaptionsPart

4
src/pages/layouts/SubPageLayout.tsx

@ -10,13 +10,13 @@ export function BlurEllipsis(props: { positionClass?: string }) {
<div <div
className={classNames( className={classNames(
props.positionClass ?? "fixed", props.positionClass ?? "fixed",
"top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25", "top-0 -right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentA blur-[100px] pointer-events-none opacity-25 transition-colors duration-75",
)} )}
/> />
<div <div
className={classNames( className={classNames(
props.positionClass ?? "fixed", props.positionClass ?? "fixed",
"top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25", "top-0 right-48 rotate-[32deg] w-[50rem] h-[15rem] rounded-[70rem] bg-background-accentB blur-[100px] pointer-events-none opacity-25 transition-colors duration-75",
)} )}
/> />
</> </>

8
src/pages/onboarding/Onboarding.tsx

@ -13,6 +13,7 @@ import {
} from "@/pages/onboarding/onboardingHooks"; } from "@/pages/onboarding/onboardingHooks";
import { Card, CardContent, Link } from "@/pages/onboarding/utils"; import { Card, CardContent, Link } from "@/pages/onboarding/utils";
import { PageTitle } from "@/pages/parts/util/PageTitle"; import { PageTitle } from "@/pages/parts/util/PageTitle";
import { getProxyUrls } from "@/utils/proxyUrls";
function VerticalLine(props: { className?: string }) { function VerticalLine(props: { className?: string }) {
return ( return (
@ -27,6 +28,7 @@ export function OnboardingPage() {
const skipModal = useModal("skip"); const skipModal = useModal("skip");
const { completeAndRedirect } = useRedirectBack(); const { completeAndRedirect } = useRedirectBack();
const { t } = useTranslation(); const { t } = useTranslation();
const noProxies = getProxyUrls().length === 0;
return ( return (
<MinimalPageLayout> <MinimalPageLayout>
@ -85,7 +87,8 @@ export function OnboardingPage() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{noProxies ? null : (
<>
<p className="text-center hidden md:block mt-12"> <p className="text-center hidden md:block mt-12">
<Trans i18nKey="onboarding.start.options.default.text"> <Trans i18nKey="onboarding.start.options.default.text">
<br /> <br />
@ -96,7 +99,6 @@ export function OnboardingPage() {
/> />
</Trans> </Trans>
</p> </p>
<div className=" max-w-[300px] mx-auto md:hidden mt-12 "> <div className=" max-w-[300px] mx-auto md:hidden mt-12 ">
<Button <Button
className="!text-type-text !bg-opacity-50" className="!text-type-text !bg-opacity-50"
@ -111,6 +113,8 @@ export function OnboardingPage() {
</span> </span>
</Button> </Button>
</div> </div>
</>
)}
</CenterContainer> </CenterContainer>
</MinimalPageLayout> </MinimalPageLayout>
); );

2
src/pages/onboarding/OnboardingExtension.tsx

@ -115,7 +115,7 @@ export function ExtensionStatus(props: {
</div> </div>
</Card> </Card>
{lastKnownStatus === "unknown" ? <RefreshBar /> : null} {lastKnownStatus === "unknown" ? <RefreshBar /> : null}
{props.showHelp ? ( {props.showHelp && props.status !== "success" ? (
<Card className="mt-4"> <Card className="mt-4">
<div className="flex items-center space-x-7"> <div className="flex items-center space-x-7">
<Icon icon={Icons.WARNING} className="text-type-danger text-2xl" /> <Icon icon={Icons.WARNING} className="text-type-danger text-2xl" />

2
src/pages/onboarding/OnboardingProxy.tsx

@ -43,7 +43,7 @@ export function OnboardingProxyPage() {
throw new Error("onboarding.proxy.input.errorNotProxy"); throw new Error("onboarding.proxy.input.errorNotProxy");
setProxySet([url]); setProxySet([url]);
if (account) { if (account && backendUrl) {
await updateSettings(backendUrl, account, { await updateSettings(backendUrl, account, {
proxyUrls: [url], proxyUrls: [url],
}); });

14
src/pages/parts/admin/BackendTestPart.tsx

@ -32,13 +32,21 @@ export function BackendTestPart() {
value: null, value: null,
}); });
if (!backendUrl) {
return setStatus({
hasTested: true,
success: false,
errorText: "Backend URL is not set",
value: null,
});
}
try { try {
const backendData = await getBackendMeta(backendUrl); const backendData = await getBackendMeta(backendUrl);
return setStatus({ return setStatus({
hasTested: true, hasTested: true,
success: true, success: true,
errorText: errorText: "",
"Failed to call backend, double check the URL key and your internet connection",
value: backendData, value: backendData,
}); });
} catch (err) { } catch (err) {
@ -46,7 +54,7 @@ export function BackendTestPart() {
hasTested: true, hasTested: true,
success: false, success: false,
errorText: errorText:
"Failed to call backend, double check the URL key and your internet connection", "Failed to call backend, double check the URL, your internet connection, and ensure CORS is properly configured on your backend.",
value: null, value: null,
}); });
} }

8
src/pages/parts/admin/TMDBTestPart.tsx

@ -29,7 +29,7 @@ export function TMDBTestPart() {
return setStatus({ return setStatus({
hasTested: true, hasTested: true,
success: false, success: false,
errorText: "TMDB api key is not set", errorText: "TMDB API key is not set",
}); });
} }
const isJWT = tmdbApiKey.split(".").length > 2; const isJWT = tmdbApiKey.split(".").length > 2;
@ -37,7 +37,7 @@ export function TMDBTestPart() {
return setStatus({ return setStatus({
hasTested: true, hasTested: true,
success: false, success: false,
errorText: "TMDB api key is not a read only key", errorText: "TMDB API key is not a read only key",
}); });
} }
@ -48,7 +48,7 @@ export function TMDBTestPart() {
hasTested: true, hasTested: true,
success: false, success: false,
errorText: errorText:
"Failed to call tmdb, double check api key and your internet connection", "Failed to call TMDB, double check API key and your internet connection",
}); });
} }
@ -61,7 +61,7 @@ export function TMDBTestPart() {
return ( return (
<> <>
<Heading2 className="mb-8 mt-12">TMDB tests</Heading2> <Heading2 className="mb-8 mt-12">TMDB test</Heading2>
<Box> <Box>
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-1"> <div className="flex-1">

20
src/pages/parts/admin/WorkerTestPart.tsx

@ -52,14 +52,18 @@ export function WorkerTestPart() {
{ id: string; status: "error" | "success"; error?: Error }[] { id: string; status: "error" | "success"; error?: Error }[]
>([]); >([]);
const [buttonDisabled, setButtonDisabled] = useState(false);
const [testState, runTests] = useAsyncFn(async () => { const [testState, runTests] = useAsyncFn(async () => {
setButtonDisabled(true);
function updateWorker(id: string, data: (typeof workerState)[number]) { function updateWorker(id: string, data: (typeof workerState)[number]) {
setWorkerState((s) => { setWorkerState((s) => {
return [...s.filter((v) => v.id !== id), data]; return [...s.filter((v) => v.id !== id), data];
}); });
} }
setWorkerState([]); setWorkerState([]);
for (const worker of workerList) {
const workerPromises = workerList.map(async (worker) => {
try { try {
if (worker.url.endsWith("/")) { if (worker.url.endsWith("/")) {
updateWorker(worker.id, { updateWorker(worker.id, {
@ -67,7 +71,7 @@ export function WorkerTestPart() {
status: "error", status: "error",
error: new Error("URL ends with slash"), error: new Error("URL ends with slash"),
}); });
continue; return;
} }
await singularProxiedFetch( await singularProxiedFetch(
worker.url, worker.url,
@ -85,7 +89,10 @@ export function WorkerTestPart() {
error: err as Error, error: err as Error,
}); });
} }
} });
await Promise.all(workerPromises);
setTimeout(() => setButtonDisabled(false), 5000);
}, [workerList, setWorkerState]); }, [workerList, setWorkerState]);
return ( return (
@ -112,7 +119,12 @@ export function WorkerTestPart() {
})} })}
<Divider /> <Divider />
<div className="flex justify-end"> <div className="flex justify-end">
<Button theme="purple" loading={testState.loading} onClick={runTests}> <Button
theme="purple"
loading={testState.loading}
onClick={buttonDisabled ? undefined : runTests}
disabled={buttonDisabled}
>
Test workers Test workers
</Button> </Button>
</div> </div>

3
src/pages/parts/auth/LoginFormPart.tsx

@ -52,6 +52,9 @@ export function LoginFormPart(props: LoginFormPartProps) {
throw err; throw err;
} }
if (!account)
throw new Error(t("auth.login.validationError") ?? undefined);
await importData(account, progressItems, bookmarkItems); await importData(account, progressItems, bookmarkItems);
await restore(account); await restore(account);

22
src/pages/parts/auth/TrustBackendPart.tsx

@ -22,8 +22,12 @@ interface TrustBackendPartProps {
export function TrustBackendPart(props: TrustBackendPartProps) { export function TrustBackendPart(props: TrustBackendPartProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const backendUrl = useBackendUrl(); const backendUrl = useBackendUrl();
const hostname = useMemo(() => new URL(backendUrl).hostname, [backendUrl]); const hostname = useMemo(
() => (backendUrl ? new URL(backendUrl).hostname : undefined),
[backendUrl],
);
const result = useAsync(() => { const result = useAsync(() => {
if (!backendUrl) return Promise.resolve(null);
return getBackendMeta(backendUrl); return getBackendMeta(backendUrl);
}, [backendUrl]); }, [backendUrl]);
const { t } = useTranslation(); const { t } = useTranslation();
@ -50,9 +54,10 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
return ( return (
<LargeCard> <LargeCard>
<LargeCardText <LargeCardText
title={t("auth.trust.title")} title={hostname ? t("auth.trust.title") : t("auth.trust.noHostTitle")}
icon={<Icon icon={Icons.CIRCLE_EXCLAMATION} />} icon={<Icon icon={Icons.CIRCLE_EXCLAMATION} />}
> >
{hostname ? (
<Trans <Trans
i18nKey="auth.trust.host" i18nKey="auth.trust.host"
values={{ values={{
@ -61,8 +66,13 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
> >
<span className="text-white" /> <span className="text-white" />
</Trans> </Trans>
) : (
<p>{t("auth.trust.noHost")}</p>
)}
</LargeCardText> </LargeCardText>
{hostname ? (
<>
<div className="border border-authentication-border rounded-xl px-4 py-8 flex flex-col items-center space-y-2 my-8"> <div className="border border-authentication-border rounded-xl px-4 py-8 flex flex-col items-center space-y-2 my-8">
{cardContent} {cardContent}
</div> </div>
@ -82,6 +92,14 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
<MwLink to="/login">.</MwLink> <MwLink to="/login">.</MwLink>
</Trans> </Trans>
</p> </p>
</>
) : (
<LargeCardButtons>
<Button theme="purple" onClick={() => navigate("/")}>
{t("auth.trust.no")}
</Button>
</LargeCardButtons>
)}
</LargeCard> </LargeCard>
); );
} }

5
src/pages/parts/auth/VerifyPassphrasePart.tsx

@ -47,6 +47,8 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
const [result, execute] = useAsyncFn( const [result, execute] = useAsyncFn(
async (inputMnemonic: string) => { async (inputMnemonic: string) => {
if (!backendUrl)
throw new Error(t("auth.verify.noBackendUrl") ?? undefined);
if (!props.mnemonic || !props.userData) if (!props.mnemonic || !props.userData)
throw new Error(t("auth.verify.invalidData") ?? undefined); throw new Error(t("auth.verify.invalidData") ?? undefined);
@ -68,6 +70,9 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
recaptchaToken, recaptchaToken,
}); });
if (!account)
throw new Error(t("auth.verify.registrationFailed") ?? undefined);
await importData(account, progressItems, bookmarkItems); await importData(account, progressItems, bookmarkItems);
await updateSettings(backendUrl, account, { await updateSettings(backendUrl, account, {

6
src/pages/parts/home/HeroPart.tsx

@ -1,5 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import Sticky from "react-sticky-el"; import Sticky from "react-sticky-el";
import { useWindowSize } from "react-use"; import { useWindowSize } from "react-use";
@ -26,7 +25,6 @@ function getTimeOfDay(date: Date): "night" | "morning" | "day" {
export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) { export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
const { t: randomT } = useRandomTranslation(); const { t: randomT } = useRandomTranslation();
const { t } = useTranslation();
const [search, setSearch, setSearchUnFocus] = searchParams; const [search, setSearch, setSearchUnFocus] = searchParams;
const [, setShowBg] = useState(false); const [, setShowBg] = useState(false);
const bannerSize = useBannerSize(); const bannerSize = useBannerSize();
@ -54,7 +52,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
const time = getTimeOfDay(new Date()); const time = getTimeOfDay(new Date());
const title = randomT(`home.titles.${time}`); const title = randomT(`home.titles.${time}`);
const placeholder = randomT(`home.search.placeholder`);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useSlashFocus(inputRef); useSlashFocus(inputRef);
@ -77,7 +75,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
onChange={setSearch} onChange={setSearch}
value={search} value={search}
onUnFocus={setSearchUnFocus} onUnFocus={setSearchUnFocus}
placeholder={t("home.search.placeholder") ?? ""} placeholder={placeholder ?? ""}
/> />
</Sticky> </Sticky>
</div> </div>

2
src/pages/parts/settings/AccountActionsPart.tsx

@ -18,7 +18,7 @@ export function AccountActionsPart() {
const deleteModal = useModal("account-delete"); const deleteModal = useModal("account-delete");
const [deleteResult, deleteExec] = useAsyncFn(async () => { const [deleteResult, deleteExec] = useAsyncFn(async () => {
if (!account) return; if (!account || !url) return;
await deleteUser(url, account); await deleteUser(url, account);
await logout(); await logout();
deleteModal.hide(); deleteModal.hide();

4
src/pages/parts/settings/ConnectionsPart.tsx

@ -55,7 +55,7 @@ function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
</p> </p>
<p className="max-w-[20rem] font-medium"> <p className="max-w-[20rem] font-medium">
<Trans i18nKey="settings.connections.workers.description"> <Trans i18nKey="settings.connections.workers.description">
<MwLink to="https://docs.movie-web.app/proxy/deploy"> <MwLink to="https://movie-web.github.io/docs/proxy/deploy">
Proxy documentation Proxy documentation
</MwLink> </MwLink>
</Trans> </Trans>
@ -125,7 +125,7 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
</p> </p>
<p className="max-w-[20rem] font-medium"> <p className="max-w-[20rem] font-medium">
<Trans i18nKey="settings.connections.server.description"> <Trans i18nKey="settings.connections.server.description">
<MwLink to="https://docs.movie-web.app/backend/deploy"> <MwLink to="https://movie-web.github.io/docs/backend/deploy">
Backend documentation Backend documentation
</MwLink> </MwLink>
</Trans> </Trans>

1
src/pages/parts/settings/DeviceListPart.tsx

@ -24,6 +24,7 @@ export function Device(props: {
const token = useAuthStore((s) => s.account?.token); const token = useAuthStore((s) => s.account?.token);
const [result, exec] = useAsyncFn(async () => { const [result, exec] = useAsyncFn(async () => {
if (!token) throw new Error("No token present"); if (!token) throw new Error("No token present");
if (!url) throw new Error("No backend set");
await removeSession(url, token, props.id); await removeSession(url, token, props.id);
props.onRemove?.(); props.onRemove?.();
}, [url, token, props.id]); }, [url, token, props.id]);

7
src/pages/parts/settings/SidebarPart.tsx

@ -14,9 +14,9 @@ import { useAuthStore } from "@/stores/auth";
const rem = 16; const rem = 16;
function SecureBadge(props: { url: string }) { function SecureBadge(props: { url: string | undefined }) {
const { t } = useTranslation(); const { t } = useTranslation();
const secure = props.url.startsWith("https://"); const secure = props.url ? props.url.startsWith("https://") : false;
return ( return (
<div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold"> <div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold">
<Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} /> <Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} />
@ -68,6 +68,7 @@ export function SidebarPart() {
const backendUrl = useBackendUrl(); const backendUrl = useBackendUrl();
const backendMeta = useAsync(async () => { const backendMeta = useAsync(async () => {
if (!backendUrl) return;
return getBackendMeta(backendUrl); return getBackendMeta(backendUrl);
}, [backendUrl]); }, [backendUrl]);
@ -159,7 +160,7 @@ export function SidebarPart() {
<SecureBadge url={backendUrl} /> <SecureBadge url={backendUrl} />
</div> </div>
<p className="text-white"> <p className="text-white">
{backendUrl.replace(/https?:\/\//, "")} {backendUrl?.replace(/https?:\/\//, "") ?? "—"}
</p> </p>
</div> </div>

27
src/pages/parts/settings/ThemePart.tsx

@ -5,20 +5,29 @@ import { Icon, Icons } from "@/components/Icon";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
const availableThemes = [ const availableThemes = [
{
id: "default",
selector: "theme-default",
key: "settings.appearance.themes.default",
},
{ {
id: "blue", id: "blue",
selector: "theme-blue",
key: "settings.appearance.themes.blue", key: "settings.appearance.themes.blue",
}, },
{ {
id: "teal", id: "teal",
selector: "theme-teal",
key: "settings.appearance.themes.teal", key: "settings.appearance.themes.teal",
}, },
{ {
id: "red", id: "red",
selector: "theme-red",
key: "settings.appearance.themes.red", key: "settings.appearance.themes.red",
}, },
{ {
id: "gray", id: "gray",
selector: "theme-gray",
key: "settings.appearance.themes.gray", key: "settings.appearance.themes.gray",
}, },
]; ];
@ -26,6 +35,7 @@ const availableThemes = [
function ThemePreview(props: { function ThemePreview(props: {
selector?: string; selector?: string;
active?: boolean; active?: boolean;
inUse?: boolean;
name: string; name: string;
onClick?: () => void; onClick?: () => void;
}) { }) {
@ -105,7 +115,7 @@ function ThemePreview(props: {
<span <span
className={classNames( className={classNames(
"inline-block px-3 py-1 leading-tight text-sm transition-opacity duration-150 rounded-full bg-pill-activeBackground text-white/85", "inline-block px-3 py-1 leading-tight text-sm transition-opacity duration-150 rounded-full bg-pill-activeBackground text-white/85",
props.active ? "opacity-100" : "opacity-0 pointer-events-none", props.inUse ? "opacity-100" : "opacity-0 pointer-events-none",
)} )}
> >
{t("settings.appearance.activeTheme")} {t("settings.appearance.activeTheme")}
@ -116,8 +126,9 @@ function ThemePreview(props: {
} }
export function ThemePart(props: { export function ThemePart(props: {
active: string | null; active: string;
setTheme: (theme: string | null) => void; inUse: string;
setTheme: (theme: string) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -125,17 +136,11 @@ export function ThemePart(props: {
<div> <div>
<Heading1 border>{t("settings.appearance.title")}</Heading1> <Heading1 border>{t("settings.appearance.title")}</Heading1>
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]"> <div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
{/* default theme */}
<ThemePreview
name={t("settings.appearance.themes.default")}
selector="theme-default"
active={props.active === null}
onClick={() => props.setTheme(null)}
/>
{availableThemes.map((v) => ( {availableThemes.map((v) => (
<ThemePreview <ThemePreview
selector={`theme-${v.id}`} selector={v.selector}
active={props.active === v.id} active={props.active === v.id}
inUse={props.inUse === v.id}
name={t(v.key)} name={t(v.key)}
key={v.id} key={v.id}
onClick={() => props.setTheme(v.id)} onClick={() => props.setTheme(v.id)}

6
src/setup/constants.ts

@ -1,6 +1,6 @@
export const APP_VERSION = import.meta.env.PACKAGE_VERSION; export const APP_VERSION = import.meta.env.PACKAGE_VERSION;
export const DISCORD_LINK = "https://discord.movie-web.app"; export const DISCORD_LINK = "https://movie-web.github.io/links/discord";
export const GITHUB_LINK = "https://github.com/movie-web/movie-web"; export const GITHUB_LINK = "https://github.com/movie-web/movie-web";
export const DONATION_LINK = "https://ko-fi.com/movieweb"; export const DONATION_LINK = "https://ko-fi.com/movieweb";
export const GA_ID = "G-44YVXRL61C"; export const GA_ID = import.meta.env.VITE_GA_ID;
export const BACKEND_URL = "https://backend.movie-web.app"; export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;

2
src/setup/ga.ts

@ -2,8 +2,10 @@ import ReactGA from "react-ga4";
import { GA_ID } from "@/setup/constants"; import { GA_ID } from "@/setup/constants";
if (GA_ID) {
ReactGA.initialize([ ReactGA.initialize([
{ {
trackingId: GA_ID, trackingId: GA_ID,
}, },
]); ]);
}

1
src/stores/bookmarks/BookmarkSyncer.tsx

@ -60,6 +60,7 @@ export function BookmarkSyncer() {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
(async () => { (async () => {
if (!url) return;
const state = useBookmarkStore.getState(); const state = useBookmarkStore.getState();
const user = useAuthStore.getState(); const user = useAuthStore.getState();
await syncBookmarks( await syncBookmarks(

1
src/stores/progress/ProgressSyncer.tsx

@ -62,6 +62,7 @@ export function ProgressSyncer() {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
(async () => { (async () => {
if (!url) return;
const state = useProgressStore.getState(); const state = useProgressStore.getState();
const user = useAuthStore.getState(); const user = useAuthStore.getState();
await syncProgress( await syncProgress(

1
src/stores/subtitles/SettingsSyncer.tsx

@ -16,6 +16,7 @@ export function SettingsSyncer() {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
(async () => { (async () => {
if (!url) return;
const state = useSubtitleStore.getState(); const state = useSubtitleStore.getState();
const user = useAuthStore.getState(); const user = useAuthStore.getState();
if (state.lastSync.lastSelectedLanguage === state.lastSelectedLanguage) if (state.lastSync.lastSelectedLanguage === state.lastSelectedLanguage)

21
src/stores/theme/index.tsx

@ -25,12 +25,31 @@ export const useThemeStore = create(
), ),
); );
export interface PreviewThemeStore {
previewTheme: string | null;
setPreviewTheme(v: string | null): void;
}
export const usePreviewThemeStore = create(
immer<PreviewThemeStore>((set) => ({
previewTheme: null,
setPreviewTheme(v) {
set((s) => {
s.previewTheme = v;
});
},
})),
);
export function ThemeProvider(props: { export function ThemeProvider(props: {
children?: ReactNode; children?: ReactNode;
applyGlobal?: boolean; applyGlobal?: boolean;
}) { }) {
const previewTheme = usePreviewThemeStore((s) => s.previewTheme);
const theme = useThemeStore((s) => s.theme); const theme = useThemeStore((s) => s.theme);
const themeSelector = theme ? `theme-${theme}` : undefined;
const themeToDisplay = previewTheme ?? theme;
const themeSelector = themeToDisplay ? `theme-${themeToDisplay}` : undefined;
return ( return (
<div className={themeSelector}> <div className={themeSelector}>

Loading…
Cancel
Save