diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 696c591aa5..1415f6f867 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,11 +21,11 @@ jobs: - name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: Initialize CodeQL - uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/init@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/autobuild@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/analyze@0225834cc549ee0ca93cb085b92954821a145866 # v2.3.5 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9201e9374c..52006a7b46 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -59,6 +59,7 @@ - [Vankerkom](https://github.com/vankerkom) - [edvwib](https://github.com/edvwib) - [Rob Farraher](https://github.com/farraherbg) + - [TelepathicWalrus](https://github.com/TelepathicWalrus) - [Pier-Luc Ducharme](https://github.com/pl-ducharme) - [Anantharaju S](https://github.com/Anantharajus) - [Merlin Sievers](https://github.com/dann-merlin) diff --git a/package-lock.json b/package-lock.json index 547803c8a7..a6664910a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2825,12 +2825,9 @@ "dev": true }, "node_modules/@jellyfin/sdk": { - "version": "0.0.0-unstable.202304122102", - "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202304122102.tgz", - "integrity": "sha512-KToOmK3GmbjovtFPgb3dYx8cV6bopo46fhTkHDnKLqsmwqBz5/QKk7Z8NbR+5YaojNAP4LUYnenZmMK9HQ2YeA==", - "dependencies": { - "compare-versions": "5.0.3" - }, + "version": "0.0.0-unstable.202305300501", + "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202305300501.tgz", + "integrity": "sha512-xAiVZQFtnRkikiYcYSue75+socgwVY+NwY3PaRDTbjH90Guo4ptcLXmlgAFcUad+J3jpwpdAR9+fKmSomUFKRA==", "peerDependencies": { "axios": "^1.3.4" } @@ -5653,11 +5650,6 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "node_modules/compare-versions": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", - "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==" - }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -22165,12 +22157,10 @@ "dev": true }, "@jellyfin/sdk": { - "version": "0.0.0-unstable.202304122102", - "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202304122102.tgz", - "integrity": "sha512-KToOmK3GmbjovtFPgb3dYx8cV6bopo46fhTkHDnKLqsmwqBz5/QKk7Z8NbR+5YaojNAP4LUYnenZmMK9HQ2YeA==", - "requires": { - "compare-versions": "5.0.3" - } + "version": "0.0.0-unstable.202305300501", + "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202305300501.tgz", + "integrity": "sha512-xAiVZQFtnRkikiYcYSue75+socgwVY+NwY3PaRDTbjH90Guo4ptcLXmlgAFcUad+J3jpwpdAR9+fKmSomUFKRA==", + "requires": {} }, "@jridgewell/gen-mapping": { "version": "0.3.2", @@ -24280,11 +24270,6 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "compare-versions": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", - "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==" - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", diff --git a/src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx b/src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx new file mode 100644 index 0000000000..6749b61284 --- /dev/null +++ b/src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx @@ -0,0 +1,112 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import CastConnected from '@mui/icons-material/CastConnected'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Cast from '@mui/icons-material/Cast'; +import IconButton from '@mui/material/IconButton'; +import { useTheme } from '@mui/material/styles'; +import Tooltip from '@mui/material/Tooltip'; + +import { playbackManager } from 'components/playback/playbackmanager'; +import globalize from 'scripts/globalize'; +import Events from 'utils/events'; + +import RemotePlayMenu, { ID } from './menus/RemotePlayMenu'; +import RemotePlayActiveMenu, { ID as ACTIVE_ID } from './menus/RemotePlayActiveMenu'; + +const RemotePlayButton = () => { + const theme = useTheme(); + const [ playerInfo, setPlayerInfo ] = useState(playbackManager.getPlayerInfo()); + + const updatePlayerInfo = useCallback(() => { + setPlayerInfo(playbackManager.getPlayerInfo()); + }, [ setPlayerInfo ]); + + useEffect(() => { + Events.on(playbackManager, 'playerchange', updatePlayerInfo); + + return () => { + Events.off(playbackManager, 'playerchange', updatePlayerInfo); + }; + }, [ updatePlayerInfo ]); + + const [ remotePlayMenuAnchorEl, setRemotePlayMenuAnchorEl ] = useState(null); + const isRemotePlayMenuOpen = Boolean(remotePlayMenuAnchorEl); + + const onRemotePlayButtonClick = useCallback((event) => { + setRemotePlayMenuAnchorEl(event.currentTarget); + }, [ setRemotePlayMenuAnchorEl ]); + + const onRemotePlayMenuClose = useCallback(() => { + setRemotePlayMenuAnchorEl(null); + }, [ setRemotePlayMenuAnchorEl ]); + + const [ remotePlayActiveMenuAnchorEl, setRemotePlayActiveMenuAnchorEl ] = useState(null); + const isRemotePlayActiveMenuOpen = Boolean(remotePlayActiveMenuAnchorEl); + + const onRemotePlayActiveButtonClick = useCallback((event) => { + setRemotePlayActiveMenuAnchorEl(event.currentTarget); + }, [ setRemotePlayActiveMenuAnchorEl ]); + + const onRemotePlayActiveMenuClose = useCallback(() => { + setRemotePlayActiveMenuAnchorEl(null); + }, [ setRemotePlayActiveMenuAnchorEl ]); + + return ( + <> + {(playerInfo && !playerInfo.isLocalPlayer) ? ( + + + + + + ) : ( + + + + + + )} + + + + + + ); +}; + +export default RemotePlayButton; diff --git a/src/apps/experimental/components/AppToolbar/index.tsx b/src/apps/experimental/components/AppToolbar/index.tsx index 0eeeecfc69..433fa4c836 100644 --- a/src/apps/experimental/components/AppToolbar/index.tsx +++ b/src/apps/experimental/components/AppToolbar/index.tsx @@ -15,6 +15,7 @@ import globalize from 'scripts/globalize'; import AppTabs from '../tabs/AppTabs'; import { isDrawerPath } from '../drawers/AppDrawer'; import UserMenuButton from './UserMenuButton'; +import RemotePlayButton from './RemotePlayButton'; interface AppToolbarProps { isDrawerOpen: boolean @@ -89,6 +90,8 @@ const AppToolbar: FC = ({ {isUserLoggedIn && ( <> + + void + playerInfo: { + name: string + isLocalPlayer: boolean + id?: string + deviceName?: string + playableMediaTypes?: string[] + supportedCommands?: string[] + } | null +} + +export const ID = 'app-remote-play-active-menu'; + +const RemotePlayActiveMenu: FC = ({ + anchorEl, + open, + onMenuClose, + playerInfo +}) => { + const [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ] = useState(playbackManager.enableDisplayMirroring()); + const isDisplayMirrorSupported = playerInfo?.supportedCommands && playerInfo.supportedCommands.indexOf('DisplayContent') !== -1; + const toggleDisplayMirror = useCallback(() => { + playbackManager.enableDisplayMirroring(!isDisplayMirrorEnabled); + setIsDisplayMirrorEnabled(!isDisplayMirrorEnabled); + }, [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ]); + + const [ isAutoCastEnabled, setIsAutoCastEnabled ] = useState(isEnabled()); + const isAutoCastSupported = supported(); + const toggleAutoCast = useCallback(() => { + enable(!isAutoCastEnabled); + setIsAutoCastEnabled(!isAutoCastEnabled); + }, [ isAutoCastEnabled, setIsAutoCastEnabled ]); + + const remotePlayerName = playerInfo?.deviceName || playerInfo?.name; + + const disconnectRemotePlayer = useCallback(() => { + if (playbackManager.getSupportedCommands().indexOf('EndSession') !== -1) { + dialog.show({ + buttons: [ + { + name: globalize.translate('Yes'), + id: 'yes' + }, { + name: globalize.translate('No'), + id: 'no' + } + ], + text: globalize.translate('ConfirmEndPlayerSession', remotePlayerName) + }).then(id => { + onMenuClose(); + + if (id === 'yes') { + playbackManager.getCurrentPlayer().endSession(); + } + playbackManager.setDefaultPlayerActive(); + }).catch(() => { + // Dialog closed + }); + } else { + onMenuClose(); + playbackManager.setDefaultPlayerActive(); + } + }, [ onMenuClose, remotePlayerName ]); + + return ( + + {remotePlayerName} + + ) + }} + > + {isDisplayMirrorSupported && ( + + {isDisplayMirrorEnabled && ( + + + + )} + + {globalize.translate('EnableDisplayMirroring')} + + + )} + + {isAutoCastSupported && ( + + {isAutoCastEnabled && ( + + + + )} + + {globalize.translate('EnableAutoCast')} + + + )} + + {(isDisplayMirrorSupported || isAutoCastSupported) && } + + + + + + + {globalize.translate('HeaderRemoteControl')} + + + + + + + + + {globalize.translate('Disconnect')} + + + + ); +}; + +export default RemotePlayActiveMenu; diff --git a/src/apps/experimental/components/AppToolbar/menus/RemotePlayMenu.tsx b/src/apps/experimental/components/AppToolbar/menus/RemotePlayMenu.tsx new file mode 100644 index 0000000000..96626eb662 --- /dev/null +++ b/src/apps/experimental/components/AppToolbar/menus/RemotePlayMenu.tsx @@ -0,0 +1,100 @@ +import Warning from '@mui/icons-material/Warning'; +import Divider from '@mui/material/Divider'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Menu, { type MenuProps } from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import React, { FC, useEffect, useState } from 'react'; + +import globalize from 'scripts/globalize'; +import { playbackManager } from 'components/playback/playbackmanager'; +import { pluginManager } from 'components/pluginManager'; +import type { PlayTarget } from 'types/playTarget'; + +import PlayTargetIcon from '../../PlayTargetIcon'; + +interface RemotePlayMenuProps extends MenuProps { + onMenuClose: () => void +} + +export const ID = 'app-remote-play-menu'; + +const RemotePlayMenu: FC = ({ + anchorEl, + open, + onMenuClose +}) => { + // TODO: Add other checks for support (Android app, secure context, etc) + const isChromecastPluginLoaded = !!pluginManager.plugins.find(plugin => plugin.id === 'chromecast'); + + const [ playbackTargets, setPlaybackTargets ] = useState([]); + + const onPlayTargetClick = (target: PlayTarget) => { + playbackManager.trySetActivePlayer(target.playerName, target); + onMenuClose(); + }; + + useEffect(() => { + const fetchPlaybackTargets = async () => { + setPlaybackTargets( + await playbackManager.getTargets() + ); + }; + + if (open) { + fetchPlaybackTargets() + .catch(err => { + console.error('[AppRemotePlayMenu] unable to get playback targets', err); + }); + } + }, [ open, setPlaybackTargets ]); + + return ( + + {!isChromecastPluginLoaded && ([ + + + + + + {globalize.translate('GoogleCastUnsupported')} + + , + + ])} + + {playbackTargets.map(target => ( + onPlayTargetClick(target)} + > + + + + + + ))} + + ); +}; + +export default RemotePlayMenu; diff --git a/src/apps/experimental/components/PlayTargetIcon.tsx b/src/apps/experimental/components/PlayTargetIcon.tsx new file mode 100644 index 0000000000..320f1c9f4c --- /dev/null +++ b/src/apps/experimental/components/PlayTargetIcon.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import Cast from '@mui/icons-material/Cast'; +import Computer from '@mui/icons-material/Computer'; +import Devices from '@mui/icons-material/Devices'; +import Smartphone from '@mui/icons-material/Smartphone'; +import Tablet from '@mui/icons-material/Tablet'; +import Tv from '@mui/icons-material/Tv'; + +import browser from 'scripts/browser'; +import type { PlayTarget } from 'types/playTarget'; + +const PlayTargetIcon = ({ target }: { target: PlayTarget }) => { + if (!target.deviceType && target.isLocalPlayer) { + if (browser.tv) { + return ; + } else if (browser.mobile) { + return ; + } + return ; + } + + switch (target.deviceType) { + case 'smartphone': + return ; + case 'tablet': + return ; + case 'desktop': + return ; + case 'cast': + return ; + case 'tv': + return ; + default: + return ; + } +}; + +export default PlayTargetIcon; diff --git a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx index 617ee2de50..7e0bb4e366 100644 --- a/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx +++ b/src/apps/experimental/components/drawers/dashboard/AdvancedDrawerSection.tsx @@ -1,4 +1,11 @@ -import { Lan, VpnKey, Article, EditNotifications, Extension, Schedule, ExpandLess, ExpandMore } from '@mui/icons-material'; +import Article from '@mui/icons-material/Article'; +import EditNotifications from '@mui/icons-material/EditNotifications'; +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import Extension from '@mui/icons-material/Extension'; +import Lan from '@mui/icons-material/Lan'; +import Schedule from '@mui/icons-material/Schedule'; +import VpnKey from '@mui/icons-material/VpnKey'; import Collapse from '@mui/material/Collapse'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; @@ -63,7 +70,7 @@ const AdvancedDrawerSection = () => { - + diff --git a/src/apps/experimental/routes/asyncRoutes/admin.ts b/src/apps/experimental/routes/asyncRoutes/admin.ts index 0f2ec6b9cf..72bcc6f32b 100644 --- a/src/apps/experimental/routes/asyncRoutes/admin.ts +++ b/src/apps/experimental/routes/asyncRoutes/admin.ts @@ -1,6 +1,7 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ + { path: 'notificationsettings.html', page: 'dashboard/notifications' }, { path: 'usernew.html', page: 'user/usernew' }, { path: 'userprofiles.html', page: 'user/userprofiles' }, { path: 'useredit.html', page: 'user/useredit' }, diff --git a/src/apps/experimental/routes/legacyRoutes/admin.ts b/src/apps/experimental/routes/legacyRoutes/admin.ts index dd1f95f6e5..e2037ddb11 100644 --- a/src/apps/experimental/routes/legacyRoutes/admin.ts +++ b/src/apps/experimental/routes/legacyRoutes/admin.ts @@ -103,18 +103,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'dashboard/metadatanfo', view: 'dashboard/metadatanfo.html' } - }, { - path: 'notificationsetting.html', - pageProps: { - controller: 'dashboard/notifications/notification/index', - view: 'dashboard/notifications/notification/index.html' - } - }, { - path: 'notificationsettings.html', - pageProps: { - controller: 'dashboard/notifications/notifications/index', - view: 'dashboard/notifications/notifications/index.html' - } }, { path: 'playbackconfiguration.html', pageProps: { diff --git a/src/apps/stable/routes/asyncRoutes/admin.ts b/src/apps/stable/routes/asyncRoutes/admin.ts index 0f2ec6b9cf..72bcc6f32b 100644 --- a/src/apps/stable/routes/asyncRoutes/admin.ts +++ b/src/apps/stable/routes/asyncRoutes/admin.ts @@ -1,6 +1,7 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ + { path: 'notificationsettings.html', page: 'dashboard/notifications' }, { path: 'usernew.html', page: 'user/usernew' }, { path: 'userprofiles.html', page: 'user/userprofiles' }, { path: 'useredit.html', page: 'user/useredit' }, diff --git a/src/apps/stable/routes/dashboard/notifications.tsx b/src/apps/stable/routes/dashboard/notifications.tsx new file mode 100644 index 0000000000..ca874d1333 --- /dev/null +++ b/src/apps/stable/routes/dashboard/notifications.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import Page from 'components/Page'; +import globalize from 'scripts/globalize'; + +const PluginLink = () => ( + +
+ +
${LabelEnableLUFSScanHelp}
+
+