Skip to content
Extraits de code Groupes Projets
Sélectionner une révision Git
  • 93fcf5d6a6f5461ccb46b09faa58b7bedb49886f
  • master par défaut protégée
  • rust-playlist-sync
  • rust
  • fix-qt-deprecated-qvariant-type
  • fix-mpris-qtwindow-race-condition
  • rust-appimage-wayland
  • windows-build-rebased
  • v2.5 protégée
  • v2.4 protégée
  • v2.3-1 protégée
  • v2.3 protégée
  • v2.2 protégée
  • v2.1 protégée
  • v2.0 protégée
  • v1.8-3 protégée
  • v1.8-2 protégée
  • v1.8-1 protégée
  • v1.8 protégée
  • v1.7 protégée
  • v1.6 protégée
  • v1.5 protégée
  • v1.4 protégée
  • v1.3 protégée
  • v1.2 protégée
  • v1.1 protégée
  • v1.0 protégée
27 résultats

app.rs

Blame
  • app.rs 33,08 Kio
    //! Definition of the amadeus application
    
    use crate::{
        components::{
            self,
            config::{AmadeusConfig, RemoteConfig},
            mainpanel::{history, playlists, queue, search},
            modal::*,
            *,
        },
        links::*,
        message::*,
        store::KaraStore,
        style_sheet::{sizes::*, ModalStyleSheet, SideBarSplit},
    };
    use anyhow::Result;
    use futures::{stream, StreamExt, TryStreamExt};
    use iced::{
        keyboard::{Event as KbdEvent, KeyCode},
        widget::{column, container, horizontal_rule, horizontal_space, row, text},
        Application, Command, Element, Event, Length, Renderer,
    };
    use iced_aw::{native::Split, split};
    use lektor_lib::{requests::*, ConnectConfigPtr};
    use lektor_payloads::{KId, Kara, KaraFilter, PlayState, PlayStateWithCurrent};
    use lektor_utils::{config::UserConfig, log};
    use std::{future::Future, sync::Arc};
    
    /// The main application.
    pub struct Amadeus {
        /// Was the config loaded for the first time?
        is_init: bool,
    
        /// The last recieved instant, to calculate the delta and maybe time some animations.
        last_instant: iced::time::Instant,
    
        /// The configuration, can be edited with the settings modal.
        config: config::State,
    
        /// The config to pass to the connect functions, updated when needed.
        connect_config: ConnectConfigPtr,
    
        /// The store where we try to cache the karas to not always query lektord. We will need to
        /// invalidate it on some events.
        kara_store: Arc<KaraStore>,
    
        /// Side bar to select what to display in the main panel.
        sidebar: sidebar::State,
    
        /// The size of the sidebar.
        sidebar_size: Option<u16>,
    
        /// The bottom bar to display the progress of the current kara.
        bottombar: bottombar::State,
    
        /// The top bar to display informations about the current kara and some controls for the
        /// application, the search button and the playback controls.
        topbar: topbar::State,
    
        /// The main panel, displays the queue, history, etc.
        mainpanel: mainpanel::State,
    
        /// Whever to show the main panel or not, if we don't show the mainpanel, then we show the
        /// config panel.
        show_main_panel: bool,
    
        /// Currently playing kara, for the title.
        current_kara: Option<Arc<Kara>>,
    
        /// The current playback state of the player.
        playback_state: PlayState,
    
        /// The modal to display.
        modal: Option<WhichModal>,
    }
    
    /// Send a command to lektord and the return must be () here... Extremly specific, but handy.
    fn send(future: impl Future<Output = Result<()>> + 'static + Send) -> Command<Message> {
        Command::perform(future, |res| {
            if let Err(err) = res {
                log::error!("{err}")
            }
            Message::None
        })
    }
    
    impl Default for Amadeus {
        fn default() -> Self {
            Self {
                // Manual defaults.
                last_instant: iced::time::Instant::now(),
                sidebar_size: Some(300),
                show_main_panel: false,
    
                // Has default.
                kara_store: Arc::new(KaraStore::new()),
                is_init: Default::default(),
                connect_config: Default::default(),
                config: Default::default(),
                sidebar: Default::default(),
                bottombar: Default::default(),
                topbar: Default::default(),
                mainpanel: Default::default(),
                current_kara: Default::default(),
                playback_state: Default::default(),
                modal: Default::default(),
            }
        }
    }
    
    impl Amadeus {
        /// Get a kara from the store or request it from lektord. If we got a kara then we pass it to
        /// the callback, otherwise we returns a [Message::None].
        fn with_kara(
            &self,
            id: KId,
            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(),
            )
        }
    
        /// Handle a config message. This is ugly so we put it in another function.
        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(),
                    ))
                });
            }
    
            let updated = {
                let cfg = Arc::make_mut(&mut self.connect_config);
                match &config {
                    config::Message::HostChanged(host) => match host.as_str().parse() {
                        Ok(host) => {
                            cfg.host = host;
                            true
                        }
                        Err(err) => {
                            log::error!("{err}");
                            false
                        }
                    },
                    config::Message::UserChanged(user) => {
                        cfg.user = user.as_str().into();
                        true
                    }
                    config::Message::TokenChanged(token) => {
                        cfg.token = token.as_str().into();
                        true
                    }
                    config::Message::SchemeChanged(scheme) => match scheme.parse() {
                        Ok(scheme) => {
                            cfg.scheme = scheme;
                            true
                        }
                        Err(_) => false,
                    },
                    config::Message::LoadConfig(config) => {
                        let RemoteConfig {
                            host,
                            scheme,
                            user: UserConfig { user, token, .. },
                        } = &config.connect;
                        (cfg.host, cfg.scheme) = (*host, *scheme);
                        (cfg.user, cfg.token) = (user.as_str().into(), token.as_str().into());
                        true
                    }
                    _ => false,
                }
            };
    
            if updated {
                self.kara_store.update(self.connect_config.clone())
            }
    
            self.config.update(config).map(Message::from)
        }
    
        /// Handle requests from the main panel.
        ///
        /// This is where we send commands to lektord.
        fn handle_main_panel_request(
            &mut self,
            req: mainpanel::Request,
        ) -> Command<<Self as Application>::Message> {
            let cfg = self.connect_config.clone();
            match req {
                // Queue
                mainpanel::Request::Queue(req) => match req {
                    queue::Request::ShuffleQueueLevel(prio) => send(shuffle_level_queue(cfg, prio))
                        .map(move |_| RefreshRequest::QueueLevel(prio).into()),
                    queue::Request::ClearQueueLevel(prio) => Command::batch([
                        self.mainpanel
                            .update(queue::Message::ClearQueueLevel(prio).into())
                            .map(Message::from),
                        send(remove_level_from_queue(cfg, prio)),
                    ]),
                    queue::Request::ClearQueue => Command::batch([
                        self.mainpanel
                            .update(queue::Message::ClearQueue.into())
                            .map(Message::from),
                        send(remove_range_from_queue(cfg, ..)),
                    ]),
                    queue::Request::ShuffleQueue => {
                        send(shuffle_queue(cfg)).map(|_| RefreshRequest::Queue.into())
                    }
                    queue::Request::RefreshQueue => self.handle_refresh_request(RefreshRequest::Queue),
                    queue::Request::RefreshQueueLevel(lvl) => {
                        self.handle_refresh_request(RefreshRequest::QueueLevel(lvl))
                    }
                    queue::Request::InnerQueueEvent(karalist::Request(req)) => {
                        self.handle_kara_request(req, None)
                    }
                    queue::Request::ToggleQueueLevel(prio, show) => self
                        .mainpanel
                        .update(queue::Message::ToggleQueueLevel(prio, show).into())
                        .map(Message::from),
                },
    
                // History
                mainpanel::Request::History(req) => match req {
                    history::Request::Clear => Command::batch([
                        self.mainpanel
                            .update(history::Message::Clear.into())
                            .map(Message::from),
                        send(remove_range_from_history(cfg, ..)),
                    ]),
                    history::Request::Refresh => self.handle_refresh_request(RefreshRequest::History),
                    history::Request::Inner(karalist::Request(req)) => {
                        self.handle_kara_request(req, None)
                    }
                },
    
                // Playlist
                mainpanel::Request::Playlists(req) => match req {
                    playlists::Request::Refresh(plt) => {
                        self.handle_refresh_request(RefreshRequest::Playlist(plt))
                    }
                    playlists::Request::Delete(plt) => Command::batch([
                        self.mainpanel
                            .update(playlists::Message::DeletePlaylist(plt.clone()).into())
                            .map(Message::from),
                        send(delete_playlist(cfg, plt)),
                    ]),
                    playlists::Request::Inner(plt, karalist::Request(req)) => {
                        self.handle_kara_request(req, Some(plt))
                    }
                    playlists::Request::AddTo(name, id) => {
                        send(add_to_playlist(cfg, name, KaraFilter::KId(id)))
                    }
                    playlists::Request::RemoveFrom(name, id) => {
                        send(remove_from_playlist(cfg, name, KaraFilter::KId(id)))
                    }
                },
    
                // Search/Database
                mainpanel::Request::Search(req) => match req {
                    search::Request::Kara(req) => self.handle_kara_request(req, None),
                    search::Request::Search => {
                        let filters: Vec<_> = self.mainpanel.search_filters().into_iter().collect();
                        log::error!("implement the search thing with filters: {filters:#?}",);
                        Command::none()
                    }
                    search::Request::UpdateFromRepo => send(update_from_repo(cfg)),
                    search::Request::Message(msg) => {
                        self.mainpanel.update(msg.into()).map(Message::from)
                    }
                },
            }
        }
    
        /// Handle requests from individual karas.
        ///
        /// When deleting something we send the command and
        /// perform the action in our representation of the database. This means that we may not be in
        /// sync, see latter how we can sync lektord and amadeus...
        ///
        /// It's in this function that we make calls to lektord.
        fn handle_kara_request(
            &mut self,
            req: kara::Request,
            plt: Option<Arc<str>>,
        ) -> Command<<Self as Application>::Message> {
            use crate::components::kara::Request::*;
            let cfg = self.connect_config.clone();
            match (plt, req) {
                (_, AddToQueue(prio, id)) => Command::batch([
                    send(add_kid_to_queue(cfg, prio, id.clone())),
                    self.with_kara(id, move |kara| {
                        queue::Message::AddKaraToQueue(prio, kara).into()
                    }),
                ]),
                (_, RemoveFromQueue(id)) => Command::batch([
                    self.mainpanel
                        .update(queue::Message::RemoveKaraFromQueue(id.clone()).into())
                        .map(Message::from),
                    send(remove_kid_from_queue(cfg, id)),
                ]),
                (_, RemoveFromHistory(id)) => Command::batch([
                    self.mainpanel
                        .update(history::Message::RemoveKara(id.clone()).into())
                        .map(Message::from),
                    send(remove_kid_from_history(cfg, id)),
                ]),
    
                // Don't need a modal as we already knwo the playlist to delete from.
                (Some(plt), RemoveFromPlaylist(id)) => Command::batch([
                    self.mainpanel
                        .update(
                            playlists::Message::RemoveKaraFromPlaylist(plt.clone(), id.clone()).into(),
                        )
                        .map(Message::from),
                    send(remove_kid_from_playlist(cfg, plt, id)),
                ]),
    
                // Need to open a modal to take decision.
                (None, RemoveFromPlaylist(id)) => {
                    let plts = self.playlist_list().to_vec();
                    let callback = OnPlaylistSelectCallback::new(move |name| {
                        Message::from_iter([
                            playlists::Request::RemoveFrom(name.clone(), id.clone()).into(),
                            playlists::Message::RemoveKaraFromPlaylist(name, id.clone()).into(),
                        ])
                    });
                    let action = |_| ShowModal::ChoosePlaylist(plts, callback).into();
                    Command::perform(async {}, action)
                }
                (Some(plt), AddToPlaylist(id)) => {
                    let plts = self.playlist_list().to_vec();
                    self.with_kara(id.clone(), |kara| {
                        let cb = OnPlaylistSelectCallback::new(move |name| {
                            Message::from_iter([
                                playlists::Request::AddTo(name.clone(), id.clone()).into(),
                                playlists::Message::AddKaraToPlaylist(name, kara.clone()).into(),
                            ])
                        });
                        ShowModal::ChoosePlaylistExpect(plt, plts, cb).into()
                    })
                }
                (None, AddToPlaylist(id)) => {
                    let plts = self.playlist_list().to_vec();
                    self.with_kara(id.clone(), |kara| {
                        let cb = OnPlaylistSelectCallback::new(move |name| {
                            Message::from_iter([
                                playlists::Request::AddTo(name.clone(), id.clone()).into(),
                                playlists::Message::AddKaraToPlaylist(name, kara.clone()).into(),
                            ])
                        });
                        ShowModal::ChoosePlaylist(plts, cb).into()
                    })
                }
    
                // Need to open a modal to display.
                (_, OpenInformation(id)) => self.update(ShowModal::KaraInfoById(id).into()),
            }
        }
    
        /// Handle the requests to make to lektord.
        ///
        /// This is where the commands are sent to lektord.
        fn handle_playback_request(
            &mut self,
            req: PlaybackRequest,
        ) -> Command<<Self as Application>::Message> {
            use crate::message::PlaybackRequest::*;
            let cfg = self.connect_config.clone();
            match req {
                ChangePlayback(state) => send(set_playback_state(cfg, state)),
                PlayNext => send(play_next(cfg)),
                PlayPrevious => send(play_previous(cfg)),
            }
        }
    
        /// 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)),
                    ])
                }
            })
        }
    
        /// Handle the refresh requests.
        ///
        /// This is where the commands are sent to lektord.
        fn handle_refresh_request(
            &mut self,
            req: RefreshRequest,
        ) -> Command<<Self as Application>::Message> {
            let cfg = self.connect_config.clone();
            let store = self.kara_store.clone();
            match req {
                RefreshRequest::Playlists => Command::perform(get_playlists(cfg), |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()
                }),
    
                RefreshRequest::Playlist(plt) => {
                    let plt2 = plt.clone();
                    Command::perform(
                        async move {
                            stream::iter(
                                get_playlist_content(cfg, plt)
                                    .await
                                    .map_err(|err| log::error!("{err}"))
                                    .unwrap_or_default(),
                            )
                            .then(|id| KaraStore::into_get(store.clone(), id))
                            .try_collect()
                            .await
                        },
                        move |karas| {
                            karas
                                .map_err(|err| log::error!("{err}"))
                                .map(|karas| Message::from(playlists::Message::Reload(plt2, karas)))
                                .unwrap_or_default()
                        },
                    )
                }
    
                RefreshRequest::History => Command::perform(
                    async move {
                        stream::iter(
                            get_history(cfg)
                                .await
                                .map_err(|err| log::error!("{err}"))
                                .unwrap_or_default(),
                        )
                        .then(|id| KaraStore::into_get(store.clone(), id))
                        .try_collect()
                        .await
                    },
                    |karas| {
                        karas
                            .map(|karas| Message::from(history::Message::Reload(karas)))
                            .map_err(|err| log::error!("{err}"))
                            .unwrap_or_default()
                    },
                ),
    
                // Fow now we always update the whole queue, see later to only update one part of the
                // queue as an optimization.
                RefreshRequest::Queue | RefreshRequest::QueueLevel(_) => Command::perform(
                    async move {
                        stream::iter(
                            get_queue(cfg)
                                .await
                                .map_err(|err| log::error!("{err}"))
                                .unwrap_or_default(),
                        )
                        .then(move |(p, id)| {
                            let store = store.clone();
                            async move { store.get(id).await.map(|kara| (p, kara)) }
                        })
                        .try_collect()
                        .await
                    },
                    |karas| {
                        karas
                            .map(|karas| Message::from(queue::Message::ReloadQueue(karas)))
                            .map_err(|err| log::error!("{err}"))
                            .unwrap_or_default()
                    },
                ),
            }
        }
    
        /// Handle the Event message. For now we only handle some keyboard events.
        fn handle_event(&mut self, event: Event) -> Command<<Self as Application>::Message> {
            match event {
                Event::Keyboard(KbdEvent::KeyReleased {
                    key_code,
                    modifiers,
                }) => match key_code {
                    KeyCode::Space => send(toggle_playback_state(self.connect_config.clone())),
                    KeyCode::N if modifiers.control() => {
                        Command::perform(async {}, |_| PlaybackRequest::PlayNext.into())
                    }
                    KeyCode::P if modifiers.control() => {
                        Command::perform(async {}, |_| PlaybackRequest::PlayPrevious.into())
                    }
                    _ => Command::none(),
                },
                _ => Command::none(),
            }
        }
    
        fn playlist_list(&self) -> &[Arc<str>] {
            self.sidebar.playlist_list()
        }
    }
    
    impl Application for Amadeus {
        type Message = Message;
        type Executor = iced::executor::Default;
        type Theme = iced::Theme;
        type Flags = AmadeusConfig;
    
        /// Create a new [Amadeus] application from the configs.
        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
                }),
                Command::perform(async {}, move |()| {
                    config::Message::LoadConfig(config).into()
                }),
                iced::system::fetch_information(config::Message::SystemInformations).map(Message::from),
            ]);
            (Default::default(), init_events)
        }
    
        /// Display the current kara as the title of the window.
        fn title(&self) -> String {
            match self.current_kara.as_ref() {
                Some(current) => format!("Amadeus [{}] - {current}", self.playback_state.as_ref()),
                None => format!("Amadeus [{}]", self.playback_state.as_ref()),
            }
        }
    
        /// Update the internal state and handle the changes to re-dispatch the messages.
        fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
            match message {
                // Some special messages.
                Message::None => Command::none(),
                Message::Multiple(messages) => {
                    Command::batch(messages.into_iter().map(|message| self.update(message)))
                }
    
                // Launch or close MPRIS.
                Message::ToggleMprisServer(true) => {
                    log::error!("should launch the mpris server, ignore for now");
                    Command::none()
                }
                Message::ToggleMprisServer(false) => {
                    log::error!("should shutdown the mpris server, ignore for now");
                    Command::none()
                }
    
                // Open modal or close it.
                Message::CloseModal => {
                    self.modal = None;
                    Command::none()
                }
                Message::DisplayModal(show) => match show {
                    ShowModal::KaraInfo(kara_ptr) => {
                        self.modal = Some(WhichModal::KaraInfo(karainfos::State::new(kara_ptr)));
                        Command::none()
                    }
                    ShowModal::KaraInfoById(id) => {
                        self.with_kara(id, |kara| Message::DisplayModal(ShowModal::KaraInfo(kara)))
                    }
                    ShowModal::ChoosePlaylist(plts, callback) => {
                        self.modal = Some(WhichModal::PlaylistSelect(
                            playlistselect::State::new(plts),
                            callback,
                        ));
                        Command::none()
                    }
                    ShowModal::ChoosePlaylistExpect(expect, plts, callback) => {
                        self.modal = Some(WhichModal::PlaylistSelect(
                            playlistselect::State::new_without(plts, expect),
                            callback,
                        ));
                        Command::none()
                    }
                },
    
                // Toggle fullscreen.
                Message::SetWindowMode(mode) => iced::window::change_mode(mode),
                Message::ToggleFullScreen => {
                    use iced::window::Mode::*;
                    iced::window::fetch_mode(|mode| match mode {
                        Fullscreen => Message::SetWindowMode(Windowed),
                        _ => Message::SetWindowMode(Fullscreen),
                    })
                }
    
                // Open a link, try latter...
                Message::OpenLinkInBrowser(link) => {
                    if let Err(err) = lektor_utils::open::that(link) {
                        log::error!("{err}");
                    };
                    Command::none()
                }
    
                // Kill lektord & exit the application
                Message::ShutdownLektord => send(shutdown_lektord(self.connect_config.clone()))
                    .map(|_| Message::ExitApplication),
                Message::ExitApplication => iced::window::close(),
    
                // Messages got from subscriptions.
                Message::Event(event) => self.handle_event(event),
                Message::Tick(instant) => {
                    let delta = instant.saturating_duration_since(self.last_instant);
                    self.last_instant = instant;
                    log::debug!("duration since last instant: {delta:?}");
                    use PlayState::*;
                    Command::perform(get_status(self.connect_config.clone()), |res| match res {
                        Ok(PlayStateWithCurrent {
                            state: s @ Play | s @ Pause,
                            current: Some((id, elapsed, duration)),
                        }) => Message::from_iter([
                            Message::ChangedPlayback(s),
                            Message::ChangedKaraId(id),
                            Message::TimeUpdate(elapsed, duration),
                        ]),
                        Ok(PlayStateWithCurrent {
                            state: Stop,
                            current: None,
                        }) => Message::ChangedPlayback(Stop),
                        Ok(state) => {
                            log::error!("got incoherent state from the server: {state:?}");
                            Message::ChangedPlayback(PlayState::Stop)
                        }
                        Err(err) => {
                            log::error!("{err}");
                            Message::None
                        }
                    })
                }
    
                // Config changed
                Message::ConfigMessage(config) => self.handle_config_message(config),
                Message::ConfigLoaded => {
                    log::info!("config was loaded, start rendering the application");
                    self.is_init = true;
                    self.command_init_ping()
                }
                Message::ReconnectToLektord(flush) => {
                    log::error!("need to handle the reconnect message with flush status: {flush}");
                    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),
                Message::MainPanelDisplay(MainPanelDisplay::Config) => {
                    self.show_main_panel = false;
                    Command::none()
                }
                Message::MainPanelDisplay(MainPanelDisplay::MainPanel(show)) => {
                    self.show_main_panel = true;
                    self.mainpanel
                        .update(mainpanel::Message::Show(show))
                        .map(Message::from)
                }
    
                // A message for the side panel.
                Message::SideBarResize(size) => {
                    self.sidebar_size = Some(size);
                    Command::none()
                }
                Message::SidebarMessage(message) => {
                    self.sidebar.update(message);
                    Command::none()
                }
    
                // 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::ChangedPlayback(PlayState::Stop));
                    Command::none()
                }
                Message::ChangedPlayback(x) => {
                    self.playback_state = x;
                    self.topbar.update(topbar::Message::ChangedPlayback(x));
                    Command::none()
                }
                Message::ChangedKaraId(kid) => {
                    let store = self.kara_store.clone();
                    Command::perform(KaraStore::into_get(store, kid), |res| match res {
                        Ok(kara) => Message::ChangedKara(kara),
                        Err(err) => {
                            log::error!("{err}");
                            Message::ChangedPlayback(PlayState::Stop)
                        }
                    })
                }
                Message::ChangedKara(kara) => {
                    self.current_kara = Some(kara.clone());
                    self.topbar.update(topbar::Message::ChangedKara(Some(kara)));
                    Command::none()
                }
    
                // Send a command to lektord
                Message::MainPanelRequest(req) => self.handle_main_panel_request(req),
                Message::PlaybackRequest(req) => self.handle_playback_request(req),
                Message::KaraRequest(req) => self.handle_kara_request(req, None),
            }
        }
    
        /// Render the application and all its component.
        fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
            if !self.is_init {
                return components::loading();
            }
    
            // Main panel
            let inner_main_panel = container(match self.show_main_panel {
                true => self.mainpanel.view().map(Message::from),
                false => self.config.view().map(Message::from),
            })
            .width(Length::Fill)
            .height(Length::Fill)
            .padding(20);
    
            let main_panel_title = match self.show_main_panel {
                true => self.mainpanel.view_title().push(self.mainpanel.view_actions().map(Message::from)),
                false => row![
                    text("Settings").size(SIZE_FONT_TITLE),
                    horizontal_space(Length::Fill),
                    tip!(icon!(SIZE_FONT_MEDIUM | Fullscreen -> Message::ToggleFullScreen)                       => Bottom | "Toggle fullscreen"),
                    tip!(icon!(SIZE_FONT_MEDIUM | Github     -> Message::OpenLinkInBrowser(LEKTORD_HOME_LINK))   => Bottom | "Open lektor homepage"),
                    tip!(icon!(SIZE_FONT_MEDIUM | Bug        -> Message::OpenLinkInBrowser(LEKTORD_ISSUES_LINK)) => Bottom | "Open issues for lektor"),
                    tip!(icon!(SIZE_FONT_MEDIUM | XOctagon   -> Message::ShutdownLektord)                        => Bottom | "Shutdown lektord and exit amadeus"),
                ],
            }
            .padding(0)
            .height(Length::Shrink)
            .width(Length::Fill);
    
            let main_panel = column![
                main_panel_title,
                horizontal_space(20),
                horizontal_rule(4),
                inner_main_panel
            ]
            .width(Length::Fill)
            .height(Length::Fill)
            .padding(14);
    
            // Screen
            let screen = Split::new(
                self.sidebar.view().map(Message::from),
                column![self.topbar.view().map(Message::from), main_panel]
                    .padding(0.0)
                    .width(Length::Fill)
                    .height(Length::Fill),
                self.sidebar_size,
                split::Axis::Vertical,
                Message::SideBarResize,
            )
            .style(iced_aw::SplitStyles::custom(SideBarSplit))
            .padding(0.0)
            .min_size_first(250)
            .height(Length::Fill)
            .width(Length::Fill);
    
            let screen = column![screen, self.bottombar.view().map(Message::from)].padding(0);
    
            // The modal
            match self.modal {
                Some(ref show) => {
                    let modal = match show {
                        WhichModal::KaraInfo(kara) => kara.view().map(|_| Message::None),
                        WhichModal::PlaylistSelect(plt, action) => {
                            plt.view().map(|a| action.clone().0(a))
                        }
                    };
                    let modal = container(modal)
                        .max_width(600)
                        .padding(10)
                        .style(iced::theme::Container::Custom(Box::new(ModalStyleSheet)));
                    Into::<Element<'_, _>>::into(Modal::new(screen, modal).on_blur(Message::CloseModal))
                        .map(|msg| Message::from_iter([msg, Message::CloseModal]))
                }
                None => screen.into(),
            }
        }
    
        /// 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(Message::Event),
                iced::time::every(iced::time::Duration::new(1, 0)).map(Message::Tick),
            ])
        }
    
        /// Scale factor from the config file. Between 0.5 and 2.
        fn scale_factor(&self) -> f64 {
            self.config.amadeus.scale_factor()
        }
    
        /// We can change the theme of the application. The style will be automatically derived. As we
        /// only have dark and light themes.
        fn theme(&self) -> Self::Theme {
            self.config.amadeus.theme.into()
        }
    }