Compare commits

...

6 commits

Author SHA1 Message Date
Timothy Z.
c3f67454ff
Merge pull request #1150 from Stremio/fix/shortcuts-section-button-visibility
Some checks failed
Build / build (push) Has been cancelled
Settings: Correct shortcuts menu button visibility
2026-03-11 18:01:09 +02:00
Timothy Z.
89515a2a75
Merge pull request #1154 from Stremio/feat/hold-to-speedup
Player: Hold spacebar or left click to speed up 2x
2026-03-11 17:26:49 +02:00
Botzy
5b83fa00b5 feat: hold left mouse btn or spacebar to speed up 2x 2026-03-09 18:15:08 +02:00
Botzy
182782a60f fix: speed menu to display 0.25 speed option 2026-03-09 15:19:02 +02:00
Timothy Z.
3d119db049 improve selected section logic for edge cases 2026-03-04 20:48:01 +02:00
Timothy Z.
df69e6eb18 change shortcuts visibility on mobile 2026-03-04 20:20:31 +02:00
4 changed files with 86 additions and 17 deletions

View file

@ -96,6 +96,11 @@ const Player = ({ urlParams, queryParams }) => {
const isNavigating = React.useRef(false);
const pressTimer = React.useRef(null);
const longPress = React.useRef(false);
const HOLD_DELAY = 200;
const onImplementationChanged = React.useCallback(() => {
video.setSubtitlesSize(settings.subtitlesSize);
video.setSubtitlesOffset(settings.subtitlesOffset);
@ -296,14 +301,14 @@ const Player = ({ urlParams, queryParams }) => {
}, [player.nextVideo, handleNextVideoNavigation, profile.settings]);
const onVideoClick = React.useCallback(() => {
if (video.state.paused !== null) {
if (video.state.paused !== null && !longPress.current) {
if (video.state.paused) {
onPlayRequestedDebounced();
} else {
onPauseRequestedDebounced();
}
}
}, [video.state.paused]);
}, [video.state.paused, longPress.current]);
const onVideoDoubleClick = React.useCallback(() => {
onPlayRequestedDebounced.cancel();
@ -625,11 +630,11 @@ const Player = ({ urlParams, queryParams }) => {
if (video.state.paused) {
onPlayRequested();
setSeeking(false);
} else {
} else if (!pressTimer.current) {
onPauseRequested();
}
}
}, [menusOpen, nextVideoPopupOpen, video.state.paused, onPlayRequested, onPauseRequested]);
}, [menusOpen, nextVideoPopupOpen, video.state.paused, pressTimer.current, onPlayRequested, onPauseRequested]);
onShortcut('seekForward', (combo) => {
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
@ -726,11 +731,31 @@ const Player = ({ urlParams, queryParams }) => {
}, [settings.escExitFullscreen]);
React.useLayoutEffect(() => {
const onKeyUp = (event) => {
if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') {
const onKeyDown = (e) => {
if (e.code !== 'Space' || e.repeat) return;
longPress.current = false;
pressTimer.current = setTimeout(() => {
longPress.current = true;
onPlaybackSpeedChanged(2);
}, HOLD_DELAY);
};
const onKeyUp = (e) => {
if (e.code !== 'Space' && e.code !== 'ArrowRight' && e.code !== 'ArrowLeft') return;
if (e.code === 'ArrowRight' || e.code === 'ArrowLeft') {
setSeeking(false);
return;
}
if (e.code === 'Space') {
clearTimeout(pressTimer.current);
pressTimer.current = null;
onPlaybackSpeedChanged(1);
}
};
const onWheel = ({ deltaY }) => {
if (menusOpen || video.state.volume === null) return;
@ -742,13 +767,41 @@ const Player = ({ urlParams, queryParams }) => {
}
}
};
const onMouseDownHold = (e) => {
if (e.button !== 0) return; // left mouse button only
longPress.current = false;
pressTimer.current = setTimeout(() => {
longPress.current = true;
onPlaybackSpeedChanged(2);
}, HOLD_DELAY);
};
const onMouseUp = (e) => {
if (e.button !== 0) return;
clearTimeout(pressTimer.current);
if (longPress.current) {
onPlaybackSpeedChanged(1);
}
};
if (routeFocused) {
window.addEventListener('keyup', onKeyUp);
window.addEventListener('keydown', onKeyDown);
window.addEventListener('wheel', onWheel);
window.addEventListener('mousedown', onMouseDownHold);
window.addEventListener('mouseup', onMouseUp);
}
return () => {
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('wheel', onWheel);
window.removeEventListener('mousedown', onMouseDownHold);
window.removeEventListener('mouseup', onMouseUp);
};
}, [routeFocused, menusOpen, video.state.volume]);

View file

@ -15,7 +15,7 @@
.options-container {
flex: 0 1 auto;
max-height: calc(3.2rem * 8);
max-height: calc(3.2rem * 10);
padding: 0 1rem 0.5rem;
.option {

View file

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useServices } from 'stremio/services';
import { Button } from 'stremio/components';
import { SECTIONS } from '../constants';
import { usePlatform } from 'stremio/common';
import styles from './Menu.less';
type Props = {
@ -15,6 +16,7 @@ type Props = {
const Menu = ({ selected, streamingServer, onSelect }: Props) => {
const { t } = useTranslation();
const { shell } = useServices();
const platform = usePlatform();
const settings = useMemo(() => (
streamingServer?.settings?.type === 'Ready' ?
@ -35,9 +37,9 @@ const Menu = ({ selected, streamingServer, onSelect }: Props) => {
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.STREAMING })} title={t('SETTINGS_NAV_STREAMING')} data-section={SECTIONS.STREAMING} onClick={onSelect}>
{ t('SETTINGS_NAV_STREAMING') }
</Button>
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.SHORTCUTS })} title={t('SETTINGS_NAV_SHORTCUTS')} data-section={SECTIONS.SHORTCUTS} onClick={onSelect}>
{ !platform.isMobile && <Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.SHORTCUTS })} title={t('SETTINGS_NAV_SHORTCUTS')} data-section={SECTIONS.SHORTCUTS} onClick={onSelect}>
{ t('SETTINGS_NAV_SHORTCUTS') }
</Button>
</Button> }
<div className={styles['spacing']} />
<div className={styles['version-info-label']} title={process.env.VERSION}>

