mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-21 11:42:05 +00:00
Merge branch 'feat/right-click-context-menu' of https://github.com/Stremio/stremio-web into feat/right-click-context-menu
This commit is contained in:
commit
a9e218db62
66 changed files with 1348 additions and 429 deletions
BIN
images/library_placeholder.png
Normal file
BIN
images/library_placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
27
package-lock.json
generated
27
package-lock.json
generated
|
|
@ -1,20 +1,20 @@
|
||||||
{
|
{
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"version": "5.0.0-beta.17",
|
"version": "5.0.0-beta.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"version": "5.0.0-beta.17",
|
"version": "5.0.0-beta.20",
|
||||||
"license": "gpl-2.0",
|
"license": "gpl-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.26.0",
|
"@babel/runtime": "7.26.0",
|
||||||
"@sentry/browser": "8.42.0",
|
"@sentry/browser": "8.42.0",
|
||||||
"@stremio/stremio-colors": "5.2.0",
|
"@stremio/stremio-colors": "5.2.0",
|
||||||
"@stremio/stremio-core-web": "0.48.5",
|
"@stremio/stremio-core-web": "0.49.0",
|
||||||
"@stremio/stremio-icons": "5.4.1",
|
"@stremio/stremio-icons": "5.4.1",
|
||||||
"@stremio/stremio-video": "0.0.48",
|
"@stremio/stremio-video": "0.0.53",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
"react-i18next": "^15.1.3",
|
"react-i18next": "^15.1.3",
|
||||||
"react-is": "18.3.1",
|
"react-is": "18.3.1",
|
||||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
"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",
|
"url": "0.11.4",
|
||||||
"use-long-press": "^3.2.0"
|
"use-long-press": "^3.2.0"
|
||||||
},
|
},
|
||||||
|
|
@ -3371,9 +3371,9 @@
|
||||||
"integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg=="
|
"integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg=="
|
||||||
},
|
},
|
||||||
"node_modules/@stremio/stremio-core-web": {
|
"node_modules/@stremio/stremio-core-web": {
|
||||||
"version": "0.48.5",
|
"version": "0.49.0",
|
||||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.48.5.tgz",
|
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.49.0.tgz",
|
||||||
"integrity": "sha512-oDTNBrv8zZi1VGbeV+1Bm6CliI2rF23ERdJpz+gv8EnbFjRIo78WIsoS0yO0EOg8HHXYsFytPq5+c0+YlxmBlA==",
|
"integrity": "sha512-oxJRVAE6z6Eh1B0qomdz6L2CVaTkwt70kDNC1TmHyGNo+Hhp2RaMlygqBKvBLXyHUXi82R67Mc11gT/JqlmaMw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.24.1"
|
"@babel/runtime": "7.24.1"
|
||||||
|
|
@ -3409,10 +3409,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@stremio/stremio-video": {
|
"node_modules/@stremio/stremio-video": {
|
||||||
"version": "0.0.48",
|
"version": "0.0.53",
|
||||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.48.tgz",
|
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.53.tgz",
|
||||||
"integrity": "sha512-6ALGXCZC4NPsfhPcrwFWQzvH6UMMRsgSkHetnOhv9WmZ5ubiyUdbBzj9atGiGuuQz8pRcze66ztrub+dsaQbpw==",
|
"integrity": "sha512-hSlk8GqMdk4N8VbcdvduYqWVZsQLgHyU7GfFmd1k+t0pSpDKAhI3C6dohG5Sr09CKCjHa8D1rls+CwMNPXLSGw==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"color": "4.2.3",
|
"color": "4.2.3",
|
||||||
|
|
@ -13374,8 +13373,8 @@
|
||||||
},
|
},
|
||||||
"node_modules/stremio-translations": {
|
"node_modules/stremio-translations": {
|
||||||
"version": "1.44.9",
|
"version": "1.44.9",
|
||||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a0f50634202f748a57907b645d2cd92fbaa479dd",
|
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#62bcc6e8f44258203c7375af59210771efb6f634",
|
||||||
"integrity": "sha512-JJpd1JJet3T6/VTNdZ2NZ7uvHJ4zkuyqo5BnTcDGqLVNO/OpicGqKhZjE4WGSgmuhsfPBU8T0ICCfzKu2xpvKg==",
|
"integrity": "sha512-8Sc5Qvd4IiObwGXkmj1XFXFavUc15My5po6G48HHDBbp42SVc5I/t7h+1yxW1A81byyBCXbL23a9iU9v49vpQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "stremio",
|
"name": "stremio",
|
||||||
"displayName": "Stremio",
|
"displayName": "Stremio",
|
||||||
"version": "5.0.0-beta.17",
|
"version": "5.0.0-beta.20",
|
||||||
"author": "Smart Code OOD",
|
"author": "Smart Code OOD",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "gpl-2.0",
|
"license": "gpl-2.0",
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
"@babel/runtime": "7.26.0",
|
"@babel/runtime": "7.26.0",
|
||||||
"@sentry/browser": "8.42.0",
|
"@sentry/browser": "8.42.0",
|
||||||
"@stremio/stremio-colors": "5.2.0",
|
"@stremio/stremio-colors": "5.2.0",
|
||||||
"@stremio/stremio-core-web": "0.48.5",
|
"@stremio/stremio-core-web": "0.49.0",
|
||||||
"@stremio/stremio-icons": "5.4.1",
|
"@stremio/stremio-icons": "5.4.1",
|
||||||
"@stremio/stremio-video": "0.0.48",
|
"@stremio/stremio-video": "0.0.53",
|
||||||
"a-color-picker": "1.2.1",
|
"a-color-picker": "1.2.1",
|
||||||
"bowser": "2.11.0",
|
"bowser": "2.11.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
"react-i18next": "^15.1.3",
|
"react-i18next": "^15.1.3",
|
||||||
"react-is": "18.3.1",
|
"react-is": "18.3.1",
|
||||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
"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",
|
"url": "0.11.4",
|
||||||
"use-long-press": "^3.2.0"
|
"use-long-press": "^3.2.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ const { useTranslation } = require('react-i18next');
|
||||||
const { Router } = require('stremio-router');
|
const { Router } = require('stremio-router');
|
||||||
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
||||||
const { NotFound } = require('stremio/routes');
|
const { NotFound } = require('stremio/routes');
|
||||||
const { PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
|
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common');
|
||||||
const ServicesToaster = require('./ServicesToaster');
|
const ServicesToaster = require('./ServicesToaster');
|
||||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||||
const SearchParamsHandler = require('./SearchParamsHandler');
|
const SearchParamsHandler = require('./SearchParamsHandler');
|
||||||
|
const { default: UpdaterBanner } = require('./UpdaterBanner');
|
||||||
const ErrorDialog = require('./ErrorDialog');
|
const ErrorDialog = require('./ErrorDialog');
|
||||||
const withProtectedRoutes = require('./withProtectedRoutes');
|
const withProtectedRoutes = require('./withProtectedRoutes');
|
||||||
const routerViewsConfig = require('./routerViewsConfig');
|
const routerViewsConfig = require('./routerViewsConfig');
|
||||||
|
|
@ -19,6 +20,8 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router))
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
const shell = useShell();
|
||||||
|
const [windowHidden, setWindowHidden] = React.useState(false);
|
||||||
const onPathNotMatch = React.useCallback(() => {
|
const onPathNotMatch = React.useCallback(() => {
|
||||||
return NotFound;
|
return NotFound;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -96,6 +99,17 @@ const App = () => {
|
||||||
services.chromecast.off('stateChanged', onChromecastStateChange);
|
services.chromecast.off('stateChanged', onChromecastStateChange);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle shell window visibility changed event
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onWindowVisibilityChanged = (state) => {
|
||||||
|
setWindowHidden(state.visible === false && state.visibility === 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
shell.on('win-visibility-changed', onWindowVisibilityChanged);
|
||||||
|
return () => shell.off('win-visibility-changed', onWindowVisibilityChanged);
|
||||||
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onCoreEvent = ({ event, args }) => {
|
const onCoreEvent = ({ event, args }) => {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
|
@ -103,6 +117,11 @@ const App = () => {
|
||||||
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
|
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
|
||||||
i18n.changeLanguage(args.settings.interfaceLanguage);
|
i18n.changeLanguage(args.settings.interfaceLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args?.settings?.quitOnClose && windowHidden) {
|
||||||
|
shell.send('quit');
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +130,10 @@ const App = () => {
|
||||||
if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
|
if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
|
||||||
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
|
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state?.profile?.settings?.quitOnClose && windowHidden) {
|
||||||
|
shell.send('quit');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const onWindowFocus = () => {
|
const onWindowFocus = () => {
|
||||||
services.core.transport.dispatch({
|
services.core.transport.dispatch({
|
||||||
|
|
@ -145,7 +168,7 @@ const App = () => {
|
||||||
services.core.transport
|
services.core.transport
|
||||||
.getState('ctx')
|
.getState('ctx')
|
||||||
.then(onCtxState)
|
.then(onCtxState)
|
||||||
.catch((e) => console.error(e));
|
.catch(console.error);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (services.core.active) {
|
if (services.core.active) {
|
||||||
|
|
@ -153,7 +176,7 @@ const App = () => {
|
||||||
services.core.transport.off('CoreEvent', onCoreEvent);
|
services.core.transport.off('CoreEvent', onCoreEvent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [initialized]);
|
}, [initialized, windowHidden]);
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ServicesProvider services={services}>
|
<ServicesProvider services={services}>
|
||||||
|
|
@ -165,14 +188,17 @@ const App = () => {
|
||||||
<PlatformProvider>
|
<PlatformProvider>
|
||||||
<ToastProvider className={styles['toasts-container']}>
|
<ToastProvider className={styles['toasts-container']}>
|
||||||
<TooltipProvider className={styles['tooltip-container']}>
|
<TooltipProvider className={styles['tooltip-container']}>
|
||||||
<ServicesToaster />
|
<FileDropProvider className={styles['file-drop-container']}>
|
||||||
<DeepLinkHandler />
|
<ServicesToaster />
|
||||||
<SearchParamsHandler />
|
<DeepLinkHandler />
|
||||||
<RouterWithProtectedRoutes
|
<SearchParamsHandler />
|
||||||
className={styles['router']}
|
<UpdaterBanner className={styles['updater-banner-container']} />
|
||||||
viewsConfig={routerViewsConfig}
|
<RouterWithProtectedRoutes
|
||||||
onPathNotMatch={onPathNotMatch}
|
className={styles['router']}
|
||||||
/>
|
viewsConfig={routerViewsConfig}
|
||||||
|
onPathNotMatch={onPathNotMatch}
|
||||||
|
/>
|
||||||
|
</FileDropProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
|
|
|
||||||
46
src/App/UpdaterBanner/UpdaterBanner.less
Normal file
46
src/App/UpdaterBanner/UpdaterBanner.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/App/UpdaterBanner/UpdaterBanner.tsx
Normal file
50
src/App/UpdaterBanner/UpdaterBanner.tsx
Normal 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;
|
||||||
2
src/App/UpdaterBanner/index.ts
Normal file
2
src/App/UpdaterBanner/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
import UpdaterBanner from './UpdaterBanner';
|
||||||
|
export default UpdaterBanner;
|
||||||
|
|
@ -63,6 +63,8 @@
|
||||||
--overlay-color: rgba(255, 255, 255, 0.05);
|
--overlay-color: rgba(255, 255, 255, 0.05);
|
||||||
--modal-background-color: rgba(15, 13, 32, 1);
|
--modal-background-color: rgba(15, 13, 32, 1);
|
||||||
--outer-glow: 0px 0px 15px rgba(123, 91, 245, 0.37);
|
--outer-glow: 0px 0px 15px rgba(123, 91, 245, 0.37);
|
||||||
|
--warning-accent-color: rgba(255, 165, 0, 1);
|
||||||
|
--danger-accent-color: rgba(220, 38, 38, 1);
|
||||||
--border-radius: 0.75rem;
|
--border-radius: 0.75rem;
|
||||||
--top-overlay-size: @top-overlay-size;
|
--top-overlay-size: @top-overlay-size;
|
||||||
--bottom-overlay-size: @bottom-overlay-size;
|
--bottom-overlay-size: @bottom-overlay-size;
|
||||||
|
|
@ -202,12 +204,32 @@ html {
|
||||||
background-color: var(--modal-background-color);
|
background-color: var(--modal-background-color);
|
||||||
box-shadow: var(--outer-glow);
|
box-shadow: var(--outer-glow);
|
||||||
transition: opacity 0.1s ease-out;
|
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) {
|
&: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 {
|
.router {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,16 @@ const ICON_FOR_TYPE = new Map([
|
||||||
['other', 'movies'],
|
['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 = [
|
const EXTERNAL_PLAYERS = [
|
||||||
{
|
{
|
||||||
label: 'EXTERNAL_PLAYER_DISABLED',
|
label: 'EXTERNAL_PLAYER_DISABLED',
|
||||||
|
|
@ -113,6 +123,8 @@ module.exports = {
|
||||||
WRITERS_LINK_CATEGORY,
|
WRITERS_LINK_CATEGORY,
|
||||||
TYPE_PRIORITIES,
|
TYPE_PRIORITIES,
|
||||||
ICON_FOR_TYPE,
|
ICON_FOR_TYPE,
|
||||||
|
MIME_SIGNATURES,
|
||||||
|
SUPPORTED_LOCAL_SUBTITLES,
|
||||||
EXTERNAL_PLAYERS,
|
EXTERNAL_PLAYERS,
|
||||||
WHITELISTED_HOSTS,
|
WHITELISTED_HOSTS,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
91
src/common/FileDrop/FileDrop.tsx
Normal file
91
src/common/FileDrop/FileDrop.tsx
Normal 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,
|
||||||
|
};
|
||||||
8
src/common/FileDrop/index.ts
Normal file
8
src/common/FileDrop/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { FileDropProvider, useFileDrop } from './FileDrop';
|
||||||
|
import onFileDrop from './onFileDrop';
|
||||||
|
|
||||||
|
export {
|
||||||
|
FileDropProvider,
|
||||||
|
useFileDrop,
|
||||||
|
onFileDrop,
|
||||||
|
};
|
||||||
14
src/common/FileDrop/onFileDrop.ts
Normal file
14
src/common/FileDrop/onFileDrop.ts
Normal 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;
|
||||||
19
src/common/FileDrop/utils.ts
Normal file
19
src/common/FileDrop/utils.ts
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
|
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
|
||||||
import useShell from './useShell';
|
import useShell from 'stremio/common/useShell';
|
||||||
import { name, isMobile } from './device';
|
import { name, isMobile } from './device';
|
||||||
|
|
||||||
interface PlatformContext {
|
interface PlatformContext {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
const createId = () => Math.floor(Math.random() * 9999) + 1;
|
|
||||||
|
|
||||||
const useShell = () => {
|
|
||||||
const transport = globalThis?.qt?.webChannelTransport;
|
|
||||||
|
|
||||||
const send = (method: string, ...args: (string | number)[]) => {
|
|
||||||
transport?.send(JSON.stringify({
|
|
||||||
id: createId(),
|
|
||||||
type: 6,
|
|
||||||
object: 'transport',
|
|
||||||
method: 'onEvent',
|
|
||||||
args: [method, ...args],
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
active: !!transport,
|
|
||||||
send,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useShell;
|
|
||||||
|
|
@ -69,6 +69,19 @@
|
||||||
transform: translateX(100%);
|
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 {
|
@keyframes fade-in-no-motion {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
// Copyright (C) 2017-2023 Smart code 203358507
|
||||||
|
|
||||||
|
const { FileDropProvider, onFileDrop } = require('./FileDrop');
|
||||||
const { PlatformProvider, usePlatform } = require('./Platform');
|
const { PlatformProvider, usePlatform } = require('./Platform');
|
||||||
const { ToastProvider, useToast } = require('./Toast');
|
const { ToastProvider, useToast } = require('./Toast');
|
||||||
const { TooltipProvider, Tooltip } = require('./Tooltips');
|
const { TooltipProvider, Tooltip } = require('./Tooltips');
|
||||||
|
|
@ -19,11 +20,15 @@ const useModelState = require('./useModelState');
|
||||||
const useNotifications = require('./useNotifications');
|
const useNotifications = require('./useNotifications');
|
||||||
const useOnScrollToBottom = require('./useOnScrollToBottom');
|
const useOnScrollToBottom = require('./useOnScrollToBottom');
|
||||||
const useProfile = require('./useProfile');
|
const useProfile = require('./useProfile');
|
||||||
|
const { default: useShell } = require('./useShell');
|
||||||
const useStreamingServer = require('./useStreamingServer');
|
const useStreamingServer = require('./useStreamingServer');
|
||||||
const useTorrent = require('./useTorrent');
|
const useTorrent = require('./useTorrent');
|
||||||
const useTranslate = require('./useTranslate');
|
const useTranslate = require('./useTranslate');
|
||||||
|
const { default: useOrientation } = require('./useOrientation');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
FileDropProvider,
|
||||||
|
onFileDrop,
|
||||||
PlatformProvider,
|
PlatformProvider,
|
||||||
usePlatform,
|
usePlatform,
|
||||||
ToastProvider,
|
ToastProvider,
|
||||||
|
|
@ -47,7 +52,9 @@ module.exports = {
|
||||||
useNotifications,
|
useNotifications,
|
||||||
useOnScrollToBottom,
|
useOnScrollToBottom,
|
||||||
useProfile,
|
useProfile,
|
||||||
|
useShell,
|
||||||
useStreamingServer,
|
useStreamingServer,
|
||||||
useTorrent,
|
useTorrent,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
|
useOrientation,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
34
src/common/useOrientation.ts
Normal file
34
src/common/useOrientation.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright (C) 2017-2025 Smart code 203358507
|
||||||
|
|
||||||
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
type DeviceOrientation = 'landscape' | 'portrait';
|
||||||
|
|
||||||
|
const useOrientation = () => {
|
||||||
|
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
|
||||||
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||||
|
|
||||||
|
const orientation: DeviceOrientation = useMemo(() => {
|
||||||
|
if (windowHeight > windowWidth) {
|
||||||
|
return 'portrait';
|
||||||
|
} else {
|
||||||
|
return 'landscape';
|
||||||
|
}
|
||||||
|
}, [windowWidth, windowHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setWindowHeight(window.innerHeight);
|
||||||
|
setWindowWidth(window.innerWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, [window.innerWidth, window.innerHeight]);
|
||||||
|
|
||||||
|
return orientation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOrientation;
|
||||||
72
src/common/useShell.ts
Normal file
72
src/common/useShell.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import EventEmitter from 'eventemitter3';
|
||||||
|
|
||||||
|
const SHELL_EVENT_OBJECT = 'transport';
|
||||||
|
const transport = globalThis?.chrome?.webview;
|
||||||
|
const events = new EventEmitter();
|
||||||
|
|
||||||
|
enum ShellEventType {
|
||||||
|
SIGNAL = 1,
|
||||||
|
INVOKE_METHOD = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShellEvent = {
|
||||||
|
id: number;
|
||||||
|
type: ShellEventType;
|
||||||
|
object: string;
|
||||||
|
args: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const createId = () => Math.floor(Math.random() * 9999) + 1;
|
||||||
|
|
||||||
|
const useShell = () => {
|
||||||
|
const on = (name: string, listener: (arg: any) => void) => {
|
||||||
|
events.on(name, listener);
|
||||||
|
};
|
||||||
|
|
||||||
|
const off = (name: string, listener: (arg: any) => void) => {
|
||||||
|
events.off(name, listener);
|
||||||
|
};
|
||||||
|
|
||||||
|
const send = (method: string, ...args: (string | number)[]) => {
|
||||||
|
try {
|
||||||
|
transport?.postMessage(JSON.stringify({
|
||||||
|
id: createId(),
|
||||||
|
type: ShellEventType.INVOKE_METHOD,
|
||||||
|
object: SHELL_EVENT_OBJECT,
|
||||||
|
method: 'onEvent',
|
||||||
|
args: [method, ...args],
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Shell', 'Failed to send event', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!transport) return;
|
||||||
|
|
||||||
|
const onMessage = ({ data }: { data: string }) => {
|
||||||
|
try {
|
||||||
|
const { type, args } = JSON.parse(data) as ShellEvent;
|
||||||
|
if (type === ShellEventType.SIGNAL) {
|
||||||
|
const [methodName, methodArg] = args;
|
||||||
|
events.emit(methodName, methodArg);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Shell', 'Failed to handle event', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
transport.addEventListener('message', onMessage);
|
||||||
|
return () => transport.removeEventListener('message', onMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
active: !!transport,
|
||||||
|
send,
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useShell;
|
||||||
|
|
@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import useBinaryState from 'stremio/common/useBinaryState';
|
import useBinaryState from 'stremio/common/useBinaryState';
|
||||||
|
import useOrientation from 'stremio/common/useOrientation';
|
||||||
import styles from './BottomSheet.less';
|
import styles from './BottomSheet.less';
|
||||||
|
|
||||||
const CLOSE_THRESHOLD = 100;
|
const CLOSE_THRESHOLD = 100;
|
||||||
|
|
@ -17,6 +18,7 @@ type Props = {
|
||||||
|
|
||||||
const BottomSheet = ({ children, title, show, onClose }: Props) => {
|
const BottomSheet = ({ children, title, show, onClose }: Props) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const orientation = useOrientation();
|
||||||
const [startOffset, setStartOffset] = useState(0);
|
const [startOffset, setStartOffset] = useState(0);
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
|
|
||||||
|
|
@ -58,6 +60,10 @@ const BottomSheet = ({ children, title, show, onClose }: Props) => {
|
||||||
!opened && onClose();
|
!opened && onClose();
|
||||||
}, [opened]);
|
}, [opened]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
opened && close();
|
||||||
|
}, [orientation]);
|
||||||
|
|
||||||
return opened && createPortal((
|
return opened && createPortal((
|
||||||
<div className={styles['bottom-sheet']}>
|
<div className={styles['bottom-sheet']}>
|
||||||
<div className={styles['backdrop']} onClick={onCloseRequest} />
|
<div className={styles['backdrop']} onClick={onCloseRequest} />
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import styles from './Button.less';
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string,
|
className?: string,
|
||||||
href?: string,
|
href?: string,
|
||||||
|
target?: string
|
||||||
title?: string,
|
title?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
tabIndex?: number,
|
tabIndex?: number,
|
||||||
|
|
|
||||||
83
src/components/Checkbox/Checkbox.less
Normal file
83
src/components/Checkbox/Checkbox.less
Normal 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: background-color 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/components/Checkbox/Checkbox.tsx
Normal file
97
src/components/Checkbox/Checkbox.tsx
Normal 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;
|
||||||
5
src/components/Checkbox/index.ts
Normal file
5
src/components/Checkbox/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (C) 2017-2025 Smart code 203358507
|
||||||
|
|
||||||
|
import Checkbox from './Checkbox';
|
||||||
|
|
||||||
|
export default Checkbox;
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useTranslation } = require('react-i18next');
|
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 useEvents = require('./useEvents');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,7 @@
|
||||||
|
|
||||||
.nav-content-container {
|
.nav-content-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding-top: calc(var(--horizontal-nav-bar-size) + var(--safe-area-inset-top));
|
top: calc(var(--horizontal-nav-bar-size) + var(--safe-area-inset-top));
|
||||||
top: 0;
|
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: var(--vertical-nav-bar-size);
|
left: var(--vertical-nav-bar-size);
|
||||||
|
|
@ -43,7 +42,7 @@
|
||||||
.main-nav-bars-container {
|
.main-nav-bars-container {
|
||||||
.nav-content-container {
|
.nav-content-container {
|
||||||
left: 0;
|
left: 0;
|
||||||
padding-bottom: var(--vertical-nav-bar-size);
|
bottom: var(--vertical-nav-bar-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.vertical-nav-bar {
|
.vertical-nav-bar {
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,23 @@ const useProfile = require('stremio/common/useProfile');
|
||||||
const usePWA = require('stremio/common/usePWA');
|
const usePWA = require('stremio/common/usePWA');
|
||||||
const useTorrent = require('stremio/common/useTorrent');
|
const useTorrent = require('stremio/common/useTorrent');
|
||||||
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
|
||||||
|
const useStreamingServer = require('stremio/common/useStreamingServer');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const NavMenuContent = ({ onClick }) => {
|
const NavMenuContent = ({ onClick }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { core } = useServices();
|
const { core } = useServices();
|
||||||
const profile = useProfile();
|
const profile = useProfile();
|
||||||
|
const streamingServer = useStreamingServer();
|
||||||
const { createTorrentFromMagnet } = useTorrent();
|
const { createTorrentFromMagnet } = useTorrent();
|
||||||
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen();
|
||||||
const [isIOSPWA, isAndroidPWA] = usePWA();
|
const [isIOSPWA, isAndroidPWA] = usePWA();
|
||||||
|
const streamingServerWarningDismissed = React.useMemo(() => {
|
||||||
|
return streamingServer.settings !== null && streamingServer.settings.type === 'Ready' || (
|
||||||
|
!isNaN(profile.settings.streamingServerWarningDismissed.getTime()) &&
|
||||||
|
profile.settings.streamingServerWarningDismissed.getTime() > Date.now()
|
||||||
|
);
|
||||||
|
}, [profile.settings, streamingServer.settings]);
|
||||||
const logoutButtonOnClick = React.useCallback(() => {
|
const logoutButtonOnClick = React.useCallback(() => {
|
||||||
core.transport.dispatch({
|
core.transport.dispatch({
|
||||||
action: 'Ctx',
|
action: 'Ctx',
|
||||||
|
|
@ -38,7 +46,7 @@ const NavMenuContent = ({ onClick }) => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className={classnames(styles['nav-menu-container'], 'animation-fade-in')} onClick={onClick}>
|
<div className={classnames(styles['nav-menu-container'], 'animation-fade-in', { [styles['with-warning']]: !streamingServerWarningDismissed } )} onClick={onClick}>
|
||||||
<div className={styles['user-info-container']}>
|
<div className={styles['user-info-container']}>
|
||||||
<div
|
<div
|
||||||
className={styles['avatar-container']}
|
className={styles['avatar-container']}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
popup-menu-container: menu-container;
|
popup-menu-container: menu-container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mobile-height: calc(var(--small-viewport-height) - var(--horizontal-nav-bar-size) - var(--vertical-nav-bar-size));
|
||||||
|
@height: calc(var(--small-viewport-height) - var(--horizontal-nav-bar-size));
|
||||||
|
|
||||||
.nav-menu-popup-label {
|
.nav-menu-popup-label {
|
||||||
.popup-menu-container {
|
.popup-menu-container {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|
@ -14,11 +17,15 @@
|
||||||
}
|
}
|
||||||
.nav-menu-container {
|
.nav-menu-container {
|
||||||
width: 22rem;
|
width: 22rem;
|
||||||
max-height: calc(100vh - var(--horizontal-nav-bar-size) - 1rem);
|
max-height: calc(@height - 1rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background-color: var(--modal-background-color);
|
background-color: var(--modal-background-color);
|
||||||
|
|
||||||
|
&.with-warning {
|
||||||
|
max-height: calc(@height - 6rem);
|
||||||
|
}
|
||||||
|
|
||||||
.user-info-container {
|
.user-info-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1.5rem 1rem;
|
padding: 1.5rem 1rem;
|
||||||
|
|
@ -108,6 +115,10 @@
|
||||||
|
|
||||||
@media only screen and (max-width: @minimum) {
|
@media only screen and (max-width: @minimum) {
|
||||||
.nav-menu-container {
|
.nav-menu-container {
|
||||||
max-height: calc(100vh - var(--horizontal-nav-bar-size) - var(--vertical-nav-bar-size) - 1rem);
|
max-height: calc(@mobile-height - 1rem);
|
||||||
|
|
||||||
|
&.with-warning {
|
||||||
|
max-height: calc(@mobile-height - 8.5rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
.nav-tab-button {
|
.nav-tab-button {
|
||||||
width: calc(var(--vertical-nav-bar-size) - 1.2rem);
|
width: calc(var(--vertical-nav-bar-size) - 1.2rem);
|
||||||
height: calc(var(--vertical-nav-bar-size) - 1.2rem);
|
height: calc(var(--vertical-nav-bar-size) - 1.2rem);
|
||||||
|
min-height: 3.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const useAnimationFrame = require('stremio/common/useAnimationFrame');
|
||||||
const useLiveRef = require('stremio/common/useLiveRef');
|
const useLiveRef = require('stremio/common/useLiveRef');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabled, onSlide, onComplete }) => {
|
const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabled, onSlide, onComplete, audioBoost }) => {
|
||||||
const minimumValueRef = useLiveRef(minimumValue !== null && !isNaN(minimumValue) ? minimumValue : 0);
|
const minimumValueRef = useLiveRef(minimumValue !== null && !isNaN(minimumValue) ? minimumValue : 0);
|
||||||
const maximumValueRef = useLiveRef(maximumValue !== null && !isNaN(maximumValue) ? maximumValue : 100);
|
const maximumValueRef = useLiveRef(maximumValue !== null && !isNaN(maximumValue) ? maximumValue : 100);
|
||||||
const valueRef = useLiveRef(value !== null && !isNaN(value) ? Math.min(maximumValueRef.current, Math.max(minimumValueRef.current, value)) : 0);
|
const valueRef = useLiveRef(value !== null && !isNaN(value) ? Math.min(maximumValueRef.current, Math.max(minimumValueRef.current, value)) : 0);
|
||||||
|
|
@ -100,13 +100,16 @@ const Slider = ({ className, value, buffered, minimumValue, maximumValue, disabl
|
||||||
return (
|
return (
|
||||||
<div ref={sliderContainerRef} className={classnames(className, styles['slider-container'], { 'disabled': disabled })} onMouseDown={onMouseDown}>
|
<div ref={sliderContainerRef} className={classnames(className, styles['slider-container'], { 'disabled': disabled })} onMouseDown={onMouseDown}>
|
||||||
<div className={styles['layer']}>
|
<div className={styles['layer']}>
|
||||||
<div className={styles['track']} />
|
<div className={classnames(styles['track'], { [styles['audio-boost']]: audioBoost })} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['layer']}>
|
<div className={styles['layer']}>
|
||||||
<div className={styles['track-before']} style={{ width: `calc(100% * ${bufferedPosition})` }} />
|
<div className={styles['track-before']} style={{ width: `calc(100% * ${bufferedPosition})` }} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['layer']}>
|
<div className={styles['layer']}>
|
||||||
<div className={styles['track-after']} style={{ width: `calc(100% * ${thumbPosition})` }} />
|
<div
|
||||||
|
className={classnames(styles['track-after'], { [styles['audio-boost']]: audioBoost })}
|
||||||
|
style={{ '--mask-width': `calc(${thumbPosition} * 100%)` }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['layer']}>
|
<div className={styles['layer']}>
|
||||||
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
|
<div className={styles['thumb']} style={{ marginLeft: `calc(100% * ${thumbPosition})` }} />
|
||||||
|
|
@ -123,7 +126,8 @@ Slider.propTypes = {
|
||||||
maximumValue: PropTypes.number,
|
maximumValue: PropTypes.number,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
onSlide: PropTypes.func,
|
onSlide: PropTypes.func,
|
||||||
onComplete: PropTypes.func
|
onComplete: PropTypes.func,
|
||||||
|
audioBoost: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Slider;
|
module.exports = Slider;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||||
|
|
||||||
|
@audio-boost-background: linear-gradient(to right,
|
||||||
|
var(--primary-foreground-color) 0%,
|
||||||
|
var(--primary-foreground-color) 50%,
|
||||||
|
var(--warning-accent-color) 75%,
|
||||||
|
var(--danger-accent-color) 100%) !important;
|
||||||
|
|
||||||
html.active-slider-within {
|
html.active-slider-within {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
|
||||||
|
|
@ -37,9 +43,15 @@ html.active-slider-within {
|
||||||
.track {
|
.track {
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
height: var(--track-size);
|
height: var(--track-size);
|
||||||
border-radius: var(--track-size);
|
border-radius: var(--track-size);
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--overlay-color);
|
||||||
|
|
||||||
|
&.audio-boost {
|
||||||
|
opacity: 0.3;
|
||||||
|
background: @audio-boost-background;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-before {
|
.track-before {
|
||||||
|
|
@ -53,9 +65,19 @@ html.active-slider-within {
|
||||||
.track-after {
|
.track-after {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
height: var(--track-size);
|
height: var(--track-size);
|
||||||
border-radius: var(--track-size);
|
border-radius: var(--track-size);
|
||||||
background-color: var(--primary-foreground-color);
|
background-color: var(--primary-foreground-color);
|
||||||
|
mask-image: linear-gradient(to right,
|
||||||
|
black 0%,
|
||||||
|
black var(--mask-width),
|
||||||
|
transparent var(--mask-width)
|
||||||
|
);
|
||||||
|
|
||||||
|
&.audio-boost {
|
||||||
|
background: @audio-boost-background;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const useBinaryState = require('stremio/common/useBinaryState');
|
||||||
const VideoPlaceholder = require('./VideoPlaceholder');
|
const VideoPlaceholder = require('./VideoPlaceholder');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const Video = ({ className, id, title, thumbnail, episode, released, upcoming, watched, progress, scheduled, deepLinks, onMarkVideoAsWatched, ...props }) => {
|
const Video = ({ className, id, title, thumbnail, season, episode, released, upcoming, watched, progress, scheduled, seasonWatched, deepLinks, onMarkVideoAsWatched, onMarkSeasonAsWatched, ...props }) => {
|
||||||
const routeFocused = useRouteFocused();
|
const routeFocused = useRouteFocused();
|
||||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||||
const popupLabelOnMouseUp = React.useCallback((event) => {
|
const popupLabelOnMouseUp = React.useCallback((event) => {
|
||||||
|
|
@ -50,6 +50,12 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
||||||
closeMenu();
|
closeMenu();
|
||||||
onMarkVideoAsWatched({ id, released }, watched);
|
onMarkVideoAsWatched({ id, released }, watched);
|
||||||
}, [id, released, watched]);
|
}, [id, released, watched]);
|
||||||
|
const toggleWatchedSeasonOnClick = React.useCallback((event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
closeMenu();
|
||||||
|
onMarkSeasonAsWatched(season, seasonWatched);
|
||||||
|
}, [season, seasonWatched, onMarkSeasonAsWatched]);
|
||||||
const videoButtonOnClick = React.useCallback(() => {
|
const videoButtonOnClick = React.useCallback(() => {
|
||||||
if (deepLinks) {
|
if (deepLinks) {
|
||||||
if (typeof deepLinks.player === 'string') {
|
if (typeof deepLinks.player === 'string') {
|
||||||
|
|
@ -142,9 +148,12 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
||||||
<Button className={styles['context-menu-option-container']} title={watched ? 'Mark as non-watched' : 'Mark as watched'} onClick={toggleWatchedOnClick}>
|
<Button className={styles['context-menu-option-container']} title={watched ? 'Mark as non-watched' : 'Mark as watched'} onClick={toggleWatchedOnClick}>
|
||||||
<div className={styles['context-menu-option-label']}>{watched ? t('CTX_MARK_NON_WATCHED') : t('CTX_MARK_WATCHED')}</div>
|
<div className={styles['context-menu-option-label']}>{watched ? t('CTX_MARK_NON_WATCHED') : t('CTX_MARK_WATCHED')}</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button className={styles['context-menu-option-container']} title={seasonWatched ? t('CTX_UNMARK_REST') : t('CTX_MARK_REST')} onClick={toggleWatchedSeasonOnClick}>
|
||||||
|
<div className={styles['context-menu-option-label']}>{seasonWatched ? t('CTX_UNMARK_REST') : t('CTX_MARK_REST')}</div>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [watched, toggleWatchedOnClick]);
|
}, [watched, seasonWatched, toggleWatchedOnClick]);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!routeFocused) {
|
if (!routeFocused) {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|
@ -182,17 +191,20 @@ Video.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
thumbnail: PropTypes.string,
|
thumbnail: PropTypes.string,
|
||||||
|
season: PropTypes.number,
|
||||||
episode: PropTypes.number,
|
episode: PropTypes.number,
|
||||||
released: PropTypes.instanceOf(Date),
|
released: PropTypes.instanceOf(Date),
|
||||||
upcoming: PropTypes.bool,
|
upcoming: PropTypes.bool,
|
||||||
watched: PropTypes.bool,
|
watched: PropTypes.bool,
|
||||||
progress: PropTypes.number,
|
progress: PropTypes.number,
|
||||||
scheduled: PropTypes.bool,
|
scheduled: PropTypes.bool,
|
||||||
|
seasonWatched: PropTypes.bool,
|
||||||
deepLinks: PropTypes.shape({
|
deepLinks: PropTypes.shape({
|
||||||
metaDetailsStreams: PropTypes.string,
|
metaDetailsStreams: PropTypes.string,
|
||||||
player: PropTypes.string
|
player: PropTypes.string
|
||||||
}),
|
}),
|
||||||
onMarkVideoAsWatched: PropTypes.func,
|
onMarkVideoAsWatched: PropTypes.func,
|
||||||
|
onMarkSeasonAsWatched: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Video;
|
module.exports = Video;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import AddonDetailsModal from './AddonDetailsModal';
|
import AddonDetailsModal from './AddonDetailsModal';
|
||||||
import BottomSheet from './BottomSheet';
|
import BottomSheet from './BottomSheet';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
|
import Checkbox from './Checkbox';
|
||||||
import Chips from './Chips';
|
import Chips from './Chips';
|
||||||
import ColorInput from './ColorInput';
|
import ColorInput from './ColorInput';
|
||||||
import ContextMenu from './ContextMenu';
|
import ContextMenu from './ContextMenu';
|
||||||
|
|
@ -32,6 +33,7 @@ export {
|
||||||
AddonDetailsModal,
|
AddonDetailsModal,
|
||||||
BottomSheet,
|
BottomSheet,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
Chips,
|
Chips,
|
||||||
ColorInput,
|
ColorInput,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const debounce = require('lodash.debounce');
|
const debounce = require('lodash.debounce');
|
||||||
const { useTranslation } = require('react-i18next');
|
const useTranslate = require('stremio/common/useTranslate');
|
||||||
const { useStreamingServer, useNotifications, withCoreSuspender, getVisibleChildrenRange, useProfile } = require('stremio/common');
|
const { useStreamingServer, useNotifications, withCoreSuspender, getVisibleChildrenRange, useProfile } = require('stremio/common');
|
||||||
const { ContinueWatchingItem, EventModal, MainNavBars, MetaItem, MetaRow } = require('stremio/components');
|
const { ContinueWatchingItem, EventModal, MainNavBars, MetaItem, MetaRow } = require('stremio/components');
|
||||||
const useBoard = require('./useBoard');
|
const useBoard = require('./useBoard');
|
||||||
|
|
@ -14,7 +14,7 @@ const { default: StreamingServerWarning } = require('./StreamingServerWarning');
|
||||||
const THRESHOLD = 5;
|
const THRESHOLD = 5;
|
||||||
|
|
||||||
const Board = () => {
|
const Board = () => {
|
||||||
const { t } = useTranslation();
|
const t = useTranslate();
|
||||||
const streamingServer = useStreamingServer();
|
const streamingServer = useStreamingServer();
|
||||||
const continueWatchingPreview = useContinueWatchingPreview();
|
const continueWatchingPreview = useContinueWatchingPreview();
|
||||||
const [board, loadBoardRows] = useBoard();
|
const [board, loadBoardRows] = useBoard();
|
||||||
|
|
@ -55,7 +55,7 @@ const Board = () => {
|
||||||
continueWatchingPreview.items.length > 0 ?
|
continueWatchingPreview.items.length > 0 ?
|
||||||
<MetaRow
|
<MetaRow
|
||||||
className={classnames(styles['board-row'], styles['continue-watching-row'], 'animation-fade-in')}
|
className={classnames(styles['board-row'], styles['continue-watching-row'], 'animation-fade-in')}
|
||||||
title={t('BOARD_CONTINUE_WATCHING')}
|
title={t.string('BOARD_CONTINUE_WATCHING')}
|
||||||
catalog={continueWatchingPreview}
|
catalog={continueWatchingPreview}
|
||||||
itemComponent={ContinueWatchingItem}
|
itemComponent={ContinueWatchingItem}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
|
|
@ -94,6 +94,7 @@ const Board = () => {
|
||||||
key={index}
|
key={index}
|
||||||
className={classnames(styles['board-row'], styles['board-row-poster'], 'animation-fade-in')}
|
className={classnames(styles['board-row'], styles['board-row-poster'], 'animation-fade-in')}
|
||||||
catalog={catalog}
|
catalog={catalog}
|
||||||
|
title={t.catalogTitle(catalog)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 0 calc(1.5rem + var(--safe-area-inset-bottom)) 2rem;
|
padding: 0 0 1.5rem 1.5rem;
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
|
@ -31,12 +31,4 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: @small) and (orientation: landscape) {
|
|
||||||
.calendar {
|
|
||||||
.content {
|
|
||||||
padding: 0 0 calc(1.5rem + var(--safe-area-inset-bottom)) 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -95,4 +95,4 @@
|
||||||
&:not(.active):hover {
|
&:not(.active):hover {
|
||||||
border-color: var(--overlay-color);
|
border-color: var(--overlay-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
overflow-y: auto;
|
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) {
|
@media only screen and (max-width: @small) and (orientation: portrait) {
|
||||||
|
|
@ -34,4 +38,4 @@
|
||||||
.list {
|
.list {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,12 +8,11 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex: none;
|
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -22,19 +21,22 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image-container {
|
||||||
flex: none;
|
padding: 1.5rem 0;
|
||||||
height: 14rem;
|
|
||||||
margin: 1.5rem 0;
|
.image {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 14rem;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
flex: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
.point {
|
.point {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -61,21 +63,47 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button-container {
|
||||||
flex: none;
|
margin: 1rem 0;
|
||||||
justify-content: center;
|
|
||||||
height: 4rem;
|
.button {
|
||||||
line-height: 4rem;
|
display: flex;
|
||||||
padding: 0 5rem;
|
justify-content: center;
|
||||||
font-size: 1.1rem;
|
height: 4rem;
|
||||||
color: var(--primary-foreground-color);
|
line-height: 4rem;
|
||||||
text-align: center;
|
padding: 0 5rem;
|
||||||
border-radius: 3.5rem;
|
font-size: 1.1rem;
|
||||||
background-color: var(--overlay-color);
|
color: var(--primary-foreground-color);
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3.5rem;
|
||||||
|
background-color: var(--overlay-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: @xsmall) {
|
||||||
|
.placeholder {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
max-height: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin: 1rem 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,16 +112,21 @@
|
||||||
.placeholder {
|
.placeholder {
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
.image {
|
|
||||||
height: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.point {
|
||||||
|
.text {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button-container {
|
||||||
width: 100%;
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,11 +14,13 @@ const Placeholder = () => {
|
||||||
<div className={styles['title']}>
|
<div className={styles['title']}>
|
||||||
{t('CALENDAR_NOT_LOGGED_IN')}
|
{t('CALENDAR_NOT_LOGGED_IN')}
|
||||||
</div>
|
</div>
|
||||||
<Image
|
<div className={styles['image-container']}>
|
||||||
className={styles['image']}
|
<Image
|
||||||
src={require('/images/calendar_placeholder.png')}
|
className={styles['image']}
|
||||||
alt={' '}
|
src={require('/images/calendar_placeholder.png')}
|
||||||
/>
|
alt={' '}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className={styles['overview']}>
|
<div className={styles['overview']}>
|
||||||
<div className={styles['point']}>
|
<div className={styles['point']}>
|
||||||
<Icon className={styles['icon']} name={'megaphone'} />
|
<Icon className={styles['icon']} name={'megaphone'} />
|
||||||
|
|
@ -33,9 +35,11 @@ const Placeholder = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className={styles['button']} href={'#/intro?form=login'}>
|
<div className={styles['button-container']}>
|
||||||
{t('LOG_IN')}
|
<Button className={styles['button']} href={'#/intro?form=login'}>
|
||||||
</Button>
|
{t('LOG_IN')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// Copyright (C) 2017-2023 Smart code 203358507
|
|
||||||
|
|
||||||
const ConsentToggle = require('./ConsentToggle');
|
|
||||||
|
|
||||||
module.exports = ConsentToggle;
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,9 +8,8 @@ const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
const { Modal, useRouteFocused } = require('stremio-router');
|
const { Modal, useRouteFocused } = require('stremio-router');
|
||||||
const { useServices } = require('stremio/services');
|
const { useServices } = require('stremio/services');
|
||||||
const { useBinaryState } = require('stremio/common');
|
const { useBinaryState } = require('stremio/common');
|
||||||
const { Button, Image } = require('stremio/components');
|
const { Button, Image, Checkbox } = require('stremio/components');
|
||||||
const CredentialsTextInput = require('./CredentialsTextInput');
|
const CredentialsTextInput = require('./CredentialsTextInput');
|
||||||
const ConsentToggle = require('./ConsentToggle');
|
|
||||||
const PasswordResetModal = require('./PasswordResetModal');
|
const PasswordResetModal = require('./PasswordResetModal');
|
||||||
const useFacebookLogin = require('./useFacebookLogin');
|
const useFacebookLogin = require('./useFacebookLogin');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
@ -308,30 +307,27 @@ const Intro = ({ queryParams }) => {
|
||||||
onChange={confirmPasswordOnChange}
|
onChange={confirmPasswordOnChange}
|
||||||
onSubmit={confirmPasswordOnSubmit}
|
onSubmit={confirmPasswordOnSubmit}
|
||||||
/>
|
/>
|
||||||
<ConsentToggle
|
<Checkbox
|
||||||
ref={termsRef}
|
ref={termsRef}
|
||||||
className={styles['consent-toggle']}
|
|
||||||
label={'I have read and agree with the Stremio'}
|
label={'I have read and agree with the Stremio'}
|
||||||
link={'Terms and conditions'}
|
link={'Terms and conditions'}
|
||||||
href={'https://www.stremio.com/tos'}
|
href={'https://www.stremio.com/tos'}
|
||||||
checked={state.termsAccepted}
|
checked={state.termsAccepted}
|
||||||
onToggle={toggleTermsAccepted}
|
onChange={toggleTermsAccepted}
|
||||||
/>
|
/>
|
||||||
<ConsentToggle
|
<Checkbox
|
||||||
ref={privacyPolicyRef}
|
ref={privacyPolicyRef}
|
||||||
className={styles['consent-toggle']}
|
|
||||||
label={'I have read and agree with the Stremio'}
|
label={'I have read and agree with the Stremio'}
|
||||||
link={'Privacy Policy'}
|
link={'Privacy Policy'}
|
||||||
href={'https://www.stremio.com/privacy'}
|
href={'https://www.stremio.com/privacy'}
|
||||||
checked={state.privacyPolicyAccepted}
|
checked={state.privacyPolicyAccepted}
|
||||||
onToggle={togglePrivacyPolicyAccepted}
|
onChange={togglePrivacyPolicyAccepted}
|
||||||
/>
|
/>
|
||||||
<ConsentToggle
|
<Checkbox
|
||||||
ref={marketingRef}
|
ref={marketingRef}
|
||||||
className={styles['consent-toggle']}
|
|
||||||
label={'I agree to receive marketing communications from Stremio'}
|
label={'I agree to receive marketing communications from Stremio'}
|
||||||
checked={state.marketingAccepted}
|
checked={state.marketingAccepted}
|
||||||
onToggle={toggleMarketingAccepted}
|
onChange={toggleMarketingAccepted}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
:
|
:
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: @minimum) {
|
@media only screen and (max-width: @xsmall) {
|
||||||
.intro-container {
|
.intro-container {
|
||||||
justify-content: initial;
|
justify-content: initial;
|
||||||
padding: 3rem 1.5rem;
|
padding: 3rem 1.5rem;
|
||||||
|
|
@ -279,6 +279,21 @@
|
||||||
.content-container {
|
.content-container {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
|
||||||
|
.form-container, .options-container {
|
||||||
|
width: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-container {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: @minimum) {
|
||||||
|
.intro-container {
|
||||||
|
.content-container {
|
||||||
.form-container, .options-container {
|
.form-container, .options-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const NotFound = require('stremio/routes/NotFound');
|
const NotFound = require('stremio/routes/NotFound');
|
||||||
const { useProfile, useNotifications, routesRegexp, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
|
const { useProfile, useNotifications, routesRegexp, useOnScrollToBottom, withCoreSuspender } = require('stremio/common');
|
||||||
const { Button, DelayedRenderer, Chips, Image, MainNavBars, Multiselect, LibItem } = require('stremio/components');
|
const { DelayedRenderer, Chips, Image, MainNavBars, Multiselect, LibItem } = require('stremio/components');
|
||||||
|
const { default: Placeholder } = require('./Placeholder');
|
||||||
const useLibrary = require('./useLibrary');
|
const useLibrary = require('./useLibrary');
|
||||||
const useSelectableInputs = require('./useSelectableInputs');
|
const useSelectableInputs = require('./useSelectableInputs');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
@ -58,65 +59,54 @@ const Library = ({ model, urlParams, queryParams }) => {
|
||||||
}, [hasNextPage, loadNextPage]);
|
}, [hasNextPage, loadNextPage]);
|
||||||
const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_TRESHOLD);
|
const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_TRESHOLD);
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (profile.auth !== null && library.selected && library.selected.request.page === 1 && library.catalog.length !== 0 ) {
|
if (scrollContainerRef.current !== null && library.selected && library.selected.request.page === 1 && library.catalog.length !== 0) {
|
||||||
scrollContainerRef.current.scrollTop = 0;
|
scrollContainerRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
}, [profile.auth, library.selected]);
|
}, [profile.auth, library.selected]);
|
||||||
return (
|
return (
|
||||||
<MainNavBars className={styles['library-container']} route={model}>
|
<MainNavBars className={styles['library-container']} route={model}>
|
||||||
<div className={styles['library-content']}>
|
{
|
||||||
{
|
profile.auth !== null ?
|
||||||
model === 'continue_watching' || profile.auth !== null ?
|
<div className={styles['library-content']}>
|
||||||
<div className={styles['selectable-inputs-container']}>
|
<div className={styles['selectable-inputs-container']}>
|
||||||
<Multiselect {...typeSelect} className={styles['select-input-container']} />
|
<Multiselect {...typeSelect} className={styles['select-input-container']} />
|
||||||
<Chips {...sortChips} className={styles['select-input-container']} />
|
<Chips {...sortChips} className={styles['select-input-container']} />
|
||||||
</div>
|
</div>
|
||||||
:
|
{
|
||||||
null
|
library.selected === null ?
|
||||||
}
|
<DelayedRenderer delay={500}>
|
||||||
{
|
<div className={styles['message-container']}>
|
||||||
model === 'library' && profile.auth === null ?
|
<Image
|
||||||
<div className={classnames(styles['message-container'], styles['no-user-message-container'])}>
|
className={styles['image']}
|
||||||
<Image
|
src={require('/images/empty.png')}
|
||||||
className={styles['image']}
|
alt={' '}
|
||||||
src={require('/images/anonymous.png')}
|
/>
|
||||||
alt={' '}
|
<div className={styles['message-label']}>{model === 'library' ? 'Library' : 'Continue Watching'} not loaded!</div>
|
||||||
/>
|
</div>
|
||||||
<div className={styles['message-label']}>Library is only available for logged in users!</div>
|
</DelayedRenderer>
|
||||||
<Button className={styles['login-button-container']} href={'#/intro'}>
|
|
||||||
<div className={styles['label']}>LOG IN</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
library.selected === null ?
|
|
||||||
<DelayedRenderer delay={500}>
|
|
||||||
<div className={styles['message-container']}>
|
|
||||||
<Image
|
|
||||||
className={styles['image']}
|
|
||||||
src={require('/images/empty.png')}
|
|
||||||
alt={' '}
|
|
||||||
/>
|
|
||||||
<div className={styles['message-label']}>{model === 'library' ? 'Library' : 'Continue Watching'} not loaded!</div>
|
|
||||||
</div>
|
|
||||||
</DelayedRenderer>
|
|
||||||
:
|
|
||||||
library.catalog.length === 0 ?
|
|
||||||
<div className={styles['message-container']}>
|
|
||||||
<Image
|
|
||||||
className={styles['image']}
|
|
||||||
src={require('/images/empty.png')}
|
|
||||||
alt={' '}
|
|
||||||
/>
|
|
||||||
<div className={styles['message-label']}>Empty {model === 'library' ? 'Library' : 'Continue Watching'}</div>
|
|
||||||
</div>
|
|
||||||
:
|
:
|
||||||
<div ref={scrollContainerRef} className={classnames(styles['meta-items-container'], 'animation-fade-in')} onScroll={onScroll}>
|
library.catalog.length === 0 ?
|
||||||
{library.catalog.map((libItem, index) => (
|
<div className={styles['message-container']}>
|
||||||
<LibItem {...libItem} notifications={notifications} removable={model === 'library'} key={index} />
|
<Image
|
||||||
))}
|
className={styles['image']}
|
||||||
</div>
|
src={require('/images/empty.png')}
|
||||||
}
|
alt={' '}
|
||||||
</div>
|
/>
|
||||||
|
<div className={styles['message-label']}>Empty {model === 'library' ? 'Library' : 'Continue Watching'}</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div ref={scrollContainerRef} className={classnames(styles['meta-items-container'], 'animation-fade-in')} onScroll={onScroll}>
|
||||||
|
{
|
||||||
|
library.catalog.map((libItem, index) => (
|
||||||
|
<LibItem {...libItem} notifications={notifications} removable={model === 'library'} key={index} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<Placeholder />
|
||||||
|
}
|
||||||
</MainNavBars>
|
</MainNavBars>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
132
src/routes/Library/Placeholder/Placeholder.less
Normal file
132
src/routes/Library/Placeholder/Placeholder.less
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
// Copyright (C) 2017-2025 Smart code 203358507
|
||||||
|
|
||||||
|
@import (reference) '~stremio/common/screen-sizes.less';
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 14rem;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
.point {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
width: 18rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
flex: none;
|
||||||
|
height: 3.25rem;
|
||||||
|
width: 3.25rem;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: auto;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-size: 500;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 4rem;
|
||||||
|
line-height: 4rem;
|
||||||
|
padding: 0 5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3.5rem;
|
||||||
|
background-color: var(--overlay-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
outline: var(--focus-outline-size) solid var(--primary-foreground-color);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: @xsmall) {
|
||||||
|
.placeholder {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
max-height: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: @minimum) {
|
||||||
|
.placeholder {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.point {
|
||||||
|
.text {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/routes/Library/Placeholder/Placeholder.tsx
Normal file
47
src/routes/Library/Placeholder/Placeholder.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright (C) 2017-2025 Smart code 203358507
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Icon from '@stremio/stremio-icons/react';
|
||||||
|
import { Button, Image } from 'stremio/components';
|
||||||
|
import styles from './Placeholder.less';
|
||||||
|
|
||||||
|
const Placeholder = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles['placeholder']}>
|
||||||
|
<div className={styles['title']}>
|
||||||
|
{t('LIBRARY_NOT_LOGGED_IN')}
|
||||||
|
</div>
|
||||||
|
<div className={styles['image-container']}>
|
||||||
|
<Image
|
||||||
|
className={styles['image']}
|
||||||
|
src={require('/images/library_placeholder.png')}
|
||||||
|
alt={' '}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles['overview']}>
|
||||||
|
<div className={styles['point']}>
|
||||||
|
<Icon className={styles['icon']} name={'cloud-library'} />
|
||||||
|
<div className={styles['text']}>
|
||||||
|
{t('NOT_LOGGED_IN_CLOUD')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles['point']}>
|
||||||
|
<Icon className={styles['icon']} name={'actors'} />
|
||||||
|
<div className={styles['text']}>
|
||||||
|
{t('NOT_LOGGED_IN_RECOMMENDATIONS')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles['button-container']}>
|
||||||
|
<Button className={styles['button']} href={'#/intro?form=login'}>
|
||||||
|
{t('LOG_IN')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Placeholder;
|
||||||
5
src/routes/Library/Placeholder/index.ts
Normal file
5
src/routes/Library/Placeholder/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Copyright (C) 2017-2025 Smart code 203358507
|
||||||
|
|
||||||
|
import Placeholder from './Placeholder';
|
||||||
|
|
||||||
|
export default Placeholder;
|
||||||
|
|
@ -66,38 +66,6 @@
|
||||||
padding: 4rem;
|
padding: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.no-user-message-container {
|
|
||||||
.login-button-container {
|
|
||||||
flex: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 20rem;
|
|
||||||
height: 3.5rem;
|
|
||||||
border-radius: 3.5rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
background-color: var(--secondary-accent-color);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
outline: var(--focus-outline-size) solid var(--secondary-accent-color);
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
flex-basis: auto;
|
|
||||||
max-height: 4.8em;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--primary-foreground-color);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
|
|
|
||||||
|
|
@ -181,12 +181,17 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
||||||
const renderMenu = React.useMemo(() => function renderMenu() {
|
const renderMenu = React.useMemo(() => function renderMenu() {
|
||||||
return (
|
return (
|
||||||
<div className={styles['context-menu-content']} onPointerDown={popupMenuOnPointerDown} onContextMenu={popupMenuOnContextMenu} onClick={popupMenuOnClick} onKeyDown={popupMenuOnKeyDown}>
|
<div className={styles['context-menu-content']} onPointerDown={popupMenuOnPointerDown} onContextMenu={popupMenuOnContextMenu} onClick={popupMenuOnClick} onKeyDown={popupMenuOnKeyDown}>
|
||||||
|
<div className={styles['context-menu-title']}>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
<Button className={styles['context-menu-option-container']} title={t('CTX_PLAY')}>
|
<Button className={styles['context-menu-option-container']} title={t('CTX_PLAY')}>
|
||||||
|
<Icon className={styles['menu-icon']} name={'play'} />
|
||||||
<div className={styles['context-menu-option-label']}>{t('CTX_PLAY')}</div>
|
<div className={styles['context-menu-option-label']}>{t('CTX_PLAY')}</div>
|
||||||
</Button>
|
</Button>
|
||||||
{
|
{
|
||||||
streamLink &&
|
streamLink &&
|
||||||
<Button className={styles['context-menu-option-container']} title={t('CTX_COPY_STREAM_LINK')} onClick={copyStreamLink}>
|
<Button className={styles['context-menu-option-container']} title={t('CTX_COPY_STREAM_LINK')} onClick={copyStreamLink}>
|
||||||
|
<Icon className={styles['menu-icon']} name={'link'} />
|
||||||
<div className={styles['context-menu-option-label']}>{t('CTX_COPY_STREAM_LINK')}</div>
|
<div className={styles['context-menu-option-label']}>{t('CTX_COPY_STREAM_LINK')}</div>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,12 +111,29 @@
|
||||||
background-color: var(--secondary-accent-color);
|
background-color: var(--secondary-accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 1.7rem;
|
||||||
|
height: 1.7rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: var(--color-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
.context-menu-container {
|
.context-menu-container {
|
||||||
max-width: calc(90% - 1.5rem);
|
max-width: calc(90% - 1.5rem);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
.context-menu-content {
|
.context-menu-content {
|
||||||
--spatial-navigation-contain: contain;
|
--spatial-navigation-contain: contain;
|
||||||
|
|
||||||
|
.context-menu-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-weight: 100;
|
||||||
|
border-bottom: 1px solid var(--color-placeholder);
|
||||||
|
color: var(--primary-foreground-color);
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
|
||||||
.context-menu-option-container {
|
.context-menu-option-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -131,8 +148,9 @@
|
||||||
|
|
||||||
.context-menu-option-label {
|
.context-menu-option-label {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 300;
|
||||||
color:var(--primary-foreground-color);
|
color: var(--primary-foreground-color);
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,17 +36,23 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
||||||
return season;
|
return season;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const video = videos?.find((video) => video.id === libraryItem?.state.video_id);
|
||||||
|
|
||||||
|
if (video && video.season && seasons.includes(video.season)) {
|
||||||
|
return video.season;
|
||||||
|
}
|
||||||
|
|
||||||
const nonSpecialSeasons = seasons.filter((season) => season !== 0);
|
const nonSpecialSeasons = seasons.filter((season) => season !== 0);
|
||||||
if (nonSpecialSeasons.length > 0) {
|
if (nonSpecialSeasons.length > 0) {
|
||||||
return nonSpecialSeasons[nonSpecialSeasons.length - 1];
|
return nonSpecialSeasons[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seasons.length > 0) {
|
if (seasons.length > 0) {
|
||||||
return seasons[seasons.length - 1];
|
return seasons[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, [seasons, season]);
|
}, [seasons, season, videos, libraryItem]);
|
||||||
const videosForSeason = React.useMemo(() => {
|
const videosForSeason = React.useMemo(() => {
|
||||||
return videos
|
return videos
|
||||||
.filter((video) => {
|
.filter((video) => {
|
||||||
|
|
@ -56,6 +62,11 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
||||||
return a.episode - b.episode;
|
return a.episode - b.episode;
|
||||||
});
|
});
|
||||||
}, [videos, selectedSeason]);
|
}, [videos, selectedSeason]);
|
||||||
|
|
||||||
|
const seasonWatched = React.useMemo(() => {
|
||||||
|
return videosForSeason.every((video) => video.watched);
|
||||||
|
}, [videosForSeason]);
|
||||||
|
|
||||||
const [search, setSearch] = React.useState('');
|
const [search, setSearch] = React.useState('');
|
||||||
const searchInputOnChange = React.useCallback((event) => {
|
const searchInputOnChange = React.useCallback((event) => {
|
||||||
setSearch(event.currentTarget.value);
|
setSearch(event.currentTarget.value);
|
||||||
|
|
@ -71,6 +82,16 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onMarkSeasonAsWatched = (season, watched) => {
|
||||||
|
core.transport.dispatch({
|
||||||
|
action: 'MetaDetails',
|
||||||
|
args: {
|
||||||
|
action: 'MarkSeasonAsWatched',
|
||||||
|
args: [season, !watched]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(className, styles['videos-list-container'])}>
|
<div className={classnames(className, styles['videos-list-container'])}>
|
||||||
{
|
{
|
||||||
|
|
@ -135,6 +156,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
||||||
id={video.id}
|
id={video.id}
|
||||||
title={video.title}
|
title={video.title}
|
||||||
thumbnail={video.thumbnail}
|
thumbnail={video.thumbnail}
|
||||||
|
season={video.season}
|
||||||
episode={video.episode}
|
episode={video.episode}
|
||||||
released={video.released}
|
released={video.released}
|
||||||
upcoming={video.upcoming}
|
upcoming={video.upcoming}
|
||||||
|
|
@ -142,7 +164,9 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
||||||
progress={video.progress}
|
progress={video.progress}
|
||||||
deepLinks={video.deepLinks}
|
deepLinks={video.deepLinks}
|
||||||
scheduled={video.scheduled}
|
scheduled={video.scheduled}
|
||||||
|
seasonWatched={seasonWatched}
|
||||||
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
||||||
|
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ const ControlBar = ({
|
||||||
onToggleOptionsMenu,
|
onToggleOptionsMenu,
|
||||||
onToggleStatisticsMenu,
|
onToggleStatisticsMenu,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
|
onTouchStart,
|
||||||
|
onTouchEnd,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { chromecast } = useServices();
|
const { chromecast } = useServices();
|
||||||
|
|
@ -104,7 +106,7 @@ const ControlBar = ({
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div {...props} className={classnames(className, styles['control-bar-container'])} onContextMenu={onContextMenu}>
|
<div {...props} className={classnames(className, styles['control-bar-container'])} onContextMenu={onContextMenu} onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
|
||||||
<SeekBar
|
<SeekBar
|
||||||
className={styles['seek-bar']}
|
className={styles['seek-bar']}
|
||||||
time={time}
|
time={time}
|
||||||
|
|
@ -139,6 +141,7 @@ const ControlBar = ({
|
||||||
<VolumeSlider
|
<VolumeSlider
|
||||||
className={styles['volume-slider']}
|
className={styles['volume-slider']}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
|
muted={muted}
|
||||||
onVolumeChangeRequested={onVolumeChangeRequested}
|
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||||
/>
|
/>
|
||||||
<div className={styles['spacing']} />
|
<div className={styles['spacing']} />
|
||||||
|
|
@ -207,6 +210,8 @@ ControlBar.propTypes = {
|
||||||
onToggleOptionsMenu: PropTypes.func,
|
onToggleOptionsMenu: PropTypes.func,
|
||||||
onToggleStatisticsMenu: PropTypes.func,
|
onToggleStatisticsMenu: PropTypes.func,
|
||||||
onContextMenu: PropTypes.func,
|
onContextMenu: PropTypes.func,
|
||||||
|
onTouchStart: PropTypes.func,
|
||||||
|
onTouchEnd: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ControlBar;
|
module.exports = ControlBar;
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,16 @@ const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const debounce = require('lodash.debounce');
|
const debounce = require('lodash.debounce');
|
||||||
const { useRouteFocused } = require('stremio-router');
|
const { useRouteFocused } = require('stremio-router');
|
||||||
|
const { useServices } = require('stremio/services');
|
||||||
const { Slider } = require('stremio/components');
|
const { Slider } = require('stremio/components');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
|
|
||||||
const VolumeSlider = ({ className, volume, onVolumeChangeRequested }) => {
|
const VolumeSlider = ({ className, volume, onVolumeChangeRequested, muted }) => {
|
||||||
|
const { shell } = useServices();
|
||||||
const disabled = volume === null || isNaN(volume);
|
const disabled = volume === null || isNaN(volume);
|
||||||
const routeFocused = useRouteFocused();
|
const routeFocused = useRouteFocused();
|
||||||
const [slidingVolume, setSlidingVolume] = React.useState(null);
|
const [slidingVolume, setSlidingVolume] = React.useState(null);
|
||||||
|
const maxVolume = shell.active ? 200: 100;
|
||||||
const resetVolumeDebounced = React.useCallback(debounce(() => {
|
const resetVolumeDebounced = React.useCallback(debounce(() => {
|
||||||
setSlidingVolume(null);
|
setSlidingVolume(null);
|
||||||
}, 100), []);
|
}, 100), []);
|
||||||
|
|
@ -45,15 +48,18 @@ const VolumeSlider = ({ className, volume, onVolumeChangeRequested }) => {
|
||||||
className={classnames(className, styles['volume-slider'], { 'active': slidingVolume !== null })}
|
className={classnames(className, styles['volume-slider'], { 'active': slidingVolume !== null })}
|
||||||
value={
|
value={
|
||||||
!disabled ?
|
!disabled ?
|
||||||
slidingVolume !== null ? slidingVolume : volume
|
!muted ?
|
||||||
|
slidingVolume !== null ? slidingVolume : volume
|
||||||
|
: 0
|
||||||
:
|
:
|
||||||
100
|
100
|
||||||
}
|
}
|
||||||
minimumValue={0}
|
minimumValue={0}
|
||||||
maximumValue={100}
|
maximumValue={maxVolume}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onSlide={onSlide}
|
onSlide={onSlide}
|
||||||
onComplete={onComplete}
|
onComplete={onComplete}
|
||||||
|
audioBoost={!!shell.active}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -61,7 +67,8 @@ const VolumeSlider = ({ className, volume, onVolumeChangeRequested }) => {
|
||||||
VolumeSlider.propTypes = {
|
VolumeSlider.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
volume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
onVolumeChangeRequested: PropTypes.func
|
onVolumeChangeRequested: PropTypes.func,
|
||||||
|
muted: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = VolumeSlider;
|
module.exports = VolumeSlider;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const langs = require('langs');
|
||||||
const { useTranslation } = require('react-i18next');
|
const { useTranslation } = require('react-i18next');
|
||||||
const { useRouteFocused } = require('stremio-router');
|
const { useRouteFocused } = require('stremio-router');
|
||||||
const { useServices } = require('stremio/services');
|
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, ContextMenu } = require('stremio/components');
|
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
|
||||||
const BufferingLoader = require('./BufferingLoader');
|
const BufferingLoader = require('./BufferingLoader');
|
||||||
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
|
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
|
||||||
|
|
@ -137,11 +137,20 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
toast.show({
|
toast.show({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: t('PLAYER_SUBTITLES_LOADED'),
|
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
|
timeout: 3000
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onExtraSubtitlesTrackAdded = React.useCallback((track) => {
|
||||||
|
if (track.local) {
|
||||||
|
video.setExtraSubtitlesTrack(track.id);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onPlayRequested = React.useCallback(() => {
|
const onPlayRequested = React.useCallback(() => {
|
||||||
video.setProp('paused', false);
|
video.setProp('paused', false);
|
||||||
setSeeking(false);
|
setSeeking(false);
|
||||||
|
|
@ -176,13 +185,11 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubtitlesTrackSelected = React.useCallback((id) => {
|
const onSubtitlesTrackSelected = React.useCallback((id) => {
|
||||||
video.setProp('selectedSubtitlesTrackId', id);
|
video.setSubtitlesTrack(id);
|
||||||
video.setProp('selectedExtraSubtitlesTrackId', null);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
|
const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
|
||||||
video.setProp('selectedSubtitlesTrackId', null);
|
video.setExtraSubtitlesTrack(id);
|
||||||
video.setProp('selectedExtraSubtitlesTrackId', id);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onAudioTrackSelected = React.useCallback((id) => {
|
const onAudioTrackSelected = React.useCallback((id) => {
|
||||||
|
|
@ -274,6 +281,10 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
event.nativeEvent.immersePrevented = true;
|
event.nativeEvent.immersePrevented = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, async (filename, buffer) => {
|
||||||
|
video.addLocalSubtitles(filename, buffer);
|
||||||
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
video.unload();
|
video.unload();
|
||||||
|
|
@ -300,6 +311,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
0,
|
0,
|
||||||
forceTranscoding: forceTranscoding || casting,
|
forceTranscoding: forceTranscoding || casting,
|
||||||
maxAudioChannels: settings.surroundSound ? 32 : 2,
|
maxAudioChannels: settings.surroundSound ? 32 : 2,
|
||||||
|
hardwareDecoding: settings.hardwareDecoding,
|
||||||
streamingServerURL: streamingServer.baseUrl ?
|
streamingServerURL: streamingServer.baseUrl ?
|
||||||
casting ?
|
casting ?
|
||||||
streamingServer.baseUrl
|
streamingServer.baseUrl
|
||||||
|
|
@ -307,7 +319,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
streamingServer.selected.transportUrl
|
streamingServer.selected.transportUrl
|
||||||
:
|
:
|
||||||
null,
|
null,
|
||||||
seriesInfo: player.seriesInfo
|
seriesInfo: player.seriesInfo,
|
||||||
}, {
|
}, {
|
||||||
chromecastTransport: chromecast.active ? chromecast.transport : null,
|
chromecastTransport: chromecast.active ? chromecast.transport : null,
|
||||||
shellTransport: shell.active ? shell.transport : null,
|
shellTransport: shell.active ? shell.transport : null,
|
||||||
|
|
@ -498,14 +510,14 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
case 'ArrowUp': {
|
case 'ArrowUp': {
|
||||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||||
onVolumeChangeRequested(video.state.volume + 5);
|
onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ArrowDown': {
|
case 'ArrowDown': {
|
||||||
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
||||||
onVolumeChangeRequested(video.state.volume - 5);
|
onVolumeChangeRequested(Math.max(video.state.volume - 5, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -563,13 +575,13 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onWheel = ({ deltaY }) => {
|
const onWheel = ({ deltaY }) => {
|
||||||
|
if (menusOpen || video.state.volume === null) return;
|
||||||
|
|
||||||
if (deltaY > 0) {
|
if (deltaY > 0) {
|
||||||
if (!menusOpen && video.state.volume !== null) {
|
onVolumeChangeRequested(Math.max(video.state.volume - 5, 0));
|
||||||
onVolumeChangeRequested(video.state.volume - 5);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!menusOpen && video.state.volume !== null) {
|
if (video.state.volume < 100) {
|
||||||
onVolumeChangeRequested(video.state.volume + 5);
|
onVolumeChangeRequested(Math.min(video.state.volume + 5, 100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -590,6 +602,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
video.events.on('ended', onEnded);
|
video.events.on('ended', onEnded);
|
||||||
video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
||||||
video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
||||||
|
video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
||||||
video.events.on('implementationChanged', onImplementationChanged);
|
video.events.on('implementationChanged', onImplementationChanged);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -597,6 +610,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
video.events.off('ended', onEnded);
|
video.events.off('ended', onEnded);
|
||||||
video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
||||||
video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
||||||
|
video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
||||||
video.events.off('implementationChanged', onImplementationChanged);
|
video.events.off('implementationChanged', onImplementationChanged);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
||||||
setSeason(parseInt(event.value));
|
setSeason(parseInt(event.value));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const seasonWatched = React.useMemo(() => {
|
||||||
|
return videos.every((video) => video.watched);
|
||||||
|
}, [videos]);
|
||||||
|
|
||||||
const onMarkVideoAsWatched = useCallback((video: Video, watched: boolean) => {
|
const onMarkVideoAsWatched = useCallback((video: Video, watched: boolean) => {
|
||||||
core.transport.dispatch({
|
core.transport.dispatch({
|
||||||
action: 'Player',
|
action: 'Player',
|
||||||
|
|
@ -57,6 +61,16 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onMarkSeasonAsWatched = (season: number, watched: boolean) => {
|
||||||
|
core.transport.dispatch({
|
||||||
|
action: 'Player',
|
||||||
|
args: {
|
||||||
|
action: 'MarkSeasonAsWatched',
|
||||||
|
args: [season, !watched]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onMouseDown = (event: React.MouseEvent) => {
|
const onMouseDown = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
@ -95,14 +109,17 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
||||||
id={video.id}
|
id={video.id}
|
||||||
title={video.title}
|
title={video.title}
|
||||||
thumbnail={video.thumbnail}
|
thumbnail={video.thumbnail}
|
||||||
|
season={video.season}
|
||||||
episode={video.episode}
|
episode={video.episode}
|
||||||
released={video.released}
|
released={video.released}
|
||||||
upcoming={video.upcoming}
|
upcoming={video.upcoming}
|
||||||
watched={video.watched}
|
watched={video.watched}
|
||||||
|
seasonWatched={seasonWatched}
|
||||||
progress={video.progress}
|
progress={video.progress}
|
||||||
deepLinks={video.deepLinks}
|
deepLinks={video.deepLinks}
|
||||||
scheduled={video.scheduled}
|
scheduled={video.scheduled}
|
||||||
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
||||||
|
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright (C) 2017-2024 Smart code 203358507
|
// Copyright (C) 2017-2024 Smart code 203358507
|
||||||
|
|
||||||
import React from 'react';
|
import React, { BaseSyntheticEvent } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from '@stremio/stremio-icons/react';
|
import Icon from '@stremio/stremio-icons/react';
|
||||||
import styles from './SideDrawerButton.less';
|
import styles from './SideDrawerButton.less';
|
||||||
|
|
@ -9,11 +9,13 @@ type Props = {
|
||||||
className: string,
|
className: string,
|
||||||
onClick: () => void,
|
onClick: () => void,
|
||||||
onContextMenu: () => void,
|
onContextMenu: () => void,
|
||||||
|
onTouchStart: (event: BaseSyntheticEvent) => void,
|
||||||
|
onTouchEnd: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SideDrawerButton = ({ className, onClick, onContextMenu }: Props) => {
|
const SideDrawerButton = ({ className, onClick, onContextMenu, onTouchStart, onTouchEnd }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(className, styles['side-drawer-button'])} onClick={onClick} onContextMenu={onContextMenu}>
|
<div className={classNames(className, styles['side-drawer-button'])} onClick={onClick} onContextMenu={onContextMenu} onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
|
||||||
<Icon name={'chevron-back'} className={styles['icon']} />
|
<Icon name={'chevron-back'} className={styles['icon']} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@ const styles = require('./styles');
|
||||||
const { t } = require('i18next');
|
const { t } = require('i18next');
|
||||||
|
|
||||||
const ORIGIN_PRIORITIES = {
|
const ORIGIN_PRIORITIES = {
|
||||||
|
'LOCAL': 3,
|
||||||
'EMBEDDED': 2,
|
'EMBEDDED': 2,
|
||||||
'EXCLUSIVE': 1
|
'EXCLUSIVE': 1,
|
||||||
};
|
};
|
||||||
const LANGUAGE_PRIORITIES = {
|
const LANGUAGE_PRIORITIES = {
|
||||||
'eng': 1
|
'local': 2,
|
||||||
|
'eng': 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SubtitlesMenu = React.memo((props) => {
|
const SubtitlesMenu = React.memo((props) => {
|
||||||
|
|
@ -161,7 +163,11 @@ const SubtitlesMenu = React.memo((props) => {
|
||||||
</Button>
|
</Button>
|
||||||
{subtitlesLanguages.map((lang, index) => (
|
{subtitlesLanguages.map((lang, index) => (
|
||||||
<Button key={index} title={languages.label(lang)} className={classnames(styles['language-option'], { 'selected': selectedSubtitlesLanguage === lang })} data-lang={lang} onClick={subtitlesLanguageOnClick}>
|
<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 ?
|
selectedSubtitlesLanguage === lang ?
|
||||||
<div className={styles['icon']} />
|
<div className={styles['icon']} />
|
||||||
|
|
@ -207,60 +213,62 @@ const SubtitlesMenu = React.memo((props) => {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['subtitles-settings-container']}>
|
<div className={styles['subtitles-settings-container']}>
|
||||||
<div className={styles['settings-header']}>Subtitles Settings</div>
|
<div className={styles['settings-header']}>{t('PLAYER_SUBTITLES_SETTINGS')}</div>
|
||||||
<DiscreteSelectInput
|
<div className={styles['settings-list']}>
|
||||||
className={styles['discrete-input']}
|
<DiscreteSelectInput
|
||||||
label={t('DELAY')}
|
className={styles['discrete-input']}
|
||||||
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
|
label={t('DELAY')}
|
||||||
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
|
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
|
||||||
onChange={onSubtitlesDelayChanged}
|
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
|
||||||
/>
|
onChange={onSubtitlesDelayChanged}
|
||||||
<DiscreteSelectInput
|
/>
|
||||||
className={styles['discrete-input']}
|
<DiscreteSelectInput
|
||||||
label={t('SIZE')}
|
className={styles['discrete-input']}
|
||||||
value={
|
label={t('SIZE')}
|
||||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
value={
|
||||||
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
|
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||||
:
|
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
|
||||||
props.extraSubtitlesSize !== null && !isNaN(props.extraSubtitlesSize) ? `${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)
|
}
|
||||||
:
|
disabled={
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||||
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
|
props.subtitlesSize === null || isNaN(props.subtitlesSize)
|
||||||
:
|
:
|
||||||
true
|
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
||||||
}
|
props.extraSubtitlesSize === null || isNaN(props.extraSubtitlesSize)
|
||||||
onChange={onSubtitlesSizeChanged}
|
:
|
||||||
/>
|
true
|
||||||
<DiscreteSelectInput
|
}
|
||||||
className={styles['discrete-input']}
|
onChange={onSubtitlesSizeChanged}
|
||||||
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
|
/>
|
||||||
value={
|
<DiscreteSelectInput
|
||||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
className={styles['discrete-input']}
|
||||||
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
|
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
|
||||||
:
|
value={
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||||
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
|
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'
|
||||||
:
|
:
|
||||||
'--'
|
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
||||||
}
|
props.extraSubtitlesOffset !== null && !isNaN(props.extraSubtitlesOffset) ? `${props.extraSubtitlesOffset}%` : '--'
|
||||||
disabled={
|
:
|
||||||
typeof props.selectedSubtitlesTrackId === 'string' ?
|
'--'
|
||||||
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
|
}
|
||||||
:
|
disabled={
|
||||||
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
typeof props.selectedSubtitlesTrackId === 'string' ?
|
||||||
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
|
props.subtitlesOffset === null || isNaN(props.subtitlesOffset)
|
||||||
:
|
:
|
||||||
true
|
typeof props.selectedExtraSubtitlesTrackId === 'string' ?
|
||||||
}
|
props.extraSubtitlesOffset === null || isNaN(props.extraSubtitlesOffset)
|
||||||
onChange={onSubtitlesOffsetChanged}
|
:
|
||||||
/>
|
true
|
||||||
|
}
|
||||||
|
onChange={onSubtitlesOffsetChanged}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,10 @@
|
||||||
.subtitles-settings-container {
|
.subtitles-settings-container {
|
||||||
width: 17rem;
|
width: 17rem;
|
||||||
|
|
||||||
|
.settings-list {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
.spacing {
|
.spacing {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,10 +79,31 @@ const useVideo = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addLocalSubtitles = (filename, buffer) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'command',
|
||||||
|
commandName: 'addLocalSubtitles',
|
||||||
|
commandArgs: {
|
||||||
|
filename,
|
||||||
|
buffer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const setProp = (name, value) => {
|
const setProp = (name, value) => {
|
||||||
dispatch({ type: 'setProp', propName: name, propValue: 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) => {
|
const onError = (error) => {
|
||||||
events.emit('error', error);
|
events.emit('error', error);
|
||||||
};
|
};
|
||||||
|
|
@ -99,6 +120,10 @@ const useVideo = () => {
|
||||||
events.emit('extraSubtitlesTrackLoaded', track);
|
events.emit('extraSubtitlesTrackLoaded', track);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onExtraSubtitlesTrackAdded = (track) => {
|
||||||
|
events.emit('extraSubtitlesTrackAdded', track);
|
||||||
|
};
|
||||||
|
|
||||||
const onPropChanged = (name, value) => {
|
const onPropChanged = (name, value) => {
|
||||||
setState((state) => ({
|
setState((state) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|
@ -125,6 +150,7 @@ const useVideo = () => {
|
||||||
video.current.on('implementationChanged', onImplementationChanged);
|
video.current.on('implementationChanged', onImplementationChanged);
|
||||||
video.current.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
video.current.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
||||||
video.current.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
video.current.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
||||||
|
video.current.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
||||||
|
|
||||||
return () => video.current.destroy();
|
return () => video.current.destroy();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -136,7 +162,10 @@ const useVideo = () => {
|
||||||
load,
|
load,
|
||||||
unload,
|
unload,
|
||||||
addExtraSubtitlesTracks,
|
addExtraSubtitlesTracks,
|
||||||
|
addLocalSubtitles,
|
||||||
setProp,
|
setProp,
|
||||||
|
setSubtitlesTrack,
|
||||||
|
setExtraSubtitlesTrack,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const React = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
const classnames = require('classnames');
|
const classnames = require('classnames');
|
||||||
const debounce = require('lodash.debounce');
|
const debounce = require('lodash.debounce');
|
||||||
const { useTranslation } = require('react-i18next');
|
const useTranslate = require('stremio/common/useTranslate');
|
||||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||||
const { withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
|
const { withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
|
||||||
const { Image, MainNavBars, MetaItem, MetaRow } = require('stremio/components');
|
const { Image, MainNavBars, MetaItem, MetaRow } = require('stremio/components');
|
||||||
|
|
@ -14,7 +14,7 @@ const styles = require('./styles');
|
||||||
const THRESHOLD = 100;
|
const THRESHOLD = 100;
|
||||||
|
|
||||||
const Search = ({ queryParams }) => {
|
const Search = ({ queryParams }) => {
|
||||||
const { t } = useTranslation();
|
const t = useTranslate();
|
||||||
const [search, loadSearchRows] = useSearch(queryParams);
|
const [search, loadSearchRows] = useSearch(queryParams);
|
||||||
const query = React.useMemo(() => {
|
const query = React.useMemo(() => {
|
||||||
return search.selected !== null ?
|
return search.selected !== null ?
|
||||||
|
|
@ -52,24 +52,24 @@ const Search = ({ queryParams }) => {
|
||||||
query === null ?
|
query === null ?
|
||||||
<div className={classnames(styles['search-hints-wrapper'])}>
|
<div className={classnames(styles['search-hints-wrapper'])}>
|
||||||
<div className={classnames(styles['search-hints-title-container'], 'animation-fade-in')}>
|
<div className={classnames(styles['search-hints-title-container'], 'animation-fade-in')}>
|
||||||
<div className={styles['search-hints-title']}>{t('SEARCH_ANYTHING')}</div>
|
<div className={styles['search-hints-title']}>{t.string('SEARCH_ANYTHING')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classnames(styles['search-hints-container'], 'animation-fade-in')}>
|
<div className={classnames(styles['search-hints-container'], 'animation-fade-in')}>
|
||||||
<div className={styles['search-hint-container']}>
|
<div className={styles['search-hint-container']}>
|
||||||
<Icon className={styles['icon']} name={'trailer'} />
|
<Icon className={styles['icon']} name={'trailer'} />
|
||||||
<div className={styles['label']}>{t('SEARCH_CATEGORIES')}</div>
|
<div className={styles['label']}>{t.string('SEARCH_CATEGORIES')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['search-hint-container']}>
|
<div className={styles['search-hint-container']}>
|
||||||
<Icon className={styles['icon']} name={'actors'} />
|
<Icon className={styles['icon']} name={'actors'} />
|
||||||
<div className={styles['label']}>{t('SEARCH_PERSONS')}</div>
|
<div className={styles['label']}>{t.string('SEARCH_PERSONS')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['search-hint-container']}>
|
<div className={styles['search-hint-container']}>
|
||||||
<Icon className={styles['icon']} name={'link'} />
|
<Icon className={styles['icon']} name={'link'} />
|
||||||
<div className={styles['label']}>{t('SEARCH_PROTOCOLS')}</div>
|
<div className={styles['label']}>{t.string('SEARCH_PROTOCOLS')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['search-hint-container']}>
|
<div className={styles['search-hint-container']}>
|
||||||
<Icon className={styles['icon']} name={'imdb-outline'} />
|
<Icon className={styles['icon']} name={'imdb-outline'} />
|
||||||
<div className={styles['label']}>{t('SEARCH_TYPES')}</div>
|
<div className={styles['label']}>{t.string('SEARCH_TYPES')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,7 +81,7 @@ const Search = ({ queryParams }) => {
|
||||||
src={require('/images/empty.png')}
|
src={require('/images/empty.png')}
|
||||||
alt={' '}
|
alt={' '}
|
||||||
/>
|
/>
|
||||||
<div className={styles['message-label']}>{ t('STREMIO_TV_SEARCH_NO_ADDONS') }</div>
|
<div className={styles['message-label']}>{ t.string('STREMIO_TV_SEARCH_NO_ADDONS') }</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
search.catalogs.map((catalog, index) => {
|
search.catalogs.map((catalog, index) => {
|
||||||
|
|
@ -115,6 +115,7 @@ const Search = ({ queryParams }) => {
|
||||||
key={index}
|
key={index}
|
||||||
className={classnames(styles['search-row'], styles['search-row-poster'], 'animation-fade-in')}
|
className={classnames(styles['search-row'], styles['search-row-poster'], 'animation-fade-in')}
|
||||||
catalog={catalog}
|
catalog={catalog}
|
||||||
|
title={t.catalogTitle(catalog)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ const Settings = () => {
|
||||||
seekTimeDurationSelect,
|
seekTimeDurationSelect,
|
||||||
seekShortTimeDurationSelect,
|
seekShortTimeDurationSelect,
|
||||||
escExitFullscreenToggle,
|
escExitFullscreenToggle,
|
||||||
|
quitOnCloseToggle,
|
||||||
playInExternalPlayerSelect,
|
playInExternalPlayerSelect,
|
||||||
nextVideoPopupDurationSelect,
|
nextVideoPopupDurationSelect,
|
||||||
bingeWatchingToggle,
|
bingeWatchingToggle,
|
||||||
|
|
@ -322,12 +323,25 @@ const Settings = () => {
|
||||||
{...interfaceLanguageSelect}
|
{...interfaceLanguageSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
shell.active &&
|
||||||
|
<div className={styles['option-container']}>
|
||||||
|
<div className={styles['option-name-container']}>
|
||||||
|
<div className={styles['label']}>{ t('SETTINGS_QUIT_ON_CLOSE') }</div>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||||
|
tabIndex={-1}
|
||||||
|
{...quitOnCloseToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div ref={playerSectionRef} className={styles['section-container']}>
|
<div ref={playerSectionRef} className={styles['section-container']}>
|
||||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>
|
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>
|
||||||
<div className={styles['section-category-container']}>
|
<div className={styles['section-category-container']}>
|
||||||
<Icon className={styles['icon']} name={'subtitles'} />
|
<Icon className={styles['icon']} name={'subtitles'} />
|
||||||
<div className={styles['label']}>{t('SETTINGS_SECTION_SUBTITLES')}</div>
|
<div className={styles['label']}>{t('SETTINGS_CLOSE_WINDOW')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['option-container']}>
|
<div className={styles['option-container']}>
|
||||||
<div className={styles['option-name-container']}>
|
<div className={styles['option-name-container']}>
|
||||||
|
|
@ -488,17 +502,19 @@ const Settings = () => {
|
||||||
{...playInExternalPlayerSelect}
|
{...playInExternalPlayerSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['option-container']}>
|
{
|
||||||
<div className={styles['option-name-container']}>
|
shell.active &&
|
||||||
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
|
<div className={styles['option-container']}>
|
||||||
</div>
|
<div className={styles['option-name-container']}>
|
||||||
<Toggle
|
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
|
||||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
</div>
|
||||||
disabled={true}
|
<Toggle
|
||||||
tabIndex={-1}
|
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||||
{...hardwareDecodingToggle}
|
tabIndex={-1}
|
||||||
/>
|
{...hardwareDecodingToggle}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div ref={streamingServerSectionRef} className={styles['section-container']}>
|
<div ref={streamingServerSectionRef} className={styles['section-container']}>
|
||||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_STREAMING') }</div>
|
<div className={styles['section-title']}>{ t('SETTINGS_NAV_STREAMING') }</div>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,23 @@ const useProfileSettingsInputs = (profile) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}), [profile.settings]);
|
}), [profile.settings]);
|
||||||
|
|
||||||
|
const quitOnCloseToggle = React.useMemo(() => ({
|
||||||
|
checked: profile.settings.quitOnClose,
|
||||||
|
onClick: () => {
|
||||||
|
core.transport.dispatch({
|
||||||
|
action: 'Ctx',
|
||||||
|
args: {
|
||||||
|
action: 'UpdateSettings',
|
||||||
|
args: {
|
||||||
|
...profile.settings,
|
||||||
|
quitOnClose: !profile.settings.quitOnClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}), [profile.settings]);
|
||||||
|
|
||||||
const subtitlesLanguageSelect = React.useMemo(() => ({
|
const subtitlesLanguageSelect = React.useMemo(() => ({
|
||||||
options: Object.keys(languageNames).map((code) => ({
|
options: Object.keys(languageNames).map((code) => ({
|
||||||
value: code,
|
value: code,
|
||||||
|
|
@ -316,6 +333,7 @@ const useProfileSettingsInputs = (profile) => {
|
||||||
audioLanguageSelect,
|
audioLanguageSelect,
|
||||||
surroundSoundToggle,
|
surroundSoundToggle,
|
||||||
escExitFullscreenToggle,
|
escExitFullscreenToggle,
|
||||||
|
quitOnCloseToggle,
|
||||||
seekTimeDurationSelect,
|
seekTimeDurationSelect,
|
||||||
seekShortTimeDurationSelect,
|
seekShortTimeDurationSelect,
|
||||||
playInExternalPlayerSelect,
|
playInExternalPlayerSelect,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ function DragAndDrop({ core }) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'application/x-subrip':
|
||||||
|
break;
|
||||||
|
case 'text/vtt':
|
||||||
|
break;
|
||||||
|
case '':
|
||||||
|
break;
|
||||||
default: {
|
default: {
|
||||||
events.emit('error', {
|
events.emit('error', {
|
||||||
message: 'Unsupported file',
|
message: 'Unsupported file',
|
||||||
|
|
|
||||||
22
src/types/global.d.ts
vendored
22
src/types/global.d.ts
vendored
|
|
@ -1,15 +1,31 @@
|
||||||
/* eslint-disable no-var */
|
/* eslint-disable no-var */
|
||||||
|
|
||||||
|
type QtTransportMessage = {
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface QtTransport {
|
interface QtTransport {
|
||||||
send: (message: string) => void,
|
send: (message: string) => void,
|
||||||
|
onmessage: (message: QtTransportMessage) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Qt {
|
interface Qt {
|
||||||
webChannelTransport: QtTransport,
|
webChannelTransport: QtTransport,
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
interface ChromeWebView {
|
||||||
var qt: Qt | undefined;
|
addEventListener: (type: 'message', listenenr: (event: any) => void) => void,
|
||||||
|
removeEventListener: (type: 'message', listenenr: (event: any) => void) => void,
|
||||||
|
postMessage: (message: string) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { };
|
interface Chrome {
|
||||||
|
webview: ChromeWebView,
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var qt: Qt | undefined;
|
||||||
|
var chrome: Chrome | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ module.exports = (env, argv) => ({
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
generator: {
|
generator: {
|
||||||
filename: `${COMMIT_HASH}/images/[name][ext][query]`
|
filename: 'images/[name][ext][query]'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -231,9 +231,9 @@ module.exports = (env, argv) => ({
|
||||||
}),
|
}),
|
||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{ from: 'favicons', to: `${COMMIT_HASH}/favicons` },
|
{ from: 'favicons', to: 'favicons' },
|
||||||
{ from: 'images', to: `${COMMIT_HASH}/images` },
|
{ from: 'images', to: 'images' },
|
||||||
{ from: 'screenshots/*.webp', to: `${COMMIT_HASH}` },
|
{ from: 'screenshots/*.webp', to: './' },
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
|
|
@ -243,8 +243,8 @@ module.exports = (env, argv) => ({
|
||||||
template: './src/index.html',
|
template: './src/index.html',
|
||||||
inject: false,
|
inject: false,
|
||||||
scriptLoading: 'blocking',
|
scriptLoading: 'blocking',
|
||||||
faviconsPath: `${COMMIT_HASH}/favicons`,
|
faviconsPath: 'favicons',
|
||||||
imagesPath: `${COMMIT_HASH}/images`,
|
imagesPath: 'images',
|
||||||
}),
|
}),
|
||||||
new WebpackPwaManifest({
|
new WebpackPwaManifest({
|
||||||
name: 'Stremio Web',
|
name: 'Stremio Web',
|
||||||
|
|
@ -261,33 +261,33 @@ module.exports = (env, argv) => ({
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: 'images/icon.png',
|
src: 'images/icon.png',
|
||||||
destination: `${COMMIT_HASH}/icons`,
|
destination: 'icons',
|
||||||
sizes: [196, 512],
|
sizes: [196, 512],
|
||||||
purpose: 'any'
|
purpose: 'any'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'images/maskable_icon.png',
|
src: 'images/maskable_icon.png',
|
||||||
destination: `${COMMIT_HASH}/maskable_icons`,
|
destination: 'maskable_icons',
|
||||||
sizes: [196, 512],
|
sizes: [196, 512],
|
||||||
purpose: 'maskable',
|
purpose: 'maskable',
|
||||||
ios: true
|
ios: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'favicons/favicon.ico',
|
src: 'favicons/favicon.ico',
|
||||||
destination: `${COMMIT_HASH}/favicons`,
|
destination: 'favicons',
|
||||||
sizes: [256],
|
sizes: [256],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
screenshots : [
|
screenshots : [
|
||||||
{
|
{
|
||||||
src: `${COMMIT_HASH}/screenshots/board_wide.webp`,
|
src: 'screenshots/board_wide.webp',
|
||||||
sizes: '1440x900',
|
sizes: '1440x900',
|
||||||
type: 'image/webp',
|
type: 'image/webp',
|
||||||
form_factor: 'wide',
|
form_factor: 'wide',
|
||||||
label: 'Homescreen of Stremio'
|
label: 'Homescreen of Stremio'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: `${COMMIT_HASH}/screenshots/board_narrow.webp`,
|
src: 'screenshots/board_narrow.webp',
|
||||||
sizes: '414x896',
|
sizes: '414x896',
|
||||||
type: 'image/webp',
|
type: 'image/webp',
|
||||||
form_factor: 'narrow',
|
form_factor: 'narrow',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue