diff --git a/.github/workflows/pages_cleanup.yml b/.github/workflows/pages_cleanup.yml new file mode 100644 index 000000000..2ee3a32c8 --- /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@v4 + 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 1b9f26b6e..2cdd60e4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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.10.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 381c5ea98..7d5ed4487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stremio", - "version": "5.0.0-beta.25", + "version": "5.0.0-beta.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.25", + "version": "5.0.0-beta.26", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", @@ -14,7 +14,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.49.4", "@stremio/stremio-icons": "5.7.1", - "@stremio/stremio-video": "0.0.60", + "@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#8212fa77c4febd22ddb611590e9fb574dc845416", + "stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -3412,9 +3412,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", @@ -13434,8 +13434,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.12", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8212fa77c4febd22ddb611590e9fb574dc845416", - "integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#abe7684165a031755e9aee39da26daa806ba7824", + "integrity": "sha512-bMpdJTFZqgemdoOQAARMPG7XaFgeu/zW/0vHmzavTM9DYUNIGuQaTC5RbVXIIII00RLOXoGLYf+dsxRVFiS9mA==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index 6765a45ed..ffcaef6b2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.25", + "version": "5.0.0-beta.26", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", @@ -19,7 +19,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.49.4", "@stremio/stremio-icons": "5.7.1", - "@stremio/stremio-video": "0.0.60", + "@stremio/stremio-video": "0.0.61", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -41,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#8212fa77c4febd22ddb611590e9fb574dc845416", + "stremio-translations": "github:Stremio/stremio-translations#abe7684165a031755e9aee39da26daa806ba7824", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/src/common/index.js b/src/common/index.js index 3354a64e4..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,6 +24,7 @@ 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'); @@ -49,6 +51,7 @@ module.exports = { useAnimationFrame, useBinaryState, useFullscreen, + useInterval, useLiveRef, useModelState, useNotifications, @@ -57,6 +60,7 @@ module.exports = { useSettings, useShell, useStreamingServer, + useTimeout, useTorrent, useTranslate, useOrientation, 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/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/Button/Button.tsx b/src/components/Button/Button.tsx index dcce809d2..a5756fc42 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -16,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/Transition/Transition.tsx b/src/components/Transition/Transition.tsx index ae84d80e5..e12b2a42b 100644 --- a/src/components/Transition/Transition.tsx +++ b/src/components/Transition/Transition.tsx @@ -13,7 +13,6 @@ const Transition = ({ children, when, name }: Props) => { const [state, setState] = useState('enter'); const [active, setActive] = useState(false); - const [transitionEnded, setTransitionEnded] = useState(false); const callbackRef = useCallback((element: HTMLElement | null) => { setElement(element); @@ -31,14 +30,12 @@ const Transition = ({ children, when, name }: Props) => { }, [name, state, active, children]); const onTransitionEnd = useCallback(() => { - setTransitionEnded(true); state === 'exit' && setMounted(false); }, [state]); useEffect(() => { setState(when ? 'enter' : 'exit'); when && setMounted(true); - setTransitionEnded(false); }, [when]); useEffect(() => { @@ -56,7 +53,6 @@ const Transition = ({ children, when, name }: Props) => { mounted && cloneElement(children, { ref: callbackRef, className, - transitionEnded }) ); }; diff --git a/src/routes/Player/Indicator/Indicator.tsx b/src/routes/Player/Indicator/Indicator.tsx index cc0ef4ccf..f88682e71 100644 --- a/src/routes/Player/Indicator/Indicator.tsx +++ b/src/routes/Player/Indicator/Indicator.tsx @@ -22,9 +22,10 @@ type VideoState = Record; type Props = { className: string, videoState: VideoState, + disabled: boolean, }; -const Indicator = ({ className, videoState }: Props) => { +const Indicator = ({ className, videoState, disabled }: Props) => { const timeout = useRef(null); const prevVideoState = useRef(videoState); @@ -60,7 +61,7 @@ const Indicator = ({ className, videoState }: Props) => { }, [videoState]); return ( - +
{label} {value}
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 ? +