diff --git a/package-lock.json b/package-lock.json index 73dc115fe..7e0bba17f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@babel/runtime": "7.16.0", "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "4.0.1", - "@stremio/stremio-core-web": "0.44.5", + "@stremio/stremio-core-web": "0.44.6", "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.23", "a-color-picker": "1.2.1", @@ -2617,9 +2617,9 @@ "integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw==" }, "node_modules/@stremio/stremio-core-web": { - "version": "0.44.5", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.5.tgz", - "integrity": "sha512-egKYHD7h8Q5CcybT5RcCGpcJDT13TVbW3fyUjcwv4McvOGWRW3iO8RMaZt/ZWbf10tXSaGA2rbmJ52tjwGWqyA==", + "version": "0.44.6", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.6.tgz", + "integrity": "sha512-Mxc6oRKgTuXU80JEacJIe4TphccZUJkyHTMUZnUx9sotVetGX+EJsyvr+HLKNMDGJHx5xcwGT/BUikdyQR/Lpw==", "dependencies": { "@babel/runtime": "7.16.0" } @@ -15757,9 +15757,9 @@ "integrity": "sha512-yT3No1gIWKLV2BhQIeSgG94EzXxmEqXJLulO+pFpziqWNUbmmEKeE+nRvW5wtoIK4SLy+v0bLd0b6HBH3KFfWw==" }, "@stremio/stremio-core-web": { - "version": "0.44.5", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.5.tgz", - "integrity": "sha512-egKYHD7h8Q5CcybT5RcCGpcJDT13TVbW3fyUjcwv4McvOGWRW3iO8RMaZt/ZWbf10tXSaGA2rbmJ52tjwGWqyA==", + "version": "0.44.6", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.6.tgz", + "integrity": "sha512-Mxc6oRKgTuXU80JEacJIe4TphccZUJkyHTMUZnUx9sotVetGX+EJsyvr+HLKNMDGJHx5xcwGT/BUikdyQR/Lpw==", "requires": { "@babel/runtime": "7.16.0" } diff --git a/package.json b/package.json index 034c65757..371aa3e90 100755 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@babel/runtime": "7.16.0", "@sentry/browser": "6.13.3", "@stremio/stremio-colors": "4.0.1", - "@stremio/stremio-core-web": "0.44.5", + "@stremio/stremio-core-web": "0.44.6", "@stremio/stremio-icons": "4.0.0", "@stremio/stremio-video": "0.0.23", "a-color-picker": "1.2.1", diff --git a/src/App/App.js b/src/App/App.js index e112a291a..c0a79b727 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -103,6 +103,12 @@ const App = () => { action: 'PullUserFromAPI' } }); + services.core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'SyncLibraryWithAPI' + } + }); } }, [initialized]); return ( diff --git a/src/App/ServicesToaster.js b/src/App/ServicesToaster.js index da1a654af..43b076c9a 100644 --- a/src/App/ServicesToaster.js +++ b/src/App/ServicesToaster.js @@ -15,11 +15,18 @@ const ServicesToaster = () => { break; } + if (args.source.event === 'LibrarySyncWithAPIPlanned' && args.source.args.uid === null) { + break; + } + toast.show({ type: 'error', title: args.source.event, message: args.error.message, - timeout: 4000 + timeout: 4000, + dataset: { + type: 'CoreEvent' + } }); break; } diff --git a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index 7dce73962..efcef4feb 100644 --- a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -8,12 +8,14 @@ const { useServices } = require('stremio/services'); const Button = require('stremio/common/Button'); const useFullscreen = require('stremio/common/useFullscreen'); const useProfile = require('stremio/common/useProfile'); +const useTorrent = require('stremio/common/useTorrent'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const styles = require('./styles'); const NavMenuContent = ({ onClick }) => { const { core } = useServices(); const profile = useProfile(); + const { createTorrentFromMagnet } = useTorrent(); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); const logoutButtonOnClick = React.useCallback(() => { core.transport.dispatch({ @@ -23,6 +25,14 @@ const NavMenuContent = ({ onClick }) => { } }); }, []); + const onPlayMagnetLinkClick = React.useCallback(async () => { + try { + const clipboardText = await navigator.clipboard.readText(); + createTorrentFromMagnet(clipboardText); + } catch(e) { + console.error(e); + } + }, []); return (
@@ -57,11 +67,7 @@ const NavMenuContent = ({ onClick }) => {
Addons
- - diff --git a/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js b/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js index 2bc3a657b..dd76c8e84 100644 --- a/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js +++ b/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js @@ -3,17 +3,17 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const magnet = require('magnet-uri'); -const { useServices } = require('stremio/services'); const Icon = require('@stremio/stremio-icons/dom'); const { useRouteFocused } = require('stremio-router'); const Button = require('stremio/common/Button'); const TextInput = require('stremio/common/TextInput'); +const useTorrent = require('stremio/common/useTorrent'); +const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const styles = require('./styles'); const SearchBar = ({ className, query, active }) => { - const { core } = useServices(); const routeFocused = useRouteFocused(); + const { createTorrentFromMagnet } = useTorrent(); const searchInputRef = React.useRef(null); const searchBarOnClick = React.useCallback(() => { if (!active) { @@ -22,17 +22,7 @@ const SearchBar = ({ className, query, active }) => { }, [active]); const queryInputOnChange = React.useCallback(() => { try { - const parsed = magnet.decode(searchInputRef.current.value); - if (parsed && typeof parsed.infoHash === 'string') { - core.transport.dispatch({ - action: 'StreamingServer', - args: { - action: 'CreateTorrent', - args: searchInputRef.current.value - } - }); - searchInputRef.current.value = ''; - } + createTorrentFromMagnet(searchInputRef.current.value); // eslint-disable-next-line no-empty } catch { } }, []); @@ -80,4 +70,17 @@ SearchBar.propTypes = { active: PropTypes.bool }; -module.exports = SearchBar; +const SearchBarFallback = ({ className }) => ( + +); + +SearchBarFallback.propTypes = SearchBar.propTypes; + +module.exports = withCoreSuspender(SearchBar, SearchBarFallback); diff --git a/src/common/Toast/ToastProvider.js b/src/common/Toast/ToastProvider.js index 571f79c14..b022c1665 100644 --- a/src/common/Toast/ToastProvider.js +++ b/src/common/Toast/ToastProvider.js @@ -28,32 +28,48 @@ const ToastProvider = ({ className, children }) => { clearTimeout(event.dataset.id); dispatch({ type: 'remove', id: event.dataset.id }); }, []); - const toast = React.useMemo(() => ({ - show: (item) => { - const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ? - item.timeout - : - DEFAULT_TIMEOUT; - const id = setTimeout(() => { - dispatch({ type: 'remove', id }); - }, timeout); - dispatch({ - type: 'add', - item: { - ...item, - id, - dataset: { - ...item.dataset, - id - }, - onClose: itemOnClose + const toast = React.useMemo(() => { + const filters = []; + return { + addFilter: (filter) => { + filters.push(filter); + }, + removeFilter: (filter) => { + const index = filters.indexOf(filter); + if (index > -1) { + filters.splice(index, 1); } - }); - }, - clear: () => { - dispatch({ type: 'clear' }); - } - }), []); + }, + show: (item) => { + if (filters.some((filter) => filter(item))) { + return; + } + + const timeout = typeof item.timeout === 'number' && !isNaN(item.timeout) ? + item.timeout + : + DEFAULT_TIMEOUT; + const id = setTimeout(() => { + dispatch({ type: 'remove', id }); + }, timeout); + dispatch({ + type: 'add', + item: { + ...item, + id, + dataset: { + ...item.dataset, + id + }, + onClose: itemOnClose + } + }); + }, + clear: () => { + dispatch({ type: 'clear' }); + } + }; + }, []); return ( {container instanceof HTMLElement ? children : null} diff --git a/src/common/index.js b/src/common/index.js index 54fac3a83..90c266e64 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -38,6 +38,7 @@ const useModelState = require('./useModelState'); const useOnScrollToBottom = require('./useOnScrollToBottom'); const useProfile = require('./useProfile'); const useStreamingServer = require('./useStreamingServer'); +const useTorrent = require('./useTorrent'); module.exports = { AddonDetailsModal, @@ -80,5 +81,6 @@ module.exports = { useModelState, useOnScrollToBottom, useProfile, - useStreamingServer + useStreamingServer, + useTorrent }; diff --git a/src/common/useTorrent.js b/src/common/useTorrent.js new file mode 100644 index 000000000..6ecfe4de6 --- /dev/null +++ b/src/common/useTorrent.js @@ -0,0 +1,50 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const React = require('react'); +const magnet = require('magnet-uri'); +const { useServices } = require('stremio/services'); +const useToast = require('stremio/common/Toast/useToast'); +const useStreamingServer = require('stremio/common/useStreamingServer'); + +const useTorrent = () => { + const { core } = useServices(); + const streamingServer = useStreamingServer(); + const toast = useToast(); + const createTorrentTimeout = React.useRef(null); + const createTorrentFromMagnet = React.useCallback((text) => { + const parsed = magnet.decode(text); + if (parsed && typeof parsed.infoHash === 'string') { + core.transport.dispatch({ + action: 'StreamingServer', + args: { + action: 'CreateTorrent', + args: text + } + }); + clearTimeout(createTorrentTimeout.current); + createTorrentTimeout.current = setTimeout(() => { + toast.show({ + type: 'error', + title: 'It\'s taking a long time to get metadata from the torrent.', + timeout: 10000 + }); + }, 10000); + } + }, []); + React.useEffect(() => { + if (streamingServer.torrent !== null) { + const [, { type }] = streamingServer.torrent; + if (type === 'Ready') { + clearTimeout(createTorrentTimeout.current); + } + } + }, [streamingServer.torrent]); + React.useEffect(() => { + return () => clearTimeout(createTorrentTimeout.current); + }, []); + return { + createTorrentFromMagnet + }; +}; + +module.exports = useTorrent; diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index e62c26633..4c061ef73 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -4,32 +4,70 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const Icon = require('@stremio/stremio-icons/dom'); -const { Button, Image } = require('stremio/common'); +const { Button, Image, Multiselect } = require('stremio/common'); const { useServices } = require('stremio/services'); const Stream = require('./Stream'); const styles = require('./styles'); +const ALL_ADDONS_KEY = 'ALL'; + const StreamsList = ({ className, ...props }) => { const { core } = useServices(); - const streams = React.useMemo(() => { + const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY); + const onAddonSelected = React.useCallback((event) => { + setSelectedAddon(event.value); + }, []); + const streamsByAddon = React.useMemo(() => { return props.streams .filter((streams) => streams.content.type === 'Ready') - .map((streams) => { - return streams.content.content.map((stream) => ({ - ...stream, - onClick: () => { - core.transport.analytics({ - event: 'StreamClicked', - args: { - stream - } - }); - }, - addonName: streams.addon.manifest.name - })); - }) - .flat(1); + .reduce((streamsByAddon, streams) => { + streamsByAddon[streams.addon.transportUrl] = { + addon: streams.addon, + streams: streams.content.content.map((stream) => ({ + ...stream, + onClick: () => { + core.transport.analytics({ + event: 'StreamClicked', + args: { + stream + } + }); + }, + addonName: streams.addon.manifest.name + })) + }; + + return streamsByAddon; + }, {}); }, [props.streams]); + const filteredStreams = React.useMemo(() => { + return selectedAddon === ALL_ADDONS_KEY ? + Object.values(streamsByAddon).map(({ streams }) => streams).flat(1) + : + streamsByAddon[selectedAddon] ? + streamsByAddon[selectedAddon].streams + : + []; + }, [streamsByAddon, selectedAddon]); + const selectableOptions = React.useMemo(() => { + return { + title: 'Select Addon', + options: [ + { + value: ALL_ADDONS_KEY, + label: 'All', + title: 'All' + }, + ...Object.keys(streamsByAddon).map((transportUrl) => ({ + value: transportUrl, + label: streamsByAddon[transportUrl].addon.manifest.name, + title: streamsByAddon[transportUrl].addon.manifest.name, + })) + ], + selected: [selectedAddon], + onSelect: onAddonSelected + }; + }, [streamsByAddon, selectedAddon]); return (
{ @@ -45,26 +83,37 @@ const StreamsList = ({ className, ...props }) => {
No streams were found!
: - streams.length === 0 ? + filteredStreams.length === 0 ?
: -
- {streams.map((stream, index) => ( - - ))} -
+ + { + Object.keys(streamsByAddon).length > 1 ? + + : + null + } +
+ {filteredStreams.map((stream, index) => ( + + ))} +
+
} + { + nextVideo !== null ? + + : + null + } - + { + metaItem?.content?.videos?.length > 0 ? + + : + null + }
@@ -163,6 +195,7 @@ ControlBar.propTypes = { subtitlesTracks: PropTypes.array, audioTracks: PropTypes.array, metaItem: PropTypes.object, + nextVideo: PropTypes.object, onPlayRequested: PropTypes.func, onPauseRequested: PropTypes.func, onMuteRequested: PropTypes.func, @@ -171,7 +204,8 @@ ControlBar.propTypes = { onSeekRequested: PropTypes.func, onToggleSubtitlesMenu: PropTypes.func, onToggleInfoMenu: PropTypes.func, - onToggleSpeedMenu: PropTypes.func + onToggleSpeedMenu: PropTypes.func, + onToggleVideosMenu: PropTypes.func }; module.exports = ControlBar; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 04cbec555..c2710eb82 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -11,6 +11,7 @@ const Icon = require('@stremio/stremio-icons/dom'); const BufferingLoader = require('./BufferingLoader'); const ControlBar = require('./ControlBar'); const InfoMenu = require('./InfoMenu'); +const VideosMenu = require('./VideosMenu'); const SubtitlesMenu = require('./SubtitlesMenu'); const SpeedMenu = require('./SpeedMenu'); const Video = require('./Video'); @@ -40,6 +41,7 @@ const Player = ({ urlParams, queryParams }) => { const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false); const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false); const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false); + const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false); const [error, setError] = React.useState(null); const [videoState, setVideoState] = React.useReducer( (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), @@ -207,6 +209,9 @@ const Player = ({ urlParams, queryParams }) => { if (!event.nativeEvent.speedMenuClosePrevented) { closeSpeedMenu(); } + if (!event.nativeEvent.videosMenuClosePrevented) { + closeVideosMenu(); + } }, []); const onContainerMouseMove = React.useCallback((event) => { setImmersed(false); @@ -327,6 +332,7 @@ const Player = ({ urlParams, queryParams }) => { React.useEffect(() => { if (player.metaItem === null || player.metaItem.type !== 'Ready') { closeInfoMenu(); + closeVideosMenu(); } }, [player.metaItem]); React.useEffect(() => { @@ -343,6 +349,8 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); React.useEffect(() => { + const toastFilter = (item) => item?.dataset?.type === 'CoreEvent'; + toast.addFilter(toastFilter); const onCastStateChange = () => { setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED); }; @@ -358,6 +366,7 @@ const Player = ({ urlParams, queryParams }) => { chromecast.on('stateChanged', onChromecastServiceStateChange); onChromecastServiceStateChange(); return () => { + toast.removeFilter(toastFilter); chromecast.off('stateChanged', onChromecastServiceStateChange); if (chromecast.active) { chromecast.transport.off( @@ -414,6 +423,7 @@ const Player = ({ urlParams, queryParams }) => { case 'KeyS': { closeInfoMenu(); closeSpeedMenu(); + closeVideosMenu(); if ((Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) || (Array.isArray(videoState.extraSubtitlesTracks) && videoState.extraSubtitlesTracks.length > 0) || (Array.isArray(videoState.audioTracks) && videoState.audioTracks.length > 0)) { @@ -425,6 +435,7 @@ const Player = ({ urlParams, queryParams }) => { case 'KeyI': { closeSubtitlesMenu(); closeSpeedMenu(); + closeVideosMenu(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleInfoMenu(); } @@ -440,10 +451,20 @@ const Player = ({ urlParams, queryParams }) => { break; } + case 'KeyV': { + closeInfoMenu(); + closeSubtitlesMenu(); + if (player.metaItem !== null && player.metaItem.type === 'Ready') { + toggleVideosMenu(); + } + + break; + } case 'Escape': { closeSubtitlesMenu(); closeInfoMenu(); closeSpeedMenu(); + closeVideosMenu(); break; } } @@ -454,7 +475,7 @@ const Player = ({ urlParams, queryParams }) => { return () => { window.removeEventListener('keydown', onKeyDown); }; - }, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu]); + }, [player.metaItem, settings.seekTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu]); React.useLayoutEffect(() => { return () => { setImmersedDebounced.cancel(); @@ -463,7 +484,7 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); return ( -
{ subtitlesTracks={videoState.subtitlesTracks.concat(videoState.extraSubtitlesTracks)} audioTracks={videoState.audioTracks} metaItem={player.metaItem} + nextVideo={player.nextVideo} onPlayRequested={onPlayRequested} onPauseRequested={onPauseRequested} onMuteRequested={onMuteRequested} @@ -541,6 +563,7 @@ const Player = ({ urlParams, queryParams }) => { onToggleSubtitlesMenu={toggleSubtitlesMenu} onToggleInfoMenu={toggleInfoMenu} onToggleSpeedMenu={toggleSpeedMenu} + onToggleVideosMenu={toggleVideosMenu} onMouseMove={onBarMouseMove} onMouseOver={onBarMouseMove} /> @@ -592,6 +615,16 @@ const Player = ({ urlParams, queryParams }) => { : null } + { + videosMenuOpen ? + + : + null + }
); }; diff --git a/src/routes/Player/VideosMenu/VideosMenu.js b/src/routes/Player/VideosMenu/VideosMenu.js new file mode 100644 index 000000000..219a329c2 --- /dev/null +++ b/src/routes/Player/VideosMenu/VideosMenu.js @@ -0,0 +1,51 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const React = require('react'); +const PropTypes = require('prop-types'); +const classnames = require('classnames'); +const Video = require('../../MetaDetails/VideosList/Video'); +const styles = require('./styles'); + +const VideosMenu = ({ className, metaItem, seriesInfo }) => { + const onMouseDown = React.useCallback((event) => { + event.nativeEvent.videosMenuClosePrevented = true; + }, []); + const videos = React.useMemo(() => { + return seriesInfo && typeof seriesInfo.season === 'number' && Array.isArray(metaItem.videos) ? + metaItem.videos.filter(({ season }) => season === seriesInfo.season) + : + metaItem.videos; + }, [metaItem, seriesInfo]); + return ( +
+ { + videos.map((video, index) => ( +
+ ); +}; + +VideosMenu.propTypes = { + className: PropTypes.string, + metaItem: PropTypes.object, + seriesInfo: PropTypes.shape({ + season: PropTypes.number, + episode: PropTypes.number, + }), +}; + +module.exports = VideosMenu; diff --git a/src/routes/Player/VideosMenu/index.js b/src/routes/Player/VideosMenu/index.js new file mode 100644 index 000000000..e604ab4cc --- /dev/null +++ b/src/routes/Player/VideosMenu/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const VideosMenu = require('./VideosMenu'); + +module.exports = VideosMenu; diff --git a/src/routes/Player/VideosMenu/styles.less b/src/routes/Player/VideosMenu/styles.less new file mode 100644 index 000000000..47444c72a --- /dev/null +++ b/src/routes/Player/VideosMenu/styles.less @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +.videos-menu-container { + width: 30rem; +} \ No newline at end of file diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 44550794e..9ca68fd52 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -14,6 +14,7 @@ const styles = require('./styles'); const GENERAL_SECTION = 'general'; const PLAYER_SECTION = 'player'; const STREAMING_SECTION = 'streaming'; +const SHORTCUTS_SECTION = 'shortcuts'; const Settings = () => { const { core } = useServices(); @@ -93,10 +94,12 @@ const Settings = () => { const generalSectionRef = React.useRef(null); const playerSectionRef = React.useRef(null); const streamingServerSectionRef = React.useRef(null); + const shortcutsSectionRef = React.useRef(null); const sections = React.useMemo(() => ([ { ref: generalSectionRef, id: GENERAL_SECTION }, { ref: playerSectionRef, id: PLAYER_SECTION }, { ref: streamingServerSectionRef, id: STREAMING_SECTION }, + { ref: shortcutsSectionRef, id: SHORTCUTS_SECTION }, ]), []); const [selectedSectionId, setSelectedSectionId] = React.useState(GENERAL_SECTION); const updateSelectedSectionId = React.useCallback(() => { @@ -142,6 +145,9 @@ const Settings = () => { +
App Version: {process.env.VERSION}
{ @@ -424,6 +430,107 @@ const Settings = () => { null }
+
+
Shortcuts
+
+
+
Play / Pause
+
+
+ Space +
+
+
+
+
Seek Forward
+
+
+ +
or
+ ⇧ Shift +
+
+ +
+
+
+
+
Seek Backward
+
+
+ +
or
+ ⇧ Shift +
+
+ +
+
+
+
+
Volume Up
+
+
+ +
+
+
+
+
Volume Down
+
+
+ +
+
+
+
+
Toggle Subtitles Menu
+
+
+ S +
+
+
+
+
Toggle Info Menu
+
+
+ I +
+
+
+
+
Toggle Fullscreen
+
+
+ F +
+
+
+
+
Navigate Between Menus
+
+
+ 1 +
to
+ 5 +
+
+
+
+
Go to Search
+
+
+ 0 +
+
+
+
+
Close Menu or Modal
+
+
+ Esc +
+
+
{ diff --git a/src/routes/Settings/styles.less b/src/routes/Settings/styles.less index be058dd23..7e297080a 100644 --- a/src/routes/Settings/styles.less +++ b/src/routes/Settings/styles.less @@ -315,6 +315,30 @@ } } } + + &.shortcut-container { + justify-content: center; + padding: 0; + overflow: visible; + + kbd { + flex: 0 1 auto; + height: 2.5rem; + min-width: 2.5rem; + line-height: 2.5rem; + padding: 0 1rem; + font-weight: 500; + color: @color-secondaryvariant1-90; + border-radius: 0.25em; + box-shadow: 0 4px 0 1px @color-background-40; + background-color: @color-background; + } + + .label { + margin: 0 1rem; + color: @color-secondaryvariant1-90; + } + } } } }