inLimbo
TUI Music Player that keeps you in Limbo.
 
Loading...
Searching...
No Matches
ui_handler.hpp
Go to the documentation of this file.
1#ifndef FTXUI_HANDLER_HPP
2#define FTXUI_HANDLER_HPP
3
8#include "keymaps.hpp"
9#include "misc.hpp"
10#include <iomanip>
11#include <sstream>
12#include <unordered_set>
13
14using namespace ftxui;
15
17#define STATUS_PLAYING "<>"
18#define STATUS_PAUSED "!!"
19#define LYRICS_AVAIL "L*"
20#define ADDN_PROPS_AVAIL "&*"
21#define STATUS_BAR_DELIM " | "
22
24#define MAX_LENGTH_SONG_NAME 50
25#define MAX_LENGTH_ARTIST_NAME 30
26
28#define SHOW_MAIN_UI 0
29#define SHOW_HELP_SCREEN 1
30#define SHOW_LYRICS_SCREEN 2
31#define SHOW_QUEUE_SCREEN 3
32#define SHOW_SONG_INFO_SCREEN 4
33
34#define MIN_DEBOUNCE_TIME_IN_MS 500
35
42
44{
45public:
47 const std::map<std::string,
48 std::map<std::string, std::map<unsigned int, std::map<unsigned int, Song>>>>&
49 initial_library)
50 : library(initial_library), INL_Thread_Manager(std::make_unique<ThreadManager>()),
51 INL_Thread_State(INL_Thread_Manager->getThreadState())
52 {
53 InitializeData();
54 CreateComponents();
55 }
56
57 void Run()
58 {
59 mprisService = std::make_unique<MPRISService>("inLimbo");
60
61 std::thread mpris_dbus_thread(
62 [&]
63 {
64 GMainLoop* loop = g_main_loop_new(nullptr, FALSE);
65 g_main_loop_run(loop);
66 });
67
68 mpris_dbus_thread
69 .detach(); // We will not monitor this thread, it can just run without our concern
70
71 auto screen = ScreenInteractive::Fullscreen();
72
73 try
74 {
75 std::thread refresh_thread(
76 [&]
77 {
78 try
79 {
80 while (!should_quit)
81 {
82 using namespace std::chrono_literals;
83
84 // Safely update volume
85 UpdateVolume();
86
87 std::this_thread::sleep_for(0.1s);
88
89 if (INL_Thread_State.is_playing)
90 {
91 current_position += 0.1;
92 if (current_position >= GetCurrentSongDuration())
93 {
94 PlayNextSong();
95 UpdatePlayingState();
96 }
97 }
98
99 // Post a custom event to refresh the UI
100 screen.PostEvent(Event::Custom);
101 }
102 }
103 catch (const std::exception& e)
104 {
105 show_dialog = true;
106 dialog_message = "Error in refresh thread: " + std::string(e.what());
107 }
108 catch (...)
109 {
110 show_dialog = true;
111 dialog_message = "Unknown error occurred in refresh thread.";
112 }
113 });
114
115 refresh_thread.detach(); // Detach thread to run independently
116 }
117 catch (const std::exception& e)
118 {
119 dialog_message = "Error starting refresh thread: " + std::string(e.what());
120 show_dialog = true;
121 }
122 catch (...)
123 {
124 dialog_message = "Unknown error occurred while starting refresh thread.";
125 show_dialog = true;
126 }
127
128 screen_ = &screen;
129
130 try
131 {
132 screen.Loop(INL_Component_State.MainRenderer);
133 }
134 catch (const std::exception& e)
135 {
136 dialog_message = "Error in UI loop: " + std::string(e.what());
137 show_dialog = true;
138 }
139 catch (...)
140 {
141 dialog_message = "Unknown error occurred in UI loop.";
142 show_dialog = true;
143 }
144 }
145
146private:
147 struct PlayingState
148 {
149 std::string artist;
150 std::string title;
151 std::string genre;
152 std::string album;
153 bool has_comment = false;
154 bool has_lyrics = false;
155 int duration;
156 unsigned int year = 0;
157 unsigned int track = 0;
158 unsigned int discNumber = 0;
159 std::string lyrics;
160 std::string comment;
161 std::unordered_map<std::string, std::string> additionalProperties;
162 std::string filePath;
163 };
164
165 PlayingState current_playing_state;
166
167 std::unique_ptr<MPRISService> mprisService;
168
169 Keybinds global_keybinds = parseKeybinds();
170 InLimboColors global_colors = parseColors();
171
172 std::unique_ptr<MiniAudioPlayer> audio_player;
173 std::unique_ptr<ThreadManager> INL_Thread_Manager; // Smart pointer to ThreadManager
174 ThreadManager::ThreadState& INL_Thread_State;
175
176 std::vector<Song> song_queue;
177 int current_song_queue_index = 0;
178
179 // Main data structure
180 std::map<std::string, std::map<std::string, std::map<unsigned int, std::map<unsigned int, Song>>>>
181 library;
182
183 // Navigation state
184 std::string current_artist;
185 unsigned int current_disc = 1;
186 unsigned int current_track = 1;
187 bool show_dialog = false;
188 std::string dialog_message;
189 double seekBuffer;
190 bool first_g_pressed = false; // Track the state of the first 'g' press
191
192 // Current view lists
193 std::vector<std::string> current_artist_names;
194 std::vector<std::string> song_queue_names;
195 std::vector<std::string> lyricLines;
196 std::vector<Element> current_song_elements;
197 std::vector<unsigned int> current_inodes;
198 std::unordered_set<int> album_name_indices;
199 int albums_indices_traversed = 1; // first element of current_song_elements is always an album
200
201 // Player state
202 int selected_artist = 0;
203 int selected_song_queue = 0;
204 int selected_inode = 1; // First element is always going to album name so we ignore it
205 int current_lyric_line = 0;
206 std::vector<Element> lyricElements;
207 int volume = 50;
208 bool muted = false;
209 int lastVolume = volume;
210 double current_position = 0;
211 int active_screen =
212 0; // 0 -> Main UI ; 1 -> Show help ; 2 -> Show lyrics; 3 -> Songs queue screen
213 bool should_quit = false;
214 bool focus_on_artists = true;
215 ScreenInteractive* screen_ = nullptr;
216
217 // UI Components
218 ComponentState INL_Component_State;
219
220 void InitializeData()
221 {
222 // Initialize current_artist_names from library
223 for (const auto& artist_pair : library)
224 {
225 current_artist_names.push_back(artist_pair.first);
226 }
227
228 // Sort artist names alphabetically
229 std::sort(current_artist_names.begin(), current_artist_names.end());
230
231 // Initialize first artist's songs if available
232 if (!current_artist_names.empty())
233 {
234 UpdateSongsForArtist(current_artist_names[0]);
235 }
236 }
237
238 /* Miniaudio class integrations */
239
240 void PlayCurrentSong()
241 {
242 INL_Thread_Manager->lockPlayMutex(INL_Thread_State);
243 if (INL_Thread_State.is_processing)
244 {
245 INL_Thread_Manager->unlockPlayMutex(INL_Thread_State);
246 return; // Another invocation is already running
247 }
248 INL_Thread_State.is_processing = true;
249
250 INL_Thread_State.play_future =
251 std::async(std::launch::async,
252 [this]()
253 {
254 try
255 {
256 if (!audio_player)
257 {
258 audio_player = std::make_unique<MiniAudioPlayer>();
259 }
260
261 Song* current_song = GetCurrentSongFromQueue();
262 if (!current_song)
263 {
264 throw std::runtime_error("Error: No current song found.");
265 }
266
267 const std::string& file_path = current_song->metadata.filePath;
268 if (file_path.empty())
269 {
270 throw std::runtime_error("Error: Invalid file path.");
271 }
272
273 // Stop previous song if any
274 if (INL_Thread_State.is_playing)
275 {
276 audio_player->stop();
277 INL_Thread_State.is_playing = false;
278 }
279
280 // Load the audio file
281 int loadAudioFileStatus = audio_player->loadFile(file_path);
282 if (loadAudioFileStatus == -1)
283 {
284 throw std::runtime_error("Error: Failed to load the audio file.");
285 }
286
287 INL_Thread_State.is_playing = true;
288
289 audio_player->play(); // Play the song (it will play in a separate thread)
290 current_playing_state.duration = audio_player->getDuration();
291 }
292 catch (const std::exception& e)
293 {
294 show_dialog = true;
295 dialog_message = e.what();
296 INL_Thread_State.is_playing = false;
297 }
298
299 INL_Thread_Manager->lockPlayMutex(INL_Thread_State);
300 INL_Thread_State.is_processing = false;
301 INL_Thread_Manager->unlockPlayMutex(INL_Thread_State);
302 });
303
304 INL_Thread_Manager->unlockPlayMutex(INL_Thread_State);
305 }
306
307 void TogglePlayback()
308 {
309 if (!audio_player)
310 return;
311
312 if (INL_Thread_State.is_playing)
313 {
314 audio_player->pause();
315 INL_Thread_State.is_playing = false;
316 }
317 else
318 {
319 if (audio_player)
320 {
321 audio_player->resume();
322 }
323 else
324 {
325 PlayCurrentSong();
326 UpdatePlayingState();
327 }
328 INL_Thread_State.is_playing = true;
329 }
330 }
331
332 void UpdateVolume()
333 {
334 if (audio_player)
335 {
336 audio_player->setVolume(volume / 100.0f);
337 }
338 }
339
340 void PlayNextSong()
341 {
342 if (current_song_queue_index + 1 < song_queue.size())
343 {
344 current_song_queue_index++;
345 if (Song* current_song = GetCurrentSongFromQueue())
346 {
347 current_position = 0;
348 PlayCurrentSong();
349 UpdatePlayingState();
350 }
351 }
352 else
353 {
354 show_dialog = true;
355 dialog_message = "Error: No more songs in the queue.";
356 }
357 }
358
359 void ReplaySong()
360 {
361 current_position = 0;
362 PlayCurrentSong();
363 UpdatePlayingState();
364 return;
365 }
366
367 void PlayPreviousSong()
368 {
369 // If rewinding within the current song
370 if (current_position > 3.0)
371 {
372 ReplaySong();
373 }
374
375 // Move to the previous song if possible
376 if (current_song_queue_index > 0)
377 {
378 current_song_queue_index--;
379 current_position = 0;
380 PlayCurrentSong();
381 UpdatePlayingState();
382 }
383 else
384 {
385 show_dialog = true;
386 dialog_message = "Error: No previous song available.";
387 }
388 }
389
390 /* ------------------------------- */
391
392 void UpdateSongsForArtist(const std::string& artist)
393 {
394 current_inodes.clear();
395 current_song_elements.clear();
396 album_name_indices.clear();
397
398 if (library.count(artist) > 0)
399 {
400 // Group songs by album and year
401 std::map<std::string, std::map<unsigned int, std::map<unsigned int, Song>>> albums;
402 for (const auto& album_pair : library.at(artist))
403 {
404 const std::string& album_name = album_pair.first;
405 for (const auto& disc_pair : album_pair.second)
406 {
407 for (const auto& track_pair : disc_pair.second)
408 {
409 albums[album_name][disc_pair.first][track_pair.first] = track_pair.second;
410 }
411 }
412 }
413
414 // Format the album and song display
415 for (const auto& [album_name, discs] : albums)
416 {
417 // Get year from the first song in the album
418 const Song& first_song = discs.begin()->second.begin()->second;
419 current_song_elements.push_back(
420 renderAlbumName(album_name, first_song.metadata.year, global_colors.album_name_bg));
421 album_name_indices.insert(current_song_elements.size() - 1);
422
423 for (const auto& [disc_number, tracks] : discs)
424 {
425 for (const auto& [track_number, song] : tracks)
426 {
427 std::string disc_track_info = " " +
428 std::to_string(disc_number) + "-" + std::to_string(track_number) + " ";
429 current_inodes.push_back(song.inode);
430 current_song_elements.push_back(
431 renderSongName(disc_track_info, song.metadata.title, song.metadata.duration));
432 }
433 }
434 }
435 }
436
437 current_artist = artist;
438 }
439
440 void ClearQueue()
441 {
442 song_queue.clear();
443 current_song_queue_index = 0;
444 }
445
446 const Song& GetCurrentSong(const std::string& artist)
447 {
448 const auto& artist_data = library.at(artist);
449
450 // Iterate through all albums, discs, and tracks
451 for (const auto& album_pair : artist_data)
452 {
453 for (const auto& disc_pair : album_pair.second)
454 {
455 for (const auto& track_pair : disc_pair.second)
456 {
457 const Song& song = track_pair.second;
458
459 if (current_inodes[selected_inode - albums_indices_traversed] == song.inode)
460 {
461 return song; // Return a reference to the matched song.
462 }
463 }
464 }
465 }
466
467 throw std::runtime_error("Song not found.");
468 }
469
470 void PlayThisSongNext(const std::string& artist)
471 {
472 const Song& get_curr_song = GetCurrentSong(artist);
473
474 try
475 {
476 song_queue.insert(song_queue.begin() + current_song_queue_index + 1, get_curr_song);
477 }
478 catch (std::exception e)
479 {
480 dialog_message = "Could not play this song next!";
481 show_dialog = true;
482 }
483
484 current_song_queue_index++;
485 return;
486 }
487
488 void EnqueueAllSongsByArtist(const std::string& artist, bool clearQueue)
489 {
490 // Clear the existing song list
491 if (clearQueue)
492 ClearQueue();
493
494 bool start_enqueue = false;
495
496 // Check if the artist exists in the library
497 if (library.find(artist) == library.end())
498 {
499 std::cerr << "Artist not found in the library: " << artist << std::endl;
500 return;
501 }
502
503 const auto& artist_data = library.at(artist);
504
505 // Iterate through all albums, discs, and tracks
506 for (const auto& album_pair : artist_data)
507 {
508 for (const auto& disc_pair : album_pair.second)
509 {
510 for (const auto& track_pair : disc_pair.second)
511 {
512 const Song& song = track_pair.second;
513
514 if (current_inodes[selected_inode - albums_indices_traversed] == song.inode)
515 {
516 start_enqueue = true;
517 }
518 // Add the song to the list
519 if (start_enqueue)
520 song_queue.push_back(song);
521 }
522 }
523 }
524 }
525
526 void AddSongToQueue()
527 {
528 // Validate indices to prevent out-of-range access
529 if (selected_artist >= current_artist_names.size() ||
530 selected_inode - albums_indices_traversed >= current_inodes.size())
531 {
532 throw std::runtime_error("Invalid artist or song selection.");
533 }
534
535 // Get a const reference to the current song
536 const Song& current_preview_song = GetCurrentSong(current_artist_names[selected_artist]);
537
538 // Add a copy of the song to the queue
539 song_queue.push_back(current_preview_song);
540
541 NavigateSongMenu(true);
542 }
543
544 void RemoveSongFromQueue()
545 {
546 if (selected_song_queue == 0)
547 {
548 dialog_message = "Unable to remove song... This is playing right now!";
549 show_dialog = true;
550 return;
551 }
552 if (selected_song_queue < song_queue.size())
553 {
554 song_queue.erase(song_queue.begin() + selected_song_queue);
555 }
556 }
557
558 Song* GetCurrentSongFromQueue()
559 {
560 if (!song_queue.empty() && current_song_queue_index < song_queue.size())
561 {
562 return &song_queue[current_song_queue_index];
563 }
564
565 dialog_message = "Something went worng";
566 show_dialog = true;
567 return nullptr;
568 }
569
570 int GetCurrentSongDuration()
571 {
572 if (!current_inodes.empty() && audio_player)
573 {
574 return current_playing_state.duration;
575 }
576 return 0;
577 }
578
579 void UpdatePlayingState()
580 {
581 if (Song* current_song = GetCurrentSongFromQueue())
582 {
583 const auto& metadata = current_song->metadata;
584
585 current_playing_state.artist = metadata.artist;
586 current_playing_state.title = metadata.title;
587 current_playing_state.album = metadata.album;
588 current_playing_state.genre = metadata.genre;
589 current_playing_state.comment = metadata.comment;
590 current_playing_state.year = metadata.year;
591 current_playing_state.track = metadata.track;
592 current_playing_state.discNumber = metadata.discNumber;
593 current_playing_state.lyrics = metadata.lyrics;
594 current_playing_state.has_comment = (metadata.comment != "No Comment");
595 current_playing_state.has_lyrics = (metadata.lyrics != "No Lyrics");
596 current_playing_state.filePath = metadata.filePath;
597 // duration gets updated in the PlayCurrentSong() thread itself
598
599 // If there's additional properties, you can either copy them or process as needed
600 for (const auto& [key, value] : metadata.additionalProperties)
601 {
602 current_playing_state.additionalProperties[key] = value;
603 }
604 }
605
606 const std::string& mprisSongTitle = current_playing_state.title;
607 const std::string& mprisSongArtist = current_playing_state.artist;
608 const std::string& mprisSongAlbum = current_playing_state.album;
609 const std::string& mprisSongComment = current_playing_state.comment;
610 const std::string& mprisSongGenre = current_playing_state.genre;
611
612 mprisService->updateMetadata(mprisSongTitle, mprisSongArtist, mprisSongAlbum,
613 static_cast<int64_t>(GetCurrentSongFromQueue()->metadata.duration),
614 mprisSongComment, mprisSongGenre, current_playing_state.track,
615 current_playing_state.discNumber);
616
617 current_lyric_line = 0;
618 }
619
620 void CreateComponents()
621 {
622 MenuOption artist_menu_options;
623 artist_menu_options.on_change = [&]()
624 {
625 if (focus_on_artists && selected_artist < current_artist_names.size())
626 {
627 UpdateSongsForArtist(current_artist_names[selected_artist]);
628 }
629 };
630 artist_menu_options.focused_entry = &selected_artist;
631
632 MenuOption song_menu_options;
633 song_menu_options.on_change = [&]() {};
634 song_menu_options.focused_entry = &selected_inode;
635
636 INL_Component_State.artists_list =
637 Menu(&current_artist_names, &selected_artist, artist_menu_options);
638 INL_Component_State.songs_list =
639 Scroller(Renderer(
640 [&]() mutable
641 {
642 return RenderSongMenu(current_song_elements); // This should return an Element
643 }),
644 &selected_inode, global_colors.menu_cursor_bg);
645
646 auto main_container =
647 Container::Horizontal({INL_Component_State.artists_list, INL_Component_State.songs_list});
648
649 /* Adding DEBOUNCE TIME (Invoking PlayCurrentSong() too fast causes resources to not be freed)
650 */
651
652 auto last_event_time = std::chrono::steady_clock::now(); // Tracks the last event time
653 int debounce_time = std::stoi(std::string(parseTOMLField(PARENT_DBG, "debounce_time_in_ms")));
654 if (debounce_time < MIN_DEBOUNCE_TIME_IN_MS)
655 debounce_time = MIN_DEBOUNCE_TIME_IN_MS; // min debounce time is 0.5s
656 const int final_debounce_time = debounce_time;
657 auto debounce_duration = std::chrono::milliseconds(final_debounce_time);
658
659 main_container |= CatchEvent(
660 [&](Event event)
661 {
662 auto is_keybind_match = [&](char key) -> bool
663 {
664 return (event.is_character() && event.character() == std::string(1, key)) ||
665 (event == Event::Special(std::string(1, static_cast<char>(key))));
666 };
667
668 if (active_screen == SHOW_HELP_SCREEN)
669 {
670 if (event.is_character() && (event.character()[0] == global_keybinds.show_help ||
671 std::toupper(global_keybinds.quit_app) ||
672 event.character()[0] == global_keybinds.quit_app))
673 {
674 active_screen = SHOW_MAIN_UI;
675 return true;
676 }
677 return false; // Prevent other keys from working
678 }
679
680 else if (active_screen == SHOW_LYRICS_SCREEN)
681 {
682 if (is_keybind_match(global_keybinds.goto_main_screen))
683 {
684 active_screen = SHOW_MAIN_UI;
685 return true;
686 }
687
688 else if (is_keybind_match(global_keybinds.scroll_down))
689 {
690 NavigateList(true);
691 return true;
692 }
693 else if (is_keybind_match(global_keybinds.scroll_up))
694 {
695 NavigateList(false);
696 return true;
697 }
698 else if (is_keybind_match('g'))
699 {
700
701 if (!first_g_pressed)
702 {
703 // First 'g' press
704 first_g_pressed = true;
705 }
706 else
707 {
708 // Second 'g' press
709 NavigateListToTop(true);
710 first_g_pressed = false; // Reset the state
711 return true;
712 }
713 }
714 else if (is_keybind_match('G'))
715 {
716 NavigateListToTop(false);
717 return true;
718 }
719 }
720
721 else if (active_screen == SHOW_QUEUE_SCREEN)
722 {
723 if (is_keybind_match(global_keybinds.remove_song_from_queue))
724 {
725 RemoveSongFromQueue();
726 return true;
727 }
728 else if (is_keybind_match(global_keybinds.scroll_down))
729 {
730 NavigateList(true);
731 return true;
732 }
733 else if (is_keybind_match(global_keybinds.scroll_up))
734 {
735 NavigateList(false);
736 return true;
737 }
738 else if (is_keybind_match('g'))
739 {
740
741 if (!first_g_pressed)
742 {
743 // First 'g' press
744 first_g_pressed = true;
745 }
746 else
747 {
748 // Second 'g' press
749 NavigateListToTop(true);
750 first_g_pressed = false; // Reset the state
751 return true;
752 }
753 }
754 else if (is_keybind_match('G'))
755 {
756 NavigateListToTop(false);
757 return true;
758 }
759 else if (is_keybind_match(global_keybinds.goto_main_screen))
760 {
761 active_screen = SHOW_MAIN_UI;
762 return true;
763 }
764 else if (is_keybind_match('x'))
765 { // Add key to dismiss dialog
766 show_dialog = false;
767 return true;
768 }
769 }
770
771 else if (event.is_mouse())
772 return false;
773
774 else if (active_screen == SHOW_SONG_INFO_SCREEN)
775 {
776 if (is_keybind_match(global_keybinds.goto_main_screen))
777 {
778 active_screen = SHOW_MAIN_UI;
779 return true;
780 }
781 }
782
783 else if (active_screen == SHOW_MAIN_UI)
784 {
785
786 if (is_keybind_match(global_keybinds.play_song) && !focus_on_artists)
787 {
788 if (!current_artist.empty())
789 {
790 EnqueueAllSongsByArtist(current_artist, true);
791
792 if (Song* current_song = GetCurrentSongFromQueue())
793 {
794 // Enqueue all songs by the current artist
795 current_position = 0;
796 PlayCurrentSong();
797 UpdatePlayingState();
798 }
799 }
800 else
801 {
802 show_dialog = true;
803 dialog_message = "No artist selected to play songs from.";
804 }
805 return true;
806 }
807 // Check against keybinds using if-else instead of switch
808 else if (is_keybind_match(global_keybinds.quit_app) ||
809 is_keybind_match(std::toupper(global_keybinds.quit_app)))
810 {
811 Quit();
812 return true;
813 }
814 else if (is_keybind_match(global_keybinds.toggle_play))
815 {
816 TogglePlayback();
817 return true;
818 }
819 else if (is_keybind_match(global_keybinds.play_song_next))
820 {
821 auto now = std::chrono::steady_clock::now();
822 if (now - last_event_time < debounce_duration)
823 {
824 audio_player->stop();
825 return false;
826 }
827 PlayNextSong();
828 last_event_time = now; // Update the last event time
829 return true;
830 }
831 else if (is_keybind_match(global_keybinds.play_song_prev))
832 {
833 auto now = std::chrono::steady_clock::now();
834 if (now - last_event_time < debounce_duration)
835 return false;
836 last_event_time = now;
837 PlayPreviousSong();
838 return true;
839 }
840 else if (is_keybind_match(global_keybinds.seek_ahead_5))
841 {
842 seekBuffer = audio_player->seekTime(5);
843 current_position += seekBuffer;
844 }
845 else if (is_keybind_match(global_keybinds.seek_behind_5))
846 {
847 if (current_position > 5)
848 {
849 seekBuffer = audio_player->seekTime(-5);
850 current_position += seekBuffer;
851 }
852 else
853 {
854 ReplaySong();
855 }
856 UpdatePlayingState();
857 }
858 else if (is_keybind_match(global_keybinds.replay_song))
859 {
860 ReplaySong();
861 return true;
862 }
863 else if (is_keybind_match(global_keybinds.vol_up))
864 {
865 volume = std::min(100, volume + 5);
866 UpdateVolume();
867 return true;
868 }
869 else if (is_keybind_match(global_keybinds.vol_down))
870 {
871 volume = std::max(0, volume - 5);
872 UpdateVolume();
873 return true;
874 }
875 else if (is_keybind_match(global_keybinds.toggle_mute))
876 {
877 muted = !muted;
878 if (muted)
879 {
880 lastVolume = volume;
881 volume = 0;
882 }
883 else
884 {
885 volume = lastVolume;
886 }
887 UpdateVolume();
888 return true;
889 }
890 else if (is_keybind_match(global_keybinds.show_help))
891 {
892 active_screen = SHOW_HELP_SCREEN;
893 return true;
894 }
895 else if (is_keybind_match(global_keybinds.scroll_down))
896 {
897 NavigateList(true);
898 return true;
899 }
900 else if (is_keybind_match(global_keybinds.scroll_up))
901 {
902 NavigateList(false);
903 return true;
904 }
905 else if (is_keybind_match('x'))
906 { // Add key to dismiss dialog
907 show_dialog = false;
908 return true;
909 }
910 else if (is_keybind_match(global_keybinds.view_lyrics) &&
911 (current_playing_state.has_lyrics || current_playing_state.has_comment))
912 {
913 active_screen = SHOW_LYRICS_SCREEN;
914 return true;
915 }
916 else if (is_keybind_match(global_keybinds.view_song_queue) && !song_queue.empty())
917 {
918 active_screen = SHOW_QUEUE_SCREEN;
919 return true;
920 }
921 else if (is_keybind_match(global_keybinds.goto_main_screen))
922 {
923 active_screen = SHOW_MAIN_UI;
924 return true;
925 }
926 else if (is_keybind_match(global_keybinds.view_current_song_info))
927 {
928 active_screen = SHOW_SONG_INFO_SCREEN;
929 return true;
930 }
931 // Some default keybinds
932 else if (is_keybind_match('g'))
933 {
934
935 if (!first_g_pressed)
936 {
937 // First 'g' press
938 first_g_pressed = true;
939 }
940 else
941 {
942 // Second 'g' press
943 NavigateListToTop(true);
944 first_g_pressed = false; // Reset the state
945 return true;
946 }
947 }
948 else if (is_keybind_match('G'))
949 {
950 NavigateListToTop(false);
951 return true;
952 }
953 else if (is_keybind_match(global_keybinds.toggle_focus))
954 {
955 focus_on_artists = !focus_on_artists;
956 if (focus_on_artists)
957 {
958 INL_Component_State.artists_list->TakeFocus();
959 }
960 else
961 {
962 INL_Component_State.songs_list->TakeFocus();
963 }
964 return true;
965 }
966 else if (is_keybind_match(global_keybinds.add_song_to_queue))
967 {
968 AddSongToQueue();
969 return true;
970 }
971 else if (is_keybind_match(global_keybinds.add_artists_songs_to_queue) && focus_on_artists)
972 {
973 /*selected_inode = 0; // sanity check, the current song pane's index should start from
974 * top to enqueue all songs*/
975 /*albums_indices_traversed = 1;*/
976 EnqueueAllSongsByArtist(current_artist_names[selected_artist], false);
977 NavigateList(true);
978 return true;
979 }
980 else if (is_keybind_match(global_keybinds.play_this_song_next) && !focus_on_artists)
981 {
982 PlayThisSongNext(current_artist_names[selected_artist]);
983 return true;
984 }
985 }
986
987 /*if (event == Event::ArrowDown && active_screen == SHOW_QUEUE_SCREEN)*/
988 /*{*/
989 /* NavigateList(true);*/
990 /* return true;*/
991 /*}*/
992 /*if (event == Event::ArrowUp && active_screen == SHOW_QUEUE_SCREEN)*/
993 /*{*/
994 /* NavigateList(false);*/
995 /* return true;*/
996 /*}*/
997
998 return false;
999 });
1000
1001 INL_Component_State.MainRenderer =
1002 Renderer(main_container,
1003 [&]
1004 {
1005 int duration = GetCurrentSongDuration();
1006 float progress = duration > 0 ? (float)current_position / duration : 0;
1007
1008 Element interface;
1009 if (active_screen == SHOW_HELP_SCREEN)
1010 {
1011 interface = RenderHelpScreen();
1012 }
1013 if (active_screen == SHOW_MAIN_UI)
1014 {
1015 interface = RenderMainInterface(progress);
1016 }
1017
1018 if (active_screen == SHOW_LYRICS_SCREEN)
1019 {
1020 interface = RenderLyricsAndInfoView();
1021 }
1022 if (active_screen == SHOW_QUEUE_SCREEN)
1023 {
1024 interface = RenderQueueScreen();
1025 }
1026 if (active_screen == SHOW_SONG_INFO_SCREEN)
1027 {
1028 interface = RenderThumbnail(
1029 current_playing_state.filePath, getCachePath(), current_playing_state.title,
1030 current_playing_state.artist, current_playing_state.album,
1031 current_playing_state.genre, current_playing_state.year,
1032 current_playing_state.track, current_playing_state.discNumber, progress);
1033 }
1034 if (show_dialog)
1035 {
1036 // Create a semi-transparent overlay with the dialog box
1037 interface =
1038 dbox({
1039 interface, // Dim the background
1040 RenderDialog() | center // Center the dialog both horizontally and vertically
1041 }) |
1043 }
1044
1045 return vbox(interface);
1046 });
1047 }
1048
1049 Element RenderDialog()
1050 {
1051 return window(
1052 text(" File Information ") | bold | center | getTrueColor(TrueColors::Color::White) |
1054 vbox({
1055 text(dialog_message) | getTrueColor(TrueColors::Color::Coral),
1056 separator() | color(Color::GrayLight),
1057 text("Press 'x' to close") | dim | center | getTrueColor(TrueColors::Color::Pink),
1058 }) |
1059 center) |
1060 size(WIDTH, LESS_THAN, 60) | size(HEIGHT, LESS_THAN, 8) |
1062 }
1063
1064 void UpdateLyrics()
1065 {
1066 lyricLines.clear();
1067 lyricLines = formatLyrics(current_playing_state.lyrics);
1068 return;
1069 }
1070
1071 Element RenderLyricsAndInfoView()
1072 {
1073
1074 std::vector<Element> additionalPropertiesText;
1075 for (const auto& [key, value] : current_playing_state.additionalProperties)
1076 {
1077 if (key != "LYRICS")
1078 {
1079 additionalPropertiesText.push_back(hbox({text(key + ": "), text(value) | dim}));
1080 }
1081 }
1082
1083 INL_Component_State.lyrics_scroller = CreateMenu(&lyricLines, &current_lyric_line);
1084
1085 UpdateLyrics();
1086
1087 std::string end_text = "Use arrow keys to scroll, Press '" +
1088 std::string(1, static_cast<char>(global_keybinds.goto_main_screen)) +
1089 "' to go back home.";
1090
1091 auto lyrics_pane = vbox({
1092 INL_Component_State.lyrics_scroller->Render() | flex,
1093 });
1094
1095 auto info_pane = window(text(" Additional Info ") | bold | center | inverted,
1096 vbox(additionalPropertiesText) | frame | flex);
1097
1098 return hbox({vbox({
1099 lyrics_pane | frame | flex | border,
1100 separator(),
1101 text(end_text) | dim | center | border,
1102 }) |
1103 flex,
1104 vbox({info_pane}) | flex}) |
1105 flex;
1106 }
1107
1108 void UpdateSongQueueList()
1109 {
1110 song_queue_names.clear();
1111
1112 for (long unsigned int i = current_song_queue_index; i < song_queue.size(); i++)
1113 {
1114 std::string eleName = song_queue[i].metadata.title + " by " + song_queue[i].metadata.artist;
1115 if (i == current_song_queue_index)
1116 eleName += " *";
1117 song_queue_names.push_back(eleName);
1118 }
1119
1120 return;
1121 }
1122
1123 Element RenderQueueScreen()
1124 {
1125 INL_Component_State.songs_queue_comp = CreateMenu(&song_queue_names, &selected_song_queue);
1126 UpdateSongQueueList();
1127
1128 auto title = text(" Song Queue ") | bold | getTrueColor(TrueColors::Color::LightBlue) |
1129 underlined | center;
1130
1131 auto separator_line = separator() | dim | flex;
1132
1133 std::string end_text = "Use '" + charToStr(global_keybinds.remove_song_from_queue) +
1134 "' to remove selected song from queue, Press '" +
1135 charToStr(global_keybinds.goto_main_screen) + "' to go back home.";
1136
1137 auto queue_container = vbox({
1138 INL_Component_State.songs_queue_comp->Render() |
1139 color(global_colors.song_queue_menu_fg) | flex,
1140 }) |
1141 border | color(global_colors.song_queue_menu_bor_col);
1142
1143 return vbox({
1144 title,
1145 queue_container | frame | flex,
1146 separator(),
1147 text(end_text) | dim | center,
1148 }) |
1149 flex;
1150 }
1151
1152 void NavigateSongMenu(bool move_down)
1153 {
1154 int initial_inode = selected_inode; // Store the initial index to detect infinite loops
1155 do
1156 {
1157 if (move_down)
1158 {
1159 selected_inode = (selected_inode + 1) % current_song_elements.size();
1160 }
1161 else
1162 {
1163 selected_inode =
1164 (selected_inode - 1 + current_song_elements.size()) % current_song_elements.size();
1165 }
1166 if (album_name_indices.find(selected_inode) != album_name_indices.end())
1167 {
1168 if (move_down)
1169 albums_indices_traversed++;
1170 else
1171 albums_indices_traversed--;
1172 }
1173 if (selected_inode == 0 && move_down)
1174 {
1175 albums_indices_traversed = 1;
1176 }
1177 // Break the loop if we traverse all elements (prevent infinite loop)
1178 if (selected_inode == initial_inode)
1179 {
1180 break;
1181 }
1182 } while (album_name_indices.find(selected_inode) !=
1183 album_name_indices.end()); // Skip album name indices
1184 }
1185
1186 void NavigateList(bool move_down)
1187 {
1188 if (active_screen == SHOW_MAIN_UI)
1189 {
1190 if (focus_on_artists)
1191 {
1192 if (!current_artist_names.empty())
1193 {
1194 if (move_down)
1195 {
1196 selected_artist = (selected_artist + 1) % current_artist_names.size();
1197 }
1198 else
1199 {
1200 selected_artist =
1201 (selected_artist - 1 + current_artist_names.size()) % current_artist_names.size();
1202 }
1203 UpdateSongsForArtist(current_artist_names[selected_artist]);
1204 selected_inode = 1; // Reset to the first song
1205 albums_indices_traversed = 1;
1206 }
1207 }
1208 else
1209 {
1210 if (!current_inodes.empty())
1211 {
1212 NavigateSongMenu(move_down);
1213 }
1214 }
1215 }
1216 else if (active_screen == SHOW_QUEUE_SCREEN)
1217 {
1218 if (move_down)
1219 {
1220 selected_song_queue = (selected_song_queue + 1) % song_queue_names.size();
1221 }
1222 else
1223 {
1224 selected_song_queue =
1225 (selected_song_queue - 1 + song_queue_names.size()) % song_queue_names.size();
1226 }
1227 }
1228 else if (active_screen == SHOW_LYRICS_SCREEN)
1229 {
1230 if (move_down)
1231 {
1232 current_lyric_line = (current_lyric_line + 1) % lyricLines.size();
1233 }
1234 else
1235 {
1236 current_lyric_line = (current_lyric_line - 1 + lyricLines.size()) % lyricLines.size();
1237 }
1238 }
1239 }
1240
1241 void NavigateListToTop(bool move_up)
1242 {
1243 if (active_screen == SHOW_MAIN_UI)
1244 {
1245 if (focus_on_artists)
1246 {
1247 if (!current_artist_names.empty())
1248 {
1249 // Navigate to the top of the artist list
1250 if (move_up)
1251 {
1252 selected_artist = 0;
1253 }
1254 else
1255 {
1256 selected_artist = current_artist_names.size() - 1;
1257 }
1258 }
1259 }
1260 else
1261 {
1262 if (!current_inodes.empty())
1263 {
1264 if (move_up)
1265 {
1266 // Navigate to the first valid song, skipping headers
1267 selected_inode = 1;
1268 albums_indices_traversed = 1;
1269 }
1270 else
1271 {
1272 // Navigate to the last valid song
1273 selected_inode = current_song_elements.size() - 1;
1274 albums_indices_traversed = album_name_indices.size();
1275 }
1276
1277 if (selected_inode < 0 || selected_inode >= current_song_elements.size())
1278 {
1279 selected_inode = 1; // Fallback
1280 albums_indices_traversed = 1;
1281 }
1282 }
1283 }
1284 }
1285 else if (active_screen == SHOW_QUEUE_SCREEN)
1286 {
1287 if (move_up)
1288 {
1289 selected_song_queue = 0;
1290 }
1291 else
1292 {
1293 selected_song_queue = song_queue_names.size() - 1;
1294 }
1295 }
1296 else if (active_screen == SHOW_LYRICS_SCREEN)
1297 {
1298 if (move_up)
1299 {
1300 current_lyric_line = 0;
1301 }
1302 else
1303 {
1304 current_lyric_line = lyricLines.size() - 1;
1305 }
1306 }
1307 }
1308
1309 Element RenderHelpScreen()
1310 {
1311 auto title = text("inLimbo Controls") | bold | getTrueColor(TrueColors::Color::Teal);
1312
1313 // Helper function to create a single keybind row
1314 auto createRow =
1315 [&](const std::string& key, const std::string& description, TrueColors::Color color)
1316 {
1317 return hbox({
1318 text(key) | getTrueColor(color),
1319 text(" -- ") | getTrueColor(TrueColors::Color::Gray),
1320 text(description) | getTrueColor(TrueColors::Color::White),
1321 });
1322 };
1323
1324 auto controls_list =
1325 vbox({
1326 createRow(charToStr(global_keybinds.toggle_play), "Toggle playback",
1328 createRow(charToStr(global_keybinds.play_song_next), "Next song",
1330 createRow(charToStr(global_keybinds.play_song_prev), "Previous song",
1332 createRow("r", "Cycle repeat mode", TrueColors::Color::LightMagenta),
1333 createRow(charToStr(global_keybinds.vol_up), "Volume up", TrueColors::Color::LightGreen),
1334 createRow(charToStr(global_keybinds.vol_down), "Volume down",
1336 createRow(charToStr(global_keybinds.toggle_mute),
1337 "Toggle muting the current instance of miniaudio", TrueColors::Color::LightRed),
1338 createRow(charToStr(global_keybinds.toggle_focus), "Switch focus",
1340 createRow("gg", "Go to top of the current list", TrueColors::Color::LightBlue),
1341 createRow("G", "Go to bottom of the current list", TrueColors::Color::LightBlue),
1342 createRow(charToStr(global_keybinds.seek_ahead_5), "Seek ahead by 5s",
1344 createRow(charToStr(global_keybinds.seek_behind_5), "Seek behind by 5s",
1346 createRow(charToStr(global_keybinds.replay_song), "Replay current song",
1348 createRow(charToStr(global_keybinds.quit_app), "Quit", TrueColors::Color::LightRed),
1349 createRow(charToStr(global_keybinds.show_help), "Toggle this help",
1351 createRow(charToStr(global_keybinds.add_song_to_queue), "Add song to queue",
1353 createRow(charToStr(global_keybinds.add_song_to_queue), "Remove song from queue",
1355 createRow(charToStr(global_keybinds.goto_main_screen), "Go to song tree view",
1357 }) |
1359
1360 auto symbols_explanation = vbox({
1361 hbox({text(LYRICS_AVAIL), text(" -> "), text("The current song has lyrics metadata.")}) |
1363 hbox({text(ADDN_PROPS_AVAIL), text(" -> "),
1364 text("The current song has additional properties metadata.")}) |
1366 });
1367
1368 std::string footer_text =
1369 "Press '" + charToStr(global_keybinds.show_help) + "' to return to inLimbo.";
1370 auto footer = text(footer_text) | getTrueColor(TrueColors::Color::LightYellow) | center;
1371
1372 return vbox({
1373 title,
1374 controls_list | border | flex,
1375 text("Symbols Legend") | bold | getTrueColor(TrueColors::Color::LightBlue),
1376 symbols_explanation | border | flex,
1377 footer,
1378 }) |
1379 flex;
1380 }
1381
1382 Color GetCurrWinColor(bool focused)
1383 {
1384 return focused ? global_colors.active_win_border_color
1385 : global_colors.inactive_win_border_color;
1386 }
1387
1388 Element RenderMainInterface(float progress)
1389 {
1390 std::string current_song_info;
1391 std::string year_info;
1392 std::string additional_info;
1393
1394 if (!current_playing_state.artist.empty())
1395 {
1396 current_song_info = " - " + current_playing_state.title;
1397 year_info = std::to_string(current_playing_state.year) + " ";
1398
1399 if (current_playing_state.genre != "Unknown Genre")
1400 {
1401 additional_info += "Genre: " + current_playing_state.genre + STATUS_BAR_DELIM;
1402 }
1403 if (current_playing_state.has_comment)
1404 {
1405 additional_info += ADDN_PROPS_AVAIL;
1406 }
1407 if (current_playing_state.has_lyrics)
1408 {
1409 additional_info += STATUS_BAR_DELIM;
1410 additional_info += LYRICS_AVAIL;
1411 }
1412 additional_info += STATUS_BAR_DELIM;
1413 }
1414
1415 std::string status =
1416 std::string(" ") + (INL_Thread_State.is_playing ? STATUS_PLAYING : STATUS_PAUSED) + " ";
1417
1418 auto left_pane = vbox({
1419 text(" Artists") | bold | color(global_colors.artists_title_bg) | inverted,
1420 separator(),
1421 INL_Component_State.artists_list->Render() | frame | flex |
1423 }) |
1424 borderHeavy | color(GetCurrWinColor(focus_on_artists));
1425
1426 auto right_pane = vbox({
1427 text(" Songs") | bold | color(global_colors.songs_title_bg) | inverted,
1428 separator(),
1429 INL_Component_State.songs_list->Render() | frame | flex |
1431 }) |
1432 borderHeavy | color(GetCurrWinColor(!focus_on_artists));
1433
1434 auto panes = vbox({hbox({
1435 left_pane | size(WIDTH, EQUAL, 100) | size(HEIGHT, EQUAL, 100) | flex,
1436 right_pane | size(WIDTH, EQUAL, 100) | size(HEIGHT, EQUAL, 100) | flex,
1437 }) |
1438 flex}) |
1439 flex;
1440
1441 auto progress_style = INL_Thread_State.is_playing
1442 ? color(global_colors.progress_bar_playing_col)
1443 : color(global_colors.progress_bar_not_playing_col);
1444 auto progress_bar = hbox({
1445 text(FormatTime((int)current_position)) | progress_style,
1446 gauge(progress) | flex | progress_style,
1447 text(FormatTime(GetCurrentSongDuration())) | progress_style,
1448 });
1449
1450 auto volume_bar = hbox({
1451 text(" Vol: ") | dim,
1452 gauge(volume / 100.0) | size(WIDTH, EQUAL, 10) | color(global_colors.volume_bar_col),
1453 text(std::to_string(volume) + "%") | dim,
1454 });
1455
1456 std::string queue_info = " ";
1457 int songs_left = song_queue.size() - current_song_queue_index - 1;
1458 if (songs_left > song_queue.size())
1459 songs_left = 0;
1460 queue_info += std::to_string(songs_left) + " songs left.";
1461 std::string up_next_song = " Next up: ";
1462 if (song_queue.size() > 1 && song_queue[current_song_queue_index + 1].metadata.title != "")
1463 up_next_song += song_queue[current_song_queue_index + 1].metadata.title + " by " +
1464 song_queue[current_song_queue_index + 1].metadata.artist;
1465 else
1466 up_next_song += "Next song not available.";
1467 auto queue_bar = hbox({
1468 text(queue_info) | dim | border | bold,
1469 text(up_next_song) | dim | border | flex | size(WIDTH, LESS_THAN, MAX_LENGTH_SONG_NAME),
1470 });
1471
1472 auto status_bar =
1473 hbox({text(status) | getTrueColor(TrueColors::Color::Black),
1474 text(current_playing_state.artist) | color(global_colors.status_bar_artist_col) | bold |
1475 size(WIDTH, LESS_THAN, MAX_LENGTH_ARTIST_NAME),
1476 text(current_song_info) | bold | color(global_colors.status_bar_song_col) |
1477 size(WIDTH, LESS_THAN, MAX_LENGTH_SONG_NAME),
1478 filler(), // Push the right-aligned content to the end
1479 hbox({text(additional_info) | getTrueColor(TrueColors::Color::Black) | flex,
1480 text(year_info) | getTrueColor(TrueColors::Color::Black) |
1481 size(WIDTH, LESS_THAN, 15),
1482 text(" ")}) |
1483 align_right}) |
1484 size(HEIGHT, EQUAL, 1) | bgcolor(global_colors.status_bar_bg);
1485
1486 return vbox({
1487 panes,
1488 hbox({
1489 progress_bar | border | flex,
1490 volume_bar | border,
1491 queue_bar,
1492 }),
1493 status_bar,
1494 }) |
1495 flex;
1496 }
1497
1498 void Quit()
1499 {
1500 should_quit = true;
1501
1502 {
1503 std::unique_lock<std::mutex> lock(INL_Thread_State.play_mutex);
1504 if (audio_player)
1505 {
1506 audio_player->stop();
1507 }
1508 }
1509
1510 if (INL_Thread_State.play_future.valid())
1511 {
1512 auto status = INL_Thread_State.play_future.wait_for(std::chrono::seconds(1));
1513 if (status != std::future_status::ready)
1514 {
1515 // Handle timeout - future didn't complete in time
1516 dialog_message = "Warning: Audio shutdown timed out";
1517 show_dialog = true;
1518 }
1519 }
1520
1521 if (screen_)
1522 {
1523 screen_->ExitLoopClosure()();
1524 }
1525 }
1526};
1527
1528#endif // FTXUI_HANDLER_HPP
void Run()
Definition ui_handler.hpp:57
MusicPlayer(const std::map< std::string, std::map< std::string, std::map< unsigned int, std::map< unsigned int, Song > > > > &initial_library)
Definition ui_handler.hpp:46
A class that manages thread states and provides utilities for thread handling.
Definition thread_manager.hpp:19
InLimboColors parseColors()
Parses colors from a TOML configuration into an InLimboColors struct.
Definition colors.hpp:307
Keybinds parseKeybinds()
Parses the keybinds from the TOML configuration.
Definition keymaps.hpp:50
auto getTrueBGColor(TrueColors::Color color)
Definition misc.hpp:145
auto getTrueColor(TrueColors::Color color)
Definition misc.hpp:143
std::string charToStr(char ch)
Definition misc.hpp:116
auto RenderThumbnail(const std::string &songFilePath, const std::string &cacheDirPath, const std::string &songTitle, const std::string &artistName, const std::string &albumName, const std::string &genre, unsigned int year, unsigned int trackNumber, unsigned int discNumber, float progress)
Definition misc.hpp:182
auto renderSongName(const std::string &disc_track_info, const std::string &song_name, const int &duration)
Definition misc.hpp:154
auto RenderSongMenu(const std::vector< Element > &items)
Definition misc.hpp:171
auto renderAlbumName(const std::string &album_name, const int &year, ftxui::Color sel_color)
Definition misc.hpp:147
auto CreateMenu(const std::vector< std::string > *vecLines, int *currLine)
Definition misc.hpp:162
auto FormatTime(int seconds)
Definition misc.hpp:133
auto formatLyrics(const std::string &lyrics)
Definition misc.hpp:27
Color
Enumeration for predefined true colors.
Definition colors.hpp:26
@ Cyan
Definition colors.hpp:37
@ White
Definition colors.hpp:28
@ LightBlue
Definition colors.hpp:34
@ Coral
Definition colors.hpp:53
@ LightGreen
Definition colors.hpp:32
@ Pink
Definition colors.hpp:48
@ Orange
Definition colors.hpp:44
@ Gray
Definition colors.hpp:41
@ LightPink
Definition colors.hpp:49
@ LightCyan
Definition colors.hpp:38
@ Teal
Definition colors.hpp:50
@ Black
Definition colors.hpp:27
@ LightMagenta
Definition colors.hpp:40
@ LightRed
Definition colors.hpp:30
@ LightYellow
Definition colors.hpp:36
Definition image_view.cpp:21
Component Scroller(Component child, int *external_selected, Color cursor_bg)
Factory function to create a Scroller component.
Definition scroller.cpp:104
Implementation of a custom Scroller component for FTXUI.
unsigned int year
Definition taglib_parser.h:45
std::string filePath
Definition taglib_parser.h:50
std::string artist
Definition taglib_parser.h:41
unsigned int inode
Definition songmap.hpp:39
Metadata metadata
Definition songmap.hpp:40
#define PARENT_DBG
Definition toml_parser.hpp:26
string_view parseTOMLField(string parent, string field)
Parses a string field from the TOML configuration.
Definition toml_parser.hpp:121
string getCachePath()
Definition toml_parser.hpp:62
#define STATUS_PAUSED
Definition ui_handler.hpp:18
#define MIN_DEBOUNCE_TIME_IN_MS
Definition ui_handler.hpp:34
#define MAX_LENGTH_ARTIST_NAME
Definition ui_handler.hpp:25
#define SHOW_SONG_INFO_SCREEN
Definition ui_handler.hpp:32
#define SHOW_LYRICS_SCREEN
Definition ui_handler.hpp:30
#define SHOW_QUEUE_SCREEN
Definition ui_handler.hpp:31
#define SHOW_MAIN_UI
Definition ui_handler.hpp:28
#define LYRICS_AVAIL
Definition ui_handler.hpp:19
#define SHOW_HELP_SCREEN
Definition ui_handler.hpp:29
#define MAX_LENGTH_SONG_NAME
Definition ui_handler.hpp:24
#define ADDN_PROPS_AVAIL
Definition ui_handler.hpp:20
#define STATUS_BAR_DELIM
Definition ui_handler.hpp:21
#define STATUS_PLAYING
Definition ui_handler.hpp:17