Sélectionner une révision Git
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()
}
}