├── .clang-format ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── codecov.yml ├── include ├── audio │ ├── base │ │ ├── analyzer.h │ │ ├── decoder.h │ │ ├── notifier.h │ │ └── playback.h │ ├── command.h │ ├── driver │ │ ├── alsa.h │ │ ├── ffmpeg.h │ │ └── fftw.h │ ├── lyric │ │ ├── base │ │ │ ├── html_parser.h │ │ │ └── url_fetcher.h │ │ ├── driver │ │ │ ├── curl_wrapper.h │ │ │ └── libxml_wrapper.h │ │ ├── lyric_finder.h │ │ └── search_config.h │ └── player.h ├── debug │ ├── dummy_analyzer.h │ ├── dummy_decoder.h │ ├── dummy_fetcher.h │ ├── dummy_parser.h │ └── dummy_playback.h ├── middleware │ └── media_controller.h ├── model │ ├── application_error.h │ ├── audio_filter.h │ ├── bar_animation.h │ ├── block_identifier.h │ ├── playlist.h │ ├── playlist_operation.h │ ├── question_data.h │ ├── song.h │ └── volume.h ├── util │ ├── arg_parser.h │ ├── file_handler.h │ ├── formatter.h │ ├── logger.h │ └── sink.h └── view │ ├── base │ ├── block.h │ ├── custom_event.h │ ├── dialog.h │ ├── element.h │ ├── event_dispatcher.h │ ├── keybinding.h │ ├── notifier.h │ └── terminal.h │ ├── block │ ├── file_info.h │ ├── main_content.h │ ├── main_content │ │ ├── audio_equalizer.h │ │ ├── song_lyric.h │ │ └── spectrum_visualizer.h │ ├── media_player.h │ ├── sidebar.h │ └── sidebar_content │ │ ├── list_directory.h │ │ └── playlist_viewer.h │ └── element │ ├── button.h │ ├── error_dialog.h │ ├── focus_controller.h │ ├── help_dialog.h │ ├── internal │ ├── base_menu.h │ ├── file_menu.h │ ├── playlist_menu.h │ └── song_menu.h │ ├── menu.h │ ├── playlist_dialog.h │ ├── question_dialog.h │ ├── style.h │ ├── tab.h │ ├── text_animation.h │ └── util.h ├── sonar-project.properties ├── src ├── CMakeLists.txt ├── audio │ ├── command.cc │ ├── driver │ │ ├── alsa.cc │ │ ├── ffmpeg.cc │ │ └── fftw.cc │ ├── lyric │ │ ├── driver │ │ │ ├── curl_wrapper.cc │ │ │ └── libxml_wrapper.cc │ │ ├── lyric_finder.cc │ │ └── search_config.cc │ └── player.cc ├── main.cc ├── middleware │ └── media_controller.cc ├── model │ ├── audio_filter.cc │ ├── bar_animation.cc │ ├── block_identifier.cc │ ├── playlist.cc │ ├── playlist_operation.cc │ ├── question_data.cc │ └── song.cc ├── util │ ├── arg_parser.cc │ ├── file_handler.cc │ ├── logger.cc │ └── sink.cc └── view │ ├── base │ ├── block.cc │ ├── custom_event.cc │ ├── dialog.cc │ ├── element.cc │ ├── keybinding.cc │ └── terminal.cc │ ├── block │ ├── file_info.cc │ ├── main_content.cc │ ├── main_content │ │ ├── audio_equalizer.cc │ │ ├── song_lyric.cc │ │ └── spectrum_visualizer.cc │ ├── media_player.cc │ ├── sidebar.cc │ └── sidebar_content │ │ ├── list_directory.cc │ │ └── playlist_viewer.cc │ └── element │ ├── button.cc │ ├── error_dialog.cc │ ├── focus_controller.cc │ ├── help_dialog.cc │ ├── internal │ ├── file_menu.cc │ ├── playlist_menu.cc │ └── song_menu.cc │ ├── menu.cc │ ├── playlist_dialog.cc │ ├── question_dialog.cc │ ├── tab.cc │ └── text_animation.cc └── test ├── CMakeLists.txt ├── audio_lyric_finder.cc ├── audio_player.cc ├── block_file_info.cc ├── block_main_content.cc ├── block_media_player.cc ├── block_sidebar.cc ├── dialog_playlist.cc ├── driver_fftw.cc ├── general ├── block.h ├── dialog.h ├── sync_testing.h └── utils.h ├── middleware_media_controller.cc ├── mock ├── analyzer_mock.h ├── audio_control_mock.h ├── decoder_mock.h ├── event_dispatcher_mock.h ├── file_handler_mock.h ├── html_parser_mock.h ├── interface_notifier_mock.h ├── lyric_finder_mock.h ├── playback_mock.h └── url_fetcher_mock.h └── util_argparser.cc /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | Standard: c++17 3 | ColumnLimit: 100 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .vscode 3 | build 4 | compile_commands.json 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(spectrum) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | 6 | # Export compilation database for external tools 7 | set(CMAKE_EXPORT_COMPILE_COMMANDS 8 | ON 9 | CACHE INTERNAL "") 10 | 11 | # Use pkg-config and FetchContent modules for CMake build 12 | find_package(PkgConfig REQUIRED) 13 | include(FetchContent) 14 | 15 | # Add compiler options for code coverage 16 | function(check_coverage library) 17 | if(ENABLE_COVERAGE) 18 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES 19 | ".*Clang") 20 | target_compile_options(${library} INTERFACE --coverage -O0 -g) 21 | target_link_libraries(${library} INTERFACE --coverage) 22 | endif() 23 | endif() 24 | endfunction() 25 | 26 | # Build options 27 | option(SPECTRUM_DEBUG 28 | "Set to ON to build without external dependencies (ALSA, FFmpeg, FFTW3)" 29 | OFF) 30 | option(ENABLE_TESTS "Set to ON to build executable for unit testing" OFF) 31 | option(ENABLE_COVERAGE "Set to ON to build tests with coverage" OFF) 32 | option(ENABLE_INSTALL "Generate the install target" ON) 33 | 34 | if(SPECTRUM_DEBUG) 35 | message(STATUS "Enabling debug mode...") 36 | add_definitions(-DSPECTRUM_DEBUG) 37 | endif() 38 | 39 | # Build application 40 | add_subdirectory(src) 41 | 42 | if(ENABLE_TESTS AND NOT SPECTRUM_DEBUG) 43 | message(STATUS "Enabling tests...") 44 | add_definitions(-DENABLE_TESTS) 45 | add_subdirectory(test) 46 | endif() 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vinicius Moura Longaray. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | :headphones: spectrum 4 |
5 |

6 | 7 |

A simple and intuitive console-based music player written in C++

