diff --git a/.github/workflows/auto_assign.yml b/.github/workflows/auto_assign.yml index 87e9c8f37..0c53dd401 100644 --- a/.github/workflows/auto_assign.yml +++ b/.github/workflows/auto_assign.yml @@ -13,8 +13,8 @@ jobs: steps: # Auto assign PR to author - name: Auto Assign PR to Author - if: github.event_name == 'pull_request' - uses: actions/github-script@v6 + if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -31,9 +31,10 @@ jobs: # Dynamic labeling based on PR/Issue title - name: Label PRs and Issues + if: github.actor != 'dependabot[bot]' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 973b6a625..a20479339 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup node uses: actions/setup-node@v4 with: diff --git a/.github/workflows/pages_cleanup.yml b/.github/workflows/pages_cleanup.yml new file mode 100644 index 000000000..58ba548a7 --- /dev/null +++ b/.github/workflows/pages_cleanup.yml @@ -0,0 +1,36 @@ +name: GitHub Pages Cleanup + +on: + schedule: + - cron: '0 0 * * 0' + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Delete directories older than 1 year + run: | + for dir in $(find . -mindepth 1 -maxdepth 2 -type d -not -path '*/\.*'); do + if ! git log -1 --since="1 year ago" -- "$dir" | grep -q .; then + echo "Deleting $dir" + rm -rf "$dir" + fi + done + + - name: Commit and push + run: | + git config --global user.name 'GitHub Pages Cleanup' + git config --global user.email 'actions@stremio.com' + git add -A + git diff --cached --quiet || git commit -m "cleanup" + git push origin gh-pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c7d99554..5c27de22d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install NPM dependencies run: npm install - name: Build @@ -19,16 +19,10 @@ jobs: - name: Zip build artifact run: zip -r stremio-web.zip ./build - name: Upload build artifact to GitHub release assets - uses: svenstaro/upload-release-action@2.9.0 + uses: svenstaro/upload-release-action@2.11.2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: stremio-web.zip asset_name: stremio-web.zip tag: ${{ github.ref }} - overwrite: true - - name: Upload build artifact to Netlify - run: | - curl -H "Content-Type: application/zip" \ - -H "Authorization: Bearer ${{ secrets.netlify_access_token }}" \ - --data-binary "@stremio-web.zip" \ - https://api.netlify.com/api/v1/sites/stremio-development.netlify.com/deploys + overwrite: true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7c005f38a..5305b2436 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "stremio", - "version": "5.0.0-beta.23", + "version": "5.0.0-beta.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.23", + "version": "5.0.0-beta.26", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "https://stremio.github.io/stremio-core/stremio-core-web/feat/settings-gamepad-support/dev/stremio-stremio-core-web-0.49.2.tgz", - "@stremio/stremio-icons": "5.4.1", - "@stremio/stremio-video": "0.0.60", + "@stremio/stremio-icons": "5.7.1", + "@stremio/stremio-video": "0.0.61", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -36,7 +36,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#a6be0425573917c2e82b66d28968c1a4d444cb96", + "stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -48,6 +48,8 @@ "@stylistic/eslint-plugin": "^2.11.0", "@stylistic/eslint-plugin-jsx": "^2.11.0", "@types/hat": "^0.0.4", + "@types/lodash.isequal": "^4.5.8", + "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.3.13", "@types/react-dom": "^18.3.1", "babel-loader": "9.2.1", @@ -66,6 +68,7 @@ "mini-css-extract-plugin": "2.9.2", "postcss-loader": "8.1.1", "readdirp": "4.0.2", + "recast": "0.23.11", "terser-webpack-plugin": "5.3.10", "thread-loader": "^4.0.4", "ts-loader": "^9.5.1", @@ -3398,9 +3401,10 @@ "license": "MIT" }, "node_modules/@stremio/stremio-icons": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-5.4.1.tgz", - "integrity": "sha512-7g4JP7tPRT1UDZxbuH/Urq7fc6te3joy8qyx/NGWIW7wO169TTISO7ZWdejzESvUVgZ/7i6rzkRmXZ3wefWcBg==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@stremio/stremio-icons/-/stremio-icons-5.7.1.tgz", + "integrity": "sha512-Z96p36LLX3G+ewMnFKmNZVsO/AtcHA33WQ3wGOYFubxiYADPRAkcLVU5rHIfiGSC9IUaUVhxQWTPVB9ScY4Q5Q==", + "license": "MIT", "workspaces": [ "react", "react-native", @@ -3409,9 +3413,9 @@ ] }, "node_modules/@stremio/stremio-video": { - "version": "0.0.60", - "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.60.tgz", - "integrity": "sha512-RbmSi+Lk+3pb6f2ZkGVCnoMoJoujvVvSLDHiLGkXnzQwjYf2B2022NKlAQmHRuHN1sjD+VEsKD8foQH4hXGG1A==", + "version": "0.0.61", + "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.61.tgz", + "integrity": "sha512-+m3ScDmImTilcpCnY5WO091SdWuDMrW8KkUs7y+ZL6PioZXNtd8fvRsmQoHKkWkkKX3K3LNTIfA7w5unITv1jA==", "license": "MIT", "dependencies": { "buffer": "6.0.3", @@ -3806,6 +3810,33 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.18", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.18.tgz", + "integrity": "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.isequal": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", + "integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4786,6 +4817,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -12497,6 +12541,23 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -13373,9 +13434,9 @@ } }, "node_modules/stremio-translations": { - "version": "1.44.10", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a6be0425573917c2e82b66d28968c1a4d444cb96", - "integrity": "sha512-77kVE/eos/SA16kzeK7TTWmqoLF0mLPCJXjITwVIVzMHr8XyBPZFOfmiVEg4M6W1W7qYqA+dHhzicyLs7hJhlw==", + "version": "1.44.12", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#abe7684165a031755e9aee39da26daa806ba7824", + "integrity": "sha512-bMpdJTFZqgemdoOQAARMPG7XaFgeu/zW/0vHmzavTM9DYUNIGuQaTC5RbVXIIII00RLOXoGLYf+dsxRVFiS9mA==", "license": "MIT" }, "node_modules/string_decoder": { @@ -13976,6 +14037,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinycolor2": { "version": "1.6.0", "dev": true, diff --git a/package.json b/package.json index c1d7ff2c1..af16f5a7b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.23", + "version": "5.0.0-beta.26", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", @@ -10,15 +10,16 @@ "start-prod": "webpack serve --mode production", "build": "webpack --mode production", "test": "jest", - "lint": "eslint src" + "lint": "eslint src", + "scan-translations": "npx jest ./tests/i18nScan.test.js" }, "dependencies": { "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "https://stremio.github.io/stremio-core/stremio-core-web/feat/settings-gamepad-support/dev/stremio-stremio-core-web-0.49.2.tgz", - "@stremio/stremio-icons": "5.4.1", - "@stremio/stremio-video": "0.0.60", + "@stremio/stremio-icons": "5.7.1", + "@stremio/stremio-video": "0.0.61", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -40,7 +41,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#a6be0425573917c2e82b66d28968c1a4d444cb96", + "stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -52,6 +53,8 @@ "@stylistic/eslint-plugin": "^2.11.0", "@stylistic/eslint-plugin-jsx": "^2.11.0", "@types/hat": "^0.0.4", + "@types/lodash.isequal": "^4.5.8", + "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.3.13", "@types/react-dom": "^18.3.1", "babel-loader": "9.2.1", @@ -70,6 +73,7 @@ "mini-css-extract-plugin": "2.9.2", "postcss-loader": "8.1.1", "readdirp": "4.0.2", + "recast": "0.23.11", "terser-webpack-plugin": "5.3.10", "thread-loader": "^4.0.4", "ts-loader": "^9.5.1", diff --git a/src/common/Toast/ToastItem/ToastItem.js b/src/common/Toast/ToastItem/ToastItem.js index 94b5a98b4..3a317ab1b 100644 --- a/src/common/Toast/ToastItem/ToastItem.js +++ b/src/common/Toast/ToastItem/ToastItem.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -8,6 +9,7 @@ const { Button } = require('stremio/components'); const styles = require('./styles'); const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) => { + const { t } = useTranslation(); const type = React.useMemo(() => { return ['success', 'alert', 'info', 'error'].includes(props.type) ? props.type @@ -74,7 +76,7 @@ const ToastItem = ({ title, message, dataset, onSelect, onClose, ...props }) => null } - diff --git a/src/common/Toast/useToast.d.ts b/src/common/Toast/useToast.d.ts new file mode 100644 index 000000000..e74d7ade8 --- /dev/null +++ b/src/common/Toast/useToast.d.ts @@ -0,0 +1,11 @@ +type ToastOptions = { + type: string, + title: string, + timeout: number, +}; + +declare const useToast: () => { + show: (options: ToastOptions) => void, +}; + +export = useToast; diff --git a/src/common/animations.less b/src/common/animations.less index 91dbe386d..3b9815f14 100644 --- a/src/common/animations.less +++ b/src/common/animations.less @@ -82,6 +82,19 @@ transform: translateY(100%); } +.fade-enter { + opacity: 0; +} + +.fade-active { + opacity: 1; + transition: opacity 0.3s cubic-bezier(0.32, 0, 0.67, 0); +} + +.fade-exit { + opacity: 0; +} + @keyframes fade-in-no-motion { 0% { opacity: 0; diff --git a/src/common/index.js b/src/common/index.js index 55ccfe045..25df5c158 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -15,6 +15,7 @@ const routesRegexp = require('./routesRegexp'); const useAnimationFrame = require('./useAnimationFrame'); const useBinaryState = require('./useBinaryState'); const { default: useFullscreen } = require('./useFullscreen'); +const { default: useInterval } = require('./useInterval'); const useLiveRef = require('./useLiveRef'); const useModelState = require('./useModelState'); const useNotifications = require('./useNotifications'); @@ -23,9 +24,11 @@ const useProfile = require('./useProfile'); const { default: useSettings } = require('./useSettings'); const { default: useShell } = require('./useShell'); const useStreamingServer = require('./useStreamingServer'); +const { default: useTimeout } = require('./useTimeout'); const useTorrent = require('./useTorrent'); const useTranslate = require('./useTranslate'); const { default: useOrientation } = require('./useOrientation'); +const { default: useLanguageSorting } = require('./useLanguageSorting'); module.exports = { FileDropProvider, @@ -48,6 +51,7 @@ module.exports = { useAnimationFrame, useBinaryState, useFullscreen, + useInterval, useLiveRef, useModelState, useNotifications, @@ -56,7 +60,9 @@ module.exports = { useSettings, useShell, useStreamingServer, + useTimeout, useTorrent, useTranslate, useOrientation, + useLanguageSorting, }; diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index 9bd5d0fc5..69cdcd494 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -42,11 +42,21 @@ const useFullscreen = () => { }; const onKeyDown = (event: KeyboardEvent) => { + + const activeElement = document.activeElement as HTMLElement; + + const inputFocused = + activeElement && + (activeElement.tagName === 'INPUT' || + activeElement.tagName === 'TEXTAREA' || + activeElement.tagName === 'SELECT' || + activeElement.isContentEditable); + if (event.code === 'Escape' && settings.escExitFullscreen) { exitFullscreen(); } - if (event.code === 'KeyF') { + if (event.code === 'KeyF' && !inputFocused) { toggleFullscreen(); } diff --git a/src/common/useInterval.ts b/src/common/useInterval.ts new file mode 100644 index 000000000..49fba607e --- /dev/null +++ b/src/common/useInterval.ts @@ -0,0 +1,26 @@ +import { useEffect, useRef } from 'react'; + +const useInterval = (duration: number) => { + const interval = useRef(null); + + const start = (callback: () => void) => { + cancel(); + interval.current = setInterval(callback, duration); + }; + + const cancel = () => { + interval.current && clearInterval(interval.current); + interval.current = null; + }; + + useEffect(() => { + return () => cancel(); + }, []); + + return { + start, + cancel, + }; +}; + +export default useInterval; diff --git a/src/common/useLanguageSorting.ts b/src/common/useLanguageSorting.ts new file mode 100644 index 000000000..d5969cf76 --- /dev/null +++ b/src/common/useLanguageSorting.ts @@ -0,0 +1,38 @@ +import { useMemo } from 'react'; +import interfaceLanguages from 'stremio/common/interfaceLanguages.json'; + +const useLanguageSorting = (options: MultiselectMenuOption[]) => { + const userLangCode = useMemo(() => { + const lang = interfaceLanguages.find((l) => l.codes.includes(navigator.language || 'en-US')); + if (lang) { + const threeLetter = lang.codes[1] || 'eng'; + const fullLocale = navigator.language || 'en-US'; + return [threeLetter, fullLocale]; + } + return ['eng']; + }, []); + + const isLanguageDropdown = useMemo(() => { + return options?.some((opt) => interfaceLanguages.some((l) => l.name === opt.label)); + }, [options]); + + const sortedOptions = useMemo(() => { + const matchingIndex = options.findIndex((opt) => { + const lang = interfaceLanguages.find((l) => l.name === opt.label); + return userLangCode.some((code) => lang?.codes.includes(code)); + }); + + if (matchingIndex === -1) { + return [...options].sort((a, b) => a.label.localeCompare(b.label)); + } + + const matchingOption = options[matchingIndex]; + const otherOptions = options.filter((_, idx) => idx !== matchingIndex).sort((a, b) => a.label.localeCompare(b.label)); + + return [matchingOption, ...otherOptions]; + }, [options, userLangCode, isLanguageDropdown]); + + return { userLangCode, isLanguageDropdown, sortedOptions }; +}; + +export default useLanguageSorting; diff --git a/src/common/useTimeout.ts b/src/common/useTimeout.ts new file mode 100644 index 000000000..e865aeefd --- /dev/null +++ b/src/common/useTimeout.ts @@ -0,0 +1,26 @@ +import { useEffect, useRef } from 'react'; + +const useTimeout = (duration: number) => { + const timeout = useRef(null); + + const start = (callback: () => void) => { + cancel(); + timeout.current = setTimeout(callback, duration); + }; + + const cancel = () => { + timeout.current && clearTimeout(timeout.current); + timeout.current = null; + }; + + useEffect(() => { + return () => cancel(); + }, []); + + return { + start, + cancel, + }; +}; + +export default useTimeout; diff --git a/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js b/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js index da5fde198..78c744b96 100644 --- a/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js +++ b/src/components/AddonDetailsModal/AddonDetails/AddonDetails.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -8,6 +9,7 @@ const { default: Image } = require('stremio/components/Image'); const styles = require('./styles'); const AddonDetails = ({ className, id, name, version, logo, description, types, transportUrl, official }) => { + const { t } = useTranslation(); const renderLogoFallback = React.useCallback(() => ( ), []); @@ -24,7 +26,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, {typeof name === 'string' && name.length > 0 ? name : id} { typeof version === 'string' && version.length > 0 ? - v. {version} + {t('ADDON_VERSION_SHORT', {version})} : null } @@ -41,7 +43,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, { typeof transportUrl === 'string' && transportUrl.length > 0 ?
- URL: + {`${t('URL')}:`} {transportUrl}
: @@ -50,7 +52,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, { Array.isArray(types) && types.length > 0 ?
- Supported types: + {`${t('ADDON_SUPPORTED_TYPES')}:`} { types.length === 1 ? @@ -66,7 +68,7 @@ const AddonDetails = ({ className, id, name, version, logo, description, types, { !official ?
-
Using third-party add-ons will always be subject to your responsibility and the governing law of the jurisdiction you are located.
+
{t('ADDON_DISCLAIMER')}
: null diff --git a/src/components/AddonDetailsModal/AddonDetailsModal.js b/src/components/AddonDetailsModal/AddonDetailsModal.js index 332eab364..a89a68b77 100644 --- a/src/components/AddonDetailsModal/AddonDetailsModal.js +++ b/src/components/AddonDetailsModal/AddonDetailsModal.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const ModalDialog = require('stremio/components/ModalDialog'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); @@ -43,13 +44,14 @@ function withRemoteAndLocalAddon(AddonDetails) { } const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { + const { t } = useTranslation(); const { core } = useServices(); const platform = usePlatform(); const addonDetails = useAddonDetails(transportUrl); const modalButtons = React.useMemo(() => { const cancelButton = { className: styles['cancel-button'], - label: 'Cancel', + label: t('BUTTON_CANCEL'), props: { onClick: (event) => { if (typeof onCloseRequest === 'function') { @@ -67,7 +69,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { addonDetails.remoteAddon.content.content.manifest.behaviorHints.configurable ? { className: styles['configure-button'], - label: 'Configure', + label: t('ADDON_CONFIGURE'), props: { onClick: (event) => { platform.openExternal(transportUrl.replace('manifest.json', 'configure')); @@ -86,7 +88,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { const toggleButton = addonDetails.localAddon !== null ? { className: styles['uninstall-button'], - label: 'Uninstall', + label: t('ADDON_UNINSTALL'), props: { onClick: (event) => { core.transport.dispatch({ @@ -113,7 +115,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { { className: styles['install-button'], - label: 'Install', + label: t('ADDON_INSTALL'), props: { onClick: (event) => { core.transport.dispatch({ @@ -141,21 +143,21 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null; }, [addonDetails.remoteAddon]); return ( - + { addonDetails.selected === null ?
- Loading addon manifest + {t('ADDON_LOADING_MANIFEST')}
: addonDetails.remoteAddon === null || addonDetails.remoteAddon.content.type === 'Loading' ?
- Loading addon manifest from {addonDetails.selected.transportUrl} + {t('ADDON_LOADING_MANIFEST_FROM', { origin: addonDetails.selected.transportUrl})}
: addonDetails.remoteAddon.content.type === 'Err' && addonDetails.localAddon === null ?
- Failed to get addon manifest from {addonDetails.selected.transportUrl} + {t('ADDON_LOADING_MANIFEST_FAILED', {origin: addonDetails.selected.transportUrl})}
{addonDetails.remoteAddon.content.content.message}
: @@ -174,17 +176,18 @@ AddonDetailsModal.propTypes = { onCloseRequest: PropTypes.func }; -const AddonDetailsModalFallback = ({ onCloseRequest }) => ( - { + const { t } = useTranslation(); + return
- Loading addon manifest + {t('ADDON_LOADING_MANIFEST')}
-
-); +
; +}; AddonDetailsModalFallback.propTypes = AddonDetailsModal.propTypes; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index f97e1ba82..a5756fc42 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -7,6 +7,7 @@ import styles from './Button.less'; type Props = { className?: string, + style?: object, href?: string, target?: string title?: string, @@ -15,6 +16,8 @@ type Props = { children: React.ReactNode, onKeyDown?: (event: React.KeyboardEvent) => void, onMouseDown?: (event: React.MouseEvent) => void, + onMouseUp?: (event: React.MouseEvent) => void, + onMouseLeave?: (event: React.MouseEvent) => void, onLongPress?: () => void, onClick?: (event: React.MouseEvent) => void, onDoubleClick?: () => void, diff --git a/src/components/Checkbox/Checkbox.less b/src/components/Checkbox/Checkbox.less index a84244ce9..9276990b3 100644 --- a/src/components/Checkbox/Checkbox.less +++ b/src/components/Checkbox/Checkbox.less @@ -23,6 +23,7 @@ .link { font-size: 0.9rem; color: var(--primary-accent-color); + margin-left: 0.5rem; &:hover { text-decoration: underline; diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index da4ae33eb..b252006eb 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -80,7 +80,6 @@ const Checkbox = React.forwardRef(({ name, disabled, cl
{label} - {' '} { href && link ?
diff --git a/src/components/Multiselect/Multiselect.js b/src/components/Multiselect/Multiselect.js index 0e353eef4..c2bf855d1 100644 --- a/src/components/Multiselect/Multiselect.js +++ b/src/components/Multiselect/Multiselect.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -11,6 +12,7 @@ const useBinaryState = require('stremio/common/useBinaryState'); const styles = require('./styles'); const Multiselect = ({ className, mode, direction, title, disabled, dataset, options, renderLabelContent, renderLabelText, onOpen, onClose, onSelect, ...props }) => { + const { t } = useTranslation(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const filteredOptions = React.useMemo(() => { return Array.isArray(options) ? @@ -122,7 +124,7 @@ const Multiselect = ({ className, mode, direction, title, disabled, dataset, opt )) :
-
No options available
+
{t('NO_OPTIONS')}
}
diff --git a/src/components/Multiselect/styles.less b/src/components/Multiselect/styles.less index f87c86d9b..4298b2697 100644 --- a/src/components/Multiselect/styles.less +++ b/src/components/Multiselect/styles.less @@ -50,7 +50,7 @@ .modal-container, .popup-menu-container { .menu-container { - max-height: calc(3.2rem * 7); + max-height: calc(3rem * 7); .option-container { display: flex; diff --git a/src/components/MultiselectMenu/Dropdown/Dropdown.less b/src/components/MultiselectMenu/Dropdown/Dropdown.less index 3bea17d22..18681b4a3 100644 --- a/src/components/MultiselectMenu/Dropdown/Dropdown.less +++ b/src/components/MultiselectMenu/Dropdown/Dropdown.less @@ -2,7 +2,8 @@ @import (reference) '~stremio/common/screen-sizes.less'; -@parent-height: 10rem; +@parent-height: 12rem; +@item-height: 3rem; .dropdown { background: var(--modal-background-color); @@ -18,7 +19,7 @@ &.open { display: block; - max-height: calc(3.3rem * 7); + max-height: calc(@item-height * 7); overflow: auto; } diff --git a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx index 411ef6ab5..00fd3ff6a 100644 --- a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx +++ b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx @@ -10,11 +10,11 @@ import styles from './Dropdown.less'; type Props = { options: MultiselectMenuOption[]; - value?: string | number; + value?: any; menuOpen: boolean | (() => void); level: number; setLevel: (level: number) => void; - onSelect: (value: string | number) => void; + onSelect: (value: any) => void; }; const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props) => { @@ -24,7 +24,7 @@ const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props const selectedOption = options.find((opt) => opt.value === value); - const handleSetOptionRef = useCallback((optionValue: string | number) => (node: HTMLButtonElement | null) => { + const handleSetOptionRef = useCallback((optionValue: any) => (node: HTMLButtonElement | null) => { if (node) { optionsRef.current.set(optionValue, node); } else { diff --git a/src/components/MultiselectMenu/Dropdown/Option/Option.less b/src/components/MultiselectMenu/Dropdown/Option/Option.less index a0ee1743f..03a5d03d1 100644 --- a/src/components/MultiselectMenu/Dropdown/Option/Option.less +++ b/src/components/MultiselectMenu/Dropdown/Option/Option.less @@ -1,6 +1,9 @@ // Copyright (C) 2017-2024 Smart code 203358507 +@height: 3rem; + .option { + height: @height; font-size: var(--font-size-normal); color: var(--primary-foreground-color); align-items: center; diff --git a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx index 9ff1480f1..5d43fb45b 100644 --- a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx +++ b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx @@ -8,8 +8,8 @@ import Icon from '@stremio/stremio-icons/react'; type Props = { option: MultiselectMenuOption; - selectedValue?: string | number; - onSelect: (value: string | number) => void; + selectedValue?: any; + onSelect: (value: any) => void; }; const Option = forwardRef(({ option, selectedValue, onSelect }, ref) => { diff --git a/src/components/MultiselectMenu/MultiselectMenu.less b/src/components/MultiselectMenu/MultiselectMenu.less index 4aee1a4a8..1a886322e 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.less +++ b/src/components/MultiselectMenu/MultiselectMenu.less @@ -1,6 +1,7 @@ // Copyright (C) 2017-2024 Smart code 203358507 @border-radius: 2.75rem; +@height: 3rem; .multiselect-menu { position: relative; @@ -14,6 +15,7 @@ } .multiselect-button { + height: @height; padding: 0.75rem 1.5rem; display: flex; flex: 1; diff --git a/src/components/MultiselectMenu/MultiselectMenu.tsx b/src/components/MultiselectMenu/MultiselectMenu.tsx index 4fef5ce81..eb288a12b 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.tsx +++ b/src/components/MultiselectMenu/MultiselectMenu.tsx @@ -11,13 +11,14 @@ import useOutsideClick from 'stremio/common/useOutsideClick'; type Props = { className?: string, - title?: string | (() => string); + title?: string | (() => string | null); options: MultiselectMenuOption[]; - value?: string | number; - onSelect: (value: string | number) => void; + value?: any; + disabled?: boolean, + onSelect: (value: any) => void; }; -const MultiselectMenu = ({ className, title, options, value, onSelect }: Props) => { +const MultiselectMenu = ({ className, title, options, value, disabled, onSelect }: Props) => { const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const multiselectMenuRef = useOutsideClick(() => closeMenu()); const [level, setLevel] = React.useState(0); @@ -32,6 +33,7 @@ const MultiselectMenu = ({ className, title, options, value, onSelect }: Props)
diff --git a/src/components/Toggle/Toggle.js b/src/components/Toggle/Toggle.js deleted file mode 100644 index 420f78392..000000000 --- a/src/components/Toggle/Toggle.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const React = require('react'); -const PropTypes = require('prop-types'); -const classnames = require('classnames'); -const { Button } = require('stremio/components'); -const styles = require('./styles'); - -const Toggle = React.forwardRef(({ className, checked, children, ...props }, ref) => { - return ( - - ); -}); - -Toggle.displayName = 'Toggle'; - -Toggle.propTypes = { - className: PropTypes.string, - checked: PropTypes.bool, - children: PropTypes.node -}; - -module.exports = Toggle; diff --git a/src/components/Toggle/styles.less b/src/components/Toggle/Toggle.less similarity index 100% rename from src/components/Toggle/styles.less rename to src/components/Toggle/Toggle.less diff --git a/src/components/Toggle/Toggle.tsx b/src/components/Toggle/Toggle.tsx new file mode 100644 index 000000000..ea14b6258 --- /dev/null +++ b/src/components/Toggle/Toggle.tsx @@ -0,0 +1,27 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import React, { forwardRef } from 'react'; +import classnames from 'classnames'; +import { Button } from 'stremio/components'; +import styles from './Toggle.less'; + +type Props = { + className?: string, + checked: boolean, + disabled?: boolean, + tabIndex?: number, + children?: React.ReactNode, +}; + +const Toggle = forwardRef(({ className, checked, children, ...props }: Props, ref) => { + return ( + + ); +}); + +Toggle.displayName = 'Toggle'; + +export default Toggle; diff --git a/src/components/Toggle/index.js b/src/components/Toggle/index.js deleted file mode 100644 index ae6c69d8a..000000000 --- a/src/components/Toggle/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const Toggle = require('./Toggle'); - -module.exports = Toggle; diff --git a/src/components/Toggle/index.ts b/src/components/Toggle/index.ts new file mode 100644 index 000000000..2884e5394 --- /dev/null +++ b/src/components/Toggle/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +import Toggle from './Toggle'; + +export default Toggle; diff --git a/src/components/Video/Video.js b/src/components/Video/Video.js index 94fbefcc7..229fe758d 100644 --- a/src/components/Video/Video.js +++ b/src/components/Video/Video.js @@ -1,9 +1,9 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { t } = require('i18next'); const { useRouteFocused } = require('stremio-router'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { Button, Image, Popup } = require('stremio/components'); @@ -12,9 +12,10 @@ const useProfile = require('stremio/common/useProfile'); const VideoPlaceholder = require('./VideoPlaceholder'); const styles = require('./styles'); -const Video = ({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }) => { +const Video = React.forwardRef(({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }, ref) => { const routeFocused = useRouteFocused(); const profile = useProfile(); + const { t } = useTranslation(); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const popupLabelOnMouseUp = React.useCallback((event) => { if (!event.nativeEvent.togglePopupPrevented) { @@ -67,10 +68,27 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc } } }, [deepLinks]); - const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ...props }) { + const renderLabel = React.useMemo(() => function renderLabel({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, children, ref: popupRef, ...props }) { const blurThumbnail = profile.settings.hideSpoilers && season && episode && !watched; + const handleRef = React.useCallback((node) => { + if (popupRef) { + if (typeof popupRef === 'function') { + popupRef(node); + } else { + popupRef.current = node; + } + } + if (ref) { + if (typeof ref === 'function') { + ref(node); + } else { + ref.current = node; + } + } + }, [popupRef]); + return ( - -
{ typeof version === 'string' && version.length > 0 ? -
v.{version}
+
{t('ADDON_VERSION_SHORT', {version})}
: null } diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index 451101cbe..da9544d51 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.js @@ -124,7 +124,7 @@ const Addons = ({ urlParams, queryParams }) => { value={search} onChange={searchInputOnChange} /> -
@@ -132,12 +132,12 @@ const Addons = ({ urlParams, queryParams }) => { installedAddons.selected !== null ? installedAddons.selectable.types.length === 0 ?
- No addons ware installed! + {t('NO_ADDONS')}
: installedAddons.catalog.length === 0 ?
- No addons ware installed for that type! + {t('NO_ADDONS_FOR_TYPE')}
:
@@ -216,7 +216,7 @@ const Addons = ({ urlParams, queryParams }) => {
{ filtersModalOpen ? - + {selectInputs.map((selectInput, index) => ( { {typeof sharedAddon.manifest.name === 'string' && sharedAddon.manifest.name.length > 0 ? sharedAddon.manifest.name : sharedAddon.manifest.id} { typeof sharedAddon.manifest.version === 'string' && sharedAddon.manifest.version.length > 0 ? - v. {sharedAddon.manifest.version} + {t('ADDON_VERSION_SHORT', { version: sharedAddon.manifest.version })} : null } diff --git a/src/routes/Addons/useSelectableInputs.js b/src/routes/Addons/useSelectableInputs.js index a8af37fbe..003da745d 100644 --- a/src/routes/Addons/useSelectableInputs.js +++ b/src/routes/Addons/useSelectableInputs.js @@ -10,8 +10,8 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => { .concat(installedAddons.selectable.catalogs) .map(({ name, deepLinks }) => ({ value: deepLinks.addons, - label: t.stringWithPrefix(name, 'ADDON_'), - title: t.stringWithPrefix(name, 'ADDON_'), + label: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'), + title: t.stringWithPrefix(name.toUpperCase(), 'ADDON_'), })), value: selectedCatalog ? selectedCatalog.deepLinks.addons : undefined, title: remoteAddons.selected !== null ? diff --git a/src/routes/Calendar/Details/Details.tsx b/src/routes/Calendar/Details/Details.tsx index cce550ee9..a03708572 100644 --- a/src/routes/Calendar/Details/Details.tsx +++ b/src/routes/Calendar/Details/Details.tsx @@ -1,6 +1,7 @@ // Copyright (C) 2017-2024 Smart code 203358507 import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import Icon from '@stremio/stremio-icons/react'; import { Button } from 'stremio/components'; import styles from './Details.less'; @@ -11,6 +12,7 @@ type Props = { }; const Details = ({ selected, items }: Props) => { + const { t } = useTranslation(); const videos = useMemo(() => { return items.find(({ date }) => date.day === selected?.day)?.items ?? []; }, [selected, items]); @@ -33,7 +35,7 @@ const Details = ({ selected, items }: Props) => { { !videos.length ?
- No new episodes for this day + {t('CALENDAR_NO_NEW_EPISODES')}
: null diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index 1c2b6f122..6f32d1f9a 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -14,6 +15,7 @@ const styles = require('./styles'); const SCROLL_TO_BOTTOM_THRESHOLD = 400; const Discover = ({ urlParams, queryParams }) => { + const { t } = useTranslation(); const { core } = useServices(); const [discover, loadNextPage] = useDiscover(urlParams, queryParams); const [selectInputs, hasNextPage] = useSelectableInputs(discover); @@ -111,7 +113,7 @@ const Discover = ({ urlParams, queryParams }) => { /> ))}
-
@@ -119,9 +121,9 @@ const Discover = ({ urlParams, queryParams }) => { { discover.catalog !== null && !discover.catalog.installed ?
-
Addon is not installed. Install now?
-
: @@ -132,7 +134,7 @@ const Discover = ({ urlParams, queryParams }) => {
{' -
No catalog selected!
+
{t('NO_CATALOG_SELECTED')}
: @@ -191,6 +193,8 @@ const Discover = ({ urlParams, queryParams }) => { trailerStreams={selectedMetaItem.trailerStreams} inLibrary={selectedMetaItem.inLibrary} toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary} + metaId={selectedMetaItem.id} + like={selectedMetaItem.like} /> : discover.catalog !== null && discover.catalog.content.type === 'Loading' ? @@ -201,7 +205,7 @@ const Discover = ({ urlParams, queryParams }) => { { inputsModalOpen ? - + {selectInputs.map(({ title, options, value, onSelect }, index) => ( { window.location = value; } }; - const selectedCatalog = discover.selectable.catalogs.find(({ selected }) => selected); const catalogSelect = { options: discover.selectable.catalogs .map(({ id, name, addon, deepLinks }) => ({ @@ -29,9 +28,9 @@ const mapSelectableInputs = (discover, t) => { label: t.catalogTitle({ addon, id, name }), title: `${name} (${addon.manifest.name})` })), - value: discover.selected?.request.path.id - ? selectedCatalog.deepLinks.discover - : undefined, + value: discover.selectable.catalogs + .filter(({ selected }) => selected) + .map(({ deepLinks }) => deepLinks.discover), title: discover.selected !== null ? () => { const selectableCatalog = discover.selectable.catalogs @@ -49,7 +48,7 @@ const mapSelectableInputs = (discover, t) => { return { isRequired: isRequired, options: options.map(({ value, deepLinks }) => ({ - label: typeof value === 'string' ? t.stringWithPrefix(value) : t.string('NONE'), + label: typeof value === 'string' ? t.string(value) : t.string('NONE'), value: JSON.stringify({ href: deepLinks.discover, value @@ -60,8 +59,8 @@ const mapSelectableInputs = (discover, t) => { value: selectedExtra.value, }), title: options.some(({ selected, value }) => selected && value === null) ? - () => t.stringWithPrefix(name, 'SELECT_') - : t.stringWithPrefix(selectedExtra.value), + () => t.string(name.toUpperCase()) + : t.string(selectedExtra.value), onSelect: (value) => { const { href } = JSON.parse(value); window.location = href; diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 6af8f0167..48acadcd4 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -299,10 +299,10 @@ const Intro = ({ queryParams }) => { {'
- Freedom to Stream + {t('WEBSITE_SLOGAN_NEW_NEW')}
- All the Video Content You Enjoy in One Place + {t('WEBSITE_SLOGAN_ALL')}
@@ -311,7 +311,7 @@ const Intro = ({ queryParams }) => { ref={emailRef} className={styles['credentials-text-input']} type={'email'} - placeholder={'Email'} + placeholder={t('EMAIL')} value={state.email} onChange={emailOnChange} onSubmit={emailOnSubmit} @@ -320,7 +320,7 @@ const Intro = ({ queryParams }) => { ref={passwordRef} className={styles['credentials-text-input']} type={'password'} - placeholder={'Password'} + placeholder={t('PASSWORD')} value={state.password} onChange={passwordOnChange} onSubmit={passwordOnSubmit} @@ -332,37 +332,37 @@ const Intro = ({ queryParams }) => { ref={confirmPasswordRef} className={styles['credentials-text-input']} type={'password'} - placeholder={'Confirm Password'} + placeholder={t('PASSWORD_CONFIRM')} value={state.confirmPassword} onChange={confirmPasswordOnChange} onSubmit={confirmPasswordOnSubmit} /> :
- +
} { @@ -372,22 +372,22 @@ const Intro = ({ queryParams }) => { null }
{ state.form === SIGNUP_FORM ? : null @@ -395,7 +395,7 @@ const Intro = ({ queryParams }) => { { state.form === LOGIN_FORM ? : null @@ -403,7 +403,7 @@ const Intro = ({ queryParams }) => { { state.form === SIGNUP_FORM ? : null @@ -421,7 +421,7 @@ const Intro = ({ queryParams }) => {
-
Authenticating...
+
{t('AUTHENTICATING')}
diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js index 6f295fdd6..8c69c1489 100644 --- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js +++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const { useRouteFocused } = require('stremio-router'); const { usePlatform } = require('stremio/common'); @@ -9,6 +10,7 @@ const CredentialsTextInput = require('../CredentialsTextInput'); const styles = require('./styles'); const PasswordResetModal = ({ email, onCloseRequest }) => { + const { t } = useTranslation(); const routeFocused = useRouteFocused(); const platform = usePlatform(); const [error, setError] = React.useState(''); @@ -23,13 +25,13 @@ const PasswordResetModal = ({ email, onCloseRequest }) => { return [ { className: styles['cancel-button'], - label: 'Cancel', + label: t('BUTTON_CANCEL'), props: { onClick: onCloseRequest } }, { - label: 'Send', + label: t('SEND'), props: { onClick: goToPasswordReset } @@ -45,7 +47,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => { } }, [routeFocused]); return ( - + { + const { t } = useTranslation(); const profile = useProfile(); const notifications = useNotifications(); const [library, loadNextPage] = useLibrary(model, urlParams, queryParams); @@ -86,7 +88,7 @@ const Library = ({ model, urlParams, queryParams }) => { src={require('/images/empty.png')} alt={' '} /> -
{model === 'library' ? 'Library' : 'Continue Watching'} not loaded!
+
{model === 'library' ? t('LIBRARY_NOT_LOADED') : t('BOARD_CONTINUE_WATCHING_NOT_LOADED')}
: @@ -97,7 +99,7 @@ const Library = ({ model, urlParams, queryParams }) => { src={require('/images/empty.png')} alt={' '} /> -
Empty {model === 'library' ? 'Library' : 'Continue Watching'}
+
{model === 'library' ? t('LIBRARY_EMPTY') : t('BOARD_CONTINUE_WATCHING_EMPTY')}
:
diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index 108ed6b40..f49965596 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { useServices, useContentGamepadNavigation } = require('stremio/services'); @@ -15,6 +16,7 @@ const styles = require('./styles'); const MetaDetails = ({ urlParams, queryParams }) => { const contentRef = React.useRef(null); + const { t } = useTranslation(); const { core } = useServices(); const metaDetails = useMetaDetails(urlParams); const [season, setSeason] = useSeason(urlParams, queryParams); @@ -131,20 +133,20 @@ const MetaDetails = ({ urlParams, queryParams }) => {
{' -
No meta was selected!
+
{t('ERR_NO_META_SELECTED')}
: metaDetails.metaItem === null ?
{' -
No addons were requested for this meta!
+
{t('ERR_NO_ADDONS_FOR_META')}
: metaDetails.metaItem.content.type === 'Err' ?
{' -
No metadata was found!
+
{t('ERR_NO_META_FOUND')}
: metaDetails.metaItem.content.type === 'Loading' ? @@ -168,6 +170,8 @@ const MetaDetails = ({ urlParams, queryParams }) => { trailerStreams={metaDetails.metaItem.content.content.trailerStreams} inLibrary={metaDetails.metaItem.content.content.inLibrary} toggleInLibrary={metaDetails.metaItem.content.content.inLibrary ? removeFromLibrary : addToLibrary} + metaId={metaDetails.metaItem.content.content.id} + ratingInfo={metaDetails.ratingInfo} /> } diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 627b41857..eebc0c3cf 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -133,7 +133,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => { : null } {' -
No addons were requested for streams!
+
{t('ERR_NO_ADDONS_FOR_STREAMS')}
: props.streams.every((streams) => streams.content.type === 'Err') ? diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index 51b51f0e7..ea870a931 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -13,7 +13,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { const options = React.useMemo(() => { return seasons.map((season) => ({ value: String(season), - label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL') + label: season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL') })); }, [seasons]); const selectedSeason = React.useMemo(() => { @@ -56,19 +56,19 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { return (
- 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} + title={season > 0 ? t('SEASON_NUMBER', { season }) : t('SPECIAL')} value={selectedSeason} onSelect={seasonOnSelect} /> -
diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js index d767769ec..4013e0b64 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBarPlaceholder/SeasonsBarPlaceholder.js @@ -1,24 +1,26 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const styles = require('./styles'); const SeasonsBarPlaceholder = ({ className }) => { + const { t } = useTranslation(); return (
-
Prev
+
{t('SEASON_PREV')}
-
Season 1
+
{t('SEASON_NUMBER', { season: 1 })}
-
Next
+
{t('SEASON_NEXT')}
diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index a47b2f517..88d53d427 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -5,6 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { t } = require('i18next'); const { useServices } = require('stremio/services'); +const { useProfile } = require('stremio/common'); const { Image, SearchBar, Toggle, Video } = require('stremio/components'); const SeasonsBar = require('./SeasonsBar'); const { default: EpisodePicker } = require('../EpisodePicker'); @@ -12,6 +13,7 @@ const styles = require('./styles'); const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, toggleNotifications }) => { const { core } = useServices(); + const profile = useProfile(); const showNotificationsToggle = React.useMemo(() => { return metaItem?.content?.content?.inLibrary && metaItem?.content?.content?.videos?.length; }, [metaItem]); @@ -122,7 +124,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
{' -
No videos found for this meta!
+
{t('ERR_NO_VIDEOS_FOR_META')}
: @@ -158,7 +160,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, return search.length === 0 || ( (typeof video.title === 'string' && video.title.toLowerCase().includes(search.toLowerCase())) || - (!isNaN(video.released.getTime()) && video.released.toLocaleString(undefined, { year: '2-digit', month: 'short', day: 'numeric' }).toLowerCase().includes(search.toLowerCase())) + (!isNaN(video.released.getTime()) && video.released.toLocaleString(profile.settings.interfaceLanguage, { year: '2-digit', month: 'short', day: 'numeric' }).toLowerCase().includes(search.toLowerCase())) ); }) .map((video, index) => ( diff --git a/src/routes/NotFound/NotFound.js b/src/routes/NotFound/NotFound.js index 323dfd866..d984496bc 100644 --- a/src/routes/NotFound/NotFound.js +++ b/src/routes/NotFound/NotFound.js @@ -1,15 +1,17 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const { HorizontalNavBar, Image } = require('stremio/components'); const styles = require('./styles'); const NotFound = () => { + const { t } = useTranslation(); return (
{ src={require('/images/empty.png')} alt={' '} /> -
Page not found!
+
{t('PAGE_NOT_FOUND')}
); diff --git a/src/routes/Player/Indicator/Indicator.less b/src/routes/Player/Indicator/Indicator.less new file mode 100644 index 000000000..699c53d23 --- /dev/null +++ b/src/routes/Player/Indicator/Indicator.less @@ -0,0 +1,23 @@ +.indicator-container { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + height: 4rem; + user-select: none; + + .indicator { + flex: none; + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 2rem; + border-radius: 4rem; + text-align: center; + font-weight: bold; + color: var(--primary-foreground-color); + background-color: var(--modal-background-color); + } +} \ No newline at end of file diff --git a/src/routes/Player/Indicator/Indicator.tsx b/src/routes/Player/Indicator/Indicator.tsx new file mode 100644 index 000000000..f88682e71 --- /dev/null +++ b/src/routes/Player/Indicator/Indicator.tsx @@ -0,0 +1,74 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { t } from 'i18next'; +import { Transition } from 'stremio/components'; +import { useBinaryState } from 'stremio/common'; +import styles from './Indicator.less'; + +type Property = { + label: string, + format: (value: number) => string, +}; + +const PROPERTIES: Record = { + 'extraSubtitlesDelay': { + label: 'SUBTITLES_DELAY', + format: (value) => `${(value / 1000).toFixed(2)}s`, + }, +}; + +type VideoState = Record; + +type Props = { + className: string, + videoState: VideoState, + disabled: boolean, +}; + +const Indicator = ({ className, videoState, disabled }: Props) => { + const timeout = useRef(null); + const prevVideoState = useRef(videoState); + + const [shown, show, hide] = useBinaryState(false); + const [current, setCurrent] = useState(null); + + const label = useMemo(() => { + const property = current && PROPERTIES[current]; + return property && t(property.label); + }, [current]); + + const value = useMemo(() => { + const property = current && PROPERTIES[current]; + const value = current && videoState[current]; + return property && value && property.format(value); + }, [current, videoState]); + + useEffect(() => { + for (const property of Object.keys(PROPERTIES)) { + const prev = prevVideoState.current[property]; + const next = videoState[property]; + + if (next && next !== prev) { + setCurrent(property); + show(); + + timeout.current && clearTimeout(timeout.current); + timeout.current = setTimeout(hide, 1000); + } + } + + prevVideoState.current = videoState; + }, [videoState]); + + return ( + +
+
+
{label} {value}
+
+
+
+ ); +}; + +export default Indicator; diff --git a/src/routes/Player/NextVideoPopup/NextVideoPopup.js b/src/routes/Player/NextVideoPopup/NextVideoPopup.js index a02d2377e..1f6bcbbe6 100644 --- a/src/routes/Player/NextVideoPopup/NextVideoPopup.js +++ b/src/routes/Player/NextVideoPopup/NextVideoPopup.js @@ -7,8 +7,10 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { CONSTANTS, useProfile } = require('stremio/common'); const { Button, Image } = require('stremio/components'); const styles = require('./styles'); +const { useTranslation } = require('react-i18next'); const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onNextVideoRequested }) => { + const { t } = useTranslation(); const profile = useProfile(); const blurPosterImage = profile.settings.hideSpoilers && metaItem.type === 'series'; const watchNowButtonRef = React.useRef(null); @@ -65,7 +67,7 @@ const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onNextVideo { typeof metaItem?.name === 'string' ?
- Next on { metaItem.name } + {t('PLAYER_NEXT_VIDEO_TITLE_SHORT')} { metaItem.name }
: null @@ -82,11 +84,11 @@ const NextVideoPopup = ({ className, metaItem, nextVideo, onDismiss, onNextVideo
diff --git a/src/routes/Player/OptionsMenu/OptionsMenu.js b/src/routes/Player/OptionsMenu/OptionsMenu.js index 56241e009..968de5341 100644 --- a/src/routes/Player/OptionsMenu/OptionsMenu.js +++ b/src/routes/Player/OptionsMenu/OptionsMenu.js @@ -9,7 +9,7 @@ const { useServices } = require('stremio/services'); const Option = require('./Option'); const styles = require('./styles'); -const OptionsMenu = ({ className, stream, playbackDevices }) => { +const OptionsMenu = ({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }) => { const { t } = useTranslation(); const { core } = useServices(); const platform = usePlatform(); @@ -25,6 +25,12 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => { const externalDevices = React.useMemo(() => { return playbackDevices.filter(({ type }) => type === 'external'); }, [playbackDevices]); + + const subtitlesTrackUrl = React.useMemo(() => { + const track = extraSubtitlesTracks?.find(({ id }) => id === selectedExtraSubtitlesTrackId); + return track?.fallbackUrl ?? track?.url ?? null; + }, [extraSubtitlesTracks, selectedExtraSubtitlesTrackId]); + const onCopyStreamButtonClick = React.useCallback(() => { if (streamingUrl || downloadUrl) { navigator.clipboard.writeText(streamingUrl || downloadUrl) @@ -52,6 +58,11 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => { platform.openExternal(streamingUrl || downloadUrl); } }, [streamingUrl, downloadUrl]); + + const onDownloadSubtitlesClick = React.useCallback(() => { + subtitlesTrackUrl && platform.openExternal(subtitlesTrackUrl); + }, [subtitlesTrackUrl]); + const onExternalDeviceRequested = React.useCallback((deviceId) => { if (streamingUrl) { core.transport.dispatch({ @@ -94,6 +105,17 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => { : null } + { + subtitlesTrackUrl ? +