mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-10 07:11:48 +00:00
Merge branch 'development' into feat/add-subtiles-menu-options-context-menu
This commit is contained in:
commit
e2d1654f49
29 changed files with 399 additions and 166 deletions
4
.github/workflows/auto_assign.yml
vendored
4
.github/workflows/auto_assign.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
# Auto assign PR to author
|
||||
- name: Auto Assign PR to Author
|
||||
if: github.event.pull_request.head.repo.fork == false && github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
|
|
|||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "stremio",
|
||||
"displayName": "Stremio",
|
||||
"version": "5.0.0-beta.32",
|
||||
"version": "5.0.0-beta.34",
|
||||
"author": "Smart Code OOD",
|
||||
"private": true,
|
||||
"license": "gpl-2.0",
|
||||
|
|
@ -17,9 +17,9 @@
|
|||
"@babel/runtime": "7.26.0",
|
||||
"@sentry/browser": "8.42.0",
|
||||
"@stremio/stremio-colors": "5.2.0",
|
||||
"@stremio/stremio-core-web": "0.55.0",
|
||||
"@stremio/stremio-core-web": "0.56.4",
|
||||
"@stremio/stremio-icons": "5.8.0",
|
||||
"@stremio/stremio-video": "0.0.70",
|
||||
"@stremio/stremio-video": "0.0.75",
|
||||
"a-color-picker": "1.2.1",
|
||||
"bowser": "2.11.0",
|
||||
"buffer": "6.0.3",
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ importers:
|
|||
specifier: 5.2.0
|
||||
version: 5.2.0
|
||||
'@stremio/stremio-core-web':
|
||||
specifier: 0.55.0
|
||||
version: 0.55.0
|
||||
specifier: 0.56.4
|
||||
version: 0.56.4
|
||||
'@stremio/stremio-icons':
|
||||
specifier: 5.8.0
|
||||
version: 5.8.0
|
||||
'@stremio/stremio-video':
|
||||
specifier: 0.0.70
|
||||
version: 0.0.70
|
||||
specifier: 0.0.75
|
||||
version: 0.0.75
|
||||
a-color-picker:
|
||||
specifier: 1.2.1
|
||||
version: 1.2.1
|
||||
|
|
@ -1120,14 +1120,14 @@ packages:
|
|||
'@stremio/stremio-colors@5.2.0':
|
||||
resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==}
|
||||
|
||||
'@stremio/stremio-core-web@0.55.0':
|
||||
resolution: {integrity: sha512-MdalnThEwnA8osQh+3/5OMzVIYZOoYmd94dN3nmCeT4rfV7IZXRFUg/uyCY+5bqigStlE3SfKEaGiSc6UnNtlQ==}
|
||||
'@stremio/stremio-core-web@0.56.4':
|
||||
resolution: {integrity: sha512-tFAMYgKrJ1bkvHRMpxDykM/844sDjgRPFk6FLhjQiwh01OHIyEgDqGo/NgwFM+CuMR4mW676SDvwNHkK0Xqg3w==}
|
||||
|
||||
'@stremio/stremio-icons@5.8.0':
|
||||
resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==}
|
||||
|
||||
'@stremio/stremio-video@0.0.70':
|
||||
resolution: {integrity: sha512-a0flQYAUdrZNMm7mmts2vpZOqN1nus7Hs9Mjl4mrN5rtduD0ojUyhD5J4lPcCpZ7WB0YdEUOGLXR19qHpgoKmg==}
|
||||
'@stremio/stremio-video@0.0.75':
|
||||
resolution: {integrity: sha512-oKXMq156BVagzziWoTsmgNYABCSfwV9hR/TM6+JR4lne5pW4qmUN17ba/Fxsr+USKHeCKUaz1u0asKBj06HfyA==}
|
||||
|
||||
'@stylistic/eslint-plugin-jsx@4.4.1':
|
||||
resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==}
|
||||
|
|
@ -5870,13 +5870,13 @@ snapshots:
|
|||
|
||||
'@stremio/stremio-colors@5.2.0': {}
|
||||
|
||||
'@stremio/stremio-core-web@0.55.0':
|
||||
'@stremio/stremio-core-web@0.56.4':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.1
|
||||
|
||||
'@stremio/stremio-icons@5.8.0': {}
|
||||
|
||||
'@stremio/stremio-video@0.0.70':
|
||||
'@stremio/stremio-video@0.0.75':
|
||||
dependencies:
|
||||
buffer: 6.0.3
|
||||
color: 4.2.3
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const ServicesToaster = () => {
|
|||
}
|
||||
case 'MagnetParsed': {
|
||||
toast.show({
|
||||
type: 'success',
|
||||
type: 'info',
|
||||
title: 'Magnet link parsed',
|
||||
timeout: 4000
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const React = require('react');
|
|||
|
||||
const ToastContext = React.createContext({
|
||||
show: () => { },
|
||||
remove: () => { },
|
||||
clear: () => { }
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const ToastProvider = ({ className, children }) => {
|
|||
},
|
||||
show: (item) => {
|
||||
if (filters.some((filter) => filter(item))) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ?
|
||||
|
|
@ -64,6 +64,11 @@ const ToastProvider = ({ className, children }) => {
|
|||
onClose: itemOnClose
|
||||
}
|
||||
});
|
||||
return id;
|
||||
},
|
||||
remove: (id) => {
|
||||
clearTimeout(id);
|
||||
dispatch({ type: 'remove', id });
|
||||
},
|
||||
clear: () => {
|
||||
dispatch({ type: 'clear' });
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const { default: useSettings } = require('./useSettings');
|
|||
const { default: useShell } = require('./useShell');
|
||||
const useStreamingServer = require('./useStreamingServer');
|
||||
const { default: useTimeout } = require('./useTimeout');
|
||||
const { default: usePlayUrl } = require('./usePlayUrl');
|
||||
const useTorrent = require('./useTorrent');
|
||||
const useTranslate = require('./useTranslate');
|
||||
const { default: useOrientation } = require('./useOrientation');
|
||||
|
|
@ -63,6 +64,7 @@ module.exports = {
|
|||
useShell,
|
||||
useStreamingServer,
|
||||
useTimeout,
|
||||
usePlayUrl,
|
||||
useTorrent,
|
||||
useTranslate,
|
||||
useOrientation,
|
||||
|
|
|
|||
65
src/common/usePlayUrl.ts
Normal file
65
src/common/usePlayUrl.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useCallback } from 'react';
|
||||
import magnet from 'magnet-uri';
|
||||
import { useServices } from 'stremio/services';
|
||||
import useToast from 'stremio/common/Toast/useToast';
|
||||
import useTorrent from 'stremio/common/useTorrent';
|
||||
import useStreamingServer from 'stremio/common/useStreamingServer';
|
||||
|
||||
const HTTP_REGEX = /^https?:\/\/.+/i;
|
||||
|
||||
const usePlayUrl = () => {
|
||||
const { core } = useServices();
|
||||
const toast = useToast();
|
||||
const { createTorrentFromMagnet } = useTorrent();
|
||||
const streamingServer = useStreamingServer();
|
||||
|
||||
const handlePlayUrl = useCallback(async (text: string): Promise<boolean> => {
|
||||
if (!text || !text.trim()) return false;
|
||||
const trimmed = text.trim();
|
||||
|
||||
if (HTTP_REGEX.test(trimmed)) {
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Loading HTTP stream…',
|
||||
timeout: 3000
|
||||
});
|
||||
try {
|
||||
const encoded = await core.transport.encodeStream({ url: trimmed });
|
||||
if (typeof encoded === 'string') {
|
||||
window.location.hash = `#/player/${encodeURIComponent(encoded)}`;
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to encode stream:', e);
|
||||
}
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'Failed to load HTTP stream.',
|
||||
timeout: 5000
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsed = magnet.decode(trimmed);
|
||||
if (parsed && typeof parsed.infoHash === 'string') {
|
||||
const serverReady = streamingServer.settings !== null
|
||||
&& streamingServer.settings.type === 'Ready';
|
||||
if (!serverReady) {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'Streaming server is not available. Cannot play magnet links.',
|
||||
timeout: 5000
|
||||
});
|
||||
return false;
|
||||
}
|
||||
createTorrentFromMagnet(trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [streamingServer.settings, createTorrentFromMagnet]);
|
||||
|
||||
return { handlePlayUrl };
|
||||
};
|
||||
|
||||
export default usePlayUrl;
|
||||
|
|
@ -6,14 +6,22 @@ const { useServices } = require('stremio/services');
|
|||
const useToast = require('stremio/common/Toast/useToast');
|
||||
const useStreamingServer = require('stremio/common/useStreamingServer');
|
||||
|
||||
const CREATE_TORRENT_TIMEOUT = 20000;
|
||||
|
||||
const useTorrent = () => {
|
||||
const { core } = useServices();
|
||||
const streamingServer = useStreamingServer();
|
||||
const toast = useToast();
|
||||
const createTorrentTimeout = React.useRef(null);
|
||||
const parsingToastId = React.useRef(null);
|
||||
const createTorrentFromMagnet = React.useCallback((text) => {
|
||||
const parsed = magnet.decode(text);
|
||||
if (parsed && typeof parsed.infoHash === 'string') {
|
||||
parsingToastId.current = toast.show({
|
||||
type: 'success',
|
||||
title: 'Loading magnet link…',
|
||||
timeout: CREATE_TORRENT_TIMEOUT
|
||||
});
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
|
|
@ -23,12 +31,13 @@ const useTorrent = () => {
|
|||
});
|
||||
clearTimeout(createTorrentTimeout.current);
|
||||
createTorrentTimeout.current = setTimeout(() => {
|
||||
toast.remove(parsingToastId.current);
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'It\'s taking a long time to get metadata from the torrent.',
|
||||
timeout: 10000
|
||||
title: 'Failed to parse magnet link.',
|
||||
timeout: 8000
|
||||
});
|
||||
}, 10000);
|
||||
}, CREATE_TORRENT_TIMEOUT);
|
||||
}
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
|
|
@ -36,6 +45,7 @@ const useTorrent = () => {
|
|||
const [, { type }] = streamingServer.torrent;
|
||||
if (type === 'Ready') {
|
||||
clearTimeout(createTorrentTimeout.current);
|
||||
toast.remove(parsingToastId.current);
|
||||
}
|
||||
}
|
||||
}, [streamingServer.torrent]);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
@width-mobile: 3rem;
|
||||
|
||||
|
||||
.ratings-container {
|
||||
.group-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
@media @phone-landscape {
|
||||
.ratings-container {
|
||||
.group-container {
|
||||
height: @height-mobile;
|
||||
|
||||
.icon-container {
|
||||
45
src/components/ActionsGroup/ActionsGroup.tsx
Normal file
45
src/components/ActionsGroup/ActionsGroup.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import { Tooltip } from 'stremio/common/Tooltips';
|
||||
import styles from './ActionsGroup.less';
|
||||
|
||||
type Item = {
|
||||
icon: string;
|
||||
label?: string;
|
||||
filled?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
items: Item[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ActionsGroup = ({ items, className }: Props) => {
|
||||
return (
|
||||
<div className={classNames(styles['group-container'], className)}>
|
||||
{
|
||||
items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={classNames(styles['icon-container'], item.className, { [styles['disabled']]: item.disabled })}
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{
|
||||
item.label &&
|
||||
<Tooltip label={item.label} position={'top'} />
|
||||
}
|
||||
<Icon name={item.icon} className={styles['icon']} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionsGroup;
|
||||
6
src/components/ActionsGroup/index.ts
Normal file
6
src/components/ActionsGroup/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (C) 2017-2025 Smart code 203358507
|
||||
|
||||
import ActionsGroup from './ActionsGroup';
|
||||
|
||||
export default ActionsGroup;
|
||||
|
||||
|
|
@ -8,6 +8,7 @@ const { useTranslation } = require('react-i18next');
|
|||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { default: Button } = require('stremio/components/Button');
|
||||
const { default: Image } = require('stremio/components/Image');
|
||||
const { default: ActionsGroup } = require('stremio/components/ActionsGroup');
|
||||
const ModalDialog = require('stremio/components/ModalDialog');
|
||||
const SharePrompt = require('stremio/components/SharePrompt');
|
||||
const CONSTANTS = require('stremio/common/CONSTANTS');
|
||||
|
|
@ -25,7 +26,7 @@ const ALLOWED_LINK_REDIRECTS = [
|
|||
routesRegexp.metadetails.regexp
|
||||
];
|
||||
|
||||
const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary, ratingInfo }, ref) => {
|
||||
const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary, watched, toggleWatched, ratingInfo }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false);
|
||||
const linksGroups = React.useMemo(() => {
|
||||
|
|
@ -98,6 +99,18 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
|||
const renderLogoFallback = React.useCallback(() => (
|
||||
<div className={styles['logo-placeholder']}>{name}</div>
|
||||
), [name]);
|
||||
const metaItemActions = React.useMemo(() => [
|
||||
{
|
||||
icon: inLibrary ? 'remove-from-library' : 'add-to-library',
|
||||
label: inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB'),
|
||||
onClick: typeof toggleInLibrary === 'function' ? toggleInLibrary : null,
|
||||
},
|
||||
{
|
||||
icon: watched ? 'eye-off' : 'eye',
|
||||
label: watched ? t('CTX_MARK_UNWATCHED') : t('CTX_MARK_WATCHED'),
|
||||
onClick: typeof toggleWatched === 'function' ? toggleWatched : undefined,
|
||||
},
|
||||
], [inLibrary, watched, toggleInLibrary, toggleWatched]);
|
||||
return (
|
||||
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })} ref={ref}>
|
||||
{
|
||||
|
|
@ -195,19 +208,6 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
|||
}
|
||||
</div>
|
||||
<div className={styles['action-buttons-container']}>
|
||||
{
|
||||
typeof toggleInLibrary === 'function' ?
|
||||
<ActionButton
|
||||
className={styles['action-button']}
|
||||
icon={inLibrary ? 'remove-from-library' : 'add-to-library'}
|
||||
label={inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB')}
|
||||
tooltip={compact}
|
||||
tabIndex={compact ? -1 : 0}
|
||||
onClick={toggleInLibrary}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof trailerHref === 'string' ?
|
||||
<ActionButton
|
||||
|
|
@ -221,6 +221,11 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof toggleInLibrary === 'function' && typeof toggleWatched === 'function'
|
||||
? <ActionsGroup items={metaItemActions} className={styles['group-container']} />
|
||||
: null
|
||||
}
|
||||
{
|
||||
typeof showHref === 'string' && compact ?
|
||||
<ActionButton
|
||||
|
|
@ -237,7 +242,7 @@ const MetaPreview = React.forwardRef(({ className, compact, name, logo, backgrou
|
|||
!compact && ratingInfo !== null ?
|
||||
<Ratings
|
||||
ratingInfo={ratingInfo}
|
||||
className={styles['ratings']}
|
||||
className={styles['group-container']}
|
||||
/>
|
||||
:
|
||||
null
|
||||
|
|
@ -298,6 +303,8 @@ MetaPreview.propTypes = {
|
|||
trailerStreams: PropTypes.array,
|
||||
inLibrary: PropTypes.bool,
|
||||
toggleInLibrary: PropTypes.func,
|
||||
watched: PropTypes.bool,
|
||||
toggleWatched: PropTypes.func,
|
||||
ratingInfo: PropTypes.object,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
import React, { useMemo } from 'react';
|
||||
import useRating from './useRating';
|
||||
import styles from './Ratings.less';
|
||||
import Icon from '@stremio/stremio-icons/react';
|
||||
import classNames from 'classnames';
|
||||
import { ActionsGroup } from 'stremio/components';
|
||||
|
||||
type Props = {
|
||||
metaId?: string;
|
||||
|
|
@ -16,15 +14,21 @@ const Ratings = ({ ratingInfo, className }: Props) => {
|
|||
const { onLiked, onLoved, liked, loved } = useRating(ratingInfo);
|
||||
const disabled = useMemo(() => ratingInfo?.type !== 'Ready', [ratingInfo]);
|
||||
|
||||
const items = useMemo(() => [
|
||||
{
|
||||
icon: liked ? 'thumbs-up' : 'thumbs-up-outline',
|
||||
disabled,
|
||||
onClick: onLiked,
|
||||
},
|
||||
{
|
||||
icon: loved ? 'heart' : 'heart-outline',
|
||||
disabled,
|
||||
onClick: onLoved,
|
||||
},
|
||||
], [liked, loved, disabled]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles['ratings-container'], className)}>
|
||||
<div className={classNames(styles['icon-container'], { [styles['disabled']]: disabled })} onClick={onLiked}>
|
||||
<Icon name={liked ? 'thumbs-up' : 'thumbs-up-outline'} className={styles['icon']} />
|
||||
</div>
|
||||
<div className={classNames(styles['icon-container'], { [styles['disabled']]: disabled })} onClick={onLoved}>
|
||||
<Icon name={loved ? 'heart' : 'heart-outline'} className={styles['icon']} />
|
||||
</div>
|
||||
</div>
|
||||
<ActionsGroup items={items} className={className} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
.action-buttons-container {
|
||||
justify-content: space-between;
|
||||
|
||||
.action-button:not(:last-child) {
|
||||
.action-button:not(:last-child), .group-container:not(:last-child) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -207,11 +207,20 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-container {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.ratings {
|
||||
margin-bottom: 1rem;
|
||||
margin-right: 1rem;
|
||||
&:global(.wide) {
|
||||
width: auto;
|
||||
padding: 0 2rem;
|
||||
border-radius: 4rem;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,17 +242,13 @@
|
|||
padding-top: 1.5rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.action-button {
|
||||
.action-button, .group-container {
|
||||
padding: 0 1.5rem !important;
|
||||
margin-right: 0rem !important;
|
||||
height: 3rem;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ratings {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -272,6 +277,10 @@
|
|||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 0 1rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ const { Button } = require('stremio/components');
|
|||
const { default: useFullscreen } = require('stremio/common/useFullscreen');
|
||||
const useProfile = require('stremio/common/useProfile');
|
||||
const usePWA = require('stremio/common/usePWA');
|
||||
const useTorrent = require('stremio/common/useTorrent');
|
||||
const { default: usePlayUrl } = require('stremio/common/usePlayUrl');
|
||||
const useToast = require('stremio/common/Toast/useToast');
|
||||
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||
const useStreamingServer = require('stremio/common/useStreamingServer');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -20,7 +21,8 @@ const NavMenuContent = ({ onClick }) => {
|
|||
const { core } = useServices();
|
||||
const profile = useProfile();
|
||||
const streamingServer = useStreamingServer();
|
||||
const { createTorrentFromMagnet } = useTorrent();
|
||||
const { handlePlayUrl } = usePlayUrl();
|
||||
const toast = useToast();
|
||||
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
||||
const [isIOSPWA, isAndroidPWA] = usePWA();
|
||||
const streamingServerWarningDismissed = React.useMemo(() => {
|
||||
|
|
@ -40,11 +42,18 @@ const NavMenuContent = ({ onClick }) => {
|
|||
const onPlayMagnetLinkClick = React.useCallback(async () => {
|
||||
try {
|
||||
const clipboardText = await navigator.clipboard.readText();
|
||||
createTorrentFromMagnet(clipboardText);
|
||||
const handled = await handlePlayUrl(clipboardText);
|
||||
if (!handled) {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: 'Clipboard does not contain a valid URL or magnet link.',
|
||||
timeout: 5000
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, []);
|
||||
}, [handlePlayUrl]);
|
||||
return (
|
||||
<div className={classnames(styles['nav-menu-container'], 'animation-fade-in', { [styles['with-warning']]: !streamingServerWarningDismissed } )} onClick={onClick}>
|
||||
<div className={styles['user-info-container']}>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
|
|||
const { useRouteFocused } = require('stremio-router');
|
||||
const Button = require('stremio/components/Button').default;
|
||||
const TextInput = require('stremio/components/TextInput').default;
|
||||
const useTorrent = require('stremio/common/useTorrent');
|
||||
const { default: usePlayUrl } = require('stremio/common/usePlayUrl');
|
||||
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||
const useSearchHistory = require('./useSearchHistory');
|
||||
const useLocalSearch = require('./useLocalSearch');
|
||||
|
|
@ -21,7 +21,7 @@ const SearchBar = React.memo(({ className, query, active }) => {
|
|||
const routeFocused = useRouteFocused();
|
||||
const searchHistory = useSearchHistory();
|
||||
const localSearch = useLocalSearch();
|
||||
const { createTorrentFromMagnet } = useTorrent();
|
||||
const { handlePlayUrl } = usePlayUrl();
|
||||
|
||||
const [historyOpen, openHistory, closeHistory, ] = useBinaryState(query === null ? true : false);
|
||||
const [currentQuery, setCurrentQuery] = React.useState(query || '');
|
||||
|
|
@ -52,12 +52,14 @@ const SearchBar = React.memo(({ className, query, active }) => {
|
|||
const value = searchInputRef.current.value;
|
||||
setCurrentQuery(value);
|
||||
openHistory();
|
||||
try {
|
||||
createTorrentFromMagnet(value);
|
||||
} catch (error) {
|
||||
console.error('Failed to create torrent from magnet:', error);
|
||||
}, []);
|
||||
|
||||
const queryInputOnPaste = React.useCallback((event) => {
|
||||
const pasted = event.clipboardData.getData('text');
|
||||
if (pasted) {
|
||||
handlePlayUrl(pasted);
|
||||
}
|
||||
}, [createTorrentFromMagnet]);
|
||||
}, [handlePlayUrl]);
|
||||
|
||||
const queryInputOnSubmit = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -108,6 +110,7 @@ const SearchBar = React.memo(({ className, query, active }) => {
|
|||
defaultValue={query}
|
||||
tabIndex={-1}
|
||||
onChange={queryInputOnChange}
|
||||
onPaste={queryInputOnPaste}
|
||||
onSubmit={queryInputOnSubmit}
|
||||
onClick={openHistory}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import TextInput from './TextInput';
|
|||
import Toggle from './Toggle';
|
||||
import Transition from './Transition';
|
||||
import Video from './Video';
|
||||
import ActionsGroup from './ActionsGroup';
|
||||
|
||||
export {
|
||||
AddonDetailsModal,
|
||||
|
|
@ -65,4 +66,5 @@ export {
|
|||
Toggle,
|
||||
Transition,
|
||||
Video,
|
||||
ActionsGroup
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false);
|
||||
const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0);
|
||||
|
||||
const selectedMetaItem = React.useMemo(() => {
|
||||
return discover.catalog?.content.type === 'Ready' &&
|
||||
discover.catalog.content.content[selectedMetaItemIndex] || null;
|
||||
}, [discover.catalog, selectedMetaItemIndex]);
|
||||
|
||||
const metasContainerRef = React.useRef();
|
||||
const metaPreviewRef = React.useRef();
|
||||
|
||||
|
|
@ -40,14 +45,6 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
}
|
||||
}, [hasNextPage, loadNextPage]);
|
||||
const selectedMetaItem = React.useMemo(() => {
|
||||
return discover.catalog !== null &&
|
||||
discover.catalog.content.type === 'Ready' &&
|
||||
discover.catalog.content.content[selectedMetaItemIndex] ?
|
||||
discover.catalog.content.content[selectedMetaItemIndex]
|
||||
:
|
||||
null;
|
||||
}, [discover.catalog, selectedMetaItemIndex]);
|
||||
const addToLibrary = React.useCallback(() => {
|
||||
if (selectedMetaItem === null) {
|
||||
return;
|
||||
|
|
@ -74,6 +71,22 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
});
|
||||
}, [selectedMetaItem]);
|
||||
const toggleWatched = React.useCallback(() => {
|
||||
if (selectedMetaItem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'MetaItemMarkAsWatched',
|
||||
args: {
|
||||
meta_item: selectedMetaItem,
|
||||
is_watched: !selectedMetaItem.watched,
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [selectedMetaItem]);
|
||||
const metaItemsOnFocusCapture = React.useCallback((event) => {
|
||||
if (event.target.dataset.index !== null && !isNaN(event.target.dataset.index)) {
|
||||
setSelectedMetaItemIndex(parseInt(event.target.dataset.index, 10));
|
||||
|
|
@ -193,6 +206,8 @@ const Discover = ({ urlParams, queryParams }) => {
|
|||
trailerStreams={selectedMetaItem.trailerStreams}
|
||||
inLibrary={selectedMetaItem.inLibrary}
|
||||
toggleInLibrary={selectedMetaItem.inLibrary ? removeFromLibrary : addToLibrary}
|
||||
watched={selectedMetaItem.watched}
|
||||
toggleWatched={toggleWatched}
|
||||
metaId={selectedMetaItem.id}
|
||||
like={selectedMetaItem.like}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,19 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
});
|
||||
}, [metaDetails]);
|
||||
const toggleWatched = React.useCallback(() => {
|
||||
if (metaDetails.metaItem === null || metaDetails.metaItem.content.type !== 'Ready') {
|
||||
return;
|
||||
}
|
||||
|
||||
core.transport.dispatch({
|
||||
action: 'MetaDetails',
|
||||
args: {
|
||||
action: 'MarkAsWatched',
|
||||
args: !metaDetails.metaItem.content.content.watched
|
||||
}
|
||||
});
|
||||
}, [metaDetails]);
|
||||
const toggleNotifications = React.useCallback(() => {
|
||||
if (metaDetails.libraryItem) {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -172,6 +185,8 @@ const MetaDetails = ({ urlParams, queryParams }) => {
|
|||
trailerStreams={metaDetails.metaItem.content.content.trailerStreams}
|
||||
inLibrary={metaDetails.metaItem.content.content.inLibrary}
|
||||
toggleInLibrary={metaDetails.metaItem.content.content.inLibrary ? removeFromLibrary : addToLibrary}
|
||||
watched={metaDetails.metaItem.content.content.watched}
|
||||
toggleWatched={toggleWatched}
|
||||
metaId={metaDetails.metaItem.content.content.id}
|
||||
ratingInfo={metaDetails.ratingInfo}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const useVideo = require('./useVideo');
|
|||
const styles = require('./styles');
|
||||
const Video = require('./Video');
|
||||
const { default: Indicator } = require('./Indicator/Indicator');
|
||||
const { default: useMediaSession } = require('./useMediaSession');
|
||||
|
||||
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
|
||||
const findTrackById = (tracks, id) => tracks.find((track) => track.id === id);
|
||||
|
|
@ -235,23 +236,17 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
|
||||
}, []);
|
||||
|
||||
const onSubtitlesTrackSelected = React.useCallback((id) => {
|
||||
video.setSubtitlesTrack(id);
|
||||
const onSubtitlesTrackSelected = React.useCallback((track) => {
|
||||
video.setSubtitlesTrack(track?.id ?? null);
|
||||
streamStateChanged({
|
||||
subtitleTrack: {
|
||||
id,
|
||||
embedded: true,
|
||||
},
|
||||
subtitleTrack: track ? { id: track.id, embedded: true, lang: track.lang } : null,
|
||||
});
|
||||
}, [streamStateChanged]);
|
||||
|
||||
const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
|
||||
video.setExtraSubtitlesTrack(id);
|
||||
const onExtraSubtitlesTrackSelected = React.useCallback((track) => {
|
||||
video.setExtraSubtitlesTrack(track?.id ?? null);
|
||||
streamStateChanged({
|
||||
subtitleTrack: {
|
||||
id,
|
||||
embedded: false,
|
||||
},
|
||||
subtitleTrack: track ? { id: track.id, embedded: false, lang: track.lang } : null,
|
||||
});
|
||||
}, [streamStateChanged]);
|
||||
|
||||
|
|
@ -466,23 +461,34 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
|
||||
const savedTrackId = player.streamState?.subtitleTrack?.id;
|
||||
const subtitlesTrack = savedTrackId ?
|
||||
findTrackById(video.state.subtitlesTracks, savedTrackId) :
|
||||
findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
|
||||
const savedLang = player.streamState?.subtitleTrack?.lang;
|
||||
const savedIsExternal = savedTrackId && player.streamState?.subtitleTrack?.embedded === false;
|
||||
|
||||
const extraSubtitlesTrack = savedTrackId ?
|
||||
findTrackById(video.state.extraSubtitlesTracks, savedTrackId) :
|
||||
findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
|
||||
const subtitlesTrack =
|
||||
savedTrackId ? findTrackById(video.state.subtitlesTracks, savedTrackId) :
|
||||
savedLang ? findTrackByLang(video.state.subtitlesTracks, savedLang) :
|
||||
findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
|
||||
|
||||
const extraSubtitlesTrack =
|
||||
savedTrackId ? findTrackById(video.state.extraSubtitlesTracks, savedTrackId) :
|
||||
savedLang ? findTrackByLang(video.state.extraSubtitlesTracks, savedLang) :
|
||||
findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
|
||||
|
||||
if (subtitlesTrack && subtitlesTrack.id) {
|
||||
video.setSubtitlesTrack(subtitlesTrack.id);
|
||||
if (video.state.selectedSubtitlesTrackId !== subtitlesTrack.id) {
|
||||
video.setSubtitlesTrack(subtitlesTrack.id);
|
||||
}
|
||||
defaultSubtitlesSelected.current = true;
|
||||
} else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
|
||||
video.setExtraSubtitlesTrack(extraSubtitlesTrack.id);
|
||||
defaultSubtitlesSelected.current = true;
|
||||
if (video.state.selectedExtraSubtitlesTrackId !== extraSubtitlesTrack.id) {
|
||||
video.setExtraSubtitlesTrack(extraSubtitlesTrack.id);
|
||||
}
|
||||
if (savedIsExternal) {
|
||||
defaultSubtitlesSelected.current = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, player.streamState]);
|
||||
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId, player.streamState]);
|
||||
|
||||
// Auto audio track selection
|
||||
React.useEffect(() => {
|
||||
|
|
@ -591,63 +597,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
|
||||
|
||||
// Media Session PlaybackState
|
||||
React.useEffect(() => {
|
||||
if (!navigator.mediaSession) return;
|
||||
|
||||
const playbackState = !video.state.paused ? 'playing' : 'paused';
|
||||
navigator.mediaSession.playbackState = playbackState;
|
||||
|
||||
return () => navigator.mediaSession.playbackState = 'none';
|
||||
}, [video.state.paused]);
|
||||
|
||||
// Media Session Metadata
|
||||
React.useEffect(() => {
|
||||
if (!navigator.mediaSession) return;
|
||||
|
||||
const metaItem = player.metaItem && player.metaItem?.type === 'Ready' ? player.metaItem.content : null;
|
||||
const videoId = player.selected ? player.selected?.streamRequest?.path?.id : null;
|
||||
const video = metaItem ? metaItem.videos.find(({ id }) => id === videoId) : null;
|
||||
|
||||
const videoInfo = video && video.season && video.episode ? ` (${video.season}x${video.episode})` : null;
|
||||
const videoTitle = video ? `${video.title}${videoInfo}` : null;
|
||||
const metaTitle = metaItem ? metaItem.name : null;
|
||||
const imageUrl = metaItem ? metaItem.logo : null;
|
||||
|
||||
const title = videoTitle ?? metaTitle;
|
||||
const artist = videoTitle ? metaTitle : undefined;
|
||||
const artwork = imageUrl ? [{ src: imageUrl }] : undefined;
|
||||
|
||||
if (title) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title,
|
||||
artist,
|
||||
artwork,
|
||||
});
|
||||
}
|
||||
}, [player.metaItem, player.selected]);
|
||||
|
||||
// Media Session Actions
|
||||
React.useEffect(() => {
|
||||
if (!navigator.mediaSession) return;
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', onPlayRequested);
|
||||
navigator.mediaSession.setActionHandler('pause', onPauseRequested);
|
||||
|
||||
const nexVideoCallback = player.nextVideo ? onNextVideoRequested : null;
|
||||
navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback);
|
||||
}, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]);
|
||||
|
||||
onShortcut('playPause', () => {
|
||||
if (video.state.paused !== null) {
|
||||
if (video.state.paused) {
|
||||
onPlayRequested();
|
||||
setSeeking(false);
|
||||
} else if (!pressTimer.current) {
|
||||
onPauseRequested();
|
||||
}
|
||||
}
|
||||
}, [video.state.paused, pressTimer.current, onPlayRequested, onPauseRequested], !menusOpen);
|
||||
useMediaSession(video.state, player, onPlayRequested, onPauseRequested, onNextVideoRequested);
|
||||
|
||||
onShortcut('seekForward', (combo) => {
|
||||
if (video.state.time !== null) {
|
||||
|
|
@ -745,7 +695,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
onShortcut('statisticsMenu', () => {
|
||||
closeMenus();
|
||||
const stream = player.selected?.stream;
|
||||
if (streamingServer?.statistics?.type !== 'Err' && typeof stream === 'string' && typeof stream === 'number') {
|
||||
if (streamingServer?.statistics?.type !== 'Err' && typeof stream?.infoHash === 'string' && typeof stream?.fileIdx === 'number') {
|
||||
toggleStatisticsMenu();
|
||||
}
|
||||
}, [player.selected, streamingServer.statistics, toggleStatisticsMenu]);
|
||||
|
|
@ -793,7 +743,17 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
if (e.code === 'Space') {
|
||||
clearTimeout(pressTimer.current);
|
||||
pressTimer.current = null;
|
||||
onPlaybackSpeedChanged(playbackSpeed.current);
|
||||
if (longPress.current) {
|
||||
onPlaybackSpeedChanged(playbackSpeed.current);
|
||||
} else if (!menusOpen && video.state.paused !== null) {
|
||||
if (video.state.paused) {
|
||||
onPlayRequested();
|
||||
setSeeking(false);
|
||||
} else {
|
||||
onPauseRequested();
|
||||
}
|
||||
}
|
||||
longPress.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -832,12 +792,23 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
clearTimeout(pressTimer.current);
|
||||
pressTimer.current = null;
|
||||
if (longPress.current) {
|
||||
onPlaybackSpeedChanged(playbackSpeed.current);
|
||||
longPress.current = false;
|
||||
}
|
||||
setSeeking(false);
|
||||
};
|
||||
|
||||
if (routeFocused) {
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('wheel', onWheel);
|
||||
window.addEventListener('mousedown', onMouseDownHold);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
window.addEventListener('blur', onBlur);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
|
|
@ -845,8 +816,9 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
window.removeEventListener('wheel', onWheel);
|
||||
window.removeEventListener('mousedown', onMouseDownHold);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
window.removeEventListener('blur', onBlur);
|
||||
};
|
||||
}, [routeFocused, menusOpen, video.state.volume]);
|
||||
}, [routeFocused, menusOpen, video.state.volume, video.state.paused]);
|
||||
|
||||
React.useEffect(() => {
|
||||
video.events.on('error', onError);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => {
|
|||
const userLanguage = languages.toCode(props.subtitlesLanguage) ?? DEFAULT_SUBTITLES_LANGUAGE;
|
||||
const interfaceLanguage = languages.toCode(props.interfaceLanguage) ?? DEFAULT_SUBTITLES_LANGUAGE;
|
||||
const priorities = [LOCAL_SUBTITLES_LANGUAGE, userLanguage, interfaceLanguage];
|
||||
const langs = Object.keys(Object.groupBy(allSubtitles, ({ lang }) => lang)).sort((a, b) => a.localeCompare(b));
|
||||
const langs = [...new Set(allSubtitles.map(({ lang }) => lang))].sort((a, b) => a.localeCompare(b));
|
||||
return sortByValues(langs, priorities);
|
||||
}, [allSubtitles, props.subtitlesLanguage, props.interfaceLanguage]);
|
||||
|
||||
|
|
@ -95,11 +95,11 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => {
|
|||
}
|
||||
} else if (track.embedded) {
|
||||
if (typeof props.onSubtitlesTrackSelected === 'function') {
|
||||
props.onSubtitlesTrackSelected(track.id);
|
||||
props.onSubtitlesTrackSelected(track);
|
||||
}
|
||||
} else {
|
||||
if (typeof props.onExtraSubtitlesTrackSelected === 'function') {
|
||||
props.onExtraSubtitlesTrackSelected(track.id);
|
||||
props.onExtraSubtitlesTrackSelected(track);
|
||||
}
|
||||
}
|
||||
}, [allSubtitles, props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
||||
|
|
@ -113,7 +113,7 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => {
|
|||
props.onExtraSubtitlesTrackSelected(track.id);
|
||||
}
|
||||
}
|
||||
}, [props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
||||
}, [subtitlesTracksForLanguage, props.onSubtitlesTrackSelected, props.onExtraSubtitlesTrackSelected]);
|
||||
const onSubtitlesDelayChanged = React.useCallback((value) => {
|
||||
if (typeof props.selectedExtraSubtitlesTrackId === 'string') {
|
||||
if (props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay)) {
|
||||
|
|
|
|||
57
src/routes/Player/useMediaSession.ts
Normal file
57
src/routes/Player/useMediaSession.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
const useMediaSession = (
|
||||
videoState: VideoState,
|
||||
player: Player,
|
||||
onPlayRequested: () => void,
|
||||
onPauseRequested: () => void,
|
||||
onNextVideoRequested: () => void,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!navigator.mediaSession) return;
|
||||
|
||||
const playbackState = !videoState.paused ? 'playing' : 'paused';
|
||||
navigator.mediaSession.playbackState = playbackState;
|
||||
|
||||
return () => {
|
||||
navigator.mediaSession.playbackState = 'none';
|
||||
};
|
||||
}, [videoState.paused]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.mediaSession) return;
|
||||
|
||||
const metaItem = player.metaItem && player.metaItem?.type === 'Ready' ? player.metaItem.content as MetaItemPlayer : null;
|
||||
const videoId = player.selected ? player.selected?.streamRequest?.path?.id : null;
|
||||
const video = metaItem?.videos.find(({ id }) => id === videoId);
|
||||
|
||||
const videoInfo = video?.season && video?.episode ? ` (${video.season}x${video.episode})` : null;
|
||||
const videoTitle = video ? `${video.title}${videoInfo}` : null;
|
||||
const metaTitle = metaItem ? metaItem.name : null;
|
||||
const imageUrl = metaItem ? metaItem.logo : null;
|
||||
|
||||
const title = videoTitle ?? metaTitle;
|
||||
const artist = (videoTitle && metaTitle) ?? undefined;
|
||||
const artwork = imageUrl ? [{ src: imageUrl }] : undefined;
|
||||
|
||||
if (title) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title,
|
||||
artist,
|
||||
artwork,
|
||||
});
|
||||
}
|
||||
}, [player.metaItem, player.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.mediaSession) return;
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', onPlayRequested);
|
||||
navigator.mediaSession.setActionHandler('pause', onPauseRequested);
|
||||
|
||||
const nexVideoCallback = player.nextVideo ? onNextVideoRequested : null;
|
||||
navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback);
|
||||
}, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]);
|
||||
};
|
||||
|
||||
export default useMediaSession;
|
||||
3
src/routes/Player/videoState.d.ts
vendored
Normal file
3
src/routes/Player/videoState.d.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
type VideoState = {
|
||||
paused?: boolean;
|
||||
};
|
||||
|
|
@ -52,6 +52,9 @@ function CoreTransport(args) {
|
|||
this.decodeStream = async function(stream) {
|
||||
return bridge.call(['decodeStream'], [stream]);
|
||||
};
|
||||
this.encodeStream = async function(stream) {
|
||||
return bridge.call(['encodeStream'], [stream]);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = CoreTransport;
|
||||
|
|
|
|||
1
src/services/Core/types.d.ts
vendored
1
src/services/Core/types.d.ts
vendored
|
|
@ -17,6 +17,7 @@ interface CoreTransport {
|
|||
getState: (model: string) => Promise<object>,
|
||||
dispatch: (action: Action, model?: string) => Promise<void>,
|
||||
decodeStream: (stream: string) => Promise<Stream>,
|
||||
encodeStream: (stream: object) => Promise<string>,
|
||||
analytics: (event: AnalyticsEvent) => Promise<void>,
|
||||
on: (name: string, listener: () => void) => void,
|
||||
off: (name: string, listener: () => void) => void,
|
||||
|
|
|
|||
1
src/types/models/Player.d.ts
vendored
1
src/types/models/Player.d.ts
vendored
|
|
@ -5,7 +5,6 @@ type LibraryItemPlayer = Pick<LibraryItem, '_id'> & {
|
|||
type VideoPlayer = Video & {
|
||||
upcoming: boolean,
|
||||
watched: boolean,
|
||||
progress: boolean | null,
|
||||
scheduled: boolean,
|
||||
deepLinks: VideoDeepLinks,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue