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