diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf4a9db50..dee0401ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,20 @@ name: Build on: - push: branches: + - development + tags-ignore: - '**' + pull_request: + branches: + - development # Allow manual dispatch in GH workflow_dispatch: +permissions: + contents: write + jobs: build: runs-on: ubuntu-latest @@ -24,7 +31,7 @@ jobs: run: npm run lint # Create recursivelly the destiantion dir with # "--parrents where no error if existing, make parent directories as needed." - - run: mkdir -p ./build/${{ github.ref_name }} + - run: mkdir -p ./build/${{ github.head_ref || github.ref_name }} - name: Deploy to GitHub Pages if: ${{ github.actor != 'dependabot[bot]' }} uses: peaceiris/actions-gh-pages@v4 @@ -33,5 +40,5 @@ jobs: publish_dir: ./build # in stremio, we use `feat/features-name` or `fix/this-bug` # so we need a recursive creation of the destination dir - destination_dir: ${{ github.ref_name }} + destination_dir: ${{ github.head_ref || github.ref_name }} allow_empty_commit: true diff --git a/package-lock.json b/package-lock.json index 76db0e04a..5775eb1a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "stremio", - "version": "5.0.0-beta.8", + "version": "5.0.0-beta.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.8", + "version": "5.0.0-beta.9", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.16.0", "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "5.0.1", - "@stremio/stremio-core-web": "0.47.2", + "@stremio/stremio-core-web": "0.47.8", "@stremio/stremio-icons": "5.2.0", "@stremio/stremio-video": "0.0.38", "a-color-picker": "1.2.1", @@ -2971,9 +2971,9 @@ "license": "MIT" }, "node_modules/@stremio/stremio-core-web": { - "version": "0.47.2", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.47.2.tgz", - "integrity": "sha512-kJXkshXT5f5go137id9MHrVA7PfHao2pGSxfEBbMDGFCqAVfF4jRFTXmfLC0cS1R+EjYhajUrSsXnEddtb2c7g==", + "version": "0.47.8", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.47.8.tgz", + "integrity": "sha512-X5yKSCm5DXR7U6oIO+2kaI1q3TnaWP6df/HFa1RBi/uw+8IYk+FB8GWpryxXyisJTFiUfQgcJDIlHROauaBQkg==", "dependencies": { "@babel/runtime": "7.24.1" } diff --git a/package.json b/package.json index bf4b1bd10..6610395a6 100755 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.8", + "version": "5.0.0-beta.9", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", "scripts": { "start": "webpack serve --mode development", + "start-prod": "webpack serve --mode production", "build": "webpack --mode production", "test": "jest", "lint": "eslint src" @@ -15,7 +16,7 @@ "@babel/runtime": "7.16.0", "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "5.0.1", - "@stremio/stremio-core-web": "0.47.2", + "@stremio/stremio-core-web": "0.47.8", "@stremio/stremio-icons": "5.2.0", "@stremio/stremio-video": "0.0.38", "a-color-picker": "1.2.1", diff --git a/src/common/AddonDetailsModal/AddonDetailsModal.js b/src/common/AddonDetailsModal/AddonDetailsModal.js index 085925aac..ffe2671d0 100644 --- a/src/common/AddonDetailsModal/AddonDetailsModal.js +++ b/src/common/AddonDetailsModal/AddonDetailsModal.js @@ -105,7 +105,9 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { } } : - addonDetails.remoteAddon !== null && addonDetails.remoteAddon.content.type === 'Ready' ? + addonDetails.remoteAddon !== null && + addonDetails.remoteAddon.content.type === 'Ready' && + !addonDetails.remoteAddon.content.content.manifest.behaviorHints.configurationRequired ? { className: styles['install-button'], @@ -131,7 +133,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { } : null; - return toggleButton !== null ? configureButton ? [cancelButton, configureButton, toggleButton] : [cancelButton, toggleButton] : [cancelButton]; + return configureButton && toggleButton ? [cancelButton, configureButton, toggleButton] : configureButton ? [cancelButton, configureButton] : toggleButton ? [cancelButton, toggleButton] : [cancelButton]; }, [addonDetails, onCloseRequest]); const modalBackground = React.useMemo(() => { return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null; diff --git a/src/common/Button/Button.js b/src/common/Button/Button.js index 9d5ef7c1e..b9afe3217 100644 --- a/src/common/Button/Button.js +++ b/src/common/Button/Button.js @@ -6,7 +6,7 @@ const classnames = require('classnames'); const styles = require('./styles'); const { useLongPress } = require('use-long-press'); -const Button = React.forwardRef(({ className, href, disabled, children, onLongPress, ...props }, ref) => { +const Button = React.forwardRef(({ className, href, disabled, children, onLongPress, onDoubleClick, ...props }, ref) => { const longPress = useLongPress(onLongPress, { detect: 'pointer' }); const onKeyDown = React.useCallback((event) => { if (typeof props.onKeyDown === 'function') { @@ -42,6 +42,7 @@ const Button = React.forwardRef(({ className, href, disabled, children, onLongPr href, onKeyDown, onMouseDown, + onDoubleClick, ...longPress() }, children @@ -58,6 +59,7 @@ Button.propTypes = { onKeyDown: PropTypes.func, onMouseDown: PropTypes.func, onLongPress: PropTypes.func, + onDoubleClick: PropTypes.func }; module.exports = Button; diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index aeaa51462..742c138d6 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -44,7 +44,7 @@ const EXTERNAL_PLAYERS = [ { label: 'EXTERNAL_PLAYER_DISABLED', value: null, - platforms: ['ios', 'android', 'windows', 'linux', 'macos'], + platforms: ['ios', 'visionos', 'android', 'windows', 'linux', 'macos'], }, { label: 'EXTERNAL_PLAYER_ALLOW_CHOOSING', @@ -54,7 +54,7 @@ const EXTERNAL_PLAYERS = [ { label: 'VLC', value: 'vlc', - platforms: ['ios', 'android'], + platforms: ['ios', 'visionos', 'android'], }, { label: 'MPV', @@ -79,12 +79,17 @@ const EXTERNAL_PLAYERS = [ { label: 'Outplayer', value: 'outplayer', - platforms: ['ios'], + platforms: ['ios', 'visionos'], + }, + { + label: 'Moonplayer (VisionOS)', + value: 'moonplayer', + platforms: ['visionos'], }, { label: 'M3U Playlist', value: 'm3u', - platforms: ['ios', 'android', 'windows', 'linux', 'macos'], + platforms: ['ios', 'visionos', 'android', 'windows', 'linux', 'macos'], }, ]; diff --git a/src/common/MultiselectMenu/Dropdown/Dropdown.less b/src/common/MultiselectMenu/Dropdown/Dropdown.less new file mode 100644 index 000000000..1e15419ef --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Dropdown.less @@ -0,0 +1,30 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.dropdown { + background: var(--modal-background-color); + display: none; + position: absolute; + width: 100%; + top: 100%; + left: 0; + z-index: 10; + box-shadow: var(--outer-glow); + border-radius: var(--border-radius); + overflow: hidden; + + &.open { + display: block; + } + + .back-button { + display: flex; + align-items: center; + gap: 0 0.5rem; + padding: 0.75rem; + color: var(--primary-foreground-color); + + .back-button-icon { + width: 1.5rem; + } + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Dropdown.tsx b/src/common/MultiselectMenu/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..9f82106d1 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Dropdown.tsx @@ -0,0 +1,55 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import Button from 'stremio/common/Button'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; +import Option from './Option'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './Dropdown.less'; + +type Props = { + options: MultiselectMenuOption[]; + selectedOption?: MultiselectMenuOption | null; + menuOpen: boolean | (() => void); + level: number; + setLevel: (level: number) => void; + onSelect: (value: number) => void; +}; + +const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => { + const { t } = useTranslation(); + + const onBackButtonClick = () => { + setLevel(level - 1); + }; + + return ( +
+ { + level > 0 ? + + : null + } + { + options + .filter((option: MultiselectMenuOption) => !option.hidden) + .map((option: MultiselectMenuOption, index) => ( +
+ ); +}; + +export default Dropdown; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.less b/src/common/MultiselectMenu/Dropdown/Option/Option.less new file mode 100644 index 000000000..a0ee1743f --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.less @@ -0,0 +1,29 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +.option { + font-size: var(--font-size-normal); + color: var(--primary-foreground-color); + align-items: center; + display: flex; + flex-direction: row; + padding: 1rem; + + .label { + flex: 1; + color: var(--primary-foreground-color); + } + + .icon { + flex: none; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + margin-left: 1rem; + background-color: var(--secondary-accent-color); + opacity: 1; + } + + &:hover { + background-color: rgba(255, 255, 255, 0.15); + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/Option.tsx b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx new file mode 100644 index 000000000..c7dbb4ebb --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/Option.tsx @@ -0,0 +1,46 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React, { useCallback, useMemo } from 'react'; +import classNames from 'classnames'; +import Button from 'stremio/common/Button'; +import styles from './Option.less'; +import Icon from '@stremio/stremio-icons/react'; + +type Props = { + option: MultiselectMenuOption; + selectedOption?: MultiselectMenuOption | null; + onSelect: (value: number) => void; +}; + +const Option = ({ option, selectedOption, onSelect }: Props) => { + // consider using option.id === selectedOption?.id instead + const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]); + + const handleClick = useCallback(() => { + onSelect(option.value); + }, [onSelect, option.value]); + + return ( + + ); +}; + +export default Option; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/Option/index.ts b/src/common/MultiselectMenu/Dropdown/Option/index.ts new file mode 100644 index 000000000..6004f7754 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/Option/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Option from './Option'; + +export default Option; \ No newline at end of file diff --git a/src/common/MultiselectMenu/Dropdown/index.ts b/src/common/MultiselectMenu/Dropdown/index.ts new file mode 100644 index 000000000..ce3622a25 --- /dev/null +++ b/src/common/MultiselectMenu/Dropdown/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import Dropdown from './Dropdown'; + +export default Dropdown; \ No newline at end of file diff --git a/src/common/MultiselectMenu/MultiselectMenu.less b/src/common/MultiselectMenu/MultiselectMenu.less new file mode 100644 index 000000000..3c7b81b59 --- /dev/null +++ b/src/common/MultiselectMenu/MultiselectMenu.less @@ -0,0 +1,39 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +@border-radius: 2.75rem; + +.multiselect-menu { + position: relative; + min-width: 8.5rem; + overflow: visible; + border-radius: @border-radius; + + &.disabled { + pointer-events: none; + opacity: 0.3; + } + + .multiselect-button { + color: var(--primary-foreground-color); + padding: 0.75rem 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0 0.5rem; + border-radius: @border-radius; + + .icon { + width: 1rem; + color: var(--primary-foreground-color); + opacity: 0.6; + + &.open { + transform: rotate(180deg); + } + } + } + + &:hover { + background-color: var(--overlay-color); + } +} \ No newline at end of file diff --git a/src/common/MultiselectMenu/MultiselectMenu.tsx b/src/common/MultiselectMenu/MultiselectMenu.tsx new file mode 100644 index 000000000..2bd298752 --- /dev/null +++ b/src/common/MultiselectMenu/MultiselectMenu.tsx @@ -0,0 +1,57 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import React from 'react'; +import Button from 'stremio/common/Button'; +import useBinaryState from 'stremio/common/useBinaryState'; +import Dropdown from './Dropdown'; +import classNames from 'classnames'; +import Icon from '@stremio/stremio-icons/react'; +import styles from './MultiselectMenu.less'; +import useOutsideClick from 'stremio/common/useOutsideClick'; + +type Props = { + className?: string, + title?: string; + options: MultiselectMenuOption[]; + selectedOption?: MultiselectMenuOption; + onSelect: (value: number) => void; +}; + +const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => { + const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); + const multiselectMenuRef = useOutsideClick(() => closeMenu()); + const [level, setLevel] = React.useState(0); + + const onOptionSelect = (value: number) => { + level ? setLevel(level + 1) : onSelect(value), closeMenu(); + }; + + return ( +
+ + { + menuOpen ? + + : null + } +
+ ); +}; + +export default MultiselectMenu; \ No newline at end of file diff --git a/src/common/MultiselectMenu/index.ts b/src/common/MultiselectMenu/index.ts new file mode 100644 index 000000000..e526218cd --- /dev/null +++ b/src/common/MultiselectMenu/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2024 Smart code 203358507 + +import MultiselectMenu from './MultiselectMenu'; + +export default MultiselectMenu; \ No newline at end of file diff --git a/src/common/MultiselectMenu/types.d.ts b/src/common/MultiselectMenu/types.d.ts new file mode 100644 index 000000000..7ed039ddd --- /dev/null +++ b/src/common/MultiselectMenu/types.d.ts @@ -0,0 +1,9 @@ +type MultiselectMenuOption = { + id?: number; + label: string; + value: number; + destination?: string; + default?: boolean; + hidden?: boolean; + level?: MultiselectMenuOption[]; +}; \ No newline at end of file diff --git a/src/common/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js b/src/common/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js index 7ecef38c6..415f563be 100644 --- a/src/common/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js +++ b/src/common/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js @@ -15,8 +15,17 @@ const NavTabButton = ({ className, logo, icon, label, href, selected, onClick }) : null ), [icon]); + const onDoubleClick = () => { + const scrollableElements = document.querySelectorAll('div'); + + scrollableElements.forEach((element) => { + if (element.scrollTop > 0) { + element.scrollTop = 0; + } + }); + }; return ( - + ); + }, [thumbnail, progress, addonName, name, description, href, target, download, onClick]); + + const renderMenu = React.useMemo(() => function renderMenu() { + return ( +
+ { - typeof thumbnail === 'string' && thumbnail.length > 0 ? -
- {' -
- : -
-
{name || addonName}
-
- } - { - progress !== null && !isNaN(progress) && progress > 0 ? -
-
-
-
- : - null + streamLink && + }
-
{description}
- - + ); + }, [copyStreamLink, onClick]); + + React.useEffect(() => { + if (!routeFocused) { + closeMenu(); + } + }, [routeFocused]); + + return ( + ); }; diff --git a/src/routes/MetaDetails/StreamsList/Stream/styles.less b/src/routes/MetaDetails/StreamsList/Stream/styles.less index 22996622c..4136c3a63 100644 --- a/src/routes/MetaDetails/StreamsList/Stream/styles.less +++ b/src/routes/MetaDetails/StreamsList/Stream/styles.less @@ -3,6 +3,14 @@ @import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; @import (reference) '~stremio/common/screen-sizes.less'; +:import('~stremio/common/Popup/styles.less') { + context-menu-container: menu-container; + menu-direction-top-left: menu-direction-top-left; + menu-direction-bottom-left: menu-direction-bottom-left; + menu-direction-top-right: menu-direction-top-right; + menu-direction-bottom-right: menu-direction-bottom-right; +} + .stream-container { display: flex; flex-direction: row; @@ -103,6 +111,33 @@ color: var(--primary-foreground-color); background-color: var(--secondary-accent-color); } + + .context-menu-container { + max-width: calc(90% - 1.5rem); + z-index: 2; + + .context-menu-content { + --spatial-navigation-contain: contain; + + .context-menu-option-container { + display: flex; + flex-direction: row; + align-items: center; + padding: 1rem 1.5rem; + + &:hover, + &:focus { + background-color: var(--overlay-color); + } + + .context-menu-option-label { + font-size: 1rem; + font-weight: 500; + color:var(--primary-foreground-color); + } + } + } + } } @media only screen and (max-width: @small) { @@ -125,6 +160,28 @@ .addon-name { font-weight: 500; } + } + + .context-menu-container { + &.menu-direction-top-left, + &.menu-direction-bottom-left { + right: 1.5rem; + } + + &.menu-direction-top-right, + &.menu-direction-bottom-right { + left: 1.5rem; + } + + &.menu-direction-top-left, + &.menu-direction-top-right { + bottom: 90%; + } + + &.menu-direction-bottom-left, + &.menu-direction-bottom-right { + top: 90%; + } } } } diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index ae7d2416e..840664c8b 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -20,8 +20,17 @@ const StreamsList = ({ className, video, ...props }) => { setSelectedAddon(event.value); }, []); const backButtonOnClick = React.useCallback(() => { - window.history.back(); - }, []); + if (video.deepLinks && typeof video.deepLinks.metaDetailsVideos === 'string') { + window.location.replace(video.deepLinks.metaDetailsVideos + ( + typeof video.season === 'number' ? + `?${new URLSearchParams({'season': video.season})}` + : + null + )); + } else { + window.history.back(); + } + }, [video]); const countLoadingAddons = React.useMemo(() => { return props.streams.filter((stream) => stream.content.type === 'Loading').length; }, [props.streams]); @@ -78,6 +87,30 @@ const StreamsList = ({ className, video, ...props }) => { }, [streamsByAddon, selectedAddon]); return (
+
+ { + video ? + + +
+ {`S${video?.season}E${video?.episode} ${(video?.title)}`} +
+
+ : + null + } + { + Object.keys(streamsByAddon).length > 1 ? + + : + null + } +
{ props.streams.length === 0 ?
@@ -109,30 +142,6 @@ const StreamsList = ({ className, video, ...props }) => { : null } -
- { - video ? - - -
- {`S${video?.season}E${video?.episode} ${(video?.title)}`} -
-
- : - null - } - { - Object.keys(streamsByAddon).length > 1 ? - - : - null - } -
{filteredStreams.map((stream, index) => ( { const options = React.useMemo(() => { @@ -16,8 +17,8 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL') })); }, [seasons]); - const selected = React.useMemo(() => { - return [String(season)]; + const selectedSeason = React.useMemo(() => { + return { label: String(season), value: String(season) }; }, [season]); const prevNextButtonOnClick = React.useCallback((event) => { if (typeof onSelect === 'function') { @@ -35,8 +36,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { }); } }, [season, seasons, onSelect]); - const seasonOnSelect = React.useCallback((event) => { - const value = parseFloat(event.value); + const seasonOnSelect = React.useCallback((value) => { if (typeof onSelect === 'function') { onSelect({ type: 'select', @@ -61,12 +61,11 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
Prev
- 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} - direction={'bottom-left'} options={options} - selected={selected} + title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} + selectedOption={selectedSeason} onSelect={seasonOnSelect} />