Merge branch 'development' into fix/copy-download-and-copy-streaming-urls

This commit is contained in:
Timothy Z. 2026-01-22 12:25:43 +02:00
commit 370443609b
19 changed files with 352 additions and 161 deletions

View file

@ -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.51.1",
"@stremio/stremio-core-web": "0.52.0",
"@stremio/stremio-icons": "5.8.0",
"@stremio/stremio-video": "0.0.64",
"@stremio/stremio-video": "0.0.70",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -41,7 +41,7 @@
"react-i18next": "^15.1.3",
"react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "github:Stremio/stremio-translations#0e7fbd8522148f5727ac6adee3b2eb96132c10ac",
"stremio-translations": "github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},

View file

@ -18,14 +18,14 @@ importers:
specifier: 5.2.0
version: 5.2.0
'@stremio/stremio-core-web':
specifier: 0.51.1
version: 0.51.1
specifier: 0.52.0
version: 0.52.0
'@stremio/stremio-icons':
specifier: 5.8.0
version: 5.8.0
'@stremio/stremio-video':
specifier: 0.0.64
version: 0.0.64
specifier: 0.0.70
version: 0.0.70
a-color-picker:
specifier: 1.2.1
version: 1.2.1
@ -90,8 +90,8 @@ importers:
specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6
version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6
stremio-translations:
specifier: github:Stremio/stremio-translations#0e7fbd8522148f5727ac6adee3b2eb96132c10ac
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac
specifier: github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef
version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef
url:
specifier: 0.11.4
version: 0.11.4
@ -1120,14 +1120,14 @@ packages:
'@stremio/stremio-colors@5.2.0':
resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==}
'@stremio/stremio-core-web@0.51.1':
resolution: {integrity: sha512-BD8i6zkDdMPeCyH50Bb7SB8r4nYx4eJwz4kLEJEl0PFjdr0gOmwHtEIgNa89ShJLNXUjPnpv4sVSNxFRG8fb5Q==}
'@stremio/stremio-core-web@0.52.0':
resolution: {integrity: sha512-zT0P8JspGZ1oI9/11f3RIt7XG9b/1fOZE+xSnP+oAyhRmzzkqrnPUJkHdJdgoVD9XELDFAS2awNfl5/eRdh5kA==}
'@stremio/stremio-icons@5.8.0':
resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==}
'@stremio/stremio-video@0.0.64':
resolution: {integrity: sha512-29w/lwU8BB6ai8LUyCnpRc2F9kPf7cpys40NCobt70MqBP/UqvYISsrnD/ijoBwvtpKdZ6ptv5h9BbDj6rrerw==}
'@stremio/stremio-video@0.0.70':
resolution: {integrity: sha512-a0flQYAUdrZNMm7mmts2vpZOqN1nus7Hs9Mjl4mrN5rtduD0ojUyhD5J4lPcCpZ7WB0YdEUOGLXR19qHpgoKmg==}
'@stylistic/eslint-plugin-jsx@4.4.1':
resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==}
@ -3738,9 +3738,6 @@ packages:
prr@1.0.1:
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
punycode@1.3.2:
resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==}
punycode@1.4.1:
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
@ -3759,11 +3756,6 @@ packages:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
querystring@0.2.0:
resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==}
engines: {node: '>=0.4.x'}
deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -4141,9 +4133,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac:
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac}
version: 1.44.14
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef:
resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef}
version: 1.45.0
string-length@4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
@ -4420,9 +4412,6 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url@0.11.0:
resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==}
url@0.11.4:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'}
@ -5881,13 +5870,13 @@ snapshots:
'@stremio/stremio-colors@5.2.0': {}
'@stremio/stremio-core-web@0.51.1':
'@stremio/stremio-core-web@0.52.0':
dependencies:
'@babel/runtime': 7.24.1
'@stremio/stremio-icons@5.8.0': {}
'@stremio/stremio-video@0.0.64':
'@stremio/stremio-video@0.0.70':
dependencies:
buffer: 6.0.3
color: 4.2.3
@ -5897,7 +5886,7 @@ snapshots:
hls.js: https://github.com/Stremio/hls.js/releases/download/v1.5.4-patch2/hls.js-1.5.4-patch2.tgz
lodash.clonedeep: 4.5.0
magnet-uri: 6.2.0
url: 0.11.0
url: 0.11.4
video-name-parser: 1.4.6
vtt.js: https://codeload.github.com/jaruba/vtt.js/tar.gz/84d33d157848407d790d78423dacc41a096294f0
@ -8941,8 +8930,6 @@ snapshots:
prr@1.0.1:
optional: true
punycode@1.3.2: {}
punycode@1.4.1: {}
punycode@2.3.1: {}
@ -8957,8 +8944,6 @@ snapshots:
dependencies:
side-channel: 1.1.0
querystring@0.2.0: {}
queue-microtask@1.2.3: {}
randombytes@2.1.0:
@ -9393,7 +9378,7 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac: {}
stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef: {}
string-length@4.0.2:
dependencies:
@ -9688,11 +9673,6 @@ snapshots:
dependencies:
punycode: 2.3.1
url@0.11.0:
dependencies:
punycode: 1.3.2
querystring: 0.2.0
url@0.11.4:
dependencies:
punycode: 1.4.1

