mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-21 07:32:02 +00:00
Merge branch 'development' into fix/remote-https-disabled-option
This commit is contained in:
commit
a7bbcb164a
28 changed files with 612 additions and 121 deletions
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
|
|
@ -1,13 +1,20 @@
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- development
|
||||||
|
tags-ignore:
|
||||||
- '**'
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- development
|
||||||
# Allow manual dispatch in GH
|
# Allow manual dispatch in GH
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
@ -24,7 +31,7 @@ jobs:
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
# Create recursivelly the destiantion dir with
|
# Create recursivelly the destiantion dir with
|
||||||
# "--parrents where no error if existing, make parent directories as needed."
|
# "--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
|
- name: Deploy to GitHub Pages
|
||||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
|
|
@ -33,5 +40,5 @@ jobs:
|
||||||
publish_dir: ./build
|
publish_dir: ./build
|
||||||
# in stremio, we use `feat/features-name` or `fix/this-bug`
|
# in stremio, we use `feat/features-name` or `fix/this-bug`
|
||||||
# so we need a recursive creation of the destination dir
|
# 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
|
allow_empty_commit: true
|
||||||
|
|
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -1,18 +1,18 @@
|
||||||
{
|
{
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"version": "5.0.0-beta.8",
|
"version": "5.0.0-beta.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"version": "5.0.0-beta.8",
|
"version": "5.0.0-beta.9",
|
||||||
"license": "gpl-2.0",
|
"license": "gpl-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.16.0",
|
"@babel/runtime": "7.16.0",
|
||||||
"@sentry/browser": "6.13.3",
|
"@sentry/browser": "6.13.3",
|
||||||
"@stremio/stremio-colors": "5.0.1",
|
"@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-icons": "5.2.0",
|
||||||
"@stremio/stremio-video": "0.0.38",
|
"@stremio/stremio-video": "0.0.38",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
|
|
@ -2971,9 +2971,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@stremio/stremio-core-web": {
|
"node_modules/@stremio/stremio-core-web": {
|
||||||
"version": "0.47.2",
|
"version": "0.47.8",
|
||||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.47.2.tgz",
|
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.47.8.tgz",
|
||||||
"integrity": "sha512-kJXkshXT5f5go137id9MHrVA7PfHao2pGSxfEBbMDGFCqAVfF4jRFTXmfLC0cS1R+EjYhajUrSsXnEddtb2c7g==",
|
"integrity": "sha512-X5yKSCm5DXR7U6oIO+2kaI1q3TnaWP6df/HFa1RBi/uw+8IYk+FB8GWpryxXyisJTFiUfQgcJDIlHROauaBQkg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.24.1"
|
"@babel/runtime": "7.24.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
{
|
{
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"displayName": "Stremio",
|
"displayName": "Stremio",
|
||||||
"version": "5.0.0-beta.8",
|
"version": "5.0.0-beta.9",
|
||||||
"author": "Smart Code OOD",
|
"author": "Smart Code OOD",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "gpl-2.0",
|
"license": "gpl-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack serve --mode development",
|
"start": "webpack serve --mode development",
|
||||||
|
"start-prod": "webpack serve --mode production",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"lint": "eslint src"
|
"lint": "eslint src"
|
||||||
|
|
@ -15,7 +16,7 @@
|
||||||
"@babel/runtime": "7.16.0",
|
"@babel/runtime": "7.16.0",
|
||||||
"@sentry/browser": "6.13.3",
|
"@sentry/browser": "6.13.3",
|
||||||
"@stremio/stremio-colors": "5.0.1",
|
"@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-icons": "5.2.0",
|
||||||
"@stremio/stremio-video": "0.0.38",
|
"@stremio/stremio-video": "0.0.38",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
className: styles['install-button'],
|
||||||
|
|
@ -131,7 +133,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
|
||||||
}
|
}
|
||||||
:
|
:
|
||||||
null;
|
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]);
|
}, [addonDetails, onCloseRequest]);
|
||||||
const modalBackground = React.useMemo(() => {
|
const modalBackground = React.useMemo(() => {
|
||||||
return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null;
|
return addonDetails.remoteAddon?.content.type === 'Ready' ? addonDetails.remoteAddon.content.content.manifest.background : null;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const classnames = require('classnames');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
const { useLongPress } = require('use-long-press');
|
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 longPress = useLongPress(onLongPress, { detect: 'pointer' });
|
||||||
const onKeyDown = React.useCallback((event) => {
|
const onKeyDown = React.useCallback((event) => {
|
||||||
if (typeof props.onKeyDown === 'function') {
|
if (typeof props.onKeyDown === 'function') {
|
||||||
|
|
@ -42,6 +42,7 @@ const Button = React.forwardRef(({ className, href, disabled, children, onLongPr
|
||||||
href,
|
href,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
|
onDoubleClick,
|
||||||
...longPress()
|
...longPress()
|
||||||
},
|
},
|
||||||
children
|
children
|
||||||
|
|
@ -58,6 +59,7 @@ Button.propTypes = {
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
onMouseDown: PropTypes.func,
|
onMouseDown: PropTypes.func,
|
||||||
onLongPress: PropTypes.func,
|
onLongPress: PropTypes.func,
|
||||||
|
onDoubleClick: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Button;
|
module.exports = Button;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const EXTERNAL_PLAYERS = [
|
||||||
{
|
{
|
||||||
label: 'EXTERNAL_PLAYER_DISABLED',
|
label: 'EXTERNAL_PLAYER_DISABLED',
|
||||||
value: null,
|
value: null,
|
||||||
platforms: ['ios', 'android', 'windows', 'linux', 'macos'],
|
platforms: ['ios', 'visionos', 'android', 'windows', 'linux', 'macos'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'EXTERNAL_PLAYER_ALLOW_CHOOSING',
|
label: 'EXTERNAL_PLAYER_ALLOW_CHOOSING',
|
||||||
|
|
@ -54,7 +54,7 @@ const EXTERNAL_PLAYERS = [
|
||||||
{
|
{
|
||||||
label: 'VLC',
|
label: 'VLC',
|
||||||
value: 'vlc',
|
value: 'vlc',
|
||||||
platforms: ['ios', 'android'],
|
platforms: ['ios', 'visionos', 'android'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'MPV',
|
label: 'MPV',
|
||||||
|
|
@ -79,12 +79,17 @@ const EXTERNAL_PLAYERS = [
|
||||||
{
|
{
|
||||||
label: 'Outplayer',
|
label: 'Outplayer',
|
||||||
value: 'outplayer',
|
value: 'outplayer',
|
||||||
platforms: ['ios'],
|
platforms: ['ios', 'visionos'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Moonplayer (VisionOS)',
|
||||||
|
value: 'moonplayer',
|
||||||
|
platforms: ['visionos'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'M3U Playlist',
|
label: 'M3U Playlist',
|
||||||
value: 'm3u',
|
value: 'm3u',
|
||||||
platforms: ['ios', 'android', 'windows', 'linux', 'macos'],
|
platforms: ['ios', 'visionos', 'android', 'windows', 'linux', 'macos'],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
30
src/common/MultiselectMenu/Dropdown/Dropdown.less
Normal file
30
src/common/MultiselectMenu/Dropdown/Dropdown.less
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/common/MultiselectMenu/Dropdown/Dropdown.tsx
Normal file
55
src/common/MultiselectMenu/Dropdown/Dropdown.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div className={classNames(styles['dropdown'], { [styles['open']]: menuOpen })} role={'listbox'}>
|
||||||
|
{
|
||||||
|
level > 0 ?
|
||||||
|
<Button className={styles['back-button']} onClick={onBackButtonClick}>
|
||||||
|
<Icon name={'caret-left'} className={styles['back-button-icon']} />
|
||||||
|
{t('BACK')}
|
||||||
|
</Button>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
options
|
||||||
|
.filter((option: MultiselectMenuOption) => !option.hidden)
|
||||||
|
.map((option: MultiselectMenuOption, index) => (
|
||||||
|
<Option
|
||||||
|
key={index}
|
||||||
|
option={option}
|
||||||
|
onSelect={onSelect}
|
||||||
|
selectedOption={selectedOption}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
29
src/common/MultiselectMenu/Dropdown/Option/Option.less
Normal file
29
src/common/MultiselectMenu/Dropdown/Option/Option.less
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/common/MultiselectMenu/Dropdown/Option/Option.tsx
Normal file
46
src/common/MultiselectMenu/Dropdown/Option/Option.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<Button
|
||||||
|
className={classNames(styles['option'], { [styles['selected']]: selected })}
|
||||||
|
key={option.id}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-selected={selected}
|
||||||
|
>
|
||||||
|
<div className={styles['label']}>{ option.label }</div>
|
||||||
|
{
|
||||||
|
selected && !option.level ?
|
||||||
|
<div className={styles['icon']} />
|
||||||
|
: null
|
||||||
|
|
||||||
|
}
|
||||||
|
{
|
||||||
|
option.level ?
|
||||||
|
<Icon name={'caret-right'} className={styles['option-caret']} />
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Option;
|
||||||
5
src/common/MultiselectMenu/Dropdown/Option/index.ts
Normal file
5
src/common/MultiselectMenu/Dropdown/Option/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (C) 2017-2024 Smart code 203358507
|
||||||
|
|
||||||
|
import Option from './Option';
|
||||||
|
|
||||||
|
export default Option;
|
||||||
5
src/common/MultiselectMenu/Dropdown/index.ts
Normal file
5
src/common/MultiselectMenu/Dropdown/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (C) 2017-2024 Smart code 203358507
|
||||||
|
|
||||||
|
import Dropdown from './Dropdown';
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
39
src/common/MultiselectMenu/MultiselectMenu.less
Normal file
39
src/common/MultiselectMenu/MultiselectMenu.less
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/common/MultiselectMenu/MultiselectMenu.tsx
Normal file
57
src/common/MultiselectMenu/MultiselectMenu.tsx
Normal file
|
|
@ -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<number>(0);
|
||||||
|
|
||||||
|
const onOptionSelect = (value: number) => {
|
||||||
|
level ? setLevel(level + 1) : onSelect(value), closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles['multiselect-menu'], className)} ref={multiselectMenuRef}>
|
||||||
|
<Button
|
||||||
|
className={classNames(styles['multiselect-button'], { [styles['open']]: menuOpen })}
|
||||||
|
onClick={toggleMenu}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-haspopup='listbox'
|
||||||
|
aria-expanded={menuOpen}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<Icon name={'caret-down'} className={classNames(styles['icon'], { [styles['open']]: menuOpen })} />
|
||||||
|
</Button>
|
||||||
|
{
|
||||||
|
menuOpen ?
|
||||||
|
<Dropdown
|
||||||
|
level={level}
|
||||||
|
setLevel={setLevel}
|
||||||
|
options={options}
|
||||||
|
onSelect={onOptionSelect}
|
||||||
|
menuOpen={menuOpen}
|
||||||
|
selectedOption={selectedOption}
|
||||||
|
/>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultiselectMenu;
|
||||||
5
src/common/MultiselectMenu/index.ts
Normal file
5
src/common/MultiselectMenu/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (C) 2017-2024 Smart code 203358507
|
||||||
|
|
||||||
|
import MultiselectMenu from './MultiselectMenu';
|
||||||
|
|
||||||
|
export default MultiselectMenu;
|
||||||
9
src/common/MultiselectMenu/types.d.ts
vendored
Normal file
9
src/common/MultiselectMenu/types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
type MultiselectMenuOption = {
|
||||||
|
id?: number;
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
destination?: string;
|
||||||
|
default?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
level?: MultiselectMenuOption[];
|
||||||
|
};
|
||||||
|
|
@ -15,8 +15,17 @@ const NavTabButton = ({ className, logo, icon, label, href, selected, onClick })
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
), [icon]);
|
), [icon]);
|
||||||
|
const onDoubleClick = () => {
|
||||||
|
const scrollableElements = document.querySelectorAll('div');
|
||||||
|
|
||||||
|
scrollableElements.forEach((element) => {
|
||||||
|
if (element.scrollTop > 0) {
|
||||||
|
element.scrollTop = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Button className={classnames(className, styles['nav-tab-button-container'], { 'selected': selected })} title={label} tabIndex={-1} href={href} onClick={onClick}>
|
<Button className={classnames(className, styles['nav-tab-button-container'], { 'selected': selected })} title={label} tabIndex={-1} href={href} onClick={onClick} onDoubleClick={onDoubleClick}>
|
||||||
{
|
{
|
||||||
typeof logo === 'string' && logo.length > 0 ?
|
typeof logo === 'string' && logo.length > 0 ?
|
||||||
<Image
|
<Image
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ const MetaPreview = require('./MetaPreview');
|
||||||
const MetaRow = require('./MetaRow');
|
const MetaRow = require('./MetaRow');
|
||||||
const ModalDialog = require('./ModalDialog');
|
const ModalDialog = require('./ModalDialog');
|
||||||
const Multiselect = require('./Multiselect');
|
const Multiselect = require('./Multiselect');
|
||||||
|
const { default: MultiselectMenu } = require('./MultiselectMenu');
|
||||||
const { HorizontalNavBar, VerticalNavBar } = require('./NavBar');
|
const { HorizontalNavBar, VerticalNavBar } = require('./NavBar');
|
||||||
const PaginationInput = require('./PaginationInput');
|
const PaginationInput = require('./PaginationInput');
|
||||||
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
|
const PlayIconCircleCentered = require('./PlayIconCircleCentered');
|
||||||
|
|
@ -63,6 +64,7 @@ module.exports = {
|
||||||
MetaRow,
|
MetaRow,
|
||||||
ModalDialog,
|
ModalDialog,
|
||||||
Multiselect,
|
Multiselect,
|
||||||
|
MultiselectMenu,
|
||||||
HorizontalNavBar,
|
HorizontalNavBar,
|
||||||
VerticalNavBar,
|
VerticalNavBar,
|
||||||
PaginationInput,
|
PaginationInput,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
// Copyright (C) 2017-2024 Smart code 203358507
|
||||||
|
|
||||||
// this detects ipad properly in safari
|
// this detects ipad properly in safari
|
||||||
// while bowser does not
|
// while bowser does not
|
||||||
function iOS() {
|
const iOS = () => {
|
||||||
return [
|
return [
|
||||||
'iPad Simulator',
|
'iPad Simulator',
|
||||||
'iPhone Simulator',
|
'iPhone Simulator',
|
||||||
|
|
@ -11,14 +11,22 @@ function iOS() {
|
||||||
'iPhone',
|
'iPhone',
|
||||||
'iPod'
|
'iPod'
|
||||||
].includes(navigator.platform)
|
].includes(navigator.platform)
|
||||||
|| (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
|
|| (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
|
||||||
}
|
};
|
||||||
|
|
||||||
const Bowser = require('bowser');
|
const Bowser = require('bowser');
|
||||||
|
|
||||||
const browser = Bowser.parse(window.navigator?.userAgent || '');
|
const browser = Bowser.parse(window.navigator?.userAgent || '');
|
||||||
|
|
||||||
const name = iOS() ? 'ios' : (browser?.os?.name || 'unknown').toLowerCase();
|
// Edge case: iPad is included in this function
|
||||||
|
// Keep in mind maxTouchPoints for Vision Pro might change in the future
|
||||||
|
const isVisionProUser = () => {
|
||||||
|
const isMacintosh = navigator.userAgent.includes('Macintosh');
|
||||||
|
const hasFiveTouchPoints = navigator.maxTouchPoints === 5;
|
||||||
|
return isMacintosh && hasFiveTouchPoints;
|
||||||
|
};
|
||||||
|
|
||||||
|
const name = isVisionProUser() ? 'visionos' : (iOS() ? 'ios' : (browser?.os?.name || 'unknown').toLowerCase());
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
27
src/common/useOutsideClick.ts
Normal file
27
src/common/useOutsideClick.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright (C) 2017-2024 Smart code 203358507
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
const useOutsideClick = (callback: () => void) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||||
|
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', handleClickOutside);
|
||||||
|
document.addEventListener('touchend', handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', handleClickOutside);
|
||||||
|
document.removeEventListener('touchend', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOutsideClick;
|
||||||
|
|
@ -82,32 +82,13 @@ const Intro = ({ queryParams }) => {
|
||||||
openLoaderModal();
|
openLoaderModal();
|
||||||
getFacebookToken()
|
getFacebookToken()
|
||||||
.then((accessToken) => {
|
.then((accessToken) => {
|
||||||
return fetch('https://www.strem.io/fb-login-with-token/' + encodeURIComponent(accessToken))
|
|
||||||
.then((resp) => resp.json())
|
|
||||||
.catch(() => {
|
|
||||||
throw new Error('Login failed at getting token from Stremio');
|
|
||||||
})
|
|
||||||
.then(({ user } = {}) => {
|
|
||||||
if (!user || typeof user.email !== 'string' || typeof user.fbLoginToken !== 'string') {
|
|
||||||
throw new Error('Login failed at getting token from Stremio');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
email: user.email,
|
|
||||||
password: user.fbLoginToken
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(({ email, password }) => {
|
|
||||||
core.transport.dispatch({
|
core.transport.dispatch({
|
||||||
action: 'Ctx',
|
action: 'Ctx',
|
||||||
args: {
|
args: {
|
||||||
action: 'Authenticate',
|
action: 'Authenticate',
|
||||||
args: {
|
args: {
|
||||||
type: 'Login',
|
type: 'Facebook',
|
||||||
email,
|
token: accessToken,
|
||||||
password,
|
|
||||||
facebook: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
const { Button, Image, useProfile, platform, useToast } = require('stremio/common');
|
const { t } = require('i18next');
|
||||||
|
const { Button, Image, useProfile, platform, useToast, Popup, useBinaryState } = require('stremio/common');
|
||||||
const { useServices } = require('stremio/services');
|
const { useServices } = require('stremio/services');
|
||||||
|
const { useRouteFocused } = require('stremio-router');
|
||||||
const StreamPlaceholder = require('./StreamPlaceholder');
|
const StreamPlaceholder = require('./StreamPlaceholder');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
|
|
@ -13,6 +15,40 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
||||||
const profile = useProfile();
|
const profile = useProfile();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { core } = useServices();
|
const { core } = useServices();
|
||||||
|
const routeFocused = useRouteFocused();
|
||||||
|
|
||||||
|
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||||
|
|
||||||
|
const popupLabelOnMouseUp = React.useCallback((event) => {
|
||||||
|
if (!event.nativeEvent.togglePopupPrevented) {
|
||||||
|
if (event.nativeEvent.ctrlKey || event.nativeEvent.button === 2) {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const popupLabelOnContextMenu = React.useCallback((event) => {
|
||||||
|
if (!event.nativeEvent.togglePopupPrevented && !event.nativeEvent.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, [toggleMenu]);
|
||||||
|
const popupLabelOnLongPress = React.useCallback((event) => {
|
||||||
|
if (event.nativeEvent.pointerType !== 'mouse' && !event.nativeEvent.togglePopupPrevented) {
|
||||||
|
toggleMenu();
|
||||||
|
}
|
||||||
|
}, [toggleMenu]);
|
||||||
|
const popupMenuOnPointerDown = React.useCallback((event) => {
|
||||||
|
event.nativeEvent.togglePopupPrevented = true;
|
||||||
|
}, []);
|
||||||
|
const popupMenuOnContextMenu = React.useCallback((event) => {
|
||||||
|
event.nativeEvent.togglePopupPrevented = true;
|
||||||
|
}, []);
|
||||||
|
const popupMenuOnClick = React.useCallback((event) => {
|
||||||
|
event.nativeEvent.togglePopupPrevented = true;
|
||||||
|
}, []);
|
||||||
|
const popupMenuOnKeyDown = React.useCallback((event) => {
|
||||||
|
event.nativeEvent.buttonClickPrevented = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const href = React.useMemo(() => {
|
const href = React.useMemo(() => {
|
||||||
return deepLinks ?
|
return deepLinks ?
|
||||||
|
|
@ -47,6 +83,10 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
||||||
null;
|
null;
|
||||||
}, [href, deepLinks]);
|
}, [href, deepLinks]);
|
||||||
|
|
||||||
|
const streamLink = React.useMemo(() => {
|
||||||
|
return deepLinks?.externalPlayer?.download;
|
||||||
|
}, [deepLinks]);
|
||||||
|
|
||||||
const markVideoAsWatched = React.useCallback(() => {
|
const markVideoAsWatched = React.useCallback(() => {
|
||||||
if (typeof videoId === 'string') {
|
if (typeof videoId === 'string') {
|
||||||
core.transport.dispatch({
|
core.transport.dispatch({
|
||||||
|
|
@ -74,41 +114,101 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
||||||
}
|
}
|
||||||
}, [props.onClick, profile.settings, markVideoAsWatched]);
|
}, [props.onClick, profile.settings, markVideoAsWatched]);
|
||||||
|
|
||||||
|
const copyStreamLink = React.useCallback((event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
closeMenu();
|
||||||
|
if (streamLink) {
|
||||||
|
navigator.clipboard.writeText(streamLink)
|
||||||
|
.then(() => {
|
||||||
|
toast.show({
|
||||||
|
type: 'success',
|
||||||
|
title: t('PLAYER_COPY_STREAM_SUCCESS'),
|
||||||
|
timeout: 4000
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.show({
|
||||||
|
type: 'error',
|
||||||
|
title: t('PLAYER_COPY_STREAM_ERROR'),
|
||||||
|
timeout: 4000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [streamLink]);
|
||||||
|
|
||||||
const renderThumbnailFallback = React.useCallback(() => (
|
const renderThumbnailFallback = React.useCallback(() => (
|
||||||
<Icon className={styles['placeholder-icon']} name={'ic_broken_link'} />
|
<Icon className={styles['placeholder-icon']} name={'ic_broken_link'} />
|
||||||
), []);
|
), []);
|
||||||
|
|
||||||
return (
|
const renderLabel = React.useMemo(() => function renderLabel({ className, children, ...props }) {
|
||||||
<Button className={classnames(className, styles['stream-container'])} title={addonName} href={href} download={download} target={target} onClick={onClick}>
|
return (
|
||||||
<div className={styles['info-container']}>
|
<Button className={classnames(className, styles['stream-container'])} title={addonName} href={href} target={target} download={download} onClick={onClick} {...props}>
|
||||||
|
<div className={styles['info-container']}>
|
||||||
|
{
|
||||||
|
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||||
|
<div className={styles['thumbnail-container']} title={name || addonName}>
|
||||||
|
<Image
|
||||||
|
className={styles['thumbnail']}
|
||||||
|
src={thumbnail}
|
||||||
|
alt={' '}
|
||||||
|
renderFallback={renderThumbnailFallback}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div className={styles['addon-name-container']} title={name || addonName}>
|
||||||
|
<div className={styles['addon-name']}>{name || addonName}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
progress !== null && !isNaN(progress) && progress > 0 ?
|
||||||
|
<div className={styles['progress-bar-container']}>
|
||||||
|
<div className={styles['progress-bar']} style={{ width: `${progress}%` }} />
|
||||||
|
<div className={styles['progress-bar-background']} />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={styles['description-container']} title={description}>{description}</div>
|
||||||
|
<Icon className={styles['icon']} name={'play'} />
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, [thumbnail, progress, addonName, name, description, href, target, download, onClick]);
|
||||||
|
|
||||||
|
const renderMenu = React.useMemo(() => function renderMenu() {
|
||||||
|
return (
|
||||||
|
<div className={styles['context-menu-content']} onPointerDown={popupMenuOnPointerDown} onContextMenu={popupMenuOnContextMenu} onClick={popupMenuOnClick} onKeyDown={popupMenuOnKeyDown}>
|
||||||
|
<Button className={styles['context-menu-option-container']} title={t('CTX_PLAY')}>
|
||||||
|
<div className={styles['context-menu-option-label']}>{t('CTX_PLAY')}</div>
|
||||||
|
</Button>
|
||||||
{
|
{
|
||||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
streamLink &&
|
||||||
<div className={styles['thumbnail-container']} title={name || addonName}>
|
<Button className={styles['context-menu-option-container']} title={t('CTX_COPY_STREAM_LINK')} onClick={copyStreamLink}>
|
||||||
<Image
|
<div className={styles['context-menu-option-label']}>{t('CTX_COPY_STREAM_LINK')}</div>
|
||||||
className={styles['thumbnail']}
|
</Button>
|
||||||
src={thumbnail}
|
|
||||||
alt={' '}
|
|
||||||
renderFallback={renderThumbnailFallback}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
<div className={styles['addon-name-container']} title={name || addonName}>
|
|
||||||
<div className={styles['addon-name']}>{name || addonName}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
progress !== null && !isNaN(progress) && progress > 0 ?
|
|
||||||
<div className={styles['progress-bar-container']}>
|
|
||||||
<div className={styles['progress-bar']} style={{ width: `${progress}%` }} />
|
|
||||||
<div className={styles['progress-bar-background']} />
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['description-container']} title={description}>{description}</div>
|
);
|
||||||
<Icon className={styles['icon']} name={'play'} />
|
}, [copyStreamLink, onClick]);
|
||||||
</Button>
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!routeFocused) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
}, [routeFocused]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popup
|
||||||
|
className={className}
|
||||||
|
onMouseUp={popupLabelOnMouseUp}
|
||||||
|
onLongPress={popupLabelOnLongPress}
|
||||||
|
onContextMenu={popupLabelOnContextMenu}
|
||||||
|
open={menuOpen}
|
||||||
|
onCloseRequest={closeMenu}
|
||||||
|
renderLabel={renderLabel}
|
||||||
|
renderMenu={renderMenu}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@
|
||||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||||
@import (reference) '~stremio/common/screen-sizes.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 {
|
.stream-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -103,6 +111,33 @@
|
||||||
color: var(--primary-foreground-color);
|
color: var(--primary-foreground-color);
|
||||||
background-color: var(--secondary-accent-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) {
|
@media only screen and (max-width: @small) {
|
||||||
|
|
@ -125,6 +160,28 @@
|
||||||
.addon-name {
|
.addon-name {
|
||||||
font-weight: 500;
|
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%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,17 @@ const StreamsList = ({ className, video, ...props }) => {
|
||||||
setSelectedAddon(event.value);
|
setSelectedAddon(event.value);
|
||||||
}, []);
|
}, []);
|
||||||
const backButtonOnClick = React.useCallback(() => {
|
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(() => {
|
const countLoadingAddons = React.useMemo(() => {
|
||||||
return props.streams.filter((stream) => stream.content.type === 'Loading').length;
|
return props.streams.filter((stream) => stream.content.type === 'Loading').length;
|
||||||
}, [props.streams]);
|
}, [props.streams]);
|
||||||
|
|
@ -78,6 +87,30 @@ const StreamsList = ({ className, video, ...props }) => {
|
||||||
}, [streamsByAddon, selectedAddon]);
|
}, [streamsByAddon, selectedAddon]);
|
||||||
return (
|
return (
|
||||||
<div className={classnames(className, styles['streams-list-container'])}>
|
<div className={classnames(className, styles['streams-list-container'])}>
|
||||||
|
<div className={styles['select-choices-wrapper']}>
|
||||||
|
{
|
||||||
|
video ?
|
||||||
|
<React.Fragment>
|
||||||
|
<Button className={classnames(styles['button-container'], styles['back-button-container'])} tabIndex={-1} onClick={backButtonOnClick}>
|
||||||
|
<Icon className={styles['icon']} name={'chevron-back'} />
|
||||||
|
</Button>
|
||||||
|
<div className={styles['episode-title']}>
|
||||||
|
{`S${video?.season}E${video?.episode} ${(video?.title)}`}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
Object.keys(streamsByAddon).length > 1 ?
|
||||||
|
<Multiselect
|
||||||
|
{...selectableOptions}
|
||||||
|
className={styles['select-input-container']}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
props.streams.length === 0 ?
|
props.streams.length === 0 ?
|
||||||
<div className={styles['message-container']}>
|
<div className={styles['message-container']}>
|
||||||
|
|
@ -109,30 +142,6 @@ const StreamsList = ({ className, video, ...props }) => {
|
||||||
:
|
:
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<div className={styles['select-choices-wrapper']}>
|
|
||||||
{
|
|
||||||
video ?
|
|
||||||
<React.Fragment>
|
|
||||||
<Button className={classnames(styles['button-container'], styles['back-button-container'])} tabIndex={-1} onClick={backButtonOnClick}>
|
|
||||||
<Icon className={styles['icon']} name={'chevron-back'} />
|
|
||||||
</Button>
|
|
||||||
<div className={styles['episode-title']}>
|
|
||||||
{`S${video?.season}E${video?.episode} ${(video?.title)}`}
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
Object.keys(streamsByAddon).length > 1 ?
|
|
||||||
<Multiselect
|
|
||||||
{...selectableOptions}
|
|
||||||
className={styles['select-input-container']}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className={styles['streams-container']}>
|
<div className={styles['streams-container']}>
|
||||||
{filteredStreams.map((stream, index) => (
|
{filteredStreams.map((stream, index) => (
|
||||||
<Stream
|
<Stream
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@
|
||||||
|
|
||||||
.streams-container {
|
.streams-container {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
overflow: visible;
|
||||||
scrollbar-color: @color-surface-light5-20 transparent;
|
scrollbar-color: @color-surface-light5-20 transparent;
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const { t } = require('i18next');
|
const { t } = require('i18next');
|
||||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
const { Button, Multiselect } = require('stremio/common');
|
const { Button } = require('stremio/common');
|
||||||
const SeasonsBarPlaceholder = require('./SeasonsBarPlaceholder');
|
const SeasonsBarPlaceholder = require('./SeasonsBarPlaceholder');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
const { MultiselectMenu } = require('stremio/common');
|
||||||
|
|
||||||
const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
||||||
const options = React.useMemo(() => {
|
const options = React.useMemo(() => {
|
||||||
|
|
@ -16,8 +17,8 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
||||||
label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')
|
label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')
|
||||||
}));
|
}));
|
||||||
}, [seasons]);
|
}, [seasons]);
|
||||||
const selected = React.useMemo(() => {
|
const selectedSeason = React.useMemo(() => {
|
||||||
return [String(season)];
|
return { label: String(season), value: String(season) };
|
||||||
}, [season]);
|
}, [season]);
|
||||||
const prevNextButtonOnClick = React.useCallback((event) => {
|
const prevNextButtonOnClick = React.useCallback((event) => {
|
||||||
if (typeof onSelect === 'function') {
|
if (typeof onSelect === 'function') {
|
||||||
|
|
@ -35,8 +36,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [season, seasons, onSelect]);
|
}, [season, seasons, onSelect]);
|
||||||
const seasonOnSelect = React.useCallback((event) => {
|
const seasonOnSelect = React.useCallback((value) => {
|
||||||
const value = parseFloat(event.value);
|
|
||||||
if (typeof onSelect === 'function') {
|
if (typeof onSelect === 'function') {
|
||||||
onSelect({
|
onSelect({
|
||||||
type: 'select',
|
type: 'select',
|
||||||
|
|
@ -61,12 +61,11 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => {
|
||||||
<Icon className={styles['icon']} name={'chevron-back'} />
|
<Icon className={styles['icon']} name={'chevron-back'} />
|
||||||
<div className={styles['label']}>Prev</div>
|
<div className={styles['label']}>Prev</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Multiselect
|
<MultiselectMenu
|
||||||
className={styles['seasons-popup-label-container']}
|
className={styles['seasons-popup-label-container']}
|
||||||
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
|
|
||||||
direction={'bottom-left'}
|
|
||||||
options={options}
|
options={options}
|
||||||
selected={selected}
|
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
|
||||||
|
selectedOption={selectedSeason}
|
||||||
onSelect={seasonOnSelect}
|
onSelect={seasonOnSelect}
|
||||||
/>
|
/>
|
||||||
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={'Next season'} data-action={'next'} onClick={prevNextButtonOnClick}>
|
<Button className={classnames(styles['next-season-button'], { 'disabled': nextDisabled })} title={'Next season'} data-action={'next'} onClick={prevNextButtonOnClick}>
|
||||||
|
|
|
||||||
|
|
@ -56,17 +56,14 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [id, released, watched]);
|
}, [id, released, watched]);
|
||||||
const href = React.useMemo(() => {
|
const videoButtonOnClick = React.useCallback(() => {
|
||||||
return deepLinks ?
|
if (deepLinks) {
|
||||||
typeof deepLinks.player === 'string' ?
|
if (typeof deepLinks.player === 'string') {
|
||||||
deepLinks.player
|
window.location = deepLinks.player;
|
||||||
:
|
} else if (typeof deepLinks.metaDetailsStreams === 'string') {
|
||||||
typeof deepLinks.metaDetailsStreams === 'string' ?
|
window.location.replace(deepLinks.metaDetailsStreams);
|
||||||
deepLinks.metaDetailsStreams
|
}
|
||||||
:
|
}
|
||||||
null
|
|
||||||
:
|
|
||||||
null;
|
|
||||||
}, [deepLinks]);
|
}, [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, ...props }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -171,7 +168,7 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
||||||
watched={watched}
|
watched={watched}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
scheduled={scheduled}
|
scheduled={scheduled}
|
||||||
href={href}
|
onClick={videoButtonOnClick}
|
||||||
{...props}
|
{...props}
|
||||||
onMouseUp={popupLabelOnMouseUp}
|
onMouseUp={popupLabelOnMouseUp}
|
||||||
onLongPress={popupLabelOnLongPress}
|
onLongPress={popupLabelOnLongPress}
|
||||||
|
|
|
||||||
|
|
@ -74,5 +74,9 @@
|
||||||
@media only screen and (max-width: @minimum) {
|
@media only screen and (max-width: @minimum) {
|
||||||
.videos-list-container {
|
.videos-list-container {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
|
.videos-container {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue