Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streams List: Season and Episode picker when no streams loaded #827

Open
wants to merge 32 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e8a6e72
feat(NumberInput): added NumberInput common component
Botsy Feb 7, 2025
15c6a23
feat(EpisodePicker): added season and episode picker when no streams …
Botsy Feb 7, 2025
461c9d3
fix(EpisodePicker): fix export
Botsy Feb 7, 2025
39f168a
fix(EpisodePicker): handle season 0 as value
Botsy Feb 7, 2025
538e462
fix(StreamsList): hide Install addons button if episode is upcoming
Botsy Feb 7, 2025
36a2896
fix(NumberInput): use color variable for font color
Botsy Feb 7, 2025
7fa4f46
feat(StreamsList): added upcoming label when no results
Botsy Feb 7, 2025
5e98355
fix(NumberInput): fix check for min and max values
Botsy Feb 10, 2025
6b30b90
fix(MetaDetails): handle search for any episode and season
Botsy Feb 10, 2025
34808d6
fix(EpisodePicker): set season and episode from url
Botsy Feb 10, 2025
0c9b992
Merge branch 'development' into feat/season-episode-inputs
Botsy Feb 10, 2025
d407e6c
fix(NumberInput): min & max validation when entering value from keyboard
Botsy Feb 10, 2025
fbdfa11
fix(StreamsList): add scroll when episode picker is shown on landscap…
Botsy Feb 10, 2025
f7494d6
fix(EpisodePicker): typings
Botsy Feb 10, 2025
6ca94a2
fix(EpisodePicker): unify styles with install addons button
Botsy Feb 11, 2025
3f60df9
refactor(EpisodePicker): improve styles and typings
Botsy Feb 11, 2025
3c2ab92
fix(EpisodePicker): make 0 the initial value for season when no value…
Botsy Feb 11, 2025
3bc075d
fix(EpisodePicker): remove named export
Botsy Feb 12, 2025
1721810
fix(NumberInput): follow style name convention
Botsy Feb 12, 2025
7a79e31
fix(EpisodePicker): use default export in StreamsList
Botsy Feb 12, 2025
6274115
refactor(EpisodePicker): simplify setting season and episode initial …
Botsy Feb 12, 2025
d7974ba
Update src/components/NumberInput/NumberInput.tsx
Botsy Feb 12, 2025
3dcd002
Update src/components/NumberInput/NumberInput.tsx
Botsy Feb 12, 2025
37020f3
Update src/routes/MetaDetails/StreamsList/StreamsList.js
Botsy Feb 12, 2025
232c64b
Update src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker…
Botsy Feb 12, 2025
aa3dedf
fix: linting
Botsy Feb 12, 2025
10a36d2
Update src/components/NumberInput/NumberInput.tsx
Botsy Feb 12, 2025
f678375
refactor(NumberInput): apply suggested improvements
Botsy Feb 12, 2025
4cd9db5
fix(NumberInput): remove unused import
Botsy Feb 12, 2025
07cc2a9
refactor(EpisodePicker): apply suggested improvements
Botsy Feb 12, 2025
675328c
fix(StreamsList): show EpisodePicker only if type is series
Botsy Feb 12, 2025
6999ef6
Update src/components/NumberInput/NumberInput.tsx
Botsy Feb 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/components/NumberInput/NumberInput.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (C) 2017-2025 Smart code 203358507

