diff --git a/ui/src/App.js b/ui/src/App.js index c30daa92..903674ea 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -22,6 +22,7 @@ import { playQueueReducer, albumViewReducer, activityReducer, + settingsReducer, } from './reducers' import createAdminStore from './store/createAdminStore' import { i18nProvider } from './i18n' @@ -50,6 +51,7 @@ const App = () => ( theme: themeReducer, addToPlaylistDialog: addToPlaylistDialogReducer, activity: activityReducer, + settings: settingsReducer, }, })} > diff --git a/ui/src/actions/index.js b/ui/src/actions/index.js index 0b161ad0..f087f97a 100644 --- a/ui/src/actions/index.js +++ b/ui/src/actions/index.js @@ -3,3 +3,4 @@ export * from './themes' export * from './albumView' export * from './dialogs' export * from './serverEvents' +export * from './settings' diff --git a/ui/src/actions/settings.js b/ui/src/actions/settings.js new file mode 100644 index 00000000..bb8942a6 --- /dev/null +++ b/ui/src/actions/settings.js @@ -0,0 +1,6 @@ +export const SET_NOTIFICATIONS_STATE = 'SET_NOTIFICATIONS_STATE' + +export const setNotificationsState = (enabled) => ({ + type: SET_NOTIFICATIONS_STATE, + data: enabled, +}) diff --git a/ui/src/audioplayer/Player.js b/ui/src/audioplayer/Player.js index cbfc31c1..a67cbab6 100644 --- a/ui/src/audioplayer/Player.js +++ b/ui/src/audioplayer/Player.js @@ -18,6 +18,7 @@ import themes from '../themes' import config from '../config' import PlayerToolbar from './PlayerToolbar' import { useHotkeys } from 'react-hotkeys-hook' +import { sendNotification, baseUrl } from '../utils' const useStyle = makeStyles((theme) => ({ audioTitle: { @@ -54,6 +55,7 @@ const Player = () => { const queue = useSelector((state) => state.queue) const current = queue.current || {} const { authenticated } = useAuthState() + const showNotifications = useSelector((state) => state.settings.notifications || false) const visible = authenticated && queue.queue.length > 0 const classes = useStyle({ visible }) @@ -230,9 +232,12 @@ const Player = () => { label: `${info.name} - ${info.singer}`, }) } + if (showNotifications) { + sendNotification(info.name, `${info.singer} - ${info.album}`, baseUrl(info.cover)) + } } }, - [dispatch] + [dispatch, showNotifications] ) const onAudioPause = useCallback( diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json index f63b7509..4bda1c13 100644 --- a/ui/src/i18n/en.json +++ b/ui/src/i18n/en.json @@ -262,7 +262,8 @@ "songsAddedToPlaylist": "Added 1 song to playlist |||| Added %{smart_count} songs to playlist", "noPlaylistsAvailable": "None available", "delete_user_title": "Delete user '%{name}'", - "delete_user_content": "Are you sure you want to delete this user and all their data (including playlists and preferences)?" + "delete_user_content": "Are you sure you want to delete this user and all their data (including playlists and preferences)?", + "notifications_blocked": "You have blocked Notifications for this site in your browser's settings or your browser does not support notifications." }, "menu": { "library": "Library", @@ -274,7 +275,8 @@ "options": { "theme": "Theme", "language": "Language", - "defaultView": "Default View" + "defaultView": "Default View", + "desktop_notifications": "Desktop Notifications" } }, "albumList": "Albums", diff --git a/ui/src/personal/Personal.js b/ui/src/personal/Personal.js index 254324ba..f1cb207b 100644 --- a/ui/src/personal/Personal.js +++ b/ui/src/personal/Personal.js @@ -8,10 +8,15 @@ import { useLocale, useSetLocale, useTranslate, + BooleanInput, + useNotify, } from 'react-admin' import { makeStyles } from '@material-ui/core/styles' import HelpOutlineIcon from '@material-ui/icons/HelpOutline' -import { changeTheme } from '../actions' +import { + changeTheme, + setNotificationsState, +} from '../actions' import themes from '../themes' import { docsUrl } from '../utils' import { useGetLanguageChoices } from '../i18n' @@ -119,6 +124,45 @@ const SelectDefaultView = (props) => { ) } +const NotificationsToggle = (props) => { + const translate = useTranslate() + const dispatch = useDispatch() + const notify = useNotify() + const currentSetting = useSelector((state) => state.settings.notifications) + const current = (() => { + if (!("Notification" in window) || Notification.permission !== 'granted') { + return false + } + return currentSetting + })() + + return ( + { + if (notificationsEnabled) { + if (!('Notification' in window) || Notification.permission === 'denied') { + notify(translate('message.notifications_blocked'), 'warning') + notificationsEnabled = false + } else { + const response = await Notification.requestPermission() + if (response !== 'granted') { + notificationsEnabled = false + } + } + if (!notificationsEnabled) { + // Need to turn switch off + } + } + dispatch(setNotificationsState(notificationsEnabled)) + }} + /> + ) +} + const Personal = () => { const translate = useTranslate() const classes = useStyles() @@ -130,6 +174,7 @@ const Personal = () => { + ) diff --git a/ui/src/reducers/index.js b/ui/src/reducers/index.js index fd121d25..0abc119b 100644 --- a/ui/src/reducers/index.js +++ b/ui/src/reducers/index.js @@ -3,3 +3,4 @@ export * from './dialogReducer' export * from './playQueue' export * from './albumView' export * from './activityReducer' +export * from './settingsReducer' diff --git a/ui/src/reducers/playQueue.js b/ui/src/reducers/playQueue.js index 0636bc98..87cbb280 100644 --- a/ui/src/reducers/playQueue.js +++ b/ui/src/reducers/playQueue.js @@ -23,6 +23,7 @@ const mapToAudioLists = (item) => { trackId: id, name: item.title, singer: item.artist, + album: item.album, albumId: item.albumId, artistId: item.albumArtistId, duration: item.duration, diff --git a/ui/src/reducers/settingsReducer.js b/ui/src/reducers/settingsReducer.js new file mode 100644 index 00000000..8f6b207c --- /dev/null +++ b/ui/src/reducers/settingsReducer.js @@ -0,0 +1,18 @@ +import { SET_NOTIFICATIONS_STATE } from '../actions' + +const initialState = { + notifications: false, +} + +export const settingsReducer = (previousState = initialState, payload) => { + const { type, data } = payload + switch (type) { + case SET_NOTIFICATIONS_STATE: + return { + ...previousState, + notifications: data + } + default: + return previousState + } +} diff --git a/ui/src/store/createAdminStore.js b/ui/src/store/createAdminStore.js index de5c145c..6dd34367 100644 --- a/ui/src/store/createAdminStore.js +++ b/ui/src/store/createAdminStore.js @@ -50,6 +50,7 @@ export default ({ theme: state.theme, queue: pick(state.queue, ['queue', 'volume']), albumView: state.albumView, + settings: state.settings, }) }), 1000 diff --git a/ui/src/utils/index.js b/ui/src/utils/index.js index b8c5c5c6..a5248211 100644 --- a/ui/src/utils/index.js +++ b/ui/src/utils/index.js @@ -1,3 +1,4 @@ export * from './baseUrl' export * from './docsUrl' export * from './formatters' +export * from './notifications' diff --git a/ui/src/utils/notifications.js b/ui/src/utils/notifications.js new file mode 100644 index 00000000..f732534d --- /dev/null +++ b/ui/src/utils/notifications.js @@ -0,0 +1,12 @@ +export const sendNotification = (title, body = '', image = '') => { + checkForNotificationPermission() + new Notification(title, { + body: body, + icon: image, + silent: true + }) +} + +const checkForNotificationPermission = () => { + return 'Notification' in window && Notification.permission === 'granted' +}