diff --git a/amadeus/i18n/en/amadeus.ftl b/amadeus/i18n/en/amadeus.ftl index 9c4207ccd3ab6b08b4870590f4da76b6b93dcedd..320a417e599aede5625a6361e24b67bc542d1feb 100644 --- a/amadeus/i18n/en/amadeus.ftl +++ b/amadeus/i18n/en/amadeus.ftl @@ -17,6 +17,9 @@ empty-history = Empty History empty-playlists = Empty playlists empty-playlist = Empty playlists { $name } +create-playlist = Create playlist +delete-playlist = Confirm playlist deletion + next-kara = Next kara prev-kara = Previous kara toggle-playback = Play/Pause diff --git a/amadeus/i18n/es-ES/amadeus.ftl b/amadeus/i18n/es-ES/amadeus.ftl index 91078f9aa6181753e247139d537f09ecef72f074..3caf928503806b48cccacb941f282b2d950ce58b 100644 --- a/amadeus/i18n/es-ES/amadeus.ftl +++ b/amadeus/i18n/es-ES/amadeus.ftl @@ -17,6 +17,9 @@ empty-history = No has escuchado nada recientemente empty-playlists = No tienes listas de reproducción empty-playlist = No hay nada en la lista { $name } +create-playlist = Crear lista de reproducción +delete-playlist = Confirmar la eliminación de la lista de reproducción + next-kara = Póxima kara prev-kara = Previo kara toggle-playback = Alternar reprod. diff --git a/amadeus/i18n/fr-FR/amadeus.ftl b/amadeus/i18n/fr-FR/amadeus.ftl index 9b53636cf2dbc2466aeb332f5b4f3edd0f306c49..64a57b50ca3ef8652e59f197dece26fe4408a71d 100644 --- a/amadeus/i18n/fr-FR/amadeus.ftl +++ b/amadeus/i18n/fr-FR/amadeus.ftl @@ -17,6 +17,9 @@ empty-history = L'historique est vide empty-playlists = Il n'y a pas de listes de lecture de disponibles empty-playlist = La liste de lecture { $name } est vide +create-playlist = Création de liste de lecture +delete-playlist = Supprimer la liste de lecture + next-kara = Kara suivant prev-kara = Kara précédent toggle-playback = Play/Pause diff --git a/amadeus/src/app.rs b/amadeus/src/app.rs index e40eda136e841aa5c6c7fd8457445739c810f1ed..91779f29650894fda0f48029f98824bdd8366339 100644 --- a/amadeus/src/app.rs +++ b/amadeus/src/app.rs @@ -23,15 +23,12 @@ use crate::{ store::Store, }; use cosmic::{ - app::{context_drawer, Core, Task}, + app::{context_drawer, Core, Message as CosmicMessage, Task}, iced::{Length, Subscription}, prelude::*, - style, theme, widget, Application, -}; -use futures::{ - prelude::*, - stream::{self, FuturesUnordered}, + style, task, theme, widget, Application, }; +use futures::{prelude::*, stream}; use lektor_lib::*; use lektor_payloads::{ Epochs, KId, Kara, PlaylistInfo, Priority, SearchFrom, PRIORITY_LENGTH, PRIORITY_VALUES, @@ -94,6 +91,9 @@ pub struct AppModel { tmp_remote_valid: bool, tmp_remote_user: String, tmp_remote_token: String, + + // Temporary things for the playlist creation + tmp_playlist_name: Option<String>, } /// A command to send to the lektord instance. @@ -129,7 +129,9 @@ pub enum LektordCommand { PlaylistDelete(KId), PlaylistRemoveKara(KId, KId), PlaylistGetContent(KId), + PlaylistGetInfos(KId), PlaylistShuffleContent(KId), + PlaylistCreate, // Misc stuff DownloadKaraInfo(KId), @@ -171,7 +173,6 @@ pub enum LektordMessage { ChangedAvailablePlaylists(Vec<KId>), ChangedAvailablePlaylistInfos(PlaylistInfo), ChangedPlaylistContent(KId, Vec<KId>), - ChangedPlaylistsContent(Vec<(KId, Vec<KId>)>), } /// Messages emitted by the application and its widgets. @@ -182,9 +183,10 @@ pub enum Message { OpenKaraInfo(KId), ToggleContextPage(ContextPage), - // Playlist selection stuff + // Playlist selection and creation stuff SelectPlaylist(KId), UnSelectPlaylist, + UpdatePlaylistCreationName(String), // Update the configuration UpdateConfig(ConfigMessage), @@ -240,6 +242,7 @@ impl Application for AppModel { tmp_remote_valid: true, tmp_remote_user: config.user().user.clone(), tmp_remote_token: config.user().token.clone(), + tmp_playlist_name: None, connect_config: Arc::new(RwLock::new(config.get_connect_config())), cosmic_config, @@ -337,6 +340,8 @@ impl Application for AppModel { ContextPage::About => context_pages::about::view(self), ContextPage::Settings => context_pages::config::view(self), ContextPage::KaraInfo(kid) => context_pages::kara_info::view(self.store.get(kid)), + ContextPage::CreatePlaylist => context_pages::playlist_creation::view(self), + ContextPage::DeletePlaylist(plt_id) => context_pages::playlist_deletion::view(plt_id), }) } @@ -419,13 +424,13 @@ impl Application for AppModel { Message::QueryWithFilters => { let config = self.connect_config.clone(); let filters: Vec<_> = self.search_filter.iter_cloned().collect(); - Task::future(async move { + task::future(async move { requests::search_karas(&*config.read().await, SearchFrom::Database, filters) .await - .map(|res| cosmic::app::message::app(Message::QueryWithFiltersResults(res))) + .map(|res| CosmicMessage::App(Message::QueryWithFiltersResults(res))) .unwrap_or_else(|err| { log::error!("failed to query with filters: {err}"); - cosmic::app::message::none() + CosmicMessage::None }) }) } @@ -438,6 +443,10 @@ impl Application for AppModel { self.selected_playlist = None; Task::none() } + Message::UpdatePlaylistCreationName(name) => { + self.tmp_playlist_name = (!name.is_empty()).then_some(name); + Task::none() + } } } @@ -464,9 +473,9 @@ impl AppModel { fn update_connect_config(&self) -> Task<Message> { let config = self.config.get_connect_config(); let connect_config = self.connect_config.clone(); - Task::future(async move { + task::future(async move { *connect_config.write_owned().await = config; - cosmic::app::message::none() + CosmicMessage::None }) } @@ -615,14 +624,34 @@ impl AppModel { // Down here, got updates from lektord. LektordMessage::ChangedAvailablePlaylists(names) => { - let playlists = self.store.keep_playlists(&names); - self.update_playlists_content(playlists) + let config = self.connect_config.clone(); + + let playlists_with_content = stream::iter(self.store.keep_playlists(&names)) + .zip(stream::repeat_with(move || config.clone())) + .filter_map(|(id, config)| async move { + requests::get_playlist_content(config.read().await.as_ref(), id) + .await + .map(|content| (id, content)) + .ok() + }); + + Task::run(playlists_with_content, CosmicMessage::App).then(|msg| match msg { + CosmicMessage::App((id, content)) => Task::batch([ + Task::done(CosmicMessage::App(Message::SendCommand( + LektordCommand::PlaylistGetInfos(id), + ))), + Task::done(CosmicMessage::App(Message::LektordUpdate( + LektordMessage::ChangedPlaylistContent(id, content), + ))), + ]), + _ => Task::none(), + }) } LektordMessage::ChangedAvailablePlaylistInfos(infos) => (self.store) .set_playlist_infos(infos) .map_or_else(Task::none, |plt_id| { - Task::done(cosmic::app::Message::App(Message::SendCommand( + task::message(CosmicMessage::App(Message::SendCommand( LektordCommand::PlaylistGetContent(plt_id), ))) }), @@ -646,51 +675,20 @@ impl AppModel { LektordMessage::ChangedPlaylistContent(name, plt) => { self.store.set_playlist_content(name, plt); - Task::none() - } - LektordMessage::ChangedPlaylistsContent(changes) => { - changes - .into_iter() - .for_each(|(name, plt)| self.store.set_playlist_content(name, plt)); - Task::none() + Task::done(CosmicMessage::App(Message::SendCommand( + LektordCommand::PlaylistGetInfos(name), + ))) } } } - /// Update playlists' content. - fn update_playlists_content(&mut self, playlists: Vec<KId>) -> Task<Message> { - let config = self.connect_config.clone(); - - Task::future(async move { - let updated_playlists = stream::iter(playlists) - .zip(stream::repeat_with(move || config.clone())) - .filter_map(|(id, config)| async move { - requests::get_playlist_content(config.read().await.as_ref(), id) - .await - .map(|content| (id, content)) - .ok() - }) - .collect::<FuturesUnordered<_>>() - .await; - - cosmic::app::Message::App(Message::LektordUpdate( - LektordMessage::ChangedPlaylistsContent(Vec::from_iter(updated_playlists)), - )) - }) - } - /// Send commands to lektord. fn send_command(&mut self, cmd: LektordCommand) -> Task<Message> { let config = self.connect_config.clone(); use lektor_payloads::*; - macro_rules! msg { - ($msg:ident $(($($args:expr),+))?) => { - cosmic::app::message::app(Message::LektordUpdate(LektordMessage::$msg $(($($args),+))?)) - }; - } macro_rules! cmd { ($req:ident ($($arg:expr),*) $(, $res:pat => $handle:expr)?) => { - Task::future(async move { + task::future(async move { cmd!(@handle $req: requests::$req(config.read().await.as_ref() $(, $arg)*).await, $($res => $handle)? @@ -698,12 +696,12 @@ impl AppModel { }) }; - (@handle $txt:ident: $req:expr, ) => { cmd!(@handle $txt: $req, _ => cosmic::app::message::none()) }; + (@handle $txt:ident: $req:expr, ) => { cmd!(@handle $txt: $req, _ => CosmicMessage::None) }; (@handle $txt:ident: $req:expr, $res:pat => $handle:expr) => { match $req { Ok($res) => $handle, Err(err) => { log::error!("failed '{}': {err}", stringify!($txt)); - return cosmic::app::message::none() + return CosmicMessage::None } }}; } @@ -720,49 +718,58 @@ impl AppModel { LektordCommand::QueueClear => cmd!(remove_range_from_queue(..)), LektordCommand::QueueCrop => cmd!(remove_range_from_queue(1..)), LektordCommand::QueueGet => cmd!(get_queue_range(..), queue => { + let mut counts = <[usize; PRIORITY_LENGTH]>::default(); + queue.iter().for_each(|(level, _)| counts[level.index()] += 1); + let mut ret = <[Vec<KId>; PRIORITY_LENGTH]>::default(); + counts.into_iter().enumerate().for_each(|(idx, count)| ret[idx].reserve(count)); queue.into_iter().for_each(|(level, kid)| ret[level.index()].push(kid)); - msg!(ChangedQueue(ret)) + + CosmicMessage::App(Message::LektordUpdate(LektordMessage::ChangedQueue(ret))) }), LektordCommand::QueueLevelShuffle(lvl) => cmd!(shuffle_level_queue(lvl)), LektordCommand::QueueLevelClear(lvl) => cmd!(remove_level_from_queue(lvl)), LektordCommand::QueueLevelGet(lvl) => cmd!(get_queue_level(lvl), queue => { - msg!(ChangedQueueLevel(lvl, queue)) + CosmicMessage::App(Message::LektordUpdate(LektordMessage::ChangedQueueLevel(lvl, queue))) }), LektordCommand::DownloadKaraInfo(kid) => cmd!(get_kara_by_kid(kid), kara => { - msg!(DownloadedKaraInfo(kara)) + CosmicMessage::App(Message::LektordUpdate(LektordMessage::DownloadedKaraInfo(kara))) }), LektordCommand::DownloadKarasInfo(kids) => cmd!(get_karas_by_kid(kids), karas => { - msg!(DownloadedKarasInfo(karas)) + CosmicMessage::App(Message::LektordUpdate(LektordMessage::DownloadedKarasInfo(karas))) }), LektordCommand::HistoryClear => cmd!(remove_range_from_history(..)), LektordCommand::HistoryGet => cmd!(get_history_range(..), history => { - msg!(ChangedHistory(history)) + CosmicMessage::App(Message::LektordUpdate(LektordMessage::ChangedHistory(history))) }), - LektordCommand::DatabaseGet => { - Task::done(cosmic::app::Message::App(Message::SendCommand( - LektordCommand::DownloadKarasInfo(self.store.take_kara_ids()), - ))) - } + LektordCommand::DatabaseGet => task::message(CosmicMessage::App(Message::SendCommand( + LektordCommand::DownloadKarasInfo(self.store.take_kara_ids()), + ))), LektordCommand::PlaylistGetContent(id) => cmd!(get_playlist_content(id), content => { - msg!(ChangedPlaylistContent(id, content)) + CosmicMessage::App(Message::LektordUpdate(LektordMessage::ChangedPlaylistContent(id, content))) }), LektordCommand::PlaylistDelete(plt_id) => cmd!(delete_playlist(plt_id)), LektordCommand::PlaylistRemoveKara(plt_id, kid) => { cmd!(remove_from_playlist(plt_id, KaraFilter::KId(kid))) } - LektordCommand::PlaylistsGet => { - cmd!(get_playlists(), playlists => { - let playlists = playlists.into_iter().map(|(id, _)| id).collect(); - msg!(ChangedAvailablePlaylists(playlists)) - }) - } + LektordCommand::PlaylistsGet => cmd!(get_playlists(), playlists => { + let playlists = playlists.into_iter().map(|(id, _)| id).collect(); + CosmicMessage::App(Message::LektordUpdate(LektordMessage::ChangedAvailablePlaylists(playlists))) + }), LektordCommand::PlaylistShuffleContent(id) => cmd!(shuffle_playlist(id), _ => { - cosmic::app::message::app(Message::SendCommand(LektordCommand::PlaylistGetContent(id))) + CosmicMessage::App(Message::SendCommand(LektordCommand::PlaylistGetContent(id))) + }), + LektordCommand::PlaylistCreate => (self.tmp_playlist_name.take()).map_or_else(Task::none, |name| { + Task::batch([cmd!(create_playlist(name.as_str()), id => { + CosmicMessage::App(Message::SendCommand(LektordCommand::PlaylistGetInfos(id))) + }), Task::done(CosmicMessage::App(Message::ToggleContextPage(ContextPage::CreatePlaylist)))]) + }), + LektordCommand::PlaylistGetInfos(plt_id) => cmd!(get_playlist_info(plt_id), infos => { + CosmicMessage::App(Message::LektordUpdate(LektordMessage::ChangedAvailablePlaylistInfos(infos))) }), } } diff --git a/amadeus/src/app/context_pages.rs b/amadeus/src/app/context_pages.rs index f74507bc738cbb230c2f8cf5ad0f56d135436cbd..32dc856becf9da21b7e5d5f2d15ad2d50699ee45 100644 --- a/amadeus/src/app/context_pages.rs +++ b/amadeus/src/app/context_pages.rs @@ -5,6 +5,8 @@ use lektor_payloads::KId; pub mod about; pub mod config; pub mod kara_info; +pub mod playlist_creation; +pub mod playlist_deletion; /// The context page to display in the context drawer. This is the pane on the right that can be /// hidden or shown. @@ -23,6 +25,14 @@ pub enum ContextPage { /// etc...) #[display("{}", fl!("kara"))] KaraInfo(KId), + + /// Create a playlist, prompts for a name. + #[display("{}", fl!("create-playlist"))] + CreatePlaylist, + + /// Delete a playlist, prompts for confirmation. + #[display("{}", fl!("delete-playlist"))] + DeletePlaylist(KId), } impl ContextPage { diff --git a/amadeus/src/app/context_pages/playlist_creation.rs b/amadeus/src/app/context_pages/playlist_creation.rs new file mode 100644 index 0000000000000000000000000000000000000000..300b11187c9509e5dfbef4cbb9fcfc2390b58f85 --- /dev/null +++ b/amadeus/src/app/context_pages/playlist_creation.rs @@ -0,0 +1,35 @@ +use crate::{ + app::{context_pages::ContextPage, AppModel, LektordCommand, Message}, + fl, +}; +use cosmic::{app::context_drawer, iced::Length, style, widget}; + +/// View the page for a playlist creation. +pub fn view(state: &AppModel) -> context_drawer::ContextDrawer<Message> { + context_drawer::context_drawer( + widget::settings::section() + .add(widget::settings::item( + "todo: user", + state.config.user().user.as_str(), + )) + .add( + widget::text_input( + "todo: place-holder", + state.tmp_playlist_name.as_deref().unwrap_or_default(), + ) + .on_input(Message::UpdatePlaylistCreationName), + ) + .add( + widget::button::text("create") + .trailing_icon(widget::icon::from_svg_bytes(crate::icons::EDIT)) + .class(style::Button::Suggested) + .width(Length::Fill) + .on_press_maybe( + (state.tmp_playlist_name.is_some()) + .then_some(Message::SendCommand(LektordCommand::PlaylistCreate)), + ), + ), + Message::ToggleContextPage(ContextPage::CreatePlaylist), + ) + .title(fl!("create-playlist")) +} diff --git a/amadeus/src/app/context_pages/playlist_deletion.rs b/amadeus/src/app/context_pages/playlist_deletion.rs new file mode 100644 index 0000000000000000000000000000000000000000..3196d1b7c28ba7c61fe89e0a807c67947a01f9fb --- /dev/null +++ b/amadeus/src/app/context_pages/playlist_deletion.rs @@ -0,0 +1,18 @@ +use crate::{ + app::{LektordCommand, Message}, + fl, +}; +use cosmic::{ + app::context_drawer, + prelude::*, + style, + widget::{self, tooltip::Position}, +}; +use lektor_payloads::KId; + +use super::ContextPage; + +/// View the page for a playlist deletion. +pub fn view<'a>(plt_id: KId) -> context_drawer::ContextDrawer<'a, Message> { + todo!() +} diff --git a/amadeus/src/app/pages/playlists.rs b/amadeus/src/app/pages/playlists.rs index cb3975858925a941744788166b1b0c949483aecb..12059718689bfeeafe6a53c4334173a39cf77b60 100644 --- a/amadeus/src/app/pages/playlists.rs +++ b/amadeus/src/app/pages/playlists.rs @@ -1,5 +1,6 @@ use crate::{ app::{ + context_pages::ContextPage, kard, pages::{PageView, PageViewControl}, LektordCommand, Message, @@ -24,8 +25,12 @@ fn view_playlist_card(playlist: &Playlist) -> Option<Element<Message>> { fn view_playlists_cards(store: &Store) -> Element<Message> { PageView::default() .titles(fl!("playlists"), fl!("empty-playlists")) - .controls([PageViewControl::new(crate::icons::RETRY) - .message(Message::SendCommand(LektordCommand::PlaylistsGet))]) + .controls([ + PageViewControl::new(crate::icons::PLUS) + .message(Message::ToggleContextPage(ContextPage::CreatePlaylist)), + PageViewControl::new(crate::icons::RETRY) + .message(Message::SendCommand(LektordCommand::PlaylistsGet)), + ]) .push_when(store.iter_playlists().count() != 0, || { store .iter_playlists() @@ -40,7 +45,7 @@ fn view_playlists_cards(store: &Store) -> Element<Message> { /// Display the page about a specific playlist. fn view_playlist_content(store: &Store, id: KId) -> Element<Message> { let Some(infos) = store.playlist(id) else { - todo!() + todo!("failed to show playlist content") }; let name = infos @@ -96,7 +101,7 @@ fn view_playlist_content(store: &Store, id: KId) -> Element<Message> { PageViewControl::new(icons::RETRY) .message(Message::SendCommand(LektordCommand::PlaylistGetContent(id))), PageViewControl::new(icons::METEOR) - .message(Message::SendCommand(LektordCommand::PlaylistDelete(id))) + .message(Message::ToggleContextPage(ContextPage::DeletePlaylist(id))) .destructive(), ]) .push_and_ignore_for_empty(infos)