diff --git a/package-lock.json b/package-lock.json index d0c7ad763..642d0ea73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stremio", - "version": "5.0.0-beta.17", + "version": "5.0.0-beta.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.17", + "version": "5.0.0-beta.18", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", @@ -14,7 +14,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.48.5", "@stremio/stremio-icons": "5.4.1", - "@stremio/stremio-video": "0.0.52", + "@stremio/stremio-video": "0.0.53", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -36,7 +36,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd", + "stremio-translations": "github:Stremio/stremio-translations#62bcc6e8f44258203c7375af59210771efb6f634", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -3409,10 +3409,9 @@ ] }, "node_modules/@stremio/stremio-video": { - "version": "0.0.52", - "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.52.tgz", - "integrity": "sha512-OlHC8FIvYEyGXcNAM4W044Dqx6CmGb5BV3fDU361SyUjO9gKXXUWdL7LwmwHeWFeuy2sK1MEg4AT2JPptvJ0rg==", - "license": "MIT", + "version": "0.0.53", + "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.53.tgz", + "integrity": "sha512-hSlk8GqMdk4N8VbcdvduYqWVZsQLgHyU7GfFmd1k+t0pSpDKAhI3C6dohG5Sr09CKCjHa8D1rls+CwMNPXLSGw==", "dependencies": { "buffer": "6.0.3", "color": "4.2.3", @@ -13374,8 +13373,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.9", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#a0f50634202f748a57907b645d2cd92fbaa479dd", - "integrity": "sha512-JJpd1JJet3T6/VTNdZ2NZ7uvHJ4zkuyqo5BnTcDGqLVNO/OpicGqKhZjE4WGSgmuhsfPBU8T0ICCfzKu2xpvKg==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#62bcc6e8f44258203c7375af59210771efb6f634", + "integrity": "sha512-8Sc5Qvd4IiObwGXkmj1XFXFavUc15My5po6G48HHDBbp42SVc5I/t7h+1yxW1A81byyBCXbL23a9iU9v49vpQA==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index e80305bc9..9ccb0cee8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.17", + "version": "5.0.0-beta.18", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", @@ -18,7 +18,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.48.5", "@stremio/stremio-icons": "5.4.1", - "@stremio/stremio-video": "0.0.52", + "@stremio/stremio-video": "0.0.53", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -40,7 +40,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#a0f50634202f748a57907b645d2cd92fbaa479dd", + "stremio-translations": "github:Stremio/stremio-translations#62bcc6e8f44258203c7375af59210771efb6f634", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/src/App/App.js b/src/App/App.js index 730e75f28..6dc2d6e0b 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -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 = () => { - - - - + + + + + + + diff --git a/src/App/UpdaterBanner/UpdaterBanner.less b/src/App/UpdaterBanner/UpdaterBanner.less new file mode 100644 index 000000000..9928fb493 --- /dev/null +++ b/src/App/UpdaterBanner/UpdaterBanner.less @@ -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; + } + } +} \ No newline at end of file diff --git a/src/App/UpdaterBanner/UpdaterBanner.tsx b/src/App/UpdaterBanner/UpdaterBanner.tsx new file mode 100644 index 000000000..3d421deb5 --- /dev/null +++ b/src/App/UpdaterBanner/UpdaterBanner.tsx @@ -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 ( +
+ +
+
+ { t('UPDATER_TITLE') } +
+ + +
+
+
+ ); +}; + +export default UpdaterBanner; diff --git a/src/App/UpdaterBanner/index.ts b/src/App/UpdaterBanner/index.ts new file mode 100644 index 000000000..e4306ecb2 --- /dev/null +++ b/src/App/UpdaterBanner/index.ts @@ -0,0 +1,2 @@ +import UpdaterBanner from './UpdaterBanner'; +export default UpdaterBanner; diff --git a/src/App/styles.less b/src/App/styles.less index 7e44a9643..50819f883 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -204,12 +204,32 @@ html { background-color: var(--modal-background-color); box-shadow: var(--outer-glow); transition: opacity 0.1s ease-out; + } + + .file-drop-container { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + border-radius: 1rem; + border: 0.5rem dashed transparent; + pointer-events: none; + transition: border-color 0.25s ease-out; &:global(.active) { - transition-delay: 0.25s; + border-color: var(--primary-accent-color); } } + .updater-banner-container { + z-index: 1; + position: absolute; + left: 0; + right: 0; + bottom: 0; + } + .router { width: 100%; height: 100%; diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index 727c152fa..8e4e3efdc 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -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, }; diff --git a/src/common/FileDrop/FileDrop.tsx b/src/common/FileDrop/FileDrop.tsx new file mode 100644 index 000000000..aae4e146b --- /dev/null +++ b/src/common/FileDrop/FileDrop.tsx @@ -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 ( + + { children } +
+ + ); +}; + +const useFileDrop = () => { + return useContext(FileDropContext); +}; + +export { + FileDropProvider, + useFileDrop, +}; diff --git a/src/common/FileDrop/index.ts b/src/common/FileDrop/index.ts new file mode 100644 index 000000000..41acfbfd7 --- /dev/null +++ b/src/common/FileDrop/index.ts @@ -0,0 +1,8 @@ +import { FileDropProvider, useFileDrop } from './FileDrop'; +import onFileDrop from './onFileDrop'; + +export { + FileDropProvider, + useFileDrop, + onFileDrop, +}; diff --git a/src/common/FileDrop/onFileDrop.ts b/src/common/FileDrop/onFileDrop.ts new file mode 100644 index 000000000..339b0219b --- /dev/null +++ b/src/common/FileDrop/onFileDrop.ts @@ -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; diff --git a/src/common/FileDrop/utils.ts b/src/common/FileDrop/utils.ts new file mode 100644 index 000000000..f9996aed3 --- /dev/null +++ b/src/common/FileDrop/utils.ts @@ -0,0 +1,19 @@ +import { MIME_SIGNATURES } from 'stremio/common/CONSTANTS'; + +const SIGNATURES = MIME_SIGNATURES as Record; + +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, +}; diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx index 41e74b24f..2212303e4 100644 --- a/src/common/Platform/Platform.tsx +++ b/src/common/Platform/Platform.tsx @@ -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 { diff --git a/src/common/animations.less b/src/common/animations.less index c7a30d2fb..91dbe386d 100644 --- a/src/common/animations.less +++ b/src/common/animations.less @@ -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; diff --git a/src/common/index.js b/src/common/index.js index 4c514dfbe..4acf8b056 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -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, diff --git a/src/common/Platform/useShell.ts b/src/common/useShell.ts similarity index 100% rename from src/common/Platform/useShell.ts rename to src/common/useShell.ts diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index f4b059bd3..f97e1ba82 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -8,6 +8,7 @@ import styles from './Button.less'; type Props = { className?: string, href?: string, + target?: string title?: string, disabled?: boolean, tabIndex?: number, diff --git a/src/components/Checkbox/Checkbox.less b/src/components/Checkbox/Checkbox.less new file mode 100644 index 000000000..718a7b129 --- /dev/null +++ b/src/components/Checkbox/Checkbox.less @@ -0,0 +1,83 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.checkbox { + display: flex; + align-items: center; + overflow: visible; + + .label { + display: flex; + flex-direction: row; + align-items: center; + padding: 0.5rem 0; + cursor: pointer; + + span { + font-size: 0.9rem; + color: var(--primary-foreground-color); + opacity: 0.6; + } + + .link { + font-size: 0.9rem; + color: var(--primary-accent-color); + + &:hover { + text-decoration: underline; + } + } + } + + .checkbox-container { + position: relative; + width: 1.5rem; + height: 1.5rem; + border-radius: 0.3rem; + background-color: var(--overlay-color); + padding: 0.1rem; + display: flex; + flex: none; + margin: 0 1rem 0 0.3rem; + align-items: center; + justify-content: center; + transition: all 0.2s ease-in-out; + cursor: pointer; + outline: none; + user-select: none; + outline-width: var(--focus-outline-size); + outline-color: @color-surface-light5; + outline-offset: 2px; + + input[type='checkbox'] { + opacity: 0; + width: 0; + height: 0; + position: absolute; + cursor: pointer; + } + + .checkbox-icon { + width: 100%; + height: 100%; + color: var(--primary-foreground-color); + } + + &.disabled { + cursor: not-allowed; + } + + &.error { + border-color: var(--color-trakt); + } + + &.checked { + background-color: var(--primary-accent-color); + } + + &:hover, &:focus { + outline-style: solid; + } + } +} diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 000000000..da4ae33eb --- /dev/null +++ b/src/components/Checkbox/Checkbox.tsx @@ -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; + name: string; + disabled?: boolean; + checked?: boolean; + className?: string; + label?: string; + link?: string; + href?: string; + onChange?: (props: { + type: string; + checked: boolean; + reactEvent: KeyboardEvent | ChangeEvent; + nativeEvent: Event; + }) => void; + error?: string; +}; + +const Checkbox = React.forwardRef(({ name, disabled, className, label, href, link, onChange, error, checked }, ref) => { + + const handleSelect = useCallback((event: ChangeEvent) => { + if (!disabled && onChange) { + onChange({ + type: 'select', + checked: event.target.checked, + reactEvent: event, + nativeEvent: event.nativeEvent, + }); + } + }, [disabled, onChange]); + + const onKeyDown = useCallback((event: KeyboardEvent) => { + if ((event.key === 'Enter' || event.key === ' ') && !disabled) { + onChange && onChange({ + type: 'select', + checked: !checked, + reactEvent: event as KeyboardEvent, + nativeEvent: event.nativeEvent, + }); + } + }, [disabled, checked, onChange]); + + return ( +
+ +
+ ); +}); + +export default Checkbox; diff --git a/src/components/Checkbox/index.ts b/src/components/Checkbox/index.ts new file mode 100644 index 000000000..fa5739580 --- /dev/null +++ b/src/components/Checkbox/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import Checkbox from './Checkbox'; + +export default Checkbox; diff --git a/src/components/EventModal/EventModal.js b/src/components/EventModal/EventModal.js index 917190ba1..f4e46b199 100644 --- a/src/components/EventModal/EventModal.js +++ b/src/components/EventModal/EventModal.js @@ -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'); diff --git a/src/components/NavBar/VerticalNavBar/styles.less b/src/components/NavBar/VerticalNavBar/styles.less index 3a6ae76d8..7d17d6054 100644 --- a/src/components/NavBar/VerticalNavBar/styles.less +++ b/src/components/NavBar/VerticalNavBar/styles.less @@ -13,6 +13,7 @@ background-color: transparent; overflow-y: auto; scrollbar-width: none; + -ms-overflow-style: none; &::-webkit-scrollbar { display: none; @@ -21,6 +22,7 @@ .nav-tab-button { width: calc(var(--vertical-nav-bar-size) - 1.2rem); height: calc(var(--vertical-nav-bar-size) - 1.2rem); + min-height: 3.5rem; } } diff --git a/src/components/Slider/styles.less b/src/components/Slider/styles.less index 1c22685c8..3acf79e62 100644 --- a/src/components/Slider/styles.less +++ b/src/components/Slider/styles.less @@ -39,7 +39,8 @@ html.active-slider-within { flex: 1; height: var(--track-size); border-radius: var(--track-size); - background-color: var(--overlay-color); + background-color: var(--primary-accent-color); + opacity: 0.2; &.audio-boost { background: linear-gradient(to right, diff --git a/src/components/index.ts b/src/components/index.ts index f65d66f81..7ef75f888 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,7 @@ import AddonDetailsModal from './AddonDetailsModal'; import BottomSheet from './BottomSheet'; import Button from './Button'; +import Checkbox from './Checkbox'; import Chips from './Chips'; import ColorInput from './ColorInput'; import ContinueWatchingItem from './ContinueWatchingItem'; @@ -31,6 +32,7 @@ export { AddonDetailsModal, BottomSheet, Button, + Checkbox, Chips, ColorInput, ContinueWatchingItem, diff --git a/src/routes/Calendar/List/Item/Item.less b/src/routes/Calendar/List/Item/Item.less index aba6bba09..b11feda2a 100644 --- a/src/routes/Calendar/List/Item/Item.less +++ b/src/routes/Calendar/List/Item/Item.less @@ -95,4 +95,4 @@ &:not(.active):hover { border-color: var(--overlay-color); } -} +} \ No newline at end of file diff --git a/src/routes/Calendar/List/List.less b/src/routes/Calendar/List/List.less index f63078680..9f2dfd774 100644 --- a/src/routes/Calendar/List/List.less +++ b/src/routes/Calendar/List/List.less @@ -10,6 +10,10 @@ width: 20rem; padding: 0 1rem; overflow-y: auto; + + @supports (scroll-padding-block-start: 0.15rem) { + scroll-padding-block-start: 0.15rem; + } } @media only screen and (max-width: @small) and (orientation: portrait) { @@ -34,4 +38,4 @@ .list { display: none; } -} +} \ No newline at end of file diff --git a/src/routes/Intro/ConsentToggle/ConsentToggle.js b/src/routes/Intro/ConsentToggle/ConsentToggle.js deleted file mode 100644 index 9a0210607..000000000 --- a/src/routes/Intro/ConsentToggle/ConsentToggle.js +++ /dev/null @@ -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 ( - -
- {label} - {' '} - { - typeof link === 'string' && link.length > 0 && typeof href === 'string' && href.length > 0 ? - - : - null - } -
-
- ); -}); - -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; diff --git a/src/routes/Intro/ConsentToggle/index.js b/src/routes/Intro/ConsentToggle/index.js deleted file mode 100644 index 8edfe4a27..000000000 --- a/src/routes/Intro/ConsentToggle/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const ConsentToggle = require('./ConsentToggle'); - -module.exports = ConsentToggle; diff --git a/src/routes/Intro/ConsentToggle/styles.less b/src/routes/Intro/ConsentToggle/styles.less deleted file mode 100644 index e8229e244..000000000 --- a/src/routes/Intro/ConsentToggle/styles.less +++ /dev/null @@ -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; - } - } - } -} \ No newline at end of file diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index fc98fe5cf..989e6febd 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -8,9 +8,8 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { Modal, useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); const { useBinaryState } = require('stremio/common'); -const { Button, Image } = require('stremio/components'); +const { Button, Image, Checkbox } = require('stremio/components'); const CredentialsTextInput = require('./CredentialsTextInput'); -const ConsentToggle = require('./ConsentToggle'); const PasswordResetModal = require('./PasswordResetModal'); const useFacebookLogin = require('./useFacebookLogin'); const styles = require('./styles'); @@ -308,30 +307,27 @@ const Intro = ({ queryParams }) => { onChange={confirmPasswordOnChange} onSubmit={confirmPasswordOnSubmit} /> - - - : diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 1df52a9ac..c491fde7e 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,7 +8,7 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common'); +const { onFileDrop, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS } = require('stremio/common'); const { HorizontalNavBar, Transition } = require('stremio/components'); const BufferingLoader = require('./BufferingLoader'); const VolumeChangeIndicator = require('./VolumeChangeIndicator'); @@ -133,11 +133,20 @@ const Player = ({ urlParams, queryParams }) => { toast.show({ type: 'success', title: t('PLAYER_SUBTITLES_LOADED'), - message: track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') : t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }), + message: + track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') : + track.local ? t('PLAYER_SUBTITLES_LOADED_LOCAL') : + t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }), timeout: 3000 }); }, []); + const onExtraSubtitlesTrackAdded = React.useCallback((track) => { + if (track.local) { + video.setExtraSubtitlesTrack(track.id); + } + }, []); + const onPlayRequested = React.useCallback(() => { video.setProp('paused', false); setSeeking(false); @@ -172,13 +181,11 @@ const Player = ({ urlParams, queryParams }) => { }, []); const onSubtitlesTrackSelected = React.useCallback((id) => { - video.setProp('selectedSubtitlesTrackId', id); - video.setProp('selectedExtraSubtitlesTrackId', null); + video.setSubtitlesTrack(id); }, []); const onExtraSubtitlesTrackSelected = React.useCallback((id) => { - video.setProp('selectedSubtitlesTrackId', null); - video.setProp('selectedExtraSubtitlesTrackId', id); + video.setExtraSubtitlesTrack(id); }, []); const onAudioTrackSelected = React.useCallback((id) => { @@ -270,6 +277,10 @@ const Player = ({ urlParams, queryParams }) => { event.nativeEvent.immersePrevented = true; }, []); + onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, async (filename, buffer) => { + video.addLocalSubtitles(filename, buffer); + }); + React.useEffect(() => { setError(null); video.unload(); @@ -296,6 +307,7 @@ const Player = ({ urlParams, queryParams }) => { 0, forceTranscoding: forceTranscoding || casting, maxAudioChannels: settings.surroundSound ? 32 : 2, + hardwareDecoding: settings.hardwareDecoding, streamingServerURL: streamingServer.baseUrl ? casting ? streamingServer.baseUrl @@ -303,7 +315,7 @@ const Player = ({ urlParams, queryParams }) => { streamingServer.selected.transportUrl : null, - seriesInfo: player.seriesInfo + seriesInfo: player.seriesInfo, }, { chromecastTransport: chromecast.active ? chromecast.transport : null, shellTransport: shell.active ? shell.transport : null, @@ -586,6 +598,7 @@ const Player = ({ urlParams, queryParams }) => { video.events.on('ended', onEnded); video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded); video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); + video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); video.events.on('implementationChanged', onImplementationChanged); return () => { @@ -593,6 +606,7 @@ const Player = ({ urlParams, queryParams }) => { video.events.off('ended', onEnded); video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded); video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); + video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); video.events.off('implementationChanged', onImplementationChanged); }; }, []); diff --git a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js index 44cd20864..39bc771e6 100644 --- a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js +++ b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js @@ -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) => { {subtitlesLanguages.map((lang, index) => (
); diff --git a/src/routes/Player/SubtitlesMenu/styles.less b/src/routes/Player/SubtitlesMenu/styles.less index d47d6ab75..71f1d5cb4 100644 --- a/src/routes/Player/SubtitlesMenu/styles.less +++ b/src/routes/Player/SubtitlesMenu/styles.less @@ -106,6 +106,10 @@ .subtitles-settings-container { width: 17rem; + .settings-list { + overflow-y: scroll; + } + .spacing { flex: 1; } diff --git a/src/routes/Player/useVideo.js b/src/routes/Player/useVideo.js index 6a59a12fb..20698ac09 100644 --- a/src/routes/Player/useVideo.js +++ b/src/routes/Player/useVideo.js @@ -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, }; }; diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 24eaaaf5d..6ad15163a 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -488,17 +488,19 @@ const Settings = () => { {...playInExternalPlayerSelect} /> -
-
-
{ t('SETTINGS_HWDEC') }
-
- -
+ { + shell.active && +
+
+
{ t('SETTINGS_HWDEC') }
+
+ +
+ }
{ t('SETTINGS_NAV_STREAMING') }
diff --git a/src/services/DragAndDrop/DragAndDrop.js b/src/services/DragAndDrop/DragAndDrop.js index bc54e2f35..1538c328e 100644 --- a/src/services/DragAndDrop/DragAndDrop.js +++ b/src/services/DragAndDrop/DragAndDrop.js @@ -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',