8 | 9 | https://github.com/v1nns/spectrum/assets/22479290/5ab537cf-34d6-4627-8d66-4f7128cd6915 10 | 11 | Introducing yet another music player for tech enthusiasts that will simplify the way you experience your favorite tunes! Immerse yourself in the sound with the powerful equalizer, allowing you to fine-tune every aspect of the music to your exact specifications, perfectly matching your mood. 12 | 13 | With an intuitive user interface and lightning-fast performance, this music player is the perfect addition to any audiophile's collection. Whether you're a casual listener or a serious music lover, this console-based music player will exceed your expectations. 14 | 15 | ## Features :speech_balloon: 16 | 17 | - Simple and intuitive terminal user interface; 18 | - Plays music in any format; 19 | - Basic playback controls such as play, pause, stop, and skip; 20 | - Displays information about the currently playing track; 21 | - Audio spectrum visualizer; 22 | - Audio equalizer; 23 | - Fetch song lyrics; 24 | - Support for playlists. 25 | 26 | ## Installation :floppy_disk: 27 | 28 | ### AUR (using yay) 29 | 30 | If you're using Arch Linux or any derivative, you can install spectrum using yay, a popular AUR helper: 31 | 32 | ```bash 33 | # Install the latest version 34 | yay -S spectrum-git 35 | ``` 36 | 37 | ### Flatpak 38 | 39 | To install Spectrum using Flatpak: 40 | 41 | 1. Add the Flathub repository (if not already added): 42 | ```bash 43 | flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 44 | ``` 45 | 46 | 2. Install spectrum: 47 | ```bash 48 | flatpak install flathub io.github.v1nns.spectrum 49 | ``` 50 | 51 | ## Development :memo: 52 | 53 | To build spectrum, you need to have a C++ compiler installed on your system. 54 | 55 | ```bash 56 | # Package dependencies (on Ubuntu) 57 | sudo apt install build-essential libasound2-dev libavcodec-dev \ 58 | libavfilter-dev libavformat-dev libfftw3-dev libswresample-dev \ 59 | libcurl4-openssl-dev libxml++2.6-dev 60 | 61 | # Clone repository 62 | git clone https://github.com/v1nns/spectrum.git 63 | cd spectrum 64 | 65 | # Generate build system in the build directory 66 | cmake -S . -B build 67 | 68 | # Build executable 69 | cmake --build build 70 | 71 | # Install to /usr/local/bin/ (optional) 72 | sudo cmake --install build 73 | 74 | # OR just execute it 75 | ./build/src/spectrum 76 | ``` 77 | 78 | To ensure that any new implementation won't impact the existing one, you may execute unit tests to check that. To enable unit testing, you should compile with the following settings: 79 | 80 | ```bash 81 | # Generate build system for testing/debugging 82 | cmake -S . -B build -DENABLE_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -G Ninja 83 | 84 | # Execute unit tests 85 | cmake --build build && ./build/test/test 86 | 87 | # For manual testing, you may take a look in the log file 88 | cmake --build build && ./build/src/spectrum -l /tmp/log.txt 89 | ``` 90 | 91 | ## Credits :placard: 92 | 93 | This software uses the following open source packages: 94 | 95 | - [FFmpeg](https://ffmpeg.org/) 96 | - [FFTW](https://www.fftw.org/) 97 | - [curl](https://curl.se/) 98 | - [libxml++](https://libxmlplusplus.github.io/libxmlplusplus/) 99 | - [FTXUI](https://github.com/ArthurSonzogni/FTXUI) 100 | - [cava](https://github.com/karlstav/cava) (visualizer is based on cava implementation) 101 | - [json](https://github.com/nlohmann/json) 102 | 103 | ## Contributing 104 | 105 | Contributions are always welcome! If you find any bugs or have suggestions for new features, please open an issue or submit a pull request. 106 | 107 | ## License 108 | 109 | This project is licensed under the MIT License. See the LICENSE file for details. 110 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # ignored files for code coverage 2 | ignore: 3 | - "src/main.cc" 4 | -------------------------------------------------------------------------------- /include/audio/base/analyzer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for audio analyzer support 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_BASE_ANALYZER_H_ 7 | #define INCLUDE_AUDIO_BASE_ANALYZER_H_ 8 | 9 | #include "model/application_error.h" 10 | 11 | namespace driver { 12 | 13 | /** 14 | * @brief Common interface to execute frequency analysis on audio data 15 | */ 16 | class Analyzer { 17 | public: 18 | /** 19 | * @brief Construct a new Analyzer object 20 | */ 21 | Analyzer() = default; 22 | 23 | /** 24 | * @brief Destroy the Analyzer object 25 | */ 26 | virtual ~Analyzer() = default; 27 | 28 | /* ******************************************************************************************** */ 29 | //! Public API 30 | 31 | /** 32 | * @brief Initialize internal structures for audio analysis 33 | * @param output_size Size for output vector from Execute 34 | */ 35 | virtual error::Code Init(int output_size) = 0; 36 | 37 | /** 38 | * @brief Run FFT on input vector to get information about audio in the frequency domain 39 | * @param in Input vector with audio raw data (signal amplitude) 40 | * @param size Input vector size 41 | * @param out Output vector where each entry represents a frequency bar 42 | */ 43 | virtual error::Code Execute(double *in, int size, double *out) = 0; 44 | 45 | /** 46 | * @brief Get internal buffer size 47 | * @return Maximum size for input vector 48 | */ 49 | virtual int GetBufferSize() = 0; 50 | 51 | /** 52 | * @brief Get output buffer size 53 | * @return Size for output vector (considering number of bars multiplied per number of channels) 54 | */ 55 | virtual int GetOutputSize() = 0; 56 | }; 57 | 58 | } // namespace driver 59 | #endif // INCLUDE_AUDIO_BASE_ANALYZER_H_ 60 | -------------------------------------------------------------------------------- /include/audio/base/decoder.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for decoder support 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_BASE_DECODER_H_ 7 | #define INCLUDE_AUDIO_BASE_DECODER_H_ 8 | 9 | #include 10 | 11 | #include "model/application_error.h" 12 | #include "model/audio_filter.h" 13 | #include "model/song.h" 14 | #include "model/volume.h" 15 | 16 | namespace driver { 17 | 18 | /** 19 | * @brief Common interface to read audio file as an input stream, decode it, apply biquad IIR 20 | * filters on extracted audio data and finally, send the result to audio callback 21 | */ 22 | class Decoder { 23 | public: 24 | /** 25 | * @brief Construct a new Decoder object 26 | */ 27 | Decoder() = default; 28 | 29 | /** 30 | * @brief Destroy the Decoder object 31 | */ 32 | virtual ~Decoder() = default; 33 | 34 | /* ******************************************************************************************** */ 35 | //! Public API for Decoder 36 | 37 | /** 38 | * @brief Function invoked after resample is available. 39 | * (for better understanding: take a look at Audio Loop from Player, and also Playback class) 40 | */ 41 | using AudioCallback = std::function; 42 | 43 | /** 44 | * @brief Open file as input stream and check for codec compatibility for decoding 45 | * @param audio_info (In/Out) In case of success, this is filled with detailed audio information 46 | * @return error::Code Application error code 47 | */ 48 | virtual error::Code OpenFile(model::Song& audio_info) = 0; 49 | 50 | /** 51 | * @brief Decode and resample input stream to desired sample format/rate 52 | * @param samples Maximum value of samples 53 | * @param callback Pass resamples to this callback 54 | * @return error::Code Application error code 55 | */ 56 | virtual error::Code Decode(int samples, AudioCallback callback) = 0; 57 | 58 | /** 59 | * @brief After file is opened and decoded, or when some error occurs, always clear internal cache 60 | */ 61 | virtual void ClearCache() = 0; 62 | 63 | /* ******************************************************************************************** */ 64 | //! Public API for Equalizer TODO: split into a new header along with FFmpeg class 65 | 66 | /** 67 | * @brief Set volume on playback stream 68 | * 69 | * @param value Desired volume (in a range between 0.f and 1.f) 70 | * @return error::Code Decoder error converted to application error code 71 | */ 72 | virtual error::Code SetVolume(model::Volume value) = 0; 73 | 74 | /** 75 | * @brief Get volume from playback stream 76 | * @return model::Volume Volume percentage (in a range between 0.f and 1.f) 77 | */ 78 | virtual model::Volume GetVolume() const = 0; 79 | 80 | /** 81 | * @brief Update audio filters in the filter chain (used for equalization) 82 | * 83 | * @param filters Audio filters 84 | * @return error::Code Decoder error converted to application error code 85 | */ 86 | virtual error::Code UpdateFilters(const model::EqualizerPreset& filters) = 0; 87 | }; 88 | 89 | } // namespace driver 90 | #endif // INCLUDE_AUDIO_BASE_DECODER_H_ 91 | -------------------------------------------------------------------------------- /include/audio/base/notifier.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for sending actions from GUI to Audio Player 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_BASE_NOTIFIER_H_ 7 | #define INCLUDE_AUDIO_BASE_NOTIFIER_H_ 8 | 9 | #include 10 | 11 | #include "model/audio_filter.h" 12 | #include "model/playlist.h" 13 | #include "model/volume.h" 14 | 15 | namespace audio { 16 | 17 | /** 18 | * @brief Interface class to notify an action to Audio Player 19 | */ 20 | class Notifier { 21 | public: 22 | /** 23 | * @brief Construct a new Notifier object 24 | */ 25 | Notifier() = default; 26 | 27 | /** 28 | * @brief Destroy the Notifier object 29 | */ 30 | virtual ~Notifier() = default; 31 | 32 | /* ******************************************************************************************** */ 33 | //! Public API 34 | 35 | /** 36 | * @brief Notify Audio Player about file selected by user on Terminal User Interface (TUI) 37 | * @param file Full path to file (may be a song or not) 38 | */ 39 | virtual void NotifyFileSelection(const std::filesystem::path& file) = 0; 40 | 41 | /** 42 | * @brief Notify Audio Player to pause/resume the current song 43 | */ 44 | virtual void PauseOrResume() = 0; 45 | 46 | /** 47 | * @brief Notify Audio Player to stop the current song 48 | */ 49 | virtual void Stop() = 0; 50 | 51 | /** 52 | * @brief Notify Audio Player to set volume 53 | * @param value Sound volume information 54 | */ 55 | virtual void SetVolume(model::Volume value) = 0; 56 | 57 | /** 58 | * @brief Notify Audio Player to resize quantity of frequency bars as result from audio analysis 59 | * @param value Maximum quantity of frequency bars 60 | */ 61 | virtual void ResizeAnalysisOutput(int value) = 0; 62 | 63 | /** 64 | * @brief Notify Audio Player to seek forward position in current playing song 65 | * @param value Offset value 66 | */ 67 | virtual void SeekForwardPosition(int value) = 0; 68 | 69 | /** 70 | * @brief Notify Audio Player to seek backward position in current playing song 71 | * @param value Offset value 72 | */ 73 | virtual void SeekBackwardPosition(int value) = 0; 74 | 75 | /** 76 | * @brief Notify Audio Player to apply audio filters in the audio chain 77 | * @param frequencies Vector of audio filters 78 | */ 79 | virtual void ApplyAudioFilters(const model::EqualizerPreset& filters) = 0; 80 | 81 | /** 82 | * @brief Notify Audio Player about playlist selected by user 83 | * @param playlist Song queue 84 | */ 85 | virtual void NotifyPlaylistSelection(const model::Playlist& playlist) = 0; 86 | }; 87 | 88 | } // namespace audio 89 | #endif // INCLUDE_AUDIO_BASE_NOTIFIER_H_ 90 | -------------------------------------------------------------------------------- /include/audio/base/playback.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for playback support 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_BASE_PLAYBACK_H_ 7 | #define INCLUDE_AUDIO_BASE_PLAYBACK_H_ 8 | 9 | #include 10 | 11 | #include "model/application_error.h" 12 | #include "model/volume.h" 13 | 14 | namespace driver { 15 | 16 | /** 17 | * @brief Common interface to create and handle playback audio stream 18 | */ 19 | class Playback { 20 | public: 21 | /** 22 | * @brief Construct a new Playback object 23 | */ 24 | Playback() = default; 25 | 26 | /** 27 | * @brief Destroy the Playback object 28 | */ 29 | virtual ~Playback() = default; 30 | 31 | /* ******************************************************************************************** */ 32 | //! Public API 33 | 34 | /** 35 | * @brief Create a Playback Stream 36 | * @return error::Code Playback error converted to application error code 37 | */ 38 | virtual error::Code CreatePlaybackStream() = 0; 39 | 40 | /** 41 | * @brief Configure Playback Stream parameters (sample format, etc...) 42 | * @return error::Code Playback error converted to application error code 43 | */ 44 | virtual error::Code ConfigureParameters() = 0; 45 | 46 | /** 47 | * @brief Make playback stream ready to play 48 | * @return error::Code Playback error converted to application error code 49 | */ 50 | virtual error::Code Prepare() = 0; 51 | 52 | /** 53 | * @brief Pause current song on playback stream 54 | * @return error::Code Playback error converted to application error code 55 | */ 56 | virtual error::Code Pause() = 0; 57 | 58 | /** 59 | * @brief Stop playing song on playback stream 60 | * @return error::Code Playback error converted to application error code 61 | */ 62 | virtual error::Code Stop() = 0; 63 | 64 | /** 65 | * @brief Directly write audio buffer to playback stream (this should be called by decoder) 66 | * 67 | * @param buffer Audio data buffer 68 | * @param size Buffer size 69 | * @return error::Code Playback error converted to application error code 70 | */ 71 | virtual error::Code AudioCallback(void* buffer, int size) = 0; 72 | 73 | /** 74 | * @brief Set volume on playback stream 75 | * 76 | * @param value Desired volume (in a range between 0.f and 1.f) 77 | * @return error::Code Playback error converted to application error code 78 | */ 79 | virtual error::Code SetVolume(model::Volume value) = 0; 80 | 81 | /** 82 | * @brief Get volume from playback stream 83 | * @return model::Volume Volume percentage (in a range between 0.f and 1.f) 84 | */ 85 | virtual model::Volume GetVolume() = 0; 86 | 87 | /** 88 | * @brief Get period size 89 | * @return uint32_t Period size 90 | */ 91 | virtual uint32_t GetPeriodSize() const = 0; 92 | }; 93 | 94 | } // namespace driver 95 | #endif // INCLUDE_AUDIO_BASE_PLAYBACK_H_ 96 | -------------------------------------------------------------------------------- /include/audio/command.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Structure for an audio player command 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_COMMAND_H_ 7 | #define INCLUDE_AUDIO_COMMAND_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "model/audio_filter.h" 15 | #include "model/volume.h" 16 | 17 | namespace audio { 18 | 19 | /** 20 | * @brief Interface for commands to be handled by audio player 21 | */ 22 | struct Command { 23 | //! Identifier for all existing events 24 | enum class Identifier { 25 | None = 8000, 26 | Play = 8001, 27 | PauseOrResume = 8002, 28 | Stop = 8003, 29 | SeekForward = 8004, 30 | SeekBackward = 8005, 31 | SetVolume = 8006, 32 | UpdateAudioFilters = 8007, 33 | Exit = 8008, 34 | }; 35 | 36 | //! Overloaded operators 37 | friend bool operator==(const Command& lhs, const Command& rhs) { return lhs.id == rhs.id; } 38 | friend bool operator!=(const Command& lhs, const Command& rhs) { return lhs.id != rhs.id; } 39 | friend bool operator==(const Command& lhs, const Command::Identifier& rhs) { 40 | return lhs.id == rhs; 41 | } 42 | friend bool operator!=(const Command& lhs, const Command::Identifier& rhs) { 43 | return lhs.id != rhs; 44 | } 45 | 46 | //! Output command to ostream 47 | friend std::ostream& operator<<(std::ostream& out, const Command& cmd); 48 | friend std::ostream& operator<<(std::ostream& out, const std::vector& cmds); 49 | 50 | //! Possible commands to be handled by audio player 51 | static Command None(); 52 | static Command Play(const std::string& filepath = ""); 53 | static Command PauseOrResume(); 54 | static Command Stop(); 55 | static Command SeekForward(int offset); 56 | static Command SeekBackward(int offset); 57 | static Command SetVolume(const model::Volume& value); 58 | static Command UpdateAudioFilters(const model::EqualizerPreset& filters); 59 | static Command Exit(); 60 | 61 | //! Possible types for content 62 | using Content = 63 | std::variant; 64 | 65 | //! Getter for command identifier 66 | Identifier GetId() const { return id; } 67 | 68 | //! Generic getter for command content 69 | template 70 | T GetContent() const { 71 | if (std::holds_alternative(content)) { 72 | return std::get(content); 73 | } 74 | return T(); 75 | } 76 | 77 | //! Variables 78 | // P.S. removed private keyword, otherwise wouldn't be possible to use C++ brace initialization 79 | Identifier id; //!< Unique type identifier for Command 80 | Content content; //!< Wrapper for content 81 | }; 82 | 83 | } // namespace audio 84 | 85 | #endif // INCLUDE_AUDIO_COMMAND_H_ 86 | -------------------------------------------------------------------------------- /include/audio/driver/alsa.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class to support using ALSA driver 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_DRIVER_ALSA_H_ 7 | #define INCLUDE_AUDIO_DRIVER_ALSA_H_ 8 | 9 | #include 10 | 11 | #include 12 | 13 | #include "audio/base/playback.h" 14 | #include "model/application_error.h" 15 | 16 | namespace driver { 17 | 18 | /** 19 | * @brief Provides an interface to use ALSA library for handling audio with hardware 20 | */ 21 | class Alsa final : public Playback { 22 | public: 23 | /** 24 | * @brief Construct a new Alsa object 25 | */ 26 | Alsa() = default; 27 | 28 | /** 29 | * @brief Destroy the Alsa object 30 | */ 31 | ~Alsa() override = default; 32 | 33 | /* ******************************************************************************************** */ 34 | //! Public API 35 | /** 36 | * @brief Create a Playback Stream using ALSA API 37 | * @return error::Code Playback error converted to application error code 38 | */ 39 | error::Code CreatePlaybackStream() override; 40 | 41 | /** 42 | * @brief Configure Playback Stream parameters (sample format, etc...) using ALSA API 43 | * @return error::Code Playback error converted to application error code 44 | */ 45 | error::Code ConfigureParameters() override; 46 | 47 | /** 48 | * @brief Ask ALSA API to make playback stream ready to play 49 | * @return error::Code Playback error converted to application error code 50 | */ 51 | error::Code Prepare() override; 52 | 53 | /** 54 | * @brief Pause current song on playback stream 55 | * @return error::Code Playback error converted to application error code 56 | */ 57 | error::Code Pause() override; 58 | 59 | /** 60 | * @brief Stop playing song on playback stream 61 | * @return error::Code Playback error converted to application error code 62 | */ 63 | error::Code Stop() override; 64 | 65 | /** 66 | * @brief Directly write audio buffer to playback stream (this should be called by decoder) 67 | * 68 | * @param buffer Audio data buffer 69 | * @param size Buffer size 70 | * @return error::Code Playback error converted to application error code 71 | */ 72 | error::Code AudioCallback(void* buffer, int size) override; 73 | 74 | /** 75 | * @brief Set volume on playback stream 76 | * 77 | * @param value Desired volume (in a range between 0.f and 1.f) 78 | * @return error::Code Playback error converted to application error code 79 | */ 80 | error::Code SetVolume(model::Volume value) override; 81 | 82 | /** 83 | * @brief Get volume from playback stream 84 | * @return model::Volume Volume percentage (in a range between 0.f and 1.f) 85 | */ 86 | model::Volume GetVolume() override; 87 | 88 | /** 89 | * @brief Get period size (previously filled by ALSA API) 90 | * @return uint32_t Period size 91 | */ 92 | uint32_t GetPeriodSize() const override { return (uint32_t)period_size_; } 93 | 94 | /* ******************************************************************************************** */ 95 | //! Utility 96 | private: 97 | /** 98 | * @brief Find and return master playback from High level control interface from ALSA (p.s.: not 99 | * necessary the use of smart pointers here because this resource is managed by ALSA) 100 | */ 101 | snd_mixer_elem_t* GetMasterPlayback(); 102 | 103 | /* ******************************************************************************************** */ 104 | //! Default Constants for Audio Parameters 105 | static constexpr const char kSelemName[] = "Master"; 106 | static constexpr int kChannels = 2; 107 | static constexpr int kSampleRate = 44100; 108 | static constexpr snd_pcm_format_t kSampleFormat = SND_PCM_FORMAT_S16_LE; 109 | 110 | /* ******************************************************************************************** */ 111 | //! Custom declarations with deleters 112 | struct PcmDeleter { 113 | void operator()(snd_pcm_t* p) const { 114 | snd_pcm_drain(p); 115 | snd_pcm_close(p); 116 | } 117 | }; 118 | 119 | struct MixerDeleter { 120 | void operator()(snd_mixer_t* p) const { snd_mixer_close(p); } 121 | }; 122 | 123 | using PcmPlayback = std::unique_ptr; 124 | 125 | using MixerControl = std::unique_ptr; 126 | 127 | /* ******************************************************************************************** */ 128 | //! Variables 129 | 130 | PcmPlayback playback_handle_; //! Playback stream handled by ALSA API 131 | MixerControl mixer_; //! High level control interface from ALSA API (to manage volume) 132 | snd_pcm_uframes_t period_size_ = 0; //! Period size (necessary in order to discover buffer size) 133 | }; 134 | 135 | } // namespace driver 136 | #endif // INCLUDE_AUDIO_DRIVER_ALSA_H_ 137 | -------------------------------------------------------------------------------- /include/audio/lyric/base/html_parser.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for HTML parsing support 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_LYRIC_BASE_HTML_PARSER_H_ 7 | #define INCLUDE_AUDIO_LYRIC_BASE_HTML_PARSER_H_ 8 | 9 | #include 10 | #include 11 | 12 | namespace lyric { 13 | 14 | //! SongLyric declaration 15 | using SongLyric = std::vector; 16 | 17 | } // namespace lyric 18 | 19 | namespace driver { 20 | 21 | /** 22 | * @brief Common interface to parse HTML content into a DOM tree 23 | */ 24 | class HtmlParser { 25 | public: 26 | /** 27 | * @brief Construct a new HtmlParser object 28 | */ 29 | HtmlParser() = default; 30 | 31 | /** 32 | * @brief Destroy the HtmlParser object 33 | */ 34 | virtual ~HtmlParser() = default; 35 | 36 | /* ******************************************************************************************** */ 37 | //! Public API 38 | 39 | /** 40 | * @brief Parse buffer data based on the given XPath 41 | * @param data Buffer data 42 | * @param xpath XPath to find 43 | * @return Song lyrics parsed from buffer data 44 | */ 45 | virtual lyric::SongLyric Parse(const std::string &data, const std::string &xpath) = 0; 46 | }; 47 | 48 | } // namespace driver 49 | #endif // INCLUDE_AUDIO_LYRIC_BASE_HTML_PARSER_H_ 50 | -------------------------------------------------------------------------------- /include/audio/lyric/base/url_fetcher.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for URL fetching support 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_LYRIC_BASE_URL_FETCHER_H_ 7 | #define INCLUDE_AUDIO_LYRIC_BASE_URL_FETCHER_H_ 8 | 9 | #include 10 | 11 | #include "model/application_error.h" 12 | 13 | namespace driver { 14 | 15 | /** 16 | * @brief Common interface to fetch content from URL 17 | */ 18 | class UrlFetcher { 19 | public: 20 | /** 21 | * @brief Construct a new UrlFetcher object 22 | */ 23 | UrlFetcher() = default; 24 | 25 | /** 26 | * @brief Destroy the UrlFetcher object 27 | */ 28 | virtual ~UrlFetcher() = default; 29 | 30 | /* ******************************************************************************************** */ 31 | //! Public API 32 | 33 | /** 34 | * @brief Fetch content from the given URL 35 | * @param URL Endpoint address 36 | * @param output Output from fetch (out) 37 | * @return Error code from operation 38 | */ 39 | virtual error::Code Fetch(const std::string &URL, std::string &output) = 0; 40 | }; 41 | 42 | } // namespace driver 43 | #endif // INCLUDE_AUDIO_LYRIC_BASE_URL_FETCHER_H_ 44 | -------------------------------------------------------------------------------- /include/audio/lyric/driver/curl_wrapper.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class to wrap CURL funcionalities 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_LYRIC_CURL_WRAPPER_H_ 7 | #define INCLUDE_AUDIO_LYRIC_CURL_WRAPPER_H_ 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "audio/lyric/base/url_fetcher.h" 16 | #include "model/application_error.h" 17 | 18 | namespace driver { 19 | 20 | /** 21 | * @brief Class to manage CURL resources and perform content fetching from the given URL 22 | */ 23 | class CURLWrapper : public driver::UrlFetcher { 24 | // The Accept request HTTP header indicates which content types, expressed as MIME types, the 25 | // client is able to understand 26 | static constexpr std::string_view kAcceptType = 27 | "Accept:text/html,application/xhtml+xml,application/xml"; 28 | 29 | // The User-Agent request header is a characteristic string that lets servers and network peers 30 | // identify the application, operating system, vendor and version of the requesting user agent. 31 | static constexpr std::string_view kUserAgent = 32 | "User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.17 (KHTML, like Gecko) " 33 | "Chrome/24.0.1312.70 Safari/537.17"; 34 | 35 | public: 36 | /** 37 | * @brief Fetch content from the given URL 38 | * @param URL Endpoint address 39 | * @param output Output from fetch (out) 40 | * @return Error code from operation 41 | */ 42 | error::Code Fetch(const std::string &URL, std::string &output) override; 43 | 44 | private: 45 | /** 46 | * @brief This callback function gets called by libcurl as soon as there is data received that 47 | * needs to be saved. For most transfers, this callback gets called many times and each invoke 48 | * delivers another chunk of data. 49 | * @param buffer Pointer to delivered data 50 | * @param size Value always equal to 1 51 | * @param nmemb Size of data 52 | * @param data Output buffer 53 | * @return Real size from received data 54 | */ 55 | static size_t WriteCallback(const char *buffer, size_t size, size_t nmemb, void *data); 56 | 57 | //! Smart pointer to manage CURL resource 58 | using SmartCURL = std::unique_ptr; 59 | }; 60 | 61 | } // namespace driver 62 | #endif // INCLUDE_AUDIO_LYRIC_CURL_WRAPPER_H_ 63 | -------------------------------------------------------------------------------- /include/audio/lyric/driver/libxml_wrapper.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class to wrap libxml++ funcionalities 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_LYRIC_LIBXML_WRAPPER_H_ 7 | #define INCLUDE_AUDIO_LYRIC_LIBXML_WRAPPER_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | 16 | #include "audio/lyric/base/html_parser.h" 17 | 18 | namespace driver { 19 | 20 | /** 21 | * @brief Class to manage libxml++ resources and perform content parsing 22 | */ 23 | class LIBXMLWrapper : public driver::HtmlParser { 24 | public: 25 | /** 26 | * @brief Parse buffer data based on the given XPath 27 | * @param data Buffer data 28 | * @param xpath XPath to find 29 | * @return Song lyrics parsed from buffer data 30 | */ 31 | lyric::SongLyric Parse(const std::string &data, const std::string &xpath) override; 32 | 33 | private: 34 | /** 35 | * @brief Filter only text nodes to emplace back on lyrics 36 | * @param node XML node 37 | * @param lyric Song lyrics (out) 38 | */ 39 | void ScrapContent(const xmlpp::Node *node, lyric::SongLyric &lyric); 40 | 41 | //! Smart pointer to manage libxml resources 42 | using XmlDocGuard = std::unique_ptr; 43 | }; 44 | 45 | } // namespace driver 46 | #endif // INCLUDE_AUDIO_LYRIC_LIBXML_WRAPPER_H_ 47 | -------------------------------------------------------------------------------- /include/audio/lyric/lyric_finder.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for Lyric Finder 4 | */ 5 | 6 | #ifndef INCLUDE_AUDIO_LYRIC_LYRIC_FINDER_H_ 7 | #define INCLUDE_AUDIO_LYRIC_LYRIC_FINDER_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include "audio/lyric/base/html_parser.h" 13 | #include "audio/lyric/base/url_fetcher.h" 14 | #include "audio/lyric/search_config.h" 15 | 16 | #ifdef ENABLE_TESTS 17 | namespace { 18 | class LyricFinderTest; 19 | } 20 | #endif 21 | 22 | namespace lyric { 23 | 24 | /** 25 | * @brief Responsible to fetch content from search engines and web scrap song lyrics from it 26 | */ 27 | class LyricFinder { 28 | /** 29 | * @brief Construct a new LyricFinder object 30 | * @param fetcher Pointer to URL fetcher interface 31 | * @param parser Pointer to HTML parser interface 32 | */ 33 | explicit LyricFinder(std::unique_ptr&& fetcher, 34 | std::unique_ptr&& parser); 35 | 36 | protected: 37 | /** 38 | * @brief Construct a new LyricFinder object 39 | */ 40 | LyricFinder() = default; 41 | 42 | public: 43 | /** 44 | * @brief Factory method: Create, initialize internal components and return LyricFinder object 45 | * @param fetcher Pass fetcher to be used within LyricFinder (optional) 46 | * @param parser Pass parser to be used within LyricFinder (optional) 47 | * @return std::unique_ptr LyricFinder instance 48 | */ 49 | static std::unique_ptr Create(driver::UrlFetcher* fetcher = nullptr, 50 | driver::HtmlParser* parser = nullptr); 51 | 52 | /** 53 | * @brief Destroy the LyricFinder object 54 | */ 55 | virtual ~LyricFinder() = default; 56 | 57 | //! Remove these 58 | LyricFinder(const LyricFinder& other) = delete; // copy constructor 59 | LyricFinder(LyricFinder&& other) = delete; // move constructor 60 | LyricFinder& operator=(const LyricFinder& other) = delete; // copy assignment 61 | LyricFinder& operator=(LyricFinder&& other) = delete; // move assignment 62 | 63 | /* ******************************************************************************************** */ 64 | //! Public API 65 | 66 | /** 67 | * @brief Search for lyrics by fetching the search engine and web scraping it 68 | * @param artist Artist name 69 | * @param title Song name 70 | * @return Song lyrics 71 | */ 72 | virtual SongLyric Search(const std::string& artist, const std::string& title); 73 | 74 | /* ******************************************************************************************** */ 75 | //! Variables 76 | private: 77 | Config engines_ = SearchConfig::Create(); //!< Search engine settings 78 | std::unique_ptr fetcher_; //!< URL fetcher 79 | std::unique_ptr parser_; //!< HTML parser 80 | 81 | /* ******************************************************************************************** */ 82 | //! Friend class for testing purpose 83 | 84 | #ifdef ENABLE_TESTS 85 | friend class ::LyricFinderTest; 86 | #endif 87 | }; 88 | 89 | } // namespace lyric 90 | #endif // INCLUDE_AUDIO_LYRIC_LYRIC_FINDER_H_ 91 | -------------------------------------------------------------------------------- /include/debug/dummy_analyzer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Dummy class for audio analyzer support 4 | */ 5 | 6 | #ifndef INCLUDE_DEBUG_DUMMY_ANALYZER_H_ 7 | #define INCLUDE_DEBUG_DUMMY_ANALYZER_H_ 8 | 9 | #include "audio/base/analyzer.h" 10 | #include "model/application_error.h" 11 | 12 | namespace driver { 13 | 14 | /** 15 | * @brief Dummy implementation 16 | */ 17 | class DummyAnalyzer : public Analyzer { 18 | public: 19 | /** 20 | * @brief Construct a new Analyzer object 21 | */ 22 | DummyAnalyzer() = default; 23 | 24 | /** 25 | * @brief Destroy the Analyzer object 26 | */ 27 | virtual ~DummyAnalyzer() = default; 28 | 29 | /* ******************************************************************************************** */ 30 | //! Public API 31 | 32 | /** 33 | * @brief Initialize internal structures for audio analysis 34 | * 35 | * @param output_size Size for output vector from Execute 36 | */ 37 | error::Code Init(int output_size) override { 38 | output_size_ = output_size; 39 | return error::kSuccess; 40 | } 41 | 42 | /** 43 | * @brief Run FFT on input vector to get information about audio in the frequency domain 44 | * 45 | * @param in Input vector with audio raw data (signal amplitude) 46 | * @param size Input vector size 47 | * @param out Output vector where each entry represents a frequency bar 48 | */ 49 | error::Code Execute(double *in, int size, double *out) override { return error::kSuccess; } 50 | 51 | /** 52 | * @brief Get internal buffer size 53 | * 54 | * @return Maximum size for input vector 55 | */ 56 | int GetBufferSize() override { return kBufferSize; } 57 | 58 | /** 59 | * @brief Get output buffer size 60 | * 61 | * @return Size for output vector (considering number of bars multiplied per number of channels) 62 | */ 63 | int GetOutputSize() override { return output_size_; } 64 | 65 | /* *********************************************************************************************/ 66 | //! Default Constants 67 | private: 68 | static constexpr int kBufferSize = 1024; //!< Base size for buffers 69 | 70 | /* ******************************************************************************************** */ 71 | //! Variables 72 | private: 73 | int output_size_; //!< Maximum output size from audio analysis 74 | }; 75 | 76 | } // namespace driver 77 | #endif // INCLUDE_DEBUG_DUMMY_ANALYZER_H_ 78 | -------------------------------------------------------------------------------- /include/debug/dummy_decoder.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Dummy class for decoder support 4 | */ 5 | 6 | #ifndef INCLUDE_DEBUG_DUMMY_DECODER_H_ 7 | #define INCLUDE_DEBUG_DUMMY_DECODER_H_ 8 | 9 | #include 10 | 11 | #include "audio/base/decoder.h" 12 | #include "model/application_error.h" 13 | #include "model/audio_filter.h" 14 | #include "model/song.h" 15 | #include "model/volume.h" 16 | #include "util/file_handler.h" 17 | 18 | namespace driver { 19 | 20 | /** 21 | * @brief Dummy implementation 22 | */ 23 | class DummyDecoder : public Decoder { 24 | public: 25 | /** 26 | * @brief Construct a new Decoder object 27 | */ 28 | DummyDecoder() = default; 29 | 30 | /** 31 | * @brief Destroy the Decoder object 32 | */ 33 | virtual ~DummyDecoder() = default; 34 | 35 | /* ******************************************************************************************** */ 36 | //! Public API that do not follow the instance lifecycle 37 | 38 | /** 39 | * @brief Check if file contains an available audio stream 40 | * @param file Full path to file 41 | * @return true if file contains an audio stream, false otherwise 42 | */ 43 | static inline bool ContainsAudioStream(const util::File& file) { return true; } 44 | 45 | /* ******************************************************************************************** */ 46 | //! Public API for Decoder 47 | 48 | /** 49 | * @brief Function invoked after resample is available. 50 | * (for better understanding: take a look at Audio Loop from Player, and also Playback class) 51 | */ 52 | using AudioCallback = std::function; 53 | 54 | /** 55 | * @brief Open file as input stream and check for codec compatibility for decoding 56 | * @param audio_info (In/Out) In case of success, this is filled with detailed audio information 57 | * @return error::Code Application error code 58 | */ 59 | error::Code OpenFile(model::Song& audio_info) override { 60 | audio_info = model::Song{.artist = "Dummy artist", 61 | .title = "Dummy title", 62 | .num_channels = 2, 63 | .sample_rate = 44100, 64 | .bit_rate = 320000, 65 | .bit_depth = 32, 66 | .duration = 120}; 67 | 68 | return error::kSuccess; 69 | } 70 | 71 | /** 72 | * @brief Decode and resample input stream to desired sample format/rate 73 | * @param samples Maximum value of samples 74 | * @param callback Pass resamples to this callback 75 | * @return error::Code Application error code 76 | */ 77 | error::Code Decode(int samples, AudioCallback callback) override { 78 | callback((void*)nullptr, 0, position_); 79 | return error::kSuccess; 80 | } 81 | 82 | /** 83 | * @brief After file is opened and decoded, or when some error occurs, always clear internal cache 84 | */ 85 | void ClearCache() override {} 86 | 87 | /* ******************************************************************************************** */ 88 | //! Public API for Equalizer 89 | 90 | /** 91 | * @brief Set volume on playback stream 92 | * 93 | * @param value Desired volume (in a range between 0.f and 1.f) 94 | * @return error::Code Decoder error converted to application error code 95 | */ 96 | error::Code SetVolume(model::Volume value) override { 97 | volume_ = value; 98 | return error::kSuccess; 99 | } 100 | 101 | /** 102 | * @brief Get volume from playback stream 103 | * @return model::Volume Volume percentage (in a range between 0.f and 1.f) 104 | */ 105 | model::Volume GetVolume() const override { return volume_; } 106 | 107 | /** 108 | * @brief Update audio filters in the filter chain (used for equalization) 109 | * 110 | * @param filters Audio filters 111 | * @return error::Code Decoder error converted to application error code 112 | */ 113 | error::Code UpdateFilters(const model::EqualizerPreset& filters) override { 114 | return error::kSuccess; 115 | } 116 | 117 | /* ******************************************************************************************** */ 118 | //! Variables 119 | private: 120 | model::Volume volume_; //!< Playback stream volume 121 | int64_t position_; //!< Audio position 122 | }; 123 | 124 | } // namespace driver 125 | #endif // INCLUDE_DEBUG_DUMMY_DECODER_H_ 126 | -------------------------------------------------------------------------------- /include/debug/dummy_fetcher.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Dummy class for URL fetching support 4 | */ 5 | 6 | #ifndef INCLUDE_DEBUG_DUMMY_FETCHER_H_ 7 | #define INCLUDE_DEBUG_DUMMY_FETCHER_H_ 8 | 9 | #include 10 | 11 | namespace driver { 12 | 13 | /** 14 | * @brief Dummy implementation 15 | */ 16 | class DummyFetcher : public UrlFetcher { 17 | public: 18 | /** 19 | * @brief Construct a new DummyFetcher object 20 | */ 21 | DummyFetcher() = default; 22 | 23 | /** 24 | * @brief Destroy the DummyFetcher object 25 | */ 26 | virtual ~DummyFetcher() = default; 27 | 28 | /* ******************************************************************************************** */ 29 | //! Public API 30 | 31 | /** 32 | * @brief Fetch content from the given URL 33 | * @param URL Endpoint address 34 | * @param output Output from fetch (out) 35 | * @return Error code from operation 36 | */ 37 | error::Code Fetch(const std::string &URL, std::string &output) override { 38 | return error::kSuccess; 39 | } 40 | }; 41 | 42 | } // namespace driver 43 | #endif // INCLUDE_DEBUG_DUMMY_FETCHER_H_ 44 | -------------------------------------------------------------------------------- /include/debug/dummy_parser.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Dummy class for HTML parsing support 4 | */ 5 | 6 | #ifndef INCLUDE_DEBUG_DUMMY_PARSER_H_ 7 | #define INCLUDE_DEBUG_DUMMY_PARSER_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include "model/application_error.h" 13 | 14 | namespace lyric { 15 | 16 | //! SongLyric declaration 17 | using SongLyric = std::vector; 18 | 19 | } // namespace lyric 20 | 21 | namespace driver { 22 | 23 | /** 24 | * @brief Dummy implementation 25 | */ 26 | class DummyParser : public HtmlParser { 27 | public: 28 | /** 29 | * @brief Construct a new DummyParser object 30 | */ 31 | DummyParser() = default; 32 | 33 | /** 34 | * @brief Destroy the DummyParser object 35 | */ 36 | virtual ~DummyParser() = default; 37 | 38 | /* ******************************************************************************************** */ 39 | //! Public API 40 | 41 | /** 42 | * @brief Parse buffer data based on the given XPath 43 | * @param data Buffer data 44 | * @param xpath XPath to find 45 | * @return Song lyrics parsed from buffer data 46 | */ 47 | lyric::SongLyric Parse(const std::string &data, const std::string &xpath) override { 48 | return lyric::SongLyric{}; 49 | } 50 | }; 51 | 52 | } // namespace driver 53 | #endif // INCLUDE_DEBUG_DUMMY_PARSER_H_ 54 | -------------------------------------------------------------------------------- /include/debug/dummy_playback.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for playback support 4 | */ 5 | 6 | #ifndef INCLUDE_DEBUG_DUMMY_PLAYBACK_H_ 7 | #define INCLUDE_DEBUG_DUMMY_PLAYBACK_H_ 8 | 9 | #include "audio/base/playback.h" 10 | #include "model/application_error.h" 11 | #include "model/volume.h" 12 | 13 | namespace driver { 14 | 15 | /** 16 | * @brief Dummy 17 | */ 18 | class DummyPlayback : public Playback { 19 | public: 20 | /** 21 | * @brief Construct a new Playback object 22 | */ 23 | DummyPlayback() = default; 24 | 25 | /** 26 | * @brief Destroy the Playback object 27 | */ 28 | ~DummyPlayback() = default; 29 | 30 | /* ******************************************************************************************** */ 31 | //! Public API 32 | 33 | /** 34 | * @brief Create a Playback Stream 35 | * @return error::Code Playback error converted to application error code 36 | */ 37 | error::Code CreatePlaybackStream() override { return error::kSuccess; } 38 | 39 | /** 40 | * @brief Configure Playback Stream parameters (sample format, etc...) 41 | * @return error::Code Playback error converted to application error code 42 | */ 43 | error::Code ConfigureParameters() override { return error::kSuccess; } 44 | 45 | /** 46 | * @brief Make playback stream ready to play 47 | * @return error::Code Playback error converted to application error code 48 | */ 49 | error::Code Prepare() override { return error::kSuccess; } 50 | 51 | /** 52 | * @brief Pause current song on playback stream 53 | * @return error::Code Playback error converted to application error code 54 | */ 55 | error::Code Pause() override { return error::kSuccess; } 56 | 57 | /** 58 | * @brief Stop playing song on playback stream 59 | * @return error::Code Playback error converted to application error code 60 | */ 61 | error::Code Stop() override { return error::kSuccess; } 62 | 63 | /** 64 | * @brief Directly write audio buffer to playback stream (this should be called by decoder) 65 | * 66 | * @param buffer Audio data buffer 67 | * @param size Buffer size 68 | * @return error::Code Playback error converted to application error code 69 | */ 70 | error::Code AudioCallback(void* buffer, int size) override { return error::kSuccess; } 71 | 72 | /** 73 | * @brief Set volume on playback stream 74 | * 75 | * @param value Desired volume (in a range between 0.f and 1.f) 76 | * @return error::Code Playback error converted to application error code 77 | */ 78 | error::Code SetVolume(model::Volume value) override { return error::kSuccess; } 79 | 80 | /** 81 | * @brief Get volume from playback stream 82 | * @return model::Volume Volume percentage (in a range between 0.f and 1.f) 83 | */ 84 | model::Volume GetVolume() override { return model::Volume(); } 85 | 86 | /** 87 | * @brief Get period size 88 | * @return uint32_t Period size 89 | */ 90 | uint32_t GetPeriodSize() const override { return kPeriodSize; } 91 | 92 | /* ******************************************************************************************** */ 93 | //! Constants 94 | private: 95 | static constexpr uint32_t kPeriodSize = 1024; 96 | }; 97 | 98 | } // namespace driver 99 | #endif // INCLUDE_DEBUG_DUMMY_PLAYBACK_H_ 100 | -------------------------------------------------------------------------------- /include/model/application_error.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief All error codes from application in a single map 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_APPLICATION_ERROR_H_ 7 | #define INCLUDE_MODEL_APPLICATION_ERROR_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace error { 15 | 16 | //! To make life easier in the first versions, error is simple an int 17 | // TODO: next step is to add a level (like critical or non-critical, warning, ...) 18 | using Code = int; 19 | 20 | //! Everything fine! 21 | static constexpr Code kSuccess = 0; 22 | static constexpr Code kUnknownError = 99; 23 | 24 | //! Terminal errors 25 | static constexpr Code kTerminalInitialization = 1; 26 | static constexpr Code kTerminalColorsUnavailable = 2; 27 | 28 | //! File and directory navigation 29 | static constexpr Code kAccessDirFailed = 20; 30 | 31 | //! Song errors 32 | static constexpr Code kInvalidFile = 30; 33 | static constexpr Code kFileNotSupported = 31; 34 | static constexpr Code kFileCompressionNotSupported = 32; 35 | static constexpr Code kUnknownNumOfChannels = 33; 36 | static constexpr Code kInconsistentHeaderInfo = 34; 37 | static constexpr Code kCorruptedData = 35; 38 | 39 | //! ALSA driver errors 40 | static constexpr Code kSetupAudioParamsFailed = 50; 41 | 42 | //! FFMPEG driver errors 43 | static constexpr Code kDecodeFileFailed = 70; 44 | static constexpr Code kSeekFrameFailed = 71; 45 | 46 | /* ********************************************************************************************** */ 47 | 48 | /** 49 | * @brief Class holding the map with all possible errors that may occur during application lifetime 50 | */ 51 | class ApplicationError { 52 | private: 53 | //! Single entry for error message 54 | using Message = std::pair; 55 | 56 | //! Array similar to a map and contains all "mapped" errors (pun intended) 57 | static constexpr std::array kErrorMap{{ 58 | {kTerminalInitialization, "Cannot initialize screen"}, 59 | {kTerminalColorsUnavailable, "No support to change colors"}, 60 | {kAccessDirFailed, "Cannot access directory"}, 61 | {kInvalidFile, "Invalid file"}, 62 | {kFileNotSupported, "File not supported"}, 63 | {kFileCompressionNotSupported, "Decoding compressed file is not supported"}, 64 | {kUnknownNumOfChannels, 65 | "File does not seem to be neither mono nor stereo (perhaps multi-track or corrupted)"}, 66 | {kInconsistentHeaderInfo, "Header data is inconsistent"}, 67 | {kCorruptedData, "File is corrupted"}, 68 | {kSetupAudioParamsFailed, "Cannot set audio parameters"}, 69 | {kDecodeFileFailed, "Cannot decode song"}, 70 | {kSeekFrameFailed, "Cannot seek frame in song"}, 71 | {kUnknownError, "Unknown error used for almost everything during development =)"}, 72 | }}; 73 | 74 | /* ******************************************************************************************** */ 75 | public: 76 | /** 77 | * @brief Get the error associated to the specific code 78 | * 79 | * @param code Error code 80 | * @return Message Error detail 81 | */ 82 | static std::string_view GetMessage(Code id) { 83 | auto find_error = [&id](Message element) { return element.first == id; }; 84 | 85 | auto error = std::find_if(kErrorMap.begin(), kErrorMap.end(), find_error); 86 | assert(error != kErrorMap.end()); 87 | 88 | return error->second; 89 | } 90 | }; 91 | 92 | } // namespace error 93 | #endif // INCLUDE_MODEL_APPLICATION_ERROR_H_ -------------------------------------------------------------------------------- /include/model/audio_filter.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Base class for an audio filter 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_AUDIO_FILTER_H_ 7 | #define INCLUDE_MODEL_AUDIO_FILTER_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace model { 16 | 17 | namespace equalizer { 18 | static constexpr int kFiltersPerPreset = 10; //!< Maximum number of audio filters for each preset 19 | } // namespace equalizer 20 | 21 | // Forward declaration 22 | struct AudioFilter; 23 | 24 | //! Music-genre name 25 | using MusicGenre = std::string; 26 | 27 | //! Single EQ preset 28 | using EqualizerPreset = std::array; 29 | 30 | //! Map of EQ presets where key is music genre, and value is an EQ preset 31 | using EqualizerPresets = std::map>; 32 | 33 | /** 34 | * @brief Class representing an audio filter, more specifically, a Biquad filter. It is a type of 35 | * digital filter that is widely used in audio processing applications. It is a second-order filter, 36 | * meaning it has two poles and two zeroes in its transfer function. This allows it to have a more 37 | * complex response than a first-order filter. 38 | */ 39 | struct AudioFilter { 40 | //! Constants 41 | static constexpr double kMinGain = -12; //!< Minimum value of gain 42 | static constexpr double kMaxGain = 12; //!< Maximum value of gain 43 | 44 | //! Overloaded operators 45 | friend std::ostream& operator<<(std::ostream& out, const AudioFilter& a); 46 | friend bool operator==(const AudioFilter& lhs, const AudioFilter& rhs); 47 | friend bool operator!=(const AudioFilter& lhs, const AudioFilter& rhs); 48 | 49 | /* ******************************************************************************************** */ 50 | //! Utilities 51 | 52 | /** 53 | * @brief Create a map of presets to use it on GUI 54 | * @return Map of EQ presets 55 | */ 56 | static EqualizerPresets CreatePresets(); 57 | 58 | /** 59 | * @brief Get audio filter name based on cutoff frequency 60 | * @return A string containing filter name using the pattern "freq_123" 61 | */ 62 | std::string GetName() const; 63 | 64 | /** 65 | * @brief Get cutoff frequency of filter 66 | * @return A string containing cutoff frequency 67 | */ 68 | std::string GetFrequency() const; 69 | 70 | /** 71 | * @brief Get filter gain of filter 72 | * @return A string containing filter gain 73 | */ 74 | std::string GetGain() const; 75 | 76 | /** 77 | * @brief Get gain as a percentage considering the min and max values for gain 78 | * @return A percentage value of gain (0~1) 79 | */ 80 | float GetGainAsPercentage() const; 81 | 82 | /** 83 | * @brief Set new value for gain and normalize it based on the range between minimum and maximum 84 | * @param value New value for gain 85 | */ 86 | void SetNormalizedGain(double value); 87 | 88 | /* ******************************************************************************************** */ 89 | //! Variables 90 | 91 | double frequency; //!< Cutoff frequency or center frequency, it is the frequency at which the 92 | //!< filter's response is half the maximum value (measured in Hertz) 93 | double Q = 1.41; //!< Ratio of center frequency to the width of the passband 94 | double gain = 0; //!< Measure of how much the amplitude of the output signal is increased or 95 | //!< decreased relative to the input signal. It is defined as the ratio of the 96 | //!< output signal's amplitude to the input signal's amplitude. 97 | 98 | bool modifiable = false; //!< Control if gain can be modified 99 | }; 100 | 101 | } // namespace model 102 | #endif // INCLUDE_MODEL_AUDIO_FILTER_H_ 103 | -------------------------------------------------------------------------------- /include/model/bar_animation.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Structure for a bar animation 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_BAR_ANIMATION_H_ 7 | #define INCLUDE_MODEL_BAR_ANIMATION_H_ 8 | 9 | #include 10 | 11 | namespace model { 12 | 13 | /** 14 | * @brief Contains bar animations that can be rendered by spectrum visualizer 15 | */ 16 | enum BarAnimation { 17 | HorizontalMirror = 11000, //!< Both channels (L/R) are mirrored horizontally (default) 18 | VerticalMirror = 11001, //!< Both channels (L/R) are mirrored vertically 19 | Mono = 11002, //!< Average from the sum of both channels (L/R) 20 | LAST = 11003, 21 | }; 22 | 23 | //! BarAnimation pretty print 24 | std::ostream& operator<<(std::ostream& out, const BarAnimation& animation); 25 | 26 | } // namespace model 27 | 28 | #endif // INCLUDE_MODEL_BAR_ANIMATION_H_ 29 | -------------------------------------------------------------------------------- /include/model/block_identifier.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Structure for block identification 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_BLOCK_IDENTIFIER_H_ 7 | #define INCLUDE_MODEL_BLOCK_IDENTIFIER_H_ 8 | 9 | #include 10 | 11 | namespace model { 12 | 13 | /** 14 | * @brief Contains an unique ID for each existing UI block 15 | */ 16 | enum class BlockIdentifier { 17 | Sidebar = 201, 18 | FileInfo = 202, 19 | MainContent = 203, 20 | MediaPlayer = 204, 21 | None = 205, 22 | }; 23 | 24 | //! BlockIdentifier pretty print 25 | std::ostream& operator<<(std::ostream& out, const BlockIdentifier& i); 26 | 27 | } // namespace model 28 | 29 | #endif // INCLUDE_MODEL_BLOCK_IDENTIFIER_H_ 30 | -------------------------------------------------------------------------------- /include/model/playlist.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Base class for a playlist 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_PLAYLIST_H_ 7 | #define INCLUDE_MODEL_PLAYLIST_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "model/song.h" 14 | 15 | namespace model { 16 | 17 | /** 18 | * @brief List of audio files to play 19 | */ 20 | struct Playlist { 21 | int index; //!< Index identifier 22 | std::string name; //!< Playlist name 23 | std::deque songs; //!< List of songs 24 | 25 | //! Overloaded operators 26 | friend std::ostream& operator<<(std::ostream& out, const Playlist& p); 27 | friend bool operator==(const Playlist& lhs, const Playlist& rhs); 28 | friend bool operator!=(const Playlist& lhs, const Playlist& rhs); 29 | 30 | /** 31 | * @brief Check if song list is empty 32 | * @return True if empty, otherwise false 33 | */ 34 | bool IsEmpty() const { return songs.empty(); } 35 | 36 | /** 37 | * @brief Returns the first element from the inner deque container with songs 38 | * @warning Be sure to check first if deque contains at least one song 39 | * @return First song in the deque 40 | */ 41 | Song PopFront(); 42 | 43 | //! Pretty-print for testing 44 | friend void PrintTo(const Playlist& p, std::ostream* os); 45 | }; 46 | 47 | using Playlists = std::vector; 48 | 49 | } // namespace model 50 | #endif // INCLUDE_MODEL_PLAYLIST_H_ 51 | -------------------------------------------------------------------------------- /include/model/playlist_operation.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Structure for playlist operation 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_PLAYLIST_OPERATION_H_ 7 | #define INCLUDE_MODEL_PLAYLIST_OPERATION_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include "model/playlist.h" 13 | 14 | namespace model { 15 | 16 | /** 17 | * @brief Wrap playlist data with a CRUD operation (designated to be sent to PlaylistDialog) 18 | */ 19 | struct PlaylistOperation { 20 | enum class Operation { 21 | None = 300, 22 | Create = 301, 23 | Modify = 302, 24 | }; 25 | 26 | Operation action; //!< Operation to execute on playlist dialog 27 | std::optional playlist; //!< Optional playlist to execute operation 28 | 29 | // Util method to get corresponding operation name 30 | static std::string GetActionName(const PlaylistOperation& playlist); 31 | 32 | //! Overloaded operators 33 | friend std::ostream& operator<<(std::ostream& out, const PlaylistOperation& s); 34 | friend bool operator==(const PlaylistOperation& lhs, const PlaylistOperation& rhs); 35 | friend bool operator!=(const PlaylistOperation& lhs, const PlaylistOperation& rhs); 36 | }; 37 | 38 | } // namespace model 39 | 40 | #endif // INCLUDE_MODEL_PLAYLIST_OPERATION_H_ 41 | -------------------------------------------------------------------------------- /include/model/question_data.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Structure for question data 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_QUESTION_DATA_H_ 7 | #define INCLUDE_MODEL_QUESTION_DATA_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | namespace model { 14 | 15 | /** 16 | * @brief Content to display on the QuestionDialog 17 | */ 18 | struct QuestionData { 19 | using Callback = std::function; 20 | 21 | std::string question; //!< Message to display on question dialog 22 | Callback cb_yes; //!< Callback to trigger when user press "Yes" 23 | Callback cb_no; //!< Callback to trigger when user press "No" 24 | 25 | //! Overloaded operators 26 | friend std::ostream& operator<<(std::ostream& out, const QuestionData& s); 27 | friend bool operator==(const QuestionData& lhs, const QuestionData& rhs); 28 | friend bool operator!=(const QuestionData& lhs, const QuestionData& rhs); 29 | }; 30 | 31 | } // namespace model 32 | 33 | #endif // INCLUDE_MODEL_QUESTION_DATA_H_ 34 | -------------------------------------------------------------------------------- /include/model/song.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Base class for a song 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_SONG_H_ 7 | #define INCLUDE_MODEL_SONG_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace model { 16 | 17 | /** 18 | * @brief Detailed audio metadata information from song 19 | */ 20 | struct Song { 21 | std::optional index; //!< Internal index 22 | std::filesystem::path filepath; //!< Full path to file 23 | std::string artist; //!< Song artist name 24 | std::string title; //!< Song title name 25 | 26 | std::optional playlist; //!< Playlist name 27 | 28 | uint16_t num_channels; //!< Number of channels (1=Mono 2=Stereo) 29 | uint32_t sample_rate; //!< Number of samples (of signal amplitude or “sound”) per second 30 | uint32_t bit_rate; //!< Bits per second 31 | uint32_t bit_depth; //!< Number of bits per sample 32 | uint32_t duration; //!< Audio duration (in seconds) 33 | 34 | //! Audio state 35 | enum class MediaState { 36 | Empty = 2001, 37 | Play = 2002, 38 | Pause = 2003, 39 | Stop = 2004, 40 | Finished = 2005, 41 | }; 42 | 43 | struct CurrentInformation { 44 | MediaState state; //!< Current song state 45 | uint32_t position; //!< Current position (in seconds) of the audio 46 | 47 | //! Overloaded operators 48 | friend bool operator==(const CurrentInformation& lhs, const CurrentInformation& rhs); 49 | friend bool operator!=(const CurrentInformation& lhs, const CurrentInformation& rhs); 50 | friend std::ostream& operator<<(std::ostream& out, const CurrentInformation& info); 51 | }; 52 | 53 | CurrentInformation curr_info; //!< Current state of song 54 | 55 | //! Overloaded operators 56 | friend std::ostream& operator<<(std::ostream& out, const Song& s); 57 | friend bool operator==(const Song& lhs, const Song& rhs); 58 | friend bool operator!=(const Song& lhs, const Song& rhs); 59 | 60 | //! Check if song is empty 61 | bool IsEmpty() const; 62 | }; 63 | 64 | /** 65 | * @brief Util method to pretty print Song structure 66 | * @param arg Song struct 67 | * @return std::string Formatted string with properties from Song 68 | */ 69 | std::string to_string(const Song& arg); 70 | 71 | /** 72 | * @brief Util method to pretty print time from Song structure 73 | * @param arg Time (in seconds) 74 | * @return std::string Formatted string with converted time from Song 75 | */ 76 | std::string time_to_string(const uint32_t& arg); 77 | 78 | } // namespace model 79 | #endif // INCLUDE_MODEL_SONG_H_ 80 | -------------------------------------------------------------------------------- /include/model/volume.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Base class for sound volume 4 | */ 5 | 6 | #ifndef INCLUDE_MODEL_VOLUME_H_ 7 | #define INCLUDE_MODEL_VOLUME_H_ 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace model { 17 | 18 | /** 19 | * @brief General sound volume 20 | */ 21 | struct Volume { 22 | public: 23 | //! Default constructor 24 | Volume() : percentage{1.f} {}; 25 | 26 | explicit Volume(float value) : percentage{std::min(std::max(value, 0.f), 1.f)} {} 27 | 28 | // Pre-increment 29 | Volume& operator++() { 30 | percentage += 0.05f; 31 | if (percentage > 1.f) percentage = 1.f; 32 | return *this; 33 | } 34 | 35 | // Post-increment 36 | Volume operator++(int) { 37 | Volume tmp{*this}; // create temporary with old value 38 | operator++(); // perform increment 39 | return tmp; // return temporary 40 | } 41 | 42 | // Pre-decrement 43 | Volume& operator--() { 44 | percentage -= 0.05f; 45 | if (percentage < 0.0f) percentage = 0.0f; 46 | return *this; 47 | } 48 | 49 | // Post-decrement 50 | Volume operator--(int) { 51 | Volume tmp{*this}; // create temporary with old value 52 | operator--(); // perform decrement 53 | return tmp; // return temporary 54 | } 55 | 56 | // Toggle volume mute 57 | void ToggleMute() { muted = !muted; } 58 | 59 | // Get mute state 60 | bool IsMuted() const { return muted; } 61 | 62 | // Convenient conversion to int 63 | explicit operator int() const { return !muted ? (int)round(percentage * 100) : 0; } 64 | 65 | // Convenient conversion to float 66 | explicit operator float() const { return !muted ? percentage : 0.F; } 67 | 68 | // For comparisons 69 | friend bool operator==(const Volume lhs, const Volume rhs) { 70 | return lhs.percentage == rhs.percentage; 71 | } 72 | friend bool operator!=(const Volume lhs, const Volume rhs) { return !(lhs == rhs); } 73 | 74 | // Output to ostream 75 | friend std::ostream& operator<<(std::ostream& out, const Volume& v) { 76 | out << "{volume:" << (int)v << "% "; 77 | out << "muted: " << (v.muted ? "true" : "false") << "}"; 78 | return out; 79 | } 80 | 81 | private: 82 | float percentage; //!< Volume percentage 83 | bool muted = false; //!< Control flag to mute/unmute volume 84 | }; 85 | 86 | /** 87 | * @brief Util method to pretty print Volume structure 88 | * @param arg Volume struct 89 | * @return std::string Formatted string with properties from Volume 90 | */ 91 | inline std::string to_string(const Volume& arg) { 92 | std::ostringstream ss; 93 | ss << (float)arg; 94 | return std::move(ss).str(); 95 | } 96 | 97 | } // namespace model 98 | #endif // INCLUDE_MODEL_VOLUME_H_ 99 | -------------------------------------------------------------------------------- /include/util/file_handler.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for file handling (for operations like listing files in a dir, read/write, etc) 4 | */ 5 | 6 | #ifndef INCLUDE_UTIL_FILE_HANDLER_H_ 7 | #define INCLUDE_UTIL_FILE_HANDLER_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include "model/playlist.h" 13 | 14 | namespace util { 15 | 16 | //! For better readability 17 | using File = std::filesystem::path; //!< Single file path 18 | using Files = std::vector; //!< List of file paths 19 | 20 | /** 21 | * @brief Class responsible to perform any file I/O operation 22 | */ 23 | class FileHandler { 24 | public: 25 | /** 26 | * @brief Construct a new FileHandler object 27 | */ 28 | FileHandler() = default; 29 | 30 | /** 31 | * @brief Destroy the FileHandler object 32 | */ 33 | virtual ~FileHandler() = default; 34 | 35 | //! Remove these 36 | FileHandler(const FileHandler& other) = delete; // copy constructor 37 | FileHandler(FileHandler&& other) = delete; // move constructor 38 | FileHandler& operator=(const FileHandler& other) = delete; // copy assignment 39 | FileHandler& operator=(FileHandler&& other) = delete; // move assignment 40 | 41 | /* ******************************************************************************************** */ 42 | //! Public API 43 | 44 | /** 45 | * @brief Get full path to home directory 46 | * @return String containing directory path 47 | */ 48 | std::string GetHome() const; 49 | 50 | /** 51 | * @brief Get full path to playlist JSON file 52 | * @return String containing filepath 53 | */ 54 | std::string GetPlaylistsPath() const; 55 | 56 | /** 57 | * @brief List all files from the given directory path 58 | * @param dir_path Full path to directory 59 | * @param parsed_files[out] Existing files in the given directory path 60 | * @return true if directory was parsed succesfully, false otherwise 61 | */ 62 | bool ListFiles(const std::filesystem::path& dir_path, Files& parsed_files); 63 | 64 | /** 65 | * @brief Parse playlists from JSON 66 | * @param playlists[out] Playlists object filled by data from JSON parsed 67 | * @return true if JSON was parsed succesfully, false otherwise 68 | */ 69 | virtual bool ParsePlaylists(model::Playlists& playlists); 70 | 71 | /** 72 | * @brief Save playlists to JSON 73 | * @param playlists Playlists object already filled 74 | * @return true if JSON was saved succesfully, false otherwise 75 | */ 76 | virtual bool SavePlaylists(const model::Playlists& playlists); 77 | 78 | /* ******************************************************************************************** */ 79 | //! Internal operations 80 | private: 81 | /** 82 | * @brief Create directory recursively (including all missing parent directories) 83 | * @param path Directory path 84 | * @param error Error code in case of fail 85 | * @return true if directory was created succesfully, false otherwise 86 | */ 87 | bool CreateDirectory(std::string const& path, std::error_code& error); 88 | }; 89 | 90 | } // namespace util 91 | #endif // INCLUDE_UTIL_FILE_HANDLER_H_ 92 | -------------------------------------------------------------------------------- /include/util/sink.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class that represents an output for logging 4 | */ 5 | 6 | #ifndef INCLUDE_UTIL_SINK_H_ 7 | #define INCLUDE_UTIL_SINK_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace util { 15 | 16 | /** 17 | * @brief Interface to manage Sink 18 | */ 19 | class Sink { 20 | public: 21 | virtual ~Sink() = default; 22 | 23 | /* ******************************************************************************************** */ 24 | //! Public API 25 | 26 | virtual void OpenStream() = 0; 27 | virtual void CloseStream() = 0; 28 | 29 | /** 30 | * @brief Forward argument to inner output stream object 31 | * @tparam T Argument typename definition 32 | * @param t Argument 33 | * @return Sink object 34 | */ 35 | template 36 | Sink& operator<<(Arg&& t) { 37 | WriteToStream(std::forward(t)); 38 | return *this; 39 | } 40 | 41 | /* ******************************************************************************************** */ 42 | //! Internal methods 43 | protected: 44 | virtual void WriteToStream([[maybe_unused]] const std::string& message){ 45 | // This method may not be used if there isn't a valid ostream opened 46 | }; 47 | }; 48 | 49 | /* ********************************************************************************************** */ 50 | 51 | /** 52 | * @brief Responsible for controlling the output streambuffer where log messages will be written 53 | * P.S. using CRTP pattern 54 | */ 55 | template 56 | class ImplSink : public Sink { 57 | protected: 58 | //! Construct a new ImplSink object (only done by derived classes) 59 | ImplSink() = default; 60 | 61 | public: 62 | //! Destroy the ImplSink object 63 | ~ImplSink() override { CloseStream(); }; 64 | 65 | //! Remove these 66 | ImplSink(const ImplSink& other) = delete; // copy constructor 67 | ImplSink(ImplSink&& other) = delete; // move constructor 68 | ImplSink& operator=(const ImplSink& other) = delete; // copy assignment 69 | ImplSink& operator=(ImplSink&& other) = delete; // move assignment 70 | 71 | /* ******************************************************************************************** */ 72 | //! Overridden methods 73 | 74 | //! Open output stream 75 | void OpenStream() final { static_cast(*this).Open(); } 76 | 77 | //! Close output stream 78 | void CloseStream() final { static_cast(*this).Close(); } 79 | 80 | private: 81 | //! Write message to output stream 82 | void WriteToStream(const std::string& message) final { 83 | if (out_stream_) { 84 | *out_stream_ << message; 85 | out_stream_->flush(); // In order to ensure that message will be written even if application 86 | // crashes, force a flush to output stream buffer 87 | } 88 | } 89 | 90 | /* ******************************************************************************************** */ 91 | //! Variables 92 | protected: 93 | std::shared_ptr out_stream_; //!< Output stream buffer to write messages 94 | }; 95 | 96 | /* ---------------------------------------------------------------------------------------------- */ 97 | /* FILE LOGGER */ 98 | /* ---------------------------------------------------------------------------------------------- */ 99 | 100 | class FileSink : public ImplSink { 101 | public: 102 | explicit FileSink(const std::string& path); 103 | ~FileSink() override = default; 104 | 105 | //! Required methods 106 | void Open(); 107 | void Close(); 108 | 109 | /* ******************************************************************************************** */ 110 | //! Variables 111 | private: 112 | std::string path_; //!< Absolute path for log file 113 | std::chrono::seconds reopen_interval_ = std::chrono::seconds(300); //!< Interval to reopen file 114 | std::chrono::system_clock::time_point last_reopen_; //!< Last timestamp that file was (re)opened 115 | }; 116 | 117 | /* ---------------------------------------------------------------------------------------------- */ 118 | /* STDOUT LOGGER */ 119 | /* ---------------------------------------------------------------------------------------------- */ 120 | 121 | class ConsoleSink : public ImplSink { 122 | public: 123 | using ImplSink::ImplSink; 124 | 125 | //! Required methods 126 | void Open(); 127 | void Close(); 128 | }; 129 | 130 | } // namespace util 131 | 132 | #endif // INCLUDE_UTIL_SINK_H_ 133 | -------------------------------------------------------------------------------- /include/view/base/block.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Base class for any content block displayed in the UI 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BASE_BLOCK_H_ 7 | #define INCLUDE_VIEW_BASE_BLOCK_H_ 8 | 9 | #include 10 | 11 | #include "ftxui/component/component_base.hpp" 12 | #include "ftxui/component/event.hpp" 13 | #include "ftxui/dom/elements.hpp" 14 | #include "model/block_identifier.h" 15 | #include "view/base/custom_event.h" 16 | 17 | namespace interface { 18 | 19 | //! Forward declaration 20 | class EventDispatcher; 21 | 22 | //! Maximum dimensions for block 23 | struct Size { 24 | int width; //!< Maximum width for block 25 | int height; //!< Maximum height for block 26 | }; 27 | 28 | /** 29 | * @brief Base class representing a block in view 30 | */ 31 | class Block : public std::enable_shared_from_this, public ftxui::ComponentBase { 32 | protected: 33 | /** 34 | * @brief Construct a new Block object (only called by derived classes) 35 | * @param dispatcher Event dispatcher 36 | * @param id Unique ID for block 37 | * @param size Block dimensions 38 | */ 39 | Block(const std::shared_ptr& dispatcher, const model::BlockIdentifier& id, 40 | const Size& size); 41 | 42 | public: 43 | /** 44 | * @brief Destroy the Block object 45 | */ 46 | ~Block() override = default; 47 | 48 | //! Unique ID 49 | model::BlockIdentifier GetId() const { return id_; } 50 | 51 | //! Block size 52 | // TODO: create a header with all size-related constants 53 | Size GetSize() const { return size_; } 54 | 55 | //! Set focus state 56 | void SetFocused(bool focused); 57 | 58 | //! Get focus state 59 | bool IsFocused() const { return focused_; } 60 | 61 | protected: 62 | //! Get decorator style for title based on internal state 63 | ftxui::Decorator GetTitleDecorator() const; 64 | 65 | //! Get decorator style for border based on internal state 66 | ftxui::Decorator GetBorderDecorator() const; 67 | 68 | //! Dispatch event to set focus 69 | void AskForFocus() const; 70 | 71 | /* ******************************************************************************************** */ 72 | //! These must be implemented by derived class 73 | public: 74 | ftxui::Element Render() override { return ftxui::Element(); } 75 | bool OnEvent(ftxui::Event) override { return false; } 76 | virtual bool OnCustomEvent(const CustomEvent&) = 0; 77 | 78 | /* ******************************************************************************************** */ 79 | //! These have optional implementation by derived class 80 | 81 | virtual void OnFocus() { 82 | // This method is called whenever block is focused 83 | } 84 | 85 | virtual void OnLostFocus() { 86 | // This method is called whenever block loses focus 87 | } 88 | 89 | /* ******************************************************************************************** */ 90 | //! Used by derived class 91 | protected: 92 | //! Get event dispatcher 93 | std::shared_ptr GetDispatcher() const; 94 | 95 | /* ******************************************************************************************** */ 96 | //! Variables 97 | private: 98 | std::weak_ptr dispatcher_; //!< Dispatch events for other blocks 99 | model::BlockIdentifier id_; //!< Block identification 100 | Size size_; //!< Block size 101 | bool focused_ = false; //!< Control flag for focus state, to help with UI navigation 102 | }; 103 | 104 | } // namespace interface 105 | #endif // INCLUDE_VIEW_BASE_BLOCK_H_ 106 | -------------------------------------------------------------------------------- /include/view/base/dialog.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for rendering customized dialogs 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_DIALOG_H_ 7 | #define INCLUDE_VIEW_ELEMENT_DIALOG_H_ 8 | 9 | #include "ftxui/component/event.hpp" 10 | #include "ftxui/dom/elements.hpp" 11 | 12 | namespace interface { 13 | 14 | class Dialog { 15 | static constexpr int kBorderSize = 2; //!< Extra padding based on border size 16 | 17 | protected: 18 | struct Size { 19 | float width = 0.f; //!< Width percentage 20 | float height = 0.f; //!< Height percentage 21 | 22 | int min_column = 0; //!< Minimum value of columns 23 | int min_line = 0; //!< Minimum value of lines 24 | 25 | int max_column = 0; //!< Maximum value of columns 26 | int max_line = 0; //!< Maximum value of lines 27 | }; 28 | 29 | //! Style for each part of the dialog 30 | struct Style { 31 | ftxui::Color background; 32 | ftxui::Color foreground; 33 | }; 34 | 35 | /** 36 | * @brief Construct a new Dialog object 37 | * @param size Size settings for dialog 38 | * @param style Dialog style to apply 39 | */ 40 | Dialog(const Size& size, const Style& style); 41 | 42 | public: 43 | /** 44 | * @brief Destroy Dialog object 45 | */ 46 | virtual ~Dialog() = default; 47 | 48 | /** 49 | * @brief Renders the component 50 | * @param curr_size Current terminal size 51 | * @return Element Built element based on internal state 52 | */ 53 | ftxui::Element Render(const ftxui::Dimensions& curr_size) const; 54 | 55 | /** 56 | * @brief Handles an event (from mouse/keyboard) 57 | * @param event Received event from screen 58 | * @return true if event was handled, otherwise false 59 | */ 60 | bool OnEvent(const ftxui::Event& event); 61 | 62 | /** 63 | * @brief Indicates if dialog is visible 64 | * @return true if dialog visible, otherwise false 65 | */ 66 | bool IsVisible() const { return opened_; } 67 | 68 | /** 69 | * @brief Set dialog as visible 70 | */ 71 | void Open() { 72 | OnOpen(); 73 | opened_ = true; 74 | } 75 | 76 | /** 77 | * @brief Set dialog as not visible 78 | */ 79 | void Close() { 80 | opened_ = false; 81 | OnClose(); 82 | } 83 | 84 | /* ******************************************************************************************** */ 85 | //! Implemented by derived class (mandatory) 86 | private: 87 | /** 88 | * @brief Renders the component (implement by derived) 89 | * @return Element Built element based on internal state 90 | */ 91 | virtual ftxui::Element RenderImpl(const ftxui::Dimensions& curr_size) const = 0; 92 | 93 | /** 94 | * @brief Handles an event (from mouse/keyboard) 95 | * @param event Received event from screen 96 | * @return true if event was handled, otherwise false 97 | */ 98 | virtual bool OnEventImpl(const ftxui::Event& event) = 0; 99 | 100 | /** 101 | * @brief Handles an event (from mouse) 102 | * @param event Received event from screen 103 | * @return true if event was handled, otherwise false 104 | */ 105 | virtual bool OnMouseEventImpl(ftxui::Event event) = 0; 106 | 107 | /* ******************************************************************************************** */ 108 | //! Implemented by derived class (optional) 109 | 110 | /** 111 | * @brief Callback to notify when dialog is opened 112 | */ 113 | virtual void OnOpen() { 114 | // Optional implementation by derived class 115 | } 116 | 117 | /** 118 | * @brief Callback to notify when dialog is closed 119 | */ 120 | virtual void OnClose() { 121 | // Optional implementation by derived class 122 | } 123 | 124 | /* ******************************************************************************************** */ 125 | //! Variables 126 | 127 | bool opened_ = false; //!< Flag to indicate dialog visilibity 128 | Size size_; //!< Dialog size settings 129 | Style style_; //!< Color style 130 | }; 131 | 132 | } // namespace interface 133 | #endif // INCLUDE_VIEW_ELEMENT_DIALOG_H_ 134 | -------------------------------------------------------------------------------- /include/view/base/element.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Base class for any element displayed in the UI 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BASE_ELEMENT_H_ 7 | #define INCLUDE_VIEW_BASE_ELEMENT_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace interface { 15 | 16 | //! Base class for elements 17 | class Element { 18 | public: 19 | //! Default destructor 20 | virtual ~Element() = default; 21 | 22 | /* ******************************************************************************************** */ 23 | //! Optional Implementation 24 | 25 | /** 26 | * @brief Renders the element 27 | * @return Element Built element based on internal state 28 | */ 29 | virtual ftxui::Element Render(); 30 | 31 | /** 32 | * @brief Handles an event from keyboard 33 | * @param event Received event from screen 34 | * @return true if event was handled, otherwise false 35 | */ 36 | virtual bool OnEvent(const ftxui::Event& event); 37 | 38 | /** 39 | * @brief Handles an event (from mouse) 40 | * @param event Received event from screen 41 | * @return true if event was handled, otherwise false 42 | */ 43 | virtual bool OnMouseEvent(ftxui::Event& event); 44 | 45 | /** 46 | * @brief Handles an action key from keyboard 47 | * @param event Received event from screen 48 | */ 49 | virtual bool HandleActionKey(const ftxui::Event& event); 50 | 51 | /** 52 | * @brief Handles a click event from mouse 53 | * @param event Received event from screen 54 | */ 55 | virtual void HandleClick(ftxui::Event& event); 56 | 57 | /** 58 | * @brief Handles a double click event from mouse 59 | * @param event Received event from screen 60 | */ 61 | virtual void HandleDoubleClick(ftxui::Event& event); 62 | 63 | /** 64 | * @brief Handles a hover event from mouse 65 | * @param event Received event from screen 66 | */ 67 | virtual void HandleHover(ftxui::Event& event); 68 | 69 | /** 70 | * @brief Handles a mouse wheel event 71 | * @param button Received button from mouse wheel event 72 | */ 73 | virtual void HandleWheel(const ftxui::Mouse::Button& button); 74 | 75 | /* ******************************************************************************************** */ 76 | //! Getters and setters 77 | 78 | /** 79 | * @brief Getter for hover state 80 | * @return true if mouse cursor is hovered on element, otherwise false 81 | */ 82 | bool IsHovered() const { return hovered_; } 83 | 84 | /** 85 | * @brief Getter for focus state 86 | * @return true if element is focused, otherwise false 87 | */ 88 | bool IsFocused() const { return focused_; } 89 | 90 | /** 91 | * @brief Getter for box element 92 | * @return Box reference 93 | */ 94 | ftxui::Box& Box() { return box_; } 95 | 96 | /** 97 | * @brief Getter for box element 98 | * @return Box copy 99 | */ 100 | const ftxui::Box& Box() const { return box_; } 101 | 102 | /** 103 | * @brief Set hover state 104 | * @param enable Flag to enable/disable state 105 | */ 106 | void SetHover(bool enable) { hovered_ = enable; } 107 | 108 | /** 109 | * @brief Set focus state 110 | * @param enable Flag to enable/disable state 111 | */ 112 | void SetFocus(bool enable) { 113 | focused_ = enable; 114 | OnFocusChanged(); 115 | } 116 | 117 | /* ******************************************************************************************** */ 118 | //! Optional implementation 119 | 120 | /** 121 | * @brief Notify when focus state has changed 122 | */ 123 | virtual void OnFocusChanged() { /* optional */ } 124 | 125 | /* ******************************************************************************************** */ 126 | //! Variables 127 | private: 128 | ftxui::Box box_; //!< Box to control if mouse cursor is over the element 129 | bool hovered_ = false; //!< Flag to indicate if element is hovered (by mouse) 130 | bool focused_ = false; //!< Flag to indicate if element is focused (set by equalizer) 131 | 132 | std::chrono::system_clock::time_point last_click_; //!< Last timestamp that mouse was clicked 133 | }; 134 | 135 | } // namespace interface 136 | #endif // INCLUDE_VIEW_BASE_ELEMENT_H_ 137 | -------------------------------------------------------------------------------- /include/view/base/event_dispatcher.h: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * \file 4 | * \brief Class for dispatch event among blocks 5 | */ 6 | 7 | #ifndef INCLUDE_VIEW_BASE_EVENT_DISPATCHER_H_ 8 | #define INCLUDE_VIEW_BASE_EVENT_DISPATCHER_H_ 9 | 10 | #include 11 | 12 | #include "model/application_error.h" 13 | #include "view/base/block.h" 14 | #include "view/base/custom_event.h" 15 | 16 | namespace interface { 17 | 18 | /** 19 | * @brief Interface class to dispatch events among blocks 20 | */ 21 | class EventDispatcher : public std::enable_shared_from_this { 22 | public: 23 | /** 24 | * @brief Construct a new Event Dispatcher object 25 | */ 26 | EventDispatcher() = default; 27 | 28 | /** 29 | * @brief Destroy the Event Dispatcher object 30 | */ 31 | virtual ~EventDispatcher() = default; 32 | 33 | //! Remove these 34 | EventDispatcher(const EventDispatcher& other) = delete; // copy constructor 35 | EventDispatcher(EventDispatcher&& other) = delete; // move constructor 36 | EventDispatcher& operator=(const EventDispatcher& other) = delete; // copy assignment 37 | EventDispatcher& operator=(EventDispatcher&& other) = delete; // move assignment 38 | 39 | //! Implemented by derived class 40 | virtual void SendEvent(const CustomEvent& event) = 0; 41 | virtual void ProcessEvent(const CustomEvent& event) = 0; 42 | virtual void SetApplicationError(error::Code id) = 0; 43 | }; 44 | 45 | } // namespace interface 46 | #endif // INCLUDE_VIEW_BASE_EVENT_DISPATCHER_H_ 47 | -------------------------------------------------------------------------------- /include/view/base/keybinding.h: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * \file 4 | * \brief Table with all mapped keybindings to use within this application 5 | */ 6 | 7 | #ifndef INCLUDE_VIEW_BASE_KEYBINDING_H_ 8 | #define INCLUDE_VIEW_BASE_KEYBINDING_H_ 9 | 10 | #include "ftxui/component/event.hpp" 11 | 12 | namespace interface::keybinding { 13 | 14 | using Key = ftxui::Event; 15 | 16 | //! Navigation keybindings 17 | struct Navigation { 18 | static Key ArrowUp; 19 | static Key ArrowDown; 20 | static Key ArrowLeft; 21 | static Key ArrowRight; 22 | 23 | static Key Up; 24 | static Key Down; 25 | static Key Left; 26 | static Key Right; 27 | 28 | static Key Space; 29 | static Key Return; 30 | static Key Escape; 31 | 32 | static Key Tab; 33 | static Key TabReverse; 34 | 35 | static Key Home; 36 | static Key End; 37 | static Key PageUp; 38 | static Key PageDown; 39 | 40 | static Key Backspace; 41 | static Key CtrlBackspace; 42 | static Key CtrlBackspaceReverse; 43 | 44 | static Key Delete; 45 | 46 | static Key Close; 47 | 48 | static Key EnableSearch; 49 | }; 50 | 51 | /* ********************************************************************************************** */ 52 | 53 | //! Dialog keybindings 54 | struct Dialog { 55 | static Key Yes; 56 | static Key No; 57 | }; 58 | 59 | /* ********************************************************************************************** */ 60 | 61 | //! General keybindings 62 | struct General { 63 | static Key ExitApplication; 64 | static Key ShowHelper; 65 | static Key ShowTabHelper; 66 | 67 | static Key FocusSidebar; 68 | static Key FocusInfo; 69 | static Key FocusMainContent; 70 | static Key FocusPlayer; 71 | }; 72 | 73 | /* ********************************************************************************************** */ 74 | 75 | //! Main Tab keybindings 76 | struct MainContent { 77 | static Key FocusVisualizer; 78 | static Key FocusEqualizer; 79 | static Key FocusLyric; 80 | }; 81 | 82 | /* ********************************************************************************************** */ 83 | 84 | //! Sidebar keybindings 85 | struct Sidebar { 86 | static Key FocusList; 87 | static Key FocusPlaylist; 88 | }; 89 | 90 | /* ********************************************************************************************** */ 91 | 92 | //! Media player keybindings 93 | struct MediaPlayer { 94 | static Key PlayOrPause; 95 | static Key Stop; 96 | static Key ClearSong; 97 | 98 | static Key SkipToPrevious; 99 | static Key SkipToNext; 100 | 101 | static Key VolumeUp; 102 | static Key VolumeDown; 103 | static Key Mute; 104 | 105 | static Key SeekForward; 106 | static Key SeekBackward; 107 | }; 108 | 109 | /* ********************************************************************************************** */ 110 | 111 | //! Visualizer keybindings 112 | struct Visualizer { 113 | static Key ChangeAnimation; 114 | static Key ToggleFullscreen; 115 | static Key IncreaseBarWidth; 116 | static Key DecreaseBarWidth; 117 | }; 118 | 119 | /* ********************************************************************************************** */ 120 | 121 | //! Equalizer keybindings 122 | struct Equalizer { 123 | static Key ApplyFilters; 124 | static Key ResetFilters; 125 | 126 | static Key IncreaseBarWidth; 127 | static Key DecreaseBarWidth; 128 | }; 129 | 130 | /* ********************************************************************************************** */ 131 | 132 | //! Lyric keybindings 133 | struct Lyric { 134 | // TODO: retry 135 | }; 136 | 137 | /* ********************************************************************************************** */ 138 | 139 | //! Files keybindings 140 | struct Files {}; 141 | 142 | /* ********************************************************************************************** */ 143 | 144 | //! Playlist keybindings 145 | struct Playlist { 146 | static Key Create; 147 | static Key Modify; 148 | static Key Delete; 149 | 150 | static Key Rename; 151 | static Key Save; 152 | }; 153 | 154 | } // namespace interface::keybinding 155 | #endif // INCLUDE_VIEW_BASE_KEYBINDING_H_ 156 | -------------------------------------------------------------------------------- /include/view/base/notifier.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class to notify GUI with information 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BASE_NOTIFIER_H_ 7 | #define INCLUDE_VIEW_BASE_NOTIFIER_H_ 8 | 9 | #include "model/application_error.h" 10 | #include "model/song.h" 11 | 12 | namespace interface { 13 | 14 | /** 15 | * @brief Interface class to notify interface with updated information 16 | */ 17 | class Notifier { 18 | public: 19 | /** 20 | * @brief Construct a new Notifier object 21 | */ 22 | Notifier() = default; 23 | 24 | /** 25 | * @brief Destroy the Notifier object 26 | */ 27 | virtual ~Notifier() = default; 28 | 29 | /* ******************************************************************************************** */ 30 | //! Public API 31 | 32 | /** 33 | * @brief Notify UI to clear any info about the song that was playing previously 34 | * @param playing Last media state 35 | */ 36 | virtual void ClearSongInformation(bool playing) = 0; 37 | 38 | /** 39 | * @brief Notify UI with detailed information from the parsed song 40 | * @param info Detailed audio information from previously file selected 41 | */ 42 | virtual void NotifySongInformation(const model::Song& info) = 0; 43 | 44 | /** 45 | * @brief Notify UI with new state information from current song 46 | * @param state Updated state information 47 | */ 48 | virtual void NotifySongState(const model::Song::CurrentInformation& state) = 0; 49 | 50 | /** 51 | * @brief Send raw audio samples to UI 52 | * @param buffer Audio samples 53 | * @param size Sample count 54 | */ 55 | virtual void SendAudioRaw(int* buffer, int size) = 0; 56 | 57 | /** 58 | * @brief Notify UI with error code from some background operation 59 | * @param code Application error code 60 | */ 61 | virtual void NotifyError(error::Code code) = 0; 62 | }; 63 | 64 | } // namespace interface 65 | #endif // INCLUDE_VIEW_BASE_NOTIFIER_H_ 66 | -------------------------------------------------------------------------------- /include/view/block/file_info.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for block containing file info 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_FILE_INFO_H_ 7 | #define INCLUDE_VIEW_BLOCK_FILE_INFO_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ftxui/dom/elements.hpp" 14 | #include "model/song.h" 15 | #include "view/base/block.h" 16 | 17 | namespace interface { 18 | 19 | /** 20 | * @brief Component with detailed information about the chosen file (in this case, some music file) 21 | */ 22 | class FileInfo : public Block { 23 | static constexpr int kMaxColumns = 36; //!< Maximum columns for Component 24 | static constexpr int kMaxRows = 15; //!< Maximum rows for Component 25 | 26 | static constexpr int kMaxSongLines = 8; //!< Always remember to check song::to_string 27 | 28 | public: 29 | /** 30 | * @brief Construct a new File Info object 31 | * @param dispatcher Block event dispatcher 32 | */ 33 | explicit FileInfo(const std::shared_ptr& dispatcher); 34 | 35 | /** 36 | * @brief Destroy the File Info object 37 | */ 38 | ~FileInfo() override = default; 39 | 40 | /** 41 | * @brief Renders the component 42 | * @return Element Built element based on internal state 43 | */ 44 | ftxui::Element Render() override; 45 | 46 | /** 47 | * @brief Handles an event (from mouse/keyboard) 48 | * @param event Received event from screen 49 | * @return true if event was handled, otherwise false 50 | */ 51 | bool OnEvent(ftxui::Event event) override; 52 | 53 | /** 54 | * @brief Handles a custom event 55 | * @param event Received event (probably sent by Audio thread) 56 | * @return true if event was handled, otherwise false 57 | */ 58 | bool OnCustomEvent(const CustomEvent& event) override; 59 | 60 | /* ******************************************************************************************* */ 61 | //! Utils 62 | 63 | /** 64 | * @brief Parse audio information into internal cache to render on UI later 65 | * @param audio Detailed audio information 66 | */ 67 | void ParseAudioInfo(const model::Song& audio); 68 | 69 | /* ******************************************************************************************* */ 70 | //! Variables 71 | private: 72 | using Entry = std::pair; //!< A pair of 73 | std::vector audio_info_; //!< Parsed audio information to render on UI 74 | 75 | bool is_song_playing_ = false; //!< Flag to control when a song is playing 76 | }; 77 | 78 | } // namespace interface 79 | #endif // INCLUDE_VIEW_BLOCK_FILE_INFO_H_ 80 | -------------------------------------------------------------------------------- /include/view/block/main_content.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for block containing main content view 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_MAIN_CONTENT_H_ 7 | #define INCLUDE_VIEW_BLOCK_MAIN_CONTENT_H_ 8 | 9 | #include 10 | 11 | #include "view/base/block.h" 12 | #include "view/element/tab.h" 13 | 14 | #ifdef ENABLE_TESTS 15 | namespace { 16 | class MainContentTest; 17 | } 18 | #endif 19 | 20 | namespace interface { 21 | 22 | /** 23 | * @brief Component to display a set of tabs and their respective content 24 | */ 25 | class MainContent : public Block { 26 | public: 27 | /** 28 | * @brief Construct a new MainContent object 29 | * @param dispatcher Block event dispatcher 30 | */ 31 | explicit MainContent(const std::shared_ptr& dispatcher); 32 | 33 | /** 34 | * @brief Destroy the MainContent object 35 | */ 36 | ~MainContent() override = default; 37 | 38 | /** 39 | * @brief Renders the component 40 | * @return Element Built element based on internal state 41 | */ 42 | ftxui::Element Render() override; 43 | 44 | /** 45 | * @brief Renders the component in fullscreen (without window borders) 46 | * @return Element Built element based on internal state 47 | */ 48 | ftxui::Element RenderFullscreen(); 49 | 50 | /** 51 | * @brief Handles an event (from mouse/keyboard) 52 | * @param event Received event from screen 53 | * @return true if event was handled, otherwise false 54 | */ 55 | bool OnEvent(ftxui::Event event) override; 56 | 57 | /** 58 | * @brief Handles a custom event 59 | * @param event Received event (probably sent by Audio thread) 60 | * @return true if event was handled, otherwise false 61 | */ 62 | bool OnCustomEvent(const CustomEvent& event) override; 63 | 64 | /** 65 | * @brief Receives an indication that block is now focused 66 | */ 67 | void OnFocus() override; 68 | 69 | /** 70 | * @brief Receives an indication that block is not focused anymore 71 | */ 72 | void OnLostFocus() override; 73 | 74 | /** 75 | * @brief Possible tab views to render on this block 76 | */ 77 | enum View { 78 | Visualizer, //!< Display spectrum visualizer (default) 79 | Equalizer, //!< Display audio equalizer 80 | Lyric, //!< Display song lyric 81 | LAST, 82 | }; 83 | 84 | /** 85 | * @brief Get width for a single bar (used for Terminal calculation) 86 | * @return Audio bar width 87 | */ 88 | int GetBarWidth(); 89 | 90 | /* ******************************************************************************************** */ 91 | //! Private methods 92 | private: 93 | //! Handle mouse event 94 | bool OnMouseEvent(ftxui::Event event); 95 | 96 | //! Create general buttons 97 | void CreateButtons(); 98 | 99 | /* ******************************************************************************************** */ 100 | //! Variables 101 | 102 | //! Buttons located on the upper-right border of block window 103 | WindowButton btn_help_; //!< Help button 104 | WindowButton btn_exit_; //!< Exit button 105 | 106 | Tab tab_elem_; //!< Tab containing multiple panels with some content 107 | 108 | bool is_fullscreen_ = 109 | false; //!< Cache flag set by parent(Terminal block) via Render or RenderFullscreen, this is 110 | //!< used later by OnEvent to decide if could process event or not 111 | 112 | /* ******************************************************************************************** */ 113 | //! Friend class for testing purpose 114 | 115 | #ifdef ENABLE_TESTS 116 | friend class ::MainContentTest; 117 | #endif 118 | }; 119 | 120 | } // namespace interface 121 | 122 | #endif // INCLUDE_VIEW_BLOCK_MAIN_CONTENT_H_ 123 | -------------------------------------------------------------------------------- /include/view/block/main_content/song_lyric.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for tab view containing song lyrics 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_MAIN_CONTENT_SONG_LYRIC_H_ 7 | #define INCLUDE_VIEW_BLOCK_MAIN_CONTENT_SONG_LYRIC_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "audio/lyric/base/html_parser.h" 15 | #include "audio/lyric/lyric_finder.h" 16 | #include "model/song.h" 17 | #include "view/element/tab.h" 18 | 19 | #ifdef ENABLE_TESTS 20 | namespace { 21 | class MainContentTest; 22 | } 23 | #endif 24 | 25 | namespace interface { 26 | 27 | /** 28 | * @brief Component to render lyric from current song 29 | */ 30 | class SongLyric : public TabItem { 31 | static constexpr std::string_view kTabName = "lyric"; //!< Tab title 32 | 33 | public: 34 | /** 35 | * @brief Construct a new SongLyric object 36 | * @param id Parent block identifier 37 | * @param dispatcher Block event dispatcher 38 | * @param on_focus Callback function to ask for focus 39 | * @param keybinding Keybinding to set item as active 40 | */ 41 | explicit SongLyric(const model::BlockIdentifier& id, 42 | const std::shared_ptr& dispatcher, 43 | const FocusCallback& on_focus, const keybinding::Key& keybinding); 44 | 45 | /** 46 | * @brief Destroy the SongLyric object 47 | */ 48 | ~SongLyric() override; 49 | 50 | /** 51 | * @brief Renders the component 52 | * @return Element Built element based on internal state 53 | */ 54 | ftxui::Element Render() override; 55 | 56 | /** 57 | * @brief Handles an event (from keyboard) 58 | * @param event Received event from screen 59 | * @return true if event was handled, otherwise false 60 | */ 61 | bool OnEvent(const ftxui::Event& event) override; 62 | 63 | /** 64 | * @brief Handles a custom event 65 | * @param event Received event (probably sent by Audio thread) 66 | * @return true if event was handled, otherwise false 67 | */ 68 | bool OnCustomEvent(const CustomEvent& event) override; 69 | 70 | /* ******************************************************************************************** */ 71 | //! Private methods 72 | private: 73 | /** 74 | * @brief Check inner state from std::future 75 | * @tparam R Result from asynchronous operation 76 | * @param f Mechanism to execute asynchronous operation 77 | * @param st Inner state 78 | * @return true if state matches, otherwise false 79 | */ 80 | template 81 | bool is_state(std::future& f, std::future_status st) const { 82 | return f.valid() && f.wait_for(std::chrono::seconds(0)) == st; 83 | } 84 | 85 | /** 86 | * @brief Check state from fetch operation that is executed asynchronously 87 | * @return true if fetch operation is still executing, otherwise false 88 | */ 89 | bool IsFetching() { return async_fetcher_ && is_state(*async_fetcher_, std::future_status::timeout); } 90 | 91 | /** 92 | * @brief Check state from fetch operation that is executed asynchronously 93 | * @return true if fetch operation finished, otherwise false 94 | */ 95 | bool IsResultReady() { return async_fetcher_ && is_state(*async_fetcher_, std::future_status::ready); } 96 | 97 | //! Result from asynchronous fetch operation (if empty, it means that failed) 98 | using FetchResult = std::optional; 99 | 100 | /** 101 | * @brief Use updated song information to fetch song lyrics 102 | */ 103 | FetchResult FetchSongLyrics(); 104 | 105 | /** 106 | * @brief Renders the song lyrics element 107 | * @param lyrics Song lyrics (each entry represents a paragraph) 108 | */ 109 | ftxui::Element DrawSongLyrics(const lyric::SongLyric& lyrics) const; 110 | 111 | /* ******************************************************************************************** */ 112 | //! Variables 113 | 114 | model::Song audio_info_; //!< Audio information from current song 115 | lyric::SongLyric lyrics_; //!< Song lyrics from current song 116 | int focused_ = 0; //!< Index for paragraph focused from song lyric 117 | 118 | std::unique_ptr finder_ = lyric::LyricFinder::Create(); //!< Lyric finder 119 | std::unique_ptr> async_fetcher_; //!< Use lyric finder asynchronously 120 | 121 | /* ******************************************************************************************** */ 122 | //! Friend class for testing purpose 123 | 124 | #ifdef ENABLE_TESTS 125 | friend class ::MainContentTest; 126 | #endif 127 | }; 128 | 129 | } // namespace interface 130 | #endif // INCLUDE_VIEW_BLOCK_MAIN_CONTENT_SONG_LYRIC_H_ 131 | -------------------------------------------------------------------------------- /include/view/block/main_content/spectrum_visualizer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for tab view containing spectrum visualizer 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_MAIN_CONTENT_AUDIO_VISUALIZER_H_ 7 | #define INCLUDE_VIEW_BLOCK_MAIN_CONTENT_AUDIO_VISUALIZER_H_ 8 | 9 | #include 10 | 11 | #include "model/bar_animation.h" 12 | #include "view/element/tab.h" 13 | 14 | namespace interface { 15 | 16 | /** 17 | * @brief Component to render different animations using audio spectrum data from current song 18 | */ 19 | class SpectrumVisualizer : public TabItem { 20 | static constexpr std::string_view kTabName = "visualizer"; //!< Tab title 21 | static constexpr int kGaugeDefaultWidth = 3; //!< Default gauge width (audio bar width) 22 | static constexpr int kGaugeMinWidth = 2; //!< Maximum value for gauge width 23 | static constexpr int kGaugeMaxWidth = 4; //!< Minimum value for gauge width 24 | static constexpr int kGaugeSpacing = 1; //!< Spacing between gauges 25 | 26 | public: 27 | /** 28 | * @brief Construct a new SpectrumVisualizer object 29 | * @param id Parent block identifier 30 | * @param dispatcher Block event dispatcher 31 | * @param on_focus Callback function to ask for focus 32 | * @param keybinding Keybinding to set item as active 33 | */ 34 | explicit SpectrumVisualizer(const model::BlockIdentifier& id, 35 | const std::shared_ptr& dispatcher, 36 | const FocusCallback& on_focus, const keybinding::Key& keybinding); 37 | 38 | /** 39 | * @brief Destroy the SpectrumVisualizer object 40 | */ 41 | ~SpectrumVisualizer() override = default; 42 | 43 | /** 44 | * @brief Renders the component 45 | * @return Element Built element based on internal state 46 | */ 47 | ftxui::Element Render() override; 48 | 49 | /** 50 | * @brief Handles an event (from keyboard) 51 | * @param event Received event from screen 52 | * @return true if event was handled, otherwise false 53 | */ 54 | bool OnEvent(const ftxui::Event& event) override; 55 | 56 | /** 57 | * @brief Handles a custom event 58 | * @param event Received event (probably sent by Audio thread) 59 | * @return true if event was handled, otherwise false 60 | */ 61 | bool OnCustomEvent(const CustomEvent& event) override; 62 | 63 | /** 64 | * @brief Get width for a single bar (used for Terminal calculation) 65 | * @return Audio bar width 66 | */ 67 | int GetBarWidth() const { return gauge_width_; } 68 | 69 | /* ******************************************************************************************** */ 70 | // Private methods 71 | private: 72 | //! Utility to create UI gauge 73 | void CreateGauge(double value, ftxui::Direction direction, ftxui::Elements& elements) const; 74 | 75 | //! Animations 76 | void DrawAnimationHorizontalMirror(ftxui::Element& visualizer); 77 | void DrawAnimationVerticalMirror(ftxui::Element& visualizer); 78 | void DrawAnimationMono(ftxui::Element& visualizer); 79 | 80 | /* ******************************************************************************************** */ 81 | //! Variables 82 | model::BarAnimation curr_anim_ = 83 | model::BarAnimation::HorizontalMirror; //!< Control which bar animation to draw 84 | std::vector spectrum_data_; //!< Audio spectrum (each entry represents a frequency bar) 85 | int gauge_width_ = kGaugeDefaultWidth; //!< Current audio bar width 86 | }; 87 | 88 | } // namespace interface 89 | #endif // INCLUDE_VIEW_BLOCK_MAIN_CONTENT_AUDIO_VISUALIZER_H_ 90 | -------------------------------------------------------------------------------- /include/view/block/media_player.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for block containing audio player 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_AUDIO_PLAYER_H_ 7 | #define INCLUDE_VIEW_BLOCK_AUDIO_PLAYER_H_ 8 | 9 | #include 10 | 11 | #include "ftxui/dom/elements.hpp" 12 | #include "model/song.h" 13 | #include "model/volume.h" 14 | #include "view/base/block.h" 15 | #include "view/element/button.h" 16 | 17 | namespace interface { 18 | 19 | /** 20 | * @brief Component with detailed information about the chosen file (in this case, some music file) 21 | */ 22 | class MediaPlayer : public Block { 23 | static constexpr int kMaxRows = 10; //!< Maximum rows for the Component 24 | 25 | public: 26 | /** 27 | * @brief Construct a new Audio Player object 28 | * @param dispatcher Block event dispatcher 29 | */ 30 | explicit MediaPlayer(const std::shared_ptr& dispatcher); 31 | 32 | /** 33 | * @brief Destroy the Audio Player object 34 | */ 35 | ~MediaPlayer() override = default; 36 | 37 | /** 38 | * @brief Renders the component 39 | * @return Element Built element based on internal state 40 | */ 41 | ftxui::Element Render() override; 42 | 43 | /** 44 | * @brief Handles an event (from mouse/keyboard) 45 | * 46 | * @param event Received event from screen 47 | * @return true if event was handled, otherwise false 48 | */ 49 | bool OnEvent(ftxui::Event event) override; 50 | 51 | /** 52 | * @brief Handles a custom event 53 | * 54 | * @param event Received event (probably sent by Audio thread) 55 | * @return true if event was handled, otherwise false 56 | */ 57 | bool OnCustomEvent(const CustomEvent& event) override; 58 | 59 | /* ******************************************************************************************** */ 60 | private: 61 | //! Handle mouse event 62 | bool OnMouseEvent(ftxui::Event event); 63 | 64 | /** 65 | * @brief Handle event for media control (e.g., play/pause, stop, clear and skip song) 66 | * @param event Received event from screen 67 | * @return true if event was handled, otherwise false 68 | */ 69 | bool HandleMediaEvent(const ftxui::Event& event) const; 70 | 71 | /** 72 | * @brief Handle event for volume control 73 | * @param event Received event from screen 74 | * @return true if event was handled, otherwise false 75 | */ 76 | bool HandleVolumeEvent(const ftxui::Event& event); 77 | 78 | /** 79 | * @brief Handle event for seek position in song 80 | * @param event Received event from screen 81 | * @return true if event was handled, otherwise false 82 | */ 83 | bool HandleSeekEvent(const ftxui::Event& event) const; 84 | 85 | //! Utility to check media state 86 | bool IsPlaying() const { 87 | return song_.curr_info.state == model::Song::MediaState::Play || 88 | song_.curr_info.state == model::Song::MediaState::Pause; 89 | } 90 | 91 | /* ******************************************************************************************** */ 92 | //! Variables 93 | 94 | MediaButton btn_play_; //!< Media player button for Play/Pause 95 | MediaButton btn_stop_; //!< Media player button for Stop 96 | MediaButton btn_previous_; //!< Media player button for Play next song 97 | MediaButton btn_next_; //!< Media player button for Play previous song 98 | 99 | model::Song song_ = model::Song{}; //!< Audio information from current song 100 | model::Volume volume_; //!< General sound volume 101 | 102 | ftxui::Box duration_box_; //!< Box for song duration component (gauge) 103 | bool is_duration_focused_ = false; //!< Flag to control if song duration box is focused 104 | }; 105 | 106 | } // namespace interface 107 | #endif // INCLUDE_VIEW_BLOCK_AUDIO_PLAYER_H_ 108 | -------------------------------------------------------------------------------- /include/view/block/sidebar.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for block containing sidebar view 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_SIDEBAR_H_ 7 | #define INCLUDE_VIEW_BLOCK_SIDEBAR_H_ 8 | 9 | #include 10 | 11 | #include "util/file_handler.h" 12 | #include "view/base/block.h" 13 | #include "view/element/tab.h" 14 | 15 | #ifdef ENABLE_TESTS 16 | namespace { 17 | class SidebarTest; 18 | } 19 | #endif 20 | 21 | namespace interface { 22 | 23 | /** 24 | * @brief Component to display a set of tabs and their respective content in the sidebar 25 | */ 26 | class Sidebar : public Block { 27 | static constexpr int kMaxColumns = 36; //!< Maximum columns for Component 28 | 29 | public: 30 | /** 31 | * @brief Construct a new Sidebar object 32 | * @param dispatcher Block event dispatcher 33 | * @param optional_path List files from custom path instead of the current one 34 | * @param file_handler Interface to file handler 35 | */ 36 | explicit Sidebar(const std::shared_ptr& dispatcher, 37 | const std::string& optional_path = "", 38 | const std::shared_ptr file_handler = nullptr); 39 | 40 | /** 41 | * @brief Destroy the Sidebar object 42 | */ 43 | ~Sidebar() override = default; 44 | 45 | /** 46 | * @brief Renders the component 47 | * @return Element Built element based on internal state 48 | */ 49 | ftxui::Element Render() override; 50 | 51 | /** 52 | * @brief Handles an event (from mouse/keyboard) 53 | * @param event Received event from screen 54 | * @return true if event was handled, otherwise false 55 | */ 56 | bool OnEvent(ftxui::Event event) override; 57 | 58 | /** 59 | * @brief Handles a custom event 60 | * @param event Received event (probably sent by Audio thread) 61 | * @return true if event was handled, otherwise false 62 | */ 63 | bool OnCustomEvent(const CustomEvent& event) override; 64 | 65 | /** 66 | * @brief Receives an indication that block is now focused 67 | */ 68 | void OnFocus() override; 69 | 70 | /** 71 | * @brief Receives an indication that block is not focused anymore 72 | */ 73 | void OnLostFocus() override; 74 | 75 | /** 76 | * @brief Possible tab views to render on this block 77 | */ 78 | enum View { 79 | Files, //!< Display file list from directory (default) 80 | Playlist, //!< Display playlist viewer 81 | LAST, 82 | }; 83 | 84 | /* ******************************************************************************************** */ 85 | //! Private methods 86 | private: 87 | //! Handle mouse event 88 | bool OnMouseEvent(ftxui::Event event); 89 | 90 | /* ******************************************************************************************** */ 91 | //! Variables 92 | 93 | Tab tab_elem_; //!< Tab containing multiple panels with some content 94 | 95 | //!< Utility class to manage files (read/write) 96 | std::shared_ptr file_handler_; 97 | 98 | /* ******************************************************************************************** */ 99 | //! Friend class for testing purpose 100 | 101 | #ifdef ENABLE_TESTS 102 | friend class ::SidebarTest; 103 | #endif 104 | }; 105 | 106 | } // namespace interface 107 | 108 | #endif // INCLUDE_VIEW_BLOCK_SIDEBAR_H_ 109 | -------------------------------------------------------------------------------- /include/view/block/sidebar_content/list_directory.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for block containing file list 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_SIDEBAR_CONTENT_LIST_DIRECTORY_H_ 7 | #define INCLUDE_VIEW_BLOCK_SIDEBAR_CONTENT_LIST_DIRECTORY_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "ftxui/dom/elements.hpp" 16 | #include "util/file_handler.h" 17 | #include "view/base/block.h" 18 | #include "view/element/menu.h" 19 | #include "view/element/tab.h" 20 | 21 | #ifdef ENABLE_TESTS 22 | #include 23 | 24 | //! Forward declaration 25 | namespace { 26 | class SidebarTest; 27 | class ListDirectoryCtorTest; 28 | } // namespace 29 | #endif 30 | 31 | namespace interface { 32 | 33 | /** 34 | * @brief Component to list files from given directory 35 | */ 36 | class ListDirectory : public TabItem { 37 | static constexpr std::string_view kTabName = "files"; //!< Tab title 38 | 39 | public: 40 | /** 41 | * @brief Construct a new ListDirectory object 42 | * @param id Parent block identifier 43 | * @param dispatcher Block event dispatcher 44 | * @param on_focus Callback function to ask for focus 45 | * @param keybinding Keybinding to set item as active 46 | * @param file_handler Utility handler to manage any file operation 47 | * @param max_columns Maximum number of visual columns to be used by this element 48 | * @param optional_path Custom directory path to fill initial list of files 49 | */ 50 | explicit ListDirectory(const model::BlockIdentifier& id, 51 | const std::shared_ptr& dispatcher, 52 | const FocusCallback& on_focus, const keybinding::Key& keybinding, 53 | const std::shared_ptr& file_handler, int max_columns, 54 | const std::string& optional_path = ""); 55 | 56 | /** 57 | * @brief Destroy the List Directory object 58 | */ 59 | ~ListDirectory() override = default; 60 | 61 | /** 62 | * @brief Renders the component 63 | * @return Element Built element based on internal state 64 | */ 65 | ftxui::Element Render() override; 66 | 67 | /** 68 | * @brief Handles an event (from mouse/keyboard) 69 | * @param event Received event from screen 70 | * @return true if event was handled, otherwise false 71 | */ 72 | bool OnEvent(const ftxui::Event& event) override; 73 | 74 | /** 75 | * @brief Handles an event (from mouse) 76 | * @param event Received event from screen 77 | * @return true if event was handled, otherwise false 78 | */ 79 | bool OnMouseEvent(ftxui::Event& event) override; 80 | 81 | /** 82 | * @brief Handles a custom event 83 | * @param event Received event (probably sent by Audio thread) 84 | * @return true if event was handled, otherwise false 85 | */ 86 | bool OnCustomEvent(const CustomEvent& event) override; 87 | 88 | /* ******************************************************************************************** */ 89 | //! File list operations 90 | private: 91 | /** 92 | * @brief Select file to play based on the current song playing 93 | * @param pick_next Pick next or previous file to play 94 | * @return Filepath 95 | */ 96 | util::File SelectFileToPlay(bool pick_next); 97 | 98 | /* ******************************************************************************************** */ 99 | //! Local cache for current active information 100 | protected: 101 | //! Get current directory 102 | const std::filesystem::path& GetCurrentDir() const { return menu_->actual().GetCurrentDir(); } 103 | 104 | /* ******************************************************************************************** */ 105 | //! Variables 106 | private: 107 | std::optional curr_playing_ = std::nullopt; //!< Current song playing 108 | 109 | int max_columns_; //!< Maximum number of columns (characters in a single line) available to use 110 | 111 | FileMenu menu_; //!< Menu with a list of files 112 | 113 | /* ******************************************************************************************** */ 114 | //! Friend test 115 | 116 | #ifdef ENABLE_TESTS 117 | friend class ::SidebarTest; 118 | friend class ::ListDirectoryCtorTest; 119 | #endif 120 | }; 121 | 122 | } // namespace interface 123 | #endif // INCLUDE_VIEW_BLOCK_SIDEBAR_CONTENT_LIST_DIRECTORY_H_ 124 | -------------------------------------------------------------------------------- /include/view/block/sidebar_content/playlist_viewer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for tab view containing playlist manager 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_BLOCK_SIDEBAR_CONTENT_PLAYLIST_MANAGER_H_ 7 | #define INCLUDE_VIEW_BLOCK_SIDEBAR_CONTENT_PLAYLIST_MANAGER_H_ 8 | 9 | #include 10 | 11 | #include "util/file_handler.h" 12 | #include "view/element/button.h" 13 | #include "view/element/menu.h" 14 | #include "view/element/tab.h" 15 | 16 | #ifdef ENABLE_TESTS 17 | namespace { 18 | class SidebarTest; 19 | } 20 | #endif 21 | 22 | namespace interface { 23 | 24 | /** 25 | * @brief Component to render and manage playlists 26 | */ 27 | class PlaylistViewer : public TabItem { 28 | static constexpr std::string_view kTabName = "playlist"; //!< Tab title 29 | static constexpr int kMaxIconColumns = 2; //!< Maximum columns for Icon 30 | 31 | public: 32 | /** 33 | * @brief Construct a new PlaylistManager object 34 | * @param id Parent block identifier 35 | * @param dispatcher Block event dispatcher 36 | * @param on_focus Callback function to ask for focus 37 | * @param keybinding Keybinding to set item as active 38 | * @param file_handler Utility handler to manage any file operation 39 | * @param max_columns Maximum number of visual columns to be used by this element 40 | */ 41 | explicit PlaylistViewer(const model::BlockIdentifier& id, 42 | const std::shared_ptr& dispatcher, 43 | const FocusCallback& on_focus, const keybinding::Key& keybinding, 44 | const std::shared_ptr& file_handler, int max_columns); 45 | 46 | /** 47 | * @brief Destroy the PlaylistManager object 48 | */ 49 | ~PlaylistViewer() override = default; 50 | 51 | /** 52 | * @brief Renders the component 53 | * @return Element Built element based on internal state 54 | */ 55 | ftxui::Element Render() override; 56 | 57 | /** 58 | * @brief Handles an event (from keyboard) 59 | * @param event Received event from screen 60 | * @return true if event was handled, otherwise false 61 | */ 62 | bool OnEvent(const ftxui::Event& event) override; 63 | 64 | /** 65 | * @brief Handles an event (from mouse) 66 | * @param event Received event from screen 67 | * @return true if event was handled, otherwise false 68 | */ 69 | bool OnMouseEvent(ftxui::Event& event) override; 70 | 71 | /** 72 | * @brief Handles a custom event 73 | * @param event Received event (probably sent by Audio thread) 74 | * @return true if event was handled, otherwise false 75 | */ 76 | bool OnCustomEvent(const CustomEvent& event) override; 77 | 78 | /** 79 | * @brief Receives an indication which this is now focused 80 | */ 81 | void OnFocus() override; 82 | 83 | /* ******************************************************************************************** */ 84 | //! UI initialization 85 | private: 86 | /** 87 | * @brief Initialize all UI buttons to manage playlists 88 | */ 89 | void CreateButtons(); 90 | 91 | /** 92 | * @brief Callback triggered by QuestionDialog on "yes" click 93 | */ 94 | void OnYes(); 95 | 96 | /* ******************************************************************************************** */ 97 | //! Variables 98 | 99 | int max_columns_; //!< Maximum number of columns (characters in a single line) available to use 100 | std::shared_ptr file_handler_; //!< Utility class to manage files (read/write) 101 | 102 | ftxui::Box box_; //!< Box for whole component 103 | 104 | std::chrono::system_clock::time_point last_click_; //!< Last timestamp that mouse was clicked 105 | 106 | PlaylistMenu menu_; //!< Menu to render a list of playlists 107 | 108 | GenericButton btn_create_; //!< Button to create a new playlist 109 | GenericButton btn_modify_; //!< Button to modify a playlist 110 | GenericButton btn_delete_; //!< Button to delete a playlist 111 | 112 | //!< Style for any button displayed in this element 113 | static Button::Style kButtonStyle; 114 | 115 | /* ******************************************************************************************** */ 116 | //! Friend class for testing purpose 117 | 118 | #ifdef ENABLE_TESTS 119 | friend class ::SidebarTest; 120 | #endif 121 | }; 122 | 123 | } // namespace interface 124 | #endif // INCLUDE_VIEW_BLOCK_SIDEBAR_CONTENT_PLAYLIST_MANAGER_H_ 125 | -------------------------------------------------------------------------------- /include/view/element/error_dialog.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for rendering error dialog 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_ERROR_DIALOG_H_ 7 | #define INCLUDE_VIEW_ELEMENT_ERROR_DIALOG_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include "view/base/dialog.h" 13 | 14 | namespace interface { 15 | 16 | /** 17 | * @brief Customized dialog box to show messages (error/warning/info) 18 | */ 19 | class ErrorDialog : public Dialog { 20 | static constexpr int kMaxColumns = 35; //!< Maximum columns for Element 21 | static constexpr int kMaxLines = 5; //!< Maximum lines for Element 22 | 23 | public: 24 | /** 25 | * @brief Construct a new ErrorDialog object 26 | */ 27 | ErrorDialog(); 28 | 29 | /** 30 | * @brief Destroy ErrorDialog object 31 | */ 32 | ~ErrorDialog() override = default; 33 | 34 | /** 35 | * @brief Set error message to show on dialog 36 | * @param message Error message 37 | */ 38 | void SetErrorMessage(const std::string_view& message); 39 | 40 | /* ******************************************************************************************** */ 41 | //! Custom implementation 42 | private: 43 | /** 44 | * @brief Renders the component 45 | * @return Element Built element based on internal state 46 | */ 47 | ftxui::Element RenderImpl(const ftxui::Dimensions& curr_size) const override; 48 | 49 | /** 50 | * @brief Handles an event (from mouse/keyboard) 51 | * @param event Received event from screen 52 | * @return true if event was handled, otherwise false 53 | */ 54 | bool OnEventImpl(const ftxui::Event& event) override; 55 | 56 | /** 57 | * @brief Handles an event (from mouse) 58 | * @param event Received event from screen 59 | * @return true if event was handled, otherwise false 60 | */ 61 | bool OnMouseEventImpl(ftxui::Event event) override; 62 | 63 | /** 64 | * @brief Callback to notify when dialog is closed 65 | */ 66 | void OnClose() override { message_.clear(); } 67 | 68 | /* ******************************************************************************************** */ 69 | //! Variables 70 | 71 | std::string message_; //!< Custom error message 72 | }; 73 | 74 | } // namespace interface 75 | #endif // INCLUDE_VIEW_ELEMENT_ERROR_DIALOG_H_ 76 | -------------------------------------------------------------------------------- /include/view/element/focus_controller.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class wrapper to control focus on a list of elements 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_FOCUS_CONTROLLER_H_ 7 | #define INCLUDE_VIEW_ELEMENT_FOCUS_CONTROLLER_H_ 8 | 9 | #include "view/base/element.h" 10 | #include "view/base/keybinding.h" 11 | 12 | namespace interface { 13 | 14 | //! Wrapper to control focus on elements based on external events 15 | // NOTE: in this first version, this class is considering only horizontal direction 16 | class FocusController final { 17 | using Keybinding = keybinding::Navigation; 18 | static constexpr int kInvalidIndex = -1; //!< Invalid index (when no element is focused) 19 | 20 | /* ******************************************************************************************** */ 21 | //! Public API 22 | public: 23 | /** 24 | * @brief Append elements to manage focus (using C++17 variadic template) 25 | * P.S.: Focus priority is based on elements order in internal vector 26 | * @tparam ...Args Elements 27 | * @param ...args Elements to be appended 28 | */ 29 | template 30 | void Append(Args&... args) { 31 | elements_.insert(elements_.end(), {static_cast(&args)...}); 32 | } 33 | 34 | /** 35 | * @brief Append array of elements to manage focus (using SFINAE to enable only for iterators) 36 | * P.S.: Focus priority is based on elements order in internal vector 37 | * @tparam Iterator Element container iterator 38 | * @param begin Initial element 39 | * @param end Last element 40 | */ 41 | template ::iterator_category, 43 | std::input_iterator_tag>> 44 | void Append(Iterator begin, Iterator end) { 45 | auto size = std::distance(begin, end); 46 | elements_.reserve(elements_.size() + size); 47 | 48 | for (auto it = begin; it != end; it++) elements_.push_back(&*it); 49 | } 50 | 51 | /** 52 | * @brief Handles an event (from keyboard) 53 | * @param event Received event from screen 54 | * @return true if event was handled, otherwise false 55 | */ 56 | bool OnEvent(const ftxui::Event& event); 57 | 58 | /** 59 | * @brief Handles an event (from mouse) 60 | * @param event Received event from screen 61 | * @return true if event was handled, otherwise false 62 | */ 63 | bool OnMouseEvent(ftxui::Event& event); 64 | 65 | /** 66 | * @brief Change index of focused element 67 | * @param index Element index 68 | */ 69 | void SetFocus(int index); 70 | 71 | /* ******************************************************************************************** */ 72 | //! Internal implementation 73 | private: 74 | /** 75 | * @brief Update focus state in both old and newly focused elements 76 | * @param old_index Element index with focus 77 | * @param new_index Element index to be focused 78 | */ 79 | void UpdateFocus(int old_index, int new_index); 80 | 81 | /** 82 | * @brief Check if contains any element focused 83 | * @return True if any element is focused, otherwise false 84 | */ 85 | bool HasElementFocused() const { return focus_index_ != kInvalidIndex; } 86 | 87 | /* ******************************************************************************************** */ 88 | //! Variables 89 | 90 | std::vector elements_; //!< List of elements ordered by focus priority 91 | int focus_index_ = kInvalidIndex; //!< Index to current element focused 92 | 93 | //!< List of mapped events to be handled as action key 94 | const std::array action_events{Keybinding::ArrowUp, Keybinding::ArrowDown, 95 | Keybinding::Up, Keybinding::Down, 96 | Keybinding::Space, Keybinding::Return}; 97 | }; 98 | 99 | } // namespace interface 100 | #endif // INCLUDE_VIEW_ELEMENT_FOCUS_CONTROLLER_H_ 101 | -------------------------------------------------------------------------------- /include/view/element/help_dialog.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for rendering a customized menu helper 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_HELP_H_ 7 | #define INCLUDE_VIEW_ELEMENT_HELP_H_ 8 | 9 | #include "view/base/dialog.h" 10 | 11 | namespace interface { 12 | 13 | /** 14 | * @brief Customized dialog box to show a helper 15 | */ 16 | class HelpDialog : public Dialog { 17 | static constexpr int kMaxColumns = 90; //!< Maximum columns for Element 18 | static constexpr int kMaxLines = 30; //!< Maximum lines for Element 19 | 20 | public: 21 | /** 22 | * @brief Construct a new Help object 23 | */ 24 | HelpDialog(); 25 | 26 | /** 27 | * @brief Destroy Help object 28 | */ 29 | ~HelpDialog() override = default; 30 | 31 | /* ******************************************************************************************** */ 32 | //! Custom implementation 33 | private: 34 | /** 35 | * @brief Renders the component 36 | * @return Element Built element based on internal state 37 | */ 38 | ftxui::Element RenderImpl(const ftxui::Dimensions& curr_size) const override; 39 | 40 | /** 41 | * @brief Handles an event (from mouse/keyboard) 42 | * @param event Received event from screen 43 | * @return true if event was handled, otherwise false 44 | */ 45 | bool OnEventImpl(const ftxui::Event& event) override; 46 | 47 | /** 48 | * @brief Handles an event (from mouse) 49 | * @param event Received event from screen 50 | * @return true if event was handled, otherwise false 51 | */ 52 | bool OnMouseEventImpl(ftxui::Event event) override; 53 | 54 | /* ******************************************************************************************** */ 55 | //! Public API 56 | public: 57 | /** 58 | * @brief Set dialog state to visible 59 | */ 60 | void ShowGeneralInfo(); 61 | 62 | /** 63 | * @brief Set dialog state to visible 64 | */ 65 | void ShowTabInfo(); 66 | 67 | /* ******************************************************************************************** */ 68 | //! UI utilities 69 | private: 70 | /** 71 | * @brief Possible tab views to render on this block 72 | */ 73 | enum class View { 74 | General, //!< Display general info (default) 75 | Tab, //!< Display tab info 76 | LAST, 77 | }; 78 | 79 | /** 80 | * @brief Build UI component for title 81 | * @param message Content to show as title 82 | * @return User interface element 83 | */ 84 | ftxui::Element title(const std::string& message) const; 85 | 86 | /** 87 | * @brief Build UI component for keybinding + command 88 | * @param keybind Keybinding option 89 | * @param description Command description 90 | * @return User interface element 91 | */ 92 | ftxui::Element command(const std::string& keybind, const std::string& description) const; 93 | 94 | /** 95 | * @brief Build UI component for general block information 96 | * @return User interface element 97 | */ 98 | ftxui::Element BuildGeneralInfo() const; 99 | 100 | /** 101 | * @brief Build UI component for tab information 102 | * @return User interface element 103 | */ 104 | ftxui::Element BuildTabInfo() const; 105 | 106 | /* ******************************************************************************************** */ 107 | //! Variables 108 | 109 | View active_ = View::General; //!< Current view displayed on dialog 110 | }; 111 | 112 | } // namespace interface 113 | #endif // INCLUDE_VIEW_ELEMENT_HELP_H_ 114 | -------------------------------------------------------------------------------- /include/view/element/internal/song_menu.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for rendering a customized menu containing text entries 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_INTERNAL_SONG_MENU_H_ 7 | #define INCLUDE_VIEW_ELEMENT_INTERNAL_SONG_MENU_H_ 8 | 9 | #include 10 | #include 11 | 12 | #include "ftxui/component/event.hpp" 13 | #include "ftxui/dom/elements.hpp" 14 | #include "model/song.h" 15 | #include "view/element/internal/base_menu.h" 16 | #include "view/element/text_animation.h" 17 | 18 | #ifdef ENABLE_TESTS 19 | namespace { 20 | class SidebarTest; 21 | } 22 | #endif 23 | 24 | namespace interface::internal { 25 | 26 | class SongMenu : public BaseMenu { 27 | friend class BaseMenu; 28 | 29 | //! Put together all possible styles for an entry in this component 30 | struct Style { 31 | ftxui::Decorator prefix; 32 | MenuEntryOption entry; 33 | }; 34 | 35 | public: 36 | //!< Callback definition for function that will be triggered when a menu entry is clicked/pressed 37 | using Callback = Callback; 38 | 39 | /** 40 | * @brief Construct a new SongMenu object 41 | * @param dispatcher Event dispatcher 42 | * @param force_refresh Callback function to update UI 43 | * @param on_click Callback function for click event on active entry 44 | */ 45 | explicit SongMenu(const std::shared_ptr& dispatcher, 46 | const TextAnimation::Callback& force_refresh, const Callback& on_click); 47 | 48 | /** 49 | * @brief Destroy Menu object 50 | */ 51 | ~SongMenu() override = default; 52 | 53 | /* ******************************************************************************************** */ 54 | //! Mandatory API implementation 55 | private: 56 | //! Renders the element 57 | ftxui::Element RenderImpl(); 58 | 59 | //! Handles an event (from mouse/keyboard) 60 | bool OnEventImpl(const ftxui::Event& event); 61 | 62 | //! Getter for menu entries size 63 | int GetSizeImpl() const; 64 | 65 | //! Getter for active entry as text (focused/selected) 66 | std::string GetActiveEntryAsTextImpl() const; 67 | 68 | //! Execute action on the active entry 69 | bool OnClickImpl(); 70 | 71 | //! While on search mode, filter all entries to keep only those matching the given text 72 | void FilterEntriesBy(const std::string& text); 73 | 74 | /* ******************************************************************************************** */ 75 | //! Setters and getters 76 | 77 | //! Save entries internally to render it as a menu 78 | void SetEntriesImpl(const std::deque& entries); 79 | 80 | //! Getter for entries 81 | std::deque GetEntriesImpl() const { 82 | return IsSearchEnabled() ? *filtered_entries_ : entries_; 83 | } 84 | 85 | //! Emplace a new entry 86 | void EmplaceImpl(const model::Song& entry) { 87 | LOG("Emplace a new entry to list"); 88 | entries_.emplace_back(entry); 89 | } 90 | 91 | //! Erase an existing entry 92 | void EraseImpl(const model::Song& entry) { 93 | LOG("Attempt to erase an entry with value=", entry.filepath); 94 | auto it = std::find_if(entries_.begin(), entries_.end(), [&entry](const model::Song& s) { 95 | return s.index == entry.index && s.filepath == entry.filepath; 96 | }); 97 | 98 | if (it != entries_.end()) { 99 | LOG("Found matching entry, erasing it, entry=", *it); 100 | entries_.erase(it); 101 | } 102 | } 103 | 104 | //! Set entry to be highlighted 105 | void SetEntryHighlightedImpl(const std::string&) {} 106 | 107 | //! Reset highlighted entry 108 | void ResetHighlightImpl() {} 109 | 110 | //! Getter for active entry (focused/selected) 111 | std::optional GetActiveEntryImpl() const; 112 | 113 | //! Reset search mode (if enabled) and highlight the given entry 114 | void ResetSearchImpl() { filtered_entries_.reset(); } 115 | 116 | /* ******************************************************************************************** */ 117 | //! Variables 118 | private: 119 | std::deque entries_; //!< List containing song entries 120 | 121 | //!< List containing only entries matching the text from search 122 | std::optional> filtered_entries_ = std::nullopt; 123 | 124 | Callback on_click_; //!< Callback function to trigger when menu entry is clicked/pressed 125 | 126 | //!< Style for each element inside this component 127 | Style style_ = Style{ 128 | .prefix = ftxui::color(ftxui::Color::SteelBlue1Bis), 129 | .entry = Colored(ftxui::Color::Grey11), 130 | }; 131 | 132 | /* ******************************************************************************************** */ 133 | //! Friend class for testing purpose 134 | 135 | #ifdef ENABLE_TESTS 136 | friend class ::SidebarTest; 137 | #endif 138 | }; 139 | 140 | } // namespace interface::internal 141 | #endif // INCLUDE_VIEW_ELEMENT_INTERNAL_SONG_MENU_H_ 142 | -------------------------------------------------------------------------------- /include/view/element/menu.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Header containing all menu specializations 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_MENU_H_ 7 | #define INCLUDE_VIEW_ELEMENT_MENU_H_ 8 | 9 | #include "view/element/internal/base_menu.h" 10 | #include "view/element/internal/file_menu.h" 11 | #include "view/element/internal/playlist_menu.h" 12 | #include "view/element/internal/song_menu.h" 13 | 14 | namespace interface { 15 | 16 | //! Aliases for all existing menu specializations 17 | using FileMenu = std::unique_ptr>; 18 | using PlaylistMenu = std::unique_ptr>; 19 | using SongMenu = std::unique_ptr>; 20 | 21 | /* --------------------------------------- Menu creation ---------------------------------------- */ 22 | 23 | namespace menu { 24 | 25 | /** 26 | * @brief Construct a new FileMenu object 27 | * @param dispatcher Event dispatcher 28 | * @param file_handler Utility handler to manage any file operation 29 | * @param force_refresh Callback function to update UI 30 | * @param on_click Callback function for click event on active entry 31 | * @param style Theme selection 32 | * @param optional_path Path to list files from 33 | */ 34 | FileMenu CreateFileMenu(const std::shared_ptr& dispatcher, 35 | const std::shared_ptr& file_handler, 36 | const TextAnimation::Callback& force_refresh, 37 | const internal::FileMenu::Callback& on_click, const Style& style, 38 | const std::string& optional_path = ""); 39 | 40 | /** 41 | * @brief Construct a new PlaylistMenu object 42 | * @param dispatcher Event dispatcher 43 | * @param force_refresh Callback function to update UI 44 | * @param on_click Callback function for click event on active entry 45 | */ 46 | PlaylistMenu CreatePlaylistMenu(const std::shared_ptr& dispatcher, 47 | const TextAnimation::Callback& force_refresh, 48 | const internal::PlaylistMenu::Callback& on_click); 49 | 50 | /** 51 | * @brief Construct a new SongMenu object 52 | * @param dispatcher Event dispatcher 53 | * @param force_refresh Callback function to update UI 54 | * @param on_click Callback function for click event on active entry 55 | */ 56 | SongMenu CreateSongMenu(const std::shared_ptr& dispatcher, 57 | const TextAnimation::Callback& force_refresh, 58 | const internal::SongMenu::Callback& on_click); 59 | 60 | } // namespace menu 61 | } // namespace interface 62 | #endif // INCLUDE_VIEW_ELEMENT_MENU_H_ 63 | -------------------------------------------------------------------------------- /include/view/element/question_dialog.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for rendering question dialog 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_QUESTION_DIALOG_H_ 7 | #define INCLUDE_VIEW_ELEMENT_QUESTION_DIALOG_H_ 8 | 9 | #include 10 | 11 | #include "model/question_data.h" 12 | #include "view/base/dialog.h" 13 | #include "view/element/button.h" 14 | 15 | namespace interface { 16 | 17 | /** 18 | * @brief Customized dialog box to show a question message 19 | */ 20 | class QuestionDialog : public Dialog { 21 | static constexpr int kMaxColumns = 45; //!< Maximum columns for Element 22 | static constexpr int kMaxLines = 5; //!< Maximum lines for Element 23 | 24 | public: 25 | /** 26 | * @brief Construct a new QuestionDialog object 27 | */ 28 | QuestionDialog(); 29 | 30 | /** 31 | * @brief Destroy QuestionDialog object 32 | */ 33 | virtual ~QuestionDialog() = default; 34 | 35 | /** 36 | * @brief Set question message to show on dialog 37 | * @param data Question message with custom callbacks 38 | */ 39 | void SetMessage(const model::QuestionData& data); 40 | 41 | /* ******************************************************************************************** */ 42 | //! Custom implementation 43 | private: 44 | /** 45 | * @brief Renders the component 46 | * @return Element Built element based on internal state 47 | */ 48 | ftxui::Element RenderImpl(const ftxui::Dimensions& curr_size) const override; 49 | 50 | /** 51 | * @brief Handles an event (from mouse/keyboard) 52 | * @param event Received event from screen 53 | * @return true if event was handled, otherwise false 54 | */ 55 | bool OnEventImpl(const ftxui::Event& event) override; 56 | 57 | /** 58 | * @brief Handles an event (from mouse) 59 | * @param event Received event from screen 60 | * @return true if event was handled, otherwise false 61 | */ 62 | bool OnMouseEventImpl(ftxui::Event event) override; 63 | 64 | /** 65 | * @brief Callback to notify when dialog is closed 66 | */ 67 | void OnClose() override { content_.reset(); } 68 | 69 | /* ******************************************************************************************** */ 70 | //! Variables 71 | 72 | std::optional content_; // !< Question content 73 | 74 | GenericButton btn_yes_; //!< "Yes" button 75 | GenericButton btn_no_; //!< "No" button 76 | }; 77 | 78 | } // namespace interface 79 | #endif // INCLUDE_VIEW_ELEMENT_QUESTION_DIALOG_H_ 80 | -------------------------------------------------------------------------------- /include/view/element/style.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Header for UI style 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_STYLE_H_ 7 | #define INCLUDE_VIEW_ELEMENT_STYLE_H_ 8 | 9 | namespace interface { 10 | 11 | // TODO: use this to set UI size constants 12 | 13 | } // namespace interface 14 | #endif // INCLUDE_VIEW_ELEMENT_STYLE_H_ 15 | -------------------------------------------------------------------------------- /include/view/element/text_animation.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Header for Text Animation element 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_TEXT_ANIMATION_H_ 7 | #define INCLUDE_VIEW_ELEMENT_TEXT_ANIMATION_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace interface { 17 | 18 | /** 19 | * @brief An structure to offset selected entry text when its content is too long 20 | */ 21 | struct TextAnimation { 22 | using Callback = std::function; //!< Callback triggered by internal thread 23 | 24 | std::mutex mutex = std::mutex(); //!< Control access for internal resources 25 | std::condition_variable notifier = 26 | std::condition_variable(); //!< Conditional variable to block thread 27 | std::thread thread = std::thread(); //!< Thread to perform offset animation on text 28 | 29 | std::atomic enabled = false; //!< Flag to control thread animation 30 | std::string text = ""; //!< Entry text to perform animation 31 | 32 | Callback cb_update = nullptr; //!< Force an UI refresh 33 | 34 | //! Destructor 35 | ~TextAnimation(); 36 | 37 | /** 38 | * @brief Start animation thread 39 | * @param entry Text content from selected entry 40 | */ 41 | void Start(const std::string& entry); 42 | 43 | /** 44 | * @brief Stop animation thread 45 | */ 46 | void Stop(); 47 | 48 | private: 49 | /** 50 | * @brief Disable thread execution 51 | */ 52 | void Notify(); 53 | 54 | /** 55 | * @brief Notify thread and wait for its stop 56 | */ 57 | void Exit(); 58 | }; 59 | 60 | } // namespace interface 61 | #endif // INCLUDE_VIEW_ELEMENT_TEXT_ANIMATION_H_ 62 | -------------------------------------------------------------------------------- /include/view/element/util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Header for UI utils 4 | */ 5 | 6 | #ifndef INCLUDE_VIEW_ELEMENT_UTIL_H_ 7 | #define INCLUDE_VIEW_ELEMENT_UTIL_H_ 8 | 9 | namespace interface { 10 | 11 | //! Similar to std::clamp, but allow hi to be lower than lo. 12 | template 13 | inline constexpr const T& clamp(const T& v, const T& lo, const T& hi) { 14 | return v < lo ? lo : hi < v ? hi : v; 15 | } 16 | 17 | } // namespace interface 18 | #endif // INCLUDE_VIEW_ELEMENT_UTIL_H_ 19 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=v1nns_spectrum 2 | sonar.organization=v1nns 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | sonar.projectName=spectrum 6 | sonar.projectVersion=1.0 7 | 8 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 9 | sonar.sources=src,include 10 | sonar.tests=test 11 | sonar.cfamily.build-wrapper-output=build_wrapper_output_directory 12 | sonar.cfamily.threads=2 13 | sonar.cfamily.gcov.reportsPath=build/gcov-reports 14 | sonar.python.version=2.7, 3.9 15 | 16 | # Encoding of the source code. Default is default system encoding 17 | #sonar.sourceEncoding=UTF-8 18 | -------------------------------------------------------------------------------- /src/audio/command.cc: -------------------------------------------------------------------------------- 1 | #include "audio/command.h" 2 | 3 | namespace audio { 4 | 5 | //! Command::Identifier pretty print 6 | std::ostream& operator<<(std::ostream& out, const Command::Identifier& i) { 7 | switch (i) { 8 | case Command::Identifier::None: 9 | out << "None"; 10 | break; 11 | case Command::Identifier::Play: 12 | out << "Play"; 13 | break; 14 | case Command::Identifier::PauseOrResume: 15 | out << "PauseOrResume"; 16 | break; 17 | case Command::Identifier::Stop: 18 | out << "Stop"; 19 | break; 20 | case Command::Identifier::SeekForward: 21 | out << "SeekForward"; 22 | break; 23 | case Command::Identifier::SeekBackward: 24 | out << "SeekBackward"; 25 | break; 26 | case Command::Identifier::SetVolume: 27 | out << "SetVolume"; 28 | break; 29 | case Command::Identifier::UpdateAudioFilters: 30 | out << "UpdateAudioFilter"; 31 | break; 32 | case Command::Identifier::Exit: 33 | out << "Exit"; 34 | break; 35 | } 36 | 37 | return out; 38 | } 39 | 40 | //! Command pretty print 41 | std::ostream& operator<<(std::ostream& out, const Command& cmd) { 42 | out << cmd.id; 43 | return out; 44 | } 45 | 46 | //! Commands pretty print 47 | std::ostream& operator<<(std::ostream& out, const std::vector& cmds) { 48 | if (cmds.empty()) { 49 | out << "Empty"; 50 | return out; 51 | } 52 | 53 | out << "{"; 54 | 55 | std::vector::const_iterator i, j; 56 | for (i = cmds.begin(), j = --cmds.end(); i != j; ++i) { 57 | out << i->id << ","; 58 | } 59 | 60 | out << j->id; 61 | out << "}"; 62 | 63 | return out; 64 | } 65 | 66 | /* ********************************************************************************************** */ 67 | 68 | // Static 69 | Command Command::None() { 70 | return Command{ 71 | .id = Identifier::None, 72 | }; 73 | } 74 | 75 | /* ********************************************************************************************** */ 76 | 77 | // Static 78 | Command Command::Play(const std::string& filepath) { 79 | return Command{ 80 | .id = Identifier::Play, 81 | .content = filepath, 82 | }; 83 | } 84 | 85 | /* ********************************************************************************************** */ 86 | 87 | // Static 88 | Command Command::PauseOrResume() { 89 | return Command{ 90 | .id = Identifier::PauseOrResume, 91 | }; 92 | } 93 | 94 | /* ********************************************************************************************** */ 95 | 96 | // Static 97 | Command Command::Stop() { 98 | return Command{ 99 | .id = Identifier::Stop, 100 | }; 101 | } 102 | 103 | /* ********************************************************************************************** */ 104 | 105 | // Static 106 | Command Command::SeekForward(int offset) { 107 | return Command{ 108 | .id = Identifier::SeekForward, 109 | .content = offset, 110 | }; 111 | } 112 | 113 | /* ********************************************************************************************** */ 114 | 115 | // Static 116 | Command Command::SeekBackward(int offset) { 117 | return Command{ 118 | .id = Identifier::SeekBackward, 119 | .content = offset, 120 | }; 121 | } 122 | 123 | /* ********************************************************************************************** */ 124 | 125 | // Static 126 | Command Command::SetVolume(const model::Volume& value) { 127 | return Command{ 128 | .id = Identifier::SetVolume, 129 | .content = value, 130 | }; 131 | } 132 | 133 | /* ********************************************************************************************** */ 134 | 135 | // Static 136 | Command Command::UpdateAudioFilters(const model::EqualizerPreset& filters) { 137 | return Command{ 138 | .id = Identifier::UpdateAudioFilters, 139 | .content = filters, 140 | }; 141 | } 142 | 143 | /* ********************************************************************************************** */ 144 | 145 | // Static 146 | Command Command::Exit() { 147 | return Command{ 148 | .id = Identifier::Exit, 149 | }; 150 | } 151 | 152 | } // namespace audio 153 | -------------------------------------------------------------------------------- /src/audio/lyric/driver/curl_wrapper.cc: -------------------------------------------------------------------------------- 1 | #include "audio/lyric/driver/curl_wrapper.h" 2 | 3 | #include 4 | 5 | #include "util/logger.h" 6 | 7 | namespace driver { 8 | 9 | error::Code CURLWrapper::Fetch(const std::string &URL, std::string &output) { 10 | // Initialize cURL 11 | SmartCURL curl(curl_easy_init(), &curl_easy_cleanup); 12 | 13 | if (!curl) { 14 | ERROR("Failed to initialize cURL"); 15 | return error::kUnknownError; 16 | } 17 | 18 | // Configure URL and write callback for response 19 | curl_easy_setopt(curl.get(), CURLOPT_URL, URL.c_str()); 20 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, CURLWrapper::WriteCallback); 21 | curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &output); 22 | 23 | // Set maximum timeout and disable any signal/alarm handlers 24 | curl_easy_setopt(curl.get(), CURLOPT_CONNECTTIMEOUT, 10L); 25 | curl_easy_setopt(curl.get(), CURLOPT_NOSIGNAL, 1); 26 | 27 | // Set header configuration for accept type and user agent 28 | curl_easy_setopt(curl.get(), CURLOPT_ACCEPT_ENCODING, kAcceptType.data()); 29 | curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, kUserAgent.data()); 30 | 31 | // Configure buffer to write error message 32 | std::vector err_buffer(CURL_ERROR_SIZE); 33 | curl_easy_setopt(curl.get(), CURLOPT_ERRORBUFFER, &err_buffer[0]); 34 | 35 | // Set up some configuration to avoid being ignored by any CGI (Common Gateway Interface) 36 | curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); 37 | curl_easy_setopt(curl.get(), CURLOPT_REFERER, URL.c_str()); 38 | 39 | // Enable TLSv1.3 version only 40 | curl_easy_setopt(curl.get(), CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_3); 41 | 42 | LOG("Fetching content from URL=", URL); 43 | if (CURLcode result = curl_easy_perform(curl.get()); result != CURLE_OK) { 44 | ERROR("Failed to execute cURL, error=", std::string(err_buffer.begin(), err_buffer.end())); 45 | return error::kUnknownError; 46 | } 47 | 48 | return error::kSuccess; 49 | } 50 | 51 | /* ********************************************************************************************** */ 52 | 53 | size_t CURLWrapper::WriteCallback(const char *buffer, size_t size, size_t nmemb, void *data) { 54 | size_t result = size * nmemb; 55 | static_cast(data)->append(buffer, result); 56 | return result; 57 | } 58 | 59 | } // namespace driver 60 | -------------------------------------------------------------------------------- /src/audio/lyric/driver/libxml_wrapper.cc: -------------------------------------------------------------------------------- 1 | #include "audio/lyric/driver/libxml_wrapper.h" 2 | 3 | #include "util/logger.h" 4 | 5 | namespace driver { 6 | 7 | lyric::SongLyric LIBXMLWrapper::Parse(const std::string &data, const std::string &xpath) { 8 | // Parse HTML and create a DOM tree 9 | XmlDocGuard doc{htmlReadDoc((const xmlChar *)data.c_str(), nullptr, nullptr, 10 | HTML_PARSE_RECOVER | HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING), 11 | &xmlFreeDoc}; 12 | 13 | // Encapsulate raw libxml document in a libxml++ wrapper 14 | xmlNode *r = xmlDocGetRootElement(doc.get()); 15 | auto root = std::make_unique(r); 16 | 17 | // Create structure to fill with lyrics 18 | lyric::SongLyric raw_lyric; 19 | 20 | try { 21 | // Find for XPath in parsed data 22 | auto elements = root->find(xpath); 23 | 24 | // If found, web scrap it to lyrics 25 | if (!elements.empty()) ScrapContent(elements.front(), raw_lyric); 26 | 27 | } catch (xmlpp::exception &err) { 28 | ERROR("Failed to scrap content using libXML++, error=", err.what()); 29 | } 30 | 31 | return raw_lyric; 32 | } 33 | 34 | /* ********************************************************************************************** */ 35 | 36 | void LIBXMLWrapper::ScrapContent(const xmlpp::Node *node, lyric::SongLyric &lyric) { 37 | // Safely convert node to classes down along its inheritance hierarchy 38 | const auto node_text = dynamic_cast(node); 39 | const auto node_content = dynamic_cast(node); 40 | 41 | // Consider only TextNode as the node that contains any lyric content 42 | if (node_text) lyric.push_back(node_text->get_content()); 43 | 44 | if (!node_content && node) { 45 | // Recurse through child nodes to filter lyric 46 | for (const auto &child : node->get_children()) { 47 | ScrapContent(child, lyric); 48 | } 49 | } 50 | } 51 | 52 | } // namespace driver 53 | -------------------------------------------------------------------------------- /src/audio/lyric/lyric_finder.cc: -------------------------------------------------------------------------------- 1 | #include "audio/lyric/lyric_finder.h" 2 | 3 | #include "model/application_error.h" 4 | 5 | #ifndef SPECTRUM_DEBUG 6 | #include "audio/lyric/driver/curl_wrapper.h" 7 | #include "audio/lyric/driver/libxml_wrapper.h" 8 | #else 9 | #include "debug/dummy_fetcher.h" 10 | #include "debug/dummy_parser.h" 11 | #endif 12 | 13 | #include "util/logger.h" 14 | 15 | namespace lyric { 16 | 17 | std::unique_ptr LyricFinder::Create(driver::UrlFetcher* fetcher, 18 | driver::HtmlParser* parser) { 19 | LOG("Create new instance of lyric finder"); 20 | 21 | #ifndef SPECTRUM_DEBUG 22 | // Create fetcher object 23 | auto ft = fetcher != nullptr ? std::unique_ptr(std::move(fetcher)) 24 | : std::make_unique(); 25 | 26 | // Create parser object 27 | auto ps = parser != nullptr ? std::unique_ptr(std::move(parser)) 28 | : std::make_unique(); 29 | #else 30 | // Create fetcher object 31 | auto ft = std::make_unique(); 32 | 33 | // Create parser object 34 | auto ps = std::make_unique(); 35 | #endif 36 | 37 | // Simply extend the LyricFinder class, as we do not want to expose the default constructor, 38 | // neither do we want to use std::make_unique explicitly calling operator new() 39 | struct MakeUniqueEnabler : public LyricFinder { 40 | explicit MakeUniqueEnabler(std::unique_ptr&& fetcher, 41 | std::unique_ptr&& parser) 42 | : LyricFinder(std::move(fetcher), std::move(parser)) {} 43 | }; 44 | 45 | // Instantiate LyricFinder 46 | return std::make_unique(std::move(ft), std::move(ps)); 47 | } 48 | 49 | /* ********************************************************************************************** */ 50 | 51 | LyricFinder::LyricFinder(std::unique_ptr&& fetcher, 52 | std::unique_ptr&& parser) 53 | : fetcher_{std::move(fetcher)}, parser_{std::move(parser)} {} 54 | 55 | /* ********************************************************************************************** */ 56 | 57 | SongLyric LyricFinder::Search(const std::string& artist, const std::string& title) { 58 | LOG("Started fetching song by artist=", artist, " title=", title); 59 | std::string buffer; 60 | SongLyric lyrics; 61 | 62 | for (const auto& engine : engines_) { 63 | // Fetch content from search engine 64 | if (auto result = fetcher_->Fetch(engine->FormatSearchUrl(artist, title), buffer); 65 | result != error::kSuccess) { 66 | ERROR("Failed to fetch URL content, error code=", result); 67 | continue; 68 | } 69 | 70 | // Web scrap content to search for lyric 71 | if (SongLyric raw = parser_->Parse(buffer, engine->xpath()); !raw.empty()) { 72 | if (SongLyric formatted = engine->FormatLyrics(raw); !formatted.empty()) { 73 | LOG("Found lyrics using search engine=", *engine); 74 | lyrics.swap(formatted); 75 | break; 76 | } 77 | } 78 | } 79 | 80 | return lyrics; 81 | } 82 | 83 | } // namespace lyric 84 | -------------------------------------------------------------------------------- /src/audio/lyric/search_config.cc: -------------------------------------------------------------------------------- 1 | #include "audio/lyric/search_config.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "util/logger.h" 7 | 8 | namespace lyric { 9 | 10 | /* ---------------------------------------------------------------------------------------------- */ 11 | /* Create config with available search engines */ 12 | /* ---------------------------------------------------------------------------------------------- */ 13 | 14 | Config SearchConfig::Create() { 15 | return Config{ 16 | std::make_unique(), 17 | std::make_unique(), 18 | }; 19 | } 20 | 21 | /* ---------------------------------------------------------------------------------------------- */ 22 | /* Google */ 23 | /* ---------------------------------------------------------------------------------------------- */ 24 | 25 | std::string Google::FormatSearchUrl(const std::string &artist, const std::string &name) const { 26 | std::string raw_url = url_ + artist + "+" + name; 27 | return std::regex_replace(raw_url, std::regex(" "), "+"); 28 | } 29 | 30 | /* ********************************************************************************************** */ 31 | 32 | SongLyric Google::FormatLyrics(const SongLyric &raw) const { 33 | std::string::size_type pos = 0; 34 | std::string::size_type prev = 0; 35 | SongLyric lyric; 36 | 37 | if (raw.size() != 1) { 38 | ERROR("Received more raw data than expected"); 39 | return lyric; 40 | } 41 | 42 | const auto &content = raw.front(); 43 | 44 | // Split into paragraphs 45 | while ((pos = content.find("\n\n", prev)) != std::string::npos) { 46 | pos += 1; // To avoid having a \n in the beginning 47 | lyric.push_back(content.substr(prev, pos - prev)); 48 | prev = pos + 1; 49 | } 50 | 51 | lyric.push_back(content.substr(prev)); 52 | return lyric; 53 | } 54 | 55 | /* ---------------------------------------------------------------------------------------------- */ 56 | /* AZLyrics */ 57 | /* ---------------------------------------------------------------------------------------------- */ 58 | 59 | std::string AZLyrics::FormatSearchUrl(const std::string &artist, const std::string &name) const { 60 | std::string formatted_url = url_ + artist + "/" + name + ".html"; 61 | 62 | // Transform string into lowercase 63 | std::transform(formatted_url.begin(), formatted_url.end(), formatted_url.begin(), 64 | [](unsigned char c) { return std::tolower(c); }); 65 | 66 | // Erase whitespaces 67 | formatted_url.erase(std::remove_if(formatted_url.begin(), formatted_url.end(), ::isspace), 68 | formatted_url.end()); 69 | 70 | return formatted_url; 71 | } 72 | 73 | /* ********************************************************************************************** */ 74 | 75 | SongLyric AZLyrics::FormatLyrics(const SongLyric &raw) const { 76 | SongLyric lyric; 77 | std::string paragraph; 78 | 79 | for (const auto &line : raw) { 80 | // first line 81 | if (line == "\r\n") continue; 82 | 83 | // newline means paragraph is finished 84 | if (line == "\n" && !paragraph.empty()) { 85 | lyric.push_back(paragraph); 86 | paragraph.clear(); 87 | continue; 88 | } 89 | 90 | // Otherwise it is a common line, remove any carriage return or line feed characters from it 91 | std::string tmp = std::regex_replace(line, std::regex("[\r\n]+"), ""); 92 | if (tmp.size() > 0) paragraph.append(tmp + "\n"); 93 | } 94 | 95 | // We may not receive the last newline, append last paragraph if not empty 96 | if (!paragraph.empty()) { 97 | lyric.push_back(paragraph); 98 | } 99 | 100 | return lyric; 101 | } 102 | 103 | } // namespace lyric 104 | -------------------------------------------------------------------------------- /src/model/bar_animation.cc: -------------------------------------------------------------------------------- 1 | #include "model/bar_animation.h" 2 | 3 | namespace model { 4 | 5 | //! BarAnimation pretty print 6 | std::ostream& operator<<(std::ostream& out, const BarAnimation& animation) { 7 | switch (animation) { 8 | case BarAnimation::HorizontalMirror: 9 | out << "HorizontalMirror"; 10 | break; 11 | case BarAnimation::VerticalMirror: 12 | out << "VerticalMirror"; 13 | break; 14 | case BarAnimation::Mono: 15 | out << "Mono"; 16 | break; 17 | case BarAnimation::LAST: 18 | out << "Invalid"; 19 | break; 20 | } 21 | 22 | return out; 23 | } 24 | 25 | } // namespace model -------------------------------------------------------------------------------- /src/model/block_identifier.cc: -------------------------------------------------------------------------------- 1 | #include "model/block_identifier.h" 2 | 3 | namespace model { 4 | 5 | //! BlockIdentifier pretty print 6 | std::ostream& operator<<(std::ostream& out, const BlockIdentifier& id) { 7 | switch (id) { 8 | case BlockIdentifier::Sidebar: 9 | out << "Sidebar"; 10 | break; 11 | case BlockIdentifier::FileInfo: 12 | out << "FileInfo"; 13 | break; 14 | case BlockIdentifier::MainContent: 15 | out << "MainContent"; 16 | break; 17 | case BlockIdentifier::MediaPlayer: 18 | out << "MediaPlayer"; 19 | break; 20 | case BlockIdentifier::None: 21 | out << "None"; 22 | break; 23 | } 24 | 25 | return out; 26 | } 27 | 28 | } // namespace model 29 | -------------------------------------------------------------------------------- /src/model/playlist.cc: -------------------------------------------------------------------------------- 1 | #include "model/playlist.h" 2 | 3 | namespace model { 4 | 5 | std::ostream& operator<<(std::ostream& out, const Playlist& p) { 6 | out << "{id:" << p.index << " playlist:" << std::quoted(p.name) << " songs:" << p.songs.size() 7 | << "}"; 8 | return out; 9 | } 10 | 11 | /* ********************************************************************************************** */ 12 | 13 | bool operator==(const Playlist& lhs, const Playlist& rhs) { 14 | return std::tie(lhs.index, lhs.name, lhs.songs) == std::tie(rhs.index, rhs.name, rhs.songs); 15 | } 16 | 17 | /* ********************************************************************************************** */ 18 | 19 | bool operator!=(const Playlist& lhs, const Playlist& rhs) { return !(lhs == rhs); } 20 | 21 | /* ********************************************************************************************** */ 22 | 23 | Song Playlist::PopFront() { 24 | auto song = songs.front(); 25 | songs.pop_front(); 26 | 27 | return song; 28 | } 29 | 30 | /* ********************************************************************************************** */ 31 | 32 | void PrintTo(const Playlist& p, std::ostream* os) { 33 | *os << "{id:" << p.index << " playlist:" << std::quoted(p.name); 34 | 35 | *os << " songs:{"; 36 | 37 | std::deque::const_iterator i, j; 38 | for (i = p.songs.begin(), j = --p.songs.end(); i != j; ++i) { 39 | *os << i->filepath << ","; 40 | } 41 | 42 | *os << j->filepath; 43 | *os << "} }"; 44 | } 45 | 46 | } // namespace model 47 | -------------------------------------------------------------------------------- /src/model/playlist_operation.cc: -------------------------------------------------------------------------------- 1 | #include "model/playlist_operation.h" 2 | 3 | namespace model { 4 | 5 | std::string PlaylistOperation::GetActionName(const PlaylistOperation& playlist) { 6 | using Operation = PlaylistOperation::Operation; 7 | 8 | switch (playlist.action) { 9 | case Operation::None: 10 | return "None"; 11 | case Operation::Create: 12 | return "Create"; 13 | case Operation::Modify: 14 | return "Modify"; 15 | default: 16 | break; 17 | } 18 | 19 | return "Unknown"; 20 | } 21 | 22 | /* ********************************************************************************************** */ 23 | 24 | //! PlaylistOperation pretty print 25 | std::ostream& operator<<(std::ostream& out, const PlaylistOperation& p) { 26 | out << "{action: " << PlaylistOperation::GetActionName(p) << ", playlist: "; 27 | 28 | if (p.playlist.has_value()) 29 | operator<<(out, p.playlist.value()); 30 | else 31 | out << "{empty}"; 32 | 33 | out << "}"; 34 | return out; 35 | } 36 | 37 | /* ********************************************************************************************** */ 38 | 39 | bool operator==(const PlaylistOperation& lhs, const PlaylistOperation& rhs) { 40 | return std::tie(lhs.action, lhs.playlist) == std::tie(rhs.action, rhs.playlist); 41 | } 42 | 43 | /* ********************************************************************************************** */ 44 | 45 | bool operator!=(const PlaylistOperation& lhs, const PlaylistOperation& rhs) { 46 | return !(lhs == rhs); 47 | } 48 | 49 | } // namespace model 50 | -------------------------------------------------------------------------------- /src/model/question_data.cc: -------------------------------------------------------------------------------- 1 | #include "model/question_data.h" 2 | 3 | #include 4 | 5 | namespace model { 6 | 7 | //! QuestionData pretty print 8 | std::ostream& operator<<(std::ostream& out, const QuestionData& q) { 9 | out << "{question: " << std::quoted(q.question) 10 | << ", cb_yes:" << (q.cb_yes ? std::quoted("not empty") : std::quoted("empty")) 11 | << ", cb_no:" << (q.cb_no ? std::quoted("not empty") : std::quoted("empty")) << "}"; 12 | 13 | return out; 14 | } 15 | 16 | /* ********************************************************************************************** */ 17 | 18 | bool operator==(const QuestionData& lhs, const QuestionData& rhs) { 19 | // Do not want to check memory's address from callbacks, right? 20 | return lhs.question == rhs.question; 21 | } 22 | 23 | /* ********************************************************************************************** */ 24 | 25 | bool operator!=(const QuestionData& lhs, const QuestionData& rhs) { return !(lhs == rhs); } 26 | 27 | } // namespace model 28 | -------------------------------------------------------------------------------- /src/util/logger.cc: -------------------------------------------------------------------------------- 1 | #include "util/logger.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace util { 8 | 9 | std::string get_timestamp() { 10 | // Get time 11 | std::chrono::system_clock::time_point time_point = std::chrono::system_clock::now(); 12 | std::time_t now = std::chrono::system_clock::to_time_t(time_point); 13 | 14 | // Convert into local time 15 | struct tm local_time; 16 | localtime_r(&now, &local_time); 17 | 18 | // Get the fractional part in milliseconds 19 | const auto milliseconds = std::chrono::time_point_cast(time_point) 20 | .time_since_epoch() 21 | .count() % 22 | 1000; 23 | 24 | // Format string 25 | std::ostringstream ss; 26 | ss << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S") << "." << std::setfill('0') << std::setw(3) 27 | << milliseconds; 28 | 29 | return std::move(ss).str(); 30 | } 31 | 32 | /* ********************************************************************************************** */ 33 | 34 | void Logger::Configure(const std::string& path) { 35 | sink_ = std::make_unique(path); 36 | 37 | // Write initial message to log 38 | std::ostringstream ss; 39 | std::string header(kHeaderColumns, '-'); 40 | 41 | ss << header << " Initializing log " << header << "\n"; 42 | Write(std::move(ss).str(), false); 43 | } 44 | 45 | /* ********************************************************************************************** */ 46 | 47 | void Logger::Configure() { sink_ = std::make_unique(); } 48 | 49 | /* ********************************************************************************************** */ 50 | 51 | void Logger::Write(const std::string& message, bool add_timestamp) { 52 | // Lock mutex 53 | std::scoped_lock lock{mutex_}; 54 | 55 | // Check if should (re)open stream 56 | sink_->OpenStream(); 57 | 58 | // Write to output stream 59 | if (add_timestamp) *sink_ << "[" << get_timestamp() << "] "; 60 | *sink_ << message; 61 | } 62 | 63 | } // namespace util 64 | -------------------------------------------------------------------------------- /src/util/sink.cc: -------------------------------------------------------------------------------- 1 | #include "util/sink.h" 2 | 3 | #include 4 | 5 | namespace util { 6 | 7 | FileSink::FileSink(const std::string& path) : ImplSink(), path_{path} {} 8 | 9 | /* ********************************************************************************************** */ 10 | 11 | void FileSink::Open() { 12 | auto now = std::chrono::system_clock::now(); 13 | 14 | if ((now - last_reopen_) > reopen_interval_) { 15 | Close(); 16 | 17 | try { 18 | // Open file 19 | out_stream_.reset(new std::ofstream(path_, std::ofstream::out | std::ofstream::app)); 20 | last_reopen_ = now; 21 | } catch (std::exception&) { 22 | CloseStream(); 23 | throw; 24 | } 25 | } 26 | } 27 | 28 | /* ********************************************************************************************** */ 29 | 30 | void FileSink::Close() { 31 | try { 32 | out_stream_.reset(); 33 | } catch (...) { 34 | // As we hold an ostream inside a shared_ptr, when we reset it, we are counting on the deleter 35 | // to release its resources... And that's why we don't care about exceptions here 36 | } 37 | } 38 | 39 | /* ********************************************************************************************** */ 40 | 41 | void ConsoleSink::Open() { 42 | if (!out_stream_) { 43 | // No-operation deleter, otherwise we will get in trouble 44 | out_stream_.reset(&std::cout, [](const void*) { 45 | // For this sink, it is used the std::cout itself, so when the shared_ptr is reset, no deleter 46 | // should be called for the object 47 | }); 48 | } 49 | } 50 | 51 | /* ********************************************************************************************** */ 52 | 53 | void ConsoleSink::Close() { 54 | try { 55 | out_stream_.reset(); 56 | } catch (...) { 57 | // As we hold an ostream inside a shared_ptr, when we reset it, we are counting on the deleter 58 | // to release its resources... And that's why we don't care about exceptions here 59 | } 60 | } 61 | 62 | } // namespace util 63 | -------------------------------------------------------------------------------- /src/view/base/block.cc: -------------------------------------------------------------------------------- 1 | #include "view/base/block.h" 2 | 3 | #include "util/logger.h" 4 | #include "view/base/event_dispatcher.h" 5 | 6 | namespace interface { 7 | 8 | Block::Block(const std::shared_ptr& dispatcher, const model::BlockIdentifier& id, 9 | const Size& size) 10 | : ftxui::ComponentBase{}, dispatcher_{dispatcher}, id_{id}, size_{size} {} 11 | 12 | /* ********************************************************************************************** */ 13 | 14 | void Block::SetFocused(bool focused) { 15 | focused_ = focused; 16 | 17 | if (focused_) 18 | OnFocus(); 19 | else 20 | OnLostFocus(); 21 | } 22 | 23 | /* ********************************************************************************************** */ 24 | 25 | ftxui::Decorator Block::GetTitleDecorator() const { 26 | using ftxui::bgcolor; 27 | using ftxui::bold; 28 | using ftxui::Color; 29 | using ftxui::color; 30 | 31 | ftxui::Decorator style = focused_ 32 | ? bgcolor(Color::SteelBlue3) | color(Color::PaleTurquoise1) | bold 33 | : bgcolor(Color::GrayDark) | color(Color::GrayLight); 34 | 35 | return style; 36 | } 37 | 38 | /* ********************************************************************************************** */ 39 | 40 | ftxui::Decorator Block::GetBorderDecorator() const { 41 | using ftxui::bgcolor; 42 | using ftxui::Color; 43 | using ftxui::color; 44 | using ftxui::nothing; 45 | 46 | ftxui::Decorator style = focused_ ? color(Color::SteelBlue3) : nothing; 47 | 48 | return style; 49 | } 50 | 51 | /* ********************************************************************************************** */ 52 | 53 | void Block::AskForFocus() const { 54 | if (focused_) return; 55 | 56 | auto dispatcher = GetDispatcher(); 57 | 58 | // Set this block as active (focused) 59 | auto event = interface::CustomEvent::SetFocused(id_); 60 | dispatcher->SendEvent(event); 61 | } 62 | 63 | /* ********************************************************************************************** */ 64 | 65 | std::shared_ptr Block::GetDispatcher() const { 66 | auto dispatcher = dispatcher_.lock(); 67 | if (!dispatcher) { 68 | ERROR("Cannot lock event dispatcher"); 69 | throw std::runtime_error("Cannot lock event dispatcher"); 70 | } 71 | 72 | return dispatcher; 73 | } 74 | 75 | } // namespace interface 76 | -------------------------------------------------------------------------------- /src/view/base/dialog.cc: -------------------------------------------------------------------------------- 1 | #include "view/base/dialog.h" 2 | 3 | #include "ftxui/dom/elements.hpp" 4 | #include "view/base/keybinding.h" 5 | 6 | namespace interface { 7 | 8 | Dialog::Dialog(const Size& size, const Style& style) : size_{size}, style_{style} { 9 | if (size.min_line) size_.min_line += kBorderSize; 10 | if (size.min_column) size_.min_column += kBorderSize; 11 | } 12 | 13 | /* ********************************************************************************************** */ 14 | 15 | ftxui::Element Dialog::Render(const ftxui::Dimensions& curr_size) const { 16 | using ftxui::EQUAL; 17 | using ftxui::HEIGHT; 18 | using ftxui::WIDTH; 19 | 20 | // Calculate both width and height 21 | int width = curr_size.dimx * size_.width; 22 | int height = curr_size.dimy * size_.height; 23 | 24 | // Check if it is not below the minimum value 25 | if (size_.min_column && width < size_.min_column) width = size_.min_column; 26 | if (size_.min_line && height < size_.min_line) height = size_.min_line; 27 | 28 | // Check if it is not above the maximum value 29 | if (size_.max_column && width > size_.max_column) width = size_.max_column; 30 | if (size_.max_line && height > size_.max_line) height = size_.max_line; 31 | 32 | // Create border decorator style 33 | auto border_decorator = ftxui::borderStyled(ftxui::DOUBLE, ftxui::Color::Grey85); 34 | 35 | // Create dialog decorator style 36 | auto decorator = ftxui::size(HEIGHT, EQUAL, height) | ftxui::size(WIDTH, EQUAL, width) | 37 | ftxui::bgcolor(style_.background) | ftxui::color(style_.foreground) | 38 | ftxui::clear_under | ftxui::center; 39 | 40 | return RenderImpl(curr_size) | border_decorator | decorator; 41 | } 42 | 43 | /* ********************************************************************************************** */ 44 | 45 | bool Dialog::OnEvent(const ftxui::Event& event) { 46 | if (event.is_mouse() && OnMouseEventImpl(event)) { 47 | return true; 48 | } 49 | 50 | if (OnEventImpl(event)) { 51 | return true; 52 | } 53 | 54 | if (event == keybinding::Navigation::Escape || event == keybinding::Navigation::Close) { 55 | Close(); 56 | return true; 57 | } 58 | 59 | return false; 60 | } 61 | 62 | } // namespace interface 63 | -------------------------------------------------------------------------------- /src/view/base/element.cc: -------------------------------------------------------------------------------- 1 | #include "view/base/element.h" 2 | 3 | namespace interface { 4 | 5 | ftxui::Element Element::Render() { return ftxui::text(""); } 6 | 7 | /* ********************************************************************************************** */ 8 | 9 | bool Element::OnEvent(const ftxui::Event& event) { 10 | // Optional implementation 11 | return false; 12 | } 13 | 14 | /* ********************************************************************************************** */ 15 | 16 | bool Element::OnMouseEvent(ftxui::Event& event) { 17 | if (event.mouse().button != ftxui::Mouse::Left && event.mouse().button != ftxui::Mouse::None) 18 | return false; 19 | 20 | if (!box_.Contain(event.mouse().x, event.mouse().y)) { 21 | hovered_ = false; 22 | return false; 23 | } 24 | 25 | hovered_ = true; 26 | 27 | if (event.mouse().button == ftxui::Mouse::WheelDown || 28 | event.mouse().button == ftxui::Mouse::WheelUp) { 29 | HandleWheel(event.mouse().button); 30 | return true; 31 | } else if (event.mouse().button == ftxui::Mouse::Left && 32 | event.mouse().motion == ftxui::Mouse::Released) { 33 | // Check if this is a double-click event 34 | auto now = std::chrono::system_clock::now(); 35 | 36 | if (now - last_click_ <= std::chrono::milliseconds(500)) 37 | HandleDoubleClick(event); 38 | else 39 | HandleClick(event); 40 | 41 | last_click_ = now; 42 | return true; 43 | } else { 44 | HandleHover(event); 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /* ********************************************************************************************** */ 51 | 52 | bool Element::HandleActionKey(const ftxui::Event& event) { 53 | // Optional implementation 54 | return false; 55 | } 56 | 57 | /* ********************************************************************************************** */ 58 | 59 | void Element::HandleClick(ftxui::Event& event) { 60 | // Optional implementation 61 | } 62 | 63 | /* ********************************************************************************************** */ 64 | 65 | void Element::HandleDoubleClick(ftxui::Event& event) { 66 | // Optional implementation 67 | } 68 | 69 | /* ********************************************************************************************** */ 70 | 71 | void Element::HandleHover(ftxui::Event& event) { 72 | // Optional implementation 73 | } 74 | 75 | /* ********************************************************************************************** */ 76 | 77 | void Element::HandleWheel(const ftxui::Mouse::Button& button) { 78 | // Optional implementation 79 | } 80 | 81 | } // namespace interface 82 | -------------------------------------------------------------------------------- /src/view/block/file_info.cc: -------------------------------------------------------------------------------- 1 | #include "view/block/file_info.h" 2 | 3 | #include 4 | 5 | #include "ftxui/component/event.hpp" 6 | #include "util/logger.h" 7 | #include "view/base/event_dispatcher.h" 8 | 9 | namespace interface { 10 | 11 | FileInfo::FileInfo(const std::shared_ptr& dispatcher) 12 | : Block{dispatcher, model::BlockIdentifier::FileInfo, 13 | interface::Size{.width = 0, .height = kMaxRows}}, 14 | audio_info_(kMaxSongLines) { 15 | // Fill with default content 16 | ParseAudioInfo(model::Song{}); 17 | } 18 | 19 | /* ********************************************************************************************** */ 20 | 21 | ftxui::Element FileInfo::Render() { 22 | using ftxui::EQUAL; 23 | using ftxui::HEIGHT; 24 | using ftxui::LESS_THAN; 25 | using ftxui::WIDTH; 26 | 27 | ftxui::Elements lines; 28 | lines.reserve(audio_info_.size()); 29 | 30 | // Choose a different color for when there is no current song 31 | ftxui::Color::Palette256 color = 32 | is_song_playing_ ? ftxui::Color::LightSteelBlue1 : ftxui::Color::LightSteelBlue3; 33 | 34 | for (const auto& [field, value] : audio_info_) { 35 | // Calculate maximum width for text value 36 | int width = kMaxColumns - field.size(); 37 | 38 | // Create element 39 | ftxui::Element item = ftxui::hbox({ 40 | ftxui::text(field) | ftxui::bold | ftxui::color(ftxui::Color::SteelBlue1), 41 | ftxui::filler(), 42 | // TODO: maybe use TextAnimation element for Field filename 43 | ftxui::text(value) | ftxui::align_right | ftxui::size(WIDTH, LESS_THAN, width) | 44 | ftxui::color(ftxui::Color(color)), 45 | }); 46 | 47 | lines.push_back(item); 48 | } 49 | 50 | ftxui::Element content = ftxui::vbox(lines); 51 | 52 | return ftxui::window(ftxui::hbox(ftxui::text(" information ") | GetTitleDecorator()), content) | 53 | ftxui::size(HEIGHT, EQUAL, kMaxRows) | GetBorderDecorator(); 54 | } 55 | 56 | /* ********************************************************************************************** */ 57 | 58 | bool FileInfo::OnEvent(ftxui::Event event) { return false; } 59 | 60 | /* ********************************************************************************************** */ 61 | 62 | bool FileInfo::OnCustomEvent(const CustomEvent& event) { 63 | // Do not return true because other blocks may use it 64 | if (event == CustomEvent::Identifier::ClearSongInfo) { 65 | LOG("Clear current song information"); 66 | ParseAudioInfo(model::Song{}); 67 | } 68 | 69 | // Do not return true because other blocks may use it 70 | if (event == CustomEvent::Identifier::UpdateSongInfo) { 71 | LOG("Received new song information from player"); 72 | ParseAudioInfo(event.GetContent()); 73 | } 74 | 75 | return false; 76 | } 77 | 78 | /* ********************************************************************************************** */ 79 | 80 | void FileInfo::ParseAudioInfo(const model::Song& audio) { 81 | audio_info_.clear(); 82 | is_song_playing_ = !audio.IsEmpty(); 83 | 84 | // Use istringstream to split string into lines and parse it as 85 | std::istringstream input{model::to_string(audio)}; 86 | 87 | for (std::string line; std::getline(input, line);) { 88 | size_t pos = line.find_first_of(':'); 89 | std::string field = line.substr(0, pos), value = line.substr(pos + 1); 90 | 91 | audio_info_.push_back({field, value}); 92 | } 93 | } 94 | 95 | } // namespace interface 96 | -------------------------------------------------------------------------------- /src/view/element/error_dialog.cc: -------------------------------------------------------------------------------- 1 | #include "view/element/error_dialog.h" 2 | 3 | #include "view/base/keybinding.h" 4 | 5 | namespace interface { 6 | 7 | ErrorDialog::ErrorDialog() 8 | : Dialog(Size{.min_column = kMaxColumns, .min_line = kMaxLines}, 9 | Style{.background = ftxui::Color::DarkRedBis, .foreground = ftxui::Color::Grey93}) {} 10 | 11 | /* ********************************************************************************************** */ 12 | 13 | void ErrorDialog::SetErrorMessage(const std::string_view& message) { 14 | message_ = message; 15 | Open(); 16 | } 17 | 18 | /* ********************************************************************************************** */ 19 | 20 | ftxui::Element ErrorDialog::RenderImpl(const ftxui::Dimensions& curr_size) const { 21 | return ftxui::vbox({ 22 | ftxui::text(" ERROR") | ftxui::bold, 23 | ftxui::text(""), 24 | ftxui::paragraph(message_) | ftxui::center | ftxui::bold, 25 | }); 26 | } 27 | 28 | /* ********************************************************************************************** */ 29 | 30 | bool ErrorDialog::OnEventImpl(const ftxui::Event& event) { 31 | using Keybind = keybinding::Navigation; 32 | 33 | if (event == Keybind::Return) { 34 | Close(); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | /* ********************************************************************************************** */ 42 | 43 | bool ErrorDialog::OnMouseEventImpl(ftxui::Event event) { return false; } 44 | 45 | } // namespace interface 46 | -------------------------------------------------------------------------------- /src/view/element/focus_controller.cc: -------------------------------------------------------------------------------- 1 | #include "view/element/focus_controller.h" 2 | 3 | #include "util/formatter.h" 4 | #include "util/logger.h" 5 | 6 | namespace interface { 7 | 8 | bool FocusController::OnEvent(const ftxui::Event& event) { 9 | // Navigate on elements 10 | if (event == Keybinding::ArrowRight || event == Keybinding::Right) { 11 | LOG("Handle navigation key=", util::EventToString(event)); 12 | 13 | // Calculate new index based on upper bound 14 | int new_index = 15 | focus_index_ + (focus_index_ < (static_cast(elements_.size()) - 1) ? 1 : 0); 16 | UpdateFocus(focus_index_, new_index); 17 | 18 | return true; 19 | } 20 | 21 | // Navigate on elements 22 | if (event == Keybinding::ArrowLeft || event == Keybinding::Left) { 23 | LOG("Handle navigation key=", util::EventToString(event)); 24 | 25 | // Calculate new index based on lower bound 26 | int new_index = focus_index_ - (focus_index_ > (kInvalidIndex + 1) ? 1 : 0); 27 | UpdateFocus(focus_index_, new_index); 28 | 29 | return true; 30 | } 31 | 32 | if (HasElementFocused()) { 33 | // Pass event to element if mapped as navigation key 34 | if (auto found = std::find(action_events.begin(), action_events.end(), event); 35 | found != action_events.end() && elements_[focus_index_]->HandleActionKey(event)) { 36 | LOG("Element with index=", focus_index_, "handled action key=", util::EventToString(event)); 37 | return true; 38 | } 39 | 40 | if (elements_[focus_index_]->OnEvent(event)) { 41 | LOG("Element with index=", focus_index_, "handled event=", util::EventToString(event)); 42 | return true; 43 | } 44 | 45 | // Remove focus state from element 46 | if (event == Keybinding::Escape) { 47 | // Invalidate old index for focused 48 | LOG("Handle navigation key=", util::EventToString(event)); 49 | UpdateFocus(focus_index_, kInvalidIndex); 50 | 51 | return true; 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | 58 | /* ********************************************************************************************** */ 59 | 60 | bool FocusController::OnMouseEvent(ftxui::Event& event) { 61 | // Iterate through all elements and pass event, if event is handled, update UI state 62 | bool event_handled = std::any_of(elements_.begin(), elements_.end(), [&event](Element* element) { 63 | if (!element) return false; 64 | return element->OnMouseEvent(event); 65 | }); 66 | 67 | return event_handled; 68 | } 69 | 70 | /* ********************************************************************************************** */ 71 | 72 | void FocusController::SetFocus(int index) { 73 | if (!elements_.empty() && (index + 1) <= elements_.size()) { 74 | UpdateFocus(focus_index_, index); 75 | } 76 | } 77 | 78 | /* ********************************************************************************************** */ 79 | 80 | void FocusController::UpdateFocus(int old_index, int new_index) { 81 | // If equal, do nothing 82 | if (old_index == new_index) return; 83 | 84 | // Remove focus from old focused frequency bar 85 | if (old_index != kInvalidIndex) elements_[old_index]->SetFocus(false); 86 | 87 | // Set focus on newly-focused frequency bar 88 | if (new_index != kInvalidIndex) elements_[new_index]->SetFocus(true); 89 | 90 | // Update internal index 91 | focus_index_ = new_index; 92 | } 93 | 94 | } // namespace interface 95 | -------------------------------------------------------------------------------- /src/view/element/menu.cc: -------------------------------------------------------------------------------- 1 | #include "view/element/menu.h" 2 | 3 | namespace interface { 4 | namespace menu { 5 | 6 | FileMenu CreateFileMenu(const std::shared_ptr& dispatcher, 7 | const std::shared_ptr& file_handler, 8 | const TextAnimation::Callback& force_refresh, 9 | const internal::FileMenu::Callback& on_click, const menu::Style& style, 10 | const std::string& optional_path) { 11 | return std::make_unique(dispatcher, file_handler, force_refresh, on_click, 12 | style, optional_path); 13 | } 14 | 15 | /* ********************************************************************************************** */ 16 | 17 | PlaylistMenu CreatePlaylistMenu(const std::shared_ptr& dispatcher, 18 | const TextAnimation::Callback& force_refresh, 19 | const internal::PlaylistMenu::Callback& on_click) { 20 | return std::make_unique(dispatcher, force_refresh, on_click); 21 | } 22 | 23 | /* ********************************************************************************************** */ 24 | 25 | SongMenu CreateSongMenu(const std::shared_ptr& dispatcher, 26 | const TextAnimation::Callback& force_refresh, 27 | const internal::SongMenu::Callback& on_click) { 28 | return std::make_unique(dispatcher, force_refresh, on_click); 29 | } 30 | 31 | } // namespace menu 32 | } // namespace interface 33 | -------------------------------------------------------------------------------- /src/view/element/question_dialog.cc: -------------------------------------------------------------------------------- 1 | #include "view/element/question_dialog.h" 2 | 3 | #include "util/logger.h" 4 | #include "view/base/keybinding.h" 5 | 6 | namespace interface { 7 | 8 | QuestionDialog::QuestionDialog() 9 | : Dialog(Size{.min_column = kMaxColumns, .min_line = kMaxLines}, 10 | Style{.background = ftxui::Color::SteelBlue, .foreground = ftxui::Color::Grey93}) { 11 | auto style = Button::Style{ 12 | .normal = 13 | Button::Style::State{ 14 | .foreground = ftxui::Color::Black, 15 | .background = ftxui::Color::SteelBlue1, 16 | }, 17 | .focused = 18 | Button::Style::State{ 19 | .foreground = ftxui::Color::DeepSkyBlue4Ter, 20 | .background = ftxui::Color::LightSkyBlue1, 21 | }, 22 | .pressed = 23 | Button::Style::State{ 24 | .foreground = ftxui::Color::SkyBlue1, 25 | .background = ftxui::Color::Blue1, 26 | }, 27 | .width = 15, 28 | }; 29 | 30 | btn_yes_ = Button::make_button_minimal( 31 | std::string("Yes"), 32 | [this]() { 33 | LOG("Handle \"yes\" button"); 34 | if (content_->cb_yes) content_->cb_yes(); 35 | 36 | Close(); 37 | return true; 38 | }, 39 | style); 40 | 41 | btn_no_ = Button::make_button_minimal( 42 | std::string("No"), 43 | [this]() { 44 | LOG("Handle \"no\" button"); 45 | if (content_->cb_no) content_->cb_no(); 46 | 47 | Close(); 48 | return true; 49 | }, 50 | style); 51 | } 52 | 53 | /* ********************************************************************************************** */ 54 | 55 | void QuestionDialog::SetMessage(const model::QuestionData& data) { content_ = data; } 56 | 57 | /* ********************************************************************************************** */ 58 | 59 | ftxui::Element QuestionDialog::RenderImpl(const ftxui::Dimensions& curr_size) const { 60 | return ftxui::vbox({ 61 | ftxui::text(""), 62 | ftxui::paragraph(content_->question) | ftxui::center | ftxui::bold | 63 | ftxui::color(ftxui::Color::Black), 64 | ftxui::text(""), 65 | ftxui::hbox(btn_yes_->Render(), ftxui::text(" "), btn_no_->Render()) | ftxui::flex | 66 | ftxui::center | ftxui::bold, 67 | }); 68 | } 69 | 70 | /* ********************************************************************************************** */ 71 | 72 | bool QuestionDialog::OnEventImpl(const ftxui::Event& event) { 73 | using Keybind = keybinding::Navigation; 74 | 75 | if (event == keybinding::Dialog::Yes) { 76 | btn_yes_->OnClick(); 77 | return true; 78 | } 79 | 80 | if (event == keybinding::Dialog::No) { 81 | btn_no_->OnClick(); 82 | return true; 83 | } 84 | 85 | if (event == Keybind::Escape || event == Keybind::Close) { 86 | Close(); 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | /* ********************************************************************************************** */ 94 | 95 | bool QuestionDialog::OnMouseEventImpl(ftxui::Event event) { 96 | if (btn_yes_->OnMouseEvent(event)) return true; 97 | if (btn_no_->OnMouseEvent(event)) return true; 98 | 99 | return false; 100 | } 101 | 102 | } // namespace interface 103 | -------------------------------------------------------------------------------- /src/view/element/tab.cc: -------------------------------------------------------------------------------- 1 | #include "view/element/tab.h" 2 | 3 | #include 4 | 5 | #include "util/formatter.h" 6 | #include "util/logger.h" 7 | 8 | namespace interface { 9 | 10 | /* ****************************************** TabItem ******************************************* */ 11 | 12 | //! TabItem pretty print 13 | std::ostream& operator<<(std::ostream& out, const TabItem& item) { 14 | out << std::quoted(item.title_); 15 | return out; 16 | } 17 | 18 | Button::Style TabItem::kTabButtonStyle = Button::Style{ 19 | .normal = 20 | Button::Style::State{ 21 | .foreground = ftxui::Color::GrayDark, 22 | .background = ftxui::Color(), 23 | }, 24 | .focused = 25 | Button::Style::State{ 26 | .foreground = ftxui::Color::GrayLight, 27 | .background = ftxui::Color::GrayDark, 28 | }, 29 | .selected = 30 | Button::Style::State{ 31 | .foreground = ftxui::Color::LightSteelBlue1, 32 | .background = ftxui::Color::SteelBlue3, 33 | }, 34 | 35 | .delimiters = Button::Delimiters{" ", " "}, 36 | }; 37 | 38 | /* ********************************************************************************************** */ 39 | 40 | TabItem::TabItem(const model::BlockIdentifier& id, 41 | const std::shared_ptr& dispatcher, const FocusCallback& on_focus, 42 | const keybinding::Key& keybinding, const std::string& title) 43 | : dispatcher_{dispatcher}, 44 | parent_id_{id}, 45 | on_focus_{on_focus}, 46 | key_{keybinding}, 47 | title_{title}, 48 | button_{Button::make_button_for_window( 49 | util::EventToString(keybinding) + ":" + title, 50 | [this]() { 51 | LOG("Handle left click mouse event on Tab button for ", title_); 52 | 53 | // Send event to set focus on this block 54 | on_focus_(); 55 | 56 | return true; 57 | }, 58 | kTabButtonStyle)} {} 59 | 60 | /* ********************************************************************************************** */ 61 | 62 | bool TabItem::OnEvent(const ftxui::Event&) { return false; } 63 | 64 | /* ********************************************************************************************** */ 65 | 66 | bool TabItem::OnMouseEvent(ftxui::Event&) { return false; } 67 | 68 | /* ********************************************************************************************** */ 69 | 70 | bool TabItem::OnCustomEvent(const CustomEvent&) { return false; } 71 | 72 | /* ********************************************************************************************** */ 73 | 74 | void TabItem::OnFocus() { 75 | // do nothing 76 | } 77 | 78 | /* ********************************************************************************************** */ 79 | 80 | void TabItem::OnLostFocus() { 81 | // do nothing 82 | } 83 | 84 | /* ******************************************** Tab ********************************************* */ 85 | 86 | void Tab::SetActive(const View& item) { 87 | if (active_ != kEmpty) { 88 | // Unselect window button from old active item 89 | active_item()->GetButton()->Unselect(); 90 | active_item()->OnLostFocus(); 91 | } 92 | 93 | // Update active tab identifier and button state 94 | active_ = item; 95 | active_item()->GetButton()->Select(); 96 | active_item()->OnFocus(); 97 | } 98 | 99 | } // namespace interface 100 | -------------------------------------------------------------------------------- /src/view/element/text_animation.cc: -------------------------------------------------------------------------------- 1 | #include "view/element/text_animation.h" 2 | 3 | namespace interface { 4 | 5 | TextAnimation::~TextAnimation() { 6 | // Ensure that thread will be stopped 7 | Stop(); 8 | } 9 | 10 | /* ********************************************************************************************** */ 11 | 12 | void TextAnimation::Start(const std::string& entry) { 13 | // Append an empty space for better aesthetics 14 | text = entry + " "; 15 | enabled = true; 16 | 17 | thread = std::thread([this] { 18 | using namespace std::chrono_literals; 19 | std::unique_lock lock(mutex); 20 | 21 | // Run the animation every 0.2 seconds while enabled is true 22 | while (!notifier.wait_for(lock, 0.2s, [this] { return enabled == false; })) { 23 | // Here comes the magic 24 | text += text.front(); 25 | text.erase(text.begin()); 26 | 27 | // Notify UI 28 | cb_update(); 29 | } 30 | }); 31 | } 32 | 33 | /* ********************************************************************************************** */ 34 | 35 | void TextAnimation::Stop() { 36 | if (enabled) { 37 | Notify(); 38 | Exit(); 39 | } 40 | } 41 | 42 | /* ********************************************************************************************** */ 43 | 44 | void TextAnimation::Notify() { 45 | std::scoped_lock lock(mutex); 46 | enabled = false; 47 | } 48 | 49 | /* ********************************************************************************************** */ 50 | 51 | void TextAnimation::Exit() { 52 | notifier.notify_one(); 53 | thread.join(); 54 | } 55 | 56 | } // namespace interface 57 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # ************************************************************************************************** 2 | # External dependencies 3 | 4 | FetchContent_Declare( 5 | googletest 6 | GIT_REPOSITORY https://github.com/google/googletest 7 | GIT_TAG v1.14.0) 8 | 9 | # For Windows: Prevent overriding the parent project's compiler/linker settings 10 | set(gtest_force_shared_crt 11 | ON 12 | CACHE BOOL "" FORCE) 13 | 14 | # Do not install anything from GTest 15 | option(INSTALL_GMOCK OFF) 16 | option(INSTALL_GTEST OFF) 17 | 18 | FetchContent_MakeAvailable(googletest) 19 | 20 | # ************************************************************************************************** 21 | # Create executable 22 | 23 | enable_testing() 24 | 25 | add_executable(test) 26 | target_sources( 27 | test 28 | PRIVATE audio_lyric_finder.cc 29 | audio_player.cc 30 | block_file_info.cc 31 | block_main_content.cc 32 | block_media_player.cc 33 | block_sidebar.cc 34 | dialog_playlist.cc 35 | driver_fftw.cc 36 | middleware_media_controller.cc 37 | util_argparser.cc) 38 | 39 | target_link_libraries(test PRIVATE GTest::gtest GTest::gmock GTest::gtest_main 40 | spectrum_lib) 41 | 42 | target_include_directories(test PRIVATE ${CMAKE_SOURCE_DIR}/include 43 | ${CMAKE_SOURCE_DIR}/test) 44 | 45 | target_compile_options(test PRIVATE -Wall -Werror -Wno-sign-compare) 46 | 47 | # Use a default path for ListDirectory block unit testing 48 | target_compile_definitions(test PUBLIC LISTDIR_PATH="${CMAKE_SOURCE_DIR}/test") 49 | 50 | check_coverage(test) 51 | 52 | include(GoogleTest) 53 | gtest_discover_tests(test DISCOVERY_TIMEOUT 30) 54 | -------------------------------------------------------------------------------- /test/block_file_info.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "general/block.h" 4 | #include "general/utils.h" 5 | #include "mock/event_dispatcher_mock.h" 6 | #include "view/block/file_info.h" 7 | 8 | namespace { 9 | 10 | using ::testing::StrEq; 11 | 12 | /** 13 | * @brief Tests with FileInfo class 14 | */ 15 | class FileInfoTest : public ::BlockTest { 16 | protected: 17 | void SetUp() override { 18 | // Create a custom screen with fixed size 19 | screen = std::make_unique(32, 15); 20 | 21 | // Create mock for event dispatcher 22 | dispatcher = std::make_shared(); 23 | 24 | // Create FileInfo block 25 | block = ftxui::Make(dispatcher); 26 | 27 | // Set this block as focused 28 | auto dummy = std::static_pointer_cast(block); 29 | dummy->SetFocused(true); 30 | } 31 | }; 32 | 33 | /* ********************************************************************************************** */ 34 | 35 | TEST_F(FileInfoTest, InitialRender) { 36 | ftxui::Render(*screen, block->Render()); 37 | 38 | std::string rendered = utils::FilterAnsiCommands(screen->ToString()); 39 | 40 | std::string expected = R"( 41 | ╭ information ─────────────────╮ 42 | │Filename │ 43 | │Artist │ 44 | │Title │ 45 | │Channels │ 46 | │Sample rate │ 47 | │Bit rate │ 48 | │Bits per sample │ 49 | │Duration │ 50 | │ │ 51 | │ │ 52 | │ │ 53 | │ │ 54 | │ │ 55 | ╰──────────────────────────────╯)"; 56 | 57 | EXPECT_THAT(rendered, StrEq(expected)); 58 | } 59 | 60 | /* ********************************************************************************************** */ 61 | 62 | TEST_F(FileInfoTest, UpdateSongInfo) { 63 | model::Song audio{ 64 | .filepath = "/some/custom/path/to/song.mp3", 65 | .artist = "Baco Exu do Blues", 66 | .title = "Lágrimas", 67 | .num_channels = 2, 68 | .sample_rate = 44100, 69 | .bit_rate = 256000, 70 | .bit_depth = 32, 71 | .duration = 123, 72 | }; 73 | 74 | // Process custom event on block 75 | auto event = interface::CustomEvent::UpdateSongInfo(audio); 76 | Process(event); 77 | 78 | ftxui::Render(*screen, block->Render()); 79 | 80 | std::string rendered = utils::FilterAnsiCommands(screen->ToString()); 81 | 82 | std::string expected = R"( 83 | ╭ information ─────────────────╮ 84 | │Filename song.mp3│ 85 | │Artist Baco Exu do Blues│ 86 | │Title Lágrimas│ 87 | │Channels 2│ 88 | │Sample rate 44.1 kHz│ 89 | │Bit rate 256 kbps│ 90 | │Bits per sample 32 bits│ 91 | │Duration 123 sec│ 92 | │ │ 93 | │ │ 94 | │ │ 95 | │ │ 96 | │ │ 97 | ╰──────────────────────────────╯)"; 98 | 99 | EXPECT_THAT(rendered, StrEq(expected)); 100 | } 101 | 102 | /* ********************************************************************************************** */ 103 | 104 | TEST_F(FileInfoTest, UpdateAndClearSongInfo) { 105 | model::Song audio{ 106 | .filepath = "/some/custom/path/to/another/song.mp3", 107 | .artist = "ARTY", 108 | .title = "Poison For Lovers", 109 | .num_channels = 2, 110 | .sample_rate = 96000, 111 | .bit_rate = 256000, 112 | .bit_depth = 32, 113 | .duration = 123, 114 | }; 115 | 116 | // Process custom event on block 117 | auto event_update = interface::CustomEvent::UpdateSongInfo(audio); 118 | Process(event_update); 119 | 120 | // Process custom event on block 121 | auto event_clear = interface::CustomEvent::ClearSongInfo(); 122 | Process(event_clear); 123 | 124 | ftxui::Render(*screen, block->Render()); 125 | 126 | std::string rendered = utils::FilterAnsiCommands(screen->ToString()); 127 | 128 | std::string expected = R"( 129 | ╭ information ─────────────────╮ 130 | │Filename │ 131 | │Artist │ 132 | │Title │ 133 | │Channels │ 134 | │Sample rate │ 135 | │Bit rate │ 136 | │Bits per sample │ 137 | │Duration │ 138 | │ │ 139 | │ │ 140 | │ │ 141 | │ │ 142 | │ │ 143 | ╰──────────────────────────────╯)"; 144 | 145 | EXPECT_THAT(rendered, StrEq(expected)); 146 | } 147 | 148 | } // namespace 149 | -------------------------------------------------------------------------------- /test/driver_fftw.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "audio/driver/fftw.h" 12 | #include "util/logger.h" 13 | 14 | namespace { 15 | 16 | using ::testing::ElementsAreArray; 17 | using ::testing::Matcher; 18 | 19 | /** 20 | * @brief Tests with FFTW class 21 | */ 22 | class FftwTest : public ::testing::Test { 23 | // using-declarations 24 | using Fftw = std::unique_ptr; 25 | 26 | protected: 27 | static void SetUpTestSuite() { util::Logger::GetInstance().Configure(); } 28 | 29 | void SetUp() override { Init(); } 30 | 31 | void TearDown() override { analyzer.reset(); } 32 | 33 | void Init() { 34 | analyzer = std::make_unique(); 35 | analyzer->Init(kNumberBars * 2); 36 | } 37 | 38 | // TODO: implement (get block starting on line :78) 39 | void PrintResults(const std::vector& result) {} 40 | 41 | protected: 42 | static constexpr int kNumberBars = 10; //!< Number of bars per channel 43 | static constexpr int kBufferSize = 1024; //!< Input buffer size 44 | 45 | Fftw analyzer; //!< Audio frequency analysis 46 | }; 47 | 48 | /* ********************************************************************************************** */ 49 | 50 | TEST_F(FftwTest, InitAndExecute) { 51 | // Create expected results 52 | const Matcher expected_200MHz[kNumberBars] = {0, 0, 0.999, 0.009, 0, 0.001, 0, 0, 0, 0}; 53 | const Matcher expected_2000MHz[kNumberBars] = {0, 0, 0, 0, 0, 0, 0.524, 0.474, 0, 0}; 54 | 55 | // Create in/out buffers 56 | int out_size = analyzer->GetOutputSize(); 57 | std::vector out(out_size, 0); 58 | std::vector in(kBufferSize, 0); 59 | 60 | // Running execute 300 times (simulating about 3.5 seconds run time 61 | for (int k = 0; k < 300; k++) { 62 | // Filling up 512*2 samples at a time, making sure the sinus wave is unbroken 63 | // 200MHz in left channel, 2000MHz in right 64 | for (int n = 0; n < kBufferSize / 2; n++) { 65 | in[n * 2] = sin(2 * M_PI * 200 / 44100 * (n + ((float)k * kBufferSize / 2))) * 20000; 66 | in[n * 2 + 1] = sin(2 * M_PI * 2000 / 44100 * (n + ((float)k * kBufferSize / 2))) * 20000; 67 | } 68 | 69 | analyzer->Execute(in.data(), kBufferSize, out.data()); 70 | } 71 | 72 | // Rounding last output to nearest 1/1000th 73 | for (int i = 0; i < out_size; i++) { 74 | out[i] = (double)round(out[i] * 1000) / 1000; 75 | } 76 | 77 | // Split result by channel 78 | std::vector left(out.begin(), out.begin() + 10); 79 | std::vector right(out.begin() + 10, out.begin() + 20); 80 | 81 | // Print results 82 | std::cout.setf(std::ios::fixed, std::ios::floatfield); 83 | std::cout << "\nlast output from channel left, max value should be at 200Hz:\n"; 84 | for (const auto& value : left) { 85 | std::cout << std::setprecision(3) << value << " \t"; 86 | } 87 | std::cout << "MHz\n\n"; 88 | 89 | std::cout << "last output from channel right, max value should be at 2000Hz:\n"; 90 | for (const auto& value : right) { 91 | std::cout << std::setprecision(3) << value << " \t"; 92 | } 93 | std::cout << "MHz\n\n"; 94 | 95 | // Check that values are equal to expectation 96 | ASSERT_THAT(left, ElementsAreArray(expected_200MHz)); 97 | ASSERT_THAT(right, ElementsAreArray(expected_2000MHz)); 98 | } 99 | 100 | } // namespace 101 | -------------------------------------------------------------------------------- /test/general/block.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for UI testing 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_GENERAL_BLOCK_H_ 7 | #define INCLUDE_TEST_GENERAL_BLOCK_H_ 8 | 9 | #include 10 | 11 | #include 12 | 13 | #include "mock/event_dispatcher_mock.h" 14 | #include "util/logger.h" 15 | 16 | namespace { 17 | 18 | /** 19 | * @brief Interface class for tests with block component from UI 20 | */ 21 | class BlockTest : public ::testing::Test { 22 | protected: 23 | static void SetUpTestSuite() { util::Logger::GetInstance().Configure(); } 24 | 25 | virtual void SetUp() override = 0; 26 | 27 | void TearDown() override { 28 | screen.reset(); 29 | dispatcher.reset(); 30 | block.reset(); 31 | } 32 | 33 | void Process(interface::CustomEvent event) { 34 | auto component = std::static_pointer_cast(block); 35 | component->OnCustomEvent(event); 36 | } 37 | 38 | protected: 39 | std::unique_ptr screen; 40 | std::shared_ptr dispatcher; 41 | ftxui::Component block; 42 | }; 43 | 44 | } // namespace 45 | #endif // INCLUDE_TEST_GENERAL_BLOCK_H_ 46 | -------------------------------------------------------------------------------- /test/general/dialog.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Interface class for UI testing 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_GENERAL_DIALOG_H_ 7 | #define INCLUDE_TEST_GENERAL_DIALOG_H_ 8 | 9 | #include 10 | 11 | #include 12 | 13 | #include "mock/event_dispatcher_mock.h" 14 | #include "util/logger.h" 15 | #include "view/base/dialog.h" 16 | 17 | namespace { 18 | 19 | /** 20 | * @brief Interface class for tests with dialog component from UI 21 | */ 22 | class DialogTest : public ::testing::Test { 23 | protected: 24 | static void SetUpTestSuite() { util::Logger::GetInstance().Configure(); } 25 | 26 | virtual void SetUp() override = 0; 27 | 28 | void TearDown() override { 29 | screen.reset(); 30 | dispatcher.reset(); 31 | dialog.reset(); 32 | } 33 | 34 | protected: 35 | std::unique_ptr screen; 36 | std::shared_ptr dispatcher; 37 | std::shared_ptr dialog; 38 | }; 39 | 40 | } // namespace 41 | #endif // INCLUDE_TEST_GENERAL_DIALOG_H_ 42 | -------------------------------------------------------------------------------- /test/general/sync_testing.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Class for synchronized testing 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_GENERAL_SYNC_TESTING_H_ 7 | #define INCLUDE_TEST_GENERAL_SYNC_TESTING_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace testing { 17 | 18 | /** 19 | * @brief Shared class to syncronize steps between multiple threads used for testing 20 | */ 21 | class TestSyncer { 22 | public: 23 | //! Default constructor/destructor 24 | TestSyncer() = default; 25 | virtual ~TestSyncer() = default; 26 | 27 | //! Remove these 28 | TestSyncer(const TestSyncer& other) = delete; // copy constructor 29 | TestSyncer(TestSyncer&& other) = delete; // move constructor 30 | TestSyncer& operator=(const TestSyncer& other) = delete; // copy assignment 31 | TestSyncer& operator=(TestSyncer&& other) = delete; // move assignment 32 | 33 | //! Keep blocked until receives desired step 34 | void WaitForStep(int step) { 35 | auto id = std::this_thread::get_id(); 36 | std::cout << "thread id [" << std::hex << id << std::dec << "] is waiting for step: " << step 37 | << std::endl; 38 | std::unique_lock lock(mutex_); 39 | cond_var_.wait(lock, [&] { return step_ == step; }); 40 | } 41 | 42 | //! Notify with new step to unblock the other thread that is waiting for it 43 | void NotifyStep(int step) { 44 | auto id = std::this_thread::get_id(); 45 | std::cout << "thread id [" << std::hex << id << std::dec << "] notifying step: " << step 46 | << std::endl; 47 | std::unique_lock lock(mutex_); 48 | step_ = step; 49 | cond_var_.notify_one(); 50 | } 51 | 52 | private: 53 | std::mutex mutex_; 54 | std::condition_variable cond_var_; 55 | std::atomic step_; // TODO: change for "queue" 56 | }; 57 | 58 | //! Default function declaration to run asynchronously 59 | using SyncThread = std::function; 60 | 61 | /** 62 | * @brief Run multiple functions, each one as an unique thread 63 | * @param functions List of functions informed by test 64 | */ 65 | static inline void RunAsyncTest(std::vector functions) { 66 | TestSyncer syncer; 67 | std::vector threads{}; 68 | 69 | for (auto& func : functions) { 70 | threads.push_back(std::thread(func, std::ref(syncer))); 71 | } 72 | 73 | for (auto& thread : threads) { 74 | thread.join(); 75 | } 76 | } 77 | 78 | } // namespace testing 79 | #endif // INCLUDE_TEST_GENERAL_SYNC_TESTING_H_ 80 | -------------------------------------------------------------------------------- /test/general/utils.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Header with utilities to be used within unit tests 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_GENERAL_UTILS_H_ 7 | #define INCLUDE_TEST_GENERAL_UTILS_H_ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "ftxui/component/component.hpp" 14 | #include "ftxui/component/component_base.hpp" 15 | #include "ftxui/component/event.hpp" 16 | #include "util/formatter.h" 17 | 18 | namespace utils { 19 | 20 | //! Filter any ANSI escape code from string 21 | inline std::string FilterAnsiCommands(const std::string& screen) { 22 | std::stringstream result; 23 | const std::regex ansi_command("(\e\\[(\\d+;)*(\\d+)?[ABCDHJKfmsu])|(\\r)"); 24 | 25 | std::regex_replace(std::ostream_iterator(result), screen.begin(), screen.end(), 26 | ansi_command, ""); 27 | 28 | // For aesthetics, add a newline in the beginning 29 | return result.str().insert(0, 1, '\n'); 30 | } 31 | 32 | /* ********************************************************************************************** */ 33 | 34 | //! Split string into characters and send each as an event to Component 35 | template 36 | inline void QueueCharacterEvents(T& component, const std::string& typed) { 37 | std::for_each(typed.begin(), typed.end(), 38 | [&component](char const& c) { component.OnEvent(ftxui::Event::Character(c)); }); 39 | } 40 | 41 | /* ********************************************************************************************** */ 42 | 43 | //! Split string by line, trim its content and join it again 44 | // NOTE: This was specially implemented for dialog rendering, in which may contain multiple empty 45 | // spaces because of size delimitation 46 | inline std::string FilterEmptySpaces(const std::string& raw) { 47 | std::istringstream input{raw}; 48 | std::ostringstream output; 49 | 50 | // For aesthetics, add a newline in the beginning 51 | output << "\n"; 52 | 53 | for (std::string line; std::getline(input, line);) { 54 | if (std::string trimmed = util::trim(line); !trimmed.empty()) { 55 | output << trimmed << "\n"; 56 | } 57 | } 58 | 59 | return std::move(output).str(); 60 | } 61 | 62 | } // namespace utils 63 | #endif // INCLUDE_TEST_GENERAL_UTILS_H_ 64 | -------------------------------------------------------------------------------- /test/mock/analyzer_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for Analyzer API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_ANALYZER_MOCK_H_ 7 | #define INCLUDE_TEST_ANALYZER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "audio/base/analyzer.h" 12 | 13 | namespace { 14 | 15 | class AnalyzerMock final : public driver::Analyzer { 16 | public: 17 | MOCK_METHOD(error::Code, Init, (int), (override)); 18 | MOCK_METHOD(error::Code, Execute, (double *, int, double *), (override)); 19 | MOCK_METHOD(int, GetBufferSize, (), (override)); 20 | MOCK_METHOD(int, GetOutputSize, (), (override)); 21 | }; 22 | 23 | } // namespace 24 | #endif // INCLUDE_TEST_ANALYZER_MOCK_H_ -------------------------------------------------------------------------------- /test/mock/audio_control_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for Audio Control API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_MOCK_AUDIO_CONTROL_MOCK_H_ 7 | #define INCLUDE_TEST_MOCK_AUDIO_CONTROL_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "audio/player.h" 12 | 13 | namespace { 14 | 15 | class AudioControlMock final : public audio::AudioControl { 16 | public: 17 | MOCK_METHOD(void, Play, (const std::filesystem::path&), (override)); 18 | MOCK_METHOD(void, Play, (const model::Playlist&), (override)); 19 | MOCK_METHOD(void, PauseOrResume, (), (override)); 20 | MOCK_METHOD(void, Stop, (), (override)); 21 | MOCK_METHOD(void, SetAudioVolume, (const model::Volume&), (override)); 22 | MOCK_METHOD(model::Volume, GetAudioVolume, (), (const, override)); 23 | MOCK_METHOD(void, SeekForwardPosition, (int value), (override)); 24 | MOCK_METHOD(void, SeekBackwardPosition, (int value), (override)); 25 | MOCK_METHOD(void, ApplyAudioFilters, (const model::EqualizerPreset&), (override)); 26 | MOCK_METHOD(void, Exit, (), (override)); 27 | }; 28 | 29 | } // namespace 30 | #endif // INCLUDE_TEST_MOCK_AUDIO_CONTROL_MOCK_H_ 31 | -------------------------------------------------------------------------------- /test/mock/decoder_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for Decoder API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_MOCK_DECODER_MOCK_H_ 7 | #define INCLUDE_TEST_MOCK_DECODER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "audio/base/decoder.h" 12 | #include "model/song.h" 13 | #include "model/volume.h" 14 | 15 | namespace { 16 | 17 | class DecoderMock final : public driver::Decoder { 18 | public: 19 | MOCK_METHOD(error::Code, OpenFile, (model::Song &), (override)); 20 | MOCK_METHOD(error::Code, Decode, (int, AudioCallback), (override)); 21 | MOCK_METHOD(void, ClearCache, (), (override)); 22 | MOCK_METHOD(error::Code, SetVolume, (model::Volume), (override)); 23 | MOCK_METHOD(model::Volume, GetVolume, (), (const, override)); 24 | MOCK_METHOD(error::Code, UpdateFilters, (const model::EqualizerPreset &), (override)); 25 | }; 26 | 27 | } // namespace 28 | #endif // INCLUDE_TEST_MOCK_DECODER_MOCK_H_ 29 | -------------------------------------------------------------------------------- /test/mock/event_dispatcher_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for UI Event Dispatcher API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_MOCK_EVENT_DISPATCHER_MOCK_H_ 7 | #define INCLUDE_TEST_MOCK_EVENT_DISPATCHER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "view/base/event_dispatcher.h" 12 | 13 | namespace { 14 | 15 | class EventDispatcherMock final : public interface::EventDispatcher { 16 | public: 17 | MOCK_METHOD(void, SendEvent, (const interface::CustomEvent&), (override)); 18 | MOCK_METHOD(void, ProcessEvent, (const interface::CustomEvent&), (override)); 19 | MOCK_METHOD(void, SetApplicationError, (error::Code), (override)); 20 | }; 21 | 22 | } // namespace 23 | #endif // INCLUDE_TEST_MOCK_EVENT_DISPATCHER_MOCK_H_ 24 | -------------------------------------------------------------------------------- /test/mock/file_handler_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for File Handler API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_MOCK_FILE_HANDLER_MOCK_H_ 7 | #define INCLUDE_TEST_MOCK_FILE_HANDLER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "model/playlist.h" 12 | #include "util/file_handler.h" 13 | 14 | namespace { 15 | 16 | class FileHandlerMock final : public util::FileHandler { 17 | public: 18 | MOCK_METHOD(bool, ParsePlaylists, (model::Playlists & playlists), (override)); 19 | MOCK_METHOD(bool, SavePlaylists, (const model::Playlists& playlists), (override)); 20 | }; 21 | 22 | } // namespace 23 | #endif // INCLUDE_TEST_MOCK_FILE_HANDLER_MOCK_H_ 24 | -------------------------------------------------------------------------------- /test/mock/html_parser_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for HTML parsing API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_HTML_PARSER_MOCK_H_ 7 | #define INCLUDE_TEST_HTML_PARSER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "audio/lyric/base/html_parser.h" 12 | 13 | namespace { 14 | 15 | class HtmlParserMock final : public driver::HtmlParser { 16 | public: 17 | MOCK_METHOD(lyric::SongLyric, Parse, (const std::string&, const std::string&), (override)); 18 | }; 19 | 20 | } // namespace 21 | #endif // INCLUDE_TEST_HTML_PARSER_MOCK_H_ -------------------------------------------------------------------------------- /test/mock/interface_notifier_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for Playback API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_INTERFACE_NOTIFIER_MOCK_H_ 7 | #define INCLUDE_TEST_INTERFACE_NOTIFIER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "view/base/notifier.h" 12 | 13 | namespace { 14 | 15 | class InterfaceNotifierMock final : public interface::Notifier { 16 | public: 17 | MOCK_METHOD(void, ClearSongInformation, (bool), (override)); 18 | MOCK_METHOD(void, NotifySongInformation, (const model::Song &), (override)); 19 | MOCK_METHOD(void, NotifySongState, (const model::Song::CurrentInformation &), (override)); 20 | MOCK_METHOD(void, SendAudioRaw, (int *, int), (override)); 21 | MOCK_METHOD(void, NotifyError, (error::Code), (override)); 22 | }; 23 | 24 | } // namespace 25 | #endif // INCLUDE_TEST_INTERFACE_NOTIFIER_MOCK_H_ 26 | -------------------------------------------------------------------------------- /test/mock/lyric_finder_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for lyric finder API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_LYRIC_FINDER_MOCK_H_ 7 | #define INCLUDE_TEST_LYRIC_FINDER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "audio/lyric/lyric_finder.h" 12 | 13 | namespace { 14 | 15 | class LyricFinderMock final : public lyric::LyricFinder { 16 | public: 17 | MOCK_METHOD(lyric::SongLyric, Search, (const std::string&, const std::string&), (override)); 18 | }; 19 | 20 | } // namespace 21 | #endif // INCLUDE_TEST_LYRIC_FINDER_MOCK_H_ 22 | -------------------------------------------------------------------------------- /test/mock/playback_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for Playback API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_PLAYBACK_MOCK_H_ 7 | #define INCLUDE_TEST_PLAYBACK_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "audio/base/playback.h" 12 | 13 | namespace { 14 | 15 | class PlaybackMock final : public driver::Playback { 16 | public: 17 | MOCK_METHOD(error::Code, CreatePlaybackStream, (), (override)); 18 | MOCK_METHOD(error::Code, ConfigureParameters, (), (override)); 19 | MOCK_METHOD(error::Code, Prepare, (), (override)); 20 | MOCK_METHOD(error::Code, Pause, (), (override)); 21 | MOCK_METHOD(error::Code, Stop, (), (override)); 22 | MOCK_METHOD(error::Code, AudioCallback, (void*, int), (override)); 23 | MOCK_METHOD(error::Code, SetVolume, (model::Volume), (override)); 24 | MOCK_METHOD(model::Volume, GetVolume, (), (override)); 25 | MOCK_METHOD(uint32_t, GetPeriodSize, (), (const override)); 26 | }; 27 | 28 | } // namespace 29 | #endif // INCLUDE_TEST_PLAYBACK_MOCK_H_ 30 | -------------------------------------------------------------------------------- /test/mock/url_fetcher_mock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file 3 | * \brief Mock class for URL fetching API 4 | */ 5 | 6 | #ifndef INCLUDE_TEST_URL_FETCHER_MOCK_H_ 7 | #define INCLUDE_TEST_URL_FETCHER_MOCK_H_ 8 | 9 | #include 10 | 11 | #include "audio/lyric/base/url_fetcher.h" 12 | 13 | namespace { 14 | 15 | class UrlFetcherMock final : public driver::UrlFetcher { 16 | public: 17 | MOCK_METHOD(error::Code, Fetch, (const std::string&, std::string&), (override)); 18 | }; 19 | 20 | } // namespace 21 | #endif // INCLUDE_TEST_URL_FETCHER_MOCK_H_ --------------------------------------------------------------------------------