Merge branch 'development' into pr/933

This commit is contained in:
Timothy Z. 2025-06-24 09:41:02 +03:00
commit 75bb1b0489
8 changed files with 90 additions and 104 deletions

View file

@ -13,7 +13,7 @@ jobs:
steps: steps:
# Auto assign PR to author # Auto assign PR to author
- name: Auto Assign PR to Author - name: Auto Assign PR to Author
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@ -31,6 +31,7 @@ jobs:
# Dynamic labeling based on PR/Issue title # Dynamic labeling based on PR/Issue title
- name: Label PRs and Issues - name: Label PRs and Issues
if: github.actor != 'dependabot[bot]'
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/github-script@v7 uses: actions/github-script@v7

View file

@ -19,7 +19,7 @@ jobs:
- name: Zip build artifact - name: Zip build artifact
run: zip -r stremio-web.zip ./build run: zip -r stremio-web.zip ./build
- name: Upload build artifact to GitHub release assets - name: Upload build artifact to GitHub release assets
uses: svenstaro/upload-release-action@2.9.0 uses: svenstaro/upload-release-action@2.10.0
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
file: stremio-web.zip file: stremio-web.zip

View file

@ -13,6 +13,7 @@ const Transition = ({ children, when, name }: Props) => {
const [state, setState] = useState('enter'); const [state, setState] = useState('enter');
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [transitionEnded, setTransitionEnded] = useState(false);
const callbackRef = useCallback((element: HTMLElement | null) => { const callbackRef = useCallback((element: HTMLElement | null) => {
setElement(element); setElement(element);
@ -30,12 +31,14 @@ const Transition = ({ children, when, name }: Props) => {
}, [name, state, active, children]); }, [name, state, active, children]);
const onTransitionEnd = useCallback(() => { const onTransitionEnd = useCallback(() => {
setTransitionEnded(true);
state === 'exit' && setMounted(false); state === 'exit' && setMounted(false);
}, [state]); }, [state]);
useEffect(() => { useEffect(() => {
setState(when ? 'enter' : 'exit'); setState(when ? 'enter' : 'exit');
when && setMounted(true); when && setMounted(true);
setTransitionEnded(false);
}, [when]); }, [when]);
useEffect(() => { useEffect(() => {
@ -53,6 +56,7 @@ const Transition = ({ children, when, name }: Props) => {
mounted && cloneElement(children, { mounted && cloneElement(children, {
ref: callbackRef, ref: callbackRef,
className, className,
transitionEnded
}) })
); );
}; };

View file

@ -12,10 +12,10 @@ const useProfile = require('stremio/common/useProfile');
const VideoPlaceholder = require('./VideoPlaceholder'); const VideoPlaceholder = require('./VideoPlaceholder');
const styles = require('./styles'); const styles = require('./styles');
const Video = ({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }) => { const Video = React.forwardRef(({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }, ref) => {
const { t } = useTranslation();
const routeFocused = useRouteFocused(); const routeFocused = useRouteFocused();
const profile = useProfile(); const profile = useProfile();
const { t } = useTranslation();
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
const popupLabelOnMouseUp = React.useCallback((event) => { const popupLabelOnMouseUp = React.useCallback((event) => {
if (!event.nativeEvent.togglePopupPrevented) { if (!event.nativeEvent.togglePopupPrevented) {
@ -71,7 +71,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
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 }) {
const blurThumbnail = profile.settings.hideSpoilers && season && episode && !watched; const blurThumbnail = profile.settings.hideSpoilers && season && episode && !watched;
return ( return (
<Button {...props} className={classnames(className, styles['video-container'])} title={title}> <Button {...props} className={classnames(className, styles['video-container'])} title={title} ref={ref}>
{ {
typeof thumbnail === 'string' && thumbnail.length > 0 ? typeof thumbnail === 'string' && thumbnail.length > 0 ?
<div className={styles['thumbnail-container']}> <div className={styles['thumbnail-container']}>
@ -186,7 +186,7 @@ const Video = ({ className, id, title, thumbnail, season, episode, released, upc
renderMenu={renderMenu} renderMenu={renderMenu}
/> />
); );
}; });
Video.Placeholder = VideoPlaceholder; Video.Placeholder = VideoPlaceholder;

View file

@ -832,6 +832,7 @@ const Player = ({ urlParams, queryParams }) => {
metaItem={player.metaItem?.content} metaItem={player.metaItem?.content}
seriesInfo={player.seriesInfo} seriesInfo={player.seriesInfo}
closeSideDrawer={closeSideDrawer} closeSideDrawer={closeSideDrawer}
selected={player.selected?.streamRequest.path.id}
/> />
</Transition> </Transition>
{ {

View file

@ -1,6 +1,6 @@
// Copyright (C) 2017-2024 Smart code 203358507 // Copyright (C) 2017-2024 Smart code 203358507
import React, { useMemo, useCallback, useState, forwardRef, memo } from 'react'; import React, { useMemo, useCallback, useState, forwardRef, memo, useRef, useEffect } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react'; import Icon from '@stremio/stremio-icons/react';
import { useServices } from 'stremio/services'; import { useServices } from 'stremio/services';
@ -14,11 +14,14 @@ type Props = {
seriesInfo: SeriesInfo; seriesInfo: SeriesInfo;
metaItem: MetaItem; metaItem: MetaItem;
closeSideDrawer: () => void; closeSideDrawer: () => void;
selected: string;
transitionEnded: boolean;
}; };
const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, ...props }: Props, ref) => { const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, className, closeSideDrawer, selected, transitionEnded, ...props }: Props, ref) => {
const { core } = useServices(); const { core } = useServices();
const [season, setSeason] = useState<number>(seriesInfo?.season); const [season, setSeason] = useState<number>(seriesInfo?.season);
const selectedVideoRef = useRef<HTMLDivElement>(null);
const metaItem = useMemo(() => { const metaItem = useMemo(() => {
return seriesInfo ? return seriesInfo ?
{ {
@ -75,6 +78,14 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
event.stopPropagation(); event.stopPropagation();
}; };
const getSelectedRef = useCallback((video: Video) => {
return video.id === selected ? selectedVideoRef : null;
}, [selected]);
useEffect(() => {
transitionEnded && selectedVideoRef?.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, [transitionEnded]);
return ( return (
<div ref={ref} className={classNames(styles['side-drawer'], className)} onMouseDown={onMouseDown}> <div ref={ref} className={classNames(styles['side-drawer'], className)} onMouseDown={onMouseDown}>
<div className={styles['close-button']} onClick={closeSideDrawer}> <div className={styles['close-button']} onClick={closeSideDrawer}>
@ -120,6 +131,7 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
scheduled={video.scheduled} scheduled={video.scheduled}
onMarkVideoAsWatched={onMarkVideoAsWatched} onMarkVideoAsWatched={onMarkVideoAsWatched}
onMarkSeasonAsWatched={onMarkSeasonAsWatched} onMarkSeasonAsWatched={onMarkSeasonAsWatched}
ref={getSelectedRef(video)}
/> />
))} ))}
</div> </div>

View file

@ -11,21 +11,6 @@ function Shell() {
const events = new EventEmitter(); const events = new EventEmitter();
function onTransportInit() {
active = true;
error = null;
starting = false;
onStateChanged();
}
function onTransportInitError(err) {
console.error(err);
active = false;
error = new Error(err);
starting = false;
onStateChanged();
transport = null;
}
function onStateChanged() { function onStateChanged() {
events.emit('stateChanged'); events.emit('stateChanged');
} }
@ -68,9 +53,22 @@ function Shell() {
active = false; active = false;
starting = true; starting = true;
transport = new ShellTransport();
transport.on('init', onTransportInit); try {
transport.on('init-error', onTransportInitError); transport = new ShellTransport();
active = true;
error = null;
starting = false;
onStateChanged();
} catch (e) {
console.error(e);
active = false;
error = new Error(e);
starting = false;
onStateChanged();
transport = null;
}
onStateChanged(); onStateChanged();
}; };
this.stop = function() { this.stop = function() {

View file

@ -2,9 +2,6 @@
const EventEmitter = require('eventemitter3'); const EventEmitter = require('eventemitter3');
let shellAvailable = false;
const shellEvents = new EventEmitter();
const QtMsgTypes = { const QtMsgTypes = {
signal: 1, signal: 1,
propertyUpdate: 2, propertyUpdate: 2,
@ -19,27 +16,6 @@ const QtMsgTypes = {
}; };
const QtObjId = 'transport'; // the ID of our transport object const QtObjId = 'transport'; // the ID of our transport object
window.initShellComm = function () {
delete window.initShellComm;
shellEvents.emit('availabilityChanged');
};
const initialize = () => {
if(!window.qt) return Promise.reject('Qt API not found');
return new Promise((resolve) => {
function onShellAvailabilityChanged() {
shellEvents.off('availabilityChanged', onShellAvailabilityChanged);
shellAvailable = true;
resolve();
}
if (shellAvailable) {
onShellAvailabilityChanged();
} else {
shellEvents.on('availabilityChanged', onShellAvailabilityChanged);
}
});
};
function ShellTransport() { function ShellTransport() {
const events = new EventEmitter(); const events = new EventEmitter();
@ -47,66 +23,60 @@ function ShellTransport() {
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
const shell = this; const shell = this;
initialize() const transport = window.qt && window.qt.webChannelTransport;
.then(() => { if (!transport) throw 'no viable transport found (qt.webChannelTransport)';
const transport = window.qt && window.qt.webChannelTransport;
if (!transport) throw 'no viable transport found (qt.webChannelTransport)';
let id = 0; let id = 0;
function send(msg) { function send(msg) {
msg.id = id++; msg.id = id++;
transport.send(JSON.stringify(msg)); transport.send(JSON.stringify(msg));
}
transport.onmessage = function (message) {
const msg = JSON.parse(message.data);
if (msg.id === 0) {
const obj = msg.data[QtObjId];
obj.properties.slice(1).forEach(function (prop) {
shell.props[prop[1]] = prop[3];
});
if (typeof shell.props.shellVersion === 'string') {
shell.shellVersionArr = (
shell.props.shellVersion.match(/(\d+)\.(\d+)\.(\d+)/) || []
)
.slice(1, 4)
.map(Number);
} }
events.emit('received-props', shell.props);
transport.onmessage = function (message) { obj.signals.forEach(function (sig) {
const msg = JSON.parse(message.data); send({
if (msg.id === 0) { type: QtMsgTypes.connectToSignal,
const obj = msg.data[QtObjId]; object: QtObjId,
signal: sig[1],
});
});
obj.properties.slice(1).forEach(function (prop) { const onEvent = obj.methods.filter(function (x) {
shell.props[prop[1]] = prop[3]; return x[0] === 'onEvent';
}); })[0];
if (typeof shell.props.shellVersion === 'string') {
shell.shellVersionArr = (
shell.props.shellVersion.match(/(\d+)\.(\d+)\.(\d+)/) || []
)
.slice(1, 4)
.map(Number);
}
events.emit('received-props', shell.props);
obj.signals.forEach(function (sig) { shell.send = function (ev, args) {
send({ send({
type: QtMsgTypes.connectToSignal, type: QtMsgTypes.invokeMethod,
object: QtObjId, object: QtObjId,
signal: sig[1], method: onEvent[1],
}); args: [ev, args || {}],
}); });
const onEvent = obj.methods.filter(function (x) {
return x[0] === 'onEvent';
})[0];
shell.send = function (ev, args) {
send({
type: QtMsgTypes.invokeMethod,
object: QtObjId,
method: onEvent[1],
args: [ev, args || {}],
});
};
shell.send('app-ready', {}); // signal that we're ready to take events
}
if (msg.object === QtObjId && msg.type === QtMsgTypes.signal)
events.emit(msg.args[0], msg.args[1]);
events.emit('init');
}; };
send({ type: QtMsgTypes.init });
}) .catch((error) => { shell.send('app-ready', {}); // signal that we're ready to take events
events.emit('init-error', error); }
});
if (msg.object === QtObjId && msg.type === QtMsgTypes.signal)
events.emit(msg.args[0], msg.args[1]);
};
send({ type: QtMsgTypes.init });
this.on = function(name, listener) { this.on = function(name, listener) {
events.on(name, listener); events.on(name, listener);