.number-input {
user-select: text;
display: flex;
max-width: 14rem;
height: 3.5rem;
margin-bottom: 1rem;
color: var(--primary-foreground-color);
background: var(--overlay-color);
border-radius: 3.5rem;

.button {
flex: none;
width: 3.5rem;
height: 3.5rem;
padding: 1rem;
background: var(--overlay-color);
border: none;
border-radius: 100%;
cursor: pointer;
z-index: 1;

.icon {
width: 100%;
height: 100%;
}
}

.number-display {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 1rem;

&::-moz-focus-inner {
border: none;
}

.label {
font-size: 0.8rem;
font-weight: 400;
opacity: 0.7;
}

.value {
font-size: 1.2rem;
display: flex;
justify-content: center;
width: 100%;
color: var(--primary-foreground-color);
text-align: center;
appearance: none;

&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
}
}
123 changes: 123 additions & 0 deletions src/components/NumberInput/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (C) 2017-2025 Smart code 203358507

import Icon from '@stremio/stremio-icons/react';
import React, { ChangeEvent, forwardRef, useCallback, useEffect, useState } from 'react';
import { type KeyboardEvent, type InputHTMLAttributes } from 'react';
import classnames from 'classnames';
import styles from './NumberInput.less';
import Button from '../Button';

type Props = InputHTMLAttributes<HTMLInputElement> & {
containerClassName?: string;
className?: string;
disabled?: boolean;
showButtons?: boolean;
defaultValue?: number;
label?: string;
min?: number;
max?: number;
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
onSubmit?: (event: KeyboardEvent<HTMLInputElement>) => void;
onUpdate?: (value: number) => void;
};

const NumberInput = forwardRef<HTMLInputElement, Props>(({ defaultValue, showButtons, onUpdate, ...props }, ref) => {
const [value, setValue] = useState<number>(defaultValue || 0);
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
props.onKeyDown && props.onKeyDown(event);

if (event.key === 'Enter' ) {
Botsy marked this conversation as resolved.
Show resolved Hide resolved
props.onSubmit && props.onSubmit(event);
}
}, [props.onKeyDown, props.onSubmit]);

const handleIncrease = () => {
const { max } = props;
if (max !== undefined) {
return setValue((prevVal) => {
const value = prevVal || 0;
return value + 1 > max ? max : value + 1;
});
}
setValue((prevVal) => {
const value = prevVal || 0;
return value + 1;
});
};

const handleDecrease = () => {
const { min } = props;
if (min !== undefined) {
return setValue((prevVal) => {
const value = prevVal || 0;
return value - 1 < min ? min : value - 1;
});
}
setValue((prevVal) => {
const value = prevVal || 0;
return value - 1;
});
};

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const min = props.min || 0;
let newValue = event.target.valueAsNumber;
if (newValue && newValue < min) {
newValue = min;
}
if (props.max !== undefined && newValue && newValue > props.max) {
newValue = props.max;
}
setValue(newValue);
};

useEffect(() => {
if (typeof onUpdate === 'function') {
onUpdate(value);
}
}, [value]);

return (
<div className={classnames(props.containerClassName, styles['number-input'])}>
{
showButtons ?
<Button
className={styles['button']}
onClick={handleDecrease}
disabled={props.disabled || (props.min !== undefined ? value <= props.min : false)}>
<Icon className={styles['icon']} name={'remove'} />
</Button>
: null
}
<div className={classnames(styles['number-display'], showButtons ? styles['buttons-container'] : '')}>
Botsy marked this conversation as resolved.
Show resolved Hide resolved
{
props.label ?

Check failure on line 94 in src/components/NumberInput/NumberInput.tsx

View workflow job for this annotation

GitHub Actions / build

Trailing spaces not allowed
<div className={styles['label']}>{props.label}</div>
: null

Check failure on line 96 in src/components/NumberInput/NumberInput.tsx

View workflow job for this annotation

GitHub Actions / build

Expected indentation of 24 spaces but found 20
}
<input
ref={ref}
type={'number'}
tabIndex={0}
value={value}
{...props}
className={classnames(props.className, styles['value'], { 'disabled': props.disabled })}
onChange={handleChange}
onKeyDown={onKeyDown}
/>
</div>
{
showButtons ?
<Button
className={styles['button']} onClick={handleIncrease} disabled={props.disabled || (props.max !== undefined ? value >= props.max : false)}>
<Icon className={styles['icon']} name={'add'} />
</Button>
: null
}
</div>
);
});

