diff --git a/.github/workflows/deploying.yml b/.github/workflows/deploying.yml index 9c642cf6..e07a60a5 100644 --- a/.github/workflows/deploying.yml +++ b/.github/workflows/deploying.yml @@ -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 diff --git a/.github/workflows/linting_testing.yml b/.github/workflows/linting_testing.yml index 4b2c2caa..c782bcb1 100644 --- a/.github/workflows/linting_testing.yml +++ b/.github/workflows/linting_testing.yml @@ -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: . diff --git a/dockerfile b/Dockerfile similarity index 92% rename from dockerfile rename to Dockerfile index 5dd5465b..cc9b84be 100644 --- a/dockerfile +++ b/Dockerfile @@ -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" diff --git a/package.json b/package.json index 8620c18b..131fbfa2 100644 --- a/package.json +++ b/package.json @@ -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" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 459ee71c..a0e60397 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==} diff --git a/src/assets/languages.ts b/src/assets/languages.ts index 53765046..3f047fca 100644 --- a/src/assets/languages.ts +++ b/src/assets/languages.ts @@ -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, diff --git a/src/assets/locales/ar.json b/src/assets/locales/ar.json index 0fc20948..6138644c 100644 --- a/src/assets/locales/ar.json +++ b/src/assets/locales/ar.json @@ -394,11 +394,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "لغة التطبيق", - "languageDescription": "اللغة المطبقة على كامل التطبيق.", - "title": "اللغة" - }, "reset": "إعادة تعيين", "save": "حفظ", "sidebar": { diff --git a/src/assets/locales/bn.json b/src/assets/locales/bn.json index 719493ee..1a7e2c03 100644 --- a/src/assets/locales/bn.json +++ b/src/assets/locales/bn.json @@ -181,14 +181,43 @@ "disclaimer": "ডাউনলোড সরাসরি প্রদানকারী থেকে নেওয়া হয়. কিভাবে ডাউনলোড দেওয়া হয় তার উপর মুভি-ওয়েবের নিয়ন্ত্রণ নেই।", "downloadPlaylist": "প্লেলিস্ট ডাউনলোড করুন", "downloadSubtitle": "বর্তমান সাবটাইটেল ডাউনলোড করুন", - "downloadVideo": "ভিডিও ডাউনলোড" + "downloadVideo": "ভিডিও ডাউনলোড", + "hlsDisclaimer": "ডাউনলোড সরাসরি প্রদানকারী থেকে নেওয়া হয়. কিভাবে ডাউনলোড দেওয়া হয় তার উপর মুভি-ওয়েবের নিয়ন্ত্রণ নেই। অনুগ্রহ করে মনে রাখবেন যে আপনি একটি HLS প্লেলিস্ট ডাউনলোড করছেন, এটি উন্নত মাল্টিমিডিয়া স্ট্রিমিংয়ের সাথে পরিচিত ব্যবহারকারীদের জন্য।", + "onAndroid": { + "1": "অ্যান্ড্রয়েডে ডাউনলোড করতে, ডাউনলোড বোতামে ক্লিক করুন তারপর, নতুন পৃষ্ঠায়, ভিডিওতে ট্যাপ করুন এবং ধরে রাখুন, তারপরে সংরক্ষণ করুন নির্বাচন করুন।", + "shortTitle": "ডাউনলোড/অ্যান্ড্রয়েড", + "title": "অ্যান্ড্রয়েডে ডাউনলোড হচ্ছে" + }, + "onIos": { + "1": "iOS-এ ডাউনলোড করতে, ডাউনলোড বোতামে ক্লিক করুন তারপর, নতুন পৃষ্ঠায়, -এ ক্লিক করুন, তারপর ফাইলে সংরক্ষণ করুন ।", + "shortTitle": "ডাউনলোড / iOS", + "title": "iOS এ ডাউনলোড হচ্ছে" + }, + "onPc": { + "1": "পিসিতে, ডাউনলোড বোতামে ক্লিক করুন তারপর, নতুন পৃষ্ঠায়, ভিডিওটিতে ডান ক্লিক করুন এবং ভিডিওটিকে এই হিসাবে সংরক্ষণ করুন নির্বাচন করুন", + "shortTitle": "ডাউনলোড/পিসি", + "title": "পিসিতে ডাউনলোড হচ্ছে" + }, + "title": "ডাউনলোড করুন" }, "episodes": { "button": "পর্বগুলি", "emptyState": "এই মরসুমে কোন পর্ব নেই, পরে আবার চেক করুন!", + "episodeBadge": "E{{episode}}", "loadingError": "ঋতু লোড করার সময় ত্রুটি৷", "loadingList": "লোড হচ্ছে..।", - "loadingTitle": "লোড হচ্ছে..।" + "loadingTitle": "লোড হচ্ছে..।", + "unairedEpisodes": "এই সিজনের এক বা একাধিক পর্ব অক্ষম করা হয়েছে কারণ সেগুলি এখনও সম্প্রচার করা হয়নি।" + }, + "playback": { + "speedLabel": "প্লেব্যাক গতি", + "title": "প্লেব্যাক সেটিংস" + }, + "quality": { + "automaticLabel": "স্বয়ংক্রিয় গুণমান", + "hint": "আপনি বিভিন্ন গুণমানের বিকল্প পেতে <0>উৎস পরিবর্তন করে দেখতে পারেন।", + "iosNoQuality": "অ্যাপল-সংজ্ঞায়িত সীমাবদ্ধতার কারণে, এই উৎসের জন্য গুণমান নির্বাচন iOS-এ উপলব্ধ নয়। বিভিন্ন মানের বিকল্প পেতে আপনি <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": "আপনার মানবতা যাচাই করা হচ্ছে..।" } } } diff --git a/src/assets/locales/ca.json b/src/assets/locales/ca.json index 7485a93d..f54e8948 100644 --- a/src/assets/locales/ca.json +++ b/src/assets/locales/ca.json @@ -18,7 +18,7 @@ }, "actions": { "copied": "S'ha copiat", - "copy": "Cipia" + "copy": "Copia" }, "auth": { "createAccount": "Encara no teniu un compte? <0>Creeu un compte.", @@ -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.", "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.", "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ó", diff --git a/src/assets/locales/cs.json b/src/assets/locales/cs.json index 1a6ea086..cdd13ba3 100644 --- a/src/assets/locales/cs.json +++ b/src/assets/locales/cs.json @@ -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. Otevřete rozšíření pomocí nabídky rozšíření ve vašem prohlížeči 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í" + }, + "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.

