mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
Merge branch 'development' into fix/library-default-filter
This commit is contained in:
commit
20577e2431
27 changed files with 574 additions and 172 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 |
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"name": "stremio",
|
||||
"version": "5.0.0-beta.18",
|
||||
"version": "5.0.0-beta.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "stremio",
|
||||
"version": "5.0.0-beta.18",
|
||||
"version": "5.0.0-beta.20",
|
||||
"license": "gpl-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@sentry/browser": "8.42.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-video": "0.0.53",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
@ -3371,9 +3371,9 @@
|
|||
"integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg=="
|
||||
},
|
||||
"node_modules/@stremio/stremio-core-web": {
|
||||
"version": "0.48.5",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.48.5.tgz",
|
||||
"integrity": "sha512-oDTNBrv8zZi1VGbeV+1Bm6CliI2rF23ERdJpz+gv8EnbFjRIo78WIsoS0yO0EOg8HHXYsFytPq5+c0+YlxmBlA==",
|
||||
"version": "0.49.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.49.0.tgz",
|
||||
"integrity": "sha512-oxJRVAE6z6Eh1B0qomdz6L2CVaTkwt70kDNC1TmHyGNo+Hhp2RaMlygqBKvBLXyHUXi82R67Mc11gT/JqlmaMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.24.1"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "stremio",
|
||||
"displayName": "Stremio",
|
||||
"version": "5.0.0-beta.18",
|
||||
"version": "5.0.0-beta.20",
|
||||
"author": "Smart Code OOD",
|
||||
"private": true,
|
||||
"license": "gpl-2.0",
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"@babel/runtime": "7.26.0",
|
||||
"@sentry/browser": "8.42.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-video": "0.0.53",
|
||||
"a-color-picker": "1.2.1",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next');
|
|||
const { Router } = require('stremio-router');
|
||||
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
||||
const { NotFound } = require('stremio/routes');
|
||||
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
|
||||
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common');
|
||||
const ServicesToaster = require('./ServicesToaster');
|
||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||
const SearchParamsHandler = require('./SearchParamsHandler');
|
||||
|
|
@ -20,6 +20,8 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router))
|
|||
|
||||
const App = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const shell = useShell();
|
||||
const [windowHidden, setWindowHidden] = React.useState(false);
|
||||
const onPathNotMatch = React.useCallback(() => {
|
||||
return NotFound;
|
||||
}, []);
|
||||
|
|
@ -97,6 +99,17 @@ const App = () => {
|
|||
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(() => {
|
||||
const onCoreEvent = ({ event, args }) => {
|
||||
switch (event) {
|
||||
|
|
@ -104,6 +117,11 @@ const App = () => {
|
|||
if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') {
|
||||
i18n.changeLanguage(args.settings.interfaceLanguage);
|
||||
}
|
||||
|
||||
if (args?.settings?.quitOnClose && windowHidden) {
|
||||
shell.send('quit');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -112,6 +130,10 @@ const App = () => {
|
|||
if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') {
|
||||
i18n.changeLanguage(state.profile.settings.interfaceLanguage);
|
||||
}
|
||||
|
||||
if (state?.profile?.settings?.quitOnClose && windowHidden) {
|
||||
shell.send('quit');
|
||||
}
|
||||
};
|
||||
const onWindowFocus = () => {
|
||||
services.core.transport.dispatch({
|
||||
|
|
@ -146,7 +168,7 @@ const App = () => {
|
|||
services.core.transport
|
||||
.getState('ctx')
|
||||
.then(onCtxState)
|
||||
.catch((e) => console.error(e));
|
||||
.catch(console.error);
|
||||
}
|
||||
return () => {
|
||||
if (services.core.active) {
|
||||
|
|
@ -154,7 +176,7 @@ const App = () => {
|
|||
services.core.transport.off('CoreEvent', onCoreEvent);
|
||||
}
|
||||
};
|
||||
}, [initialized]);
|
||||
}, [initialized, windowHidden]);
|
||||
return (
|
||||
<React.StrictMode>
|
||||
<ServicesProvider services={services}>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const { default: useShell } = require('./useShell');
|
|||
const useStreamingServer = require('./useStreamingServer');
|
||||
const useTorrent = require('./useTorrent');
|
||||
const useTranslate = require('./useTranslate');
|
||||
const { default: useOrientation } = require('./useOrientation');
|
||||
|
||||
module.exports = {
|
||||
FileDropProvider,
|
||||
|
|
@ -55,4 +56,5 @@ module.exports = {
|
|||
useStreamingServer,
|
||||
useTorrent,
|
||||
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;
|
||||
|
|
@ -1,21 +1,71 @@
|
|||
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 transport = globalThis?.qt?.webChannelTransport;
|
||||
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)[]) => {
|
||||
transport?.send(JSON.stringify({
|
||||
id: createId(),
|
||||
type: 6,
|
||||
object: 'transport',
|
||||
method: 'onEvent',
|
||||
args: [method, ...args],
|
||||
}));
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { createPortal } from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
import useBinaryState from 'stremio/common/useBinaryState';
|
||||
import useOrientation from 'stremio/common/useOrientation';
|
||||
import styles from './BottomSheet.less';
|
||||
|
||||
const CLOSE_THRESHOLD = 100;
|
||||
|
|
@ -17,6 +18,7 @@ type Props = {
|
|||
|
||||
const BottomSheet = ({ children, title, show, onClose }: Props) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const orientation = useOrientation();
|
||||
const [startOffset, setStartOffset] = useState(0);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
|
|
@ -58,6 +60,10 @@ const BottomSheet = ({ children, title, show, onClose }: Props) => {
|
|||
!opened && onClose();
|
||||
}, [opened]);
|
||||
|
||||
useEffect(() => {
|
||||
opened && close();
|
||||
}, [orientation]);
|
||||
|
||||
return opened && createPortal((
|
||||
<div className={styles['bottom-sheet']}>
|
||||
<div className={styles['backdrop']} onClick={onCloseRequest} />
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
margin: 0 1rem 0 0.3rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@
|
|||
|
||||
.nav-content-container {
|
||||
position: absolute;
|
||||
padding-top: calc(var(--horizontal-nav-bar-size) + var(--safe-area-inset-top));
|
||||
top: 0;
|
||||
top: calc(var(--horizontal-nav-bar-size) + var(--safe-area-inset-top));
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: var(--vertical-nav-bar-size);
|
||||
|
|
@ -43,7 +42,7 @@
|
|||
.main-nav-bars-container {
|
||||
.nav-content-container {
|
||||
left: 0;
|
||||
padding-bottom: var(--vertical-nav-bar-size);
|
||||
bottom: var(--vertical-nav-bar-size);
|
||||
}
|
||||
|
||||
.vertical-nav-bar {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const useBinaryState = require('stremio/common/useBinaryState');
|
|||
const VideoPlaceholder = require('./VideoPlaceholder');
|
||||
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 [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const popupLabelOnMouseUp = React.useCallback((event) => {
|
||||
|
|
@ -50,6 +50,12 @@ const Video = ({ className, id, title, thumbnail, episode, released, upcoming, w
|
|||
closeMenu();
|
||||
onMarkVideoAsWatched({ 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(() => {
|
||||
if (deepLinks) {
|
||||
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}>
|
||||
<div className={styles['context-menu-option-label']}>{watched ? t('CTX_MARK_NON_WATCHED') : t('CTX_MARK_WATCHED')}</div>
|
||||
</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>
|
||||
);
|
||||
}, [watched, toggleWatchedOnClick]);
|
||||
}, [watched, seasonWatched, toggleWatchedOnClick]);
|
||||
React.useEffect(() => {
|
||||
if (!routeFocused) {
|
||||
closeMenu();
|
||||
|
|
@ -182,17 +191,20 @@ Video.propTypes = {
|
|||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
season: PropTypes.number,
|
||||
episode: PropTypes.number,
|
||||
released: PropTypes.instanceOf(Date),
|
||||
upcoming: PropTypes.bool,
|
||||
watched: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
scheduled: PropTypes.bool,
|
||||
seasonWatched: PropTypes.bool,
|
||||
deepLinks: PropTypes.shape({
|
||||
metaDetailsStreams: PropTypes.string,
|
||||
player: PropTypes.string
|
||||
}),
|
||||
onMarkVideoAsWatched: PropTypes.func,
|
||||
onMarkSeasonAsWatched: PropTypes.func,
|
||||
};
|
||||
|
||||
module.exports = Video;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const React = require('react');
|
||||
const classnames = require('classnames');
|
||||
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 { ContinueWatchingItem, EventModal, MainNavBars, MetaItem, MetaRow } = require('stremio/components');
|
||||
const useBoard = require('./useBoard');
|
||||
|
|
@ -14,7 +14,7 @@ const { default: StreamingServerWarning } = require('./StreamingServerWarning');
|
|||
const THRESHOLD = 5;
|
||||
|
||||
const Board = () => {
|
||||
const { t } = useTranslation();
|
||||
const t = useTranslate();
|
||||
const streamingServer = useStreamingServer();
|
||||
const continueWatchingPreview = useContinueWatchingPreview();
|
||||
const [board, loadBoardRows] = useBoard();
|
||||
|
|
@ -55,7 +55,7 @@ const Board = () => {
|
|||
continueWatchingPreview.items.length > 0 ?
|
||||
<MetaRow
|
||||
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}
|
||||
itemComponent={ContinueWatchingItem}
|
||||
notifications={notifications}
|
||||
|
|
@ -94,6 +94,7 @@ const Board = () => {
|
|||
key={index}
|
||||
className={classnames(styles['board-row'], styles['board-row-poster'], 'animation-fade-in')}
|
||||
catalog={catalog}
|
||||
title={t.catalogTitle(catalog)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 0 calc(1.5rem + var(--safe-area-inset-bottom)) 2rem;
|
||||
padding: 0 0 1.5rem 1.5rem;
|
||||
|
||||
.main {
|
||||
flex: auto;
|
||||
|
|
@ -31,12 +31,4 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,11 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.title {
|
||||
flex: none;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
|
|
@ -22,19 +21,22 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.image {
|
||||
flex: none;
|
||||
height: 14rem;
|
||||
margin: 1.5rem 0;
|
||||
.image-container {
|
||||
padding: 1.5rem 0;
|
||||
|
||||
.image {
|
||||
height: 100%;
|
||||
max-height: 14rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.overview {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4rem;
|
||||
margin-bottom: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.point {
|
||||
display: flex;
|
||||
|
|
@ -61,21 +63,47 @@
|
|||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
flex: none;
|
||||
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);
|
||||
.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;
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,16 +112,21 @@
|
|||
.placeholder {
|
||||
padding: 1rem 2rem;
|
||||
|
||||
.image {
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.overview {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.point {
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
.button-container {
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,13 @@ const Placeholder = () => {
|
|||
<div className={styles['title']}>
|
||||
{t('CALENDAR_NOT_LOGGED_IN')}
|
||||
</div>
|
||||
<Image
|
||||
className={styles['image']}
|
||||
src={require('/images/calendar_placeholder.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
<div className={styles['image-container']}>
|
||||
<Image
|
||||
className={styles['image']}
|
||||
src={require('/images/calendar_placeholder.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['overview']}>
|
||||
<div className={styles['point']}>
|
||||
<Icon className={styles['icon']} name={'megaphone'} />
|
||||
|
|
@ -33,9 +35,11 @@ const Placeholder = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className={styles['button']} href={'#/intro?form=login'}>
|
||||
{t('LOG_IN')}
|
||||
</Button>
|
||||
<div className={styles['button-container']}>
|
||||
<Button className={styles['button']} href={'#/intro?form=login'}>
|
||||
{t('LOG_IN')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: @minimum) {
|
||||
@media only screen and (max-width: @xsmall) {
|
||||
.intro-container {
|
||||
justify-content: initial;
|
||||
padding: 3rem 1.5rem;
|
||||
|
|
@ -279,6 +279,21 @@
|
|||
.content-container {
|
||||
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 {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const NotFound = require('stremio/routes/NotFound');
|
||||
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 useSelectableInputs = require('./useSelectableInputs');
|
||||
const styles = require('./styles');
|
||||
|
|
@ -58,7 +59,7 @@ const Library = ({ model, urlParams, queryParams }) => {
|
|||
}, [hasNextPage, loadNextPage]);
|
||||
const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_TRESHOLD);
|
||||
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;
|
||||
}
|
||||
}, [profile.auth, library.selected]);
|
||||
|
|
@ -69,59 +70,48 @@ const Library = ({ model, urlParams, queryParams }) => {
|
|||
}, [typeSelect.selected, library.selected]);
|
||||
return (
|
||||
<MainNavBars className={styles['library-container']} route={model}>
|
||||
<div className={styles['library-content']}>
|
||||
{
|
||||
model === 'continue_watching' || profile.auth !== null ?
|
||||
{
|
||||
profile.auth !== null ?
|
||||
<div className={styles['library-content']}>
|
||||
<div className={styles['selectable-inputs-container']}>
|
||||
<Multiselect {...typeSelect} className={styles['select-input-container']} />
|
||||
<Chips {...sortChips} className={styles['select-input-container']} />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
model === 'library' && profile.auth === null ?
|
||||
<div className={classnames(styles['message-container'], styles['no-user-message-container'])}>
|
||||
<Image
|
||||
className={styles['image']}
|
||||
src={require('/images/anonymous.png')}
|
||||
alt={' '}
|
||||
/>
|
||||
<div className={styles['message-label']}>Library is only available for logged in users!</div>
|
||||
<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>
|
||||
{
|
||||
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>
|
||||
:
|
||||
<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>
|
||||
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.map((libItem, index) => (
|
||||
<LibItem {...libItem} notifications={notifications} removable={model === 'library'} key={index} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<Placeholder />
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
&.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 {
|
||||
flex: none;
|
||||
width: 12rem;
|
||||
|
|
|
|||
|
|
@ -36,17 +36,23 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
|||
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);
|
||||
if (nonSpecialSeasons.length > 0) {
|
||||
return nonSpecialSeasons[nonSpecialSeasons.length - 1];
|
||||
return nonSpecialSeasons[0];
|
||||
}
|
||||
|
||||
if (seasons.length > 0) {
|
||||
return seasons[seasons.length - 1];
|
||||
return seasons[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [seasons, season]);
|
||||
}, [seasons, season, videos, libraryItem]);
|
||||
const videosForSeason = React.useMemo(() => {
|
||||
return videos
|
||||
.filter((video) => {
|
||||
|
|
@ -56,6 +62,11 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
|||
return a.episode - b.episode;
|
||||
});
|
||||
}, [videos, selectedSeason]);
|
||||
|
||||
const seasonWatched = React.useMemo(() => {
|
||||
return videosForSeason.every((video) => video.watched);
|
||||
}, [videosForSeason]);
|
||||
|
||||
const [search, setSearch] = React.useState('');
|
||||
const searchInputOnChange = React.useCallback((event) => {
|
||||
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 (
|
||||
<div className={classnames(className, styles['videos-list-container'])}>
|
||||
{
|
||||
|
|
@ -135,6 +156,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
|||
id={video.id}
|
||||
title={video.title}
|
||||
thumbnail={video.thumbnail}
|
||||
season={video.season}
|
||||
episode={video.episode}
|
||||
released={video.released}
|
||||
upcoming={video.upcoming}
|
||||
|
|
@ -142,7 +164,9 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect,
|
|||
progress={video.progress}
|
||||
deepLinks={video.deepLinks}
|
||||
scheduled={video.scheduled}
|
||||
seasonWatched={seasonWatched}
|
||||
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
||||
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
|||
setSeason(parseInt(event.value));
|
||||
}, []);
|
||||
|
||||
const seasonWatched = React.useMemo(() => {
|
||||
return videos.every((video) => video.watched);
|
||||
}, [videos]);
|
||||
|
||||
const onMarkVideoAsWatched = useCallback((video: Video, watched: boolean) => {
|
||||
core.transport.dispatch({
|
||||
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) => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
|
@ -95,14 +109,17 @@ const SideDrawer = memo(forwardRef<HTMLDivElement, Props>(({ seriesInfo, classNa
|
|||
id={video.id}
|
||||
title={video.title}
|
||||
thumbnail={video.thumbnail}
|
||||
season={video.season}
|
||||
episode={video.episode}
|
||||
released={video.released}
|
||||
upcoming={video.upcoming}
|
||||
watched={video.watched}
|
||||
seasonWatched={seasonWatched}
|
||||
progress={video.progress}
|
||||
deepLinks={video.deepLinks}
|
||||
scheduled={video.scheduled}
|
||||
onMarkVideoAsWatched={onMarkVideoAsWatched}
|
||||
onMarkSeasonAsWatched={onMarkSeasonAsWatched}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
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 { withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
|
||||
const { Image, MainNavBars, MetaItem, MetaRow } = require('stremio/components');
|
||||
|
|
@ -14,7 +14,7 @@ const styles = require('./styles');
|
|||
const THRESHOLD = 100;
|
||||
|
||||
const Search = ({ queryParams }) => {
|
||||
const { t } = useTranslation();
|
||||
const t = useTranslate();
|
||||
const [search, loadSearchRows] = useSearch(queryParams);
|
||||
const query = React.useMemo(() => {
|
||||
return search.selected !== null ?
|
||||
|
|
@ -52,24 +52,24 @@ const Search = ({ queryParams }) => {
|
|||
query === null ?
|
||||
<div className={classnames(styles['search-hints-wrapper'])}>
|
||||
<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 className={classnames(styles['search-hints-container'], 'animation-fade-in')}>
|
||||
<div className={styles['search-hint-container']}>
|
||||
<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 className={styles['search-hint-container']}>
|
||||
<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 className={styles['search-hint-container']}>
|
||||
<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 className={styles['search-hint-container']}>
|
||||
<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>
|
||||
|
|
@ -81,7 +81,7 @@ const Search = ({ queryParams }) => {
|
|||
src={require('/images/empty.png')}
|
||||
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>
|
||||
:
|
||||
search.catalogs.map((catalog, index) => {
|
||||
|
|
@ -115,6 +115,7 @@ const Search = ({ queryParams }) => {
|
|||
key={index}
|
||||
className={classnames(styles['search-row'], styles['search-row-poster'], 'animation-fade-in')}
|
||||
catalog={catalog}
|
||||
title={t.catalogTitle(catalog)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ const Settings = () => {
|
|||
seekTimeDurationSelect,
|
||||
seekShortTimeDurationSelect,
|
||||
escExitFullscreenToggle,
|
||||
quitOnCloseToggle,
|
||||
playInExternalPlayerSelect,
|
||||
nextVideoPopupDurationSelect,
|
||||
bingeWatchingToggle,
|
||||
|
|
@ -322,12 +323,25 @@ const Settings = () => {
|
|||
{...interfaceLanguageSelect}
|
||||
/>
|
||||
</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 ref={playerSectionRef} className={styles['section-container']}>
|
||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>
|
||||
<div className={styles['section-category-container']}>
|
||||
<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 className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,23 @@ const useProfileSettingsInputs = (profile) => {
|
|||
});
|
||||
}
|
||||
}), [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(() => ({
|
||||
options: Object.keys(languageNames).map((code) => ({
|
||||
value: code,
|
||||
|
|
@ -316,6 +333,7 @@ const useProfileSettingsInputs = (profile) => {
|
|||
audioLanguageSelect,
|
||||
surroundSoundToggle,
|
||||
escExitFullscreenToggle,
|
||||
quitOnCloseToggle,
|
||||
seekTimeDurationSelect,
|
||||
seekShortTimeDurationSelect,
|
||||
playInExternalPlayerSelect,
|
||||
|
|
|
|||
22
src/types/global.d.ts
vendored
22
src/types/global.d.ts
vendored
|
|
@ -1,15 +1,31 @@
|
|||
/* eslint-disable no-var */
|
||||
|
||||
type QtTransportMessage = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
interface QtTransport {
|
||||
send: (message: string) => void,
|
||||
onmessage: (message: QtTransportMessage) => void,
|
||||
}
|
||||
|
||||
interface Qt {
|
||||
webChannelTransport: QtTransport,
|
||||
}
|
||||
|
||||
declare global {
|
||||
var qt: Qt | undefined;
|
||||
interface ChromeWebView {
|
||||
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 {};
|
||||
|
|
|
|||
Loading…
Reference in a new issue