Merge pull request #812 from movie-web/dev

V4.3.0
This commit is contained in:
William Oldham 2024-01-23 21:07:13 +00:00 committed by GitHub
commit bb8f0599b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 2699 additions and 622 deletions

View File

@ -12,16 +12,16 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: 'pnpm'
- name: Install pnpm packages
@ -31,7 +31,7 @@ jobs:
run: pnpm run build:pwa
- name: Upload production-ready build files
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: pwa
path: ./dist
@ -42,16 +42,16 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: 'pnpm'
- name: Install pnpm packages
@ -61,7 +61,7 @@ jobs:
run: pnpm run build
- name: Upload production-ready build files
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: normal
path: ./dist
@ -73,10 +73,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download PWA artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: pwa
path: ./dist_pwa
@ -85,7 +85,7 @@ jobs:
run: cd dist_pwa && zip -r ../movie-web.pwa.zip .
- name: Download normal artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: normal
path: ./dist_normal
@ -142,17 +142,17 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@ -160,7 +160,7 @@ jobs:
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
@ -170,9 +170,12 @@ jobs:
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
push: true
platforms: linux/amd64,linux/arm64
context: .
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -14,16 +14,16 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: 'pnpm'
- name: Install pnpm packages
@ -38,16 +38,16 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: 'pnpm'
- name: Install pnpm packages
@ -55,3 +55,21 @@ jobs:
- name: Build Project
run: pnpm run build
docker:
name: Build Docker
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
push: false
platforms: linux/amd64,linux/arm64
context: .

View File

@ -1,4 +1,4 @@
FROM node:16.15-alpine as build
FROM node:20-alpine as build
WORKDIR /app
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

View File

@ -1,6 +1,6 @@
{
"name": "movie-web",
"version": "4.2.5",
"version": "4.3.0",
"private": true,
"homepage": "https://movie-web.app",
"scripts": {
@ -29,8 +29,9 @@
"@formkit/auto-animate": "^0.8.1",
"@headlessui/react": "^1.7.17",
"@ladjs/country-language": "^1.0.3",
"@movie-web/providers": "^2.0.5",
"@movie-web/providers": "^2.1.0",
"@noble/hashes": "^1.3.3",
"@plasmohq/messaging": "^0.6.1",
"@react-spring/web": "^9.7.3",
"@scure/bip39": "^1.2.2",
"@sozialhelden/ietf-language-tags": "^5.4.2",
@ -62,6 +63,7 @@
"react-sticky-el": "^2.1.0",
"react-turnstile": "^1.1.2",
"react-use": "^17.4.2",
"semver": "^7.5.4",
"slugify": "^1.6.6",
"subsrt-ts": "^2.1.2",
"zustand": "^4.4.7"
@ -70,6 +72,7 @@
"@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@babel/preset-typescript": "^7.23.3",
"@rollup/wasm-node": "^4.9.4",
"@types/chromecast-caf-sender": "^1.0.8",
"@types/crypto-js": "^4.2.1",
"@types/dompurify": "^3.0.5",
@ -85,6 +88,7 @@
"@types/react-router-dom": "^5.3.3",
"@types/react-stickynode": "^4.0.3",
"@types/react-transition-group": "^4.4.10",
"@types/semver": "^7.5.6",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@vitejs/plugin-react": "^4.2.1",
@ -113,7 +117,7 @@
"tailwindcss-themer": "^4.0.0",
"type-fest": "^4.8.3",
"typescript": "^5.3.3",
"vite": "^5.0.10",
"vite": "^5.0.12",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-package-version": "^1.1.0",
"vite-plugin-pwa": "^0.17.4",
@ -125,7 +129,8 @@
"get-func-name@<2.0.1": ">=2.0.1",
"postcss@<8.4.31": ">=8.4.31",
"@babel/traverse@<7.23.2": ">=7.23.2",
"crypto-js@<4.2.0": ">=4.2.0"
"crypto-js@<4.2.0": ">=4.2.0",
"rollup": "npm:@rollup/wasm-node"
}
}
}

305
pnpm-lock.yaml generated
View File