Vezměte prosím na vědomí, že stahujete HLS playlist, který není doporučen stahovat pokud nejste obeznámeni s pokročilým streamováním médií. 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 klepněte a podržte na videu a poté vyberte uložit.", "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.", "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.", "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", diff --git a/src/assets/locales/de.json b/src/assets/locales/de.json index e5152a44..ec1de41b 100644 --- a/src/assets/locales/de.json +++ b/src/assets/locales/de.json @@ -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.", "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.", "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": { diff --git a/src/assets/locales/el.json b/src/assets/locales/el.json index 062ac419..3793e316 100644 --- a/src/assets/locales/el.json +++ b/src/assets/locales/el.json @@ -404,11 +404,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "Γλώσσα εφαρμογής", - "languageDescription": "Γλώσσα που εφαρμόζεται σε ολόκληρη την εφαρμογή.", - "title": "Τοποθεσία" - }, "reset": "Επαναφορά", "save": "Αποθήκευση", "sidebar": { diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 9329ec0e..15038d6f 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -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.

Please note that you are downloading an HLS playlist, it is not recommended to download if you are not familiar with advanced streaming formats. Try different sources for different formats.", "onAndroid": { "1": "To download on Android, click the download button then, on the new page, tap and hold on the video, then select save.", "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.", "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.", "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" + } + } + }, + "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. Open the extension through your browsers extension menu 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" + } } } diff --git a/src/assets/locales/es.json b/src/assets/locales/es.json index f051ceb0..075dd58a 100644 --- a/src/assets/locales/es.json +++ b/src/assets/locales/es.json @@ -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.", "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.", "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": { diff --git a/src/assets/locales/et.json b/src/assets/locales/et.json index 0663d58d..48280d66 100644 --- a/src/assets/locales/et.json +++ b/src/assets/locales/et.json @@ -404,11 +404,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "Rakenduse keel", - "languageDescription": "Keel on rakendatud kogu rakendusele.", - "title": "Lokaal" - }, "reset": "Lähtesta", "save": "Salvesta", "sidebar": { diff --git a/src/assets/locales/fa.json b/src/assets/locales/fa.json index e68517d2..fc9b3906 100644 --- a/src/assets/locales/fa.json +++ b/src/assets/locales/fa.json @@ -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>دستورالعمل ها.", "label": "سرور سفارشی", "urlLabel": "لینک سرور سفارشی" }, "title": "اتصالات", "workers": { "addButton": "اضافه کردن worker جدید", - "description": "برای کار کردن برنامه، تمام ترافیک از طریق پروکسی ها هدایت می شود. این کار را انجام دهید اگر می خواهید از worker های خود استفاده کنید.", + "description": "برای ایجاد عملکرد برنامه، تمام ترافیک از طریق پروکسی ها هدایت می شود. اگر میخواهید این کار انجام دهید حتما از worker های خودتان استفاده کنید. <0>دستورالعمل ها.", "emptyState": "هنوز هیچ worker ای وجود ندارد، یکی اضافه کنید", "label": "استفاده از worker های پروکسی سفارشی", "urlLabel": "لینک worker ها", "urlPlaceholder": "https://" } }, - "locale": { - "language": "زبان", - "languageDescription": "زبانی در کل برنامه اعمال می‌شود.", - "title": "زبان" - }, "reset": "بازنشانی", "save": "ذخیره", "sidebar": { diff --git a/src/assets/locales/fr.json b/src/assets/locales/fr.json index 7c80dc24..9b321529 100644 --- a/src/assets/locales/fr.json +++ b/src/assets/locales/fr.json @@ -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. Ouvrez l'extension via le menu des extensions de votre navigateur 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" + }, + "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.

Veuillez noter que vous téléchargez une liste de lecture HLS, il n'est pas recommandé de télécharger si vous n'êtes pas familier avec les formats de streaming avancés. . 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, tapez et maintenez sur la vidéo, puis sélectionnez enregistrer.", "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.", "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.", "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 à l’ensemble de l’application.", + "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", diff --git a/src/assets/locales/gl.json b/src/assets/locales/gl.json index 63fe31fa..37218b8b 100644 --- a/src/assets/locales/gl.json +++ b/src/assets/locales/gl.json @@ -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.", "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.", "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": { diff --git a/src/assets/locales/gu.json b/src/assets/locales/gu.json index b1a857ea..03974f70 100644 --- a/src/assets/locales/gu.json +++ b/src/assets/locales/gu.json @@ -404,11 +404,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "એપ્લિકેશન ભાષા", - "languageDescription": "સમગ્ર એપ્લિકેશન પર લાગુ ભાષા.", - "title": "સ્થળ" - }, "reset": "રીસેટ કરો", "save": "સાચવો", "sidebar": { diff --git a/src/assets/locales/he.json b/src/assets/locales/he.json index 10cef7af..a0ce15ce 100644 --- a/src/assets/locales/he.json +++ b/src/assets/locales/he.json @@ -404,11 +404,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "שפת האפליקציה", - "languageDescription": "השפה החלה על האפליקציה כולה.", - "title": "מקומי" - }, "reset": "איפוס", "save": "לשמור", "sidebar": { diff --git a/src/assets/locales/hi.json b/src/assets/locales/hi.json index 7ff5f443..944538c2 100644 --- a/src/assets/locales/hi.json +++ b/src/assets/locales/hi.json @@ -404,11 +404,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "अनुप्रयोग भाषा", - "languageDescription": "भाषा संपूर्ण अनुप्रयोग पर लागू होती है।", - "title": "स्थानीय" - }, "reset": "रीसेट", "save": "सेव", "sidebar": { diff --git a/src/assets/locales/id.json b/src/assets/locales/id.json index 08e19bfd..e542c475 100644 --- a/src/assets/locales/id.json +++ b/src/assets/locales/id.json @@ -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": { diff --git a/src/assets/locales/it.json b/src/assets/locales/it.json index 5ec260ec..0074098f 100644 --- a/src/assets/locales/it.json +++ b/src/assets/locales/it.json @@ -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.", "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.", "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": { diff --git a/src/assets/locales/ko.json b/src/assets/locales/ko.json index 7d3fd3f9..459ad986 100644 --- a/src/assets/locales/ko.json +++ b/src/assets/locales/ko.json @@ -378,11 +378,6 @@ "urlLabel": "워커 URL" } }, - "locale": { - "language": "애플리케이션 언어", - "languageDescription": "전체 애플리케이션에 적용되는 언어입니다.", - "title": "지" - }, "reset": "초기화", "save": "저장", "sidebar": { diff --git a/src/assets/locales/lv.json b/src/assets/locales/lv.json index de2ad471..0d5b3c99 100644 --- a/src/assets/locales/lv.json +++ b/src/assets/locales/lv.json @@ -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": { diff --git a/src/assets/locales/minion.json b/src/assets/locales/minion.json index b3f6cf4d..d69836ff 100644 --- a/src/assets/locales/minion.json +++ b/src/assets/locales/minion.json @@ -404,11 +404,6 @@ "urlPlaceholder": "banana://" } }, - "locale": { - "language": "Banana", - "languageDescription": "Banana applied to the entire banana.", - "title": "Banana" - }, "reset": "Banana", "save": "Banana", "sidebar": { diff --git a/src/assets/locales/ne.json b/src/assets/locales/ne.json index e06617e9..5046e60e 100644 --- a/src/assets/locales/ne.json +++ b/src/assets/locales/ne.json @@ -390,25 +390,20 @@ }, "connections": { "server": { - "description": "यदि तपाईं आफ्नो डेटा भण्डारण गर्न अनुकूलन ब्याकइन्डमा जडान गर्न चाहनुहुन्छ भने, यसलाई सक्षम गर्नुहोस् र URL प्रदान गर्नुहोस्।", + "description": "यदि तपाईं आफ्नो डेटा भण्डारण गर्न अनुकूलन ब्याकइन्डमा जडान गर्न चाहनुहुन्छ भने, यसलाई सक्षम गर्नुहोस् र URL प्रदान गर्नुहोस्। <0>निर्देशनहरू।", "label": "अनुकूलन सर्भर", "urlLabel": "अनुकूलन सर्भर URL" }, "title": "संबन्धहरु", "workers": { "addButton": "नया worker हरु हाल्नुहोस", - "description": "एप्लिकेसन प्रकार्य बनाउनको लागि, सबै ट्राफिक प्रोक्सीहरू मार्फत रूट गरिएको छ। यदि तपाईं आफ्नो कामदारहरू ल्याउन चाहनुहुन्छ भने यसलाई सक्षम गर्नुहोस्।", + "description": "एप्लिकेसन प्रकार्य बनाउनको लागि, सबै ट्राफिक प्रोक्सीहरू मार्फत रूट गरिएको छ। यदि तपाईं आफ्नो कामदारहरू ल्याउन चाहनुहुन्छ भने यसलाई सक्षम गर्नुहोस्। <0>निर्देशनहरू।", "emptyState": "अहिलेसम्म worker हरु छैनन्, तल एउटा थप्नुहोस्", "label": "आफ्नै proxy workers हरु चलाउनुहोस्", "urlLabel": "Worker URL हरु", "urlPlaceholder": "https://" } }, - "locale": { - "language": "एपको भाषा", - "languageDescription": "सम्पूर्ण अनुप्रयोगमा भाषा लागू गरियो।", - "title": "भाषा" - }, "reset": "रिसेट गर्नुहोस्", "save": "सेभ गर्नुहोस्", "sidebar": { diff --git a/src/assets/locales/nl.json b/src/assets/locales/nl.json index 6cc614dd..28f2a20e 100644 --- a/src/assets/locales/nl.json +++ b/src/assets/locales/nl.json @@ -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.", "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.", "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": { diff --git a/src/assets/locales/pirate.json b/src/assets/locales/pirate.json index a42e8398..c6868b03 100644 --- a/src/assets/locales/pirate.json +++ b/src/assets/locales/pirate.json @@ -375,11 +375,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "Application language", - "languageDescription": "Language applied to the entire application.", - "title": "Locale" - }, "reset": "Reset", "save": "Save", "sidebar": { diff --git a/src/assets/locales/pl.json b/src/assets/locales/pl.json index dd347592..312bfd07 100644 --- a/src/assets/locales/pl.json +++ b/src/assets/locales/pl.json @@ -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": { diff --git a/src/assets/locales/pt-BR.json b/src/assets/locales/pt-BR.json index 38de1037..ec76f7f6 100644 --- a/src/assets/locales/pt-BR.json +++ b/src/assets/locales/pt-BR.json @@ -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.", "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.", "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": { diff --git a/src/assets/locales/pt-PT.json b/src/assets/locales/pt-PT.json new file mode 100644 index 00000000..d5288b0a --- /dev/null +++ b/src/assets/locales/pt-PT.json @@ -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.", + "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.", + "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}} - 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, toque e segure no vídeo, depois selecione guardar.", + "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 , depois em Guardar no Ficheiro .", + "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 Guardar vídeo como", + "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 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 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" + } +} diff --git a/src/assets/locales/ro.json b/src/assets/locales/ro.json index 6944648e..961526f6 100644 --- a/src/assets/locales/ro.json +++ b/src/assets/locales/ro.json @@ -404,11 +404,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "Limba aplicației", - "languageDescription": "Limbajul aplicat întregii aplicații.", - "title": "Local" - }, "reset": "Resetare", "save": "Salvează", "sidebar": { diff --git a/src/assets/locales/ru.json b/src/assets/locales/ru.json index babbb343..c547bba2 100644 --- a/src/assets/locales/ru.json +++ b/src/assets/locales/ru.json @@ -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 пользователя" } }, diff --git a/src/assets/locales/sl.json b/src/assets/locales/sl.json index 1bc86e22..2c2f6657 100644 --- a/src/assets/locales/sl.json +++ b/src/assets/locales/sl.json @@ -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": { diff --git a/src/assets/locales/sv.json b/src/assets/locales/sv.json index f5cc4f50..94263b93 100644 --- a/src/assets/locales/sv.json +++ b/src/assets/locales/sv.json @@ -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": { diff --git a/src/assets/locales/th.json b/src/assets/locales/th.json index 79b31e30..7f0f4943 100644 --- a/src/assets/locales/th.json +++ b/src/assets/locales/th.json @@ -389,11 +389,6 @@ "urlPlaceholder": "https://" } }, - "locale": { - "language": "ภาษา", - "languageDescription": "ภาษาที่ใช้กับแอปพลิเคชันทั้งหมด", - "title": "ตำแหน่งที่ตั้ง" - }, "reset": "เริ่มใหม่", "save": "บันทึก", "sidebar": { diff --git a/src/assets/locales/tok.json b/src/assets/locales/tok.json index 50d2d862..5fa05c9a 100644 --- a/src/assets/locales/tok.json +++ b/src/assets/locales/tok.json @@ -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": { diff --git a/src/assets/locales/tr.json b/src/assets/locales/tr.json index 7ea3ec30..dd87cadb 100644 --- a/src/assets/locales/tr.json +++ b/src/assets/locales/tr.json @@ -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.", "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.", "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" }, diff --git a/src/assets/locales/uk.json b/src/assets/locales/uk.json index 240a57ab..5124e55d 100644 --- a/src/assets/locales/uk.json +++ b/src/assets/locales/uk.json @@ -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": "Якщо ви встановили розширення, але воно не виявлено. Відкрийте розширення в меню розширень вашого браузеру і дотримуйтеся вказівок на екрані.", + "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>використовувати налаштування за замовчуванням" + }, + "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 немає контролю над тим, як надаються завантаження.

Зверніть увагу, що ви завантажуєте список відтворення HLS, його не рекомендується завантажувати, якщо ви не знайомі з розширеними форматами потокового передавання. Спробуйте різні джерела для інших форматів.", "onAndroid": { "1": "Щоб завантажити на Android, натисніть кнопку завантаження, потім на новій сторінці торкніться й утримуйте відео, а потім виберіть зберегти.", "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>Інструкції.", "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>Інструкція.", "emptyState": "Немає працівників", "label": "Використовувати власних проксі-працівників", "urlLabel": "URL-у працівника", "urlPlaceholder": "https://" } }, - "locale": { - "language": "Мова застосунку", - "languageDescription": "Виберіть мову, яку ви хочете використовувати.", - "title": "Налаштування локації" + "preferences": { + "language": "Мова додатку", + "languageDescription": "Мова застосована до всього додатку.", + "thumbnail": "Створити мініатюри", + "thumbnailDescription": "Часто відео не мають мініатюр. Ви можете активувати цей параметр для їх генерації під час відтворення, але це може уповільнити відтворення відео.", + "thumbnailLabel": "Сгенерувати мініатюри", + "title": "Параметри" }, "reset": "Скинути налаштування", "save": "Зберегти", diff --git a/src/assets/locales/vi.json b/src/assets/locales/vi.json index 3af12198..098a100d 100644 --- a/src/assets/locales/vi.json +++ b/src/assets/locales/vi.json @@ -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}} - 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": { diff --git a/src/assets/locales/zh.json b/src/assets/locales/zh.json index 0f915770..03fe3347 100644 --- a/src/assets/locales/zh.json +++ b/src/assets/locales/zh.json @@ -390,25 +390,20 @@ }, "connections": { "server": { - "description": "若您想连接到自定义后端保存数据,请启用此选项并提供 URL。", + "description": "若您想连接到自定义后端保存数据,请启用此选项并提供 URL。 <0>查看指引。", "label": "自定义服务器", "urlLabel": "自定义服务器 URL" }, "title": "连接", "workers": { "addButton": "添加新的 Worker", - "description": "要让应用程序正常运作,所有流量会通过代理路由。若您想使用自己的 Worker,请启用该选项。", + "description": "要让应用程序正常运作,所有流量会通过代理路由。若您想使用自己的 Worker,请启用该选项。 <0>查看指引。", "emptyState": "还没有 Worker,在下方添加一个", "label": "使用自定义代理 Worker", "urlLabel": "Worker URL", "urlPlaceholder": "https://" } }, - "locale": { - "language": "应用程序语言", - "languageDescription": "当前已应用到整个应用程序的语言。", - "title": "本地化" - }, "reset": "重设", "save": "保存", "sidebar": { diff --git a/src/backend/extension/compatibility.ts b/src/backend/extension/compatibility.ts new file mode 100644 index 00000000..53ffd7a1 --- /dev/null +++ b/src/backend/extension/compatibility.ts @@ -0,0 +1,7 @@ +import { satisfies } from "semver"; + +const allowedExtensionRange = "~1.0.0"; + +export function isAllowedExtensionVersion(version: string): boolean { + return satisfies(version, allowedExtensionRange); +} diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts new file mode 100644 index 00000000..1f2a2b00 --- /dev/null +++ b/src/backend/extension/messaging.ts @@ -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( + message: MessageKey, + payload: MessagesMetadata[MessageKey]["req"] | undefined = undefined, + timeout: number = -1, +) { + return new Promise((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( + ops: MessagesMetadata["makeRequest"]["req"], +): Promise | null> { + return sendMessage("makeRequest", ops); +} + +export async function setDomainRule( + ops: MessagesMetadata["prepareStream"]["req"], +): Promise { + return sendMessage("prepareStream", ops); +} + +export async function sendPage( + ops: MessagesMetadata["openPage"]["req"], +): Promise { + 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 { + const info = await extensionInfo(); + if (!info?.success) return false; + const allowedVersion = isAllowedExtensionVersion(info.version); + if (!allowedVersion) return false; + return info.allowed && info.hasPermission; +} diff --git a/src/backend/extension/plasmo.ts b/src/backend/extension/plasmo.ts new file mode 100644 index 00000000..c13898be --- /dev/null +++ b/src/backend/extension/plasmo.ts @@ -0,0 +1,68 @@ +export interface ExtensionBaseRequest {} + +export type ExtensionBaseResponse = + | ({ + 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; + body?: string | FormData | URLSearchParams | Record; +} + +export type ExtensionMakeRequestResponse = ExtensionBaseResponse<{ + response: { + statusCode: number; + headers: Record; + finalUrl: string; + body: T; + }; +}>; + +export interface ExtensionPrepareStreamRequest extends ExtensionBaseRequest { + ruleId: number; + targetDomains: string[]; + requestHeaders?: Record; + responseHeaders?: Record; +} + +export interface MmMetadata { + hello: { + req: ExtensionBaseRequest; + res: ExtensionHelloResponse; + }; + makeRequest: { + req: ExtensionMakeRequest; + res: ExtensionMakeRequestResponse; + }; + 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 {} +} diff --git a/src/backend/extension/streams.ts b/src/backend/extension/streams.ts new file mode 100644 index 00000000..588718af --- /dev/null +++ b/src/backend/extension/streams.ts @@ -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 { + const headers: Record = {}; + 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), + }); +} diff --git a/src/backend/helpers/fetch.ts b/src/backend/helpers/fetch.ts index cc3e735e..f9aa145a 100644 --- a/src/backend/helpers/fetch.ts +++ b/src/backend/helpers/fetch.ts @@ -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 = Parameters>; type R = ReturnType>; diff --git a/src/utils/providers.ts b/src/backend/providers/fetchers.ts similarity index 58% rename from src/utils/providers.ts rename to src/backend/providers/fetchers.ts index e5c8503c..95267595 100644 --- a/src/utils/providers.ts +++ b/src/backend/providers/fetchers.ts @@ -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, +): 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({ + 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; +} diff --git a/src/backend/providers/providers.ts b/src/backend/providers/providers.ts new file mode 100644 index 00000000..ac4a7dfa --- /dev/null +++ b/src/backend/providers/providers.ts @@ -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, + }); +} diff --git a/src/components/LinksDropdown.tsx b/src/components/LinksDropdown.tsx index 90be9a17..c0da66ee 100644 --- a/src/components/LinksDropdown.tsx +++ b/src/components/LinksDropdown.tsx @@ -109,7 +109,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) { return (
evt.key === "Enter" && toggleOpen()} diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx index ed0eebf8..70e2c780 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -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) diff --git a/src/components/buttons/IconPatch.tsx b/src/components/buttons/IconPatch.tsx index 302d14b2..b9a7e525 100644 --- a/src/components/buttons/IconPatch.tsx +++ b/src/components/buttons/IconPatch.tsx @@ -25,7 +25,7 @@ export function IconPatch(props: IconPatchProps) { return (
diff --git a/src/components/buttons/Toggle.tsx b/src/components/buttons/Toggle.tsx index 3fcb0071..8cedc245 100644 --- a/src/components/buttons/Toggle.tsx +++ b/src/components/buttons/Toggle.tsx @@ -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 (
- disable()} selected={!lang}> + disable()} selected={!selectedCaptionId}> {t("player.menus.subtitles.offChoice")} diff --git a/src/components/player/atoms/settings/Downloads.tsx b/src/components/player/atoms/settings/Downloads.tsx index 9dfa8309..bba9cf20 100644 --- a/src/components/player/atoms/settings/Downloads.tsx +++ b/src/components/player/atoms/settings/Downloads.tsx @@ -27,6 +27,7 @@ function StyleTrans(props: { k: string }) { i18nKey={props.k} components={{ bold: , + br:
, ios_share: ( ), @@ -123,24 +124,6 @@ export function DownloadView({ id }: { id: string }) { ); } -export function CantDownloadView({ id }: { id: string }) { - const router = useOverlayRouter(id); - const { t } = useTranslation(); - - return ( - <> - router.navigate("/")}> - {t("player.menus.downloads.title")} - - - - - - - - ); -} - function AndroidExplanationView({ id }: { id: string }) { const router = useOverlayRouter(id); const { t } = useTranslation(); @@ -202,11 +185,6 @@ export function DownloadRoutes({ id }: { id: string }) { - - - - - diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx index 03d0875d..f995308e 100644 --- a/src/components/player/atoms/settings/SourceSelectingView.tsx +++ b/src/components/player/atoms/settings/SourceSelectingView.tsx @@ -147,7 +147,7 @@ export function SourceSelectionView({ router.navigate("/")}> {t("player.menus.sources.title")} - + {sources.map((v) => ( hlsLevelToQuality(v)) diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts index 604bdeca..8ba8480a 100644 --- a/src/components/player/display/displayInterface.ts +++ b/src/components/player/display/displayInterface.ts @@ -41,6 +41,7 @@ export interface DisplayMeta { } export interface DisplayCaption { + id: string; srtData: string; language: string; url?: string; diff --git a/src/components/player/hooks/useCaptions.ts b/src/components/player/hooks/useCaptions.ts index 9f96c41c..458c704a 100644 --- a/src/components/player/hooks/useCaptions.ts +++ b/src/components/player/hooks/useCaptions.ts @@ -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, }; } diff --git a/src/components/player/hooks/useSourceSelection.ts b/src/components/player/hooks/useSourceSelection.ts index e28507cf..3813fa9f 100644 --- a/src/components/player/hooks/useSourceSelection.ts +++ b/src/components/player/hooks/useSourceSelection.ts @@ -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), diff --git a/src/components/player/internals/ScrapeCard.tsx b/src/components/player/internals/ScrapeCard.tsx index 479bf2b1..dc057901 100644 --- a/src/components/player/internals/ScrapeCard.tsx +++ b/src/components/player/internals/ScrapeCard.tsx @@ -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> = { pending: "player.scraping.items.pending", }; -const statusMap: Record = { - failure: "error", - notfound: "noresult", - pending: "loading", - success: "success", - waiting: "waiting", -}; +const statusMap: Record = + { + failure: "error", + notfound: "noresult", + pending: "loading", + success: "success", + waiting: "waiting", + }; export function ScrapeItem(props: ScrapeItemProps) { const { t } = useTranslation(); diff --git a/src/components/player/internals/StatusCircle.tsx b/src/components/player/internals/StatusCircle.tsx index 32855321..ede7392e 100644 --- a/src/components/player/internals/StatusCircle.tsx +++ b/src/components/player/internals/StatusCircle.tsx @@ -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 (
diff --git a/src/components/player/internals/ThumbnailScraper.tsx b/src/components/player/internals/ThumbnailScraper.tsx index b0fc8474..cede84c8 100644 --- a/src/components/player/internals/ThumbnailScraper.tsx +++ b/src/components/player/internals/ThumbnailScraper.tsx @@ -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(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; } diff --git a/src/components/player/utils/captions.ts b/src/components/player/utils/captions.ts index abccee9f..bc2079db 100644 --- a/src/components/player/utils/captions.ts +++ b/src/components/player/utils/captions.ts @@ -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, diff --git a/src/components/player/utils/convertRunoutputToSource.ts b/src/components/player/utils/convertRunoutputToSource.ts index fba59e63..f54c5396 100644 --- a/src/components/player/utils/convertRunoutputToSource.ts +++ b/src/components/player/utils/convertRunoutputToSource.ts @@ -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"); diff --git a/src/components/text-inputs/AuthInputBox.tsx b/src/components/text-inputs/AuthInputBox.tsx index c79c079d..2e34594a 100644 --- a/src/components/text-inputs/AuthInputBox.tsx +++ b/src/components/text-inputs/AuthInputBox.tsx @@ -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 ( -
+
{props.label ? (

{props.label}

) : null} diff --git a/src/components/utils/ErrorLine.tsx b/src/components/utils/ErrorLine.tsx new file mode 100644 index 00000000..b0761fee --- /dev/null +++ b/src/components/utils/ErrorLine.tsx @@ -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 ( +

+ + {props.children} +

+ ); +} diff --git a/src/components/utils/Flare.tsx b/src/components/utils/Flare.tsx index 1fc20825..9c19e027 100644 --- a/src/components/utils/Flare.tsx +++ b/src/components/utils/Flare.tsx @@ -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) {
{ + return needsOnboarding(); + }); + + if (error) throw new Error("Failed to detect onboarding"); + if (loading) return null; + if (value) + return ( + + ); + return ; +} + export default PlayerView; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 0fb27d85..18b755ca 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -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() { )}
-
- +
diff --git a/src/pages/layouts/MinimalPageLayout.tsx b/src/pages/layouts/MinimalPageLayout.tsx new file mode 100644 index 00000000..6d2cf34d --- /dev/null +++ b/src/pages/layouts/MinimalPageLayout.tsx @@ -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 ( +
+ + {/* Main page */} +
+ + + +
+
{props.children}
+
+ ); +} diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx new file mode 100644 index 00000000..525ed7df --- /dev/null +++ b/src/pages/onboarding/Onboarding.tsx @@ -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 ( +
+
+
+ ); +} + +export function OnboardingPage() { + const navigate = useNavigateOnboarding(); + const skipModal = useModal("skip"); + const { completeAndRedirect } = useRedirectBack(); + const { t } = useTranslation(); + + return ( + + + + + + {t("onboarding.defaultConfirm.title")} + + + {t("onboarding.defaultConfirm.description")} + +
+ + +
+
+
+ + + + {t("onboarding.start.title")} + + + {t("onboarding.start.explainer")} + + +
+ navigate("/onboarding/proxy")}> + + {t("onboarding.start.options.proxy.action")} + + +
+ + or + +
+ navigate("/onboarding/extension")}> + + {t("onboarding.start.options.extension.action")} + + +
+ +

+ +
+ +
+

+
+
+ ); +} diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx new file mode 100644 index 00000000..3c17e3b8 --- /dev/null +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -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 { + 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 = ( + <> + +

{t("onboarding.extension.status.loading")}

+ + ); + if (props.status === "disallowed" || props.status === "noperms") + content = ( + <> +

{t("onboarding.extension.status.disallowed")}

+ + + ); + else if (props.status === "failed") + content =

{t("onboarding.extension.status.failed")}

; + else if (props.status === "outdated") + content =

{t("onboarding.extension.status.outdated")}

; + else if (props.status === "success") + content = ( +

+ + {t("onboarding.extension.status.success")} +

+ ); + return ( + <> + +
+ {content} +
+
+ +
+ +

+ , + }} + /> +

+
+
+ + ); +} + +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 ( + + + + + + {t("onboarding.extension.title")} + + + {t("onboarding.extension.explainer")} + + {installLink ? ( + + {t("onboarding.extension.link")} + + ) : null} + + +
+ + {value === "success" ? ( + + ) : null} +
+
+
+ ); +} diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx new file mode 100644 index 00000000..ae76b9aa --- /dev/null +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -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 ( + + + + + + {t("onboarding.proxy.title")} + + + {t("onboarding.proxy.explainer")} + + {installLink ? ( + + {t("onboarding.proxy.link")} + + ) : null} +
+ + {error ? {t(error.message)} : null} +
+ +
+ + +
+
+
+ ); +} diff --git a/src/pages/onboarding/onboardingHooks.ts b/src/pages/onboarding/onboardingHooks.ts new file mode 100644 index 00000000..cccf8825 --- /dev/null +++ b/src/pages/onboarding/onboardingHooks.ts @@ -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; +} diff --git a/src/pages/onboarding/utils.tsx b/src/pages/onboarding/utils.tsx new file mode 100644 index 00000000..3a57952b --- /dev/null +++ b/src/pages/onboarding/utils.tsx @@ -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 ( +
+ {props.children} +
+ ); +} + +export function CardContent(props: { + title: ReactNode; + description: ReactNode; + subtitle: ReactNode; + colorClass: string; + children?: React.ReactNode; +}) { + return ( +
+
+ + + {props.subtitle} + + {props.title} + + {props.description} + +
+
{props.children}
+
+ ); +} + +export function Link(props: { + children?: React.ReactNode; + to?: string; + href?: string; + className?: string; + target?: "_blank"; +}) { + const navigate = useNavigate(); + return ( +
{ + 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} + + + ); +} diff --git a/src/pages/parts/auth/AccountCreatePart.tsx b/src/pages/parts/auth/AccountCreatePart.tsx index 674a3be6..6f8b9ad4 100644 --- a/src/pages/parts/auth/AccountCreatePart.tsx +++ b/src/pages/parts/auth/AccountCreatePart.tsx @@ -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, diff --git a/src/pages/parts/player/MetaPart.tsx b/src/pages/parts/player/MetaPart.tsx index 4930fffb..1b84b7bf 100644 --- a/src/pages/parts/player/MetaPart.tsx +++ b/src/pages/parts/player/MetaPart.tsx @@ -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 = 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 ( + + + + {t("player.metadata.extensionPermission.badge")} + + {t("player.metadata.extensionPermission.title")} + {t("player.metadata.extensionPermission.text")} + + + + ); + } + if (error && error.message === "dmca") { return ( - Removed - Media has been removed - - This media is no longer available due to a takedown notice or - copyright claim. - + + {t("player.metadata.dmca.badge")} + + {t("player.metadata.dmca.title")} + {t("player.metadata.dmca.text")}
@@ -118,7 +124,11 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) { {t("settings.connections.server.label")}

- {t("settings.connections.server.description")} + + + Backend documentation + +

@@ -147,6 +157,7 @@ export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
{t("settings.connections.title")}
+ 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: , - })); - - const selected = options.find( - (item) => item.id === getLocaleInfo(props.language)?.code, - ); - - return ( -
- {t("settings.locale.title")} -

- {t("settings.locale.language")} -

-

- {t("settings.locale.languageDescription")} -

- props.setLanguage(opt.id)} - /> -
- ); -} diff --git a/src/pages/parts/settings/PreferencesPart.tsx b/src/pages/parts/settings/PreferencesPart.tsx new file mode 100644 index 00000000..2c1c5f37 --- /dev/null +++ b/src/pages/parts/settings/PreferencesPart.tsx @@ -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: , + })); + + const selected = options.find( + (item) => item.id === getLocaleInfo(props.language)?.code, + ); + + return ( +
+ {t("settings.preferences.title")} +
+

+ {t("settings.preferences.language")} +

+

+ {t("settings.preferences.languageDescription")} +

+ props.setLanguage(opt.id)} + /> +
+ +
+

