inLimbo
TUI Music Player that keeps you in Limbo.
 
Loading...
Searching...
No Matches
ui_handler.hpp
Go to the documentation of this file.
1#pragma once
2
7#include "./properties.hpp"
8#include "dirsort/songmap.hpp"
9#include "misc.hpp"
10#include "states/state.hpp"
11
12using namespace ftxui;
13
15#define STATUS_PLAYING "<>"
16#define STATUS_PAUSED "!!"
17
19#define SHOW_MAIN_UI 0
20#define SHOW_HELP_SCREEN 1
21#define SHOW_LYRICS_SCREEN 2
22#define SHOW_QUEUE_SCREEN 3
23#define SHOW_SONG_INFO_SCREEN 4
24#define SHOW_AUDIO_CONF_SCREEN 5
25#define SHOW_SEARCH_INPUT 6
26
27#define MIN_DEBOUNCE_TIME_IN_MS 500
28
35
37{
38public:
40 const std::map<std::string,
41 std::map<std::string, std::map<unsigned int, std::map<unsigned int, Song>>>>&
42 initial_library,
43 Keybinds& keybinds, InLimboColors& colors)
44 : library(initial_library), INL_Thread_Manager(std::make_unique<ThreadManager>()),
45 INL_Thread_State(INL_Thread_Manager->getThreadState()), global_keybinds(keybinds),
46 global_colors(colors), searchIndices() // Initialize these vectors to avoid any abrupt exits
47 {
48 InitializeData();
49 CreateComponents();
50 }
51
52 void Run()
53 {
54 mprisService = std::make_unique<MPRISService>("inLimbo");
55
56 INL_Thread_State.mpris_dbus_thread = std::make_unique<std::thread>(
57 [this]
58 {
59 GMainLoop* loop = g_main_loop_new(nullptr, FALSE);
60 g_main_loop_run(loop);
61 g_main_loop_unref(loop);
62 });
63
64 INL_Thread_State.mpris_dbus_thread
65 ->detach(); // We will not be concerned with this thread entirely, hence it is detached
66
67 auto screen = ScreenInteractive::Fullscreen();
68 std::atomic<bool> screen_active{true};
69
70 std::thread refresh_thread(
71 [&]()
72 {
73 while (screen_active)
74 {
75 try
76 {
77 using namespace std::chrono_literals;
78
79 UpdateVolume();
80
81 std::this_thread::sleep_for(0.1s);
82
83 if (INL_Thread_State.is_playing)
84 {
85 current_position += 0.1;
86 // [TODO]: FIX BUG=> If we have a lot of activity (say we spam a lot of up/down,
87 // eventually after 10-15s, it will force itself to the next song)
88 if (current_position >= GetCurrentSongDuration())
89 {
90 PlayNextSong();
91 UpdatePlayingState();
92 }
93 }
94
95 SafePostEvent(screen, Event::Custom);
96 }
97 catch (const std::exception& e)
98 {
99 SetDialogMessage("Error in refresh thread: " + std::string(e.what()));
100 }
101 catch (...)
102 {
103 SetDialogMessage("Unknown error occurred in refresh thread.");
104 }
105 }
106 });
107
108 screen_ = &screen;
109
110 try
111 {
112 screen.Loop(INL_Component_State.MainRenderer);
113 }
114 catch (const std::exception& e)
115 {
116 SetDialogMessage("Error in UI loop: " + std::string(e.what()));
117 }
118 catch (...)
119 {
120 SetDialogMessage("Unknown error occurred in UI loop.");
121 }
122
123 // Signal the thread to stop and wait for cleanup (Should be better than detaching the thread)
124 screen_active = false;
125 if (refresh_thread.joinable())
126 {
127 refresh_thread.join();
128 }
129 }
130
131private:
132 PlayingState current_playing_state;
133 SearchState current_search_state;
134 ComponentState INL_Component_State;
135 QueueState qState;
136
137 std::unique_ptr<MPRISService> mprisService;
138
139 Keybinds global_keybinds;
140 GlobalProps global_props = parseProps();
141 InLimboColors global_colors;
142
143 std::shared_ptr<MiniAudioPlayer> audio_player;
144 std::unique_ptr<ThreadManager> INL_Thread_Manager; // Smart pointer to ThreadManager
145 ThreadManager::ThreadState& INL_Thread_State;
146
147 // Main data structure
148 std::map<std::string, std::map<std::string, std::map<unsigned int, std::map<unsigned int, Song>>>>
149 library;
150
151 // Navigation state
152 std::string current_artist;
153 bool show_dialog = false;
154 bool is_search_active = false;
155 bool show_audio_devices = false;
156 std::string dialog_message;
157 double seekBuffer;
158 bool first_g_pressed = false; // Track the state of the first 'g' press
159
160 // Current view lists
161 std::vector<std::string> current_artist_names;
162 std::vector<std::string> current_artist_song_names;
163 std::vector<std::string> lyricLines;
164 std::vector<std::string> audioDevices;
165 std::vector<int> searchIndices;
166 std::vector<AudioDevice> audioDeviceMap;
167 std::vector<std::string> audioDeviceConf;
168 std::vector<Element> current_song_elements;
169 std::vector<unsigned int> current_inodes;
170 std::unordered_set<int> album_name_indices;
171 int albums_indices_traversed = 1; // first element of current_song_elements is always an album
172
173 std::mutex state_mutex, event_mutex;
174 std::queue<ftxui::Event> event_queue;
175
176 // Player state
177 int selected_artist = 0;
178 int selected_audio_dev_line = 0;
179 int selected_inode = 1; // First element is always going to album name so we ignore it
180 int current_lyric_line = 0;
181 std::vector<Element> lyricElements;
182 int volume = 50;
183 bool muted = false;
184 int lastVolume = volume;
185 double current_position = 0;
186 int active_screen = 0; // 0 -> Main UI ; 1 -> Show help ; 2 -> Show lyrics; 3 -> Songs queue
187 // screen; 4 -> Song info screen; 5 -> Audio sinks screen
188 bool should_quit = false;
189 bool focus_on_artists = true;
190 ScreenInteractive* screen_ = nullptr;
191
192 void InitializeData()
193 {
194 for (const auto& artist_pair : library)
195 {
196 current_artist_names.push_back(artist_pair.first);
197 }
198 // Sort artist names alphabetically
199 std::sort(current_artist_names.begin(), current_artist_names.end());
200
201 if (!current_artist_names.empty())
202 {
203 UpdateSongsForArtist(current_artist_names[0]);
204 }
205 // Update the Search Query for Artists (it wont be updated again during the lifetime of the
206 // player so doing it here is fine)
207 for (size_t i = 0; i < current_artist_names.size(); ++i)
208 current_search_state.ArtistSearchTrie.insert(current_artist_names[i], i);
209 }
210
211 void SetDialogMessage(const std::string& message)
212 {
213 std::lock_guard<std::mutex> lock(state_mutex);
214 dialog_message = message;
215 show_dialog = true;
216 }
217
218 void SafePostEvent(ScreenInteractive& screen, const ftxui::Event& event)
219 {
220 {
221 std::lock_guard<std::mutex> lock(event_mutex);
222 event_queue.push(event);
223 }
224 screen.PostEvent(event);
225 }
226
227 void ProcessEvents(ScreenInteractive& screen)
228 {
229 std::lock_guard<std::mutex> lock(event_mutex);
230 while (!event_queue.empty())
231 {
232 // Process the event if needed
233 event_queue.pop();
234 }
235 }
236
237 /* Miniaudio class integrations */
238
239 auto getOrCreateAudioPlayer() -> std::shared_ptr<MiniAudioPlayer>
240 {
241 static std::mutex player_mutex;
242 if (!audio_player)
243 {
244 audio_player = std::make_shared<MiniAudioPlayer>();
245 }
246 return audio_player;
247 }
248
249 void PlayCurrentSong()
250 {
251 INL_Thread_Manager->lockPlayMutex(INL_Thread_State);
252 if (INL_Thread_State.is_processing)
253 {
254 return; // Another invocation is already running
255 }
256 INL_Thread_State.is_processing = true;
257
258 // Enqueue the song playback task
259 INL_Thread_Manager->getWorkerThreadPool().enqueue(
260 [this]()
261 {
262 try
263 {
264 audio_player = getOrCreateAudioPlayer();
265 audioDeviceMap = audio_player->enumerateDevices();
266 Song* current_song = qState.GetCurrentSongFromQueue();
267 if (!current_song)
268 {
269 throw std::runtime_error("Error: No current song found.");
270 }
271 const std::string& file_path = current_song->metadata.filePath;
272 if (file_path.empty())
273 {
274 throw std::runtime_error("Error: Invalid file path.");
275 }
276
277 if (INL_Thread_State.is_playing)
278 {
279 audio_player->stop();
280 }
281
282 auto load_future = audio_player->loadFileAsync(file_path, true);
283 // Wait for the asynchronous load to complete
284 int loadAudioFileStatus = load_future.get();
285 if (loadAudioFileStatus == -1)
286 {
287 throw std::runtime_error("Error: Failed to load the audio file.");
288 }
289 INL_Thread_State.is_playing = true;
290 UpdateVolume();
291 audio_player->play(); // Play the song (it will play in a separate thread)
292 auto durationFuture = audio_player->getDurationAsync();
293 current_playing_state.duration = durationFuture.get();
294 }
295 catch (const std::exception& e)
296 {
297 SetDialogMessage(e.what());
298 audio_player->stop();
299 INL_Thread_State.is_playing = false;
300 }
301
302 // Reset processing state
303 INL_Thread_State.is_processing = false;
304 });
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 try
343 {
344 if (qState.song_queue.empty())
345 {
346 SetDialogMessage("Error: Queue is empty.");
347 INL_Thread_State.is_playing = false;
348 return;
349 }
350
351 if (qState.qIndex + 1 >= qState.song_queue.size())
352 {
353 SetDialogMessage("Error: No more songs in the queue.");
354 return;
355 }
356
357 qState.incrementQIndex();
358
359 Song* current_song = qState.GetCurrentSongFromQueue();
360 if (!current_song)
361 {
362 INL_Thread_State.is_playing = false;
363 SetDialogMessage("Error: Invalid song in queue.");
364 return;
365 }
366
367 current_position = 0;
368 PlayCurrentSong();
369 UpdatePlayingState();
370 }
371 catch (std::exception e)
372 {
373 show_dialog = true;
374 dialog_message = "Error: Invalid!!.";
375 return;
376 }
377 }
378
379 void ReplaySong()
380 {
381 current_position = 0;
382 PlayCurrentSong();
383 UpdatePlayingState();
384 return;
385 }
386
387 void PlayPreviousSong()
388 {
389 try
390 {
391 if (qState.song_queue.empty())
392 {
393 SetDialogMessage("Error: Queue is empty.");
394 INL_Thread_State.is_playing = false;
395 return;
396 }
397
398 if (qState.qIndex + 1 >= qState.song_queue.size())
399 {
400 SetDialogMessage("Error: No more previous songs in the queue.");
401 return;
402 }
403
404 qState.decrementQIndex();
405
406 Song* current_song = qState.GetCurrentSongFromQueue();
407 if (!current_song)
408 {
409 INL_Thread_State.is_playing = false;
410 SetDialogMessage("Error: Invalid song in queue.");
411 return;
412 }
413
414 current_position = 0;
415 PlayCurrentSong();
416 UpdatePlayingState();
417 }
418 catch (std::exception e)
419 {
420 show_dialog = true;
421 dialog_message = "Error: Invalid!!.";
422 return;
423 }
424 }
425
426 void findAudioSinks()
427 {
428 audioDevices.clear();
429 for (const auto& device : audioDeviceMap)
430 {
431 audioDevices.push_back(device.name);
432 }
433 return;
434 }
435
436 auto RenderAudioConsole() -> Element
437 {
438 INL_Component_State.audioDeviceMenu = CreateMenu(&audioDevices, &selected_audio_dev_line);
439 findAudioSinks();
440 if (audioDevices.empty())
441 return vbox({text("No sinks available.") | bold | border});
442
443 auto title = text(" Audio Console ") | bold | getTrueColor(TrueColors::Color::LightBlue) |
444 underlined | center;
445
446 auto audioDevComp = vbox({INL_Component_State.audioDeviceMenu->Render() | flex}) | border;
447
448 audioDeviceConf = audio_player->getAudioPlaybackDetails();
449
450 vector<Element> audioConfDetails;
451 for (const auto& i : audioDeviceConf)
452 audioConfDetails.push_back(text(i) | frame);
453
454 auto finalAudioConfDetailsElement = vbox(audioConfDetails);
455
456 return vbox({title, separator(), audioDevComp | frame | flex, separator(),
457 finalAudioConfDetailsElement | flex | border}) |
458 flex | borderRounded;
459 }
460
461 /* ------------------------------- */
462
463 void UpdateSongsForArtist(const std::string& artist)
464 {
465 current_inodes.clear();
466 current_song_elements.clear();
467 album_name_indices.clear();
468 current_search_state.SongSearchTrie.clear();
469 current_search_state.songIndex = 0;
470
471 if (library.count(artist) > 0)
472 {
473 std::map<std::string, std::map<unsigned int, std::map<unsigned int, Song>>> albums;
474 for (const auto& album_pair : library.at(artist))
475 {
476 const std::string& album_name = album_pair.first;
477 for (const auto& disc_pair : album_pair.second)
478 {
479 for (const auto& track_pair : disc_pair.second)
480 {
481 albums[album_name][disc_pair.first][track_pair.first] = track_pair.second;
482 }
483 }
484 }
485
486 for (const auto& [album_name, discs] : albums)
487 {
488 const Song& first_song = discs.begin()->second.begin()->second;
489 current_song_elements.push_back(renderAlbumName(album_name, first_song.metadata.year,
490 global_colors.album_name_bg,
491 global_colors.album_name_fg));
492 album_name_indices.insert(current_song_elements.size() - 1);
493
494 for (const auto& [disc_number, tracks] : discs)
495 {
496 for (const auto& [track_number, song] : tracks)
497 {
498 std::string disc_track_info = formatDiscTrackInfo(disc_number, track_number);
499 current_inodes.push_back(song.inode);
500 current_search_state.SongSearchTrie.insert(song.metadata.title,
501 current_search_state.songIndex++);
502 current_song_elements.push_back(
503 renderSongName(disc_track_info, song.metadata.title, song.metadata.duration));
504 }
505 }
506 }
507 }
508
509 current_artist = artist;
510 selected_inode = 1;
511 albums_indices_traversed = 1;
512 }
513
514 auto GetCurrentSong(const std::string& artist) -> const Song&
515 {
516 const auto& artist_data = library.at(artist);
517
518 // Iterate through all albums, discs, and tracks
519 for (const auto& album_pair : artist_data)
520 {
521 for (const auto& disc_pair : album_pair.second)
522 {
523 for (const auto& track_pair : disc_pair.second)
524 {
525 const Song& song = track_pair.second;
526
527 if (current_inodes[selected_inode - albums_indices_traversed] == song.inode)
528 {
529 return song;
530 }
531 }
532 }
533 }
534
535 throw std::runtime_error("Song not found.");
536 }
537
538 void PlayThisSongNext(const std::string& artist)
539 {
540 const Song& get_curr_song = GetCurrentSong(artist);
541
542 try
543 {
544 qState.insertSongToIndex(get_curr_song);
545 }
546 catch (std::exception e)
547 {
548 SetDialogMessage("Could not play this song next!");
549 }
550
551 qState.incrementQIndex();
552 return;
553 }
554
555 void EnqueueAllSongsByArtist(const std::string& artist, bool clearQueue)
556 {
557 if (clearQueue)
558 qState.clearQueue();
559
560 bool start_enqueue = false;
561
562 if (library.find(artist) == library.end())
563 {
564 std::cerr << "Artist not found in the library: " << artist << std::endl;
565 return;
566 }
567
568 const auto& artist_data = library.at(artist);
569
570 // Iterate through all albums, discs, and tracks
571 for (const auto& album_pair : artist_data)
572 {
573 for (const auto& disc_pair : album_pair.second)
574 {
575 for (const auto& track_pair : disc_pair.second)
576 {
577 const Song& song = track_pair.second;
578
579 if (current_inodes[selected_inode - albums_indices_traversed] == song.inode)
580 {
581 start_enqueue = true;
582 }
583 if (start_enqueue)
584 qState.qPush(song);
585 }
586 }
587 }
588 }
589
590 void AddSongToQueue()
591 {
592 if (selected_artist >= current_artist_names.size() ||
593 selected_inode - albums_indices_traversed >= current_inodes.size())
594 {
595 throw std::runtime_error("Invalid artist or song selection.");
596 }
597
598 // Get a const reference to the current song
599 const Song& current_preview_song = GetCurrentSong(current_artist_names[selected_artist]);
600
601 qState.qPush(current_preview_song);
602
603 NavigateSongMenu(true);
604 }
605
606 void RemoveSongFromQueue()
607 {
608 if (qState.qScreenIndex == 0)
609 {
610 SetDialogMessage("Unable to remove song... This is playing right now!");
611 return;
612 }
613 if (qState.qScreenIndex < qState.getQueueSize())
614 {
615 qState.qPopIndex();
616 }
617 }
618
619 auto GetCurrentSongDuration() -> int
620 {
621 if (!current_inodes.empty() && audio_player)
622 {
623 return current_playing_state.duration;
624 }
625 return 0;
626 }
627
628 void UpdatePlayingState()
629 {
630 // Retrieve the current song from the queue
631 Song* current_song = qState.GetCurrentSongFromQueue();
632 if (!current_song)
633 {
634 SetDialogMessage("Current Song not available!");
635 return;
636 }
637
638 const auto& metadata = current_song->metadata;
639
640 // Offload metadata updates to a worker thread
641 INL_Thread_Manager->getWorkerThreadPool().enqueue(
642 [this, metadata]()
643 {
644 // Perform updates locally before committing to the shared state
645 PlayingState new_state = current_playing_state;
646
647 if (new_state.filePath == metadata.filePath)
648 {
649 return;
650 }
651
652 new_state.copyMetadata(metadata);
653
654 // Commit to shared state under mutex
655 {
656 std::lock_guard<std::mutex> lock(state_mutex);
657 current_playing_state = std::move(new_state);
658 }
659
660 mprisService->updateMetadata(current_playing_state.title, current_playing_state.artist,
661 current_playing_state.album,
662 static_cast<int64_t>(current_playing_state.duration),
663 current_playing_state.comment, current_playing_state.genre,
664 current_playing_state.track, current_playing_state.discNumber);
665
666 current_lyric_line = 0;
667 });
668 }
669
670 void CreateComponents()
671 {
672 INL_Component_State.artists_list = Scroller(
673 Renderer([&]() mutable { return RenderArtistMenu(current_artist_names); }), &selected_artist,
674 global_colors.artists_menu_col_bg, global_colors.inactive_menu_cursor_bg);
675 INL_Component_State.songs_list = Scroller(
676 Renderer(
677 [&]() mutable
678 {
679 return RenderSongMenu(current_song_elements);
680 }),
681 &selected_inode, global_colors.menu_cursor_bg, global_colors.inactive_menu_cursor_bg);
682
683 auto main_container =
684 Container::Horizontal({INL_Component_State.artists_list, INL_Component_State.songs_list});
685
686 /* Adding DEBOUNCE TIME (Invoking PlayCurrentSong() too fast causes resources to not be freed)
687 * [TODO] REMOVED DEBOUNCE FOR NOW
688 */
689
690 auto last_event_time = std::chrono::steady_clock::now(); // Tracks the last event time
691 int debounce_time = std::stoi(std::string(parseTOMLField(PARENT_DBG, "debounce_time_in_ms")));
692 if (debounce_time < MIN_DEBOUNCE_TIME_IN_MS)
693 debounce_time = MIN_DEBOUNCE_TIME_IN_MS; // min debounce time is 0.5s
694 const int final_debounce_time = debounce_time;
695 auto debounce_duration = std::chrono::milliseconds(final_debounce_time);
696
697 main_container |= CatchEvent(
698 [&](Event event)
699 {
700 auto is_keybind_match = [&](char key) -> bool
701 {
702 return (event.is_character() && event.character() == std::string(1, key)) ||
703 (event == Event::Special(std::string(1, static_cast<char>(key))));
704 };
705
706 if (is_search_active)
707 {
708 if (event.is_character())
709 {
710 current_search_state.input += event.character();
711
712 current_search_state.artistIndex = 0;
713 current_search_state.songIndex = 0;
714
715 if (focus_on_artists)
716 {
717 searchIndices =
718 current_search_state.ArtistSearchTrie.search(current_search_state.input);
719 if (!searchIndices.empty())
720 {
721 selected_artist = searchIndices[current_search_state.artistIndex];
722 UpdateSongsForArtist(current_artist_names[selected_artist]);
723 }
724 }
725 else
726 {
727 searchIndices =
728 current_search_state.SongSearchTrie.search(current_search_state.input);
729 if (!searchIndices.empty())
730 {
731 selected_inode = searchIndices[current_search_state.songIndex];
732 }
733 }
734 return true;
735 }
736 else if (event == Event::Backspace)
737 {
738 if (!current_search_state.input.empty())
739 current_search_state.input.pop_back();
740 return true;
741 }
742 else if (event == Event::Return || event == Event::Escape)
743 {
744 is_search_active = false;
745 current_search_state.input.clear();
746 return true;
747 }
748 }
749
750 if (active_screen == SHOW_HELP_SCREEN)
751 {
752 if (event.is_character() && (event.character()[0] == global_keybinds.show_help ||
753 std::toupper(global_keybinds.quit_app) ||
754 event.character()[0] == global_keybinds.quit_app))
755 {
756 active_screen = SHOW_MAIN_UI;
757 return true;
758 }
759 return false; // Prevent other keys from working
760 }
761
762 else if (active_screen == SHOW_LYRICS_SCREEN)
763 {
764 if (is_keybind_match(global_keybinds.goto_main_screen))
765 {
766 active_screen = SHOW_MAIN_UI;
767 return true;
768 }
769 }
770
771 else if (active_screen == SHOW_QUEUE_SCREEN)
772 {
773 if (is_keybind_match(global_keybinds.remove_song_from_queue))
774 {
775 RemoveSongFromQueue();
776 return true;
777 }
778 else if (is_keybind_match(global_keybinds.goto_main_screen))
779 {
780 active_screen = SHOW_MAIN_UI;
781 return true;
782 }
783 }
784
785 else if (active_screen == SHOW_SONG_INFO_SCREEN)
786 {
787 if (is_keybind_match(global_keybinds.goto_main_screen))
788 {
789 active_screen = SHOW_MAIN_UI;
790 return true;
791 }
792 }
793
794 else if (active_screen == SHOW_AUDIO_CONF_SCREEN)
795 {
796 if (is_keybind_match(global_keybinds.goto_main_screen))
797 {
798 show_audio_devices = false;
799 active_screen = SHOW_MAIN_UI;
800 return true;
801 }
802 }
803 else if (active_screen == SHOW_MAIN_UI)
804 {
805
806 if (is_keybind_match(global_keybinds.play_song) && !focus_on_artists)
807 {
808 if (!current_artist.empty())
809 {
810 EnqueueAllSongsByArtist(current_artist, true);
811
812 if (Song* current_song = qState.GetCurrentSongFromQueue())
813 {
814 // Enqueue all songs by the current artist
815 current_position = 0;
816 PlayCurrentSong();
817 UpdatePlayingState();
818 }
819 }
820 else
821 {
822 SetDialogMessage("No artist selected to play songs from.");
823 }
824 return true;
825 }
826 // Check against keybinds using if-else instead of switch
827 else if (is_keybind_match(global_keybinds.quit_app) ||
828 is_keybind_match(std::toupper(global_keybinds.quit_app)))
829 {
830 Quit();
831 return true;
832 }
833 else if (is_keybind_match(global_keybinds.toggle_play))
834 {
835 TogglePlayback();
836 return true;
837 }
838 else if (is_keybind_match(global_keybinds.play_song_next))
839 {
840 /*auto now = std::chrono::steady_clock::now();*/
841 /*if (now - last_event_time < debounce_duration)*/
842 /*{*/
843 /* audio_player->stop();*/
844 /* return false;*/
845 /*}*/
846 PlayNextSong();
847 /*last_event_time = now; // Update the last event time*/
848 return true;
849 }
850 else if (is_keybind_match(global_keybinds.play_song_prev))
851 {
852 /*auto now = std::chrono::steady_clock::now();*/
853 /*if (now - last_event_time < debounce_duration)*/
854 /* return false;*/
855 /*last_event_time = now;*/
856 PlayPreviousSong();
857 return true;
858 }
859 else if (is_keybind_match(global_keybinds.seek_ahead_5) && INL_Thread_State.is_playing)
860 {
861 seekBuffer = audio_player->seekTime(5);
862 current_position += seekBuffer;
863 return true;
864 }
865 else if (is_keybind_match(global_keybinds.seek_behind_5) && INL_Thread_State.is_playing)
866 {
867 if (current_position > 5)
868 {
869 seekBuffer = audio_player->seekTime(-5);
870 current_position += seekBuffer;
871 }
872 else
873 {
874 ReplaySong();
875 }
876 UpdatePlayingState();
877 return true;
878 }
879 else if (is_keybind_match(global_keybinds.replay_song))
880 {
881 ReplaySong();
882 return true;
883 }
884 else if (is_keybind_match(global_keybinds.vol_up))
885 {
886 volume = std::min(100, volume + 5);
887 UpdateVolume();
888 return true;
889 }
890 else if (is_keybind_match(global_keybinds.vol_down))
891 {
892 volume = std::max(0, volume - 5);
893 UpdateVolume();
894 return true;
895 }
896 else if (is_keybind_match(global_keybinds.toggle_mute))
897 {
898 volume = handleToggleMute(&volume, &lastVolume, &muted);
899 UpdateVolume();
900 return true;
901 }
902 else if (is_keybind_match(global_keybinds.show_help))
903 {
904 active_screen = SHOW_HELP_SCREEN;
905 return true;
906 }
907 else if (is_keybind_match(global_keybinds.toggle_audio_devices))
908 {
909 show_audio_devices = !show_audio_devices;
910 if (show_audio_devices)
911 {
912 active_screen = SHOW_AUDIO_CONF_SCREEN;
913 }
914 else
915 {
916 active_screen = SHOW_MAIN_UI;
917 }
918 return true;
919 }
920 else if (is_keybind_match(global_keybinds.view_lyrics) &&
921 (current_playing_state.HasLyrics() || current_playing_state.HasComments()))
922 {
923 active_screen = SHOW_LYRICS_SCREEN;
924 return true;
925 }
926 else if (is_keybind_match(global_keybinds.view_song_queue) && !qState.song_queue.empty())
927 {
928 active_screen = SHOW_QUEUE_SCREEN;
929 return true;
930 }
931 else if (is_keybind_match(global_keybinds.goto_main_screen))
932 {
933 active_screen = SHOW_MAIN_UI;
934 return true;
935 }
936 else if (is_keybind_match(global_keybinds.view_current_song_info))
937 {
938 active_screen = SHOW_SONG_INFO_SCREEN;
939 return true;
940 }
941 else if (is_keybind_match(global_keybinds.toggle_focus))
942 {
943 focus_on_artists = !focus_on_artists;
944 if (focus_on_artists)
945 {
946 INL_Component_State.artists_list->TakeFocus();
947 }
948 else
949 {
950 INL_Component_State.songs_list->TakeFocus();
951 }
952 return true;
953 }
954 else if (is_keybind_match(global_keybinds.add_song_to_queue))
955 {
956 AddSongToQueue();
957 return true;
958 }
959 else if (is_keybind_match(global_keybinds.add_artists_songs_to_queue) && focus_on_artists)
960 {
961 EnqueueAllSongsByArtist(current_artist_names[selected_artist], false);
962 if (!INL_Thread_State.is_playing)
963 {
964 current_position = 0;
965 PlayCurrentSong();
966 UpdatePlayingState();
967 }
968 NavigateList(true);
969 return true;
970 }
971 else if (is_keybind_match(global_keybinds.play_this_song_next) && !focus_on_artists)
972 {
973 PlayThisSongNext(current_artist_names[selected_artist]);
974 return true;
975 }
976 else if (is_keybind_match(global_keybinds.search_menu))
977 {
978 is_search_active = true;
979 searchIndices.clear();
980 current_search_state.input.clear();
981 return true;
982 }
983 else if (is_keybind_match(global_keybinds.search_item_next))
984 {
985 if (focus_on_artists)
986 {
987 // Increment index and wrap around if necessary
988 UpdateSelectedIndex(current_search_state.artistIndex, searchIndices.size(), true);
989 selected_artist = searchIndices[current_search_state.artistIndex];
990 UpdateSongsForArtist(current_artist_names[selected_artist]);
991 return true;
992 }
993 else
994 {
995 // Inc index and wrap around if necessary
996 UpdateSelectedIndex(current_search_state.songIndex, searchIndices.size(), true);
997 selected_inode = searchIndices[current_search_state.songIndex];
998 return true;
999 }
1000 }
1001 else if (is_keybind_match(global_keybinds.search_item_prev))
1002 {
1003 if (focus_on_artists)
1004 {
1005 // Decrement index and wrap around if necessary
1006 UpdateSelectedIndex(current_search_state.artistIndex, searchIndices.size(), false);
1007 selected_artist = searchIndices[current_search_state.artistIndex];
1008 UpdateSongsForArtist(current_artist_names[selected_artist]);
1009 return true;
1010 }
1011 else
1012 {
1013 // Decrement index and wrap around if necessary
1014 UpdateSelectedIndex(current_search_state.songIndex, searchIndices.size(), false);
1015 selected_inode = searchIndices[current_search_state.songIndex];
1016 return true;
1017 }
1018 }
1019 }
1020
1021 if (is_keybind_match(global_keybinds.scroll_down) || event == Event::ArrowDown)
1022 {
1023 NavigateList(true);
1024 return true;
1025 }
1026 else if (is_keybind_match(global_keybinds.scroll_up) || event == Event::ArrowUp)
1027 {
1028 NavigateList(false);
1029 return true;
1030 }
1031 /* @Some default keybinds */
1032 else if (is_keybind_match('g'))
1033 {
1034
1035 if (!first_g_pressed)
1036 {
1037 // First 'g' press
1038 first_g_pressed = true;
1039 }
1040 else
1041 {
1042 // Second 'g' press
1043 NavigateListToTop(true);
1044 first_g_pressed = false;
1045 return true;
1046 }
1047 }
1048 else if (is_keybind_match('G'))
1049 {
1050 NavigateListToTop(false);
1051 return true;
1052 }
1053 else if (is_keybind_match('x'))
1054 {
1055 show_dialog = false;
1056 return true;
1057 }
1058
1059 return false;
1060 });
1061
1062 INL_Component_State.MainRenderer =
1063 Renderer(main_container,
1064 [&]
1065 {
1066 int duration = GetCurrentSongDuration();
1067 float progress = duration > 0 ? (float)current_position / duration : 0;
1068
1069 Element interface;
1070 switch (active_screen)
1071 {
1072 case SHOW_HELP_SCREEN:
1073 interface = RenderHelpScreen(global_keybinds);
1074 break;
1075 case SHOW_MAIN_UI:
1076 interface = RenderMainInterface(progress);
1077 break;
1078 case SHOW_LYRICS_SCREEN:
1079 interface = RenderLyricsAndInfoView();
1080 break;
1081 case SHOW_QUEUE_SCREEN:
1082 interface = RenderQueueScreen();
1083 break;
1085 interface = RenderAudioConsole();
1086 break;
1088 interface = RenderThumbnail(
1089 current_playing_state.filePath, getCachePath(), current_playing_state.title,
1090 current_playing_state.artist, current_playing_state.album,
1091 current_playing_state.genre, current_playing_state.year,
1092 current_playing_state.track, current_playing_state.discNumber, progress);
1093 break;
1094 }
1095 if (show_dialog)
1096 {
1097 interface = dbox({interface, RenderDialog(dialog_message) | center}) |
1099 }
1100 return window(text(" inLimbo ") | bold, vbox(interface));
1101 });
1102 }
1103
1104 void UpdateLyrics()
1105 {
1106 lyricLines.clear();
1107 lyricLines = current_playing_state.formatLyrics();
1108 return;
1109 }
1110
1111 auto RenderLyricsAndInfoView() -> Element
1112 {
1113
1114 std::vector<Element> additionalPropertiesText;
1115 for (const auto& [key, value] : current_playing_state.additionalProperties)
1116 {
1117 if (key != "LYRICS")
1118 {
1119 additionalPropertiesText.push_back(hbox({text(key + ": "), text(value) | dim}));
1120 }
1121 }
1122
1123 INL_Component_State.lyrics_scroller = CreateMenu(&lyricLines, &current_lyric_line);
1124
1125 UpdateLyrics();
1126
1127 std::string end_text = "Use arrow keys to scroll, Press '" +
1128 charToStr(global_keybinds.goto_main_screen) + "' to go back home.";
1129
1130 auto lyrics_pane = vbox({
1131 INL_Component_State.lyrics_scroller->Render() | flex,
1132 });
1133
1134 auto info_pane = window(text(" Additional Info ") | bold | center,
1135 vbox(additionalPropertiesText) | frame | flex);
1136
1137 return hbox({vbox({
1138 lyrics_pane | frame | flex | border,
1139 separator(),
1140 text(end_text) | dim | center | border,
1141 }) |
1142 flex,
1143 vbox({info_pane}) | flex}) |
1144 flex;
1145 }
1146
1147 auto RenderQueueScreen() -> Element
1148 {
1149 INL_Component_State.songs_queue_comp =
1150 CreateMenu(&qState.song_queue_names, &qState.qScreenIndex);
1151 qState.UpdateSongQueueList();
1152
1153 auto title = text(" Song Queue ") | bold | getTrueColor(TrueColors::Color::LightBlue) |
1154 underlined | center;
1155
1156 auto separator_line = separator() | dim | flex;
1157
1158 std::string end_text = "Use '" + charToStr(global_keybinds.remove_song_from_queue) +
1159 "' to remove selected song from queue, Press '" +
1160 charToStr(global_keybinds.goto_main_screen) + "' to go back home.";
1161
1162 auto queue_container = vbox({
1163 INL_Component_State.songs_queue_comp->Render() |
1164 color(global_colors.song_queue_menu_fg) | flex,
1165 }) |
1166 border | color(global_colors.song_queue_menu_bor_col);
1167
1168 return vbox({
1169 title,
1170 queue_container | frame | flex,
1171 filler(),
1172 text(end_text) | dim | center,
1173 }) |
1174 flex;
1175 }
1176
1177 void NavigateSongMenu(bool move_down)
1178 {
1179 if (current_song_elements.empty())
1180 return;
1181
1182 int initial_inode = selected_inode;
1183 do
1184 {
1185 UpdateSelectedIndex(selected_inode, current_song_elements.size(), move_down);
1186
1187 if (album_name_indices.count(selected_inode))
1188 {
1189 move_down ? ++albums_indices_traversed : --albums_indices_traversed;
1190 }
1191
1192 if (selected_inode == 0 && move_down)
1193 {
1194 albums_indices_traversed = 1;
1195 }
1196 else if (selected_inode == current_song_elements.size() - 1 && !move_down)
1197 {
1198 albums_indices_traversed = album_name_indices.size();
1199 }
1200
1201 if (selected_inode == initial_inode)
1202 break;
1203 } while (album_name_indices.count(selected_inode)); // Skip album headers
1204 }
1205
1206 void NavigateList(bool move_down)
1207 {
1208 switch (active_screen)
1209 {
1210 case SHOW_MAIN_UI:
1211 if (focus_on_artists && !current_artist_names.empty())
1212 {
1213 UpdateSelectedIndex(selected_artist, current_artist_names.size(), move_down);
1214 UpdateSongsForArtist(current_artist_names[selected_artist]);
1215 }
1216 else if (!current_inodes.empty())
1217 {
1218 NavigateSongMenu(move_down);
1219 }
1220 break;
1221
1222 case SHOW_QUEUE_SCREEN:
1223 UpdateSelectedIndex(qState.qScreenIndex, qState.song_queue_names.size(), move_down);
1224 break;
1225
1226 case SHOW_LYRICS_SCREEN:
1227 UpdateSelectedIndex(current_lyric_line, lyricLines.size(), move_down);
1228 break;
1230 UpdateSelectedIndex(selected_audio_dev_line, audioDevices.size(), move_down);
1231 break;
1232 }
1233 }
1234
1235 void NavigateListToTop(bool move_up)
1236 {
1237 switch (active_screen)
1238 {
1239 case SHOW_MAIN_UI:
1240 if (focus_on_artists && !current_artist_names.empty())
1241 {
1242 selected_artist = move_up ? 0 : current_artist_names.size() - 1;
1243 UpdateSongsForArtist(current_artist_names[selected_artist]);
1244 selected_inode = 1;
1245 albums_indices_traversed = 1;
1246 }
1247 else if (!current_inodes.empty())
1248 {
1249 selected_inode = move_up ? 1 : current_song_elements.size() - 1;
1250 albums_indices_traversed = move_up ? 1 : album_name_indices.size();
1251 }
1252 break;
1253
1254 case SHOW_QUEUE_SCREEN:
1255 qState.qScreenIndex = move_up ? 0 : qState.song_queue_names.size() - 1;
1256 break;
1257
1258 case SHOW_LYRICS_SCREEN:
1259 current_lyric_line = move_up ? 0 : lyricLines.size() - 1;
1260 break;
1261 }
1262 }
1263
1264 auto GetCurrWinColor(bool focused) -> Color
1265 {
1266 return focused ? global_colors.active_win_border_color
1267 : global_colors.inactive_win_border_color;
1268 }
1269
1270 auto RenderProgressBar(float progress) -> Element
1271 {
1272 auto progress_style = INL_Thread_State.is_playing
1273 ? color(global_colors.progress_bar_playing_col)
1274 : color(global_colors.progress_bar_not_playing_col);
1275
1276 return hbox({
1277 text(FormatTime((int)current_position)) | progress_style,
1278 gauge(progress) | flex | progress_style,
1279 text(FormatTime(GetCurrentSongDuration())) | progress_style,
1280 });
1281 }
1282
1283 auto RenderQueueBar() -> Element
1284 {
1285 std::string queue_info = " ";
1286 int songs_left = qState.getQueueSize() - qState.qIndex - 1;
1287 if (songs_left >= qState.getQueueSize() && songs_left < 0)
1288 songs_left = 0;
1289 queue_info += std::to_string(songs_left) + " songs left.";
1290
1291 std::string up_next_song = " Next up: ";
1292 if (qState.getQueueSize() > 1 && songs_left > 0)
1293 up_next_song += qState.song_queue[qState.qIndex + 1].metadata.title + " by " +
1294 qState.song_queue[qState.qIndex + 1].metadata.artist;
1295 else
1296 up_next_song += "Next song not available.";
1297
1298 return hbox({
1299 text(queue_info) | dim | border | bold,
1300 text(up_next_song) | dim | border | flex | size(WIDTH, LESS_THAN, MAX_LENGTH_SONG_NAME),
1301 });
1302 }
1303
1304 auto RenderMainInterface(float progress) -> Element
1305 {
1306 std::string current_song_info;
1307 std::string year_info;
1308 std::string additional_info;
1309
1310 if (!current_playing_state.artist.empty())
1311 {
1312 current_song_info = SONG_TITLE_DELIM + current_playing_state.title;
1313 year_info = std::to_string(current_playing_state.year) + " ";
1314 additional_info = formatAdditionalInfo(
1315 current_playing_state.genre, current_playing_state.has_comment,
1316 current_playing_state.has_lyrics, global_props.show_bitrate, current_playing_state.bitrate);
1317 }
1318
1319 std::string status =
1320 std::string(" ") + (INL_Thread_State.is_playing ? STATUS_PLAYING : STATUS_PAUSED) + " ";
1321
1322 auto left_pane = vbox({
1323 text(" Artists") | bold | color(global_colors.artists_title_bg) | inverted,
1324 separator(),
1325 INL_Component_State.artists_list->Render() | frame | flex |
1327 }) |
1328 borderHeavy | color(GetCurrWinColor(focus_on_artists));
1329
1330 auto right_pane = vbox({
1331 text(" Songs") | bold | color(global_colors.songs_title_bg) | inverted,
1332 separator(),
1333 INL_Component_State.songs_list->Render() | frame | flex |
1335 }) |
1336 borderHeavy | color(GetCurrWinColor(!focus_on_artists));
1337
1338 auto panes = vbox({hbox({
1339 left_pane | size(WIDTH, EQUAL, 100) | size(HEIGHT, EQUAL, 100) | flex,
1340 right_pane | size(WIDTH, EQUAL, 100) | size(HEIGHT, EQUAL, 100) | flex,
1341 }) |
1342 flex}) |
1343 flex;
1344
1345 auto progress_bar = RenderProgressBar(progress);
1346 auto volume_bar = RenderVolumeBar(volume, global_colors.volume_bar_col);
1347 auto queue_bar = RenderQueueBar();
1348 auto status_bar = RenderStatusBar(status, current_song_info, additional_info, year_info,
1349 global_colors, current_playing_state.artist);
1350 auto search_bar =
1351 is_search_active == true ? RenderSearchBar(current_search_state.input) : filler();
1352
1353 return vbox({panes,
1354 hbox({
1355 progress_bar | border | flex_grow,
1356 volume_bar | border,
1357 queue_bar,
1358 }),
1359 status_bar, search_bar}) |
1360 flex;
1361 }
1362
1363 void Quit()
1364 {
1365 should_quit = true;
1366
1367 {
1368 INL_Thread_Manager->lockPlayMutex(INL_Thread_State);
1369 if (audio_player)
1370 {
1371 audio_player->stop();
1372 }
1373 }
1374
1375 if (INL_Thread_State.play_future.valid())
1376 {
1377 auto status = INL_Thread_State.play_future.wait_for(std::chrono::milliseconds(50));
1378 if (status != std::future_status::ready)
1379 {
1380 // Handle timeout - future didn't complete in time
1381 SetDialogMessage("Warning: Audio shutdown timed out");
1382 }
1383 }
1384
1385 if (screen_)
1386 {
1387 screen_->ExitLoopClosure()();
1388 }
1389 }
1390};
void Run()
Definition ui_handler.hpp:52
MusicPlayer(const std::map< std::string, std::map< std::string, std::map< unsigned int, std::map< unsigned int, Song > > > > &initial_library, Keybinds &keybinds, InLimboColors &colors)
Definition ui_handler.hpp:39
A class that manages thread states and provides utilities for thread handling.
Definition thread_manager.hpp:20
auto getTrueColor(TrueColors::Color color)
Definition misc.hpp:194
auto handleToggleMute(int *volume, int *lastVolume, bool *muted) -> int
Definition misc.hpp:28
auto charToStr(char ch) -> std::string
Definition misc.hpp:167
#define SONG_TITLE_DELIM
Definition misc.hpp:23
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:284
auto RenderDialog(const std::string &dialog_message) -> Element
Definition misc.hpp:359
auto RenderArtistMenu(const std::vector< std::string > &artist_list)
Definition misc.hpp:248
auto renderAlbumName(const std::string &album_name, const int &year, ftxui::Color sel_color, ftxui::Color sel_color_fg)
Definition misc.hpp:198
auto renderSongName(const std::string &disc_track_info, const std::string &song_name, const int &duration)
Definition misc.hpp:207
auto formatAdditionalInfo(const std::string &genre, const bool &has_comment, const bool &has_lyrics, const bool &show_bitrate, const int &bitrate) -> std::string
Definition misc.hpp:140
void UpdateSelectedIndex(int &index, int max_size, bool move_down)
Definition misc.hpp:497
auto RenderStatusBar(const std::string &status, const std::string &current_song_info, const std::string &additional_info, const std::string &year_info, InLimboColors &global_colors, const std::string &current_artist) -> Element
Definition misc.hpp:504
auto RenderSongMenu(const std::vector< Element > &items)
Definition misc.hpp:237
auto formatDiscTrackInfo(const int &disc_number, const int &track_number)
Definition misc.hpp:43
auto RenderVolumeBar(int volume, ftxui::Color volume_bar_col) -> Element
Definition misc.hpp:526
auto CreateMenu(const std::vector< std::string > *vecLines, int *currLine)
Definition misc.hpp:228
#define MAX_LENGTH_SONG_NAME
Definition misc.hpp:20
auto FormatTime(int seconds)
Definition misc.hpp:184
auto RenderSearchBar(std::string &user_input) -> Element
Definition misc.hpp:350
Color
Enumeration for predefined true colors.
Definition colors.hpp:24
@ White
Definition colors.hpp:26
@ LightBlue
Definition colors.hpp:32
Definition image_view.cpp:17
auto Scroller(Component child, int *external_selected, Color cursor_bg, Color inactive_menu_cursor_bg) -> Component
Factory function to create a Scroller component.
Definition scroller.cpp:119
auto parseProps() -> GlobalProps
Parses the global properties from the TOML configuration.
Definition properties.hpp:28
Implementation of a custom Scroller component for FTXUI.
Holds the components used for rendering the UI.
Definition state.hpp:22
Struct to hold global property settings.
Definition properties.hpp:14
Represents a collection of colors used in the application.
Definition colors.hpp:182
Struct to hold keybinding mappings.
Definition keymaps.hpp:15
unsigned int year
Definition taglib_parser.h:46
std::string filePath
Definition taglib_parser.h:51
Represents the current state of the song being played.
Definition state.hpp:40
std::string filePath
Definition state.hpp:56
void copyMetadata(const Metadata &metadata)
Copies the metadata from a given Metadata object.
Definition state.hpp:66
Represents the state of the song queue.
Definition state.hpp:227
Holds the state for searching artists and songs.
Definition state.hpp:209
unsigned int inode
Definition songmap.hpp:47
Metadata metadata
Definition songmap.hpp:48
Represents the state of a thread managed by ThreadManager.
Definition thread_manager.hpp:27
#define PARENT_DBG
Definition toml_parser.hpp:25
string getCachePath()
Definition toml_parser.hpp:83
auto parseTOMLField(string parent, string field) -> string_view
Parses a string field from the TOML configuration.
Definition toml_parser.hpp:144
#define STATUS_PAUSED
Definition ui_handler.hpp:16
#define MIN_DEBOUNCE_TIME_IN_MS
Definition ui_handler.hpp:27
#define SHOW_SONG_INFO_SCREEN
Definition ui_handler.hpp:23
#define SHOW_LYRICS_SCREEN
Definition ui_handler.hpp:21
#define SHOW_QUEUE_SCREEN
Definition ui_handler.hpp:22
#define SHOW_MAIN_UI
Definition ui_handler.hpp:19
#define SHOW_HELP_SCREEN
Definition ui_handler.hpp:20
#define SHOW_AUDIO_CONF_SCREEN
Definition ui_handler.hpp:24
#define STATUS_PLAYING
Definition ui_handler.hpp:15