From 03e64ff8680cd214f61fd56a2b6049c82b1b2d36 Mon Sep 17 00:00:00 2001 From: pabera <1260686+pabera@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:35:04 +0100 Subject: [PATCH] New card actions: play, pause, prev, next, toggle, repeat, shuffle (#2179) * Adding additional player controls to assign to cards * streamline some RPC command namings across jukebox and webapp * Fix typos * Fix Repeat toggles * Fix toggle_shuffle * Fix typo in command * Fix another typo * Fix wrong parameter for shuffle * Update German translation for single-repeat * Simplify Shuffle option * Simplify Repeat option * Streamline Player functions with new set_ methods in jukebox * Abstract OptionsSelector * Refactor some code * Fix indentation * Rename set_ methods * Undo some doc changes --- documentation/developers/docker.md | 15 ++++- documentation/developers/status.md | 7 +- src/jukebox/components/playermpd/__init__.py | 55 +++++++++++++-- src/jukebox/components/rpc_command_alias.py | 18 +++++ src/webapp/public/locales/de/translation.json | 44 +++++++++--- src/webapp/public/locales/en/translation.json | 45 ++++++++++--- src/webapp/src/commands/index.js | 13 +++- .../Cards/controls/actions/audio/index.js | 34 +++++++++- .../actions/audio/slider-change-volume.js | 4 +- .../controls/actions/synchronisation/index.js | 11 ++- .../rfidcards/change-on-rfid-scan-options.js | 67 ------------------- .../Cards/controls/options-selector.js | 61 +++++++++++++++++ src/webapp/src/components/Player/controls.js | 34 +++++----- src/webapp/src/components/Player/cover.js | 6 +- src/webapp/src/components/Player/index.js | 2 +- src/webapp/src/components/Player/seekbar.js | 2 +- src/webapp/src/config.js | 9 ++- src/webapp/src/i18n.js | 1 + 18 files changed, 297 insertions(+), 131 deletions(-) delete mode 100644 src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js create mode 100644 src/webapp/src/components/Cards/controls/options-selector.js diff --git a/documentation/developers/docker.md b/documentation/developers/docker.md index d7242750e..80651ce84 100644 --- a/documentation/developers/docker.md +++ b/documentation/developers/docker.md @@ -47,7 +47,20 @@ They can be run individually or in combination. To do that, we use ### Mac 1. [Install Docker & Compose (Mac)](https://docs.docker.com/docker-for-mac/install/) -2. [Install pulseaudio](https://gist.github.com/seongyongkim/b7d630a03e74c7ab1c6b53473b592712) (Other references: [[1]](https://devops.datenkollektiv.de/running-a-docker-soundbox-on-mac.html), [[2]](https://stackoverflow.com/a/50939994/1062438)) +2. Install pulseaudio + 1. Use Homebrew to install + ``` + $ brew install pulseaudio + ``` + 2. Enable pulseaudio network capabilities. In an editor, open `/opt/homebrew/Cellar/pulseaudio/16.1/etc/pulse/default.pa` (you might need to adapt this path to your own system settings). Uncomment the following line. + ``` + load-module module-native-protocol-tcp + ``` + 3. Restart the pulseaudio service + ``` + $ brew services restart pulseaudio + ``` + 4. If you have trouble with your audio, try these resources to troubleshoot: [[1]](https://gist.github.com/seongyongkim/b7d630a03e74c7ab1c6b53473b592712), [[2]](https://devops.datenkollektiv.de/running-a-docker-soundbox-on-mac.html), [[3]](https://stackoverflow.com/a/50939994/1062438) > [!NOTE] > In order for Pulseaudio to work properly with Docker on your Mac, you need to start Pulseaudio in a specific way. Otherwise MPD will throw an exception. See [Pulseaudio issues on Mac](#pulseaudio-issue-on-mac) for more info. diff --git a/documentation/developers/status.md b/documentation/developers/status.md index 2fb92897f..48c2b6c3b 100644 --- a/documentation/developers/status.md +++ b/documentation/developers/status.md @@ -107,10 +107,9 @@ Topics marked _in progress_ are already in the process of implementation by comm - [ ] Folder configuration (_in progress_) - [ ] [Reference](https://github.com/MiczFlor/RPi-Jukebox-RFID/wiki/MANUAL#manage-playout-behaviour) - [ ] Resume: Save and restore position (how interact with shuffle?) - - [ ] Single: Enable mpc single - - [ ] Shuffle: Enable mpc random (not shuffle) - - Rename to random, as this is mpc random - - [ ] Loop: Loop playlist + - [ ] Repeat Playlist + - [ ] Repeat Song + - [ ] Shuffle ### MPD Player diff --git a/src/jukebox/components/playermpd/__init__.py b/src/jukebox/components/playermpd/__init__.py index e854ee5ee..c77c21051 100644 --- a/src/jukebox/components/playermpd/__init__.py +++ b/src/jukebox/components/playermpd/__init__.py @@ -335,11 +335,6 @@ def seek(self, new_time): with self.mpd_lock: self.mpd_client.seekcur(new_time) - @plugs.tag - def shuffle(self, random): - # As long as we don't work with waiting lists (aka playlist), this implementation is ok! - self.mpd_retry_with_mutex(self.mpd_client.random, 1 if random else 0) - @plugs.tag def rewind(self): """ @@ -363,7 +358,6 @@ def replay(self): @plugs.tag def toggle(self): """Toggle pause state, i.e. do a pause / resume depending on current state""" - logger.debug("Toggle") with self.mpd_lock: self.mpd_client.pause() @@ -378,8 +372,27 @@ def replay_if_stopped(self): if self.mpd_status['state'] == 'stop': self.play_folder(self.music_player_status['player_status']['last_played_folder']) + # Shuffle + def _shuffle(self, random): + # As long as we don't work with waiting lists (aka playlist), this implementation is ok! + self.mpd_retry_with_mutex(self.mpd_client.random, 1 if random else 0) + @plugs.tag - def repeatmode(self, mode): + def shuffle(self, option='toggle'): + if option == 'toggle': + if self.mpd_status['random'] == '0': + self._shuffle(1) + else: + self._shuffle(0) + elif option == 'enable': + self._shuffle(1) + elif option == 'disable': + self._shuffle(0) + else: + logger.error(f"'{option}' does not exist for 'shuffle'") + + # Repeat + def _repeatmode(self, mode): if mode == 'repeat': repeat = 1 single = 0 @@ -394,6 +407,34 @@ def repeatmode(self, mode): self.mpd_client.repeat(repeat) self.mpd_client.single(single) + @plugs.tag + def repeat(self, option='toggle'): + if option == 'toggle': + if self.mpd_status['repeat'] == '0': + self._repeatmode('repeat') + elif self.mpd_status['repeat'] == '1' and self.mpd_status['single'] == '0': + self._repeatmode('single') + else: + self._repeatmode(None) + elif option == 'toggle_repeat': + if self.mpd_status['repeat'] == '0': + self._repeatmode('repeat') + else: + self._repeatmode(None) + elif option == 'toggle_repeat_single': + if self.mpd_status['single'] == '0': + self._repeatmode('single') + else: + self._repeatmode(None) + elif option == 'enable_repeat': + self._repeatmode('repeat') + elif option == 'enable_repeat_single': + self._repeatmode('single') + elif option == 'disable': + self._repeatmode(None) + else: + logger.error(f"'{option}' does not exist for 'repeat'") + @plugs.tag def get_current_song(self, param): return self.mpd_status diff --git a/src/jukebox/components/rpc_command_alias.py b/src/jukebox/components/rpc_command_alias.py index bb484891a..e56727ff4 100644 --- a/src/jukebox/components/rpc_command_alias.py +++ b/src/jukebox/components/rpc_command_alias.py @@ -36,6 +36,12 @@ 'package': 'player', 'plugin': 'ctrl', 'method': 'play_folder'}, + 'play': { + 'package': 'player', + 'plugin': 'ctrl', + 'method': 'play', + 'note': 'Play the currently selected song', + 'ignore_card_removal_action': True}, 'pause': { 'package': 'player', 'plugin': 'ctrl', @@ -57,6 +63,18 @@ 'plugin': 'ctrl', 'method': 'toggle', 'ignore_card_removal_action': True}, + 'shuffle': { + 'package': 'player', + 'plugin': 'ctrl', + 'method': 'shuffle', + 'note': 'Shuffle', + 'ignore_card_removal_action': True}, + 'repeat': { + 'package': 'player', + 'plugin': 'ctrl', + 'method': 'repeat', + 'note': 'Repeat', + 'ignore_card_removal_action': True}, # VOLUME 'set_volume': { diff --git a/src/webapp/public/locales/de/translation.json b/src/webapp/public/locales/de/translation.json index 85d39b879..f3bd89782 100644 --- a/src/webapp/public/locales/de/translation.json +++ b/src/webapp/public/locales/de/translation.json @@ -23,7 +23,14 @@ "timer_fade_volume": "Fade volume", "toggle_output": "Audio-Ausgang umschalten", "sync_rfidcards_all": "Alle Audiodateien und Karteneinträge synchronisieren", - "sync_rfidcards_change_on_rfid_scan": "Aktivierung ändern für 'on RFID scan' " + "sync_rfidcards_change_on_rfid_scan": "Aktivierung ändern für 'on RFID scan'", + "next_song": "Nächster Song", + "pause": "Pause", + "play": "Abspielen", + "prev_song": "Vorheriger Song", + "shuffle": "Zufallswiedergabe", + "repeat": "Wiedergabe wiederholen", + "toggle": "Abspielen/Pause umschalten" } }, "controls-selector": { @@ -56,8 +63,25 @@ "no-music-selected": "Es ist keine Musik ausgewählt.", "loading-song-error": "Während des Ladens des Songs ist ein Fehler aufgetreten." }, - "volume": { - "title": "Lautstärke Stufen" + "audio": { + "repeat": { + "description": "Wähle den zu setzenden Status.", + "label-toggle": "Umschalten - Schaltet durch 1) Wiedergabeliste Wiederholen, 2) Song Wiederholen, 3) Wiederholen Deaktiveren", + "label-toggle-repeat": "Wiedergabeliste Wiederholen umschalten", + "label-toggle-repeat-single": "Song Wiederholen umschalten", + "label-enable-repeat": "Wiedergabeliste Wiederholen aktivieren", + "label-enable-repeat-single": "Song Wiederholen aktivieren", + "label-disable": "Wiederholen deaktivieren" + }, + "shuffle": { + "description": "Wähle den zu setzenden Status.", + "label-toggle": "Umschalten", + "label-enable": "Aktivieren", + "label-disable": "Deaktivieren" + }, + "volume": { + "title": "Lautstärke Stufen" + } }, "timers": { "description": "Wähle die Anzahl der Minuten nachdem die Aktion ausgeführt werden soll." @@ -138,17 +162,17 @@ "player": { "controls": { "shuffle": { - "activate": "Shuffle aktivieren", - "deactivate": "Shuffle deaktivieren" + "enable": "Zufallswiedergabe aktivieren", + "disable": "Zufallswiedergabe deaktivieren" }, - "skip": "Zurück", + "prev_song": "Zurück", "play": "Abspielen", "pause": "Pause", - "next": "Weiter", + "next_song": "Weiter", "repeat": { - "activate": "Wiederholen aktivieren", - "activate-single": "1 Wiederholen aktivieren", - "deactivate": "Wiederholen deaktivieren" + "enable": "Wiedergabeliste Wiederholen aktivieren", + "enable-single": "Song Wiederholen aktivieren", + "disable": "Wiederholen deaktivieren" } }, "cover": { diff --git a/src/webapp/public/locales/en/translation.json b/src/webapp/public/locales/en/translation.json index d03941eb6..348d3771d 100644 --- a/src/webapp/public/locales/en/translation.json +++ b/src/webapp/public/locales/en/translation.json @@ -23,7 +23,14 @@ "timer_fade_volume": "Fade volume", "toggle_output": "Toggle audio output", "sync_rfidcards_all": "Sync all audiofiles and card entries", - "sync_rfidcards_change_on_rfid_scan": "Change activation of 'on RFID scan'" + "sync_rfidcards_change_on_rfid_scan": "Change activation of 'on RFID scan'", + "next_song": "Next song", + "pause": "Pause", + "play": "Play", + "prev_song": "Previous song", + "shuffle": "Shuffle", + "repeat": "Repeat", + "toggle": "Toggle Play/Pause" } }, "controls-selector": { @@ -56,8 +63,25 @@ "no-music-selected": "No music selected", "loading-song-error": "An error occurred while loading song." }, - "volume": { - "title": "Volume steps" + "audio": { + "repeat": { + "description": "Choose the state to set.", + "label-toggle": "Toggle - Loops through 1) Repeat playlist, 2) Repeat song, 3) Disable repeat", + "label-toggle-repeat": "Toggle Repeat Playlist", + "label-toggle-repeat-single": "Toggle Repeat Song", + "label-enable-repeat": "Enable Repeat Playlist", + "label-enable-repeat-single": "Enable Repeat Song", + "label-disable": "Disable" + }, + "shuffle": { + "description": "Choose the state to set.", + "label-toggle": "Toggle", + "label-enable": "Enable", + "label-disable": "Disable" + }, + "volume": { + "title": "Volume steps" + } }, "timers": { "description": "Choose the amount of minutes you want the action to be performed." @@ -138,17 +162,17 @@ "player": { "controls": { "shuffle": { - "activate": "Activate shuffle", - "deactivate": "Deactivate shuffle" + "enable": "Enable shuffle", + "disable": "Disable shuffle" }, - "skip": "Skip previous track", + "prev_song": "Skip to previous track", "play": "Play", "pause": "Pause", - "next": "Skip next track", + "next_song": "Skip to next track", "repeat": { - "activate": "Activate repeat", - "activate-single": "Activate single repeat", - "deactivate": "Deactivate repeat" + "enable": "Enable Playlist repeat", + "enable-single": "Enable Song repeat", + "disable": "Disable repeat" } }, "cover": { @@ -207,6 +231,7 @@ }, "secondswipe": { "title": "Second Swipe", + "description": "Second action after the same card swiped again", "restart": "Restart playlist", "toggle": "Toggle pause / play", "skip": "Skip to next track", diff --git a/src/webapp/src/commands/index.js b/src/webapp/src/commands/index.js index 2352e46b7..8c844d8da 100644 --- a/src/webapp/src/commands/index.js +++ b/src/webapp/src/commands/index.js @@ -82,25 +82,32 @@ const commands = { plugin: 'ctrl', method: 'pause', }, - previous: { + prev_song: { _package: 'player', plugin: 'ctrl', method: 'prev', }, - next: { + next_song: { _package: 'player', plugin: 'ctrl', method: 'next', }, + toggle: { + _package: 'player', + plugin: 'ctrl', + method: 'toggle', + }, shuffle: { _package: 'player', plugin: 'ctrl', method: 'shuffle', + argKeys: ['option'], }, repeat: { _package: 'player', plugin: 'ctrl', - method: 'repeatmode', + method: 'repeat', + argKeys: ['option'], }, seek: { _package: 'player', diff --git a/src/webapp/src/components/Cards/controls/actions/audio/index.js b/src/webapp/src/components/Cards/controls/actions/audio/index.js index 1d3ddc465..aba96cdc2 100644 --- a/src/webapp/src/components/Cards/controls/actions/audio/index.js +++ b/src/webapp/src/components/Cards/controls/actions/audio/index.js @@ -2,10 +2,11 @@ import React from 'react'; import CommandSelector from '../../command-selector'; import SliderChangeVolume from './slider-change-volume'; +import OptionsSelector from '../../options-selector'; import { getActionAndCommand } from '../../../utils'; -const SelectVolume = ({ +const SelectAudioVolume = ({ actionData, handleActionDataChange, }) => { @@ -23,8 +24,37 @@ const SelectVolume = ({ handleActionDataChange={handleActionDataChange} /> } + {command === 'shuffle' && + + } + {command === 'repeat' && + + } ); }; -export default SelectVolume; \ No newline at end of file +export default SelectAudioVolume; \ No newline at end of file diff --git a/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js b/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js index 2e70b489a..64f5edada 100644 --- a/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js +++ b/src/webapp/src/components/Cards/controls/actions/audio/slider-change-volume.js @@ -37,12 +37,12 @@ const SliderChangeVolume = ({ - {t('cards.controls.actions.volume.title')} + {t('cards.controls.actions.audio.volume.title')} {command === 'sync_rfidcards_change_on_rfid_scan' && - } diff --git a/src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js b/src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js deleted file mode 100644 index 6d2b2fde6..000000000 --- a/src/webapp/src/components/Cards/controls/actions/synchronisation/rfidcards/change-on-rfid-scan-options.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - Grid, - FormControl, - FormControlLabel, - Radio, - RadioGroup, - Typography, -} from '@mui/material'; - -import { - getActionAndCommand, - getArgsValues, -} from '../../../../utils'; - - -const ChangeOnRfidScan = ({ - actionData, - handleActionDataChange, -}) => { - const { t } = useTranslation(); - - const { action, command } = getActionAndCommand(actionData); - const [option] = getArgsValues(actionData); - - const onChange = (event, option) => { - handleActionDataChange(action, command, { option }) - }; - - return ( - - - - {t('cards.controls.actions.synchronisation.rfidcards.description')} - - - - } - label={t('cards.controls.actions.synchronisation.rfidcards.label-toggle')} - value="toggle" - /> - } - label={t('cards.controls.actions.synchronisation.rfidcards.label-enable')} - value="enable" - /> - } - label={t('cards.controls.actions.synchronisation.rfidcards.label-disable')} - value="disable" - /> - - - - - ); -}; - -export default ChangeOnRfidScan; diff --git a/src/webapp/src/components/Cards/controls/options-selector.js b/src/webapp/src/components/Cards/controls/options-selector.js new file mode 100644 index 000000000..7a2fd1b22 --- /dev/null +++ b/src/webapp/src/components/Cards/controls/options-selector.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + Grid, + FormControl, + FormControlLabel, + Radio, + RadioGroup, + Typography, +} from '@mui/material'; + +import { + getActionAndCommand, + getArgsValues, +} from '../utils.js'; + +const OptionsSelector = ({ + actionType, + actionData, + handleActionDataChange, + optionLabel, + options, +}) => { + const { t } = useTranslation(); + const { action, command } = getActionAndCommand(actionData); + const [option] = getArgsValues(actionData); + + const onChange = (event, option) => { + handleActionDataChange(action, command, { option }) + }; + + return ( + + + + {t(optionLabel)} + + + + {options.map(({ labelKey, value }) => ( + } + label={t(labelKey)} + value={value} + /> + ))} + + + + + ); +}; + +export default OptionsSelector; diff --git a/src/webapp/src/components/Player/controls.js b/src/webapp/src/components/Player/controls.js index dc433cef8..48a587a32 100644 --- a/src/webapp/src/components/Player/controls.js +++ b/src/webapp/src/components/Player/controls.js @@ -32,15 +32,11 @@ const Controls = () => { } = state; const toggleShuffle = () => { - request('shuffle', { random: !isShuffle }); + request('shuffle', { option: 'toggle' }); } const toggleRepeat = () => { - let mode = null; - if (!isRepeat && !isSingle) mode = 'repeat'; - if (isRepeat && !isSingle) mode = 'single'; - - request('repeat', { mode }); + request('repeat', { option: 'toggle' }); } useEffect(() => { @@ -58,14 +54,14 @@ const Controls = () => { const labelShuffle = () => ( isShuffle - ? t('player.controls.shuffle.deactivate') - : t('player.controls.shuffle.activate') + ? t('player.controls.shuffle.disable') + : t('player.controls.shuffle.enable') ); const labelRepeat = () => { - if (!isRepeat) return t('player.controls.repeat.activate'); - if (isRepeat && !isSingle) return t('player.controls.repeat.activate-single'); - if (isRepeat && isSingle) return t('player.controls.repeat.deactivate'); + if (!isRepeat) return t('player.controls.repeat.enable'); + if (isRepeat && !isSingle) return t('player.controls.repeat.enable-single'); + if (isRepeat && isSingle) return t('player.controls.repeat.disable'); }; return ( @@ -89,14 +85,14 @@ const Controls = () => { - {/* Skip previous track */} + {/* Skip to previous song */} request('previous')} + onClick={e => request('prev_song')} size="large" sx={iconStyles} - title={t('player.controls.skip')} + title={t('player.controls.prev_song')} > @@ -127,14 +123,14 @@ const Controls = () => { } - {/* Skip next track */} + {/* Skip to next song */} request('next')} + onClick={e => request('next_song')} size="large" sx={iconStyles} - title={t('player.controls.next')} + title={t('player.controls.next_song')} > diff --git a/src/webapp/src/components/Player/cover.js b/src/webapp/src/components/Player/cover.js index d20133acb..a744b7b5f 100644 --- a/src/webapp/src/components/Player/cover.js +++ b/src/webapp/src/components/Player/cover.js @@ -33,7 +33,11 @@ const Cover = ({ coverImage }) => { {t('player.cover.title')}} {!coverImage && { if (result) { setCoverImage(`/cover-cache/${result}`); setBackgroundImage([ - 'linear-gradient(to bottom, rgba(18, 18, 18, 0.7), rgba(18, 18, 18, 1))', + 'linear-gradient(to bottom, rgba(18, 18, 18, 0.5), rgba(18, 18, 18, 1))', `url(/cover-cache/${result})` ].join(',')); }; diff --git a/src/webapp/src/components/Player/seekbar.js b/src/webapp/src/components/Player/seekbar.js index aab3682ee..b138bf827 100644 --- a/src/webapp/src/components/Player/seekbar.js +++ b/src/webapp/src/components/Player/seekbar.js @@ -69,7 +69,7 @@ const SeekBar = () => { direction="row" justifyContent="space-between" sx={ { - marginTop: '-20px', + marginTop: '-10px', }} > diff --git a/src/webapp/src/config.js b/src/webapp/src/config.js index 35b5a980e..482db205e 100644 --- a/src/webapp/src/config.js +++ b/src/webapp/src/config.js @@ -42,7 +42,14 @@ const JUKEBOX_ACTIONS_MAP = { audio: { commands: { change_volume: {}, - toggle_output: {} + toggle_output: {}, + play: {}, + pause: {}, + toggle: {}, + next_song: {}, + prev_song: {}, + shuffle: {}, + repeat: {}, }, }, diff --git a/src/webapp/src/i18n.js b/src/webapp/src/i18n.js index b2a752fdd..d374abae3 100644 --- a/src/webapp/src/i18n.js +++ b/src/webapp/src/i18n.js @@ -18,6 +18,7 @@ i18n // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: true, + // lng: 'en', fallbackLng: 'en', interpolation: { escapeValue: false, // not needed for react as it escapes by default