use self::browser::Browser;
use self::logger::Logger;
use self::playlist::Playlist;
use super::AppCallback;
use super::component::actionhandler::{
ActionHandler, ComponentEffect, DominantKeyRouter, KeyHandleAction, KeyRouter, Scrollable,
TextHandler, YoutuiEffect, apply_action_mapped, get_visible_keybinds_as_readable_iter,
handle_key_stack,
};
use super::server::{IncreaseVolume, SetVolume};
use super::structures::*;
use crate::async_rodio_sink::{SeekDirection, VolumeUpdate};
use crate::config::Config;
use crate::config::keymap::Keymap;
use crate::keyaction::{DisplayableKeyAction, DisplayableMode};
use crate::widgets::ScrollingTableState;
use action::{AppAction, ListAction, PAGE_KEY_LINES, SEEK_AMOUNT, TextEntryAction};
use async_callback_manager::{AsyncTask, Constraint};
use crossterm::event::{Event, KeyEvent};
use itertools::Either;
use std::time::Duration;
pub mod action;
pub mod browser;
pub mod draw;
pub mod draw_media_controls;
mod footer;
mod header;
pub mod logger;
pub mod playlist;
#[derive(Debug)]
pub enum WindowContext {
Browser,
Playlist,
Logs,
}
pub struct YoutuiWindow {
context: WindowContext,
prev_context: WindowContext,
playlist: Playlist,
browser: Browser,
logger: Logger,
config: Config,
key_stack: Vec<KeyEvent>,
help: HelpMenu,
tick: u64,
}
impl_youtui_component!(YoutuiWindow);
pub struct HelpMenu {
pub shown: bool,
cur: usize,
len: usize,
pub widget_state: ScrollingTableState,
}
impl HelpMenu {
fn new() -> Self {
HelpMenu {
shown: Default::default(),
cur: Default::default(),
len: Default::default(),
widget_state: Default::default(),
}
}
}
impl_youtui_component!(HelpMenu);
impl Scrollable for HelpMenu {
fn increment_list(&mut self, amount: isize) {
self.cur = self
.cur
.saturating_add_signed(amount)
.min(self.len.saturating_sub(1));
}
fn is_scrollable(&self) -> bool {
true
}
}
impl DominantKeyRouter<AppAction> for YoutuiWindow {
fn dominant_keybinds_active(&self) -> bool {
self.help.shown
|| match self.context {
WindowContext::Browser => self.browser.dominant_keybinds_active(),
WindowContext::Playlist => false,
WindowContext::Logs => false,
}
}
fn get_dominant_keybinds<'a>(
&self,
config: &'a Config,
) -> impl Iterator<Item = &'a Keymap<AppAction>> + 'a {
if self.help.shown {
return Either::Right(Either::Right(
[&config.keybinds.help, &config.keybinds.list].into_iter(),
));
}
match self.context {
WindowContext::Browser => {
Either::Left(Either::Left(self.browser.get_dominant_keybinds(config)))
}
WindowContext::Playlist => {
Either::Left(Either::Right(self.playlist.get_active_keybinds(config)))
}
WindowContext::Logs => {
Either::Right(Either::Left(self.logger.get_active_keybinds(config)))
}
}
}
}
impl Scrollable for YoutuiWindow {
fn increment_list(&mut self, amount: isize) {
if self.help.shown {
return self.help.increment_list(amount);
}
match self.context {
WindowContext::Browser => self.browser.increment_list(amount),
WindowContext::Playlist => self.playlist.increment_list(amount),
WindowContext::Logs => (),
}
}
fn is_scrollable(&self) -> bool {
self.help.shown
|| match self.context {
WindowContext::Browser => self.browser.is_scrollable(),
WindowContext::Playlist => self.playlist.is_scrollable(),
WindowContext::Logs => false,
}
}
}
impl KeyRouter<AppAction> for YoutuiWindow {
fn get_active_keybinds<'a>(
&self,
config: &'a Config,
) -> impl Iterator<Item = &'a Keymap<AppAction>> + 'a {
let kb = if self.is_scrollable() {
Either::Left(std::iter::once(&config.keybinds.list))
} else {
Either::Right(std::iter::empty())
};
if self.dominant_keybinds_active() {
return Either::Right(Either::Right(self.get_dominant_keybinds(config).chain(kb)));
}
let kb = kb.chain(std::iter::once(&config.keybinds.global));
let kb = if self.is_text_handling() {
Either::Left(kb.chain(std::iter::once(&config.keybinds.text_entry)))
} else {
Either::Right(kb)
};
match self.context {
WindowContext::Browser => Either::Left(Either::Left(
kb.chain(self.browser.get_active_keybinds(config)),
)),
WindowContext::Playlist => Either::Left(Either::Right(
kb.chain(self.playlist.get_active_keybinds(config)),
)),
WindowContext::Logs => Either::Right(Either::Left(
kb.chain(self.logger.get_active_keybinds(config)),
)),
}
}
fn get_all_keybinds<'a>(
&self,
config: &'a Config,
) -> impl Iterator<Item = &'a Keymap<AppAction>> + 'a {
std::iter::once(&config.keybinds.global)
.chain(self.browser.get_all_keybinds(config))
.chain(self.playlist.get_all_keybinds(config))
.chain(self.logger.get_all_keybinds(config))
}
}
impl TextHandler for YoutuiWindow {
fn is_text_handling(&self) -> bool {
if self.help.shown {
return false;
}
match self.context {
WindowContext::Browser => self.browser.is_text_handling(),
WindowContext::Playlist => self.playlist.is_text_handling(),
WindowContext::Logs => self.logger.is_text_handling(),
}
}
fn get_text(&self) -> std::option::Option<&str> {
match self.context {
WindowContext::Browser => self.browser.get_text(),
WindowContext::Playlist => self.playlist.get_text(),
WindowContext::Logs => self.logger.get_text(),
}
}
fn replace_text(&mut self, text: impl Into<String>) {
match self.context {
WindowContext::Browser => self.browser.replace_text(text),
WindowContext::Playlist => self.playlist.replace_text(text),
WindowContext::Logs => self.logger.replace_text(text),
}
}
fn clear_text(&mut self) -> bool {
match self.context {
WindowContext::Browser => self.browser.clear_text(),
WindowContext::Playlist => self.playlist.clear_text(),
WindowContext::Logs => self.logger.clear_text(),
}
}
fn handle_text_event_impl(&mut self, event: &Event) -> Option<ComponentEffect<Self>> {
match self.context {
WindowContext::Browser => self
.browser
.handle_text_event_impl(event)
.map(|effect| effect.map_frontend(|this: &mut YoutuiWindow| &mut this.browser)),
WindowContext::Playlist => self
.playlist
.handle_text_event_impl(event)
.map(|effect| effect.map_frontend(|this: &mut YoutuiWindow| &mut this.playlist)),
WindowContext::Logs => self
.logger
.handle_text_event_impl(event)
.map(|effect| effect.map_frontend(|this: &mut YoutuiWindow| &mut this.logger)),
}
}
}
impl ActionHandler<AppAction> for YoutuiWindow {
fn apply_action(&mut self, action: AppAction) -> impl Into<YoutuiEffect<Self>> {
match action {
AppAction::VolUp => {
return Into::<YoutuiEffect<Self>>::into(self.handle_increase_volume(5));
}
AppAction::VolDown => return self.handle_increase_volume(-5).into(),
AppAction::NextSong => return self.handle_next().into(),
AppAction::PrevSong => return self.handle_prev().into(),
AppAction::SeekForward => {
return self.handle_seek(SEEK_AMOUNT, SeekDirection::Forward).into();
}
AppAction::SeekBack => {
return self.handle_seek(SEEK_AMOUNT, SeekDirection::Back).into();
}
AppAction::ToggleHelp => self.toggle_help(),
AppAction::Quit => return (AsyncTask::new_no_op(), Some(AppCallback::Quit)).into(),
AppAction::ViewLogs => self.handle_change_context(WindowContext::Logs),
AppAction::PlayPause => return self.pauseplay().into(),
AppAction::Log(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.logger);
}
AppAction::Playlist(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.playlist);
}
AppAction::Help(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.help);
}
AppAction::Browser(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::Filter(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::Sort(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::BrowserArtists(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::BrowserSearch(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::BrowserArtistSongs(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::BrowserSongs(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::BrowserPlaylists(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::BrowserPlaylistSongs(a) => {
return apply_action_mapped(self, a, |this: &mut Self| &mut this.browser);
}
AppAction::TextEntry(a) => return self.handle_text_entry_action(a).into(),
AppAction::List(a) => return self.handle_list_action(a).into(),
AppAction::NoOp => (),
};
AsyncTask::new_no_op().into()
}
}
impl YoutuiWindow {
pub fn new(config: Config) -> (YoutuiWindow, ComponentEffect<YoutuiWindow>) {
let (playlist, task) = Playlist::new();
let this = YoutuiWindow {
context: WindowContext::Browser,
prev_context: WindowContext::Browser,
playlist,
config,
browser: Browser::new(),
logger: Logger::new(),
key_stack: Vec::new(),
help: HelpMenu::new(),
tick: 0,
};
(
this,
task.map_frontend(|this: &mut Self| &mut this.playlist),
)
}
pub fn get_help_list_items(&self) -> impl Iterator<Item = DisplayableKeyAction<'_>> {
match self.context {
WindowContext::Browser => Either::Left(Either::Right(
get_visible_keybinds_as_readable_iter(self.browser.get_all_keybinds(&self.config)),
)),
WindowContext::Playlist => Either::Right(get_visible_keybinds_as_readable_iter(
self.playlist.get_all_keybinds(&self.config),
)),
WindowContext::Logs => Either::Left(Either::Left(
get_visible_keybinds_as_readable_iter(self.logger.get_all_keybinds(&self.config)),
)),
}
.chain(get_visible_keybinds_as_readable_iter(
std::iter::once(&self.config.keybinds.global)
.chain(std::iter::once(&self.config.keybinds.list))
.chain(std::iter::once(&self.config.keybinds.text_entry)),
))
}
pub async fn handle_crossterm_event(
&mut self,
event: crossterm::event::Event,
) -> YoutuiEffect<Self> {
if let Some(effect) = self.try_handle_text(&event) {
return effect.into();
};
match event {
Event::Key(k) => return self.handle_key_event(k),
Event::Mouse(m) => return self.handle_mouse_event(m).into(),
other => tracing::warn!("Received unimplemented {:?} event", other),
}
AsyncTask::new_no_op().into()
}
pub async fn handle_media_controls_event(
&mut self,
event: souvlaki::MediaControlEvent,
) -> YoutuiEffect<Self> {
let convert_dir = |dir| match dir {
souvlaki::SeekDirection::Forward => SeekDirection::Forward,
souvlaki::SeekDirection::Backward => SeekDirection::Back,
};
match event {
souvlaki::MediaControlEvent::Play => return self.resume().into(),
souvlaki::MediaControlEvent::Pause => return self.pause().into(),
souvlaki::MediaControlEvent::Toggle => return self.pauseplay().into(),
souvlaki::MediaControlEvent::Next => return self.handle_next().into(),
souvlaki::MediaControlEvent::Previous => return self.handle_prev().into(),
souvlaki::MediaControlEvent::Stop => return self.stop().into(),
souvlaki::MediaControlEvent::Seek(seek_direction) => {
return self
.handle_seek(SEEK_AMOUNT, convert_dir(seek_direction))
.into();
}
souvlaki::MediaControlEvent::SeekBy(seek_direction, duration) => {
return self
.handle_seek(duration, convert_dir(seek_direction))
.into();
}
souvlaki::MediaControlEvent::SetPosition(media_position) => {
return self.handle_seek_to(media_position.0).into();
}
souvlaki::MediaControlEvent::SetVolume(v) => {
return self.handle_set_volume((v * 100.0) as u8).into();
}
souvlaki::MediaControlEvent::Quit => {
return (AsyncTask::new_no_op(), Some(AppCallback::Quit)).into();
}
souvlaki::MediaControlEvent::OpenUri(_) => {
tracing::info!("Received intentionally unhandled event {:?}", event)
}
souvlaki::MediaControlEvent::Raise => {
tracing::info!("Received intentionally unhandled event {:?}", event)
}
}
AsyncTask::new_no_op().into()
}
pub async fn handle_tick(&mut self) {
self.tick = self.tick.wrapping_add(1);
self.playlist.handle_tick().await;
}
fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) -> YoutuiEffect<Self> {
self.key_stack.push(key_event);
self.global_handle_key_stack()
}
fn handle_mouse_event(
&mut self,
mouse_event: crossterm::event::MouseEvent,
) -> ComponentEffect<Self> {
tracing::warn!("Received unimplemented {:?} mouse event", mouse_event);
AsyncTask::new_no_op()
}
pub fn handle_list_action(&mut self, action: ListAction) -> ComponentEffect<Self> {
if self.is_scrollable() {
match action {
ListAction::Up => self.increment_list(-1),
ListAction::Down => self.increment_list(1),
ListAction::PageUp => self.increment_list(-PAGE_KEY_LINES),
ListAction::PageDown => self.increment_list(PAGE_KEY_LINES),
}
}
AsyncTask::new_no_op()
}
pub fn handle_text_entry_action(&mut self, action: TextEntryAction) -> ComponentEffect<Self> {
if !self.is_text_handling() {
return AsyncTask::new_no_op();
}
match self.context {
WindowContext::Browser => self
.browser
.handle_text_entry_action(action)
.map_frontend(|this: &mut Self| &mut this.browser),
WindowContext::Playlist => AsyncTask::new_no_op(),
WindowContext::Logs => AsyncTask::new_no_op(),
}
}
pub fn pauseplay(&mut self) -> ComponentEffect<Self> {
self.playlist
.pauseplay()
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn resume(&mut self) -> ComponentEffect<Self> {
self.playlist
.resume()
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn pause(&mut self) -> ComponentEffect<Self> {
self.playlist
.pause()
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn stop(&mut self) -> ComponentEffect<Self> {
self.playlist
.stop()
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn handle_next(&mut self) -> ComponentEffect<Self> {
self.playlist
.handle_next()
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn handle_prev(&mut self) -> ComponentEffect<Self> {
self.playlist
.handle_previous()
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn handle_increase_volume(&mut self, inc: i8) -> ComponentEffect<Self> {
self.increase_volume(inc);
AsyncTask::new_future_option(
IncreaseVolume(inc),
HandleVolumeUpdate,
Some(Constraint::new_block_same_type()),
)
}
pub fn handle_set_volume(&mut self, new_vol: u8) -> ComponentEffect<Self> {
self.set_volume(new_vol);
AsyncTask::new_future_option(
SetVolume(new_vol),
HandleVolumeUpdate,
Some(Constraint::new_block_same_type()),
)
}
pub fn handle_seek(
&mut self,
duration: Duration,
direction: SeekDirection,
) -> ComponentEffect<Self> {
self.playlist
.handle_seek(duration, direction)
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn handle_seek_to(&mut self, position: Duration) -> ComponentEffect<Self> {
self.playlist
.handle_seek_to(position)
.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn handle_volume_update(&mut self, update: VolumeUpdate) {
self.playlist.handle_volume_update(update)
}
pub fn handle_add_songs_to_playlist(
&mut self,
song_list: Vec<ListSong>,
) -> ComponentEffect<Self> {
let (_, effect) = self.playlist.push_song_list(song_list);
effect.map_frontend(|this: &mut Self| &mut this.playlist)
}
pub fn handle_add_songs_to_playlist_and_play(
&mut self,
song_list: Vec<ListSong>,
) -> ComponentEffect<Self> {
let effect = self.playlist.reset();
let (id, next_effect) = self.playlist.push_song_list(song_list);
effect
.push(next_effect)
.push(self.playlist.play_song_id(id))
.map_frontend(|this: &mut Self| &mut this.playlist)
}
fn global_handle_key_stack(&mut self) -> YoutuiEffect<Self> {
match handle_key_stack(self.get_active_keybinds(&self.config), &self.key_stack) {
KeyHandleAction::Action(a) => {
let effect = self.apply_action(a).into();
self.key_stack.clear();
effect
}
KeyHandleAction::Mode { .. } => AsyncTask::new_no_op().into(),
KeyHandleAction::NoMap => {
self.key_stack.clear();
AsyncTask::new_no_op().into()
}
}
}
fn key_pending(&self) -> bool {
!self.key_stack.is_empty()
}
pub fn toggle_help(&mut self) {
if self.help.shown {
self.help.shown = false;
} else {
self.help.shown = true;
self.help.cur = 0;
self.help.len = self.get_help_list_items().count();
}
}
fn increase_volume(&mut self, inc: i8) {
self.playlist.increase_volume(inc);
}
fn set_volume(&mut self, new_vol: u8) {
self.playlist.set_volume(new_vol);
}
pub fn handle_change_context(&mut self, new_context: WindowContext) {
std::mem::swap(&mut self.context, &mut self.prev_context);
self.context = new_context;
}
fn _revert_context(&mut self) {
std::mem::swap(&mut self.context, &mut self.prev_context);
}
fn get_cur_displayable_mode(
&self,
) -> Option<DisplayableMode<'_, impl Iterator<Item = DisplayableKeyAction<'_>>>> {
let KeyHandleAction::Mode { name, keys } =
handle_key_stack(self.get_active_keybinds(&self.config), &self.key_stack)
else {
return None;
};
let displayable_commands = keys
.iter()
.map(|(kb, kt)| DisplayableKeyAction::from_keybind_and_action_tree(kb, kt));
Some(DisplayableMode {
displayable_commands,
description: name.into(),
})
}
}
#[derive(Debug, PartialEq)]
struct HandleVolumeUpdate;
impl_youtui_task_handler!(
HandleVolumeUpdate,
VolumeUpdate,
YoutuiWindow,
|_, update| |this: &mut YoutuiWindow| {
YoutuiWindow::handle_volume_update(this, update);
AsyncTask::new_no_op()
}
);