@ -9,6 +9,7 @@ overrides:
postcss@<8.4.31: '>=8.4.31'
'@babel/traverse@<7.23.2': '>=7.23.2'
crypto-js@<4.2.0: '>=4.2.0'
rollup: npm:@rollup/wasm-node
dependencies:
'@formkit/auto-animate':
@ -21,11 +22,14 @@ dependencies:
specifier: ^1.0.3
version: 1.0.3
'@movie-web/providers':
specifier: ^2.0.5
version: 2.0.5
specifier: ^2.1.0
version: 2.1.0
'@noble/hashes':
specifier: ^1.3.3
version: 1.3.3
'@plasmohq/messaging':
specifier: ^0.6.1
version: 0.6.1(react@18.2.0)
'@react-spring/web':
specifier: ^9.7.3
version: 9.7.3(react-dom@18.2.0)(react@18.2.0)
@ -119,6 +123,9 @@ dependencies:
react-use:
specifier: ^17.4.2
version: 17.4.2(react-dom@18.2.0)(react@18.2.0)
semver:
specifier: ^7.5.4
version: 7.5.4
slugify:
specifier: ^1.6.6
version: 1.6.6
@ -139,6 +146,9 @@ devDependencies:
'@babel/preset-typescript':
specifier: ^7.23.3
version: 7.23.3(@babel/core@7.23.6)
'@rollup/wasm-node':
specifier: ^4.9.4
version: 4.9.4
'@types/chromecast-caf-sender':
specifier: ^1.0.8
version: 1.0.8
@ -184,6 +194,9 @@ devDependencies:
'@types/react-transition-group':
specifier: ^4.4.10
version: 4.4.10
'@types/semver':
specifier: ^7.5.6
version: 7.5.6
'@typescript-eslint/eslint-plugin':
specifier: ^6.15.0
version: 6.15.0(@typescript-eslint/parser@6.15.0)(eslint@8.56.0)(typescript@5.3.3)
@ -192,7 +205,7 @@ devDependencies:
version: 6.15.0(eslint@8.56.0)(typescript@5.3.3)
'@vitejs/plugin-react':
specifier: ^4.2.1
version: 4.2.1(vite@5.0.10)
version: 4.2.1(vite@5.0.12)
autoprefixer:
specifier: ^10.4.16
version: 10.4.16(postcss@8.4.32)
@ -252,7 +265,7 @@ devDependencies:
version: 0.5.9(prettier@3.1.1)
rollup-plugin-visualizer:
specifier: ^5.11.0
version: 5.11.0(rollup@2.79.1)
version: 5.11.0(@rollup/wasm-node@4.9.6)
tailwind-scrollbar:
specifier: ^3.0.5
version: 3.0.5(tailwindcss@3.4.0)
@ -269,20 +282,20 @@ devDependencies:
specifier: ^5.3.3
version: 5.3.3
vite:
specifier: ^5.0.10
version: 5.0.10(@types/node@20.10.5)
specifier: ^5.0.12
version: 5.0.12(@types/node@20.10.5)
vite-plugin-checker:
specifier: ^0.6.2
version: 0.6.2(eslint@8.56.0)(typescript@5.3.3)(vite@5.0.10)
version: 0.6.2(eslint@8.56.0)(typescript@5.3.3)(vite@5.0.12)
vite-plugin-package-version:
specifier: ^1.1.0
version: 1.1.0(vite@5.0.10)
version: 1.1.0(vite@5.0.12)
vite-plugin-pwa:
specifier: ^0.17.4
version: 0.17.4(vite@5.0.10)(workbox-build@7.0.0)(workbox-window@7.0.0)
version: 0.17.4(vite@5.0.12)(workbox-build@7.0.0)(workbox-window@7.0.0)
vite-plugin-static-copy:
specifier: ^1.0.0
version: 1.0.0(vite@5.0.10)
version: 1.0.0(vite@5.0.12)
vitest:
specifier: ^1.1.0
version: 1.1.0(@types/node@20.10.5)(jsdom@23.0.1)
@ -1917,8 +1930,8 @@ packages:
engines: {node: '>= 14'}
dev: false
/@movie-web/providers@2.0.5:
resolution: {integrity: sha512-cefPTFXE7ctYeiibjk4HcNL3anRZ3lgYDAaJdzFzUrvkcSdxonP8GgGfDfPwmWWKip9dbP8Xv5aeauV/wrfaag==}
/@movie-web/providers@2.1.0:
resolution: {integrity: sha512-L7Nn5n1+0HNXha0A6bymJSGVLhyC4qd5S2r5Xk5FeqxMlqKBqOlMpUmfHiZOssog70sxTAvRfFqmKkM4UXV8kg==}
dependencies:
cheerio: 1.0.0-rc.12
crypto-js: 4.2.0
@ -1976,6 +1989,18 @@ packages:
tslib: 2.6.2
dev: true
/@plasmohq/messaging@0.6.1(react@18.2.0):
resolution: {integrity: sha512-/nn1k8SG5z++o/NnZu+byHWcC9MhPLxfmvj+AP3buqMn7uwfYDcYWURLuMW2Knw08HBg+wku2v1Ltt4evN0nzA==}
peerDependencies:
react: ^16.8.6 || ^17 || ^18
peerDependenciesMeta:
react:
optional: true
dependencies:
nanoid: 5.0.3
react: 18.2.0
dev: false
/@react-spring/animated@9.7.3(react@18.2.0):
resolution: {integrity: sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==}
peerDependencies:
@ -2029,163 +2054,78 @@ packages:
engines: {node: '>=14.0.0'}
dev: false
/@rollup/plugin-babel@5.3.1(@babel/core@7.23.6)(rollup@2.79.1):
/@rollup/plugin-babel@5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.9.6):
resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
engines: {node: '>= 10.0.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@types/babel__core': ^7.1.9
rollup: ^1.20.0||^2.0.0
rollup: npm:@rollup/wasm-node
peerDependenciesMeta:
'@types/babel__core':
optional: true
dependencies:
'@babel/core': 7.23.6
'@babel/helper-module-imports': 7.22.15
'@rollup/pluginutils': 3.1.0(rollup@2.79.1)
rollup: 2.79.1
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.9.6)
rollup: /@rollup/wasm-node@4.9.6
dev: true
/@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1):
/@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.9.6):
resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
engines: {node: '>= 10.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0
rollup: npm:@rollup/wasm-node
dependencies:
'@rollup/pluginutils': 3.1.0(rollup@2.79.1)
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.9.6)
'@types/resolve': 1.17.1
builtin-modules: 3.3.0
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.4
rollup: 2.79.1
rollup: /@rollup/wasm-node@4.9.6
dev: true
/@rollup/plugin-replace@2.4.2(rollup@2.79.1):
/@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.9.6):
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
peerDependencies:
rollup: ^1.20.0 || ^2.0.0
rollup: npm:@rollup/wasm-node
dependencies:
'@rollup/pluginutils': 3.1.0(rollup@2.79.1)
'@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.9.6)
magic-string: 0.25.9
rollup: 2.79.1
rollup: /@rollup/wasm-node@4.9.6
dev: true
/@rollup/pluginutils@3.1.0(rollup@2.79.1):
/@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.9.6):
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
engines: {node: '>= 8.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0
rollup: npm:@rollup/wasm-node
dependencies:
'@types/estree': 0.0.39
estree-walker: 1.0.1
picomatch: 2.3.1
rollup: 2.79.1
rollup: /@rollup/wasm-node@4.9.6
dev: true
/@rollup/rollup-android-arm-eabi@4.9.1:
resolution: {integrity: sha512-6vMdBZqtq1dVQ4CWdhFwhKZL6E4L1dV6jUjuBvsavvNJSppzi6dLBbuV+3+IyUREaj9ZFvQefnQm28v4OCXlig==}
cpu: [arm]
os: [android]
requiresBuild: true
/@rollup/wasm-node@4.9.4:
resolution: {integrity: sha512-K9ZPYMCxP7sBElj5du0En/zpbhXTQxpWI7RlF+8bNpLUozhzg2Pcx2h3cBCzV7xtiUt0dc+pF2Ib3/Sg8R0JMA==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
dependencies:
'@types/estree': 1.0.5
optionalDependencies:
fsevents: 2.3.3
dev: true
optional: true
/@rollup/rollup-android-arm64@4.9.1:
resolution: {integrity: sha512-Jto9Fl3YQ9OLsTDWtLFPtaIMSL2kwGyGoVCmPC8Gxvym9TCZm4Sie+cVeblPO66YZsYH8MhBKDMGZ2NDxuk/XQ==}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-arm64@4.9.1:
resolution: {integrity: sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-x64@4.9.1:
resolution: {integrity: sha512-KyP/byeXu9V+etKO6Lw3E4tW4QdcnzDG/ake031mg42lob5tN+5qfr+lkcT/SGZaH2PdW4Z1NX9GHEkZ8xV7og==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.9.1:
resolution: {integrity: sha512-Yqz/Doumf3QTKplwGNrCHe/B2p9xqDghBZSlAY0/hU6ikuDVQuOUIpDP/YcmoT+447tsZTmirmjgG3znvSCR0Q==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-gnu@4.9.1:
resolution: {integrity: sha512-u3XkZVvxcvlAOlQJ3UsD1rFvLWqu4Ef/Ggl40WAVCuogf4S1nJPHh5RTgqYFpCOvuGJ7H5yGHabjFKEZGExk5Q==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-musl@4.9.1:
resolution: {integrity: sha512-0XSYN/rfWShW+i+qjZ0phc6vZ7UWI8XWNz4E/l+6edFt+FxoEghrJHjX1EY/kcUGCnZzYYRCl31SNdfOi450Aw==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-riscv64-gnu@4.9.1:
resolution: {integrity: sha512-LmYIO65oZVfFt9t6cpYkbC4d5lKHLYv5B4CSHRpnANq0VZUQXGcCPXHzbCXCz4RQnx7jvlYB1ISVNCE/omz5cw==}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-gnu@4.9.1:
resolution: {integrity: sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-musl@4.9.1:
resolution: {integrity: sha512-t4QSR7gN+OEZLG0MiCgPqMWZGwmeHhsM4AkegJ0Kiy6TnJ9vZ8dEIwHw1LcZKhbHxTY32hp9eVCMdR3/I8MGRw==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-arm64-msvc@4.9.1:
resolution: {integrity: sha512-7XI4ZCBN34cb+BH557FJPmh0kmNz2c25SCQeT9OiFWEgf8+dL6ZwJ8f9RnUIit+j01u07Yvrsuu1rZGxJCc51g==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-ia32-msvc@4.9.1:
resolution: {integrity: sha512-yE5c2j1lSWOH5jp+Q0qNL3Mdhr8WuqCNVjc6BxbVfS5cAS6zRmdiw7ktb8GNpDCEUJphILY6KACoFoRtKoqNQg==}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-x64-msvc@4.9.1:
resolution: {integrity: sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/wasm-node@4.9.6:
resolution: {integrity: sha512-B3FpAkroTE6q+MRHzv8XLBgPbxdjJiy5UnduZNQ/4lxeF1JT2O/OAr0JPpXeRG/7zpKm/kdqU/4m6AULhmnSqw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
dependencies:
'@types/estree': 1.0.5
optionalDependencies:
fsevents: 2.3.3
/@scure/base@1.1.5:
resolution: {integrity: sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==}
@ -2274,6 +2214,9 @@ packages:
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
dev: true
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
/@types/filesystem@0.0.32:
resolution: {integrity: sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==}
dependencies:
@ -2397,8 +2340,8 @@ packages:
/@types/scheduler@0.16.3:
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
/@types/semver@7.5.1:
resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==}
/@types/semver@7.5.6:
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==}
dev: true
/@types/trusted-types@2.0.3:
@ -2517,7 +2460,7 @@ packages:
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
'@types/json-schema': 7.0.12
'@types/semver': 7.5.1
'@types/semver': 7.5.6
'@typescript-eslint/scope-manager': 6.15.0
'@typescript-eslint/types': 6.15.0
'@typescript-eslint/typescript-estree': 6.15.0(typescript@5.3.3)
@ -2540,7 +2483,7 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
/@vitejs/plugin-react@4.2.1(vite@5.0.10):
/@vitejs/plugin-react@4.2.1(vite@5.0.12):
resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
@ -2551,7 +2494,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6)
'@types/babel__core': 7.20.5
react-refresh: 0.14.0
vite: 5.0.10(@types/node@20.10.5)
vite: 5.0.12(@types/node@20.10.5)
transitivePeerDependencies:
- supports-color
dev: true
@ -5096,7 +5039,6 @@ packages:
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
dev: true
/magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
@ -5142,7 +5084,7 @@ packages:
'@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.6)
'@babel/types': 7.23.6
kleur: 4.1.5
rollup: 3.29.4
rollup: /@rollup/wasm-node@4.9.6
unplugin: 1.5.1
transitivePeerDependencies:
- supports-color
@ -5244,6 +5186,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/nanoid@5.0.3:
resolution: {integrity: sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
/nanoid@5.0.4:
resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==}
engines: {node: ^18 || >=20}
@ -6064,73 +6012,36 @@ packages:
glob: 7.2.3
dev: true
/rollup-plugin-terser@7.0.2(rollup@2.79.1):
/rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.9.6):
resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser
peerDependencies:
rollup: ^2.0.0
rollup: npm:@rollup/wasm-node
dependencies:
'@babel/code-frame': 7.23.5
jest-worker: 26.6.2
rollup: 2.79.1
rollup: /@rollup/wasm-node@4.9.6
serialize-javascript: 4.0.0
terser: 5.19.3
dev: true
/rollup-plugin-visualizer@5.11.0(rollup@2.79.1):
/rollup-plugin-visualizer@5.11.0(@rollup/wasm-node@4.9.6):
resolution: {integrity: sha512-exM0Ms2SN3AgTzMeW7y46neZQcyLY7eKwWAop1ZoRTCZwyrIRdMMJ6JjToAJbML77X/9N8ZEpmXG4Z/Clb9k8g==}
engines: {node: '>=14'}
hasBin: true
peerDependencies:
rollup: 2.x || 3.x || 4.x
rollup: npm:@rollup/wasm-node
peerDependenciesMeta:
rollup:
optional: true
dependencies:
open: 8.4.2
picomatch: 2.3.1
rollup: 2.79.1
rollup: /@rollup/wasm-node@4.9.6
source-map: 0.7.4
yargs: 17.7.2
dev: true
/rollup@2.79.1:
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.3
dev: true
/rollup@3.29.4:
resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
fsevents: 2.3.3
dev: false
/rollup@4.9.1:
resolution: {integrity: sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.9.1
'@rollup/rollup-android-arm64': 4.9.1
'@rollup/rollup-darwin-arm64': 4.9.1
'@rollup/rollup-darwin-x64': 4.9.1
'@rollup/rollup-linux-arm-gnueabihf': 4.9.1
'@rollup/rollup-linux-arm64-gnu': 4.9.1
'@rollup/rollup-linux-arm64-musl': 4.9.1
'@rollup/rollup-linux-riscv64-gnu': 4.9.1
'@rollup/rollup-linux-x64-gnu': 4.9.1
'@rollup/rollup-linux-x64-musl': 4.9.1
'@rollup/rollup-win32-arm64-msvc': 4.9.1
'@rollup/rollup-win32-ia32-msvc': 4.9.1
'@rollup/rollup-win32-x64-msvc': 4.9.1
fsevents: 2.3.3
dev: true
/rrweb-cssom@0.6.0:
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
dev: true
@ -6240,7 +6151,6 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
/serialize-javascript@4.0.0:
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
@ -6974,7 +6884,7 @@ packages:
debug: 4.3.4
pathe: 1.1.1
picocolors: 1.0.0
vite: 5.0.10(@types/node@20.10.5)
vite: 5.0.12(@types/node@20.10.5)
transitivePeerDependencies:
- '@types/node'
- less
@ -6986,7 +6896,7 @@ packages:
- terser
dev: true
/vite-plugin-checker@0.6.2(eslint@8.56.0)(typescript@5.3.3)(vite@5.0.10):
/vite-plugin-checker@0.6.2(eslint@8.56.0)(typescript@5.3.3)(vite@5.0.12):
resolution: {integrity: sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==}
engines: {node: '>=14.16'}
peerDependencies:
@ -7032,22 +6942,22 @@ packages:
strip-ansi: 6.0.1
tiny-invariant: 1.3.1
typescript: 5.3.3
vite: 5.0.10(@types/node@20.10.5)
vite: 5.0.12(@types/node@20.10.5)
vscode-languageclient: 7.0.0
vscode-languageserver: 7.0.0
vscode-languageserver-textdocument: 1.0.8
vscode-uri: 3.0.7
dev: true
/vite-plugin-package-version@1.1.0(vite@5.0.10):
/vite-plugin-package-version@1.1.0(vite@5.0.12):
resolution: {integrity: sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==}
peerDependencies:
vite: '>=2.0.0-beta.69'
dependencies:
vite: 5.0.10(@types/node@20.10.5)
vite: 5.0.12(@types/node@20.10.5)
dev: true
/vite-plugin-pwa@0.17.4(vite@5.0.10)(workbox-build@7.0.0)(workbox-window@7.0.0):
/vite-plugin-pwa@0.17.4(vite@5.0.12)(workbox-build@7.0.0)(workbox-window@7.0.0):
resolution: {integrity: sha512-j9iiyinFOYyof4Zk3Q+DtmYyDVBDAi6PuMGNGq6uGI0pw7E+LNm9e+nQ2ep9obMP/kjdWwzilqUrlfVRj9OobA==}
engines: {node: '>=16.0.0'}
peerDependencies:
@ -7058,14 +6968,14 @@ packages:
debug: 4.3.4
fast-glob: 3.3.2
pretty-bytes: 6.1.1
vite: 5.0.10(@types/node@20.10.5)
vite: 5.0.12(@types/node@20.10.5)
workbox-build: 7.0.0
workbox-window: 7.0.0
transitivePeerDependencies:
- supports-color
dev: true
/vite-plugin-static-copy@1.0.0(vite@5.0.10):
/vite-plugin-static-copy@1.0.0(vite@5.0.12):
resolution: {integrity: sha512-kMlrB3WDtC5GzFedNIPkpjnOAr8M11PfWOiUaONrUZ3AqogTsOmIhTt6w7Fh311wl8pN81ld7sfuOEogFJ9N8A==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
@ -7075,11 +6985,11 @@ packages:
fast-glob: 3.3.1
fs-extra: 11.1.1
picocolors: 1.0.0
vite: 5.0.10(@types/node@20.10.5)
vite: 5.0.12(@types/node@20.10.5)
dev: true
/vite@5.0.10(@types/node@20.10.5):
resolution: {integrity: sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw==}
/vite@5.0.12(@types/node@20.10.5):
resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
peerDependencies:
@ -7109,7 +7019,7 @@ packages:
'@types/node': 20.10.5
esbuild: 0.19.10
postcss: 8.4.32
rollup: 4.9.1
rollup: /@rollup/wasm-node@4.9.6
optionalDependencies:
fsevents: 2.3.3
dev: true
@ -7159,7 +7069,7 @@ packages:
strip-literal: 1.3.0
tinybench: 2.5.1
tinypool: 0.8.1
vite: 5.0.10(@types/node@20.10.5)
vite: 5.0.12(@types/node@20.10.5)
vite-node: 1.1.0(@types/node@20.10.5)
why-is-node-running: 2.2.2
transitivePeerDependencies:
@ -7371,9 +7281,9 @@ packages:
'@babel/core': 7.23.6
'@babel/preset-env': 7.23.6(@babel/core@7.23.6)
'@babel/runtime': 7.23.6
'@rollup/plugin-babel': 5.3.1(@babel/core@7.23.6)(rollup@2.79.1)
'@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.1)
'@rollup/plugin-replace': 2.4.2(rollup@2.79.1)
'@rollup/plugin-babel': 5.3.1(@babel/core@7.23.6)(@rollup/wasm-node@4.9.6)
'@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.9.6)
'@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.9.6)
'@surma/rollup-plugin-off-main-thread': 2.2.3
ajv: 8.12.0
common-tags: 1.8.2
@ -7382,8 +7292,8 @@ packages:
glob: 7.2.3
lodash: 4.17.21
pretty-bytes: 5.6.0
rollup: 2.79.1
rollup-plugin-terser: 7.0.2(rollup@2.79.1)
rollup: /@rollup/wasm-node@4.9.6
rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.9.6)
source-map: 0.8.0-beta.0
stringify-object: 3.3.0
strip-comments: 2.0.1
@ -7550,7 +7460,6 @@ packages:
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true
/yaml@2.3.2:
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}

View File

@ -26,6 +26,7 @@ import pa from "@/assets/locales/pa.json";
import pirate from "@/assets/locales/pirate.json";
import pl from "@/assets/locales/pl.json";
import ptbr from "@/assets/locales/pt-BR.json";
import ptpt from "@/assets/locales/pt-PT.json";
import ro from "@/assets/locales/ro.json";
import ru from "@/assets/locales/ru.json";
import sl from "@/assets/locales/sl.json";
@ -64,6 +65,7 @@ export const locales = {
tok,
hi,
"pt-BR": ptbr,
"pt-PT": ptpt,
uk,
bg,
bn,

View File

@ -394,11 +394,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "لغة التطبيق",
"languageDescription": "اللغة المطبقة على كامل التطبيق.",
"title": "اللغة"
},
"reset": "إعادة تعيين",
"save": "حفظ",
"sidebar": {

View File

@ -181,14 +181,43 @@
"disclaimer": "ডাউনলোড সরাসরি প্রদানকারী থেকে নেওয়া হয়. কিভাবে ডাউনলোড দেওয়া হয় তার উপর মুভি-ওয়েবের নিয়ন্ত্রণ নেই।",
"downloadPlaylist": "প্লেলিস্ট ডাউনলোড করুন",
"downloadSubtitle": "বর্তমান সাবটাইটেল ডাউনলোড করুন",
"downloadVideo": "ভিডিও ডাউনলোড"
"downloadVideo": "ভিডিও ডাউনলোড",
"hlsDisclaimer": "ডাউনলোড সরাসরি প্রদানকারী থেকে নেওয়া হয়. কিভাবে ডাউনলোড দেওয়া হয় তার উপর মুভি-ওয়েবের নিয়ন্ত্রণ নেই। অনুগ্রহ করে মনে রাখবেন যে আপনি একটি HLS প্লেলিস্ট ডাউনলোড করছেন, এটি উন্নত মাল্টিমিডিয়া স্ট্রিমিংয়ের সাথে পরিচিত ব্যবহারকারীদের জন্য।",
"onAndroid": {
"1": "অ্যান্ড্রয়েডে ডাউনলোড করতে, ডাউনলোড বোতামে ক্লিক করুন তারপর, নতুন পৃষ্ঠায়, ভিডিওতে <bold>ট্যাপ করুন এবং ধরে রাখুন</bold>, তারপরে <bold>সংরক্ষণ করুন</bold> নির্বাচন করুন।",
"shortTitle": "ডাউনলোড/অ্যান্ড্রয়েড",
"title": "অ্যান্ড্রয়েডে ডাউনলোড হচ্ছে"
},
"onIos": {
"1": "iOS-এ ডাউনলোড করতে, ডাউনলোড বোতামে ক্লিক করুন তারপর, নতুন পৃষ্ঠায়, <bold><ios_share /></bold>-এ ক্লিক করুন, তারপর <bold>ফাইলে সংরক্ষণ করুন <ios_files /></bold>।",
"shortTitle": "ডাউনলোড / iOS",
"title": "iOS এ ডাউনলোড হচ্ছে"
},
"onPc": {
"1": "পিসিতে, ডাউনলোড বোতামে ক্লিক করুন তারপর, নতুন পৃষ্ঠায়, ভিডিওটিতে ডান ক্লিক করুন এবং <bold>ভিডিওটিকে এই হিসাবে সংরক্ষণ করুন</bold> নির্বাচন করুন",
"shortTitle": "ডাউনলোড/পিসি",
"title": "পিসিতে ডাউনলোড হচ্ছে"
},
"title": "ডাউনলোড করুন"
},
"episodes": {
"button": "পর্বগুলি",
"emptyState": "এই মরসুমে কোন পর্ব নেই, পরে আবার চেক করুন!",
"episodeBadge": "E{{episode}}",
"loadingError": "ঋতু লোড করার সময় ত্রুটি৷",
"loadingList": "লোড হচ্ছে..।",
"loadingTitle": "লোড হচ্ছে..।"
"loadingTitle": "লোড হচ্ছে..।",
"unairedEpisodes": "এই সিজনের এক বা একাধিক পর্ব অক্ষম করা হয়েছে কারণ সেগুলি এখনও সম্প্রচার করা হয়নি।"
},
"playback": {
"speedLabel": "প্লেব্যাক গতি",
"title": "প্লেব্যাক সেটিংস"
},
"quality": {
"automaticLabel": "স্বয়ংক্রিয় গুণমান",
"hint": "আপনি বিভিন্ন গুণমানের বিকল্প পেতে <0>উৎস পরিবর্তন</0> করে দেখতে পারেন।",
"iosNoQuality": "অ্যাপল-সংজ্ঞায়িত সীমাবদ্ধতার কারণে, এই উৎসের জন্য গুণমান নির্বাচন iOS-এ উপলব্ধ নয়। বিভিন্ন মানের বিকল্প পেতে আপনি <0>অন্য উৎসে স্যুইচ করার</0> চেষ্টা করতে পারেন।",
"title": "গুণমান"
},
"settings": {
"downloadItem": "ডাউনলোড করুন",
@ -228,6 +257,29 @@
"title": "সাবটাইটেল",
"unknownLanguage": "অজানা"
}
},
"metadata": {
"api": {
"text": "API মেটাডেটা লোড করা যায়নি, অনুগ্রহ করে আপনার ইন্টারনেট সংযোগ পরীক্ষা করুন।",
"title": "API মেটাডেটা লোড করতে ব্যর্থ হয়েছে"
},
"failed": {
"badge": "ব্যর্থ",
"homeButton": "বাড়িতে যেতে",
"text": "TMDB থেকে মিডিয়ার মেটাডেটা লোড করা যায়নি। আপনার ইন্টারনেট সংযোগে TMDB ডাউন বা ব্লক করা আছে কিনা তা অনুগ্রহ করে চেক করুন।",
"title": "মেটাডেটা লোড করতে ব্যর্থ হয়েছে"
},
"notFound": {
"badge": "পাওয়া যায়নি",
"homeButton": "বাড়িতে ফিরে যাও",
"text": "আপনার অনুরোধ করা মিডিয়া আমরা খুঁজে পাইনি। হয় এটি সরানো হয়েছে অথবা আপনি URL-এর সাথে হেরফের করেছেন।"
}
},
"turnstile": {
"description": "ডানদিকে ক্যাপচা সম্পূর্ণ করে আপনি যে মানুষ তা যাচাই করুন। সিনেমা-ওয়েবকে নিরাপদ রাখতেই এই!",
"error": "আপনার মানবতা যাচাই করতে ব্যর্থ হয়েছে. অনুগ্রহপূর্বক আবার চেষ্টা করুন।",
"title": "আপনি যে মানুষ তা আমাদের যাচাই করতে হবে।",
"verifyingHumanity": "আপনার মানবতা যাচাই করা হচ্ছে..।"
}
}
}

View File

@ -18,7 +18,7 @@
},
"actions": {
"copied": "S'ha copiat",
"copy": "Cipia"
"copy": "Copia"
},
"auth": {
"createAccount": "Encara no teniu un compte? <0>Creeu un compte.</0>",
@ -108,7 +108,7 @@
"sectionTitle": "Continueu mirant"
},
"mediaList": {
"stopEditing": "Atura l'edició"
"stopEditing": "Deixa d'editar"
},
"search": {
"allResults": "Això és tot el que tenim!",
@ -390,30 +390,25 @@
},
"connections": {
"server": {
"description": "Si voleu connectar-vos a un rerefons personalitzat per a emmagatzemar les vostres dades, activeu-ho i proporcioneu l'URL.",
"description": "Si voleu connectar-vos a un rerefons personalitzat per a emmagatzemar les vostres dades, activeu-ho i proporcioneu l'URL. <0>Instruccions.</0>",
"label": "Servidor personalitzat",
"urlLabel": "URL del servidor personalitzat"
},
"title": "Connexions",
"workers": {
"addButton": "Afig un «worker»",
"description": "Per fer funcionar l'aplicació, tot el trànsit s'encamina a través de servidors intermediaris. Activeu-ho si voleu portar els vostres propis «workers».",
"description": "Per fer funcionar l'aplicació, tot el trànsit s'encamina a través de servidors intermediaris. Activeu-ho si voleu portar els vostres propis «workers».<0>Instruccions.</0>",
"emptyState": "Encara no hi ha «workers», afegiu-ne un a continuació",
"label": "Utilitza «workers» intermediaris personalitzats",
"urlLabel": "URL dels «workers»",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Llengua de l'aplicació",
"languageDescription": "La llengua s'aplica a tota l'aplicació.",
"title": "Llengua"
},
"reset": "Restableix",
"save": "Desa",
"sidebar": {
"info": {
"appVersion": "Versió de l'aplicacií",
"appVersion": "Versió de l'aplicació",
"backendUrl": "URL del rerefons",
"backendVersion": "Versió del rerefons",
"hostname": "Nom de l'amfitrió",

View File

@ -95,6 +95,7 @@
"about": "O nás",
"dmca": "DMCA",
"login": "Přihlásit se",
"onboarding": "Nastavení",
"pagetitle": "{{title}} - movie-web",
"register": "Zaregistrovat se",
"settings": "Nastavení"
@ -165,6 +166,65 @@
"message": "Dívali jsme se všude: pod koši, ve skříni, za proxy, ale nakonec jsme nemohli najít stránku, kterou hledáte.",
"title": "Tuto stránku se nepodařilo najít"
},
"onboarding": {
"defaultConfirm": {
"cancel": "Zrušit",
"confirm": "Použít výchozí nastavení",
"description": "Výchozí nastavení nemá nejlepší streamy a může být strašně pomalá.",
"title": "Jste si jist?"
},
"extension": {
"back": "Zpět",
"explainer": "Pomocí rozšíření prohlížeče můžete získat nejlepší streamy, které nabízíme. S pouhou instalací.",
"extensionHelp": "Pokud jste rozšíření nainstalovali, ale nebylo zjištěno. <bold>Otevřete rozšíření pomocí nabídky rozšíření ve vašem prohlížeči</bold> a postupujte podle pokynů na obrazovce.",
"link": "Instalovat rozšíření",
"status": {
"disallowed": "Rozšíření není pro tuto stránku povoleno",
"disallowedAction": "Povolit rozšíření",
"failed": "Nezdařilo se získávání stavu",
"loading": "Čekání na instalaci rozšíření",
"outdated": "Verze rozšíření je příliš stará",
"success": "Rozšíření funguje podle očekávání!"
},
"submit": "Pokračovat",
"title": "Začněme s rozšířením"
},
"proxy": {
"back": "Zpět",
"explainer": "Pomocí metody proxy můžete získat streamy ve skvělé kvalitě vytvořením proxy serveru.",
"input": {
"errorConnection": "Nelze se připojit k proxy",
"errorInvalidUrl": "Adresa URL není platná",
"errorNotProxy": "Byla očekávaná proxy, ale byla předána webová stránka",
"label": "Proxy URL",
"placeholder": "https://"
},
"link": "Naučit se vytvořit proxy",
"submit": "Předložit proxy",
"title": "Pojďme uďelat novou proxy"
},
"start": {
"explainer": "Abyste získali co nejlepší streamy. Musíte si zvolit, kterou streamovací metodu chcete použít.",
"options": {
"default": {
"text": "Nechci zdroje s dobrou kvalitou,<0 /> <1>použít výchozí nastavení</1>"
},
"extension": {
"action": "Nainstalujte si rozšíření",
"description": "Nainstalujte si rozšíření prohlížeče a získejte přístup k nejlepším zdrojům.",
"quality": "Nejlepší kvalita",
"title": "Rozšíření prohlížeče"
},
"proxy": {
"action": "Nastavit proxy",
"description": "Nastavte si proxy během 5 minut, pro získání přístupu k skvělým zdrojům.",
"quality": "Dobrá kvalita",
"title": "Vlastní proxy"
}
},
"title": "Pojďme vám nastavit movie-web"
}
},
"overlays": {
"close": "Zavřít"
},
@ -182,7 +242,7 @@
"downloadPlaylist": "Stáhnout playlist",
"downloadSubtitle": "Stáhnout aktuální titulky",
"downloadVideo": "Stáhnout video",
"hlsDisclaimer": "Stahování probíhá přímo u poskytovatele. movie-web nemá kontrolu nad tím, jak jsou stahování poskytovány. Vezměte prosím na vědomí, že stahujete HLS playlist, který je určen pro uživatele obeznámené s pokročilým streamováním médií.",
"hlsDisclaimer": "Stahování probíhá přímo u poskytovatele. movie-web nemá kontrolu nad tím, jak jsou stahování poskytovány.<br /><br />Vezměte prosím na vědomí, že stahujete HLS playlist, který <bold>není doporučen stahovat pokud nejste obeznámeni s pokročilým streamováním médií</bold>. Raději skuste jiný zdroj pro jiný formát.",
"onAndroid": {
"1": "Na Androidu klikněte na tlačítko stahování, poté na nové stránce <bold>klepněte a podržte</bold> na videu a poté vyberte <bold>uložit</bold>.",
"shortTitle": "Stahování / Android",
@ -263,6 +323,17 @@
"text": "Metadata API nelze načíst, zkontrolujte prosím vaše připojení k internetu.",
"title": "Nepodařilo se načíst API metadata"
},
"dmca": {
"badge": "Odstraněno",
"text": "Toto média není dostupné, kvůli oznámení o zastavení šíření nebo nároku na autorská práva.",
"title": "Média byla odstraněna"
},
"extensionPermission": {
"badge": "Chybí povolení",
"button": "Použít rozšíření",
"text": "Máte rozšíření, ale k jeho použití potřebujeme vaše povolení.",
"title": "Konfigurace rozšíření"
},
"failed": {
"badge": "Neúspěšný",
"homeButton": "Jít domů",
@ -390,24 +461,49 @@
},
"connections": {
"server": {
"description": "Pokud se chcete připojit k vlastnímu backendu pr ukládání dat, povolte toto a zadejte URL adresu.",
"description": "Pokud se chcete připojit k vlastnímu backendu pro ukládání dat, povolte toto a zadejte URL adresu. <0>Instrukce.</0>",
"label": "Vlastní server",
"urlLabel": "URL adresa vlastního serveru"
},
"setup": {
"doSetup": "Proveďte nastavení",
"errorStatus": {
"description": "Vypadá to, že jedna nebo více položek v tomto nastavení potřebuje vaši pozornost.",
"title": "Něco potřebuje vaši pozornost"
},
"itemError": "S tímto nastavením je něco špatně. Projděte znovu nastavením abyste to opravili.",
"items": {
"default": "Výchozí nastavení",
"extension": "Rozšíření",
"proxy": "Vlastní proxy"
},
"redoSetup": "Proveďte znovu nastavení",
"successStatus": {
"description": "Všechny věci jsou připraveny, abyste mohli začít sledovat svá oblíbená média.",
"title": "Všechno je nastaveno!"
},
"unsetStatus": {
"description": "Prosím klikněte na vedlejší tlačítko abyste začali proces nastavování.",
"title": "Neprošli jste nastavením"
}
},
"title": "Spojení",
"workers": {
"addButton": "Přidat nového pracovníka",
"description": "Aby byla aplikace funkční, veškerá trafika prochází přes proxy. Povolte toto, pokud chcete používat svoje vlastní pracovníky.",
"description": "Aby byla aplikace funkční, veškerá trafika prochází přes proxy. Povolte toto, pokud chcete používat svoje vlastní pracovníky. <0>Instrukce.</0>",
"emptyState": "Zatím žádní pracovníci, přidej jednoho dolů",
"label": "Použít vlastní proxy pracovníky",
"urlLabel": "URL adresy pracovníků",
"urlPlaceholder": "https://"
}
},
"locale": {
"preferences": {
"language": "Jazyk aplikace",
"languageDescription": "Jazyk použitý na celou aplikaci.",
"title": "Lokální"
"languageDescription": "Jazyk aplikován na celou aplikaci.",
"thumbnail": "Generovat miniatury",
"thumbnailDescription": "Videa většinou nemají miniatury. Toto nastavení můžete povolit, ale mohou zpomalit vaše video.",
"thumbnailLabel": "Generovat miniatury",
"title": "Preference"
},
"reset": "Resetovat",
"save": "Uložit",

View File

@ -263,6 +263,11 @@
"text": "API Metadaten konnten nicht geladen werden, überprüfe deine Netzwerkverbindung.",
"title": "API Metadaten konnten nicht geladen werden"
},
"dmca": {
"badge": "Entfernt",
"text": "Das Video ist aufgrund einer Takedown-Anfrage oder eines Urheberrechtsanspruchs nicht mehr verfügbar.",
"title": "Video wurde entfernt"
},
"failed": {
"badge": "Fehlgeschlagen",
"homeButton": "Zurück zur Startseite",
@ -390,25 +395,30 @@
},
"connections": {
"server": {
"description": "Falls du dich mit einem anderen Server verbinden willst, um deine Daten zu speichern. Aktiviere dies und gebe die URL an.",
"description": "Falls du dich mit einem anderen Server verbinden willst, um deine Daten zu speichern. Aktiviere dies und gebe die URL an. <0>Anweisungen.</0>",
"label": "Eigener Server",
"urlLabel": "Eigene Server-URL"
},
"setup": {
"doSetup": "Einrichten",
"items": {
"extension": "Erweiterung"
},
"redoSetup": "Erneut einrichten",
"successStatus": {
"title": "Alles eingerichtet!"
}
},
"title": "Verbindung",
"workers": {
"addButton": "Neuen Worker hinzufügen",
"description": "Damit die App funktioniert werden alle Anfrage durch einen Proxy geleitet. Aktiviere dies, falls du deinen eigenen Worker verwenden willst.",
"description": "Damit die App funktioniert werden alle Anfrage durch einen Proxy geleitet. Aktiviere dies, falls du deinen eigenen Worker verwenden willst. <0>Anweisungen.</0>",
"emptyState": "Keine Worker vorhanden, füge einen unten hinzu",
"label": "Verwenden deinen eigenen Worker-Proxys",
"urlLabel": "Worker-URLs",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "App-Sprache",
"languageDescription": "Sprache für die ganze App.",
"title": "Sprache"
},
"reset": "Zurücksetzen",
"save": "Speichern",
"sidebar": {

View File

@ -404,11 +404,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Γλώσσα εφαρμογής",
"languageDescription": "Γλώσσα που εφαρμόζεται σε ολόκληρη την εφαρμογή.",
"title": "Τοποθεσία"
},
"reset": "Επαναφορά",
"save": "Αποθήκευση",
"sidebar": {

View File

@ -97,7 +97,8 @@
"login": "Login",
"pagetitle": "{{title}} - movie-web",
"register": "Register",
"settings": "Settings"
"settings": "Settings",
"onboarding": "Setup"
}
},
"home": {
@ -231,7 +232,7 @@
"downloadSubtitle": "Download current subtitle",
"downloadPlaylist": "Download playlist",
"downloadVideo": "Download video",
"hlsDisclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided. Please note that you are downloading an HLS playlist, this is intended for users familiar with advanced multimedia streaming.",
"hlsDisclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.<br /><br />Please note that you are downloading an HLS playlist, it is <bold>not recommended to download if you are not familiar with advanced streaming formats</bold>. Try different sources for different formats.",
"onAndroid": {
"1": "To download on Android, click the download button then, on the new page, <bold>tap and hold</bold> on the video, then select <bold>save</bold>.",
"shortTitle": "Download / Android",
@ -276,6 +277,17 @@
"homeButton": "Back to home",
"text": "We couldn't find the media you requested. Either it's been removed or you tampered with the URL.",
"title": "Couldn't find that media."
},
"extensionPermission": {
"badge": "Permission Missing",
"title": "Configure the extension",
"text": "You have the browser extension, but we need your permission to get started using the extension.",
"button": "Use extension"
},
"dmca": {
"badge": "Removed",
"title": "Media has been removed",
"text": "This media is no longer available due to a takedown notice or copyright claim."
}
},
"nextEpisode": {
@ -392,25 +404,50 @@
"colorLabel": "Color"
},
"connections": {
"setup": {
"errorStatus": {
"title": "Something needs your attention",
"description": "It seems that one or more items in this setup need your attention."
},
"unsetStatus": {
"title": "You haven't gone through setup",
"description": "Please click the button to the right to start the setup process."
},
"successStatus": {
"title": "Everything is set up!",
"description": "All things are in place for you to start watching your favourite media."
},
"redoSetup": "Redo setup",
"doSetup": "Do setup",
"itemError": "There is something wrong with this setting. Go through setup again to fix it.",
"items": {
"extension": "Extension",
"proxy": "Custom proxy",
"default": "Default setup"
}
},
"server": {
"description": "If you would like to connect to a custom backend to store your data, enable this and provide the URL.",
"description": "If you would like to connect to a custom backend to store your data, enable this and provide the URL. <0>Instructions.</0>",
"label": "Custom server",
"urlLabel": "Custom server URL"
},
"title": "Connections",
"workers": {
"addButton": "Add new worker",
"description": "To make the application function, all traffic is routed through proxies. Enable this if you want to bring your own workers.",
"description": "To make the application function, all traffic is routed through proxies. Enable this if you want to bring your own workers. <0>Instructions.</0>",
"emptyState": "No workers yet, add one below",
"label": "Use custom proxy workers",
"urlLabel": "Worker URLs",
"urlPlaceholder": "https://"
}
},
"locale": {
"preferences": {
"language": "Application language",
"languageDescription": "Language applied to the entire application.",
"title": "Locale"
"title": "Preferences",
"thumbnail": "Generate thumbnails",
"thumbnailDescription": "Most of the time, videos don't have thumbnails. You can enable this setting to generate them on the fly but they can make your video slower.",
"thumbnailLabel": "Generate thumbnails"
},
"reset": "Reset",
"save": "Save",
@ -429,5 +466,64 @@
}
},
"unsaved": "You have unsaved changes"
},
"onboarding": {
"start": {
"title": "Let's get you setup with movie-web",
"explainer": "To get the best streams possible. You will need to choose which streaming method you want to use.",
"options": {
"proxy": {
"quality": "Good quality",
"title": "Custom proxy",
"description": "Setup a proxy in just 5 minutes and gain access to great sources.",
"action": "Setup proxy"
},
"extension": {
"quality": "Best quality",
"title": "Browser extension",
"description": "Install browser extension and gain access to the best sources.",
"action": "Install extension"
},
"default": {
"text": "I don't want good quality streams,<0 /> <1>use the default setup</1>"
}
}
},
"proxy": {
"title": "Let's make a new proxy",
"explainer": "With the proxy method, you can get great quality streams by making a self-service proxy.",
"link": "Learn how to make a proxy",
"input": {
"label": "Proxy URL",
"placeholder": "https://",
"errorInvalidUrl": "Not a valid URL",
"errorConnection": "Could not connect to proxy",
"errorNotProxy": "Expected a proxy but got a website"
},
"back": "Go back",
"submit": "Submit proxy"
},
"extension": {
"title": "Let's start with an extension",
"explainer": "Using the browser extension, you can get the best streams we have to offer. With just a simple install.",
"extensionHelp": "If you've installed the extension but it's not detected. <bold>Open the extension through your browsers extension menu</bold> and follow the steps on screen.",
"link": "Install extension",
"back": "Go back",
"status": {
"loading": "Waiting for you to install the extension",
"disallowed": "Extension is not enabled for this page",
"disallowedAction": "Enable extension",
"failed": "Failed to request status",
"outdated": "Extension version too old",
"success": "Extension is working as expected!"
},
"submit": "Continue"
},
"defaultConfirm": {
"title": "Are you sure?",
"description": "The default setup does not have the best streams and can be unbearably slow.",
"cancel": "Cancel",
"confirm": "Use default setup"
}
}
}

View File

@ -390,25 +390,20 @@
},
"connections": {
"server": {
"description": "Si deseas conectarte a un backend personalizado para almacenar tus datos, habilita esto y proporciona la URL.",
"description": "Si deseas conectarte a un backend personalizado para almacenar tus datos, habilita esto y proporciona la URL. <0>Instrucciones.</0>",
"label": "Servidor personalizado",
"urlLabel": "URL del servidor personalizado"
},
"title": "Conexiones",
"workers": {
"addButton": "Agregar nuevo worker",
"description": "Para que la aplicación funcione, todo el tráfico se enruta a través de proxies. Habilita esto si quieres usar tus propios workers.",
"description": "Para que la aplicación funcione, todo el tráfico se enruta a través de proxies. Habilita esto si quieres usar tus propios workers. <0>Instrucciones.</0>",
"emptyState": "Aún no hay workers, agrega uno a continuación",
"label": "Usar proxy workers personalizados",
"urlLabel": "URL de los workers",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Idioma de la aplicación",
"languageDescription": "Idioma aplicado a toda la aplicación.",
"title": "Idioma"
},
"reset": "Restablecer",
"save": "Guardar",
"sidebar": {

View File

@ -404,11 +404,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Rakenduse keel",
"languageDescription": "Keel on rakendatud kogu rakendusele.",
"title": "Lokaal"
},
"reset": "Lähtesta",
"save": "Salvesta",
"sidebar": {

View File

@ -206,7 +206,8 @@
"episodeBadge": "قسمت {{episode}}",
"loadingError": "مشکلی در دریافت قسمت ها پیش آمده",
"loadingList": "در حال دریافت...",
"loadingTitle": "در حال دریافت..."
"loadingTitle": "در حال دریافت...",
"unairedEpisodes": "یک یا چند قسمت در این فصل غیرفعال شده است به دلیل اینکه هنوز پخش نشده است."
},
"playback": {
"speedLabel": "سرعت پخش",
@ -258,6 +259,10 @@
}
},
"metadata": {
"api": {
"text": "داده API بارگیری نشد، لطفا اتصال اینترنت خود را بررسی کنید.",
"title": "داده API بارگیری نشد"
},
"failed": {
"badge": "ناموفق بود",
"homeButton": "بازگشت به خانه",
@ -307,6 +312,12 @@
"remaining": "{{timeLeft}} مشاهده شده • {{timeFinished, datetime}} دیگر تمام می‌شود",
"shortRegular": "{{timeWatched}}",
"shortRemaining": "-{{timeLeft}}"
},
"turnstile": {
"description": "لطفا انسانیت خود را با تموم کردن چالش های کپچا به طور درست ثابت کنید. برای امن نگه داشتن فیلم وب!",
"error": "انسانیت شما تأیید نشد. لطفا دوباره تلاش کنید.",
"title": "ما باید برسی کنیم که شما انسان هستید.",
"verifyingHumanity": "تایید کردن انسانیت شما..."
}
},
"screens": {
@ -379,25 +390,20 @@
},
"connections": {
"server": {
"description": "اگر می خواهید برای ذخیره داده های خود به یک بک-اند سفارشی متصل شوید، این را فعال و لینک را وارد کنید.",
"description": "اگر میخواهید به یک بک-اند سفارشی برای ذخیره داده متصل شوید، با فعال و ارائه کردن این لینک ادامه دهید. <0>دستورالعمل ها.</0>",
"label": "سرور سفارشی",
"urlLabel": "لینک سرور سفارشی"
},
"title": "اتصالات",
"workers": {
"addButton": "اضافه کردن worker جدید",
"description": "برای کار کردن برنامه، تمام ترافیک از طریق پروکسی ها هدایت می شود. این کار را انجام دهید اگر می خواهید از worker های خود استفاده کنید.",
"description": "برای ایجاد عملکرد برنامه، تمام ترافیک از طریق پروکسی ها هدایت می شود. اگر میخواهید این کار انجام دهید حتما از worker های خودتان استفاده کنید. <0>دستورالعمل ها.</0>",
"emptyState": "هنوز هیچ worker ای وجود ندارد، یکی اضافه کنید",
"label": "استفاده از worker های پروکسی سفارشی",
"urlLabel": "لینک worker ها",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "زبان",
"languageDescription": "زبانی در کل برنامه اعمال می‌شود.",
"title": "زبان"
},
"reset": "بازنشانی",
"save": "ذخیره",
"sidebar": {

View File

@ -35,7 +35,7 @@
"description": "Veuillez fournir votre passphrase pour accéder à votre compte",
"deviceLengthError": "Veuillez saisir un nom d'appareil",
"passphraseLabel": "Passphrase de 12 mots",
"passphrasePlaceholder": "Passphrase",
"passphrasePlaceholder": "Phrase secrète",
"submit": "Se connecter",
"title": "Se connecter à votre compte",
"validationError": "Passphrase incorrecte ou incomplete"
@ -95,6 +95,7 @@
"about": "À propos",
"dmca": "DMCA",
"login": "Se connecter",
"onboarding": "Setup",
"pagetitle": "{{title}} - movie-web",
"register": "Créer un compte",
"settings": "Paramètres"
@ -165,6 +166,65 @@
"message": "Nous avons cherché partout : sous les poubelles, dans le placard, derrière le proxy, mais nous n'avons finalement pas trouvé la page que vous cherchez.",
"title": "Impossible de trouver cette page"
},
"onboarding": {
"defaultConfirm": {
"cancel": "Annuler",
"confirm": "Utiliser la configuration de départ",
"description": "La configuration par défaut n'offre pas les meilleurs flux et peut être insupportablement lente.",
"title": "Es-tu sûr?"
},
"extension": {
"back": "Retour en arrière",
"explainer": "En utilisant l'extension de navigateur, vous pouvez obtenir les meilleurs flux que nous avons à offrir. Avec juste une simple installation.",
"extensionHelp": "Si vous avez installé l'extension mais qu'elle n'est pas détectée. <bold>Ouvrez l'extension via le menu des extensions de votre navigateur</bold> et suivez les étapes à l'écran.",
"link": "Installer l'extension",
"status": {
"disallowed": "L'extension n'est pas activée pour cette page",
"disallowedAction": "Activer l'extension",
"failed": "Échec de la demande de statut",
"loading": "En attendant que vous installiez l'extension",
"outdated": "Version d'extension trop ancienne",
"success": "L'extension fonctionne comme prévu!"
},
"submit": "Continuer",
"title": "Commençons par une extension"
},
"proxy": {
"back": "Retour en arrière",
"explainer": "Avec la méthode du proxy, vous pouvez obtenir des flux de bonne qualité en créant un proxy en libre-service.",
"input": {
"errorConnection": "Impossible de se connecter au proxy",
"errorInvalidUrl": "URL non valide",
"errorNotProxy": "Je m'attendais à un proxy mais j'ai obtenu un site Web",
"label": "URL du proxy",
"placeholder": "https://"
},
"link": "Apprenez à créer un proxy",
"submit": "Soumettre le proxy",
"title": "Créons un nouveau proxy"
},
"start": {
"explainer": "Pour obtenir les meilleurs flux possibles. Vous devrez choisir la méthode de streaming que vous souhaitez utiliser.",
"options": {
"default": {
"text": "Je ne veux pas de flux de bonne qualité,<0 /> <1>use the default setup</1>"
},
"extension": {
"action": "Installer l'extension",
"description": "Installez l'extension de navigateur et accédez aux meilleures sources.",
"quality": "Meilleur qualité",
"title": "Extension du navigateur"
},
"proxy": {
"action": "Configurez le proxy",
"description": "Configurez un proxy en seulement 5 minutes et accédez à d'excellentes sources.",
"quality": "Bonne qualité",
"title": "Proxy personnalisé"
}
},
"title": "Commençons par vous configurer movie-web"
}
},
"overlays": {
"close": "Fermer"
},
@ -182,7 +242,7 @@
"downloadPlaylist": "Télécharger la liste de lecture",
"downloadSubtitle": "Télécharger les sous-titres",
"downloadVideo": "Télécharger la vidéo",
"hlsDisclaimer": "Les téléchargements sont pris directement de la source. Movie-Web n'exerce aucun contrôle sur les méthodes des fournisseurs de téléchargement. Veuillez noter que vous téléchargez une liste de lecture HLS, destinée aux utilisateurs habitués au streaming multimédia avancé.",
"hlsDisclaimer": "Les téléchargements sont effectués directement auprès du fournisseur. movie-web n'a aucun contrôle sur la façon dont les téléchargements sont fournis.<br /><br />Veuillez noter que vous téléchargez une liste de lecture HLS, il n'est <bold>pas recommandé de télécharger si vous n'êtes pas familier avec les formats de streaming avancés. </bold>. Essayez différentes sources pour différents formats.",
"onAndroid": {
"1": "Pour télécharger sur Android, cliquez sur le bouton de téléchargement puis, sur la nouvelle page, <bold>tapez et maintenez </bold> sur la vidéo, puis sélectionnez <bold>enregistrer</bold>.",
"shortTitle": "Télécharger / Android",
@ -263,6 +323,17 @@
"text": "Impossible de charger les métadonnées de l'API, veuillez vérifier votre connexion Internet.",
"title": "Échec du chargement des métadonnées de l'API"
},
"dmca": {
"badge": "Supprimé",
"text": "Ce média n'est plus disponible en raison d'un avis de retrait ou d'une réclamation pour atteinte aux droits d'auteur.",
"title": "Le média a été supprimé"
},
"extensionPermission": {
"badge": "Autorisation manquante",
"button": "Utiliser l'extension",
"text": "Vous disposez de l'extension de navigateur, mais nous avons besoin de votre autorisation pour commencer à utiliser l'extension.",
"title": "Configurer l'extension"
},
"failed": {
"badge": "Échec",
"homeButton": "Revenir à l'accueil",
@ -390,24 +461,49 @@
},
"connections": {
"server": {
"description": "Si vous souhaitez vous connecter à un backend personnalisé pour stocker vos données, activez cette option et indiquez l'URL.",
"description": "Si vous désirez utiliser un système de stockage externe pour enregistrer vos données, activez cette option et indiquez l'URL. <0>Instructions.</0>",
"label": "Serveur personnalisé",
"urlLabel": "URL du serveur personnalisé"
},
"setup": {
"doSetup": "Faire la configuration",
"errorStatus": {
"description": "Il semble qu'un ou plusieurs éléments de cette configuration nécessitent votre attention.",
"title": "Quelque chose nécessite votre attention"
},
"itemError": "Ce paramètre présente un problème. Résolvez le problème en redémarrant la configuration.",
"items": {
"default": "Configuration par défaut",
"extension": "Extension",
"proxy": "Proxy personnalisé"
},
"redoSetup": "Refaire la configuration",
"successStatus": {
"description": "Tout est réuni pour que vous puissiez commencer à regarder vos médias préférés.",
"title": "Tout est mis en place !"
},
"unsetStatus": {
"description": "Pour commencer le processus de configuration, veuillez cliquer sur le bouton à droite.",
"title": "Vous n'avez pas fait la configuration"
}
},
"title": "Connexions",
"workers": {
"addButton": "Ajouter un nouveau worker",
"description": "Pour que l'application fonctionne, tout le trafic est acheminé via des proxys. Activez cette option si vous souhaitez faire appel à vos propres workers.",
"description": "Pour que l'application fonctionne, tout le trafic est acheminé via des proxys. Activez cette option si vous souhaitez faire appel à vos propres workers. <0>Instructions.</0>",
"emptyState": "Pas encore de workers, ajoutez-en un ci-dessous",
"label": "Utiliser des agents proxy personnalisés",
"urlLabel": "URLs des workers",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Langue de l'application",
"languageDescription": "Langue appliquée dans l'ensemble de l'app.",
"title": "Local"
"preferences": {
"language": "Language de l'application",
"languageDescription": "Langue appliquée à lensemble de lapplication.",
"thumbnail": "Générer des miniatures",
"thumbnailDescription": "La plupart du temps, les vidéos n'ont pas de miniatures. Vous pouvez activer ce paramètre pour les générer à la volée, mais ils peuvent ralentir votre vidéo.",
"thumbnailLabel": "Générer des miniatures",
"title": "Préférences"
},
"reset": "Réinitialiser",
"save": "Sauvegarder",

View File

@ -390,25 +390,20 @@
},
"connections": {
"server": {
"description": "Se che gustaría conectar un servidor personalizado de backend para almacenar os teus datos, activa esto e indica a URL.",
"description": "Se che gustaría conectar un servidor personalizado de backend para almacenar os teus datos, activa esto e indica a URL. <0>Instruccións.</0>",
"label": "Servidor personalizado",
"urlLabel": "Servidor personalizado URL"
},
"title": "Conexións",
"workers": {
"addButton": "Añadir novo",
"description": "Para facer que a aplicación funcione, todo o tráfico é organizado en proxies. Activa esta opción se queres empregar os teus propios workers.",
"description": "Para facer que a aplicación funcione, todo o tráfico é organizado en proxies. Activa esta opción se queres empregar os teus propios workers. <0>Instruccións.</0>",
"emptyState": "Non hai workers aínda, engade un abaixo",
"label": "Usar proxy workers personalizados",
"urlLabel": "URLs dos workers",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Lingua da aplicación",
"languageDescription": "Lingua empregada en toda aplicación.",
"title": "Local"
},
"reset": "Reinicio",
"save": "Gardar",
"sidebar": {

View File

@ -404,11 +404,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "એપ્લિકેશન ભાષા",
"languageDescription": "સમગ્ર એપ્લિકેશન પર લાગુ ભાષા.",
"title": "સ્થળ"
},
"reset": "રીસેટ કરો",
"save": "સાચવો",
"sidebar": {

View File

@ -404,11 +404,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "שפת האפליקציה",
"languageDescription": "השפה החלה על האפליקציה כולה.",
"title": "מקומי"
},
"reset": "איפוס",
"save": "לשמור",
"sidebar": {

View File

@ -404,11 +404,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "अनुप्रयोग भाषा",
"languageDescription": "भाषा संपूर्ण अनुप्रयोग पर लागू होती है।",
"title": "स्थानीय"
},
"reset": "रीसेट",
"save": "सेव",
"sidebar": {

View File

@ -393,11 +393,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Bahasa aplikasi",
"languageDescription": "Bahasa yang akan digunakan di seluruh aplikasi.",
"title": "Bahasa"
},
"reset": "Reset",
"save": "Simpan",
"sidebar": {

View File

@ -120,19 +120,19 @@
},
"titles": {
"day": {
"default": "Cosa vorresti vedere questo pomeriggio?",
"default": "Cosa vorresti guardare questo pomeriggio?",
"extra": [
"Senti avventuroso? Jurassic Park potrebbe essere la scelta perfetta."
]
},
"morning": {
"default": "Cosa vorresti vedere questa mattina?",
"default": "Cosa vorresti guardare questa mattina?",
"extra": [
"Ho sentito che «Prima Dell'alba» è buono"
]
},
"night": {
"default": "Cosa vorresti vedere questa stasera?",
"default": "Cosa vorresti guardare questa sera?",
"extra": [
"Stanco? Ho sentito che L'esorciccio è buono."
]
@ -390,25 +390,20 @@
},
"connections": {
"server": {
"description": "Se si desideri connettersi a un backend personalizzato per memorizzare i dati, attivare questo e fornire l'URL.",
"description": "Se si desideri connettersi a un backend personalizzato per memorizzare i dati, attivare questo e fornire l'URL. <0>Istruzioni.</0>",
"label": "Server personalizzato",
"urlLabel": "URL del server personalizzato"
},
"title": "Connessioni",
"workers": {
"addButton": "Aggiungere un nuovo lavoratore",
"description": "Per far funzionare l'applicazione, tutto il traffico viene instradato attraverso i proxy. Abilitare questa opzione se si desidera portare i propri lavoratori.",
"description": "Per far funzionare l'applicazione, tutto il traffico viene instradato attraverso i proxy. Abilitare questa opzione se si desidera portare i propri lavoratori. <0>Istruzioni.</0>",
"emptyState": "Non ci sono ancora lavoratori, aggiungetene uno sotto",
"label": "Utilizzare proxy worker personalizzati",
"urlLabel": "URL dei lavoratori",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Lingua di applicazione",
"languageDescription": "Lingua applicata all'intera applicazione.",
"title": "Località"
},
"reset": "Reset",
"save": "Salva",
"sidebar": {

View File

@ -378,11 +378,6 @@
"urlLabel": "워커 URL"
}
},
"locale": {
"language": "애플리케이션 언어",
"languageDescription": "전체 애플리케이션에 적용되는 언어입니다.",
"title": "지"
},
"reset": "초기화",
"save": "저장",
"sidebar": {

View File

@ -372,11 +372,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Lietojumprogrammas valoda",
"languageDescription": "Visai lietojumprogrammai lietotā valoda.",
"title": "Lokalizācija"
},
"reset": "Restartēt",
"save": "Saglabāt",
"sidebar": {

View File

@ -404,11 +404,6 @@
"urlPlaceholder": "banana://"
}
},
"locale": {
"language": "Banana",
"languageDescription": "Banana applied to the entire banana.",
"title": "Banana"
},
"reset": "Banana",
"save": "Banana",
"sidebar": {

View File

@ -390,25 +390,20 @@
},
"connections": {
"server": {
"description": "यदि तपाईं आफ्नो डेटा भण्डारण गर्न अनुकूलन ब्याकइन्डमा जडान गर्न चाहनुहुन्छ भने, यसलाई सक्षम गर्नुहोस् र URL प्रदान गर्नुहोस्।",
"description": "यदि तपाईं आफ्नो डेटा भण्डारण गर्न अनुकूलन ब्याकइन्डमा जडान गर्न चाहनुहुन्छ भने, यसलाई सक्षम गर्नुहोस् र URL प्रदान गर्नुहोस्। <0>निर्देशनहरू।</0>",
"label": "अनुकूलन सर्भर",
"urlLabel": "अनुकूलन सर्भर URL"
},
"title": "संबन्धहरु",
"workers": {
"addButton": "नया worker हरु हाल्नुहोस",
"description": "एप्लिकेसन प्रकार्य बनाउनको लागि, सबै ट्राफिक प्रोक्सीहरू मार्फत रूट गरिएको छ। यदि तपाईं आफ्नो कामदारहरू ल्याउन चाहनुहुन्छ भने यसलाई सक्षम गर्नुहोस्।",
"description": "एप्लिकेसन प्रकार्य बनाउनको लागि, सबै ट्राफिक प्रोक्सीहरू मार्फत रूट गरिएको छ। यदि तपाईं आफ्नो कामदारहरू ल्याउन चाहनुहुन्छ भने यसलाई सक्षम गर्नुहोस्। <0>निर्देशनहरू।</0>",
"emptyState": "अहिलेसम्म worker हरु छैनन्, तल एउटा थप्नुहोस्",
"label": "आफ्नै proxy workers हरु चलाउनुहोस्",
"urlLabel": "Worker URL हरु",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "एपको भाषा",
"languageDescription": "सम्पूर्ण अनुप्रयोगमा भाषा लागू गरियो।",
"title": "भाषा"
},
"reset": "रिसेट गर्नुहोस्",
"save": "सेभ गर्नुहोस्",
"sidebar": {

View File

@ -390,25 +390,20 @@
},
"connections": {
"server": {
"description": "Als je verbinding wilt maken met een eigen backend om je gegevens op te slaan, schakel dan deze optie in en geef de URL op.",
"description": "Als je verbinding wilt maken met een eigen backend om je gegevens op te slaan, schakel dan deze optie in en geef de URL op. <0>Instructies.</0>",
"label": "Eigen server",
"urlLabel": "Eigen server URL"
},
"title": "Verbindingen",
"workers": {
"addButton": "Nieuwe worker toevoegen",
"description": "Om de applicatie te laten werken, wordt al het verkeer omgeleid via proxies. Schakel dit in als je je eigen workers wilt gebruiken.",
"description": "Om de applicatie te laten werken, wordt al het verkeer omgeleid via proxies. Schakel dit in als je je eigen workers wilt gebruiken. <0>Instructies.</0>",
"emptyState": "Nog geen workers, voeg er hieronder een toe",
"label": "Eigen proxy werker gebruiken",
"urlLabel": "Worker URLs",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Applicatietaal",
"languageDescription": "Taal wordt toegepast op de hele applicatie.",
"title": "Lokaal"
},
"reset": "Resetten",
"save": "Wijzigingen opslaan",
"sidebar": {

View File

@ -375,11 +375,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Application language",
"languageDescription": "Language applied to the entire application.",
"title": "Locale"
},
"reset": "Reset",
"save": "Save",
"sidebar": {

View File

@ -206,7 +206,8 @@
"episodeBadge": "E{{episode}}",
"loadingError": "Błąd podczas ładowania sezonu",
"loadingList": "Wczytywanie...",
"loadingTitle": "Wczytywanie..."
"loadingTitle": "Wczytywanie...",
"unairedEpisodes": "Jeden lub więcej odcinków tego sezonu zostało wyłączonych, ponieważ nie zostały jeszcze wyemitowane."
},
"playback": {
"speedLabel": "Szybkość odtwarzania",
@ -258,6 +259,10 @@
}
},
"metadata": {
"api": {
"text": "Nie można załadować metadanych API, sprawdź połączenie internetowe.",
"title": "Nie udało się załadować metadanych API"
},
"failed": {
"badge": "Nie powiodło się",
"homeButton": "Wróć na stronę główną",
@ -309,6 +314,7 @@
"shortRemaining": "-{{timeLeft}}"
},
"turnstile": {
"description": "Proszę potwierdź że jesteś człowiekiem, wypełniając Captcha po prawej stronie. Ma to zapewnić bezpieczeństwo movie-web!",
"error": "Nie udało się zweryfikować Twojego człowieczeństwa. Proszę spróbuj ponownie.",
"title": "Musimy sprawdzić, czy jesteś człowiekiem.",
"verifyingHumanity": "Sprawdzasz swoje człowieczeństwo..."
@ -398,11 +404,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Język aplikacji",
"languageDescription": "Język zastosowany do całej aplikacji.",
"title": "Ustawienia regionalne"
},
"reset": "Reset",
"save": "Zapisz",
"sidebar": {

View File

@ -122,7 +122,7 @@
"day": {
"default": "O que você gostaria de assistir esta tarde?",
"extra": [
"Me sentindo aventureiro? Jurassic Park pode ser a escolha perfeita."
"Se sentindo aventureiro? Jurassic Park pode ser a escolha perfeita."
]
},
"morning": {
@ -390,25 +390,20 @@
},
"connections": {
"server": {
"description": "Se você deseja se conectar a um backend personalizado para armazenar seus dados, ative isso e forneça a URL.",
"description": "Se você deseja se conectar a um backend personalizado para armazenar seus dados, ative isso e forneça a URL. <0>Instruções.</0>",
"label": "Servidor personalizado",
"urlLabel": "URL do servidor personalizado"
},
"title": "Conexões",
"workers": {
"addButton": "Adicionar novo worker",
"description": "Para fazer o aplicativo funcionar, todo o tráfego é roteado através de proxies. Ative isso se você quiser trazer seus próprios workers.",
"description": "Para fazer o aplicativo funcionar, todo o tráfego é roteado através de proxies. Ative isso se você quiser trazer seus próprios workers. <0>Instruções.</0>",
"emptyState": "Ainda não há workers, adicione um abaixo",
"label": "Usar proxy workers personalizados",
"urlLabel": "URLs dos workers",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Idioma do aplicativo",
"languageDescription": "Idioma aplicado a todo o aplicativo.",
"title": "Região"
},
"reset": "Redefinir",
"save": "Salvar",
"sidebar": {

View File

@ -0,0 +1,432 @@
{
"about": {
"description": "movie-web é uma aplicação web que pesquisa a internet por streams. A equipa visa uma abordagem maioritariamente minimalista na consumação de conteúdos.",
"faqTitle": "Perguntas frequentes",
"q1": {
"body": "movie-web não hospeda nenhum conteúdo. Quando clica para assistir a algo, a internet é pesquisada para o media selecionado (Na tela de carregamento e na aba 'fontes de vídeo', pode ver qual a fonte que está a ser utilizada). O media nunca é carregado pelo movie-web, tudo é feito através deste mecanismo de pesquisa.",
"title": "De onde vem o conteúdo?"
},
"q2": {
"body": "Não é possível solicitar um programa ou filme, o movie-web não gere nenhum conteúdo. Todo o conteúdo é visualizado através de fontes na internet.",
"title": "Onde posso solicitar um programa ou filme?"
},
"q3": {
"body": "Os nossos resultados de pesquisa são alimentados pela The Movie Database (TMDB) e são exibidos independentemente de as nossas fontes realmente terem o conteúdo.",
"title": "Os resultados da pesquisa mostram o programa ou filme, por que não consigo reproduzi-lo?"
},
"title": "Sobre o movie-web"
},
"actions": {
"copied": "Copiado",
"copy": "Copiar"
},
"auth": {
"createAccount": "Ainda não tem uma conta? <0>Crie uma conta.</0>",
"deviceNameLabel": "Nome do dispositivo",
"deviceNamePlaceholder": "Telemóvel pessoal",
"generate": {
"description": "A sua frase-passe age como o seu nome de utilizador e senha. Certifique-se de a manter segura, pois precisará dela para entrar na sua conta",
"next": "Guardei a minha frase-passe",
"passphraseFrameLabel": "Frase-passe",
"title": "A sua frase-passe"
},
"hasAccount": "Já tem uma conta? <0>Entre aqui.</0>",
"login": {
"description": "Por favor, introduza a sua frase-passe para entrar na sua conta",
"deviceLengthError": "Por favor, introduza um nome de dispositivo",
"passphraseLabel": "Frase-passe de 12 palavras",
"passphrasePlaceholder": "Frase-passe",
"submit": "Entrar",
"title": "Entrar na sua conta",
"validationError": "Frase-passe incorreta ou incompleta"
},
"register": {
"information": {
"color1": "Cor de perfil um",
"color2": "Cor de perfil dois",
"header": "Introduza um nome para o seu dispositivo e escolha cores e um ícone de utilizador da sua escolha",
"icon": "Ícone de utilizador",
"next": "Próximo",
"title": "Informações da conta"
}
},
"trust": {
"failed": {
"text": "Configurou corretamente?",
"title": "Falha ao conectar-se ao servidor"
},
"host": "Está a conectar-se a <0>{{hostname}}</0> - confirme se confia antes de criar uma conta",
"no": "Voltar",
"title": "Confia neste servidor?",
"yes": "Confio neste servidor"
},
"verify": {
"description": "Por favor, introduza a sua frase-passe anterior para confirmar que a guardou e para criar a sua conta",
"invalidData": "Dados inválidos",
"noMatch": "A frase-passe não coincide",
"passphraseLabel": "A sua frase-passe de 12 palavras",
"recaptchaFailed": "Falha na validação do ReCaptcha",
"register": "Criar conta",
"title": "Confirmar a sua frase-passe"
}
},
"errors": {
"badge": "Houve um erro",
"details": "Detalhes do erro",
"reloadPage": "Recarregar a página",
"showError": "Mostrar detalhes do erro",
"title": "Encontrámos um erro!"
},
"footer": {
"legal": {
"disclaimer": "Aviso legal",
"disclaimerText": "movie-web não hospeda quaisquer ficheiros, apenas faz ligações para serviços de terceiros. Problemas legais devem ser tratados com os anfitriões e fornecedores de ficheiros. O movie-web não é responsável por quaisquer ficheiros multimédia mostrados pelos fornecedores de vídeo."
},
"links": {
"discord": "Discord",
"dmca": "DMCA",
"github": "GitHub"
},
"tagline": "Assista aos seus programas e filmes favoritos com esta aplicação de streaming de código aberto."
},
"global": {
"name": "movie-web",
"pages": {
"about": "Sobre",
"dmca": "DMCA",
"login": "Entrar",
"pagetitle": "{{title}} - movie-web",
"register": "Registrar",
"settings": "Configurações"
}
},
"home": {
"bookmarks": {
"sectionTitle": "Marcadores"
},
"continueWatching": {
"sectionTitle": "Continuar a assistir"
},
"mediaList": {
"stopEditing": "Parar de editar"
},
"search": {
"allResults": "É tudo o que temos!",
"failed": "Falha ao encontrar mídia, tente novamente!",
"loading": "A carregar...",
"noResults": "Não conseguimos encontrar nada!",
"placeholder": "O que deseja assistir?",
"sectionTitle": "Resultados da pesquisa"
},
"titles": {
"day": {
"default": "O que gostaria de assistir esta tarde?",
"extra": [
"Sentindo-se aventureiro? Jurassic Park pode ser a escolha perfeita."
]
},
"morning": {
"default": "O que gostaria de assistir esta manhã?",
"extra": [
"Dizem que Antes do Amanhecer é bom"
]
},
"night": {
"default": "O que gostaria de assistir esta noite?",
"extra": [
"Cansado? Dizem que O Exorcista é bom."
]
}
}
},
"media": {
"episodeDisplay": "T{{season}} E{{episode}}",
"types": {
"movie": "Filme",
"show": "Série"
}
},
"navigation": {
"banner": {
"offline": "Verifique a sua conexão à internet"
},
"menu": {
"about": "Sobre nós",
"donation": "Doar",
"logout": "Sair",
"register": "Sincronizar com a nuvem",
"settings": "Configurações",
"support": "Suporte"
}
},
"notFound": {
"badge": "Não encontrado",
"goHome": "Voltar para casa",
"message": "Procurámos em todo lugar: embaixo dos caixotes, no armário, atrás do proxy, mas, no final, não conseguimos encontrar a página que procura.",
"title": "Não foi possível encontrar essa página"
},
"overlays": {
"close": "Fechar"
},
"player": {
"back": {
"default": "Voltar para casa",
"short": "Voltar"
},
"casting": {
"enabled": "Transmitindo para o dispositivo..."
},
"menus": {
"downloads": {
"disclaimer": "Os downloads são feitos diretamente pelo fornecedor. O movie-web não tem controle sobre como os downloads são fornecidos.",
"downloadPlaylist": "Baixar lista de reprodução",
"downloadSubtitle": "Baixar legenda atual",
"downloadVideo": "Baixar vídeo",
"hlsDisclaimer": "Os downloads são feitos diretamente pelo fornecedor. O movie-web não tem controle sobre como os downloads são fornecidos. Por favor, note que está a baixar uma lista de reprodução HLS, isso é destinado a utilizadores familiarizados com streaming multimídia avançado.",
"onAndroid": {
"1": "Para baixar no Android, clique no botão de download e, na nova página, <bold>toque e segure</bold> no vídeo, depois selecione <bold>guardar</bold>.",
"shortTitle": "Baixar / Android",
"title": "Baixando no Android"
},
"onIos": {
"1": "Para baixar no iOS, clique no botão de download e, na nova página, clique em <bold><ios_share /></bold>, depois em <bold>Guardar no Ficheiro <ios_files /></bold>.",
"shortTitle": "Baixar / iOS",
"title": "Baixando no iOS"
},
"onPc": {
"1": "No PC, clique no botão de download e, na nova página, clique com o botão direito no vídeo e selecione <bold>Guardar vídeo como</bold>",
"shortTitle": "Baixar / PC",
"title": "Baixando no PC"
},
"title": "Baixar"
},
"episodes": {
"button": "Episódios",
"emptyState": "Não há episódios nesta temporada, volte mais tarde!",
"episodeBadge": "E{{episode}}",
"loadingError": "Erro ao carregar a temporada",
"loadingList": "A carregar...",
"loadingTitle": "A carregar...",
"unairedEpisodes": "Um ou mais episódios nesta temporada foram desativados porque ainda não foram transmitidos."
},
"playback": {
"speedLabel": "Velocidade de reprodução",
"title": "Configurações de reprodução"
},
"quality": {
"automaticLabel": "Qualidade automática",
"hint": "Pode tentar <0>mudar de fonte</0> para obter opções de qualidade diferentes.",
"iosNoQuality": "Devido a limitações definidas pela Apple, a seleção de qualidade não está disponível no iOS para esta fonte. Pode tentar <0>mudar para outra fonte</0> para obter opções de qualidade diferentes.",
"title": "Qualidade"
},
"settings": {
"downloadItem": "Download",
"enableSubtitles": "Ativar legendas",
"experienceSection": "Experiência de visualização",
"playbackItem": "Configurações de reprodução",
"qualityItem": "Qualidade",
"sourceItem": "Fontes de vídeo",
"subtitleItem": "Configurações de legendas",
"videoSection": "Configurações de vídeo"
},
"sources": {
"failed": {
"text": "Houve um erro ao tentar encontrar vídeos, por favor, tente uma fonte diferente.",
"title": "Falha ao obter"
},
"noEmbeds": {
"text": "Não conseguimos encontrar nenhum embed, por favor, tente uma fonte diferente.",
"title": "Nenhum embed encontrado"
},
"noStream": {
"text": "Esta fonte não tem transmissões para este filme ou série.",
"title": "Sem transmissão"
},
"title": "Fontes",
"unknownOption": "Desconhecido"
},
"subtitles": {
"customChoice": "Selecionar legenda do arquivo",
"customizeLabel": "Personalizar",
"offChoice": "Desativar",
"settings": {
"backlink": "Legendas personalizadas",
"delay": "Atraso das legendas",
"fixCapitals": "Corrigir maiúsculas"
},
"title": "Legendas",
"unknownLanguage": "Desconhecido"
}
},
"metadata": {
"api": {
"text": "Não foi possível carregar os metadados da API, por favor verifique a sua conexão à internet.",
"title": "Falha ao carregar os metadados da API"
},
"failed": {
"badge": "Falhou",
"homeButton": "Ir para casa",
"text": "Não foi possível carregar os metadados do media da TMDB. Por favor, verifique se a TMDB está indisponível ou bloqueada na sua conexão à internet.",
"title": "Falha ao carregar os metadados"
},
"notFound": {
"badge": "Não encontrado",
"homeButton": "Voltar para casa",
"text": "Não conseguimos encontrar o conteúdo que solicitou. Ou foi removido ou houve manipulação na URL.",
"title": "Não foi possível encontrar esse conteúdo."
}
},
"nextEpisode": {
"cancel": "Cancelar",
"next": "Próximo episódio"
},
"playbackError": {
"badge": "Erro de reprodução",
"errors": {
"errorAborted": "A recuperação do conteúdo foi cancelada a pedido do utilizador.",
"errorDecode": "Apesar de ter sido anteriormente considerado utilizável, ocorreu um erro ao tentar decodificar o recurso multimédia, resultando em um erro.",
"errorGenericMedia": "Ocorreu um erro desconhecido no multimédia.",
"errorNetwork": "Ocorreu algum tipo de erro de rede que impediu a recuperação bem-sucedida do multimédia, apesar de estar disponível anteriormente.",
"errorNotSupported": "O objeto multimédia ou do fornecedor de multimédia não é suportado."
},
"homeButton": "Ir para casa",
"text": "Ocorreu um erro ao tentar reproduzir o conteúdo multimédia. Por favor, tente novamente.",
"title": "Falha ao reproduzir o vídeo!"
},
"scraping": {
"items": {
"failure": "Ocorreu um erro",
"notFound": "Não possui o vídeo",
"pending": "A verificar vídeos..."
},
"notFound": {
"badge": "Não encontrado",
"detailsButton": "Mostrar detalhes",
"homeButton": "Ir para casa",
"text": "Pesquisámos pelos nossos fornecedores e não conseguimos encontrar o conteúdo que procura! Não alojamos o conteúdo multimédia e não temos controlo sobre o que está disponível. Por favor, clique em 'Mostrar detalhes' abaixo para mais informações.",
"title": "Não conseguimos encontrar isso"
}
},
"time": {
"regular": "{{timeWatched}} / {{duration}}",
"remaining": "{{timeLeft}} restantes • Termina às {{timeFinished, datetime}}",
"shortRegular": "{{timeWatched}}",
"shortRemaining": "-{{timeLeft}}"
},
"turnstile": {
"description": "Por favor, verifique que é humano completando o Captcha à direita. Isso é para manter o movie-web seguro!",
"error": "Falha ao verificar a sua humanidade. Por favor, tente novamente.",
"title": "Precisamos verificar que você é humano.",
"verifyingHumanity": "Verificando a sua humanidade..."
}
},
"screens": {
"dmca": {
"text": "Bem-vindo à página de contacto DMCA da movie-web! Respeitamos os direitos de propriedade intelectual e queremos resolver rapidamente quaisquer preocupações de direitos autorais. Se acredita que a sua obra protegida por direitos autorais foi usada indevidamente na nossa plataforma, envie um aviso DMCA detalhado para o email abaixo. Inclua uma descrição do material protegido por direitos autorais, os seus detalhes de contacto e uma declaração de boa fé. Comprometemo-nos a resolver essas questões prontamente e agradecemos a sua cooperação para manter a movie-web como um lugar que respeita a criatividade e os direitos autorais.",
"title": "DMCA"
},
"loadingApp": "A carregar a aplicação",
"loadingUser": "A carregar o seu perfil",
"loadingUserError": {
"logout": "Terminar sessão",
"reset": "Repor servidor personalizado",
"text": "Falha ao carregar o seu perfil",
"textWithReset": "Falha ao carregar o seu perfil do servidor personalizado. Deseja repor para o servidor padrão?"
},
"migration": {
"failed": "Falha ao migrar os seus dados.",
"inProgress": "Por favor aguarde, estamos a migrar os seus dados. Isto não deverá demorar muito."
}
},
"settings": {
"account": {
"accountDetails": {
"deviceNameLabel": "Nome do dispositivo",
"deviceNamePlaceholder": "Telemóvel pessoal",
"editProfile": "Editar",
"logoutButton": "Terminar sessão"
},
"actions": {
"delete": {
"button": "Eliminar conta",
"confirmButton": "Eliminar conta",
"confirmDescription": "Tem a certeza de que deseja eliminar a sua conta? Todos os seus dados serão perdidos!",
"confirmTitle": "Tem a certeza?",
"text": "Esta ação é irreversível. Todos os dados serão eliminados e nada poderá ser recuperado.",
"title": "Eliminar conta"
},
"title": "Ações"
},
"devices": {
"deviceNameLabel": "Nome do dispositivo",
"failed": "Falha ao carregar as sessões",
"removeDevice": "Remover",
"title": "Dispositivos"
},
"profile": {
"finish": "Terminar edição",
"firstColor": "Cor do perfil um",
"secondColor": "Cor do perfil dois",
"title": "Editar imagem de perfil",
"userIcon": "Ícone de utilizador"
},
"register": {
"cta": "Começar",
"text": "Partilhe o seu progresso de visualização entre dispositivos e mantenha-os sincronizados.",
"title": "Sincronizar com a nuvem"
},
"title": "Conta"
},
"appearance": {
"activeTheme": "Ativo",
"themes": {
"blue": "Azul",
"default": "Padrão",
"gray": "Cinzento",
"red": "Vermelho",
"teal": "Verde-azulado"
},
"title": "Aparência"
},
"connections": {
"server": {
"description": "Se desejar ligar a um servidor personalizado para armazenar os seus dados, ative isto e forneça o URL.",
"label": "Servidor personalizado",
"urlLabel": "URL do servidor personalizado"
},
"title": "Conexões",
"workers": {
"addButton": "Adicionar novo trabalhador",
"description": "Para que a aplicação funcione, todo o tráfego é encaminhado através de proxies. Ative isto se quiser utilizar os seus próprios trabalhadores.",
"emptyState": "Ainda não há trabalhadores, adicione um abaixo",
"label": "Utilizar trabalhadores de proxy personalizados",
"urlLabel": "URLs do trabalhador",
"urlPlaceholder": "https://"
}
},
"reset": "Repor",
"save": "Guardar",
"sidebar": {
"info": {
"appVersion": "Versão da aplicação",
"backendUrl": "URL do backend",
"backendVersion": "Versão do backend",
"hostname": "Hostname",
"insecure": "Inseguro",
"notLoggedIn": "Não está autenticado",
"secure": "Seguro",
"title": "Informações da aplicação",
"unknownVersion": "Desconhecida",
"userId": "ID de utilizador"
}
},
"subtitles": {
"backgroundLabel": "Opacidade do fundo",
"colorLabel": "Cor",
"previewQuote": "Não devo temer. O medo é o assassino da mente.",
"textSizeLabel": "Tamanho do texto",
"title": "Legendas"
},
"unsaved": "Tem alterações não guardadas"
}
}

View File

@ -404,11 +404,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Limba aplicației",
"languageDescription": "Limbajul aplicat întregii aplicații.",
"title": "Local"
},
"reset": "Resetare",
"save": "Salvează",
"sidebar": {

View File

@ -64,13 +64,17 @@
"description": "Пожалуйста, введите фразу, полученную ранее, чтобы подтвердить, что вы ее сохранили, и создать свой аккаунт",
"invalidData": "Дата инвалидная",
"noMatch": "Парольная фраза не совпадает",
"register": "Создать учётную запись"
"passphraseLabel": "Ваша 12-словная парольная фраза",
"recaptchaFailed": "Проверка ReCaptcha не удалась",
"register": "Создать учётную запись",
"title": "Подтвердите парольную фразу"
}
},
"errors": {
"details": "Подробности ошибки",
"reloadPage": "Перезагрузить страницу",
"showError": "Показать сведения об ошибке"
"showError": "Показать сведения об ошибке",
"title": "Мы столкнулись с ошибкой!"
},
"footer": {
"legal": {
@ -128,6 +132,9 @@
"support": "Поддержка"
}
},
"notFound": {
"title": "Не удалось найти эту страницу"
},
"overlays": {
"close": "Закрыть"
},
@ -164,7 +171,24 @@
"subtitleItem": "Настройки субтитров",
"videoSection": "Настройки видео"
},
"sources": {
"failed": {
"text": "При попытке найти видео произошла ошибка, пожалуйста, попробуйте использовать другой источник."
},
"noEmbeds": {
"text": "Мы не смогли найти ни одной вставки, пожалуйста, попробуйте использовать другой источник.",
"title": "Не найдено ни одной вставки"
},
"noStream": {
"text": "В этом источнике нет потоков для этого фильма или сериала.",
"title": "Нет потока"
},
"title": "Источники",
"unknownOption": "Неизвестный"
},
"subtitles": {
"customChoice": "Выбрать субтитры из файла",
"offChoice": "Выключить",
"settings": {
"backlink": "Пользовательские субтитры"
},
@ -182,6 +206,20 @@
},
"text": "При попытке воспроизвести медиа файл произошла ошибка. Пожалуйста, попробуйте ещё раз.",
"title": "Не удалось воспроизвести видео!"
},
"scraping": {
"notFound": {
"detailsButton": "Показать детали",
"title": "Мы не смогли найти"
}
},
"time": {
"regular": "{{timeWatched}} / {{duration}}"
}
},
"screens": {
"dmca": {
"title": "DMCA"
}
},
"settings": {
@ -216,6 +254,8 @@
"userIcon": "Значок пользователя"
},
"register": {
"cta": "Начать",
"text": "Обменивайтесь информацией о прогрессе часов между устройствами и синхронизируйте их.",
"title": "Синхронизировать с облаком"
},
"title": "Аккаунт"
@ -241,26 +281,25 @@
"workers": {
"addButton": "Добавить новый прокси-сервер",
"description": "Для работы приложения весь трафик маршрутизируется через прокси. Включите это, если вы хотите использовать свои собственных прокси-серверы.",
"emptyState": "Прокси ещё нет, добавьте их ниже",
"emptyState": "Прокси отсутствуют, добавьте их ниже",
"label": "Использовать прокси-сервера",
"urlLabel": "URL-адреса",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Язык приложения",
"languageDescription": "Язык применяется ко всему приложению.",
"title": "Локализация"
},
"reset": "Сброс",
"save": "Сохранить",
"sidebar": {
"info": {
"appVersion": "Версия приложения",
"backendUrl": "Внутренний URL-адрес",
"backendVersion": "Серверная версия",
"hostname": "Имя хоста",
"insecure": "Небезопасно",
"notLoggedIn": "Вы не авторизованы",
"secure": "Безопасный",
"title": "Информация о приложении",
"unknownVersion": "Неизвестный",
"userId": "ID пользователя"
}
},

View File

@ -393,11 +393,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Jezik aplikacije",
"languageDescription": "Jezik, ki se uporablja za celotno aplikacijo.",
"title": "Jezik"
},
"reset": "Ponastavi",
"save": "Shrani",
"sidebar": {

View File

@ -372,11 +372,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Språk för applikationen",
"languageDescription": "Språket som används i hela applikationen.",
"title": "Plats"
},
"reset": "Återställ",
"save": "Spara",
"sidebar": {

View File

@ -389,11 +389,6 @@
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "ภาษา",
"languageDescription": "ภาษาที่ใช้กับแอปพลิเคชันทั้งหมด",
"title": "ตำแหน่งที่ตั้ง"
},
"reset": "เริ่มใหม่",
"save": "บันทึก",
"sidebar": {

View File

@ -309,6 +309,9 @@
}
},
"screens": {
"dmca": {
"title": "DMCA"
},
"loadingApp": "mi alasa e ilo",
"loadingUser": "mi alasa e lipu sina",
"loadingUserError": {
@ -316,6 +319,9 @@
"reset": "o sin e lawa ilo sina",
"text": "alasa li pakala",
"textWithReset": "alasa tan lawa ilo sina li pakala. sina wile e lawa ilo mi anu seme?"
},
"migration": {
"inProgress": "o awen lili. mi alasa tawa e sona sina"
}
},
"settings": {
@ -369,13 +375,13 @@
"title": "kule"
},
"connections": {
"server": {
"description": "sina wile e poki sona ante la o pana e nimi ona lon ni",
"label": "lawa ante",
"urlLabel": "nimi pi lawa ante"
},
"title": "kulupu"
},
"locale": {
"language": "toki ilo",
"languageDescription": "ilo li toki kepeken toki ni:",
"title": "toki"
},
"reset": "o weka e ante",
"save": "o ante",
"subtitles": {

View File

@ -74,7 +74,7 @@
"badge": "Bir şeyler ters gitti",
"details": "Hata detayları",
"reloadPage": "Sayfayı yenile",
"showError": "Hata detaylarını göster",
"showError": "Hata ayrıntılarını göster",
"title": "Bir hatayla karşılaştık!"
},
"footer": {
@ -206,7 +206,8 @@
"episodeBadge": "B{{episode}}",
"loadingError": "Sezon yüklenirken hata oluştu",
"loadingList": "Yükleniyor...",
"loadingTitle": "Yükleniyor..."
"loadingTitle": "Yükleniyor...",
"unairedEpisodes": "Bu sezondaki bir veya daha fazla bölüm henüz yayınlanmadığı için devre dışı bırakılmıştır."
},
"playback": {
"speedLabel": "Oynatma hızı",
@ -246,7 +247,7 @@
},
"subtitles": {
"customChoice": "Altyazı dosyası yükle",
"customizeLabel": "Kişiselleştir",
"customizeLabel": "Seçenekler",
"offChoice": "Kapat",
"settings": {
"backlink": "Kişisel altyazılar",
@ -258,6 +259,10 @@
}
},
"metadata": {
"api": {
"text": "API üstverisi yüklenemedi, lütfen internet bağlantınızı kontrol edin.",
"title": "API üstverisi yüklenemedi"
},
"failed": {
"badge": "Başarısız oldu",
"homeButton": "Ana sayfaya dön",
@ -296,7 +301,7 @@
},
"notFound": {
"badge": "Bulunamadı",
"detailsButton": "Detayları göster",
"detailsButton": "Ayrıntıları göster",
"homeButton": "Ana sayfaya git",
"text": "Sağlayıcılarımız arasında arama yaptık ve aradığınız medyayı bulamadık! Medyaları barındırmıyoruz ve mevcut olanlar üzerinde hiçbir kontrolümüz yok. Daha fazla ayrıntı için lütfen aşağıdaki 'Ayrıntıları göster' seçeneğine tıklayın.",
"title": "Bunu bulamadık"
@ -307,6 +312,12 @@
"remaining": "{{timeLeft}} kaldı • {{timeFinished, datetime}}'de bitiyor",
"shortRegular": "{{timeWatched}}",
"shortRemaining": "-{{timeLeft}}"
},
"turnstile": {
"description": "Lütfen sağ taraftaki Captcha'yı çözerek insan olduğunuzu doğrulayın. Bu, movie-web'i güvende tutmak içindir!",
"error": "İnsan olduğunuz doğrulanamadı. Lütfen tekrar deneyin.",
"title": "İnsan olduğunuzu doğrulamamız gerekiyor.",
"verifyingHumanity": "İnsan olduğunuz doğrulanıyor..."
}
},
"screens": {
@ -379,25 +390,20 @@
},
"connections": {
"server": {
"description": "Verilerinizi depolamak için özel bir arkayüze bağlanmak istiyorsanız, bunu etkinleştirin ve URL'yi sağlayın.",
"description": "Verilerinizi depolamak için özel bir arkayüze bağlanmak istiyorsanız, bunu etkinleştirin ve URL'yi sağlayın. <0>Yönergeler.</0>",
"label": "Özel sunucu",
"urlLabel": "Özel sunucu URL'si"
},
"title": "Bağlantılar",
"workers": {
"addButton": "Yeni işleyici ekle",
"description": "Uygulamanın çalışması için tüm trafik vekil sunucular üzerinden yönlendirilir. Kendi işleyicilerinizi getirmek istiyorsanız bunu etkinleştirin.",
"description": "Uygulamanın çalışması için tüm trafik vekil sunucular üzerinden yönlendirilir. Kendi işleyicilerinizi getirmek istiyorsanız bunu etkinleştirin.<0>Yönergeler.</0>",
"emptyState": "Henüz işleyici yok, aşağıya bir tane ekleyin",
"label": "Özel vekil sunucu işleyici kullan",
"urlLabel": "İşleyici URL'leri",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Uygulama dili",
"languageDescription": "Uygulamanın tamamına uygulanan dil.",
"title": "Yerelleştirme"
},
"reset": "Sıfırla",
"save": "Kaydet",
"sidebar": {
@ -417,7 +423,7 @@
"subtitles": {
"backgroundLabel": "Arka plan opaklığı",
"colorLabel": "Renk",
"previewQuote": "Korkmamalıyım. Korku akıl katilidir.",
"previewQuote": "Korkmamalıyım. Korku aklın katilidir.",
"textSizeLabel": "Yazı boyutu",
"title": "Altyazılar"
},

View File

@ -95,6 +95,7 @@
"about": "Про",
"dmca": "DMCA",
"login": "Логін",
"onboarding": "Встановлення",
"pagetitle": "{{title}} - movie-web",
"register": "Зареєструватися",
"settings": "Налаштування"
@ -165,6 +166,65 @@
"message": "Ми шукали всюди: під смітниками, у шафі, за проксі-сервером, але зрештою не змогли знайти сторінку, яку ви шукали.",
"title": "Не вдалося знайти цю сторінку"
},
"onboarding": {
"defaultConfirm": {
"cancel": "Скасувати",
"confirm": "Використовувати налаштування за умовчанням",
"description": "Налаштування за замовчуванням не мають найкращих потоків і можуть бути нестерпно повільними.",
"title": "Ви впевнені?"
},
"extension": {
"back": "Повернутись назад",
"explainer": "Використовуючи розширення для браузера, ви можете отримати найякісніші трансляції, які ми можемо запропонувати. Просто встановивши його.",
"extensionHelp": "Якщо ви встановили розширення, але воно не виявлено. <bold>Відкрийте розширення в меню розширень вашого браузеру</bold> і дотримуйтеся вказівок на екрані.",
"link": "Встановити розширення",
"status": {
"disallowed": "Розширення не ввімкнено для цієї сторінки",
"disallowedAction": "Активувати розширення",
"failed": "Не вдалося отримати статус",
"loading": "Очікуємо, поки ви встановите розширення",
"outdated": "Версія розширення застаріла",
"success": "Розширення працює як очікувалося!"
},
"submit": "Продовжити",
"title": "Почати використовувати розширення"
},
"proxy": {
"back": "Повернутись назад",
"explainer": "З використанням проксі ви можете отримати високоякісні потоки, створивши самостійний проксі-сервіс.",
"input": {
"errorConnection": "Не вдалося підключитися до проксі",
"errorInvalidUrl": "Не валідний URL",
"errorNotProxy": "Очікувався проксі, але отримано вебсайт",
"label": "URL проксі",
"placeholder": "https://"
},
"link": "Дізнайтесь як створити проксі",
"submit": "Надати проксі",
"title": "Давайте створимо новий проксі"
},
"start": {
"explainer": "Щоб отримати найкращу трансляцію. Вам потрібно буде вибрати, який метод стрімінгу ви хочете використовувати.",
"options": {
"default": {
"text": "Мені не потрібна хороша якість потоків,<0 /> <1>використовувати налаштування за замовчуванням</1>"
},
"extension": {
"action": "Встановити розширення",
"description": "Встановіть розширення для браузера та отримайте доступ до найкращих джерел.",
"quality": "Найкраща якість",
"title": "Розширення браузера"
},
"proxy": {
"action": "Налаштування проксі",
"description": "Налаштуйте проксі всього за 5 хвилин і отримайте доступ до чудових джерел.",
"quality": "Гарна якість",
"title": "Користувацький проксі"
}
},
"title": "Давайте налаштуємо вам movie-web"
}
},
"overlays": {
"close": "Закрити"
},
@ -182,7 +242,7 @@
"downloadPlaylist": "Завантажити плейлист",
"downloadSubtitle": "Завантажити поточні субтитри",
"downloadVideo": "Завантажити відео",
"hlsDisclaimer": "Завантаження виконуються безпосередньо від постачальника. У movie-web немає контролю над тим, як надаються завантаження. Будь ласка, зверніть увагу, що ви завантажуєте список відтворення HLS, він призначений для користувачів, знайомих із розширеним потоковим мультимедійним вмістом.",
"hlsDisclaimer": "Завантаження виконуються безпосередньо від постачальника. У movie-web немає контролю над тим, як надаються завантаження.<br /><br />Зверніть увагу, що ви завантажуєте список відтворення HLS, його <bold>не рекомендується завантажувати, якщо ви не знайомі з розширеними форматами потокового передавання</bold>. Спробуйте різні джерела для інших форматів.",
"onAndroid": {
"1": "Щоб завантажити на Android, натисніть кнопку завантаження, потім на новій сторінці <bold>торкніться й утримуйте</bold> відео, а потім виберіть <bold>зберегти</bold>.",
"shortTitle": "Завантажити / Android",
@ -263,6 +323,17 @@
"text": "Не вдалося завантажити метадані API, перевірте підключення до Інтернету.",
"title": "Не вдалося завантажити метадані API"
},
"dmca": {
"badge": "Видалено",
"text": "Це медіа більше не доступне через повідомлення про видалення або позов про порушення авторських прав.",
"title": "Медіа було видалено"
},
"extensionPermission": {
"badge": "Дозвіл Відсутній",
"button": "Використовувати розширення",
"text": "У вас вже є розширення для браузера, але нам потрібен ваш дозвіл, щоб почати використовувати його.",
"title": "Налаштуйте продовження"
},
"failed": {
"badge": "Не вдалося",
"homeButton": "Повернутися на головну",
@ -322,7 +393,7 @@
},
"screens": {
"dmca": {
"text": "Ласкаво просимо на контактну сторінку DMCA від movie-web! Ми поважаємо права інтелектуальної власності і прагнемо швидко вирішувати будь-які проблеми, пов'язані з авторськими правами. Якщо ви вважаєте, що ваша робота, захищена авторським правом, була неналежним чином використана на нашій платформі, будь ласка, надішліть детальне повідомлення DMCA на електронну адресу нижче. Будь ласка, додайте опис матеріалу, захищеного авторським правом, ваші контактні дані та заяву з обґрунтуванням ваших сумлінних переконань. Ми прагнемо оперативно вирішити ці питання і будемо вдячні за вашу співпрацю у збереженні movie-web місцем, де поважають творчість і авторські права.",
"text": "Вітаємо на нашій сторінці зв'язку DMCA! Ми поважаємо права інтелектуальної власності і хочемо вирішити будь-які проблеми з авторськими правами швидко. Якщо ви вважаєте, що ваші авторські права були неправильно використані на нашій платформі, будь ласка, надішліть детальне повідомлення DMCA на електронну адресу нижче. Будь ласка, вкажіть опис авторського матеріалу, ваші контактні дані та заяву про добросовісну віру. Ми зобов'язані вирішити ці питання оперативно і вдячні за вашу співпрацю в збереженні movie-web місцем, яке поважає творчість та авторські права.",
"title": "DMCA"
},
"loadingApp": "Завантаження застосунку",
@ -381,7 +452,7 @@
"activeTheme": "Активна тема",
"themes": {
"blue": "Блакитний",
"default": "Основний",
"default": "За замовчуванням",
"gray": "Сірий",
"red": "Червоний",
"teal": "Бірюзовий"
@ -390,24 +461,49 @@
},
"connections": {
"server": {
"description": "Якщо ви хочете підключитися до кастомного серверу для зберігання ваших даних, увімкніть це та надайте URL.",
"description": "Якщо ви бажаєте підключитися до користувацького сервера для зберігання даних, увімкніть це та вкажіть URL-адресу. <0>Інструкції.</0>",
"label": "Власний сервер",
"urlLabel": "URL сервера"
},
"setup": {
"doSetup": "Виконайте налаштування",
"errorStatus": {
"description": "Здається, що один або декілька пунктів у цьому налаштуванні потребують вашої уваги.",
"title": "Дещо потребує вашої уваги"
},
"itemError": "Щось не так із цією настройкою. Пройдіть налаштування ще раз, щоб виправити це.",
"items": {
"default": "Налаштування за замовчанням",
"extension": "Розширення",
"proxy": "Користувацький проксі"
},
"redoSetup": "Повторити налаштування",
"successStatus": {
"description": "Усе готово для того, щоб ви могли почати дивитися улюблені медіа.",
"title": "Все готово!"
},
"unsetStatus": {
"description": "Будь ласка, натисніть кнопку праворуч, щоб розпочати процес налаштування.",
"title": "Ви не завершили налаштування"
}
},
"title": "З'єднання",
"workers": {
"addButton": "Додати нового працівника",
"description": "Щоб додаток працював, весь трафік маршрутизується через проксі-сервери. Увімкніть це, якщо ви хочете використовувати власні проксі воркери.",
"description": "Щоб додаток працював, весь трафік маршрутизується через проксі-сервери. Увімкніть це, якщо ви хочете використовувати власні проксі воркери. <0>Інструкція.</0>",
"emptyState": "Немає працівників",
"label": "Використовувати власних проксі-працівників",
"urlLabel": "URL-у працівника",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "Мова застосунку",
"languageDescription": "Виберіть мову, яку ви хочете використовувати.",
"title": "Налаштування локації"
"preferences": {
"language": "Мова додатку",
"languageDescription": "Мова застосована до всього додатку.",
"thumbnail": "Створити мініатюри",
"thumbnailDescription": "Часто відео не мають мініатюр. Ви можете активувати цей параметр для їх генерації під час відтворення, але це може уповільнити відтворення відео.",
"thumbnailLabel": "Сгенерувати мініатюри",
"title": "Параметри"
},
"reset": "Скинути налаштування",
"save": "Зберегти",

View File

@ -1,18 +1,18 @@
{
"about": {
"description": "movie-web là một ứng dụng web tìm kiếm các truyền pháp trực tuyến trên internet. Nhóm phát triển ứng dụng này nhắm đến một cách tiêu thụ nội dung chủ yếu là đơn giản hơn.",
"description": "movie-web là một ứng dụng web tìm kiếm các nguồn truyền phát trực tuyến trên Internet. Nhóm phát triển ứng dụng này nhắm đến một cách dễ dàng hơn trong việc tiêu thụ nội dung.",
"faqTitle": "Các câu hỏi thường gặp",
"q1": {
"body": "movie-web không lưu trữ bất kì nội dung nào. Khi bạn bấm vào một cái gì đó để xem, ứng dụng sẽ tìm kiếm nội dung đó trên internet (Trên màn hình tải và trong tab 'nguồn video' bạn sẽ tìm thấy nguồn đang được dùng). Nội dung không bao giờ được tải lên trên movie-web, mọi thứ đều thông qua cơ chế tìm kiếm này.",
"body": "movie-web không lưu trữ bất kì nội dung nào. Khi bạn chọn xem một nội dung nào đó, ứng dụng sẽ tìm kiếm nội dung đó trên Internet (Khi nội dung tải và trong tab 'nguồn video' bạn sẽ tìm thấy nguồn đang được dùng). Nội dung không bao giờ được tải lên trên movie-web, mọi thứ đều được tìm kiếm thông qua phương thức này.",
"title": "Nội dung đến từ đâu?"
},
"q2": {
"body": "Việc yêu cầu một chương trình truyền hình hoặc phim là bất khả thi bởi vì movie-web không quản lý nội dung nào. Tất cả nội dung được truyền thông qua các nguồn trên internet.",
"title": "Tôi có thể yêu cầu một chương trình truyền hình hoặc phim ở đâu?"
"body": "Việc yêu cầu thêm một chương trình truyền hình hoặc phim là điều bất khả thi bởi vì movie-web không quản lý bất kỳ nội dung nào. Tất cả nội dung được truyền thông qua những nguồn trên internet.",
"title": "Tôi có thể yêu cầu thêm một chương trình truyền hình hoặc phim ở đâu?"
},
"q3": {
"body": "Các kết quả tìm kiếm được cung cấp bởi The Movie Database (TMDB) và hiện lên bất kể các nguồn của trang thực sự có lưu trữ nội dung hay không.",
"title": "Tại sao kết quả tìm kiếm hiển thị chương trình truyền hình hoặc phim nhưng tôi không thể chơi nó?"
"body": "Các kết quả tìm kiếm được cung cấp bởi The Movie Database (TMDB) và hiện lên bất kể các nguồn của trang thực sự có lưu trữ nội dung đó hay không.",
"title": "Tại sao kết quả tìm kiếm hiển thị chương trình truyền hình hoặc phim nhưng tôi không thể xem nó?"
},
"title": "Về movie-web"
},
@ -52,12 +52,49 @@
},
"trust": {
"failed": {
"text": "Bạn đã lắp đặt nó một cách chính xác chưa?"
}
"text": "Bạn đã cài đặt nó một cách chính xác chưa?",
"title": "Không thể truy vấn máy chủ"
},
"host": "Bạn đang kết nối đến máy chủ <0>{{hostname}}</0> - vui lòng chắc chắn rằng bạn tin tưởng máy chủ này trước khi tạo tài khoản",
"no": "Quay lại",
"title": "Bạn có tin tưởng máy chủ này không?",
"yes": "Tôi tin tưởng máy chủ này"
},
"verify": {
"description": "Vui lòng nhập mật ngữ của bạn lúc nãy đễ chắc chắn rằng bạn đã lưu nó và để tạo tài khoản",
"invalidData": "Dữ liệu không hợp lệ",
"noMatch": "Mật ngữ không khớp",
"passphraseLabel": "Mật ngữ 12 ký tự của bạn",
"recaptchaFailed": "Xác minh bằng ReCaptcha không hợp lệ",
"register": "Tạo tài khoản",
"title": "Nhập lại mật ngữ của bạn"
}
},
"errors": {
"badge": "Lỗi",
"details": "Thông tin về lỗi",
"reloadPage": "Tải lại trang",
"showError": "Hiển thị thông tin về lỗi",
"title": "Đã xảy ra lỗi!"
},
"footer": {
"legal": {
"disclaimer": "Tuyên bố miễn trừ trách nhiệm",
"disclaimerText": "movie-web không lưu trữ bất kì file nào, nó chỉ đến những đường dẫn của các dịch vụ bên thứ ba. Bất kỳ vấn đề nào về pháp lý nên được đưa đến chủ sỡ hữu của file hoặc những nhà cung cấp đó. movie-web hoàn toàn không chịu trách nhiệm cho bất kỳ nội dung nào được chiếu từ các nhà cung cấp."
},
"links": {
"discord": "Discord",
"dmca": "DMCA",
"github": "GitHub"
},
"tagline": "Xem các chương trình và phim yêu thích của bạn với ứng dụng phát trực tuyến nguồn mở này."
},
"global": {
"name": "movie-web"
"name": "movie-web",
"pages": {
"pagetitle": "{{title}} - movie-web",
"register": "Đăng ký"
}
},
"home": {
"bookmarks": {

View File

@ -390,25 +390,20 @@
},
"connections": {
"server": {
"description": "若您想连接到自定义后端保存数据,请启用此选项并提供 URL。",
"description": "若您想连接到自定义后端保存数据,请启用此选项并提供 URL。 <0>查看指引。</0>",
"label": "自定义服务器",
"urlLabel": "自定义服务器 URL"
},
"title": "连接",
"workers": {
"addButton": "添加新的 Worker",
"description": "要让应用程序正常运作,所有流量会通过代理路由。若您想使用自己的 Worker请启用该选项。",
"description": "要让应用程序正常运作,所有流量会通过代理路由。若您想使用自己的 Worker请启用该选项。 <0>查看指引。</0>",
"emptyState": "还没有 Worker在下方添加一个",
"label": "使用自定义代理 Worker",
"urlLabel": "Worker URL",
"urlPlaceholder": "https://"
}
},
"locale": {
"language": "应用程序语言",
"languageDescription": "当前已应用到整个应用程序的语言。",
"title": "本地化"
},
"reset": "重设",
"save": "保存",
"sidebar": {

View File

@ -0,0 +1,7 @@
import { satisfies } from "semver";
const allowedExtensionRange = "~1.0.0";
export function isAllowedExtensionVersion(version: string): boolean {
return satisfies(version, allowedExtensionRange);
}

View File

@ -0,0 +1,71 @@
import {
MessagesMetadata,
sendToBackgroundViaRelay,
} from "@plasmohq/messaging";
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility";
import { ExtensionMakeRequestResponse } from "@/backend/extension/plasmo";
let activeExtension = false;
function sendMessage<MessageKey extends keyof MessagesMetadata>(
message: MessageKey,
payload: MessagesMetadata[MessageKey]["req"] | undefined = undefined,
timeout: number = -1,
) {
return new Promise<MessagesMetadata[MessageKey]["res"] | null>((resolve) => {
if (timeout >= 0) setTimeout(() => resolve(null), timeout);
sendToBackgroundViaRelay<
MessagesMetadata[MessageKey]["req"],
MessagesMetadata[MessageKey]["res"]
>({
name: message,
body: payload,
})
.then((res) => {
activeExtension = true;
resolve(res);
})
.catch(() => {
activeExtension = false;
resolve(null);
});
});
}
export async function sendExtensionRequest<T>(
ops: MessagesMetadata["makeRequest"]["req"],
): Promise<ExtensionMakeRequestResponse<T> | null> {
return sendMessage("makeRequest", ops);
}
export async function setDomainRule(
ops: MessagesMetadata["prepareStream"]["req"],
): Promise<MessagesMetadata["prepareStream"]["res"] | null> {
return sendMessage("prepareStream", ops);
}
export async function sendPage(
ops: MessagesMetadata["openPage"]["req"],
): Promise<MessagesMetadata["openPage"]["res"] | null> {
return sendMessage("openPage", ops);
}
export async function extensionInfo(): Promise<
MessagesMetadata["hello"]["res"] | null
> {
const message = await sendMessage("hello", undefined, 300);
return message;
}
export function isExtensionActiveCached(): boolean {
return activeExtension;
}
export async function isExtensionActive(): Promise<boolean> {
const info = await extensionInfo();
if (!info?.success) return false;
const allowedVersion = isAllowedExtensionVersion(info.version);
if (!allowedVersion) return false;
return info.allowed && info.hasPermission;
}

View File

@ -0,0 +1,68 @@
export interface ExtensionBaseRequest {}
export type ExtensionBaseResponse<T = object> =
| ({
success: true;
} & T)
| {
success: false;
error: string;
};
export type ExtensionHelloResponse = ExtensionBaseResponse<{
version: string;
allowed: boolean;
hasPermission: boolean;
}>;
export interface ExtensionMakeRequest extends ExtensionBaseRequest {
url: string;
method: string;
headers?: Record<string, string>;
body?: string | FormData | URLSearchParams | Record<string, any>;
}
export type ExtensionMakeRequestResponse<T> = ExtensionBaseResponse<{
response: {
statusCode: number;
headers: Record<string, string>;
finalUrl: string;
body: T;
};
}>;
export interface ExtensionPrepareStreamRequest extends ExtensionBaseRequest {
ruleId: number;
targetDomains: string[];
requestHeaders?: Record<string, string>;
responseHeaders?: Record<string, string>;
}
export interface MmMetadata {
hello: {
req: ExtensionBaseRequest;
res: ExtensionHelloResponse;
};
makeRequest: {
req: ExtensionMakeRequest;
res: ExtensionMakeRequestResponse<any>;
};
prepareStream: {
req: ExtensionPrepareStreamRequest;
res: ExtensionBaseResponse;
};
openPage: {
req: ExtensionBaseRequest & {
page: string;
redirectUrl: string;
};
res: ExtensionBaseResponse;
};
}
interface MpMetadata {}
declare module "@plasmohq/messaging" {
interface MessagesMetadata extends MmMetadata {}
interface PortsMetadata extends MpMetadata {}
}

View File

@ -0,0 +1,43 @@
import { Stream } from "@movie-web/providers";
import { setDomainRule } from "@/backend/extension/messaging";
function extractDomain(url: string): string | null {
try {
const u = new URL(url);
return u.hostname;
} catch {
return null;
}
}
function extractDomainsFromStream(stream: Stream): string[] {
if (stream.type === "hls") {
return [extractDomain(stream.playlist)].filter((v): v is string => !!v);
}
if (stream.type === "file") {
return Object.values(stream.qualities)
.map((v) => extractDomain(v.url))
.filter((v): v is string => !!v);
}
return [];
}
function buildHeadersFromStream(stream: Stream): Record<string, string> {
const headers: Record<string, string> = {};
Object.entries(stream.headers ?? {}).forEach((entry) => {
headers[entry[0]] = entry[1];
});
Object.entries(stream.preferredHeaders ?? {}).forEach((entry) => {
headers[entry[0]] = entry[1];
});
return headers;
}
export async function prepareStream(stream: Stream) {
await setDomainRule({
ruleId: 1,
targetDomains: extractDomainsFromStream(stream),
requestHeaders: buildHeadersFromStream(stream),
});
}

View File

@ -1,7 +1,7 @@
import { ofetch } from "ofetch";
import { getApiToken, setApiToken } from "@/backend/helpers/providerApi";
import { getLoadbalancedProxyUrl } from "@/utils/providers";
import { getLoadbalancedProxyUrl } from "@/backend/providers/fetchers";
type P<T> = Parameters<typeof ofetch<T, any>>;
type R<T> = ReturnType<typeof ofetch<T, any>>;

View File

@ -1,12 +1,6 @@
import {
Fetcher,
ProviderControls,
makeProviders,
makeSimpleProxyFetcher,
makeStandardFetcher,
targets,
} from "@movie-web/providers";
import { Fetcher, makeSimpleProxyFetcher } from "@movie-web/providers";
import { sendExtensionRequest } from "@/backend/extension/messaging";
import { getApiToken, setApiToken } from "@/backend/helpers/providerApi";
import { getProviderApiUrls, getProxyUrls } from "@/utils/proxyUrls";
@ -48,7 +42,7 @@ async function fetchButWithApiTokens(
return response;
}
function makeLoadBalancedSimpleProxyFetcher() {
export function makeLoadBalancedSimpleProxyFetcher() {
const fetcher: Fetcher = async (a, b) => {
const currentFetcher = makeSimpleProxyFetcher(
getLoadbalancedProxyUrl(),
@ -59,8 +53,32 @@ function makeLoadBalancedSimpleProxyFetcher() {
return fetcher;
}
export const providers = makeProviders({
fetcher: makeStandardFetcher(fetch),
proxiedFetcher: makeLoadBalancedSimpleProxyFetcher(),
target: targets.BROWSER,
}) as any as ProviderControls;
function makeFinalHeaders(
readHeaders: string[],
headers: Record<string, string>,
): Headers {
const lowercasedHeaders = readHeaders.map((v) => v.toLowerCase());
return new Headers(
Object.entries(headers).filter((entry) =>
lowercasedHeaders.includes(entry[0].toLowerCase()),
),
);
}
export function makeExtensionFetcher() {
const fetcher: Fetcher = async (url, ops) => {
const result = (await sendExtensionRequest<any>({
url,
...ops,
})) as any;
if (!result?.success) throw new Error(`extension error: ${result?.error}`);
const res = result.response;
return {
body: res.body,
finalUrl: res.finalUrl,
statusCode: res.statusCode,
headers: makeFinalHeaders(ops.readHeaders, res.headers),
};
};
return fetcher;
}

View File

@ -0,0 +1,27 @@
import {
makeProviders,
makeStandardFetcher,
targets,
} from "@movie-web/providers";
import { isExtensionActiveCached } from "@/backend/extension/messaging";
import {
makeExtensionFetcher,
makeLoadBalancedSimpleProxyFetcher,
} from "@/backend/providers/fetchers";
export function getProviders() {
if (isExtensionActiveCached()) {
return makeProviders({
fetcher: makeExtensionFetcher(),
target: targets.BROWSER_EXTENSION,
consistentIpForRequests: true,
});
}
return makeProviders({
fetcher: makeStandardFetcher(fetch),
proxiedFetcher: makeLoadBalancedSimpleProxyFetcher(),
target: targets.BROWSER,
});
}

View File

@ -109,7 +109,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
return (
<div className="relative is-dropdown">
<div
className="cursor-pointer tabbable rounded-full flex gap-2 text-white items-center py-2 px-3 bg-pill-background bg-opacity-50 hover:bg-pill-backgroundHover transition-[background,transform] duration-100 hover:scale-105"
className="cursor-pointer tabbable rounded-full flex gap-2 text-white items-center py-2 px-3 bg-pill-background bg-opacity-50 hover:bg-pill-backgroundHover backdrop-blur-lg transition-[background,transform] duration-100 hover:scale-105"
tabIndex={0}
onClick={toggleOpen}
onKeyUp={(evt) => evt.key === "Enter" && toggleOpen()}

View File

@ -41,7 +41,7 @@ export function Button(props: Props) {
props.padding ?? "px-4 py-3",
props.className,
colorClasses,
props.disabled ? "cursor-not-allowed bg-opacity-60 text-opacity-60" : null,
props.disabled ? "!cursor-not-allowed bg-opacity-60 text-opacity-60" : null,
);
if (props.disabled)

View File

@ -25,7 +25,7 @@ export function IconPatch(props: IconPatchProps) {
return (
<div className={props.className || undefined} onClick={props.onClick}>
<div
className={`flex items-center justify-center rounded-full border-2 border-transparent bg-pill-background bg-opacity-50 transition-[background-color,color,transform,border-color] duration-75 ${transparentClasses} ${clickableClasses} ${activeClasses} ${sizeClasses}`}
className={`flex items-center justify-center rounded-full border-2 border-transparent backdrop-blur-lg bg-pill-background bg-opacity-50 transition-[background-color,color,transform,border-color] duration-75 ${transparentClasses} ${clickableClasses} ${activeClasses} ${sizeClasses}`}
>
<Icon icon={props.icon} />
</div>

View File

@ -1,6 +1,6 @@
import classNames from "classnames";
export function Toggle(props: { onClick: () => void; enabled?: boolean }) {
export function Toggle(props: { onClick?: () => void; enabled?: boolean }) {
return (
<button
type="button"

View File

@ -16,7 +16,7 @@ export function BrandPill(props: {
"flex items-center space-x-2 rounded-full px-4 py-2 text-type-logo",
props.backgroundClass ?? "bg-pill-background bg-opacity-50",
props.clickable
? "transition-[transform,background-color] hover:scale-105 hover:bg-pill-backgroundHover hover:text-type-logo active:scale-95"
? "transition-[transform,background-color] hover:scale-105 hover:bg-pill-backgroundHover backdrop-blur-lg hover:text-type-logo active:scale-95"
: "",
)}
>

View File

@ -0,0 +1,25 @@
export interface StepperProps {
current: number;
steps: number;
className?: string;
}
export function Stepper(props: StepperProps) {
const percentage = (props.current / props.steps) * 100;
return (
<div className={props.className}>
<p className="mb-2">
{props.current}/{props.steps}
</p>
<div className="max-w-full h-1 w-32 bg-onboarding-bar rounded-full overflow-hidden">
<div
className="h-full bg-onboarding-barFilled transition-[width] rounded-full"
style={{
width: `${percentage.toFixed(0)}%`,
}}
/>
</div>
</div>
);
}

View File

@ -1,3 +1,4 @@
import classNames from "classnames";
import { ReactNode } from "react";
interface ThinContainerProps {
@ -16,3 +17,16 @@ export function ThinContainer(props: ThinContainerProps) {
</div>
);
}
export function CenterContainer(props: ThinContainerProps) {
return (
<div
className={classNames(
"min-h-screen w-full flex justify-center p-8 py-24 items-center",
props.classNames,
)}
>
<div className="w-[700px] max-w-full">{props.children}</div>
</div>
);
}

View File

@ -19,7 +19,7 @@ export function useModal(id: string) {
export function ModalCard(props: { children?: ReactNode }) {
return (
<div className="w-full max-w-[30rem] m-4">
<div className="w-full bg-dropdown-background rounded-xl p-8 pointer-events-auto">
<div className="w-full bg-modal-background rounded-xl p-8 pointer-events-auto">
{props.children}
</div>
</div>

View File

@ -75,6 +75,7 @@ function CustomCaptionOption() {
setCaption({
language: "custom",
srtData: converted,
id: "custom-caption",
});
setCustomSubs();
});
@ -115,39 +116,38 @@ function useSubtitleList(subs: CaptionListItem[], searchQuery: string) {
export function CaptionsView({ id }: { id: string }) {
const { t } = useTranslation();
const router = useOverlayRouter(id);
const lang = usePlayerStore((s) => s.caption.selected?.language);
const selectedCaptionId = usePlayerStore((s) => s.caption.selected?.id);
const [currentlyDownloading, setCurrentlyDownloading] = useState<
string | null
>(null);
const { selectLanguage, disable } = useCaptions();
const { selectCaptionById, disable } = useCaptions();
const captionList = usePlayerStore((s) => s.captionList);
const [searchQuery, setSearchQuery] = useState("");
const subtitleList = useSubtitleList(captionList, searchQuery);
const [downloadReq, startDownload] = useAsyncFn(
async (language: string) => {
setCurrentlyDownloading(language);
return selectLanguage(language);
async (captionId: string) => {
setCurrentlyDownloading(captionId);
return selectCaptionById(captionId);
},
[selectLanguage, setCurrentlyDownloading],
[selectCaptionById, setCurrentlyDownloading],
);
const content = subtitleList.map((v, i) => {
const content = subtitleList.map((v) => {
return (
<CaptionOption
// key must use index to prevent url collisions
// eslint-disable-next-line react/no-array-index-key
key={`${i}-${v.url}`}
key={v.id}
countryCode={v.language}
selected={lang === v.language}
loading={v.language === currentlyDownloading && downloadReq.loading}
selected={v.id === selectedCaptionId}
loading={v.id === currentlyDownloading && downloadReq.loading}
error={
v.language === currentlyDownloading && downloadReq.error
v.id === currentlyDownloading && downloadReq.error
? downloadReq.error.toString()
: undefined
}
onClick={() => startDownload(v.language)}
onClick={() => startDownload(v.id)}
>
{v.languageName}
</CaptionOption>
@ -176,7 +176,7 @@ export function CaptionsView({ id }: { id: string }) {
</div>
</div>
<Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3">
<CaptionOption onClick={() => disable()} selected={!lang}>
<CaptionOption onClick={() => disable()} selected={!selectedCaptionId}>
{t("player.menus.subtitles.offChoice")}
</CaptionOption>
<CustomCaptionOption />

View File

@ -27,6 +27,7 @@ function StyleTrans(props: { k: string }) {
i18nKey={props.k}
components={{
bold: <Menu.Highlight />,
br: <br />,
ios_share: (
<Icon icon={Icons.IOS_SHARE} className="inline-block text-xl -mb-1" />
),
@ -123,24 +124,6 @@ export function DownloadView({ id }: { id: string }) {
);
}
export function CantDownloadView({ id }: { id: string }) {
const router = useOverlayRouter(id);
const { t } = useTranslation();
return (
<>
<Menu.BackLink onClick={() => router.navigate("/")}>
{t("player.menus.downloads.title")}
</Menu.BackLink>
<Menu.Section>
<Menu.Paragraph>
<StyleTrans k="player.menus.downloads.hlsExplanation" />
</Menu.Paragraph>
</Menu.Section>
</>
);
}
function AndroidExplanationView({ id }: { id: string }) {
const router = useOverlayRouter(id);
const { t } = useTranslation();
@ -202,11 +185,6 @@ export function DownloadRoutes({ id }: { id: string }) {
<DownloadView id={id} />
</Menu.CardWithScrollable>
</OverlayPage>
<OverlayPage id={id} path="/download/unable" width={343} height={440}>
<Menu.CardWithScrollable>
<CantDownloadView id={id} />
</Menu.CardWithScrollable>
</OverlayPage>
<OverlayPage id={id} path="/download/ios" width={343} height={440}>
<Menu.CardWithScrollable>
<IOSExplanationView id={id} />

View File

@ -147,7 +147,7 @@ export function SourceSelectionView({
<Menu.BackLink onClick={() => router.navigate("/")}>
{t("player.menus.sources.title")}
</Menu.BackLink>
<Menu.Section>
<Menu.Section className="pb-4">
{sources.map((v) => (
<SelectableLink
key={v.id}

View File

@ -41,6 +41,7 @@ function qualityToHlsLevel(quality: SourceQuality): number | null {
);
return found ? +found[0] : null;
}
function hlsLevelsToQualities(levels: Level[]): SourceQuality[] {
return levels
.map((v) => hlsLevelToQuality(v))

View File

@ -41,6 +41,7 @@ export interface DisplayMeta {
}
export interface DisplayCaption {
id: string;
srtData: string;
language: string;
url?: string;

View File

@ -14,22 +14,32 @@ export function useCaptions() {
const lastSelectedLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const captionList = usePlayerStore((s) => s.captionList);
const selectLanguage = useCallback(
async (language: string) => {
const caption = captionList.find((v) => v.language === language);
const selectCaptionById = useCallback(
async (captionId: string) => {
const caption = captionList.find((v) => v.id === captionId);
if (!caption) return;
const srtData = await downloadCaption(caption);
setCaption({
id: caption.id,
language: caption.language,
srtData,
url: caption.url,
});
resetSubtitleSpecificSettings();
setLanguage(language);
setLanguage(caption.language);
},
[setLanguage, captionList, setCaption, resetSubtitleSpecificSettings],
);
const selectLanguage = useCallback(
async (language: string) => {
const caption = captionList.find((v) => v.language === language);
if (!caption) return;
return selectCaptionById(caption.id);
},
[captionList, selectCaptionById],
);
const disable = useCallback(async () => {
setCaption(null);
setLanguage(null);
@ -56,5 +66,6 @@ export function useCaptions() {
selectLastUsedLanguage,
toggleLastUsed,
selectLastUsedLanguageIfEnabled,
selectCaptionById,
};
}

View File

@ -5,6 +5,8 @@ import {
} from "@movie-web/providers";
import { useAsyncFn } from "react-use";
import { isExtensionActiveCached } from "@/backend/extension/messaging";
import { prepareStream } from "@/backend/extension/streams";
import {
connectServerSideEvents,
makeProviderUrl,
@ -13,12 +15,13 @@ import {
scrapeSourceOutputToProviderMetric,
useReportProviders,
} from "@/backend/helpers/report";
import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers";
import { getProviders } from "@/backend/providers/providers";
import { convertProviderCaption } from "@/components/player/utils/captions";
import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { metaToScrapeMedia } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers";
export function useEmbedScraping(
routerId: string,
@ -47,7 +50,7 @@ export function useEmbedScraping(
);
result = await conn.promise();
} else {
result = await providers.runEmbedScraper({
result = await getProviders().runEmbedScraper({
id: embedId,
url,
});
@ -70,6 +73,7 @@ export function useEmbedScraping(
report([
scrapeSourceOutputToProviderMetric(meta, sourceId, null, "success", null),
]);
if (isExtensionActiveCached()) await prepareStream(result.stream[0]);
setSourceId(sourceId);
setCaption(null);
setSource(
@ -111,7 +115,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
);
result = await conn.promise();
} else {
result = await providers.runSourceScraper({
result = await getProviders().runSourceScraper({
id: sourceId,
media: scrapeMedia,
});
@ -130,6 +134,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
]);
if (result.stream) {
if (isExtensionActiveCached()) await prepareStream(result.stream[0]);
setCaption(null);
setSource(
convertRunoutputToSource({ stream: result.stream[0] }),
@ -155,7 +160,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
);
embedResult = await conn.promise();
} else {
embedResult = await providers.runEmbedScraper({
embedResult = await getProviders().runEmbedScraper({
id: result.embeds[0].embedId,
url: result.embeds[0].url,
});
@ -186,6 +191,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) {
]);
setSourceId(sourceId);
setCaption(null);
if (isExtensionActiveCached()) await prepareStream(embedResult.stream[0]);
setSource(
convertRunoutputToSource({ stream: embedResult.stream[0] }),
convertProviderCaption(embedResult.stream[0].captions),

View File

@ -2,7 +2,10 @@ import classNames from "classnames";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { StatusCircle } from "@/components/player/internals/StatusCircle";
import {
StatusCircle,
StatusCircleProps,
} from "@/components/player/internals/StatusCircle";
import { Transition } from "@/components/utils/Transition";
export interface ScrapeItemProps {
@ -23,13 +26,14 @@ const statusTextMap: Partial<Record<ScrapeCardProps["status"], string>> = {
pending: "player.scraping.items.pending",
};
const statusMap: Record<ScrapeCardProps["status"], StatusCircle["type"]> = {
failure: "error",
notfound: "noresult",
pending: "loading",
success: "success",
waiting: "waiting",
};
const statusMap: Record<ScrapeCardProps["status"], StatusCircleProps["type"]> =
{
failure: "error",
notfound: "noresult",
pending: "loading",
success: "success",
waiting: "waiting",
};
export function ScrapeItem(props: ScrapeItemProps) {
const { t } = useTranslation();

View File

@ -4,23 +4,24 @@ import classNames from "classnames";
import { Icon, Icons } from "@/components/Icon";
import { Transition } from "@/components/utils/Transition";
export interface StatusCircle {
export interface StatusCircleProps {
type: "loading" | "success" | "error" | "noresult" | "waiting";
percentage?: number;
className?: string;
}
export interface StatusCircleLoading extends StatusCircle {
export interface StatusCircleLoading extends StatusCircleProps {
type: "loading";
percentage: number;
}
function statusIsLoading(
props: StatusCircle | StatusCircleLoading,
props: StatusCircleProps | StatusCircleLoading,
): props is StatusCircleLoading {
return props.type === "loading";
}
export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
export function StatusCircle(props: StatusCircleProps | StatusCircleLoading) {
const [spring] = useSpring(
() => ({
percentage: statusIsLoading(props) ? props.percentage : 0,
@ -30,18 +31,21 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
return (
<div
className={classNames({
"p-0.5 border-current border-[3px] rounded-full h-6 w-6 relative transition-colors":
true,
"text-video-scraping-loading": props.type === "loading",
"text-video-scraping-noresult text-opacity-50":
props.type === "waiting",
"text-video-scraping-error bg-video-scraping-error":
props.type === "error",
"text-green-500 bg-green-500": props.type === "success",
"text-video-scraping-noresult bg-video-scraping-noresult":
props.type === "noresult",
})}
className={classNames(
{
"p-0.5 border-current border-[3px] rounded-full h-6 w-6 relative transition-colors":
true,
"text-video-scraping-loading": props.type === "loading",
"text-video-scraping-noresult text-opacity-50":
props.type === "waiting",
"text-video-scraping-error bg-video-scraping-error":
props.type === "error",
"text-green-500 bg-green-500": props.type === "success",
"text-video-scraping-noresult bg-video-scraping-noresult":
props.type === "noresult",
},
props.className,
)}
>
<Transition animation="fade" show={statusIsLoading(props)}>
<svg
@ -65,13 +69,13 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) {
</Transition>
<Transition animation="fade" show={props.type === "error"}>
<Icon
className="absolute inset-0 flex items-center justify-center text-white"
className="absolute inset-0 flex items-center justify-center text-background-main"
icon={Icons.X}
/>
</Transition>
<Transition animation="fade" show={props.type === "success"}>
<Icon
className="absolute inset-0 flex items-center text-xs justify-center text-white"
className="absolute inset-0 flex items-center text-sm justify-center text-background-main"
icon={Icons.CHECKMARK}
/>
</Transition>

View File

@ -5,6 +5,7 @@ import { playerStatus } from "@/stores/player/slices/source";
import { ThumbnailImage } from "@/stores/player/slices/thumbnails";
import { usePlayerStore } from "@/stores/player/store";
import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities";
import { usePreferencesStore } from "@/stores/preferences";
import { processCdnLink } from "@/utils/cdn";
import { isSafari } from "@/utils/detectFeatures";
@ -128,6 +129,7 @@ export function ThumbnailScraper() {
const resetImages = usePlayerStore((s) => s.thumbnails.resetImages);
const meta = usePlayerStore((s) => s.meta);
const source = usePlayerStore((s) => s.source);
const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails);
const workerRef = useRef<ThumnbnailWorker | null>(null);
// object references dont always trigger changes, so we serialize it to detect *any* change
@ -159,8 +161,8 @@ export function ThumbnailScraper() {
// start worker with the stream
useEffect(() => {
startRef.current();
}, [sourceSeralized]);
if (enableThumbnails) startRef.current();
}, [sourceSeralized, enableThumbnails]);
// destroy worker on unmount
useEffect(() => {
@ -183,8 +185,8 @@ export function ThumbnailScraper() {
workerRef.current.destroy();
workerRef.current = null;
}
startRef.current();
}, [serializedMeta, sourceSeralized, status]);
if (enableThumbnails) startRef.current();
}, [serializedMeta, sourceSeralized, status, enableThumbnails]);
return null;
}

View File

@ -80,6 +80,7 @@ export function convertProviderCaption(
captions: RunOutput["stream"]["captions"],
): CaptionListItem[] {
return captions.map((v) => ({
id: v.id,
language: v.language,
url: v.url,
needsProxy: v.hasCorsRestrictions,

View File

@ -28,6 +28,7 @@ export function convertRunoutputToSource(out: {
return {
type: "hls",
url: out.stream.playlist,
preferredHeaders: out.stream.preferredHeaders,
};
}
if (out.stream.type === "file") {
@ -49,6 +50,7 @@ export function convertRunoutputToSource(out: {
return {
type: "file",
qualities,
preferredHeaders: out.stream.preferredHeaders,
};
}
throw new Error("unrecognized type");

View File

@ -1,3 +1,5 @@
import classNames from "classnames";
import { TextInputControl } from "./TextInputControl";
export function AuthInputBox(props: {
@ -8,9 +10,10 @@ export function AuthInputBox(props: {
placeholder?: string;
onChange?: (data: string) => void;
passwordToggleable?: boolean;
className?: string;
}) {
return (
<div className="space-y-3">
<div className={classNames("space-y-3", props.className)}>
{props.label ? (
<p className="font-bold text-white">{props.label}</p>
) : null}

View File

@ -0,0 +1,18 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { Icon, Icons } from "@/components/Icon";
export function ErrorLine(props: { children?: ReactNode; className?: string }) {
return (
<p
className={classNames(
"inline-flex items-center text-type-danger",
props.className,
)}
>
<Icon icon={Icons.WARNING} className="text-xl mr-4" />
{props.children}
</p>
);
}

View File

@ -69,7 +69,7 @@ function Light(props: FlareProps) {
},
)}
style={{
backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`,
backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}) / 1), rgba(var(${cssVar}) / 0) 70%)`,
backgroundPosition: `var(--bg-x) var(--bg-y)`,
backgroundRepeat: "no-repeat",
backgroundSize: `${size.toFixed(0)}px ${size.toFixed(0)}px`,
@ -85,7 +85,7 @@ function Light(props: FlareProps) {
<div
className="absolute inset-0 opacity-10"
style={{
background: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`,
background: `radial-gradient(circle at center, rgba(var(${cssVar}) / 1), rgba(var(${cssVar}) / 0) 70%)`,
backgroundPosition: `var(--bg-x) var(--bg-y)`,
backgroundRepeat: "no-repeat",
backgroundSize: `${size.toFixed(0)}px ${size.toFixed(0)}px`,

View File

@ -5,12 +5,15 @@ import {
} from "@movie-web/providers";
import { RefObject, useCallback, useEffect, useRef, useState } from "react";
import { isExtensionActiveCached } from "@/backend/extension/messaging";
import { prepareStream } from "@/backend/extension/streams";
import {
connectServerSideEvents,
getCachedMetadata,
makeProviderUrl,
} from "@/backend/helpers/providerApi";
import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers";
import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers";
import { getProviders } from "@/backend/providers/providers";
export interface ScrapingItems {
id: string;
@ -168,12 +171,14 @@ export function useScrape() {
conn.on("update", updateEvent);
conn.on("discoverEmbeds", discoverEmbedsEvent);
const sseOutput = await conn.promise();
if (sseOutput && isExtensionActiveCached())
await prepareStream(sseOutput.stream);
return getResult(sseOutput === "" ? null : sseOutput);
}
if (!providers) return null;
startScrape();
const providers = getProviders();
const output = await providers.runAll({
media,
events: {
@ -183,6 +188,8 @@ export function useScrape() {
discoverEmbeds: discoverEmbedsEvent,
},
});
if (output && isExtensionActiveCached())
await prepareStream(output.stream);
return getResult(output);
},
[

View File

@ -49,6 +49,7 @@ export function useSettingsState(
icon: string;
}
| undefined,
enableThumbnails: boolean,
) {
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
useDerived(proxyUrls);
@ -71,6 +72,12 @@ export function useSettingsState(
] = useDerived(deviceName);
const [profileState, setProfileState, resetProfile, profileChanged] =
useDerived(profile);
const [
enableThumbnailsState,
setEnableThumbnailsState,
resetEnableThumbnails,
enableThumbnailsChanged,
] = useDerived(enableThumbnails);
function reset() {
resetTheme();
@ -80,6 +87,7 @@ export function useSettingsState(
resetBackendUrl();
resetDeviceName();
resetProfile();
resetEnableThumbnails();
}
const changed =
@ -89,7 +97,8 @@ export function useSettingsState(
deviceNameChanged ||
backendUrlChanged ||
proxyUrlsChanged ||
profileChanged;
profileChanged ||
enableThumbnailsChanged;
return {
reset,
@ -129,5 +138,10 @@ export function useSettingsState(
set: setProfileState,
changed: profileChanged,
},
enableThumbnails: {
state: enableThumbnailsState,
set: setEnableThumbnailsState,
changed: enableThumbnailsChanged,
},
};
}

View File

@ -1,6 +1,12 @@
import { RunOutput } from "@movie-web/providers";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Navigate,
useLocation,
useNavigate,
useParams,
} from "react-router-dom";
import { useAsync } from "react-use";
import { usePlayer } from "@/components/player/hooks/usePlayer";
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
@ -15,9 +21,10 @@ import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart";
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
import { useLastNonPlayerLink } from "@/stores/history";
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
import { needsOnboarding } from "@/utils/onboarding";
import { parseTimestamp } from "@/utils/timestamp";
export function PlayerView() {
export function RealPlayerView() {
const navigate = useNavigate();
const params = useParams<{
media: string;
@ -109,4 +116,25 @@ export function PlayerView() {
);
}
export function PlayerView() {
const loc = useLocation();
const { loading, error, value } = useAsync(() => {
return needsOnboarding();
});
if (error) throw new Error("Failed to detect onboarding");
if (loading) return null;
if (value)
return (
<Navigate
replace
to={{
pathname: "/onboarding",
search: `redirect=${encodeURIComponent(loc.pathname)}`,
}}
/>
);
return <RealPlayerView />;
}
export default PlayerView;

View File

@ -31,11 +31,12 @@ import { ThemePart } from "@/pages/parts/settings/ThemePart";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { useLanguageStore } from "@/stores/language";
import { usePreferencesStore } from "@/stores/preferences";
import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme";
import { SubPageLayout } from "./layouts/SubPageLayout";
import { LocalePart } from "./parts/settings/LocalePart";
import { PreferencesPart } from "./parts/settings/PreferencesPart";
function SettingsLayout(props: { children: React.ReactNode }) {
const { isMobile } = useIsMobile();
@ -115,6 +116,9 @@ export function SettingsPage() {
const backendUrlSetting = useAuthStore((s) => s.backendUrl);
const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails);
const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails);
const account = useAuthStore((s) => s.account);
const updateProfile = useAuthStore((s) => s.setAccountProfile);
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
@ -136,6 +140,7 @@ export function SettingsPage() {
proxySet,
backendUrlSetting,
account?.profile,
enableThumbnails,
);
const saveChanges = useCallback(async () => {
@ -168,6 +173,7 @@ export function SettingsPage() {
}
}
setEnableThumbnails(state.enableThumbnails.state);
setAppLanguage(state.appLanguage.state);
setTheme(state.theme.state);
setSubStyling(state.subtitleStyling.state);
@ -186,6 +192,7 @@ export function SettingsPage() {
state,
account,
backendUrl,
setEnableThumbnails,
setAppLanguage,
setTheme,
setSubStyling,
@ -225,10 +232,12 @@ export function SettingsPage() {
<RegisterCalloutPart />
)}
</div>
<div id="settings-locale" className="mt-48">
<LocalePart
<div id="settings-preferences" className="mt-48">
<PreferencesPart
language={state.appLanguage.state}
setLanguage={state.appLanguage.set}
enableThumbnails={state.enableThumbnails.state}
setEnableThumbnails={state.enableThumbnails.set}
/>
</div>
<div id="settings-appearance" className="mt-48">

View File

@ -0,0 +1,28 @@
import { Link } from "react-router-dom";
import { BrandPill } from "@/components/layout/BrandPill";
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
export function MinimalPageLayout(props: { children: React.ReactNode }) {
return (
<div
className="bg-background-main min-h-screen"
style={{
backgroundImage:
"linear-gradient(to bottom, var(--tw-gradient-from), var(--tw-gradient-to) 800px)",
}}
>
<BlurEllipsis />
{/* Main page */}
<div className="fixed px-7 py-5 left-0 top-0">
<Link
className="block tabbable rounded-full text-xs ssm:text-base"
to="/"
>
<BrandPill clickable />
</Link>
</div>
<div className="min-h-screen">{props.children}</div>
</div>
);
}

View File

@ -0,0 +1,102 @@
import classNames from "classnames";
import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Stepper } from "@/components/layout/Stepper";
import { CenterContainer } from "@/components/layout/ThinContainer";
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
import { Heading1, Heading2, Paragraph } from "@/components/utils/Text";
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
import {
useNavigateOnboarding,
useRedirectBack,
} from "@/pages/onboarding/onboardingHooks";
import { Card, CardContent, Link } from "@/pages/onboarding/utils";
import { PageTitle } from "@/pages/parts/util/PageTitle";
function VerticalLine(props: { className?: string }) {
return (
<div className={classNames("w-full grid justify-center", props.className)}>
<div className="w-px h-10 bg-onboarding-divider" />
</div>
);
}
export function OnboardingPage() {
const navigate = useNavigateOnboarding();
const skipModal = useModal("skip");
const { completeAndRedirect } = useRedirectBack();
const { t } = useTranslation();
return (
<MinimalPageLayout>
<PageTitle subpage k="global.pages.onboarding" />
<Modal id={skipModal.id}>
<ModalCard>
<Heading1 className="!mt-0 !mb-4 !text-2xl">
{t("onboarding.defaultConfirm.title")}
</Heading1>
<Paragraph className="!mt-1 !mb-12">
{t("onboarding.defaultConfirm.description")}
</Paragraph>
<div className="flex items-end justify-between">
<Button theme="secondary" onClick={skipModal.hide}>
{t("onboarding.defaultConfirm.cancel")}
</Button>
<Button theme="purple" onClick={() => completeAndRedirect()}>
{t("onboarding.defaultConfirm.confirm")}
</Button>
</div>
</ModalCard>
</Modal>
<CenterContainer>
<Stepper steps={2} current={1} className="mb-12" />
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
{t("onboarding.start.title")}
</Heading2>
<Paragraph className="max-w-[320px]">
{t("onboarding.start.explainer")}
</Paragraph>
<div className="w-full grid grid-cols-[1fr,auto,1fr] gap-3">
<Card onClick={() => navigate("/onboarding/proxy")}>
<CardContent
colorClass="!text-onboarding-good"
title={t("onboarding.start.options.proxy.title")}
subtitle={t("onboarding.start.options.proxy.quality")}
description={t("onboarding.start.options.proxy.description")}
>
<Link>{t("onboarding.start.options.proxy.action")}</Link>
</CardContent>
</Card>
<div className="grid grid-rows-[1fr,auto,1fr] justify-center gap-4">
<VerticalLine className="items-end" />
<span className="text-xs uppercase font-bold">or</span>
<VerticalLine />
</div>
<Card onClick={() => navigate("/onboarding/extension")}>
<CardContent
colorClass="!text-onboarding-best"
title={t("onboarding.start.options.extension.title")}
subtitle={t("onboarding.start.options.extension.quality")}
description={t("onboarding.start.options.extension.description")}
>
<Link>{t("onboarding.start.options.extension.action")}</Link>
</CardContent>
</Card>
</div>
<p className="text-center mt-12">
<Trans i18nKey="onboarding.start.options.default.text">
<br />
<a
onClick={skipModal.show}
type="button"
className="text-onboarding-link hover:opacity-75 cursor-pointer"
/>
</Trans>
</p>
</CenterContainer>
</MinimalPageLayout>
);
}

View File

@ -0,0 +1,155 @@
import { ReactNode } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useAsyncFn, useInterval } from "react-use";
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility";
import { extensionInfo, sendPage } from "@/backend/extension/messaging";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading";
import { Stepper } from "@/components/layout/Stepper";
import { CenterContainer } from "@/components/layout/ThinContainer";
import { Heading2, Paragraph } from "@/components/utils/Text";
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
import {
useNavigateOnboarding,
useRedirectBack,
} from "@/pages/onboarding/onboardingHooks";
import { Card, Link } from "@/pages/onboarding/utils";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { conf } from "@/setup/config";
type ExtensionStatus =
| "unknown"
| "failed"
| "disallowed"
| "noperms"
| "outdated"
| "success";
async function getExtensionState(): Promise<ExtensionStatus> {
const info = await extensionInfo();
if (!info) return "unknown"; // cant talk to extension
if (!info.success) return "failed"; // extension failed to respond
if (!info.allowed) return "disallowed"; // extension is not enabled on this page
if (!info.hasPermission) return "noperms"; // extension has no perms to do it's tasks
if (!isAllowedExtensionVersion(info.version)) return "outdated"; // extension is too old
return "success"; // no problems
}
export function ExtensionStatus(props: {
status: ExtensionStatus;
loading: boolean;
}) {
const { t } = useTranslation();
let content: ReactNode = null;
if (props.loading || props.status === "unknown")
content = (
<>
<Loading />
<p>{t("onboarding.extension.status.loading")}</p>
</>
);
if (props.status === "disallowed" || props.status === "noperms")
content = (
<>
<p>{t("onboarding.extension.status.disallowed")}</p>
<Button
onClick={() => {
sendPage({
page: "PermissionGrant",
redirectUrl: window.location.href,
});
}}
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
{t("onboarding.extension.status.disallowedAction")}
</Button>
</>
);
else if (props.status === "failed")
content = <p>{t("onboarding.extension.status.failed")}</p>;
else if (props.status === "outdated")
content = <p>{t("onboarding.extension.status.outdated")}</p>;
else if (props.status === "success")
content = (
<p className="flex items-center">
<Icon icon={Icons.CHECKMARK} className="text-type-success mr-4" />
{t("onboarding.extension.status.success")}
</p>
);
return (
<>
<Card>
<div className="flex py-6 flex-col space-y-2 items-center justify-center">
{content}
</div>
</Card>
<Card className="mt-4">
<div className="flex items-center space-x-7">
<Icon icon={Icons.WARNING} className="text-type-danger text-2xl" />
<p className="flex-1">
<Trans
i18nKey="onboarding.extension.extensionHelp"
components={{
bold: <span className="text-white" />,
}}
/>
</p>
</div>
</Card>
</>
);
}
export function OnboardingExtensionPage() {
const { t } = useTranslation();
const navigate = useNavigateOnboarding();
const { completeAndRedirect } = useRedirectBack();
const installLink = conf().ONBOARDING_EXTENSION_INSTALL_LINK;
const [{ loading, value }, exec] = useAsyncFn(
async (triggeredManually: boolean = false) => {
const status = await getExtensionState();
if (status === "success" && triggeredManually) completeAndRedirect();
return status;
},
[completeAndRedirect],
);
useInterval(exec, 1000);
return (
<MinimalPageLayout>
<PageTitle subpage k="global.pages.onboarding" />
<CenterContainer>
<Stepper steps={2} current={2} className="mb-12" />
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
{t("onboarding.extension.title")}
</Heading2>
<Paragraph className="max-w-[320px] mb-4">
{t("onboarding.extension.explainer")}
</Paragraph>
{installLink ? (
<Link href={installLink} target="_blank" className="mb-12">
{t("onboarding.extension.link")}
</Link>
) : null}
<ExtensionStatus status={value ?? "unknown"} loading={loading} />
<div className="flex justify-between items-center mt-8">
<Button onClick={() => navigate("/onboarding")} theme="secondary">
{t("onboarding.extension.back")}
</Button>
{value === "success" ? (
<Button onClick={() => exec(true)} theme="purple">
{t("onboarding.extension.submit")}
</Button>
) : null}
</div>
</CenterContainer>
</MinimalPageLayout>
);
}

View File

@ -0,0 +1,85 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use";
import { singularProxiedFetch } from "@/backend/helpers/fetch";
import { Button } from "@/components/buttons/Button";
import { Stepper } from "@/components/layout/Stepper";
import { CenterContainer } from "@/components/layout/ThinContainer";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { Divider } from "@/components/utils/Divider";
import { ErrorLine } from "@/components/utils/ErrorLine";
import { Heading2, Paragraph } from "@/components/utils/Text";
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
import {
useNavigateOnboarding,
useRedirectBack,
} from "@/pages/onboarding/onboardingHooks";
import { Link } from "@/pages/onboarding/utils";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
const testUrl = "https://postman-echo.com/get";
export function OnboardingProxyPage() {
const { t } = useTranslation();
const navigate = useNavigateOnboarding();
const { completeAndRedirect } = useRedirectBack();
const [url, setUrl] = useState("");
const setProxySet = useAuthStore((s) => s.setProxySet);
const installLink = conf().ONBOARDING_PROXY_INSTALL_LINK;
const [{ loading, error }, test] = useAsyncFn(async () => {
if (!url.startsWith("http"))
throw new Error("onboarding.proxy.input.errorInvalidUrl");
try {
const res = await singularProxiedFetch(url, testUrl, {});
if (res.url !== testUrl)
throw new Error("onboarding.proxy.input.errorNotProxy");
setProxySet([url]);
completeAndRedirect();
} catch (e) {
throw new Error("onboarding.proxy.input.errorConnection");
}
}, [url, completeAndRedirect, setProxySet]);
return (
<MinimalPageLayout>
<PageTitle subpage k="global.pages.onboarding" />
<CenterContainer>
<Stepper steps={2} current={2} className="mb-12" />
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
{t("onboarding.proxy.title")}
</Heading2>
<Paragraph className="max-w-[320px] !mb-5">
{t("onboarding.proxy.explainer")}
</Paragraph>
{installLink ? (
<Link href={installLink} target="_blank" className="mb-12">
{t("onboarding.proxy.link")}
</Link>
) : null}
<div className="w-[400px] max-w-full mt-14 mb-28">
<AuthInputBox
label={t("onboarding.proxy.input.label")}
value={url}
onChange={setUrl}
placeholder={t("onboarding.proxy.input.placeholder")}
className="mb-4"
/>
{error ? <ErrorLine>{t(error.message)}</ErrorLine> : null}
</div>
<Divider />
<div className="flex justify-between">
<Button theme="secondary" onClick={() => navigate("/onboarding")}>
{t("onboarding.proxy.back")}
</Button>
<Button theme="purple" loading={loading} onClick={test}>
{t("onboarding.proxy.submit")}
</Button>
</div>
</CenterContainer>
</MinimalPageLayout>
);
}

View File

@ -0,0 +1,37 @@
import { useCallback } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useQueryParam } from "@/hooks/useQueryParams";
import { useOnboardingStore } from "@/stores/onboarding";
export function useRedirectBack() {
const [url] = useQueryParam("redirect");
const navigate = useNavigate();
const setCompleted = useOnboardingStore((s) => s.setCompleted);
const redirectBack = useCallback(() => {
navigate(url ?? "/");
}, [navigate, url]);
const completeAndRedirect = useCallback(() => {
setCompleted(true);
redirectBack();
}, [redirectBack, setCompleted]);
return { completeAndRedirect };
}
export function useNavigateOnboarding() {
const navigate = useNavigate();
const loc = useLocation();
const nav = useCallback(
(path: string) => {
navigate({
pathname: path,
search: loc.search,
});
},
[navigate, loc],
);
return nav;
}

View File

@ -0,0 +1,91 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import { Icon, Icons } from "@/components/Icon";
import { Heading2, Heading3, Paragraph } from "@/components/utils/Text";
export function Card(props: {
children?: React.ReactNode;
className?: string;
onClick?: () => void;
}) {
return (
<div
className={classNames(
{
"bg-onboarding-card duration-300 border border-onboarding-border rounded-lg p-7":
true,
"hover:bg-onboarding-cardHover transition-colors cursor-pointer":
!!props.onClick,
},
props.className,
)}
onClick={props.onClick}
>
{props.children}
</div>
);
}
export function CardContent(props: {
title: ReactNode;
description: ReactNode;
subtitle: ReactNode;
colorClass: string;
children?: React.ReactNode;
}) {
return (
<div className="grid grid-rows-[1fr,auto] h-full">
<div>
<Icon
icon={Icons.RISING_STAR}
className={classNames("text-4xl mb-8 block", props.colorClass)}
/>
<Heading3
className={classNames(
"!mt-0 !mb-0 !text-xs uppercase",
props.colorClass,
)}
>
{props.subtitle}
</Heading3>
<Heading2 className="!mb-0 !mt-1 !text-base">{props.title}</Heading2>
<Paragraph className="max-w-[320px] !my-4">
{props.description}
</Paragraph>
</div>
<div>{props.children}</div>
</div>
);
}
export function Link(props: {
children?: React.ReactNode;
to?: string;
href?: string;
className?: string;
target?: "_blank";
}) {
const navigate = useNavigate();
return (
<a
onClick={() => {
if (props.to) navigate(props.to);
}}
href={props.href}
target={props.target}
className={classNames(
"text-onboarding-link cursor-pointer inline-flex gap-2 items-center group hover:opacity-75 transition-opacity",
props.className,
)}
rel="noreferrer"
>
{props.children}
<Icon
icon={Icons.ARROW_RIGHT}
className="group-hover:translate-x-0.5 transition-transform text-xl group-active:translate-x-0"
/>
</a>
);
}

View File

@ -5,7 +5,6 @@ import { Avatar } from "@/components/Avatar";
import { Button } from "@/components/buttons/Button";
import { ColorPicker, initialColor } from "@/components/form/ColorPicker";
import { IconPicker, initialIcon } from "@/components/form/IconPicker";
import { Icon, Icons } from "@/components/Icon";
import {
LargeCard,
LargeCardButtons,

View File

@ -3,6 +3,8 @@ import { useNavigate, useParams } from "react-router-dom";
import { useAsync } from "react-use";
import type { AsyncReturnType } from "type-fest";
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility";
import { extensionInfo, sendPage } from "@/backend/extension/messaging";
import {
fetchMetadata,
setCachedMetadata,
@ -10,6 +12,8 @@ import {
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
import { decodeTMDBId } from "@/backend/metadata/tmdb";
import { MWMediaType } from "@/backend/metadata/types/mw";
import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers";
import { getProviders } from "@/backend/providers/providers";
import { Button } from "@/components/buttons/Button";
import { Icons } from "@/components/Icon";
import { IconPill } from "@/components/layout/IconPill";
@ -18,7 +22,6 @@ import { Paragraph } from "@/components/text/Paragraph";
import { Title } from "@/components/text/Title";
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
import { conf } from "@/setup/config";
import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers";
export interface MetaPartProps {
onGetMeta?: (meta: DetailedMeta, episodeId?: string) => void;
@ -41,8 +44,17 @@ export function MetaPart(props: MetaPartProps) {
const navigate = useNavigate();
const { error, value, loading } = useAsync(async () => {
const info = await extensionInfo();
const isValidExtension =
info?.success && isAllowedExtensionVersion(info.version) && info.allowed;
if (isValidExtension) {
if (!info.hasPermission) throw new Error("extension-no-permission");
}
// use api metadata or providers metadata
const providerApiUrl = getLoadbalancedProviderApiUrl();
if (providerApiUrl) {
if (providerApiUrl && !isValidExtension) {
try {
await fetchMetadata(providerApiUrl);
} catch (err) {
@ -50,11 +62,12 @@ export function MetaPart(props: MetaPartProps) {
}
} else {
setCachedMetadata([
...providers.listSources(),
...providers.listEmbeds(),
...getProviders().listSources(),
...getProviders().listEmbeds(),
]);
}
// get media meta data
let data: ReturnType<typeof decodeTMDBId> = null;
try {
if (!params.media) throw new Error("no media params");
@ -98,16 +111,42 @@ export function MetaPart(props: MetaPartProps) {
props.onGetMeta?.(meta, epId);
}, []);
if (error && error.message === "extension-no-permission") {
return (
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.WAND}>
{t("player.metadata.extensionPermission.badge")}
</IconPill>
<Title>{t("player.metadata.extensionPermission.title")}</Title>
<Paragraph>{t("player.metadata.extensionPermission.text")}</Paragraph>
<Button
onClick={() => {
sendPage({
page: "PermissionGrant",
redirectUrl: window.location.href,
});
}}
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
{t("player.metadata.extensionPermission.button")}
</Button>
</ErrorContainer>
</ErrorLayout>
);
}
if (error && error.message === "dmca") {
return (
<ErrorLayout>
<ErrorContainer>
<IconPill icon={Icons.DRAGON}>Removed</IconPill>
<Title>Media has been removed</Title>
<Paragraph>
This media is no longer available due to a takedown notice or
copyright claim.
</Paragraph>
<IconPill icon={Icons.DRAGON}>
{t("player.metadata.dmca.badge")}
</IconPill>
<Title>{t("player.metadata.dmca.title")}</Title>
<Paragraph>{t("player.metadata.dmca.text")}</Paragraph>
<Button
href="/"
theme="purple"

View File

@ -1,13 +1,15 @@
import { Dispatch, SetStateAction, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
import { MwLink } from "@/components/text/Link";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { Divider } from "@/components/utils/Divider";
import { Heading1 } from "@/components/utils/Text";
import { SetupPart } from "@/pages/parts/settings/SetupPart";
interface ProxyEditProps {
proxyUrls: string[] | null;
@ -52,7 +54,11 @@ function ProxyEdit({ proxyUrls, setProxyUrls }: ProxyEditProps) {
{t("settings.connections.workers.label")}
</p>
<p className="max-w-[20rem] font-medium">
{t("settings.connections.workers.description")}
<Trans i18nKey="settings.connections.workers.description">
<MwLink to="https://docs.movie-web.app/proxy/deploy">
Proxy documentation
</MwLink>
</Trans>
</p>
</div>
<div>
@ -118,7 +124,11 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) {
{t("settings.connections.server.label")}
</p>
<p className="max-w-[20rem] font-medium">
{t("settings.connections.server.description")}
<Trans i18nKey="settings.connections.server.description">
<MwLink to="https://docs.movie-web.app/backend/deploy">
Backend documentation
</MwLink>
</Trans>
</p>
</div>
<div>
@ -147,6 +157,7 @@ export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
<div>
<Heading1 border>{t("settings.connections.title")}</Heading1>
<div className="space-y-6">
<SetupPart />
<ProxyEdit
proxyUrls={props.proxyUrls}
setProxyUrls={props.setProxyUrls}

View File

@ -1,44 +0,0 @@
import { useTranslation } from "react-i18next";
import { FlagIcon } from "@/components/FlagIcon";
import { Dropdown } from "@/components/form/Dropdown";
import { Heading1 } from "@/components/utils/Text";
import { appLanguageOptions } from "@/setup/i18n";
import { getLocaleInfo, sortLangCodes } from "@/utils/language";
export function LocalePart(props: {
language: string;
setLanguage: (l: string) => void;
}) {
const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
const options = appLanguageOptions
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
.map((opt) => ({
id: opt.code,
name: `${opt.name}${opt.nativeName ? `${opt.nativeName}` : ""}`,
leftIcon: <FlagIcon langCode={opt.code} />,
}));
const selected = options.find(
(item) => item.id === getLocaleInfo(props.language)?.code,
);
return (
<div>
<Heading1 border>{t("settings.locale.title")}</Heading1>
<p className="text-white font-bold mb-3">
{t("settings.locale.language")}
</p>
<p className="max-w-[20rem] font-medium">
{t("settings.locale.languageDescription")}
</p>
<Dropdown
options={options}
selectedItem={selected || options[0]}
setSelectedItem={(opt) => props.setLanguage(opt.id)}
/>
</div>
);
}

View File

@ -0,0 +1,67 @@
import { useTranslation } from "react-i18next";
import { Toggle } from "@/components/buttons/Toggle";
import { FlagIcon } from "@/components/FlagIcon";
import { Dropdown } from "@/components/form/Dropdown";
import { Heading1 } from "@/components/utils/Text";
import { appLanguageOptions } from "@/setup/i18n";
import { getLocaleInfo, sortLangCodes } from "@/utils/language";
export function PreferencesPart(props: {
language: string;
setLanguage: (l: string) => void;
enableThumbnails: boolean;
setEnableThumbnails: (v: boolean) => void;
}) {
const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
const options = appLanguageOptions
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
.map((opt) => ({
id: opt.code,
name: `${opt.name}${opt.nativeName ? `${opt.nativeName}` : ""}`,
leftIcon: <FlagIcon langCode={opt.code} />,
}));
const selected = options.find(
(item) => item.id === getLocaleInfo(props.language)?.code,
);
return (
<div className="space-y-12">
<Heading1 border>{t("settings.preferences.title")}</Heading1>
<div>
<p className="text-white font-bold mb-3">
{t("settings.preferences.language")}
</p>
<p className="max-w-[20rem] font-medium">
{t("settings.preferences.languageDescription")}
</p>
<Dropdown
options={options}
selectedItem={selected || options[0]}
setSelectedItem={(opt) => props.setLanguage(opt.id)}
/>
</div>
<div>
<p className="text-white font-bold mb-3">
{t("settings.preferences.thumbnail")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.preferences.thumbnailDescription")}
</p>
<div
onClick={() => props.setEnableThumbnails(!props.enableThumbnails)}
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
>
<Toggle enabled={props.enableThumbnails} />
<p className="flex-1 text-white font-bold">
{t("settings.preferences.thumbnailLabel")}
</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,204 @@
import classNames from "classnames";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAsync } from "react-use";
import { isExtensionActive } from "@/backend/extension/messaging";
import { singularProxiedFetch } from "@/backend/helpers/fetch";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading";
import { SettingsCard } from "@/components/layout/SettingsCard";
import {
StatusCircle,
StatusCircleProps,
} from "@/components/player/internals/StatusCircle";
import { Heading3 } from "@/components/utils/Text";
import { useAuthStore } from "@/stores/auth";
const testUrl = "https://postman-echo.com/get";
type Status = "success" | "unset" | "error";
type SetupData = {
extension: Status;
proxy: Status;
defaultProxy: Status;
};
function testProxy(url: string) {
return new Promise<void>((resolve, reject) => {
setTimeout(() => reject(new Error("Timed out!")), 1000);
singularProxiedFetch(url, testUrl, {})
.then((res) => {
if (res.url !== testUrl) return reject(new Error("Not a proxy"));
resolve();
})
.catch(reject);
});
}
function useIsSetup() {
const proxyUrls = useAuthStore((s) => s.proxySet);
const { loading, value } = useAsync(async (): Promise<SetupData> => {
const extensionStatus: Status = (await isExtensionActive())
? "success"
: "unset";
let proxyStatus: Status = "unset";
if (proxyUrls && proxyUrls.length > 0) {
try {
await testProxy(proxyUrls[0]);
proxyStatus = "success";
} catch {
proxyStatus = "error";
}
}
return {
extension: extensionStatus,
proxy: proxyStatus,
defaultProxy: "success",
};
}, [proxyUrls]);
let globalState: Status = "unset";
if (value?.extension === "success" || value?.proxy === "success")
globalState = "success";
if (value?.proxy === "error" || value?.extension === "error")
globalState = "error";
return {
setupStates: value,
globalState,
loading,
};
}
function SetupCheckList(props: {
status: Status;
grey?: boolean;
highlight?: boolean;
children?: ReactNode;
}) {
const { t } = useTranslation();
const statusMap: Record<Status, StatusCircleProps["type"]> = {
error: "error",
success: "success",
unset: "noresult",
};
return (
<div className="flex items-start text-type-dimmed my-4">
<StatusCircle
type={statusMap[props.status]}
className={classNames({
"!text-video-scraping-noresult !bg-video-scraping-noresult opacity-50":
props.grey,
"scale-90 mr-3": true,
})}
/>
<div>
<p
className={classNames({
"!text-white": props.grey && props.highlight,
"!text-type-dimmed opacity-75": props.grey && !props.highlight,
"text-type-danger": props.status === "error",
"text-white": props.status === "success",
})}
>
{props.children}
</p>
{props.status === "error" ? (
<p className="max-w-96">
{t("settings.connections.setup.itemError")}
</p>
) : null}
</div>
</div>
);
}
export function SetupPart() {
const { t } = useTranslation();
const navigate = useNavigate();
const { loading, setupStates, globalState } = useIsSetup();
if (loading || !setupStates) {
return (
<SettingsCard>
<div className="flex py-6 items-center justify-center">
<Loading />
</div>
</SettingsCard>
);
}
const textLookupMap: Record<
Status,
{ title: string; desc: string; button: string }
> = {
error: {
title: "settings.connections.setup.errorStatus.title",
desc: "settings.connections.setup.errorStatus.description",
button: "settings.connections.setup.redoSetup",
},
success: {
title: "settings.connections.setup.successStatus.title",
desc: "settings.connections.setup.successStatus.description",
button: "settings.connections.setup.redoSetup",
},
unset: {
title: "settings.connections.setup.unsetStatus.title",
desc: "settings.connections.setup.unsetStatus.description",
button: "settings.connections.setup.doSetup",
},
};
return (
<SettingsCard>
<div className="flex items-start gap-4">
<div>
<div
className={classNames({
"rounded-full h-12 w-12 flex bg-opacity-15 justify-center items-center":
true,
"text-type-success bg-type-success": globalState === "success",
"text-type-danger bg-type-danger":
globalState === "error" || globalState === "unset",
})}
>
<Icon
icon={globalState === "success" ? Icons.CHECKMARK : Icons.X}
className="text-xl"
/>
</div>
</div>
<div className="flex-1">
<Heading3 className="!mb-3">
{t(textLookupMap[globalState].title)}
</Heading3>
<p className="max-w-[20rem] font-medium mb-6">
{t(textLookupMap[globalState].desc)}
</p>
<SetupCheckList status={setupStates.extension}>
{t("settings.connections.setup.items.extension")}
</SetupCheckList>
<SetupCheckList status={setupStates.proxy}>
{t("settings.connections.setup.items.proxy")}
</SetupCheckList>
<SetupCheckList
grey
highlight={globalState === "unset"}
status={setupStates.defaultProxy}
>
{t("settings.connections.setup.items.default")}
</SetupCheckList>
</div>
<div className="mt-5">
<Button theme="purple" onClick={() => navigate("/onboarding")}>
{t(textLookupMap[globalState].button)}
</Button>
</div>
</div>
</SettingsCard>
);
}

View File

@ -44,9 +44,9 @@ export function SidebarPart() {
icon: Icons.USER,
},
{
textKey: "settings.locale.title",
id: "settings-locale",
icon: Icons.BOOKMARK,
textKey: "settings.preferences.title",
id: "settings-preferences",
icon: Icons.SETTINGS,
},
{
textKey: "settings.appearance.title",

View File

@ -19,6 +19,9 @@ import { DmcaPage, shouldHaveDmcaPage } from "@/pages/Dmca";
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
import { HomePage } from "@/pages/HomePage";
import { LoginPage } from "@/pages/Login";
import { OnboardingPage } from "@/pages/onboarding/Onboarding";
import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension";
import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy";
import { RegisterPage } from "@/pages/Register";
import { Layout } from "@/setup/Layout";
import { useHistoryListener } from "@/stores/history";
@ -119,6 +122,12 @@ function App() {
<Route path="/register" element={<RegisterPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/onboarding" element={<OnboardingPage />} />
<Route
path="/onboarding/extension"
element={<OnboardingExtensionPage />}
/>
<Route path="/onboarding/proxy" element={<OnboardingProxyPage />} />
{shouldHaveDmcaPage() ? (
<Route path="/dmca" element={<DmcaPage />} />

View File

@ -19,6 +19,9 @@ interface Config {
DISALLOWED_IDS: string;
TURNSTILE_KEY: string;
CDN_REPLACEMENTS: string;
HAS_ONBOARDING: string;
ONBOARDING_EXTENSION_INSTALL_LINK: string;
ONBOARDING_PROXY_INSTALL_LINK: string;
}
export interface RuntimeConfig {
@ -34,6 +37,9 @@ export interface RuntimeConfig {
DISALLOWED_IDS: string[];
TURNSTILE_KEY: string | null;
CDN_REPLACEMENTS: Array<string[]>;
HAS_ONBOARDING: boolean;
ONBOARDING_EXTENSION_INSTALL_LINK: string | null;
ONBOARDING_PROXY_INSTALL_LINK: string | null;
}
const env: Record<keyof Config, undefined | string> = {
@ -42,6 +48,10 @@ const env: Record<keyof Config, undefined | string> = {
GITHUB_LINK: undefined,
DONATION_LINK: undefined,
DISCORD_LINK: undefined,
ONBOARDING_EXTENSION_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_EXTENSION_INSTALL_LINK,
ONBOARDING_PROXY_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_PROXY_INSTALL_LINK,
DMCA_EMAIL: import.meta.env.VITE_DMCA_EMAIL,
CORS_PROXY_URL: import.meta.env.VITE_CORS_PROXY_URL,
NORMAL_ROUTER: import.meta.env.VITE_NORMAL_ROUTER,
@ -49,6 +59,7 @@ const env: Record<keyof Config, undefined | string> = {
DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS,
TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY,
CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS,
HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING,
};
// loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js)
@ -69,6 +80,8 @@ function getKey(key: keyof Config, defaultString?: string): string {
export function conf(): RuntimeConfig {
const dmcaEmail = getKey("DMCA_EMAIL");
const extensionLink = getKey("ONBOARDING_EXTENSION_INSTALL_LINK");
const proxyInstallLink = getKey("ONBOARDING_PROXY_INSTALL_LINK");
const turnstileKey = getKey("TURNSTILE_KEY");
return {
APP_VERSION,
@ -76,12 +89,17 @@ export function conf(): RuntimeConfig {
DONATION_LINK,
DISCORD_LINK,
DMCA_EMAIL: dmcaEmail.length > 0 ? dmcaEmail : null,
ONBOARDING_EXTENSION_INSTALL_LINK:
extensionLink.length > 0 ? extensionLink : null,
ONBOARDING_PROXY_INSTALL_LINK:
proxyInstallLink.length > 0 ? proxyInstallLink : null,
BACKEND_URL: getKey("BACKEND_URL", BACKEND_URL),
TMDB_READ_API_KEY: getKey("TMDB_READ_API_KEY"),
PROXY_URLS: getKey("CORS_PROXY_URL")
.split(",")
.map((v) => v.trim()),
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
HAS_ONBOARDING: getKey("HAS_ONBOARDING", "false") === "true",
TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null,
DISALLOWED_IDS: getKey("DISALLOWED_IDS", "")
.split(",")

View File

@ -46,7 +46,8 @@ export function useLastNonPlayerLink() {
(v) =>
!v.path.startsWith("/media") && // cannot be a player link
location.pathname !== v.path && // cannot be current link
!v.path.startsWith("/s/"), // cannot be a quick search link
!v.path.startsWith("/s/") && // cannot be a quick search link
!v.path.startsWith("/onboarding"), // cannot be an onboarding link
);
return route?.path ?? "/";
}, [routes, location]);

View File

@ -0,0 +1,22 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
export interface OnboardingStore {
completed: boolean;
setCompleted(v: boolean): void;
}
export const useOnboardingStore = create(
persist(
immer<OnboardingStore>((set) => ({
completed: false,
setCompleted(v) {
set((s) => {
s.completed = v;
});
},
})),
{ name: "__MW::onboarding" },
),
);

View File

@ -42,12 +42,14 @@ export interface PlayerMeta {
}
export interface Caption {
id: string;
language: string;
url?: string;
srtData: string;
}
export interface CaptionListItem {
id: string;
language: string;
url: string;
needsProxy: boolean;
@ -116,6 +118,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
},
setSourceId(id) {
set((s) => {
s.status = playerStatus.PLAYING;
s.sourceId = id;
});
},
@ -153,6 +156,8 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
s.qualities = qualities as SourceQuality[];
s.currentQuality = loadableStream.quality;
s.captionList = captions;
s.interface.error = undefined;
s.status = playerStatus.PLAYING;
});
const store = get();
store.redisplaySource(startAt);
@ -166,7 +171,10 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
automaticQuality: qualityPreferences.quality.automaticQuality,
lastChosenQuality: quality,
});
set((s) => {
s.interface.error = undefined;
s.status = playerStatus.PLAYING;
});
store.display?.load({
source: loadableStream.stream,
startAt,
@ -182,6 +190,8 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
if (!selectedQuality) return;
set((s) => {
s.currentQuality = quality;
s.status = playerStatus.PLAYING;
s.interface.error = undefined;
});
store.display?.load({
source: selectedQuality,

View File

@ -1,4 +1,4 @@
import { Qualities } from "@movie-web/providers";
import { Qualities, Stream } from "@movie-web/providers";
import { QualityStore } from "@/stores/quality";
@ -14,16 +14,19 @@ export type SourceFileStream = {
export type LoadableSource = {
type: StreamType;
url: string;
preferredHeaders?: Stream["preferredHeaders"];
};
export type SourceSliceSource =
| {
type: "file";
qualities: Partial<Record<SourceQuality, SourceFileStream>>;
preferredHeaders?: Stream["preferredHeaders"];
}
| {
type: "hls";
url: string;
preferredHeaders?: Stream["preferredHeaders"];
};
const qualitySorting: Record<SourceQuality, number> = {

View File

@ -0,0 +1,24 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
export interface PreferencesStore {
enableThumbnails: boolean;
setEnableThumbnails(v: boolean): void;
}
export const usePreferencesStore = create(
persist(
immer<PreferencesStore>((set) => ({
enableThumbnails: false,
setEnableThumbnails(v) {
set((s) => {
s.enableThumbnails = v;
});
},
})),
{
name: "__MW::preferences",
},
),
);

View File

@ -86,7 +86,7 @@ function populateLanguageCode(language: string): string {
* @returns pretty format for language, null if it no info can be found for language
*/
export function getPrettyLanguageNameFromLocale(locale: string): string | null {
const tag = getTag(populateLanguageCode(locale), true);
const tag = getTag(locale, true);
const lang = tag?.language?.Description?.[0] ?? null;
if (!lang) return null;

23
src/utils/onboarding.ts Normal file
View File

@ -0,0 +1,23 @@
import { isExtensionActive } from "@/backend/extension/messaging";
import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
import { useOnboardingStore } from "@/stores/onboarding";
export async function needsOnboarding(): Promise<boolean> {
// if onboarding is dislabed, no onboarding needed
if (!conf().HAS_ONBOARDING) return false;
// if extension is active and working, no onboarding needed
const extensionActive = await isExtensionActive();
if (extensionActive) return false;
// if there is any custom proxy urls, no onboarding needed
const proxyUrls = useAuthStore.getState().proxySet;
if (proxyUrls) return false;
// if onboarding has been completed, no onboarding needed
const completed = useOnboardingStore.getState().completed;
if (completed) return false;
return true;
}

View File

@ -137,6 +137,11 @@ export const defaultTheme = {
accentA: tokens.purple.c500,
accentB: tokens.blue.c500,
},
// Modals
modal: {
background: tokens.shade.c800,
},
// typography
type: {
@ -147,6 +152,7 @@ export const defaultTheme = {
divider: tokens.ash.c500,
secondary: tokens.ash.c100,
danger: tokens.semantic.red.c100,
success: tokens.semantic.green.c100,
link: tokens.purple.c100,
linkHover: tokens.purple.c50,
},
@ -228,10 +234,24 @@ export const defaultTheme = {
}
},
// Utilities
utils: {
divider: tokens.ash.c300,
},
// Onboarding
onboarding: {
bar: tokens.shade.c400,
barFilled: tokens.purple.c300,
divider: tokens.shade.c200,
card: tokens.shade.c800,
cardHover: tokens.shade.c700,
border: tokens.shade.c600,
good: tokens.purple.c100,
best: tokens.semantic.yellow.c100,
link: tokens.purple.c100,
},
// Error page
errors: {
card: tokens.shade.c800,

View File

@ -95,6 +95,10 @@ export default createTheme({
accentB: tokens.blue.c500
},
modal: {
background: tokens.shade.c800,
},
type: {
logo: tokens.purple.c100,
text: tokens.shade.c50,

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