+ {t("settings.preferences.thumbnail")} +

+

+ {t("settings.preferences.thumbnailDescription")} +

+
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" + > + +

+ {t("settings.preferences.thumbnailLabel")} +

+
+
+
+ ); +} diff --git a/src/pages/parts/settings/SetupPart.tsx b/src/pages/parts/settings/SetupPart.tsx new file mode 100644 index 00000000..d188c759 --- /dev/null +++ b/src/pages/parts/settings/SetupPart.tsx @@ -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((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 => { + 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 = { + error: "error", + success: "success", + unset: "noresult", + }; + + return ( +
+ +
+

+ {props.children} +

+ {props.status === "error" ? ( +

+ {t("settings.connections.setup.itemError")} +

+ ) : null} +
+
+ ); +} + +export function SetupPart() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { loading, setupStates, globalState } = useIsSetup(); + if (loading || !setupStates) { + return ( + +
+ +
+
+ ); + } + + 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 ( + +
+
+
+ +
+
+
+ + {t(textLookupMap[globalState].title)} + +

+ {t(textLookupMap[globalState].desc)} +

+ + {t("settings.connections.setup.items.extension")} + + + {t("settings.connections.setup.items.proxy")} + + + {t("settings.connections.setup.items.default")} + +
+
+ +
+
+
+ ); +} diff --git a/src/pages/parts/settings/SidebarPart.tsx b/src/pages/parts/settings/SidebarPart.tsx index 2b6e5c3f..47469a5e 100644 --- a/src/pages/parts/settings/SidebarPart.tsx +++ b/src/pages/parts/settings/SidebarPart.tsx @@ -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", diff --git a/src/setup/App.tsx b/src/setup/App.tsx index afa5f8ba..37c55e0f 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -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() { } /> } /> } /> + } /> + } + /> + } /> {shouldHaveDmcaPage() ? ( } /> diff --git a/src/setup/config.ts b/src/setup/config.ts index 2e1634a4..ef04a786 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -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; + HAS_ONBOARDING: boolean; + ONBOARDING_EXTENSION_INSTALL_LINK: string | null; + ONBOARDING_PROXY_INSTALL_LINK: string | null; } const env: Record = { @@ -42,6 +48,10 @@ const env: Record = { 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 = { 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(",") diff --git a/src/stores/history/index.ts b/src/stores/history/index.ts index c90a7309..c1f507ee 100644 --- a/src/stores/history/index.ts +++ b/src/stores/history/index.ts @@ -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]); diff --git a/src/stores/onboarding/index.tsx b/src/stores/onboarding/index.tsx new file mode 100644 index 00000000..be5af563 --- /dev/null +++ b/src/stores/onboarding/index.tsx @@ -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((set) => ({ + completed: false, + setCompleted(v) { + set((s) => { + s.completed = v; + }); + }, + })), + { name: "__MW::onboarding" }, + ), +); diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index 56e84f74..0312b8b0 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -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 = (set, get) => ({ }, setSourceId(id) { set((s) => { + s.status = playerStatus.PLAYING; s.sourceId = id; }); }, @@ -153,6 +156,8 @@ export const createSourceSlice: MakeSlice = (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 = (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 = (set, get) => ({ if (!selectedQuality) return; set((s) => { s.currentQuality = quality; + s.status = playerStatus.PLAYING; + s.interface.error = undefined; }); store.display?.load({ source: selectedQuality, diff --git a/src/stores/player/utils/qualities.ts b/src/stores/player/utils/qualities.ts index dbd84b5c..e5140d53 100644 --- a/src/stores/player/utils/qualities.ts +++ b/src/stores/player/utils/qualities.ts @@ -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>; + preferredHeaders?: Stream["preferredHeaders"]; } | { type: "hls"; url: string; + preferredHeaders?: Stream["preferredHeaders"]; }; const qualitySorting: Record = { diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx new file mode 100644 index 00000000..65fae22d --- /dev/null +++ b/src/stores/preferences/index.tsx @@ -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((set) => ({ + enableThumbnails: false, + setEnableThumbnails(v) { + set((s) => { + s.enableThumbnails = v; + }); + }, + })), + { + name: "__MW::preferences", + }, + ), +); diff --git a/src/utils/language.ts b/src/utils/language.ts index 6fda7df8..41b8168b 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -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; diff --git a/src/utils/onboarding.ts b/src/utils/onboarding.ts new file mode 100644 index 00000000..c2678b1c --- /dev/null +++ b/src/utils/onboarding.ts @@ -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 { + // 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; +} diff --git a/themes/default.ts b/themes/default.ts index 7a010338..bd31b3ff 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -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, diff --git a/themes/list/blue.ts b/themes/list/blue.ts index e5b73409..e10592dc 100644 --- a/themes/list/blue.ts +++ b/themes/list/blue.ts @@ -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, diff --git a/themes/list/gray.ts b/themes/list/gray.ts index a0b9b742..c0c434e8 100644 --- a/themes/list/gray.ts +++ b/themes/list/gray.ts @@ -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, diff --git a/themes/list/red.ts b/themes/list/red.ts index 89614632..b42b935f 100644 --- a/themes/list/red.ts +++ b/themes/list/red.ts @@ -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, diff --git a/themes/list/teal.ts b/themes/list/teal.ts index cbae4748..742f4a32 100644 --- a/themes/list/teal.ts +++ b/themes/list/teal.ts @@ -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,