View file

@ -3,6 +3,10 @@
"name": "العربية",
"codes": ["ar-AR", "ara"]
},
{
"name": "Беларуская",
"codes": ["be-BY", "bel"]
},
{
"name": "български език",
"codes": ["bg-BG", "bul"]
@ -13,7 +17,7 @@
},
{
"name": "català",
"codes": ["ca-CA", "cat"]
"codes": ["ca-ES", "cat"]
},
{
"name": "čeština",
@ -43,6 +47,10 @@
"name": "español",
"codes": ["es-ES", "spa"]
},
{
"name": "Eesti",
"codes": ["et-EE", "est"]
},
{
"name": "euskara",
"codes": ["eu-ES", "eus"]
@ -111,6 +119,10 @@
"name": "Norsk nynorsk",
"codes": ["nn-NO", "nno"]
},
{
"name": "ਪੰਜਾਬੀ",
"codes": ["pa-IN", "pan"]
},
{
"name": "język polski",
"codes": ["pl-PL", "pol"]
@ -151,6 +163,10 @@
"name": "తెలుగు",
"codes": ["te-IN", "tel"]
},
{
"name": "தமிழ்",
"codes": ["tl-TM", "tam"]
},
{
"name": "Türkçe",
"codes": ["tr-TR", "tur"]
@ -159,6 +175,10 @@
"name": "українська мова",
"codes": ["uk-UA", "ukr"]
},
{
"name": "اُرْدُو",
"codes": ["ur-PK", "urd"]
},
{
"name": "Tiếng Việt",
"codes": ["vi-VN", "vie"]

View file

@ -7,6 +7,7 @@
align-self: stretch;
display: flex;
flex-direction: column;
max-height: 25rem;
width: 16rem;
.header {

View file

@ -29,6 +29,9 @@ const styles = require('./styles');
const Video = require('./Video');
const { default: Indicator } = require('./Indicator/Indicator');
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);
const Player = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
const services = useServices();
@ -37,8 +40,8 @@ const Player = ({ urlParams, queryParams }) => {
return queryParams.has('forceTranscoding');
}, [queryParams]);
const profile = useProfile();
const [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings();
const [player, videoParamsChanged, streamStateChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
const [settings] = useSettings();
const streamingServer = useStreamingServer();
const statistics = useStatistics(player, streamingServer);
const video = useVideo();
@ -93,17 +96,12 @@ const Player = ({ urlParams, queryParams }) => {
const isNavigating = React.useRef(false);
const onImplementationChanged = React.useCallback(() => {
video.setProp('subtitlesSize', settings.subtitlesSize);
video.setProp('subtitlesOffset', settings.subtitlesOffset);
video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
video.setProp('extraSubtitlesSize', settings.subtitlesSize);
video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
}, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
video.setSubtitlesSize(settings.subtitlesSize);
video.setSubtitlesOffset(settings.subtitlesOffset);
video.setSubtitlesTextColor(settings.subtitlesTextColor);
video.setSubtitlesBackgroundColor(settings.subtitlesBackgroundColor);
video.setSubtitlesOutlineColor(settings.subtitlesOutlineColor);
}, [settings]);
const handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => {
if (ended) {
@ -190,53 +188,71 @@ const Player = ({ urlParams, queryParams }) => {
}, []);
const onPlayRequested = React.useCallback(() => {
video.setProp('paused', false);
video.setPaused(false);
setSeeking(false);
}, []);
const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []);
const onPauseRequested = React.useCallback(() => {
video.setProp('paused', true);
video.setPaused(true);
}, []);
const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
const onMuteRequested = React.useCallback(() => {
video.setProp('muted', true);
video.setMuted(true);
}, []);
const onUnmuteRequested = React.useCallback(() => {
video.setProp('muted', false);
video.setMuted(false);
}, []);
const onVolumeChangeRequested = React.useCallback((volume) => {
video.setProp('volume', volume);
video.setVolume(volume);
}, []);
const onSeekRequested = React.useCallback((time) => {
video.setProp('time', time);
video.setTime(time);
seek(time, video.state.duration, video.state.manifest?.name);
}, [video.state.duration, video.state.manifest]);
const onPlaybackSpeedChanged = React.useCallback((rate) => {
video.setProp('playbackSpeed', rate);
video.setPlaybackSpeed(rate);
}, []);
const onSubtitlesTrackSelected = React.useCallback((id) => {
video.setSubtitlesTrack(id);
}, []);
streamStateChanged({
subtitleTrack: {
id,
embedded: true,
},
});
}, [streamStateChanged]);
const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
video.setExtraSubtitlesTrack(id);
}, []);
streamStateChanged({
subtitleTrack: {
id,
embedded: false,
},
});
}, [streamStateChanged]);
const onAudioTrackSelected = React.useCallback((id) => {
video.setProp('selectedAudioTrackId', id);
}, []);
video.setAudioTrack(id);
streamStateChanged({
audioTrack: {
id,
},
});
}, [streamStateChanged]);
const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
video.setProp('extraSubtitlesDelay', delay);
}, []);
video.setSubtitlesDelay(delay);
streamStateChanged({ subtitleDelay: delay });
}, [streamStateChanged]);
const onIncreaseSubtitlesDelay = React.useCallback(() => {
const delay = video.state.extraSubtitlesDelay + 250;
@ -249,8 +265,9 @@ const Player = ({ urlParams, queryParams }) => {
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
const onSubtitlesSizeChanged = React.useCallback((size) => {
updateSettings({ subtitlesSize: size });
}, [updateSettings]);
video.setSubtitlesSize(size);
streamStateChanged({ subtitleSize: size });
}, [streamStateChanged]);
const onUpdateSubtitlesSize = React.useCallback((delta) => {
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(video.state.subtitlesSize);
@ -259,8 +276,9 @@ const Player = ({ urlParams, queryParams }) => {
}, [video.state.subtitlesSize, onSubtitlesSizeChanged]);
const onSubtitlesOffsetChanged = React.useCallback((offset) => {
updateSettings({ subtitlesOffset: offset });
}, [updateSettings]);
video.setSubtitlesOffset(offset);
streamStateChanged({ subtitleOffset: offset });
}, [streamStateChanged]);
const onDismissNextVideoPopup = React.useCallback(() => {
closeNextVideoPopup();
@ -361,6 +379,7 @@ const Player = ({ urlParams, queryParams }) => {
forceTranscoding: forceTranscoding || casting,
maxAudioChannels: settings.surroundSound ? 32 : 2,
hardwareDecoding: settings.hardwareDecoding,
assSubtitlesStyling: settings.assSubtitlesStyling,
videoMode: settings.videoMode,
platform: platform.name,
streamingServerURL: streamingServer.baseUrl ?
@ -387,31 +406,6 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [player.subtitles, video.state.stream]);
React.useEffect(() => {
video.setProp('subtitlesSize', settings.subtitlesSize);
video.setProp('extraSubtitlesSize', settings.subtitlesSize);
}, [settings.subtitlesSize]);
React.useEffect(() => {
video.setProp('subtitlesOffset', settings.subtitlesOffset);
video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
}, [settings.subtitlesOffset]);
React.useEffect(() => {
video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
}, [settings.subtitlesTextColor]);
React.useEffect(() => {
video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
}, [settings.subtitlesBackgroundColor]);
React.useEffect(() => {
video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
}, [settings.subtitlesOutlineColor]);
React.useEffect(() => {
!seeking && timeChanged(video.state.time, video.state.duration, video.state.manifest?.name);
}, [video.state.time, video.state.duration, video.state.manifest, seeking]);
@ -444,41 +438,69 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [player.nextVideo, video.state.time, video.state.duration]);
// Auto subtitles track selection
React.useEffect(() => {
if (!defaultSubtitlesSelected.current) {
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
if (settings.subtitlesLanguage === null) {
onSubtitlesTrackSelected(null);
onExtraSubtitlesTrackSelected(null);
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
defaultSubtitlesSelected.current = true;
return;
}
const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
const savedTrackId = player.streamState?.subtitleTrack?.id;
const subtitlesTrack = savedTrackId ?
findTrackById(video.state.subtitlesTracks, savedTrackId) :
findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
const extraSubtitlesTrack = savedTrackId ?
findTrackById(video.state.extraSubtitlesTracks, savedTrackId) :
findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
if (subtitlesTrack && subtitlesTrack.id) {
onSubtitlesTrackSelected(subtitlesTrack.id);
video.setSubtitlesTrack(subtitlesTrack.id);
defaultSubtitlesSelected.current = true;
} else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
video.setExtraSubtitlesTrack(extraSubtitlesTrack.id);
defaultSubtitlesSelected.current = true;
}
}
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, player.streamState]);
// Auto audio track selection
React.useEffect(() => {
if (!defaultAudioTrackSelected.current) {
const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
const audioTrack = findTrackByLang(video.state.audioTracks, settings.audioLanguage);
const savedTrackId = player.streamState?.audioTrack?.id;
const audioTrack = savedTrackId ?
findTrackById(video.state.audioTracks, savedTrackId) :
findTrackByLang(video.state.audioTracks, settings.audioLanguage);
if (audioTrack && audioTrack.id) {
onAudioTrackSelected(audioTrack.id);
video.setAudioTrack(audioTrack.id);
defaultAudioTrackSelected.current = true;
}
}
}, [video.state.audioTracks]);
}, [video.state.audioTracks, player.streamState]);
// Saved subtitles settings
React.useEffect(() => {
if (video.state.stream !== null) {
const delay = player.streamState?.subtitleDelay;
if (typeof delay === 'number') {
video.setSubtitlesDelay(delay);
}
const size = player.streamState?.subtitleSize;
if (typeof size === 'number') {
video.setSubtitlesSize(size);
}
const offset = player.streamState?.subtitleOffset;
if (typeof offset === 'number') {
video.setSubtitlesOffset(offset);
}
}
}, [video.state.stream, player.streamState]);
React.useEffect(() => {
defaultSubtitlesSelected.current = false;

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import classNames from 'classnames';
import Icon from '@stremio/stremio-icons/react';
@ -37,6 +37,18 @@ const Stepper = ({ className, label, value, unit, step, min, max, disabled, onCh
timeout.cancel();
};
const decreaseDisabled = useMemo(() => {
return disabled || typeof value !== 'number' || (typeof min === 'number' && value <= min);
}, [disabled, min, value]);
const increaseDisabled = useMemo(() => {
return disabled || typeof value !== 'number' || (typeof max === 'number' && value >= max);
}, [disabled, max, value]);
const valueLabel = useMemo(() => {
return (disabled || typeof value !== 'number') ? '--' : `${value}${unit}`;
}, [disabled, value, unit]);
const updateValue = useCallback((delta: number) => {
onChange(clamp(localValue.current + delta, min, max));
}, [onChange]);
@ -72,7 +84,7 @@ const Stepper = ({ className, label, value, unit, step, min, max, disabled, onCh
</div>
<div className={styles['content']}>
<Button
className={classNames(styles['button'], { 'disabled': disabled })}
className={classNames(styles['button'], { 'disabled': decreaseDisabled })}
onMouseDown={onDecrementMouseDown}
onMouseUp={onDecrementMouseUp}
onMouseLeave={cancel}
@ -80,10 +92,10 @@ const Stepper = ({ className, label, value, unit, step, min, max, disabled, onCh
<Icon className={styles['icon']} name={'remove'} />
</Button>
<div className={styles['value']}>
{ disabled ? '--' : `${value}${unit}` }
{ valueLabel }
</div>
<Button
className={classNames(styles['button'], { 'disabled': disabled })}
className={classNames(styles['button'], { 'disabled': increaseDisabled })}
onMouseDown={onIncrementMouseDown}
onMouseUp={onIncrementMouseUp}
onMouseLeave={cancel}

View file

@ -86,6 +86,9 @@ const usePlayer = (urlParams) => {
};
}
}, [urlParams]);
const player = useModelState({ model: 'player', action, map });
const videoParamsChanged = React.useCallback((videoParams) => {
core.transport.dispatch({
action: 'Player',
@ -153,8 +156,22 @@ const usePlayer = (urlParams) => {
}, 'player');
}, []);
const player = useModelState({ model: 'player', action, map });
return [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo];
const streamStateChanged = React.useCallback((partialStreamState) => {
return core.transport.dispatch({
action: 'Player',
args: {
action: 'StreamStateChanged',
args: {
state: {
...player.streamState,
...partialStreamState,
},
},
},
}, 'player');
}, [player.streamState]);
return [player, videoParamsChanged, streamStateChanged, timeChanged, seek, pausedChanged, ended, nextVideo];
};
module.exports = usePlayer;

View file

@ -94,6 +94,30 @@ const useVideo = () => {
dispatch({ type: 'setProp', propName: name, propValue: value });
};
const setPaused = (state) => {
setProp('paused', state);
};
const setVolume = (volume) => {
setProp('volume', volume);
};
const setMuted = (state) => {
setProp('muted', state);
};
const setTime = (time) => {
setProp('time', time);
};
const setPlaybackSpeed = (rate) => {
setProp('playbackSpeed', rate);
};
const setAudioTrack = (id) => {
setProp('selectedAudioTrackId', id);
};
const setSubtitlesTrack = (id) => {
setProp('selectedSubtitlesTrackId', id);
setProp('selectedExtraSubtitlesTrackId', null);
@ -104,6 +128,35 @@ const useVideo = () => {
setProp('selectedExtraSubtitlesTrackId', id);
};
const setSubtitlesDelay = (delay) => {
setProp('extraSubtitlesDelay', delay);
};
const setSubtitlesSize = (size) => {
setProp('subtitlesSize', size);
setProp('extraSubtitlesSize', size);
};
const setSubtitlesOffset = (offset) => {
setProp('subtitlesOffset', offset);
setProp('extraSubtitlesOffset', offset);
};
const setSubtitlesTextColor = (color) => {
setProp('subtitlesTextColor', color);
setProp('extraSubtitlesTextColor', color);
};
const setSubtitlesBackgroundColor = (color) => {
setProp('subtitlesBackgroundColor', color);
setProp('extraSubtitlesBackgroundColor', color);
};
const setSubtitlesOutlineColor = (color) => {
setProp('subtitlesOutlineColor', color);
setProp('extraSubtitlesOutlineColor', color);
};
const onError = (error) => {
events.emit('error', error);
};
@ -171,8 +224,19 @@ const useVideo = () => {
unload,
addExtraSubtitlesTracks,
addLocalSubtitles,
setProp,
setPaused,
setVolume,
setMuted,
setTime,
setPlaybackSpeed,
setAudioTrack,
setSubtitlesTrack,
setSubtitlesDelay,
setSubtitlesSize,
setSubtitlesOffset,
setSubtitlesTextColor,
setSubtitlesBackgroundColor,
setSubtitlesOutlineColor,
setExtraSubtitlesTrack,
};
};

View file

@ -1,13 +1,12 @@
import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, MultiselectMenu, Toggle } from 'stremio/components';
import { Button } from 'stremio/components';
import { useServices } from 'stremio/services';
import { usePlatform, useToast } from 'stremio/common';
import { Section, Option, Link } from '../components';
import User from './User';
import useDataExport from './useDataExport';
import styles from './General.less';
import useGeneralOptions from './useGeneralOptions';
type Props = {
profile: Profile,
@ -15,18 +14,11 @@ type Props = {
const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
const { t } = useTranslation();
const { core, shell } = useServices();
const { core } = useServices();
const platform = usePlatform();
const toast = useToast();
const [dataExport, loadDataExport] = useDataExport();
const {
interfaceLanguageSelect,
quitOnCloseToggle,
escExitFullscreenToggle,
hideSpoilersToggle,
} = useGeneralOptions(profile);
const [traktAuthStarted, setTraktAuthStarted] = useState(false);
const isTraktAuthenticated = useMemo(() => {
@ -143,39 +135,6 @@ const General = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
</Button>
</Option>
</Section>
<Section>
<Option label={'SETTINGS_UI_LANGUAGE'}>
<MultiselectMenu
className={'multiselect'}
{...interfaceLanguageSelect}
/>
</Option>
{
shell.active &&
<Option label={'SETTINGS_QUIT_ON_CLOSE'}>
<Toggle
tabIndex={-1}
{...quitOnCloseToggle}
/>
</Option>
}
{
shell.active &&
<Option label={'SETTINGS_FULLSCREEN_EXIT'}>
<Toggle
tabIndex={-1}
{...escExitFullscreenToggle}
/>
</Option>
}
<Option label={'SETTINGS_BLUR_UNWATCHED_IMAGE'}>
<Toggle
tabIndex={-1}
{...hideSpoilersToggle}
/>
</Option>
</Section>
</>;
});

View file

@ -0,0 +1,57 @@
import React, { forwardRef } from 'react';
import { useServices } from 'stremio/services';
import { MultiselectMenu, Toggle } from 'stremio/components';
import { Section, Option } from '../components';
import useInterfaceOptions from './useInterfaceOptions';
type Props = {
profile: Profile,
};
const Interface = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
const { shell } = useServices();
const {
interfaceLanguageSelect,
quitOnCloseToggle,
escExitFullscreenToggle,
hideSpoilersToggle,
} = useInterfaceOptions(profile);
return (
<Section ref={ref} label={'INTERFACE'}>
<Option label={'SETTINGS_UI_LANGUAGE'}>
<MultiselectMenu
className={'multiselect'}
{...interfaceLanguageSelect}
/>
</Option>
{
shell.active &&
<Option label={'SETTINGS_QUIT_ON_CLOSE'}>
<Toggle
tabIndex={-1}
{...quitOnCloseToggle}
/>
</Option>
}
{
shell.active &&
<Option label={'SETTINGS_FULLSCREEN_EXIT'}>
<Toggle
tabIndex={-1}
{...escExitFullscreenToggle}
/>
</Option>
}
<Option label={'SETTINGS_BLUR_UNWATCHED_IMAGE'}>
<Toggle
tabIndex={-1}
{...hideSpoilersToggle}
/>
</Option>
</Section>
);
});
export default Interface;

View file

@ -0,0 +1,2 @@
import Interface from './Interface';
export default Interface;

View file

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { interfaceLanguages, useLanguageSorting } from 'stremio/common';
import { useServices } from 'stremio/services';
const useGeneralOptions = (profile: Profile) => {
const useInterfaceOptions = (profile: Profile) => {
const { core } = useServices();
const interfaceLanguageOptions = useMemo(() =>
@ -89,4 +89,4 @@ const useGeneralOptions = (profile: Profile) => {
};
};
export default useGeneralOptions;
export default useInterfaceOptions;

View file

@ -26,6 +26,9 @@ const Menu = ({ selected, streamingServer, onSelect }: Props) => {
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.GENERAL })} title={t('SETTINGS_NAV_GENERAL')} data-section={SECTIONS.GENERAL} onClick={onSelect}>
{ t('SETTINGS_NAV_GENERAL') }
</Button>
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.INTERFACE })} title={t('INTERFACE')} data-section={SECTIONS.INTERFACE} onClick={onSelect}>
{ t('INTERFACE') }
</Button>
<Button className={classNames(styles['button'], { [styles['selected']]: selected === SECTIONS.PLAYER })} title={t('SETTINGS_NAV_PLAYER')} data-section={SECTIONS.PLAYER} onClick={onSelect}>
{ t('SETTINGS_NAV_PLAYER') }
</Button>

