From 475a9148dec0522de7313b4d01aef8f53516b0e9 Mon Sep 17 00:00:00 2001 From: Kubat <mael.martin31@gmail.com> Date: Tue, 7 Nov 2023 19:11:00 +0100 Subject: [PATCH] AMADEUS: Keep track of connection state --- amadeus/src/app.rs | 282 +++++++++++------- amadeus/src/components/karalist.rs | 19 +- amadeus/src/components/mainpanel/playlists.rs | 17 +- amadeus/src/components/mod.rs | 8 - amadeus/src/components/sidebar.rs | 8 + amadeus/src/message.rs | 65 +++- 6 files changed, 271 insertions(+), 128 deletions(-) diff --git a/amadeus/src/app.rs b/amadeus/src/app.rs index 852ca543..3cc2ecee 100644 --- a/amadeus/src/app.rs +++ b/amadeus/src/app.rs @@ -13,7 +13,7 @@ use crate::{ store::KaraStore, style_sheet::{sizes::*, ModalStyleSheet, SideBarSplit}, }; -use anyhow::Result; +use anyhow::{anyhow, Result}; use futures::{stream, StreamExt, TryStreamExt}; use iced::{ keyboard::{Event as KbdEvent, KeyCode}, @@ -22,8 +22,8 @@ use iced::{ }; use iced_aw::{native::Split, split}; use lektor_lib::{requests::*, ConnectConfigPtr}; -use lektor_payloads::{KId, Kara, KaraFilter, PlayState, PlayStateWithCurrent, Priority}; -use lektor_utils::{config::UserConfig, log}; +use lektor_payloads::{Infos, KId, Kara, KaraFilter, PlayState, PlayStateWithCurrent, Priority}; +use lektor_utils::{config::UserConfig, either, log}; use std::{future::Future, sync::Arc}; /// The main application. @@ -31,6 +31,9 @@ pub struct Amadeus { /// Was the config loaded for the first time? is_init: bool, + /// Is amadeus connected to lektord + is_connected: bool, + /// The last recieved instant, to calculate the delta and maybe time some animations. last_instant: iced::time::Instant, @@ -89,12 +92,13 @@ impl Default for Amadeus { Self { // Manual defaults. last_instant: iced::time::Instant::now(), + kara_store: Arc::new(KaraStore::new()), sidebar_size: Some(300), show_main_panel: false, + is_connected: false, + is_init: false, // Has default. - kara_store: Arc::new(KaraStore::new()), - is_init: Default::default(), connect_config: Default::default(), config: Default::default(), sidebar: Default::default(), @@ -117,10 +121,9 @@ impl Amadeus { cb: impl FnOnce(Arc<Kara>) -> Message + 'static + Send, ) -> Command<Message> { let store = self.kara_store.clone(); - Command::perform( - async move { store.get(id).await.map_err(|err| log::error!("{err}")).ok() }, - |kara| kara.map(cb).unwrap_or_default(), - ) + Command::perform(async move { store.get(id).await }, |kara| { + kara.map(cb).into() + }) } fn get_history(&self) -> impl Future<Output = Option<Vec<Arc<Kara>>>> { @@ -184,9 +187,18 @@ impl Amadeus { fn handle_config_message(&mut self, config: config::Message) -> Command<Message> { if let config::Message::TryConnect = config { return Command::perform(get_infos(self.connect_config.clone()), |res| { - Message::ConfigMessage(config::Message::Infos( - res.map_err(|err| log::error!("{err}")).ok(), - )) + Message::from_err_ors( + res.map(|info| { + Message::from_iter([ + Message::ConfigMessage(config::Message::Infos(Some(info))), + Message::ConnectionStatus(true), + ]) + }), + [ + Message::ConfigMessage(config::Message::Infos(None)), + Message::ConnectionStatus(false), + ], + ) }); } @@ -317,7 +329,8 @@ impl Amadeus { send(remove_from_playlist(cfg, name, KaraFilter::KId(id))) } playlists::Request::ChangeView => { - Command::perform(async {}, |_| Message::MainPanelDisplay(Default::default())) + let show = either!(self.is_connected => MainPanelDisplay::MainPanel(mainpanel::Show::Queue); MainPanelDisplay::Config); + Command::perform(async {}, |_| Message::MainPanelDisplay(show)) } }, @@ -440,18 +453,21 @@ impl Amadeus { /// Perform the init ping if needed fn command_init_ping(&self) -> Command<<Self as Application>::Message> { - if !self.config.amadeus.open_config_if_init_ping_failed { - return Command::none(); - } - Command::perform(get_infos(self.connect_config.clone()), |res| match res { - Ok(infos) => Message::ConfigMessage(config::Message::Infos(Some(infos))), - Err(err) => { - log::error!("{err}"); - Message::from_iter([ - Message::MainPanelDisplay(MainPanelDisplay::Config), - Message::ConfigMessage(config::Message::Infos(None)), - ]) - } + let flag = self.config.amadeus.open_config_if_init_ping_failed; + Command::perform(get_infos(self.connect_config.clone()), move |res| { + Message::from_err_or_elses( + res.map(|infos| { + Message::from_iter([ + Message::ConfigMessage(config::Message::Infos(Some(infos))), + Message::ConnectionStatus(true), + ]) + }), + || { + [Message::ConfigMessage(config::Message::Infos(None))] + .into_iter() + .chain(flag.then_some(Message::MainPanelDisplay(MainPanelDisplay::Config))) + }, + ) }) } @@ -465,24 +481,20 @@ impl Amadeus { match req { RefreshRequest::Playlists => { Command::perform(get_playlists(self.connect_config.clone()), |res| { - res.map_err(|err| log::error!("{err}")) - .map(|plts| { - let main_keep = Message::from(playlists::Message::KeepPlaylists( - plts.iter().map(|(name, _)| name.as_ref().into()).collect(), - )); - let side_keep = sidebar::Message::from_iter( - plts.iter().map(|(name, _)| name.clone()), - ) - .into(); - let update_each = plts.into_iter().map(|(name, infos)| { - let name = name.as_ref().into(); - Message::from(playlists::Message::UpdatePlaylistInfos(name, infos)) - }); - Message::from_iter( - [main_keep, side_keep].into_iter().chain(update_each), - ) - }) - .unwrap_or_default() + res.map(|plts| { + let main_keep = Message::from(playlists::Message::KeepPlaylists( + plts.iter().map(|(name, _)| name.as_ref().into()).collect(), + )); + let side_keep = + sidebar::Message::from_iter(plts.iter().map(|(name, _)| name.clone())) + .into(); + let update_each = plts.into_iter().map(|(name, infos)| { + let name = name.as_ref().into(); + Message::from(playlists::Message::UpdatePlaylistInfos(name, infos)) + }); + Message::from_iter([main_keep, side_keep].into_iter().chain(update_each)) + }) + .into() }) } @@ -524,10 +536,8 @@ impl Application for Amadeus { fn new(config: Self::Flags) -> (Self, Command<Self::Message>) { let init_events = Command::batch([ iced::font::load(iced_aw::graphics::icons::ICON_FONT_BYTES).map(|res| { - if let Err(err) = res { - log::error!("failed to load iced_aw icon font: {err:?}") - } - Message::None + res.map_err(|err| anyhow!("load icon font err: {err:?}")) + .into() }), Command::perform(async {}, move |()| { config::Message::LoadConfig(config).into() @@ -603,24 +613,80 @@ impl Application for Amadeus { }) } - // Open a link, try latter... - Message::OpenLinkInBrowser(link) => { - if let Err(err) = lektor_utils::open::that(link) { - log::error!("{err}"); - }; - Command::none() - } + // Open a link. + Message::OpenLinkInBrowser(link) => Command::perform(async {}, move |_| { + lektor_utils::open::that(link) + .map_err(|err| anyhow!("failed to open {link}: {err:?}")) + .into() + }), // Kill lektord & exit the application Message::ExitApplication => iced::window::close(), Message::ShutdownLektord => send(shutdown_lektord(self.connect_config.clone())) .map(|_| Message::ExitApplication), + // Update on connection status... + Message::ConnectionStatus(true) if !self.is_connected => { + self.is_connected = true; + Command::batch([ + self.handle_refresh_request(RefreshRequest::Playlists), + self.update(Message::MainPanelDisplay(MainPanelDisplay::MainPanel( + mainpanel::Show::Queue, + ))), + ]) + } + Message::ConnectionStatus(false) if self.is_connected => { + self.is_connected = false; + let flag = self.config.amadeus.open_config_if_init_ping_failed; + Command::batch([ + flag.then(|| self.update(Message::MainPanelDisplay(MainPanelDisplay::Config))) + .unwrap_or_else(|| Command::none()), + Command::perform(async {}, |_| { + Message::from_iter([ + Message::ConfigMessage(config::Message::Infos(None)), + Message::MainPanelMessage(mainpanel::Message::Queue( + mainpanel::queue::Message::Clear, + )), + Message::MainPanelMessage(mainpanel::Message::History( + mainpanel::history::Message::Clear, + )), + Message::MainPanelMessage(mainpanel::Message::Playlists( + mainpanel::playlists::Message::Clear, + )), + Message::SidebarMessage(sidebar::Message::ClearPlaylists), + ]) + }), + ]) + } + Message::ConnectionStatus(_) => Command::none(), + // Messages got from subscriptions. - Message::Tick(instant) => { - let delta = instant.saturating_duration_since(self.last_instant); - self.last_instant = instant; - log::debug!("duration since last instant: {delta:?}"); + Message::BigTick(instant) => { + self.last_instant = self.last_instant.max(instant); + Command::perform(get_infos(self.connect_config.clone()), |res| match res { + Ok(Infos { + last_epoch: Some(epoch), + version, + }) => Message::from_iter([ + Message::ConnectionStatus(true), + Message::DatabaseEpoch(epoch), + Message::RefreshRequest(RefreshRequest::Playlists), + Message::ConfigMessage(config::Message::Infos(Some(Infos { + version, + last_epoch: Some(epoch), + }))), + ]), + Ok(infos) => Message::from_iter([ + Message::ConnectionStatus(true), + Message::RefreshRequest(RefreshRequest::Playlists), + Message::ConfigMessage(config::Message::Infos(Some(infos))), + ]), + Err(_) => Message::ConnectionStatus(true), + }) + } + Message::SmollTick(instant) => { + self.last_instant = self.last_instant.max(instant); + let cfg = self.connect_config.clone(); let queue = Command::perform(self.get_queue(), |res| { res.map(|queue| { @@ -628,7 +694,7 @@ impl Application for Amadeus { queue::Message::Reload(queue), )) }) - .unwrap_or_default() + .unwrap_or(Message::ConnectionStatus(false)) }); let history = Command::perform(self.get_history(), |res| { @@ -637,30 +703,27 @@ impl Application for Amadeus { history::Message::Reload(history), )) }) - .unwrap_or_default() + .unwrap_or(Message::ConnectionStatus(false)) }); - let status = Command::perform(get_status(self.connect_config.clone()), |res| { - res.map_err(|err| log::error!("{err}")) - .map(|res| match res { - PlayStateWithCurrent { - state: s @ PlayState::Play | s @ PlayState::Pause, - current: Some((id, elapsed, duration)), - } => Message::from_iter([ - Message::ChangedPlayback(s), - Message::ChangedKaraId(id), - Message::TimeUpdate(elapsed, duration), - ]), - PlayStateWithCurrent { - state: PlayState::Stop, - current: None, - } => Message::ChangedPlayback(PlayState::Stop), - state => { - log::error!("got incoherent state from the server: {state:?}"); - Message::ChangedPlayback(PlayState::Stop) - } - }) - .unwrap_or_default() + let status = Command::perform(get_status(cfg), |res| match res { + Ok(PlayStateWithCurrent { + state: s @ PlayState::Play | s @ PlayState::Pause, + current: Some((id, elapsed, duration)), + }) => Message::from_iter([ + Message::ChangedPlayback(s), + Message::ChangedKaraId(id), + Message::TimeUpdate(elapsed, duration), + ]), + Ok(PlayStateWithCurrent { + state: PlayState::Stop, + current: None, + }) => Message::ChangedPlayback(PlayState::Stop), + Ok(state) => { + log::error!("got incoherent state from the server: {state:?}"); + Message::ChangedPlayback(PlayState::Stop) + } + Err(_) => Message::ConnectionStatus(false), }); Command::batch([queue, history, status]) @@ -677,6 +740,10 @@ impl Application for Amadeus { log::error!("need to handle the reconnect message with flush status: {flush}"); Command::none() } + Message::DatabaseEpoch(epoch) => { + log::error!("need to handle epoch {epoch}"); + Command::none() + } // Change what the main panel displays + We have informations to pass to it Message::MainPanelMessage(message) => self.mainpanel.update(message).map(Message::from), @@ -699,20 +766,15 @@ impl Application for Amadeus { } // Refresh from lektord. - Message::RefreshRequest(req) => self.handle_refresh_request(req), - - // Update the positions. Message::TimeUpdate(elapsed, duration) => { self.bottombar.update(bottombar::Message(elapsed, duration)); Command::none() } - - // Update playback state, a bit of logic here... Message::ChangedPlayback(PlayState::Stop) => { self.playback_state = PlayState::Stop; self.current_kara = None; - self.topbar.update(topbar::Message::ChangedKara(None)); self.bottombar.update(Default::default()); + self.topbar.update(topbar::Message::ChangedKara(None)); self.topbar .update(topbar::Message::ChangedPlayback(PlayState::Stop)); Command::none() @@ -724,10 +786,10 @@ impl Application for Amadeus { } Message::ChangedKaraId(kid) => { Command::perform(KaraStore::into_get(self.kara_store.clone(), kid), |res| { - res.map(Message::ChangedKara).unwrap_or_else(|err| { - log::error!("{err}"); - Message::ChangedPlayback(PlayState::Stop) - }) + Message::from_err_or( + res.map(Message::ChangedKara), + Message::ChangedPlayback(PlayState::Stop), + ) }) } Message::ChangedKara(kara) => { @@ -736,9 +798,10 @@ impl Application for Amadeus { Command::none() } - // Send a command to lektord + // Send a command to lektord, we delegate things to not clutter more this function. Message::MainPanelRequest(req) => self.handle_main_panel_request(req), Message::PlaybackRequest(req) => self.handle_playback_request(req), + Message::RefreshRequest(req) => self.handle_refresh_request(req), Message::KaraRequest(req) => self.handle_kara_request(req, None), } } @@ -824,21 +887,28 @@ impl Application for Amadeus { /// We need a tick every second to query lektord plus we listen for any event. fn subscription(&self) -> iced::Subscription<Self::Message> { - iced::Subscription::batch([ - iced::subscription::events().map(|event| match event { - Event::Keyboard(KbdEvent::KeyReleased { - key_code, - modifiers, - }) => match key_code { - KeyCode::Space => PlaybackRequest::TogglePlaybackState.into(), - KeyCode::N if modifiers.control() => PlaybackRequest::PlayNext.into(), - KeyCode::P if modifiers.control() => PlaybackRequest::PlayPrevious.into(), - _ => Message::None, - }, + use iced::time::{every, Duration}; + let (smoll_time, big_time) = either!(self.is_connected => + (Some(Duration::new(1, 0)), Duration::new(20, 0)); + (None, Duration::new(30, 0)) + ); + let keycodes = iced::subscription::events().map(|event| match event { + Event::Keyboard(KbdEvent::KeyReleased { + key_code, + modifiers, + }) => match key_code { + KeyCode::Space => PlaybackRequest::TogglePlaybackState.into(), + KeyCode::N if modifiers.control() => PlaybackRequest::PlayNext.into(), + KeyCode::P if modifiers.control() => PlaybackRequest::PlayPrevious.into(), _ => Message::None, - }), - iced::time::every(iced::time::Duration::new(1, 0)).map(Message::Tick), - ]) + }, + _ => Message::None, + }); + iced::Subscription::batch( + [keycodes, every(big_time).map(Message::BigTick)] + .into_iter() + .chain(smoll_time.map(|smoll_time| every(smoll_time).map(Message::SmollTick))), + ) } /// Scale factor from the config file. Between 0.5 and 2. diff --git a/amadeus/src/components/karalist.rs b/amadeus/src/components/karalist.rs index c7580a06..c9eaa98e 100644 --- a/amadeus/src/components/karalist.rs +++ b/amadeus/src/components/karalist.rs @@ -1,10 +1,9 @@ -use crate::components::{self, kara}; +use crate::components::kara; use iced::{ widget::{column, horizontal_rule}, Element, Length, }; use lektor_payloads::{KId, Kara}; -use lektor_utils::either; use std::sync::Arc; #[derive(Default)] @@ -54,13 +53,13 @@ impl State { } pub fn view(&self) -> Element<'_, Request> { - either!(self.is_empty() - => components::loading_centered_shrink() - ; self.0.iter() - .fold(column![], |c, kara| c.push(kara.view().map(Request)).push(horizontal_rule(2))) - .width(Length::Fill) - .spacing(2) - .into() - ) + self.0 + .iter() + .fold(column![], |c, kara| { + c.push(kara.view().map(Request)).push(horizontal_rule(2)) + }) + .width(Length::Fill) + .spacing(2) + .into() } } diff --git a/amadeus/src/components/mainpanel/playlists.rs b/amadeus/src/components/mainpanel/playlists.rs index 050b6228..9f1d0549 100644 --- a/amadeus/src/components/mainpanel/playlists.rs +++ b/amadeus/src/components/mainpanel/playlists.rs @@ -23,6 +23,9 @@ pub enum Message { /// Reload the content of a playlist. Reload(Arc<str>, Vec<Arc<Kara>>), + /// Clear all playlist data. + Clear, + /// Update informations about a playlist. UpdatePlaylistInfos(Arc<str>, PlaylistInfo), @@ -108,13 +111,21 @@ impl State { Message::KeepPlaylists(plts) => { self.keep(&plts); match self.to_show { - Some(ref show) if !self.contains(show) => { - Command::perform(async {}, |_| Request::ChangeView) - } + Some(ref show) => (!self.contains(show)) + .then(|| Command::perform(async {}, |_| Request::ChangeView)) + .unwrap_or_else(|| Command::none()), _ => Command::none(), } } + Message::Clear => { + // If the playlist view was cleared, then the view was already changed, so we don't + // send the change view request. + self.playlists.clear(); + self.to_show.take(); + Command::none() + } + Message::Reload(plt, karas) => { let (.., plt) = self.get_mut_or_insert(plt); plt.update(karalist::Message::Reload(karas)); diff --git a/amadeus/src/components/mod.rs b/amadeus/src/components/mod.rs index 2f76ee1b..b216e6a8 100644 --- a/amadeus/src/components/mod.rs +++ b/amadeus/src/components/mod.rs @@ -16,14 +16,6 @@ pub fn loading<'a, T: 'a>() -> iced::Element<'a, T> { .into() } -pub fn loading_centered_shrink<'a, T: 'a>() -> iced::Element<'a, T> { - iced::widget::container(iced_aw::Spinner::new()) - .width(iced::Length::Fill) - .center_x() - .center_y() - .into() -} - pub fn file_size<'a, T: 'a>(bytes: usize) -> iced::Element<'a, T> { let kilos = bytes.div_euclid(1024); let megas = kilos.div_euclid(1024); diff --git a/amadeus/src/components/sidebar.rs b/amadeus/src/components/sidebar.rs index 8d44702d..42e18b75 100644 --- a/amadeus/src/components/sidebar.rs +++ b/amadeus/src/components/sidebar.rs @@ -43,6 +43,9 @@ pub enum Message { /// Need to update the list of playlists. Playlists(Vec<Arc<str>>), + /// Clear all the playlists... + ClearPlaylists, + /// Delete a specific playlist. DeletePlaylist(Arc<str>), } @@ -71,6 +74,11 @@ impl State { Command::none() } + Message::ClearPlaylists => { + self.playlists.clear(); + Command::none() + } + Message::Playlists(playlists) => { let _ = std::mem::replace(&mut self.playlists, playlists); Command::none() diff --git a/amadeus/src/message.rs b/amadeus/src/message.rs index d7356469..6147027c 100644 --- a/amadeus/src/message.rs +++ b/amadeus/src/message.rs @@ -5,6 +5,7 @@ use crate::components::{ sidebar, topbar, }; use lektor_payloads::{KId, Kara, PlayState, Priority}; +use lektor_utils::log; use std::sync::Arc; /// What to display in the main panel. @@ -43,11 +44,20 @@ pub enum Message { None, /// We got a tick. - Tick(iced::time::Instant), + SmollTick(iced::time::Instant), + + /// We got another tick. + BigTick(iced::time::Instant), /// Open the issues link with firefox. OpenLinkInBrowser(&'static str), + /// The status of the connexion changed, either it went online ([true]) or offline ([false]). + ConnectionStatus(bool), + + /// Got the database epoch. Need to check if it changed and act accordingly. + DatabaseEpoch(u64), + /// Shutdown lektord. ShutdownLektord, @@ -122,6 +132,45 @@ pub enum Message { CloseModal, } +impl Message { + /// Transform a result in a message or return the default thingy. + #[allow(dead_code)] + pub fn from_err_or(res: Result<Self, anyhow::Error>, err: Message) -> Self { + res.map_err(|err| log::error!("{err}")).unwrap_or(err) + } + + /// Transform a result in a message or return the default thingy. Build from an iterator. + #[allow(dead_code)] + pub fn from_err_ors( + res: Result<Self, anyhow::Error>, + err: impl IntoIterator<Item = Message>, + ) -> Self { + res.map_err(|err| log::error!("{err}")) + .unwrap_or_else(|()| err.into_iter().collect()) + } + + /// Transform a result in a message or return the default thingy. Call function version. + #[allow(dead_code)] + pub fn from_err_or_else( + res: Result<Self, anyhow::Error>, + err: impl FnOnce() -> Message, + ) -> Self { + res.map_err(|err| log::error!("{err}")) + .unwrap_or_else(|()| err()) + } + + /// Transform a result in a message or return the default thingy. Call function version. Build + /// from an iterator. + #[allow(dead_code)] + pub fn from_err_or_elses<Iter: IntoIterator<Item = Message>>( + res: Result<Self, anyhow::Error>, + err: impl FnOnce() -> Iter, + ) -> Self { + res.map_err(|err| log::error!("{err}")) + .unwrap_or_else(|()| Message::from_iter(err())) + } +} + impl std::fmt::Display for MainPanelDisplay { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -142,6 +191,20 @@ impl Default for MainPanelDisplay { } } +impl From<Result<Message, anyhow::Error>> for Message { + fn from(value: Result<Message, anyhow::Error>) -> Self { + value + .map_err(|err| log::error!("{err}")) + .unwrap_or_default() + } +} + +impl From<Result<(), anyhow::Error>> for Message { + fn from(value: Result<(), anyhow::Error>) -> Self { + value.map(|()| Message::None).into() + } +} + impl Iterator for Message { type Item = Message; -- GitLab