View file

@ -41,13 +41,27 @@ const Settings = () => {
const updateSelectedSectionId = useCallback(() => {
const container = sectionsContainerRef.current;
for (const section of sections) {
const sectionContainer = section.ref.current;
if (sectionContainer && (sectionContainer.offsetTop + container!.offsetTop) < container!.scrollTop + 50) {
setSelectedSectionId(section.id);
}
if (!container) return;
const availableSections = sections.filter((section) => section.ref.current);
if (!availableSections.length) return;
const { scrollTop, clientHeight, scrollHeight, offsetTop } = container;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10;
if (isAtBottom) {
setSelectedSectionId(availableSections[availableSections.length - 1].id);
return;
}
}, []);
const marker = scrollTop + 50;
const activeSection = availableSections.reduce((current, section) => {
const sectionTop = section.ref.current!.offsetTop + offsetTop;
return sectionTop <= marker ? section : current;
}, availableSections[0]);
setSelectedSectionId(activeSection.id);
}, [sections]);
const onMenuSelect = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
const section = sections.find((section) => {
@ -55,11 +69,11 @@ const Settings = () => {
});
const container = sectionsContainerRef.current;
section && container!.scrollTo({
section && container?.scrollTo({
top: section.ref.current!.offsetTop - container!.offsetTop,
behavior: 'smooth'
});
}, []);
}, [sections]);
const onContainerScroll = useCallback(throttle(() => {
updateSelectedSectionId();