View file

@ -19,6 +19,7 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
subtitlesTextColorInput,
subtitlesBackgroundColorInput,
subtitlesOutlineColorInput,
assSubtitlesStylingToggle,
audioLanguageSelect,
surroundSoundToggle,
seekTimeDurationSelect,
@ -149,6 +150,15 @@ const Player = forwardRef<HTMLDivElement, Props>(({ profile }: Props, ref) => {
/>
</Option>
}
{
shell.active &&
<Option label={'SETTINGS_ASS_SUBTITLES_STYLING'}>
<Toggle
tabIndex={-1}
{...assSubtitlesStylingToggle}
/>
</Option>
}
</Category>
</Section>
);

View file

@ -92,6 +92,22 @@ const usePlayerOptions = (profile: Profile) => {
}
}), [profile.settings]);
const assSubtitlesStylingToggle = useMemo(() => ({
checked: profile.settings.assSubtitlesStyling,
onClick: () => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
assSubtitlesStyling: !profile.settings.assSubtitlesStyling
}
}
});
}
}), [profile.settings]);
const subtitlesOutlineColorInput = useMemo(() => ({
value: profile.settings.subtitlesOutlineColor,
onChange: (value: string) => {
@ -341,6 +357,7 @@ const usePlayerOptions = (profile: Profile) => {
subtitlesTextColorInput,
subtitlesBackgroundColorInput,
subtitlesOutlineColorInput,
assSubtitlesStylingToggle,
audioLanguageSelect,
surroundSoundToggle,
seekTimeDurationSelect,

View file

@ -9,6 +9,7 @@ import { MainNavBars } from 'stremio/components';
import { SECTIONS } from './constants';
import Menu from './Menu';
import General from './General';
import Interface from './Interface';
import Player from './Player';
import Streaming from './Streaming';
import Shortcuts from './Shortcuts';
@ -23,12 +24,14 @@ const Settings = () => {
const sectionsContainerRef = useRef<HTMLDivElement>(null);
const generalSectionRef = useRef<HTMLDivElement>(null);
const interfaceSectionRef = useRef<HTMLDivElement>(null);
const playerSectionRef = useRef<HTMLDivElement>(null);
const streamingServerSectionRef = useRef<HTMLDivElement>(null);
const shortcutsSectionRef = useRef<HTMLDivElement>(null);
const sections = useMemo(() => ([
{ ref: generalSectionRef, id: SECTIONS.GENERAL },
{ ref: interfaceSectionRef, id: SECTIONS.INTERFACE },
{ ref: playerSectionRef, id: SECTIONS.PLAYER },
{ ref: streamingServerSectionRef, id: SECTIONS.STREAMING },
{ ref: shortcutsSectionRef, id: SECTIONS.SHORTCUTS },
@ -82,6 +85,10 @@ const Settings = () => {
ref={generalSectionRef}
profile={profile}
/>
<Interface
ref={interfaceSectionRef}
profile={profile}
/>
<Player
ref={playerSectionRef}
profile={profile}

View file

@ -1,6 +1,7 @@
const SECTIONS = {
GENERAL: 'general',
PLAYER: 'player',
INTERFACE: 'interface',
STREAMING: 'streaming',
SHORTCUTS: 'shortcuts',
};

View file

@ -42,6 +42,7 @@ type Settings = {
subtitlesOutlineColor: string,
subtitlesSize: number,
subtitlesTextColor: string,
assSubtitlesStyling: boolean,
surroundSound: boolean,
pauseOnMinimize: boolean,
};

View file

@ -30,6 +30,23 @@ type SeriesInfo = {
season: number,
};
type SubtitlesTrackState = {
id: string,
embedded: boolean,
};
type AudioTrackState = {
id: string,
};
type StreamState = {
subtitleTrack?: SubtitlesTrackState,
subtitleDelay?: number,
subtitleSize?: number,
subtitleOffset?: number,
audioTrack?: AudioTrackState,
};
type Player = {
addon: Addon | null,
libraryItem: LibraryItemPlayer | null,
@ -42,6 +59,7 @@ type Player = {
subtitlesPath: ResourceRequestPath,
} | null,
seriesInfo: SeriesInfo | null,
streamState: StreamState | null,
subtitles: Subtitle[],
title: string | null,
};