Merge branch 'development' into feat/shell-volume-booster

This commit is contained in:
Ivelin Megdanov 2025-02-18 11:34:01 +02:00
commit 1fc5ab290d
36 changed files with 648 additions and 214 deletions

19
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "stremio",
"version": "5.0.0-beta.17",
"version": "5.0.0-beta.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "stremio",
"version": "5.0.0-beta.17",
"version": "5.0.0-beta.18",
"license": "gpl-2.0",
"dependencies": {
"@babel/runtime": "7.26.0",
@ -14,7 +14,7 @@
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.48.5",
"@stremio/stremio-icons": "5.4.1",
"@stremio/stremio-video": "0.0.52",
"@stremio/stremio-video": "0.0.53",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -36,7 +36,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#a0f50634202f748a57907b645d2cd92fbaa479dd",
"stremio-translations": "github:Stremio/stremio-translations#62bcc6e8f44258203c7375af59210771efb6f634",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
@ -3409,10 +3409,9 @@
]
},
"node_modules/@stremio/stremio-video": {
"version": "0.0.52",
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.52.tgz",
"integrity": "sha512-OlHC8FIvYEyGXcNAM4W044Dqx6CmGb5BV3fDU361SyUjO9gKXXUWdL7LwmwHeWFeuy2sK1MEg4AT2JPptvJ0rg==",
"license": "MIT",
"version": "0.0.53",
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.53.tgz",
"integrity": "sha512-hSlk8GqMdk4N8VbcdvduYqWVZsQLgHyU7GfFmd1k+t0pSpDKAhI3C6dohG5Sr09CKCjHa8D1rls+CwMNPXLSGw==",
"dependencies": {
"buffer": "6.0.3",
"color": "4.2.3",
@ -13374,8 +13373,8 @@
},
"node_modules/stremio-translations": {
"version": "1.44.9",
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a0f50634202f748a57907b645d2cd92fbaa479dd",
"integrity": "sha512-JJpd1JJet3T6/VTNdZ2NZ7uvHJ4zkuyqo5BnTcDGqLVNO/OpicGqKhZjE4WGSgmuhsfPBU8T0ICCfzKu2xpvKg==",
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#62bcc6e8f44258203c7375af59210771efb6f634",
"integrity": "sha512-8Sc5Qvd4IiObwGXkmj1XFXFavUc15My5po6G48HHDBbp42SVc5I/t7h+1yxW1A81byyBCXbL23a9iU9v49vpQA==",
"license": "MIT"
},
"node_modules/string_decoder": {

View file

@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
"version": "5.0.0-beta.17",
"version": "5.0.0-beta.18",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
@ -18,7 +18,7 @@
"@stremio/stremio-colors": "5.2.0",
"@stremio/stremio-core-web": "0.48.5",
"@stremio/stremio-icons": "5.4.1",
"@stremio/stremio-video": "0.0.52",
"@stremio/stremio-video": "0.0.53",
"a-color-picker": "1.2.1",
"bowser": "2.11.0",
"buffer": "6.0.3",
@ -40,7 +40,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#a0f50634202f748a57907b645d2cd92fbaa479dd",
"stremio-translations": "github:Stremio/stremio-translations#62bcc6e8f44258203c7375af59210771efb6f634",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},

View file

@ -6,10 +6,11 @@ const { useTranslation } = require('react-i18next');
const { Router } = require('stremio-router');
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
const ServicesToaster = require('./ServicesToaster');
const DeepLinkHandler = require('./DeepLinkHandler');
const SearchParamsHandler = require('./SearchParamsHandler');
const { default: UpdaterBanner } = require('./UpdaterBanner');
const ErrorDialog = require('./ErrorDialog');
const withProtectedRoutes = require('./withProtectedRoutes');
const routerViewsConfig = require('./routerViewsConfig');
@ -165,14 +166,17 @@ const App = () => {
<PlatformProvider>
<ToastProvider className={styles['toasts-container']}>
<TooltipProvider className={styles['tooltip-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
<FileDropProvider className={styles['file-drop-container']}>
<ServicesToaster />
<DeepLinkHandler />
<SearchParamsHandler />
<UpdaterBanner className={styles['updater-banner-container']} />
<RouterWithProtectedRoutes
className={styles['router']}
viewsConfig={routerViewsConfig}
onPathNotMatch={onPathNotMatch}
/>
</FileDropProvider>
</TooltipProvider>
</ToastProvider>
</PlatformProvider>

View file

@ -0,0 +1,46 @@
.updater-banner {
height: 4rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0 1rem;
font-size: 1rem;
font-weight: bold;
color: var(--primary-foreground-color);
background-color: var(--primary-accent-color);
.button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1rem;
border-radius: var(--border-radius);
color: var(--primary-background-color);
background-color: var(--primary-foreground-color);
transition: all 0.1s ease-out;
&:hover {
color: var(--primary-foreground-color);
background-color: transparent;
box-shadow: inset 0 0 0 0.15rem var(--primary-foreground-color);
}
}
.close {
position: absolute;
right: 0;
height: 4rem;
width: 4rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.icon {
height: 2rem;
}
}
}

View file

@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import Icon from '@stremio/stremio-icons/react';
import { useTranslation } from 'react-i18next';
import { useServices } from 'stremio/services';
import { useBinaryState, useShell } from 'stremio/common';
import { Button, Transition } from 'stremio/components';
import styles from './UpdaterBanner.less';
type Props = {
className: string,
};
const UpdaterBanner = ({ className }: Props) => {
const { t } = useTranslation();
const { shell } = useServices();
const shellTransport = useShell();
const [visible, show, hide] = useBinaryState(false);
const onInstallClick = () => {
shellTransport.send('autoupdater-notif-clicked');
};
useEffect(() => {
shell.transport && shell.transport.on('autoupdater-show-notif', show);
return () => {
shell.transport && shell.transport.off('autoupdater-show-notif', show);
};
}, []);
return (
<div className={className}>
<Transition when={visible} name={'slide-up'}>
<div className={styles['updater-banner']}>
<div className={styles['label']}>
{ t('UPDATER_TITLE') }
</div>
<Button className={styles['button']} onClick={onInstallClick}>
{ t('UPDATER_INSTALL_BUTTON') }
</Button>
<Button className={styles['close']} onClick={hide}>
<Icon className={styles['icon']} name={'close'} />
</Button>
</div>
</Transition>
</div>
);
};
export default UpdaterBanner;

View file

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

View file

@ -204,12 +204,32 @@ html {
background-color: var(--modal-background-color);
box-shadow: var(--outer-glow);
transition: opacity 0.1s ease-out;
}
.file-drop-container {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: 1rem;
border: 0.5rem dashed transparent;
pointer-events: none;
transition: border-color 0.25s ease-out;
&:global(.active) {
transition-delay: 0.25s;
border-color: var(--primary-accent-color);
}
}
.updater-banner-container {
z-index: 1;
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
.router {
width: 100%;
height: 100%;

View file

@ -41,6 +41,16 @@ const ICON_FOR_TYPE = new Map([
['other', 'movies'],
]);
const MIME_SIGNATURES = {
'application/x-subrip': ['310D0A', '310A'],
'text/vtt': ['574542565454'],
};
const SUPPORTED_LOCAL_SUBTITLES = [
'application/x-subrip',
'text/vtt',
];
const EXTERNAL_PLAYERS = [
{
label: 'EXTERNAL_PLAYER_DISABLED',
@ -113,6 +123,8 @@ module.exports = {
WRITERS_LINK_CATEGORY,
TYPE_PRIORITIES,
ICON_FOR_TYPE,
MIME_SIGNATURES,
SUPPORTED_LOCAL_SUBTITLES,
EXTERNAL_PLAYERS,
WHITELISTED_HOSTS,
};

View file

@ -0,0 +1,91 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import classNames from 'classnames';
import { isFileType } from './utils';
export type FileType = string;
export type FileDropListener = (filename: string, buffer: ArrayBuffer) => void;
type FileDropContext = {
on: (type: FileType, listener: FileDropListener) => void,
off: (type: FileType, listener: FileDropListener) => void,
};
const FileDropContext = createContext({} as FileDropContext);
type Props = {
className: string,
children: JSX.Element,
};
const FileDropProvider = ({ className, children }: Props) => {
const [listeners, setListeners] = useState<[FileType, FileDropListener][]>([]);
const [active, setActive] = useState(false);
const onDragOver = (event: DragEvent) => {
event.preventDefault();
setActive(true);
};
const onDragLeave = () => {
setActive(false);
};
const onDrop = useCallback((event: DragEvent) => {
event.preventDefault();
const { dataTransfer } = event;
if (dataTransfer && dataTransfer?.files.length > 0) {
const file = dataTransfer.files[0];
file
.arrayBuffer()
.then((buffer) => {
listeners
.filter(([type]) => file.type ? type === file.type : isFileType(buffer, type))
.forEach(([, listerner]) => listerner(file.name, buffer));
});
}
setActive(false);
}, [listeners]);
const on = (type: FileType, listener: FileDropListener) => {
setListeners((listeners) => {
return [...listeners, [type, listener]];
});
};
const off = (type: FileType, listener: FileDropListener) => {
setListeners((listeners) => {
return listeners.filter(([key, value]) => key !== type && value !== listener);
});
};
useEffect(() => {
window.addEventListener('dragover', onDragOver);
window.addEventListener('dragleave', onDragLeave);
window.addEventListener('drop', onDrop);
return () => {
window.removeEventListener('dragover', onDragOver);
window.removeEventListener('dragleave', onDragLeave);
window.removeEventListener('drop', onDrop);
};
}, [onDrop]);
return (
<FileDropContext.Provider value={{ on, off }}>
{ children }
<div className={classNames(className, { 'active': active })} />
</FileDropContext.Provider>
);
};
const useFileDrop = () => {
return useContext(FileDropContext);
};
export {
FileDropProvider,
useFileDrop,
};

View file

@ -0,0 +1,8 @@
import { FileDropProvider, useFileDrop } from './FileDrop';
import onFileDrop from './onFileDrop';
export {
FileDropProvider,
useFileDrop,
onFileDrop,
};

View file

@ -0,0 +1,14 @@
import { useEffect } from 'react';
import { type FileType, type FileDropListener, useFileDrop } from './FileDrop';
const onFileDrop = (types: FileType[], listener: FileDropListener) => {
const { on, off } = useFileDrop();
useEffect(() => {
types.forEach((type) => on(type, listener));
return () => types.forEach((type) => off(type, listener));
}, []);
};
export default onFileDrop;

View file

@ -0,0 +1,19 @@
import { MIME_SIGNATURES } from 'stremio/common/CONSTANTS';
const SIGNATURES = MIME_SIGNATURES as Record<string, string[]>;
const isFileType = (buffer: ArrayBuffer, type: string) => {
const signatures = SIGNATURES[type];
return signatures.some((signature) => {
const array = new Uint8Array(buffer);
const signatureBuffer = Buffer.from(signature, 'hex');
const bufferToCompare = array.subarray(0, signatureBuffer.length);
return Buffer.compare(signatureBuffer, bufferToCompare) === 0;
});
};
export {
isFileType,
};

View file

@ -1,6 +1,6 @@
import React, { createContext, useContext } from 'react';
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
import useShell from './useShell';
import useShell from 'stremio/common/useShell';
import { name, isMobile } from './device';
interface PlatformContext {

View file

@ -69,6 +69,19 @@
transform: translateX(100%);
}
.slide-up-enter {
transform: translateY(100%);
}
.slide-up-active {
transform: translateY(0%);
transition: transform 0.3s cubic-bezier(0.32, 0, 0.67, 0);
}
.slide-up-exit {
transform: translateY(100%);
}
@keyframes fade-in-no-motion {
0% {
opacity: 0;

View file

@ -1,5 +1,6 @@
// Copyright (C) 2017-2023 Smart code 203358507
const { FileDropProvider, onFileDrop } = require('./FileDrop');
const { PlatformProvider, usePlatform } = require('./Platform');
const { ToastProvider, useToast } = require('./Toast');
const { TooltipProvider, Tooltip } = require('./Tooltips');
@ -19,11 +20,14 @@ const useModelState = require('./useModelState');
const useNotifications = require('./useNotifications');
const useOnScrollToBottom = require('./useOnScrollToBottom');
const useProfile = require('./useProfile');
const { default: useShell } = require('./useShell');
const useStreamingServer = require('./useStreamingServer');
const useTorrent = require('./useTorrent');
const useTranslate = require('./useTranslate');
module.exports = {
FileDropProvider,
onFileDrop,
PlatformProvider,
usePlatform,
ToastProvider,
@ -47,6 +51,7 @@ module.exports = {
useNotifications,
useOnScrollToBottom,
useProfile,
useShell,
useStreamingServer,
useTorrent,
useTranslate,

View file

@ -8,6 +8,7 @@ import styles from './Button.less';
type Props = {
className?: string,
href?: string,
target?: string
title?: string,
disabled?: boolean,
tabIndex?: number,

View file

@ -0,0 +1,83 @@
// Copyright (C) 2017-2025 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
.checkbox {
display: flex;
align-items: center;
overflow: visible;
.label {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5rem 0;
cursor: pointer;
span {
font-size: 0.9rem;
color: var(--primary-foreground-color);
opacity: 0.6;
}
.link {
font-size: 0.9rem;
color: var(--primary-accent-color);
&:hover {
text-decoration: underline;
}
}
}
.checkbox-container {
position: relative;
width: 1.5rem;
height: 1.5rem;
border-radius: 0.3rem;
background-color: var(--overlay-color);
padding: 0.1rem;
display: flex;
flex: none;
margin: 0 1rem 0 0.3rem;
align-items: center;
justify-content: center;
transition: all 0.2s ease-in-out;
cursor: pointer;
outline: none;
user-select: none;
outline-width: var(--focus-outline-size);
outline-color: @color-surface-light5;
outline-offset: 2px;
input[type='checkbox'] {
opacity: 0;
width: 0;
height: 0;
position: absolute;
cursor: pointer;
}
.checkbox-icon {
width: 100%;
height: 100%;
color: var(--primary-foreground-color);
}
&.disabled {
cursor: not-allowed;
}
&.error {
border-color: var(--color-trakt);
}
&.checked {
background-color: var(--primary-accent-color);
}
&:hover, &:focus {
outline-style: solid;
}
}
}

View file

@ -0,0 +1,97 @@
// Copyright (C) 2017-2025 Smart code 203358507
import React, { useCallback, ChangeEvent, KeyboardEvent, RefCallback } from 'react';
import classNames from 'classnames';
import styles from './Checkbox.less';
import Button from '../Button';
import Icon from '@stremio/stremio-icons/react';
type Props = {
ref?: RefCallback<HTMLInputElement>;
name: string;
disabled?: boolean;
checked?: boolean;
className?: string;
label?: string;
link?: string;
href?: string;
onChange?: (props: {
type: string;
checked: boolean;
reactEvent: KeyboardEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>;
nativeEvent: Event;
}) => void;
error?: string;
};
const Checkbox = React.forwardRef<HTMLInputElement, Props>(({ name, disabled, className, label, href, link, onChange, error, checked }, ref) => {
const handleSelect = useCallback((event: ChangeEvent<HTMLInputElement>) => {
if (!disabled && onChange) {
onChange({
type: 'select',
checked: event.target.checked,
reactEvent: event,
nativeEvent: event.nativeEvent,
});
}
}, [disabled, onChange]);
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
if ((event.key === 'Enter' || event.key === ' ') && !disabled) {
onChange && onChange({
type: 'select',
checked: !checked,
reactEvent: event as KeyboardEvent<HTMLInputElement>,
nativeEvent: event.nativeEvent,
});
}
}, [disabled, checked, onChange]);
return (
<div className={classNames(styles['checkbox'], className)}>
<label className={styles['label']} htmlFor={name}>
<div
className={classNames(
styles['checkbox-container'],
{ [styles['checked']]: checked },
{ [styles['disabled']]: disabled },
{ [styles['error']]: error }
)}
role={'checkbox'}
tabIndex={disabled ? -1 : 0}
aria-checked={checked}
onKeyDown={onKeyDown}
>
<input
ref={ref}
id={name}
type={'checkbox'}
checked={checked}
disabled={disabled}
onChange={handleSelect}
className={styles['input']}
/>
{
checked ?
<Icon name={'checkmark'} className={styles['checkbox-icon']} />
: null
}
</div>
<div>
<span>{label}</span>
{' '}
{
href && link ?
<Button className={styles['link']} href={href} target={'_blank'} tabIndex={-1}>
{link}
</Button>
: null
}
</div>
</label>
</div>
);
});
export default Checkbox;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2025 Smart code 203358507
import Checkbox from './Checkbox';
export default Checkbox;

View file

@ -2,7 +2,8 @@
const React = require('react');
const { useTranslation } = require('react-i18next');
const { Button, ModalDialog } = require('stremio/components');
const { default: Button } = require('stremio/components/Button');
const ModalDialog = require('stremio/components/ModalDialog');
const useEvents = require('./useEvents');
const styles = require('./styles');
const { default: Icon } = require('@stremio/stremio-icons/react');

View file

@ -13,6 +13,7 @@
background-color: transparent;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
@ -21,6 +22,7 @@
.nav-tab-button {
width: calc(var(--vertical-nav-bar-size) - 1.2rem);
height: calc(var(--vertical-nav-bar-size) - 1.2rem);
min-height: 3.5rem;
}
}

View file

@ -39,7 +39,8 @@ html.active-slider-within {
flex: 1;
height: var(--track-size);
border-radius: var(--track-size);
background-color: var(--overlay-color);
background-color: var(--primary-accent-color);
opacity: 0.2;
&.audio-boost {
background: linear-gradient(to right,

View file

@ -1,6 +1,7 @@
import AddonDetailsModal from './AddonDetailsModal';
import BottomSheet from './BottomSheet';
import Button from './Button';
import Checkbox from './Checkbox';
import Chips from './Chips';
import ColorInput from './ColorInput';
import ContinueWatchingItem from './ContinueWatchingItem';
@ -31,6 +32,7 @@ export {
AddonDetailsModal,
BottomSheet,
Button,
Checkbox,
Chips,
ColorInput,
ContinueWatchingItem,

View file

@ -95,4 +95,4 @@
&:not(.active):hover {
border-color: var(--overlay-color);
}
}
}

View file

@ -10,6 +10,10 @@
width: 20rem;
padding: 0 1rem;
overflow-y: auto;
@supports (scroll-padding-block-start: 0.15rem) {
scroll-padding-block-start: 0.15rem;
}
}
@media only screen and (max-width: @small) and (orientation: portrait) {
@ -34,4 +38,4 @@
.list {
display: none;
}
}
}

View file

@ -1,56 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { Button, Toggle } = require('stremio/components');
const styles = require('./styles');
const ConsentToggle = React.forwardRef(({ className, label, link, href, onToggle, ...props }, ref) => {
const toggleOnClick = React.useCallback((event) => {
if (typeof props.onClick === 'function') {
props.onClick(event);
}
if (!event.nativeEvent.togglePrevented && typeof onToggle === 'function') {
onToggle({
type: 'toggle',
reactEvent: event,
nativeEvent: event.nativeEvent
});
}
}, [onToggle, props.onClick]);
const linkOnClick = React.useCallback((event) => {
event.nativeEvent.togglePrevented = true;
}, []);
return (
<Toggle {...props} ref={ref} className={classnames(className, styles['consent-toggle-container'])} onClick={toggleOnClick}>
<div className={styles['label']}>
{label}
{' '}
{
typeof link === 'string' && link.length > 0 && typeof href === 'string' && href.length > 0 ?
<Button className={styles['link']} href={href} target={'_blank'} tabIndex={-1} onClick={linkOnClick}>
{link}
</Button>
:
null
}
</div>
</Toggle>
);
});
ConsentToggle.displayName = 'ConsentToggle';
ConsentToggle.propTypes = {
className: PropTypes.string,
checked: PropTypes.bool,
label: PropTypes.string,
link: PropTypes.string,
href: PropTypes.string,
onToggle: PropTypes.func,
onClick: PropTypes.func
};
module.exports = ConsentToggle;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
const ConsentToggle = require('./ConsentToggle');
module.exports = ConsentToggle;

View file

@ -1,43 +0,0 @@
// Copyright (C) 2017-2023 Smart code 203358507
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
:import('~stremio/components/Toggle/styles.less') {
checkbox-icon: icon;
}
.consent-toggle-container {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5rem 0;
border-radius: var(--border-radius);
&:focus {
outline: none;
background-color: var(--overlay-color);
}
&:global(.checked) {
.label {
opacity: 1;
}
}
.label {
flex: 1;
margin-left: 1rem;
font-size: 0.9rem;
color: var(--primary-foreground-color);
opacity: 0.6;
.link {
font-size: 0.9rem;
color: var(--primary-accent-color);
&:hover {
text-decoration: underline;
}
}
}
}

View file

@ -8,9 +8,8 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
const { Modal, useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { useBinaryState } = require('stremio/common');
const { Button, Image } = require('stremio/components');
const { Button, Image, Checkbox } = require('stremio/components');
const CredentialsTextInput = require('./CredentialsTextInput');
const ConsentToggle = require('./ConsentToggle');
const PasswordResetModal = require('./PasswordResetModal');
const useFacebookLogin = require('./useFacebookLogin');
const styles = require('./styles');
@ -308,30 +307,27 @@ const Intro = ({ queryParams }) => {
onChange={confirmPasswordOnChange}
onSubmit={confirmPasswordOnSubmit}
/>
<ConsentToggle
<Checkbox
ref={termsRef}
className={styles['consent-toggle']}
label={'I have read and agree with the Stremio'}
link={'Terms and conditions'}
href={'https://www.stremio.com/tos'}
checked={state.termsAccepted}
onToggle={toggleTermsAccepted}
onChange={toggleTermsAccepted}
/>
<ConsentToggle
<Checkbox
ref={privacyPolicyRef}
className={styles['consent-toggle']}
label={'I have read and agree with the Stremio'}
link={'Privacy Policy'}
href={'https://www.stremio.com/privacy'}
checked={state.privacyPolicyAccepted}
onToggle={togglePrivacyPolicyAccepted}
onChange={togglePrivacyPolicyAccepted}
/>
<ConsentToggle
<Checkbox
ref={marketingRef}
className={styles['consent-toggle']}
label={'I agree to receive marketing communications from Stremio'}
checked={state.marketingAccepted}
onToggle={toggleMarketingAccepted}
onChange={toggleMarketingAccepted}
/>
</React.Fragment>
:

View file

@ -8,7 +8,7 @@ const langs = require('langs');
const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
const { onFileDrop, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS } = require('stremio/common');
const { HorizontalNavBar, Transition } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
@ -133,11 +133,20 @@ const Player = ({ urlParams, queryParams }) => {
toast.show({
type: 'success',
title: t('PLAYER_SUBTITLES_LOADED'),
message: track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') : t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
message:
track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') :
track.local ? t('PLAYER_SUBTITLES_LOADED_LOCAL') :
t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
timeout: 3000
});
}, []);
const onExtraSubtitlesTrackAdded = React.useCallback((track) => {
if (track.local) {
video.setExtraSubtitlesTrack(track.id);
}
}, []);
const onPlayRequested = React.useCallback(() => {
video.setProp('paused', false);
setSeeking(false);
@ -172,13 +181,11 @@ const Player = ({ urlParams, queryParams }) => {
}, []);
const onSubtitlesTrackSelected = React.useCallback((id) => {
video.setProp('selectedSubtitlesTrackId', id);
video.setProp('selectedExtraSubtitlesTrackId', null);
video.setSubtitlesTrack(id);
}, []);
const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
video.setProp('selectedSubtitlesTrackId', null);
video.setProp('selectedExtraSubtitlesTrackId', id);
video.setExtraSubtitlesTrack(id);
}, []);
const onAudioTrackSelected = React.useCallback((id) => {
@ -270,6 +277,10 @@ const Player = ({ urlParams, queryParams }) => {
event.nativeEvent.immersePrevented = true;
}, []);
onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, async (filename, buffer) => {
video.addLocalSubtitles(filename, buffer);
});
React.useEffect(() => {
setError(null);
video.unload();
@ -296,6 +307,7 @@ const Player = ({ urlParams, queryParams }) => {
0,
forceTranscoding: forceTranscoding || casting,
maxAudioChannels: settings.surroundSound ? 32 : 2,
hardwareDecoding: settings.hardwareDecoding,
streamingServerURL: streamingServer.baseUrl ?
casting ?
streamingServer.baseUrl
@ -303,7 +315,7 @@ const Player = ({ urlParams, queryParams }) => {
streamingServer.selected.transportUrl
:
null,
seriesInfo: player.seriesInfo
seriesInfo: player.seriesInfo,
}, {
chromecastTransport: chromecast.active ? chromecast.transport : null,
shellTransport: shell.active ? shell.transport : null,
@ -586,6 +598,7 @@ const Player = ({ urlParams, queryParams }) => {
video.events.on('ended', onEnded);
video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
video.events.on('implementationChanged', onImplementationChanged);
return () => {
@ -593,6 +606,7 @@ const Player = ({ urlParams, queryParams }) => {
video.events.off('ended', onEnded);
video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
video.events.off('implementationChanged', onImplementationChanged);
};
}, []);

View file

@ -10,11 +10,13 @@ const styles = require('./styles');
const { t } = require('i18next');
const ORIGIN_PRIORITIES = {
'LOCAL': 3,
'EMBEDDED': 2,
'EXCLUSIVE': 1
'EXCLUSIVE': 1,
};
const LANGUAGE_PRIORITIES = {
'eng': 1
'local': 2,
'eng': 1,
};
const SubtitlesMenu = React.memo((props) => {
@ -161,7 +163,11 @@ const SubtitlesMenu = React.memo((props) => {
</Button>
{subtitlesLanguages.map((lang, index) => (
<Button key={index} title={languages.label(lang)} className={classnames(styles['language-option'], { 'selected': selectedSubtitlesLanguage === lang })} data-lang={lang} onClick={subtitlesLanguageOnClick}>
<div className={styles['language-label']}>{languages.label(lang)}</div>
<div className={styles['language-label']}>
{
lang === 'local' ? t('LOCAL') : languages.label(lang)
}
</div>
{
selectedSubtitlesLanguage === lang ?
<div className={styles['icon']} />
@ -207,60 +213,62 @@ const SubtitlesMenu = React.memo((props) => {
}
</div>
<div className={styles['subtitles-settings-container']}>
<div className={styles['settings-header']}>Subtitles Settings</div>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('DELAY')}
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
onChange={onSubtitlesDelayChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('SIZE')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize) ? `${props.extraSubtitlesSize}%` : '--'
<div className={styles['settings-header']}>{t('PLAYER_SUBTITLES_SETTINGS')}</div>
<div className={styles['settings-list']}>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('DELAY')}
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
onChange={onSubtitlesDelayChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('SIZE')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize === null || isNaN(props.subtitlesSize)
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize) ? `${props.extraSubtitlesSize}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize === null || isNaN(props.subtitlesSize)
:
true
}
onChange={onSubtitlesSizeChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
:
true
}
onChange={onSubtitlesSizeChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
:
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
:
'--'
}
disabled={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
:
true
}
onChange={onSubtitlesOffsetChanged}
/>
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
:
true
}
onChange={onSubtitlesOffsetChanged}
/>
</div>
</div>
</div>
);

View file

@ -106,6 +106,10 @@
.subtitles-settings-container {
width: 17rem;
.settings-list {
overflow-y: scroll;
}
.spacing {
flex: 1;
}

View file

@ -79,10 +79,31 @@ const useVideo = () => {
});
};
const addLocalSubtitles = (filename, buffer) => {
dispatch({
type: 'command',
commandName: 'addLocalSubtitles',
commandArgs: {
filename,
buffer,
},
});
};
const setProp = (name, value) => {
dispatch({ type: 'setProp', propName: name, propValue: value });
};
const setSubtitlesTrack = (id) => {
setProp('selectedSubtitlesTrackId', id);
setProp('selectedExtraSubtitlesTrackId', null);
};
const setExtraSubtitlesTrack = (id) => {
setProp('selectedSubtitlesTrackId', null);
setProp('selectedExtraSubtitlesTrackId', id);
};
const onError = (error) => {
events.emit('error', error);
};
@ -99,6 +120,10 @@ const useVideo = () => {
events.emit('extraSubtitlesTrackLoaded', track);
};
const onExtraSubtitlesTrackAdded = (track) => {
events.emit('extraSubtitlesTrackAdded', track);
};
const onPropChanged = (name, value) => {
setState((state) => ({
...state,
@ -125,6 +150,7 @@ const useVideo = () => {
video.current.on('implementationChanged', onImplementationChanged);
video.current.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
video.current.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.current.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
return () => video.current.destroy();
}, []);
@ -136,7 +162,10 @@ const useVideo = () => {
load,
unload,
addExtraSubtitlesTracks,
addLocalSubtitles,
setProp,
setSubtitlesTrack,
setExtraSubtitlesTrack,
};
};

View file

@ -488,17 +488,19 @@ const Settings = () => {
{...playInExternalPlayerSelect}
/>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
</div>
<Toggle
className={classnames(styles['option-input-container'], styles['toggle-container'])}
disabled={true}
tabIndex={-1}
{...hardwareDecodingToggle}
/>
</div>
{
shell.active &&
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
</div>
<Toggle
className={classnames(styles['option-input-container'], styles['toggle-container'])}
tabIndex={-1}
{...hardwareDecodingToggle}
/>
</div>
}
</div>
<div ref={streamingServerSectionRef} className={styles['section-container']}>
<div className={styles['section-title']}>{ t('SETTINGS_NAV_STREAMING') }</div>

View file

@ -36,6 +36,12 @@ function DragAndDrop({ core }) {
}
break;
}
case 'application/x-subrip':
break;
case 'text/vtt':
break;
case '':
break;
default: {
events.emit('error', {
message: 'Unsupported file',