NumberInput.displayName = 'NumberInput';

export default NumberInput;
5 changes: 5 additions & 0 deletions src/components/NumberInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) 2017-2025 Smart code 203358507

import NumberInput from './NumberInput';

export default NumberInput;
2 changes: 2 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ModalDialog from './ModalDialog';
import Multiselect from './Multiselect';
import MultiselectMenu from './MultiselectMenu';
import { HorizontalNavBar, VerticalNavBar } from './NavBar';
import NumberInput from './NumberInput';
import Popup from './Popup';
import RadioButton from './RadioButton';
import SearchBar from './SearchBar';
Expand Down Expand Up @@ -48,6 +49,7 @@ export {
MultiselectMenu,
HorizontalNavBar,
VerticalNavBar,
NumberInput,
Popup,
RadioButton,
SearchBar,
Expand Down
10 changes: 9 additions & 1 deletion src/routes/MetaDetails/MetaDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ const MetaDetails = ({ urlParams, queryParams }) => {
const seasonOnSelect = React.useCallback((event) => {
setSeason(event.value);
}, [setSeason]);
const handleEpisodeSearch = React.useCallback((season, episode) => {
const searchVideoHash = encodeURIComponent(`${urlParams.id}:${season}:${episode}`);
const url = window.location.hash;
const searchVideoPath = url.replace(encodeURIComponent(urlParams.videoId), searchVideoHash);
window.location = searchVideoPath;
}, [urlParams, window.location]);

const renderBackgroundImageFallback = React.useCallback(() => null, []);
const renderBackground = React.useMemo(() => !!(
metaPath &&
Expand Down Expand Up @@ -129,7 +136,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
metaDetails.metaItem === null ?
<div className={styles['meta-message-container']}>
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<div className={styles['message-label']}>No addons ware requested for this meta!</div>
<div className={styles['message-label']}>No addons were requested for this meta!</div>
</div>
:
metaDetails.metaItem.content.type === 'Err' ?
Expand Down Expand Up @@ -169,6 +176,7 @@ const MetaDetails = ({ urlParams, queryParams }) => {
className={styles['streams-list']}
streams={metaDetails.streams}
video={video}
onEpisodeSearch={handleEpisodeSearch}
/>
:
metaPath !== null ?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (C) 2017-2025 Smart code 203358507

.button-container {
flex: none;
align-self: stretch;
display: flex;
align-items: center;
justify-content: center;
border: var(--focus-outline-size) solid var(--primary-accent-color);
background-color: var(--primary-accent-color);
height: 4rem;
padding: 0 2rem;
margin: 1rem auto;
border-radius: 2rem;

&:hover {
background-color: transparent;
}

.label {
flex: 0 1 auto;
font-size: 1rem;
font-weight: 700;
max-height: 3.5rem;
text-align: center;
color: var(--primary-foreground-color);
margin-bottom: 0;
}
}
44 changes: 44 additions & 0 deletions src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (C) 2017-2025 Smart code 203358507

import React from 'react';
import { useTranslation } from 'react-i18next';
import { Button, NumberInput } from 'stremio/components';
import styles from './EpisodePicker.less';

type Props = {
className?: string,
seriesId: string;
onSubmit: (season: number, episode: number) => void;
};

const EpisodePicker = ({ className, onSubmit }: Props) => {
const { t } = useTranslation();
const splitPath = window.location.hash.split('/');
const videoId = decodeURIComponent(splitPath[splitPath.length - 1]);
const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : [];
const [season, setSeason] = React.useState(isNaN(parseInt(pathSeason)) ? 0 : parseInt(pathSeason));
const [episode, setEpisode] = React.useState(isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode));
const handleSeasonChange = (value: number) => setSeason(!isNaN(value) ? value : 0);

const handleEpisodeChange = (value: number) => setEpisode(!isNaN(value) ? value : 1);

const handleSubmit = React.useCallback(() => {
if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) {
onSubmit(season, episode);
}
}, [onSubmit, season, episode]);

const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]);

return (

Check failure on line 33 in src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx

View workflow job for this annotation

GitHub Actions / build

Expected indentation of 4 spaces but found 0
<div className={className}>
<NumberInput min={0} label={t('SEASON')} defaultValue={season} onUpdate={handleSeasonChange} showButtons />
<NumberInput min={1} label={t('EPISODE')} defaultValue={episode} onUpdate={handleEpisodeChange} showButtons />
<Button className={styles['button-container']} onClick={handleSubmit} disabled={disabled}>
<div className={styles['label']}>{t('SIDEBAR_SHOW_STREAMS')}</div>
</Button>
</div>
);
};

export default EpisodePicker;
5 changes: 5 additions & 0 deletions src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (C) 2017-2025 Smart code 203358507

import SeasonEpisodePicker from './EpisodePicker';

export default SeasonEpisodePicker;
22 changes: 18 additions & 4 deletions src/routes/MetaDetails/StreamsList/StreamsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
const Stream = require('./Stream');
const styles = require('./styles');
const { usePlatform, useProfile } = require('stremio/common');
const { default: SeasonEpisodePicker } = require('./EpisodePicker');

const ALL_ADDONS_KEY = 'ALL';

const StreamsList = ({ className, video, ...props }) => {
const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => {
const { t } = useTranslation();
const { core } = useServices();
const platform = usePlatform();
Expand All @@ -25,8 +26,8 @@
setSelectedAddon(event.value);
}, [platform]);
const showInstallAddonsButton = React.useMemo(() => {
return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true;
}, [profile]);
return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true && !video?.upcoming;
}, [profile, video]);
const backButtonOnClick = React.useCallback(() => {
if (video.deepLinks && typeof video.deepLinks.metaDetailsVideos === 'string') {
window.location.replace(video.deepLinks.metaDetailsVideos + (
Expand Down Expand Up @@ -93,6 +94,11 @@
onSelect: onAddonSelected
};
}, [streamsByAddon, selectedAddon]);

const handleEpisodePicker = React.useCallback((season, episode) => {
onEpisodeSearch(season, episode);
}, [onEpisodeSearch]);

return (
<div className={classnames(className, styles['streams-list-container'])}>
<div className={styles['select-choices-wrapper']}>
Expand Down Expand Up @@ -122,12 +128,19 @@
{
props.streams.length === 0 ?
<div className={styles['message-container']}>
<SeasonEpisodePicker className={styles['search']} onSubmit={handleEpisodePicker} />
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<div className={styles['label']}>No addons were requested for streams!</div>
</div>
:
props.streams.every((streams) => streams.content.type === 'Err') ?
<div className={styles['message-container']}>
<SeasonEpisodePicker className={styles['search']} onSubmit={handleEpisodePicker} />
{
video?.upcoming ?

Check failure on line 140 in src/routes/MetaDetails/StreamsList/StreamsList.js

View workflow job for this annotation

GitHub Actions / build

Trailing spaces not allowed
<div className={styles['label']}>{t('UPCOMING')}...</div>
: null

Check failure on line 142 in src/routes/MetaDetails/StreamsList/StreamsList.js

View workflow job for this annotation

GitHub Actions / build

Expected indentation of 36 spaces but found 32
}
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<div className={styles['label']}>{t('NO_STREAM')}</div>
{
Expand Down Expand Up @@ -193,7 +206,8 @@
StreamsList.propTypes = {
className: PropTypes.string,
streams: PropTypes.arrayOf(PropTypes.object).isRequired,
video: PropTypes.object
video: PropTypes.object,
onEpisodeSearch: PropTypes.func
};

module.exports = StreamsList;
Loading
Loading