mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +00:00
Merge branch 'development' into feat/play-more-streams
This commit is contained in:
commit
a42734f66c
28 changed files with 455 additions and 63 deletions
19
package-lock.json
generated
19
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "stremio",
|
||||
"version": "5.0.0-beta.16",
|
||||
"version": "5.0.0-beta.18",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "stremio",
|
||||
"version": "5.0.0-beta.16",
|
||||
"version": "5.0.0-beta.18",
|
||||
"license": "gpl-2.0",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.26.0",
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"@stremio/stremio-colors": "5.2.0",
|
||||
"@stremio/stremio-core-web": "https://stremio.github.io/stremio-core/stremio-core-web/feat/stream-rar-and-zip-stream-creation/dev/stremio-stremio-core-web-0.48.5.tgz",
|
||||
"@stremio/stremio-icons": "5.4.1",
|
||||
"@stremio/stremio-video": "0.0.48",
|
||||
"@stremio/stremio-video": "0.0.53",
|
||||
"a-color-picker": "1.2.1",
|
||||
"bowser": "2.11.0",
|
||||
"buffer": "6.0.3",
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
"react-i18next": "^15.1.3",
|
||||
"react-is": "18.3.1",
|
||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#62bcc6e8f44258203c7375af59210771efb6f634",
|
||||
"url": "0.11.4",
|
||||
"use-long-press": "^3.2.0"
|
||||
},
|
||||
|
|
@ -3409,10 +3409,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@stremio/stremio-video": {
|
||||
"version": "0.0.48",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.48.tgz",
|
||||
"integrity": "sha512-6ALGXCZC4NPsfhPcrwFWQzvH6UMMRsgSkHetnOhv9WmZ5ubiyUdbBzj9atGiGuuQz8pRcze66ztrub+dsaQbpw==",
|
||||
"license": "MIT",
|
||||
"version": "0.0.53",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.53.tgz",
|
||||
"integrity": "sha512-hSlk8GqMdk4N8VbcdvduYqWVZsQLgHyU7GfFmd1k+t0pSpDKAhI3C6dohG5Sr09CKCjHa8D1rls+CwMNPXLSGw==",
|
||||
"dependencies": {
|
||||
"buffer": "6.0.3",
|
||||
"color": "4.2.3",
|
||||
|
|
@ -13374,8 +13373,8 @@
|
|||
},
|
||||
"node_modules/stremio-translations": {
|
||||
"version": "1.44.9",
|
||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a0f50634202f748a57907b645d2cd92fbaa479dd",
|
||||
"integrity": "sha512-JJpd1JJet3T6/VTNdZ2NZ7uvHJ4zkuyqo5BnTcDGqLVNO/OpicGqKhZjE4WGSgmuhsfPBU8T0ICCfzKu2xpvKg==",
|
||||
"resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#62bcc6e8f44258203c7375af59210771efb6f634",
|
||||
"integrity": "sha512-8Sc5Qvd4IiObwGXkmj1XFXFavUc15My5po6G48HHDBbp42SVc5I/t7h+1yxW1A81byyBCXbL23a9iU9v49vpQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "stremio",
|
||||
"displayName": "Stremio",
|
||||
"version": "5.0.0-beta.16",
|
||||
"version": "5.0.0-beta.18",
|
||||
"author": "Smart Code OOD",
|
||||
"private": true,
|
||||
"license": "gpl-2.0",
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
"@stremio/stremio-colors": "5.2.0",
|
||||
"@stremio/stremio-core-web": "https://stremio.github.io/stremio-core/stremio-core-web/feat/stream-rar-and-zip-stream-creation/dev/stremio-stremio-core-web-0.48.5.tgz",
|
||||
"@stremio/stremio-icons": "5.4.1",
|
||||
"@stremio/stremio-video": "0.0.48",
|
||||
"@stremio/stremio-video": "0.0.53",
|
||||
"a-color-picker": "1.2.1",
|
||||
"bowser": "2.11.0",
|
||||
"buffer": "6.0.3",
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
"react-i18next": "^15.1.3",
|
||||
"react-is": "18.3.1",
|
||||
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd",
|
||||
"stremio-translations": "github:Stremio/stremio-translations#62bcc6e8f44258203c7375af59210771efb6f634",
|
||||
"url": "0.11.4",
|
||||
"use-long-press": "^3.2.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ const { useTranslation } = require('react-i18next');
|
|||
const { Router } = require('stremio-router');
|
||||
const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
|
||||
const { NotFound } = require('stremio/routes');
|
||||
const { PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
|
||||
const { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common');
|
||||
const ServicesToaster = require('./ServicesToaster');
|
||||
const DeepLinkHandler = require('./DeepLinkHandler');
|
||||
const SearchParamsHandler = require('./SearchParamsHandler');
|
||||
const { default: UpdaterBanner } = require('./UpdaterBanner');
|
||||
const ErrorDialog = require('./ErrorDialog');
|
||||
const withProtectedRoutes = require('./withProtectedRoutes');
|
||||
const routerViewsConfig = require('./routerViewsConfig');
|
||||
|
|
@ -165,14 +166,17 @@ const App = () => {
|
|||
<PlatformProvider>
|
||||
<ToastProvider className={styles['toasts-container']}>
|
||||
<TooltipProvider className={styles['tooltip-container']}>
|
||||
<ServicesToaster />
|
||||
<DeepLinkHandler />
|
||||
<SearchParamsHandler />
|
||||
<RouterWithProtectedRoutes
|
||||
className={styles['router']}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
/>
|
||||
<FileDropProvider className={styles['file-drop-container']}>
|
||||
<ServicesToaster />
|
||||
<DeepLinkHandler />
|
||||
<SearchParamsHandler />
|
||||
<UpdaterBanner className={styles['updater-banner-container']} />
|
||||
<RouterWithProtectedRoutes
|
||||
className={styles['router']}
|
||||
viewsConfig={routerViewsConfig}
|
||||
onPathNotMatch={onPathNotMatch}
|
||||
/>
|
||||
</FileDropProvider>
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
</PlatformProvider>
|
||||
|
|
|
|||
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;
|
||||
|
|
@ -202,12 +202,32 @@ html {
|
|||
background-color: var(--modal-background-color);
|
||||
box-shadow: var(--outer-glow);
|
||||
transition: opacity 0.1s ease-out;
|
||||
}
|
||||
|
||||
.file-drop-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 1rem;
|
||||
border: 0.5rem dashed transparent;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.25s ease-out;
|
||||
|
||||
&:global(.active) {
|
||||
transition-delay: 0.25s;
|
||||
border-color: var(--primary-accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.updater-banner-container {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.router {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,16 @@ const ICON_FOR_TYPE = new Map([
|
|||
['other', 'movies'],
|
||||
]);
|
||||
|
||||
const MIME_SIGNATURES = {
|
||||
'application/x-subrip': ['310D0A', '310A'],
|
||||
'text/vtt': ['574542565454'],
|
||||
};
|
||||
|
||||
const SUPPORTED_LOCAL_SUBTITLES = [
|
||||
'application/x-subrip',
|
||||
'text/vtt',
|
||||
];
|
||||
|
||||
const EXTERNAL_PLAYERS = [
|
||||
{
|
||||
label: 'EXTERNAL_PLAYER_DISABLED',
|
||||
|
|
@ -113,6 +123,8 @@ module.exports = {
|
|||
WRITERS_LINK_CATEGORY,
|
||||
TYPE_PRIORITIES,
|
||||
ICON_FOR_TYPE,
|
||||
MIME_SIGNATURES,
|
||||
SUPPORTED_LOCAL_SUBTITLES,
|
||||
EXTERNAL_PLAYERS,
|
||||
WHITELISTED_HOSTS,
|
||||
};
|
||||
|
|
|
|||
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 { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
|
||||
import useShell from './useShell';
|
||||
import useShell from 'stremio/common/useShell';
|
||||
import { name, isMobile } from './device';
|
||||
|
||||
interface PlatformContext {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,19 @@
|
|||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.slide-up-enter {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.slide-up-active {
|
||||
transform: translateY(0%);
|
||||
transition: transform 0.3s cubic-bezier(0.32, 0, 0.67, 0);
|
||||
}
|
||||
|
||||
.slide-up-exit {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@keyframes fade-in-no-motion {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const { FileDropProvider, onFileDrop } = require('./FileDrop');
|
||||
const { PlatformProvider, usePlatform } = require('./Platform');
|
||||
const { ToastProvider, useToast } = require('./Toast');
|
||||
const { TooltipProvider, Tooltip } = require('./Tooltips');
|
||||
|
|
@ -19,11 +20,14 @@ const useModelState = require('./useModelState');
|
|||
const useNotifications = require('./useNotifications');
|
||||
const useOnScrollToBottom = require('./useOnScrollToBottom');
|
||||
const useProfile = require('./useProfile');
|
||||
const { default: useShell } = require('./useShell');
|
||||
const useStreamingServer = require('./useStreamingServer');
|
||||
const useTorrent = require('./useTorrent');
|
||||
const useTranslate = require('./useTranslate');
|
||||
|
||||
module.exports = {
|
||||
FileDropProvider,
|
||||
onFileDrop,
|
||||
PlatformProvider,
|
||||
usePlatform,
|
||||
ToastProvider,
|
||||
|
|
@ -47,6 +51,7 @@ module.exports = {
|
|||
useNotifications,
|
||||
useOnScrollToBottom,
|
||||
useProfile,
|
||||
useShell,
|
||||
useStreamingServer,
|
||||
useTorrent,
|
||||
useTranslate,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ const PropTypes = require('prop-types');
|
|||
const classnames = require('classnames');
|
||||
const AColorPicker = require('a-color-picker');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { Button, ModalDialog } = require('stremio/components');
|
||||
const { Button } = require('stremio/components');
|
||||
const ModalDialog = require('stremio/components/ModalDialog');
|
||||
const useBinaryState = require('stremio/common/useBinaryState');
|
||||
const ColorPicker = require('./ColorPicker');
|
||||
const styles = require('./styles');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
const React = require('react');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { Button, ModalDialog } = require('stremio/components');
|
||||
const { default: Button } = require('stremio/components/Button');
|
||||
const ModalDialog = require('stremio/components/ModalDialog');
|
||||
const useEvents = require('./useEvents');
|
||||
const styles = require('./styles');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
|
|
|
|||
|
|
@ -181,12 +181,17 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
const renderMenu = React.useMemo(() => function renderMenu() {
|
||||
return (
|
||||
<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')}>
|
||||
<Icon className={styles['menu-icon']} name={'play'} />
|
||||
<div className={styles['context-menu-option-label']}>{t('CTX_PLAY')}</div>
|
||||
</Button>
|
||||
{
|
||||
streamLink &&
|
||||
<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>
|
||||
</Button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,12 +111,29 @@
|
|||
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 {
|
||||
max-width: calc(90% - 1.5rem);
|
||||
z-index: 2;
|
||||
|
||||
.context-menu-content {
|
||||
--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 {
|
||||
display: flex;
|
||||
|
|
@ -131,8 +148,9 @@
|
|||
|
||||
.context-menu-option-label {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color:var(--primary-foreground-color);
|
||||
font-weight: 300;
|
||||
color: var(--primary-foreground-color);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ const ControlBar = ({
|
|||
<VolumeSlider
|
||||
className={styles['volume-slider']}
|
||||
volume={volume}
|
||||
muted={muted}
|
||||
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||
/>
|
||||
<div className={styles['spacing']} />
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const { useRouteFocused } = require('stremio-router');
|
|||
const { Slider } = require('stremio/components');
|
||||
const styles = require('./styles');
|
||||
|
||||
const VolumeSlider = ({ className, volume, onVolumeChangeRequested }) => {
|
||||
const VolumeSlider = ({ className, volume, onVolumeChangeRequested, muted }) => {
|
||||
const disabled = volume === null || isNaN(volume);
|
||||
const routeFocused = useRouteFocused();
|
||||
const [slidingVolume, setSlidingVolume] = React.useState(null);
|
||||
|
|
@ -45,7 +45,9 @@ const VolumeSlider = ({ className, volume, onVolumeChangeRequested }) => {
|
|||
className={classnames(className, styles['volume-slider'], { 'active': slidingVolume !== null })}
|
||||
value={
|
||||
!disabled ?
|
||||
slidingVolume !== null ? slidingVolume : volume
|
||||
!muted ?
|
||||
slidingVolume !== null ? slidingVolume : volume
|
||||
: 0
|
||||
:
|
||||
100
|
||||
}
|
||||
|
|
@ -61,7 +63,8 @@ const VolumeSlider = ({ className, volume, onVolumeChangeRequested }) => {
|
|||
VolumeSlider.propTypes = {
|
||||
className: PropTypes.string,
|
||||
volume: PropTypes.number,
|
||||
onVolumeChangeRequested: PropTypes.func
|
||||
onVolumeChangeRequested: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
};
|
||||
|
||||
module.exports = VolumeSlider;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const langs = require('langs');
|
|||
const { useTranslation } = require('react-i18next');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const { useServices } = require('stremio/services');
|
||||
const { useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
|
||||
const { onFileDrop, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS } = require('stremio/common');
|
||||
const { HorizontalNavBar, Transition } = require('stremio/components');
|
||||
const BufferingLoader = require('./BufferingLoader');
|
||||
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
|
||||
|
|
@ -133,11 +133,20 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
toast.show({
|
||||
type: 'success',
|
||||
title: t('PLAYER_SUBTITLES_LOADED'),
|
||||
message: track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') : t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
|
||||
message:
|
||||
track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') :
|
||||
track.local ? t('PLAYER_SUBTITLES_LOADED_LOCAL') :
|
||||
t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
|
||||
timeout: 3000
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onExtraSubtitlesTrackAdded = React.useCallback((track) => {
|
||||
if (track.local) {
|
||||
video.setExtraSubtitlesTrack(track.id);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onPlayRequested = React.useCallback(() => {
|
||||
video.setProp('paused', false);
|
||||
setSeeking(false);
|
||||
|
|
@ -172,13 +181,11 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
}, []);
|
||||
|
||||
const onSubtitlesTrackSelected = React.useCallback((id) => {
|
||||
video.setProp('selectedSubtitlesTrackId', id);
|
||||
video.setProp('selectedExtraSubtitlesTrackId', null);
|
||||
video.setSubtitlesTrack(id);
|
||||
}, []);
|
||||
|
||||
const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
|
||||
video.setProp('selectedSubtitlesTrackId', null);
|
||||
video.setProp('selectedExtraSubtitlesTrackId', id);
|
||||
video.setExtraSubtitlesTrack(id);
|
||||
}, []);
|
||||
|
||||
const onAudioTrackSelected = React.useCallback((id) => {
|
||||
|
|
@ -270,6 +277,10 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
event.nativeEvent.immersePrevented = true;
|
||||
}, []);
|
||||
|
||||
onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, async (filename, buffer) => {
|
||||
video.addLocalSubtitles(filename, buffer);
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
setError(null);
|
||||
video.unload();
|
||||
|
|
@ -295,6 +306,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
0,
|
||||
forceTranscoding: forceTranscoding || casting,
|
||||
maxAudioChannels: settings.surroundSound ? 32 : 2,
|
||||
hardwareDecoding: settings.hardwareDecoding,
|
||||
streamingServerURL: streamingServer.baseUrl ?
|
||||
casting ?
|
||||
streamingServer.baseUrl
|
||||
|
|
@ -302,7 +314,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
streamingServer.selected.transportUrl
|
||||
:
|
||||
null,
|
||||
seriesInfo: player.seriesInfo
|
||||
seriesInfo: player.seriesInfo,
|
||||
}, {
|
||||
chromecastTransport: chromecast.active ? chromecast.transport : null,
|
||||
shellTransport: shell.active ? shell.transport : null,
|
||||
|
|
@ -585,6 +597,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
video.events.on('ended', onEnded);
|
||||
video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
||||
video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
||||
video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
||||
video.events.on('implementationChanged', onImplementationChanged);
|
||||
|
||||
return () => {
|
||||
|
|
@ -592,6 +605,7 @@ const Player = ({ urlParams, queryParams }) => {
|
|||
video.events.off('ended', onEnded);
|
||||
video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
||||
video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
||||
video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
||||
video.events.off('implementationChanged', onImplementationChanged);
|
||||
};
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ const styles = require('./styles');
|
|||
const { t } = require('i18next');
|
||||
|
||||
const ORIGIN_PRIORITIES = {
|
||||
'LOCAL': 3,
|
||||
'EMBEDDED': 2,
|
||||
'EXCLUSIVE': 1
|
||||
'EXCLUSIVE': 1,
|
||||
};
|
||||
const LANGUAGE_PRIORITIES = {
|
||||
'eng': 1
|
||||
'local': 2,
|
||||
'eng': 1,
|
||||
};
|
||||
|
||||
const SubtitlesMenu = React.memo((props) => {
|
||||
|
|
@ -161,7 +163,11 @@ const SubtitlesMenu = React.memo((props) => {
|
|||
</Button>
|
||||
{subtitlesLanguages.map((lang, index) => (
|
||||
<Button key={index} title={languages.label(lang)} className={classnames(styles['language-option'], { 'selected': selectedSubtitlesLanguage === lang })} data-lang={lang} onClick={subtitlesLanguageOnClick}>
|
||||
<div className={styles['language-label']}>{languages.label(lang)}</div>
|
||||
<div className={styles['language-label']}>
|
||||
{
|
||||
lang === 'local' ? t('LOCAL') : languages.label(lang)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
selectedSubtitlesLanguage === lang ?
|
||||
<div className={styles['icon']} />
|
||||
|
|
|
|||
|
|
@ -79,10 +79,31 @@ const useVideo = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const addLocalSubtitles = (filename, buffer) => {
|
||||
dispatch({
|
||||
type: 'command',
|
||||
commandName: 'addLocalSubtitles',
|
||||
commandArgs: {
|
||||
filename,
|
||||
buffer,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const setProp = (name, value) => {
|
||||
dispatch({ type: 'setProp', propName: name, propValue: value });
|
||||
};
|
||||
|
||||
const setSubtitlesTrack = (id) => {
|
||||
setProp('selectedSubtitlesTrackId', id);
|
||||
setProp('selectedExtraSubtitlesTrackId', null);
|
||||
};
|
||||
|
||||
const setExtraSubtitlesTrack = (id) => {
|
||||
setProp('selectedSubtitlesTrackId', null);
|
||||
setProp('selectedExtraSubtitlesTrackId', id);
|
||||
};
|
||||
|
||||
const onError = (error) => {
|
||||
events.emit('error', error);
|
||||
};
|
||||
|
|
@ -99,6 +120,10 @@ const useVideo = () => {
|
|||
events.emit('extraSubtitlesTrackLoaded', track);
|
||||
};
|
||||
|
||||
const onExtraSubtitlesTrackAdded = (track) => {
|
||||
events.emit('extraSubtitlesTrackAdded', track);
|
||||
};
|
||||
|
||||
const onPropChanged = (name, value) => {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
|
|
@ -125,6 +150,7 @@ const useVideo = () => {
|
|||
video.current.on('implementationChanged', onImplementationChanged);
|
||||
video.current.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
||||
video.current.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
||||
video.current.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
||||
|
||||
return () => video.current.destroy();
|
||||
}, []);
|
||||
|
|
@ -136,7 +162,10 @@ const useVideo = () => {
|
|||
load,
|
||||
unload,
|
||||
addExtraSubtitlesTracks,
|
||||
addLocalSubtitles,
|
||||
setProp,
|
||||
setSubtitlesTrack,
|
||||
setExtraSubtitlesTrack,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -187,6 +187,12 @@ const Settings = () => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof shell?.transport?.props?.shellVersion === 'string' ?
|
||||
<div className={styles['version-info-label']} title={shell.transport.props.shellVersion}>Shell Version: {shell.transport.props.shellVersion}</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div ref={sectionsContainerRef} className={styles['sections-container']} onScroll={sectionsContainerOnScroll}>
|
||||
<div ref={generalSectionRef} className={styles['section-container']}>
|
||||
|
|
@ -482,17 +488,19 @@ const Settings = () => {
|
|||
{...playInExternalPlayerSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
disabled={true}
|
||||
tabIndex={-1}
|
||||
{...hardwareDecodingToggle}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
shell.active &&
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
|
||||
</div>
|
||||
<Toggle
|
||||
className={classnames(styles['option-input-container'], styles['toggle-container'])}
|
||||
tabIndex={-1}
|
||||
{...hardwareDecodingToggle}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div ref={streamingServerSectionRef} className={styles['section-container']}>
|
||||
<div className={styles['section-title']}>{ t('SETTINGS_NAV_STREAMING') }</div>
|
||||
|
|
@ -717,6 +725,23 @@ const Settings = () => {
|
|||
:
|
||||
null
|
||||
}
|
||||
{
|
||||
typeof shell?.transport?.props?.shellVersion === 'string' ?
|
||||
<div className={styles['option-container']}>
|
||||
<div className={styles['option-name-container']}>
|
||||
<div className={styles['label']}>
|
||||
Shell Version
|
||||
</div>
|
||||
</div>
|
||||
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
|
||||
<div className={styles['label']}>
|
||||
{ shell.transport.props.shellVersion }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ function DragAndDrop({ core }) {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'application/x-subrip':
|
||||
break;
|
||||
case 'text/vtt':
|
||||
break;
|
||||
case '':
|
||||
break;
|
||||
default: {
|
||||
events.emit('error', {
|
||||
message: 'Unsupported file',
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ module.exports = (env, argv) => ({
|
|||
exclude: /node_modules/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: `${COMMIT_HASH}/images/[name][ext][query]`
|
||||
filename: 'images/[name][ext][query]'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -231,9 +231,9 @@ module.exports = (env, argv) => ({
|
|||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: 'favicons', to: `${COMMIT_HASH}/favicons` },
|
||||
{ from: 'images', to: `${COMMIT_HASH}/images` },
|
||||
{ from: 'screenshots/*.webp', to: `${COMMIT_HASH}` },
|
||||
{ from: 'favicons', to: 'favicons' },
|
||||
{ from: 'images', to: 'images' },
|
||||
{ from: 'screenshots/*.webp', to: './' },
|
||||
]
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
|
|
@ -243,8 +243,8 @@ module.exports = (env, argv) => ({
|
|||
template: './src/index.html',
|
||||
inject: false,
|
||||
scriptLoading: 'blocking',
|
||||
faviconsPath: `${COMMIT_HASH}/favicons`,
|
||||
imagesPath: `${COMMIT_HASH}/images`,
|
||||
faviconsPath: 'favicons',
|
||||
imagesPath: 'images',
|
||||
}),
|
||||
new WebpackPwaManifest({
|
||||
name: 'Stremio Web',
|
||||
|
|
@ -261,33 +261,33 @@ module.exports = (env, argv) => ({
|
|||
icons: [
|
||||
{
|
||||
src: 'images/icon.png',
|
||||
destination: `${COMMIT_HASH}/icons`,
|
||||
destination: 'icons',
|
||||
sizes: [196, 512],
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: 'images/maskable_icon.png',
|
||||
destination: `${COMMIT_HASH}/maskable_icons`,
|
||||
destination: 'maskable_icons',
|
||||
sizes: [196, 512],
|
||||
purpose: 'maskable',
|
||||
ios: true
|
||||
},
|
||||
{
|
||||
src: 'favicons/favicon.ico',
|
||||
destination: `${COMMIT_HASH}/favicons`,
|
||||
destination: 'favicons',
|
||||
sizes: [256],
|
||||
}
|
||||
],
|
||||
screenshots : [
|
||||
{
|
||||
src: `${COMMIT_HASH}/screenshots/board_wide.webp`,
|
||||
src: 'screenshots/board_wide.webp',
|
||||
sizes: '1440x900',
|
||||
type: 'image/webp',
|
||||
form_factor: 'wide',
|
||||
label: 'Homescreen of Stremio'
|
||||
},
|
||||
{
|
||||
src: `${COMMIT_HASH}/screenshots/board_narrow.webp`,
|
||||
src: 'screenshots/board_narrow.webp',
|
||||
sizes: '414x896',
|
||||
type: 'image/webp',
|
||||
form_factor: 'narrow',
|
||||
|
|
|
|||
Loading…
Reference in a new issue