diff --git a/src/rust/amadeus/Cargo.toml b/src/rust/amadeus/Cargo.toml index 7d2fc6fe956188a373e5fd4611dea1c08d18e6bd..794c4105759dcf9e1bef2ed9e03c7ad8dcda888a 100644 --- a/src/rust/amadeus/Cargo.toml +++ b/src/rust/amadeus/Cargo.toml @@ -8,7 +8,6 @@ license.workspace = true [dependencies] serde.workspace = true clap.workspace = true - iced.workspace = true commons = { path = "../commons" } diff --git a/src/rust/amadeus/src/action.rs b/src/rust/amadeus/src/action.rs new file mode 100644 index 0000000000000000000000000000000000000000..145aa96cd9263bf74fad3a69d06e9d4d6443c76f --- /dev/null +++ b/src/rust/amadeus/src/action.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, Copy)] +#[non_exhaustive] +pub enum Action { + TogglePause, + SetPause(bool), + PlayNext, + PlayPrev, +} diff --git a/src/rust/amadeus/src/amadeus.rs b/src/rust/amadeus/src/amadeus.rs index c319547f60c129e737503f0efb6bfcb77c5c7491..66378b94bd155cac94194f9e1b9e255ba7e8e713 100644 --- a/src/rust/amadeus/src/amadeus.rs +++ b/src/rust/amadeus/src/amadeus.rs @@ -1,31 +1,39 @@ -use crate::{client, AmadeusConfig}; +use crate::{action, buttons::queue_control, client, AmadeusConfig}; use iced::{ - executor, subscription, widget::column, Alignment, Application, Command, Element, Event, - Subscription, Theme, + executor, subscription, + widget::{column, row}, + Alignment, Application, Command, Element, Event, Subscription, Theme, }; #[derive(Debug)] pub struct Amadeus { client: client::Client, + queue_control: queue_control::QueueControl, flags: AmadeusConfig, } #[derive(Debug, Clone)] pub enum Message { + None, + Disconnect, Event(Event), + Notification(action::Action), + Action(action::Action), Client(client::Message), + QueueControl(queue_control::Message), } impl Application for Amadeus { type Executor = executor::Default; - type Theme = Theme; type Message = Message; + type Theme = Theme; type Flags = AmadeusConfig; fn new(flags: Self::Flags) -> (Self, Command<Self::Message>) { ( Self { client: client::Client::new(), + queue_control: queue_control::QueueControl::new(), flags, }, Command::none(), @@ -36,38 +44,82 @@ impl Application for Amadeus { String::from("Amadeus") } - fn theme(&self) -> Theme { - Theme::Dark - } - fn update(&mut self, message: Self::Message) -> Command<Self::Message> { match message { + Message::None => Command::none(), + + // We dispatch the multiple messages to handle them one by one. + Message::Client(client::Message::Multiple(messages)) => Command::batch( + messages + .into_iter() + .map(|message| self.update(Message::Client(message))), + ), + + // The disconnect message is a special one, we propage it + // everywhere. + Message::Disconnect => { + self.client.update(&self.flags, client::Message::Disconnect); + self.queue_control + .update(queue_control::Message::Disconnect); + Command::none() + } + Message::Event(_) => Command::none(), - Message::Client(client::Message::Multiple(messages)) => { - Command::batch(messages.into_iter().map(|message| { - self.client - .update(&self.flags, message) - .map(Message::Client) - })) + + Message::QueueControl(message) => { + self.queue_control + .update(message) + .map(|message| match message { + queue_control::Message::Action(message) => Message::Action(message), + queue_control::Message::SetState(_) => Message::None, + queue_control::Message::Disconnect => Message::Disconnect, + }) + } + + Message::Client(message) => { + self.client + .update(&self.flags, message) + .map(|message| match message { + client::Message::Disconnect => Message::Disconnect, + client::Message::Action(message) => Message::Notification(message), + client::Message::UpdateState(state) => { + Message::QueueControl(queue_control::Message::SetState(state)) + } + message => Message::Client(message), + }) + } + + // On notifications we update the queue controler. + Message::Notification(message) => self.update(Message::QueueControl( + queue_control::Message::Action(message), + )), + + // On an action, we ask the client to perform it, it will return a + // notification latter. + Message::Action(message) => { + self.update(Message::Client(client::Message::Action(message))) } - Message::Client(message) => self - .client - .update(&self.flags, message) - .map(Message::Client), } } + fn view(&self) -> Element<Self::Message> { + column![row![ + self.queue_control.view().map(Message::QueueControl), + self.client.view().map(Message::Client) + ]] + .padding(20) + .align_items(Alignment::Center) + .into() + } + + fn theme(&self) -> Theme { + Theme::Dark + } + fn subscription(&self) -> Subscription<Self::Message> { Subscription::batch([ subscription::events().map(Message::Event), self.client.subscription().map(Message::Client), ]) } - - fn view(&self) -> Element<Self::Message> { - column![self.client.view().map(Message::Client)] - .padding(20) - .align_items(Alignment::Center) - .into() - } } diff --git a/src/rust/amadeus/src/buttons/mod.rs b/src/rust/amadeus/src/buttons/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb269fdea7df80c3c621792ba325086d245a3110 --- /dev/null +++ b/src/rust/amadeus/src/buttons/mod.rs @@ -0,0 +1 @@ +pub mod queue_control; diff --git a/src/rust/amadeus/src/buttons/queue_control.rs b/src/rust/amadeus/src/buttons/queue_control.rs new file mode 100644 index 0000000000000000000000000000000000000000..edcf7420a2c82c3b208c6d701ba2114ecdee029f --- /dev/null +++ b/src/rust/amadeus/src/buttons/queue_control.rs @@ -0,0 +1,85 @@ +use crate::action; +use iced::{widget::text, Command, Element}; + +#[derive(Debug, Clone, Copy)] +pub enum Message { + Action(action::Action), + SetState(amalib::LektorState), + Disconnect, +} + +#[derive(Debug, Clone, Copy)] +struct State(Option<amalib::LektorState>); + +#[derive(Debug, Clone, Copy)] +pub struct QueueControl { + state: State, +} + +impl QueueControl { + pub fn new() -> Self { + Self { state: State(None) } + } + + pub fn update(&mut self, message: Message) -> Command<Message> { + use amalib::LektorState::*; + match message { + Message::Disconnect => { + self.state = State(None); + Command::none() + } + + Message::SetState(state) => { + self.state = State(Some(state)); + Command::none() + } + + Message::Action(message) => match message { + action::Action::TogglePause => { + self.state = State(Some(match self.state.0 { + Some(Pause(pos)) => Play(pos), + Some(Play(pos)) => Pause(pos), + _ => return Command::none(), + })); + Command::none() + } + + action::Action::SetPause(pause) => { + self.state = State(Some(match self.state.0 { + Some(Pause(pos)) if !pause => Play(pos), + Some(Play(pos)) if pause => Pause(pos), + _ => return Command::none(), + })); + Command::none() + } + + action::Action::PlayNext => match &mut self.state.0 { + Some(Pause(pos) | Play(pos)) => { + *pos = pos.saturating_add(1); + Command::none() + } + _ => Command::none(), + }, + + action::Action::PlayPrev => match &mut self.state.0 { + Some(Pause(pos) | Play(pos)) => { + *pos = pos.saturating_sub(1); + Command::none() + } + _ => Command::none(), + }, + }, + } + } + + pub fn view(&self) -> Element<Message> { + const SIZE: u16 = 20; + use amalib::LektorState::*; + match self.state.0 { + Some(Pause(pos)) => text(format!("pause({pos})")).size(SIZE).into(), + Some(Play(pos)) => text(format!("play({pos})")).size(SIZE).into(), + Some(Stopped) => text("stopped").size(SIZE).into(), + None => text("unk").size(SIZE).into(), + } + } +} diff --git a/src/rust/amadeus/src/client.rs b/src/rust/amadeus/src/client.rs index 9c4a600b9c94875409d6831ae1d776930f166af4..b7c2dc496f8c8035c43ea99585e0c67f361570ed 100644 --- a/src/rust/amadeus/src/client.rs +++ b/src/rust/amadeus/src/client.rs @@ -1,4 +1,4 @@ -use crate::AmadeusConfig; +use crate::{action, AmadeusConfig}; use commons::{log, Report}; use iced::{ futures::{channel::mpsc, join, lock::Mutex, StreamExt}, @@ -27,26 +27,20 @@ impl std::fmt::Display for Address { } } -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum Action { - TogglePause, - SetPause(bool), - PlayNext, - PlayPrev, -} - #[derive(Debug, Clone)] pub enum Message { Connect, Connected(Arc<Mutex<amalib::AmaClient>>), - WorkerReady(mpsc::Sender<Action>), + WorkerReady(mpsc::Sender<action::Action>), /// Got an error... - Error(Arc<Report<amalib::AmaClientError>>), + Errored(Arc<Report<amalib::AmaClientError>>), /// Perform an action... - Action(Action), + Action(action::Action), + + /// Got an update of the state from lektord. + UpdateState(amalib::LektorState), /// Disconnect from the lektord instance. Disconnect, @@ -59,17 +53,20 @@ pub enum Message { } #[derive(Debug)] -pub enum State { +enum State { Offline, Online(Arc<Mutex<amalib::AmaClient>>), - OnlineAndReady(Arc<Mutex<amalib::AmaClient>>, mpsc::Sender<Action>), + OnlineAndReady(Arc<Mutex<amalib::AmaClient>>, mpsc::Sender<action::Action>), AskConnection(Address), } #[derive(Debug)] -pub enum WorkerState { +enum WorkerState { Starting(Arc<Mutex<amalib::AmaClient>>), - Ready(Arc<Mutex<amalib::AmaClient>>, mpsc::Receiver<Action>), + Ready( + Arc<Mutex<amalib::AmaClient>>, + mpsc::Receiver<action::Action>, + ), } #[derive(Debug)] @@ -86,30 +83,38 @@ impl Client { pub fn update(&mut self, flags: &AmadeusConfig, message: Message) -> Command<Message> { match (message, &mut self.state) { + // We are connected to the lektord instance, but the worker + // subscriber is not ready yet. (Message::Connected(client), State::AskConnection(addr)) => { log::info!("connected to lektord at {addr:?}"); self.state = State::Online(client); Command::none() } + // We propagate the update status message. + (message @ Message::UpdateState(_), _) => Command::perform(async { () }, |()| message), + + // The worker subscriber is now ready! (Message::WorkerReady(sender), State::Online(client)) => { log::info!("connected to lektord and worker is ready"); self.state = State::OnlineAndReady(client.clone(), sender); Command::none() } - (Message::Error(err), _) => { + // An error occured, we disconect everything. + (Message::Errored(err), _) => { log::error!("{err}"); - self.state = State::Offline; - Command::none() + Command::perform(async { () }, |()| Message::Disconnect) } + // We disconnect everything! (Message::Disconnect, _) => { log::info!("asked a disconnection..."); self.state = State::Offline; Command::none() } + // We are connected and the worker is ready, we perform an action! (Message::Action(action), State::OnlineAndReady(_, sender)) => { if let Err(err) = sender.start_send(action) { log::error!("failed to send action to worker: {err}"); @@ -117,6 +122,8 @@ impl Client { Command::none() } + // We want to connect and we are offline, we try to connect the + // client to the lektord instance. (Message::Connect, State::Offline) => { let address = flags.remote.address.clone(); self.state = State::AskConnection(address.clone()); @@ -131,12 +138,14 @@ impl Client { .await }, |res| match res { - Err(err) => Message::Error(Arc::new(err)), + Err(err) => Message::Errored(Arc::new(err)), Ok(client) => Message::Connected(Arc::new(Mutex::new(client)).clone()), }, ) } + // Ignore and log the discarded message. It should not happen, it + // means that we missed something above. (message, _) => { log::error!("ignored message {message:?}"); Command::none() @@ -168,8 +177,16 @@ impl Client { match state { WorkerState::Starting(client) => { log::info!("worker is ready..."); - let (s, r) = mpsc::channel::<Action>(crate::CHANNEL_SIZE); - (Some(Message::WorkerReady(s)), WorkerState::Ready(client, r)) + let (s, r) = mpsc::channel::<action::Action>(crate::CHANNEL_SIZE); + let status = client.lock().await.get_status().await; + let message = match status { + Ok(status) => Message::Multiple(vec![ + Message::WorkerReady(s), + Message::UpdateState(status), + ]), + Err(err) => Message::Errored(Arc::new(err)), + }; + (Some(message), WorkerState::Ready(client, r)) } WorkerState::Ready(client, mut receiver) => { let poll_notifs = async { @@ -177,15 +194,16 @@ impl Client { notifs.into_iter().map(Message::LektorIdleNotification) }; let poll_actions = async { + use action::Action::*; let message = match receiver.select_next_some().await { - Action::SetPause(true) => client.lock().await.set_pause_state().await, - Action::SetPause(false) => client.lock().await.set_pause_state().await, - Action::TogglePause => client.lock().await.toggle_pause_state().await, + SetPause(true) => client.lock().await.set_pause_state().await, + SetPause(false) => client.lock().await.set_pause_state().await, + TogglePause => client.lock().await.toggle_pause_state().await, _ => Ok(()), }; match message { Ok(()) => None, - Err(err) => Some(Message::Error(Arc::new(err))), + Err(err) => Some(Message::Errored(Arc::new(err))), } }; let (notifs, actions) = join!(poll_notifs, poll_actions); diff --git a/src/rust/amadeus/src/main.rs b/src/rust/amadeus/src/main.rs index 837a1bbe00f07675e35b0e96163c17a04cfce9d0..a24cf4b31f5f43fbe8834011a1519dd379ff38f3 100644 --- a/src/rust/amadeus/src/main.rs +++ b/src/rust/amadeus/src/main.rs @@ -4,7 +4,9 @@ use commons::log; use iced::{Application, Settings}; use serde::{Deserialize, Serialize}; +mod action; mod amadeus; +mod buttons; mod client; pub(crate) const CHANNEL_SIZE: usize = 128; diff --git a/src/rust/amalib/src/amadeus.rs b/src/rust/amalib/src/amadeus.rs index 59062a6049db8100bb0ac0d5ce0573d3c4f3e8df..7368647305e3293e77d0ab591d1763c46195bb24 100644 --- a/src/rust/amalib/src/amadeus.rs +++ b/src/rust/amalib/src/amadeus.rs @@ -146,6 +146,13 @@ impl AmaClient { self.idle.get_notifications().await } + pub async fn get_status(&mut self) -> StackedResult<LektorState, AmaClientError> { + let status = send!(self.connexion => LektorQuery::PlaybackStatus; + LektorResponse::PlaybackStatus(status) => { status } + ); + Ok(status.state()) + } + pub async fn toggle_pause_state(&mut self) -> StackedResult<(), AmaClientError> { match send!(self.connexion => LektorQuery::PlaybackStatus; LektorResponse::PlaybackStatus(status) => { status.state() }