diff --git a/DEV.md b/DEV.md index 772a6c2..b76c379 100644 --- a/DEV.md +++ b/DEV.md @@ -6,11 +6,11 @@ Todo types: [BUG] \[loc\](/src/...) - Bugfix, mandatory location [GIT] \[loc\](/src/...) - Git related feature, optional location -Fixed todos have to add `**DONE**` prefix to the type +Todos that have been merged have to add `**DONE**` prefix to the type ### #0 -[FEAT] - [side_nav](/src/ui/gui/components/side_nav.rs) -Add dropdown menu for `side_nav` playlist +**DONE** ~~[FEAT] - [side_nav](/src/ui/gui/components/mod.rs) +Add dropdown menu for `side_nav` playlist~~ ### #1 [FEAT] - [gui](/src/ui/gui/) @@ -25,7 +25,7 @@ Better styling Add music player footer ### #4 -[FEAT] - [gui](/src/ui/gui/components/song_list.rs) +[FEAT] - [gui](/src/ui/gui/components/song_list/mod.rs) Add numbers to `song_list` table ### #5 @@ -33,7 +33,7 @@ Add numbers to `song_list` table Add music player logic ### #6 -[FEAT] - [manifest](/src/manifest/) +[FEAT] - [manifest](/src/manifest/mod.rs) Add support for images by possibly storing the images in json or custom format ### #7 @@ -47,7 +47,7 @@ standalone one, moving default paths and using [#10](#10): | music-output | `~/Music/mcmg/*` | `%userprofile%/Music/mcmg/*` | ### #8 -[FEAT] - [cli](/src/ui/cli/) +[FEAT] - [cli](/src/ui/cli/mod.rs) add missing commands that are available via gui - Downloading single songs, from the manifest and standalone as an utility @@ -78,3 +78,7 @@ Add custom type for downloading, one for simple http downloads, and archived one ### #15 [FEAT] - [dependencies](/Cargo.toml) Clean up dependencies, remove unneeded features for executable size + +### #16 +[FEAT] - [song_list](/src/ui/gui/components/song_list/mod.rs) +Add a checkmark or an X depending on if the song is downloaded to disk diff --git a/src/downloader.rs b/src/downloader.rs index 1089598..8e53ffa 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -82,10 +82,10 @@ impl Downloader { for (name, playlist) in manifest.get_playlists() { for (song_name, song) in playlist.get_songs() { self.download_song(cfg, song_name, song, name, format)?; - self.count += crate::process_manager::wait_for_procs_untill(10)?; + self.count += crate::process_manager::wait_for_procs_until(10)?; } } - self.count += crate::process_manager::wait_for_procs_untill(0)?; + self.count += crate::process_manager::wait_for_procs_until(0)?; Ok(self.count) } diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index 1c9e015..5a2c9ba 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -66,6 +66,12 @@ impl Manifest { pub fn get_playlists_mut(&mut self) -> &mut HashMap { &mut self.playlists } + pub fn remove_playlist(&mut self, playlist_name: &String) -> Option { + self.playlists.remove(playlist_name) + } + pub fn remove_song(&mut self, playlist_name: &String, song_name: &String) -> Option { + self.get_playlist_mut(playlist_name)?.remove_song(song_name) + } pub fn get_song_count(&self) -> usize { let mut count = 0; for v in self.playlists.values() { diff --git a/src/process_manager.rs b/src/process_manager.rs index 951c416..eff3296 100644 --- a/src/process_manager.rs +++ b/src/process_manager.rs @@ -65,7 +65,7 @@ pub fn purge_done_procs() -> usize { } /// Waits for processes to finish until the proc count is lower or equal to `max` -pub fn wait_for_procs_untill(max: usize) -> anyhow::Result { +pub fn wait_for_procs_until(max: usize) -> anyhow::Result { // NOTE: This looks really fucked because i dont want to deadlock the processes so i lock PROCESSES for as little as possible // NOTE: So its also kinda really slow let mut finish_count = 0; diff --git a/src/ui/cli/add.rs b/src/ui/cli/add.rs index 4250b99..f453f1b 100644 --- a/src/ui/cli/add.rs +++ b/src/ui/cli/add.rs @@ -27,7 +27,7 @@ pub fn song(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downl if should_download { downloader.download_song(cfg, name, &song, playlist, manifest.get_format())?; - crate::process_manager::wait_for_procs_untill(0)?; + crate::process_manager::wait_for_procs_until(0)?; } Ok(()) diff --git a/src/ui/gui/components/context_menu.rs b/src/ui/gui/components/context_menu.rs deleted file mode 100644 index 2c54b4a..0000000 --- a/src/ui/gui/components/context_menu.rs +++ /dev/null @@ -1,57 +0,0 @@ -use egui::{Color32, RichText}; -use crate::{manifest::song::Song, ui::gui::windows::{song_edit::GuiSongEditor, WindowIndex}}; - -pub struct ContextMenu; - -// NOTE: This should be a component but theres no easy way to do that, so we just make it folow the -// trait manually, ish, more like a convention -impl /* ComponentUi for */ ContextMenu { - pub fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui, pname: &String, sname: &String, song: &Song) { - if ui.button("Edit").clicked() { - let w = gui.windows.get_window::(WindowIndex::SongEdit); - w.set_active_song(pname, sname, song.get_url_str(), song.get_type()); - gui.windows.open(WindowIndex::SongEdit, true); - ui.close_menu(); - } - - if ui.button("Download").clicked() { - if let Err(e) = gui.downloader.download_song_nb(&gui.cfg, pname, sname, song, gui.manifest.get_format()) { - log::error!("{e}"); - gui.throw_error(format!("Failed to download song {sname}: {e}")); - } - ui.close_menu(); - } - - if ui.button("Open Source").clicked() { - if let Err(e) = open::that(song.get_url_str()) { - log::error!("{e}"); - gui.throw_error(format!("Failed to open song source: {e}")); - } - ui.close_menu(); - } - if ui.button("Play").clicked() { - let p = crate::util::get_song_path(pname, sname, gui.manifest.get_format()); - - if !p.exists() { - gui.throw_error("Song does not exist on disk".to_string()); - } else if let Err(e) = open::that(p) { - log::error!("{e}"); - gui.throw_error(format!("Failed to play song: {e}")); - } - ui.close_menu(); - } - if ui.button("Delete from disk").clicked() { - let p = crate::util::get_song_path(pname, sname, gui.manifest.get_format()); - if p.exists() { - if let Err(e) = std::fs::remove_file(p) { - gui.throw_error(format!("Failed to delete file: {e}")); - } - } - ui.close_menu(); - } - if ui.button(RichText::new("Delete").color(Color32::RED)).clicked() { - gui.throw_error("TODO"); - ui.close_menu(); - } - } -} diff --git a/src/ui/gui/components/mod.rs b/src/ui/gui/components/mod.rs index b08f4ae..6e43c83 100644 --- a/src/ui/gui/components/mod.rs +++ b/src/ui/gui/components/mod.rs @@ -3,7 +3,6 @@ use super::Gui; pub mod nav; pub mod song_list; -pub mod context_menu; pub mod side_nav; pub mod search_bar; @@ -18,3 +17,8 @@ pub trait ComponentUi { pub trait ComponentUiMut { fn ui(&mut self, gui: &mut Gui, ui: &mut egui::Ui); } + +pub trait ComponentContextMenu { + type Data; + fn ui(gui: &mut Gui, ui: &mut egui::Ui, data: &Self::Data); +} diff --git a/src/ui/gui/components/side_nav/context_menu.rs b/src/ui/gui/components/side_nav/context_menu.rs new file mode 100644 index 0000000..9783d20 --- /dev/null +++ b/src/ui/gui/components/side_nav/context_menu.rs @@ -0,0 +1,51 @@ +use egui::{Color32, RichText, TextBuffer}; + +use crate::ui::gui::{components::ComponentContextMenu, windows::{self, WindowIndex}}; + + +#[derive(Debug)] +pub struct ContextMenu; + +impl ComponentContextMenu for ContextMenu { + type Data = String; // Playlist name + fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui, playlist_name: &Self::Data) { + if ui.button("Edit").clicked() { + ui.close_menu(); + } + + if ui.button("Download all").clicked() { + let Some(playlist) = gui.manifest.get_playlist(playlist_name) else { + gui.throw_error(&format!("Playlist not found: {}", playlist_name)); + ui.close_menu(); + return; + }; + + for (song_name, song) in playlist.get_songs() { + if let Err(e) = gui.downloader.download_song_nb(&gui.cfg, &playlist_name, song_name, song, gui.manifest.get_format()) { + gui.throw_error(&format!("Could not download song: {e}")); + ui.close_menu(); + return; + } + } + ui.close_menu(); + } + + if ui.button("Delete from disk").clicked() { + let p = crate::util::get_playlist_path(playlist_name); + if p.exists() { + if let Err(e) = std::fs::remove_dir_all(p) { + gui.throw_error(format!("Failed to delete directory: {e}")); + } + } + ui.close_menu(); + } + if ui.button(RichText::new("Delete").color(Color32::RED)).clicked() { + let w = gui.windows.get_window::(WindowIndex::Confirm); + w.set_message(&"side_nav_playlist_manifest_delete", &"This will delete the playlist from the manifest file. This is NOT reversible", &vec![playlist_name.clone()]); + gui.windows.open(WindowIndex::Confirm, true); + ui.close_menu(); + } + + + } +} diff --git a/src/ui/gui/components/side_nav.rs b/src/ui/gui/components/side_nav/mod.rs similarity index 52% rename from src/ui/gui/components/side_nav.rs rename to src/ui/gui/components/side_nav/mod.rs index 947a58a..ccd9fd7 100644 --- a/src/ui/gui/components/side_nav.rs +++ b/src/ui/gui/components/side_nav/mod.rs @@ -1,8 +1,9 @@ -use std::borrow::BorrowMut; +use egui::{Color32, Label, RichText, Sense}; +use crate::ui::gui::windows::{self, WindowIndex}; -use egui::{Button, Color32, Label, RichText, Sense}; +use super::{ComponentContextMenu, ComponentUi}; -use super::ComponentUi; +mod context_menu; @@ -22,9 +23,10 @@ impl ComponentUi for SideNav { gui.current_playlist = pname.to_string(); } ui.horizontal(|ui| { - let tint = Color32::from_hex("#333377").unwrap(); - ui.add(egui::Image::new(crate::data::NOTE_ICON).tint(tint)); + ui.add(egui::Image::new(crate::data::NOTE_ICON).tint(tint)) + .context_menu(|ui| context_menu::ContextMenu::ui(gui, ui, &pname)); + ui.horizontal(|ui| { let text; if gui.current_playlist == *pname { @@ -34,13 +36,38 @@ impl ComponentUi for SideNav { } let button = Label::new(text).sense(Sense::click()).selectable(false); - if ui.add(button).clicked() { + let button = ui.add(button); + if button.clicked() { gui.current_playlist = pname.to_string(); } + + button.context_menu(|ui| context_menu::ContextMenu::ui(gui, ui, &pname)); }); }); + } }); + + check_if_needs_delete(gui); } // #333377 } + + +fn check_if_needs_delete(gui: &mut crate::ui::gui::Gui) { + // Check for items that need to be deleted + let (id, resp, data) = gui.windows.get_window::(WindowIndex::Confirm).get_response(); + match (id.as_str(), resp) { + ("side_nav_playlist_manifest_delete", Some(true)) => { + gui.manifest.remove_playlist(&data[0]); + let _ = gui.manifest.save(None); + gui.windows.get_window::(WindowIndex::Confirm).reset(); + } + ("side_nav_playlist_manifest_delete", Some(false)) => { + log::debug!("FALSE"); + gui.windows.get_window::(WindowIndex::Confirm).reset(); + } + _ => () + } + +} diff --git a/src/ui/gui/components/song_list/context_menu.rs b/src/ui/gui/components/song_list/context_menu.rs new file mode 100644 index 0000000..d4fcdcd --- /dev/null +++ b/src/ui/gui/components/song_list/context_menu.rs @@ -0,0 +1,92 @@ +use egui::{Color32, RichText}; +use crate::{manifest::song::{Song, SongType}, ui::gui::windows::{self, song_edit::GuiSongEditor, WindowIndex}}; + +use super::ComponentContextMenu; + +pub struct ContextMenu; + +pub struct SongInfo { + pname: String, + sname: String, + song: Song, +} + +impl SongInfo { + pub fn new(pname: &String, sname: &String, song: &Song) -> Self { + Self { + pname: pname.clone(), + sname: sname.clone(), + song: song.clone() + } + } + pub fn playlist_name(&self) -> &String { + &self.pname + } + pub fn song_name(&self) -> &String { + &self.sname + } + pub fn song_url(&self) -> &String { + self.song.get_url_str() + } + pub fn song_type(&self) -> &SongType { + self.song.get_type() + } + pub fn song(&self) -> &Song { + &self.song + } +} + +impl ComponentContextMenu for ContextMenu { + type Data = SongInfo; + fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui, data: &Self::Data) { + if ui.button("Edit").clicked() { + let w = gui.windows.get_window::(WindowIndex::SongEdit); + w.set_active_song(data.playlist_name(), data.song_name(), data.song_url(), data.song_type()); + gui.windows.open(WindowIndex::SongEdit, true); + ui.close_menu(); + } + + if ui.button("Download").clicked() { + if let Err(e) = gui.downloader.download_song_nb(&gui.cfg, data.playlist_name(), data.song_name(), data.song(), gui.manifest.get_format()) { + log::error!("{e}"); + gui.throw_error(format!("Failed to download song {}: {e}", data.song_name())); + } + ui.close_menu(); + } + + if ui.button("Open Source").clicked() { + if let Err(e) = open::that(data.song_url()) { + log::error!("{e}"); + gui.throw_error(format!("Failed to open song source: {e}")); + } + ui.close_menu(); + } + if ui.button("Play").clicked() { + let p = crate::util::get_song_path(data.playlist_name(), data.song_name(), gui.manifest.get_format()); + + if !p.exists() { + gui.throw_error("Song does not exist on disk".to_string()); + } else if let Err(e) = open::that(p) { + log::error!("{e}"); + gui.throw_error(format!("Failed to play song: {e}")); + } + ui.close_menu(); + } + if ui.button("Delete from disk").clicked() { + let p = crate::util::get_song_path(data.playlist_name(), data.song_name(), gui.manifest.get_format()); + if p.exists() { + if let Err(e) = std::fs::remove_file(p) { + gui.throw_error(format!("Failed to delete file: {e}")); + } + } + ui.close_menu(); + } + if ui.button(RichText::new("Delete").color(Color32::RED)).clicked() { + let w = gui.windows.get_window::(WindowIndex::Confirm); + w.set_message(&"song_list_song_manifest_delete", &"This will delete the song from the manifest file. This is NOT reversible", &vec![data.playlist_name().clone(), data.song_name().clone()]); + gui.windows.open(WindowIndex::Confirm, true); + ui.close_menu(); + ui.close_menu(); + } + } +} diff --git a/src/ui/gui/components/song_list.rs b/src/ui/gui/components/song_list/mod.rs similarity index 75% rename from src/ui/gui/components/song_list.rs rename to src/ui/gui/components/song_list/mod.rs index 287fc8b..a96374c 100644 --- a/src/ui/gui/components/song_list.rs +++ b/src/ui/gui/components/song_list/mod.rs @@ -1,13 +1,14 @@ use egui::Color32; use egui_extras::{Column, TableBuilder}; -use crate::manifest::song::SongType; +use crate::{manifest::song::SongType, ui::gui::windows::{self, WindowIndex}}; -use super::{context_menu::ContextMenu, search_bar::SearchType, ComponentUi}; +use super::{search_bar::SearchType, ComponentContextMenu, ComponentUi}; + +mod context_menu; #[derive(Debug, Default)] -pub struct SongList { -} +pub struct SongList; impl ComponentUi for SongList { fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui) { @@ -25,17 +26,8 @@ impl ComponentUi for SongList { .striped(true) .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) .resizable(true) - //.column(Column::auto()) - //.column(Column::auto()) - //.column( - // Column::remainder() - // .at_least(40.0) - // .clip(true) - // .resizable(true), - //) .column(Column::auto()) .column(Column::remainder()) - //.column(Column::remainder()) .min_scrolled_height(0.0) .max_scroll_height(available_height) .sense(egui::Sense::click()); @@ -55,10 +47,6 @@ impl ComponentUi for SongList { }; table.header(20.0, |mut header| { - // header.col(|_|{}); - //header.col(|ui| { - // ui.strong("Playlist");vec.sort_by_key(|name| name.to_lowercase()); - //}); header.col(|ui| { ui.strong("Source"); }); @@ -97,11 +85,8 @@ impl ComponentUi for SongList { (SearchType::Url, _) => (), } body.row(18.0, |mut row| { - - //row.col(|ui| { - // ui.label(pname.clone()) - // .context_menu(|ui| ContextMenu::ui(gui, ui, &pname, &sname, &s)); - //}); + let song_info = context_menu::SongInfo::new(&pname, &sname, &s); + row.col(|ui| { let color = match s.get_type() { @@ -111,20 +96,41 @@ impl ComponentUi for SongList { }; ui.colored_label(color, s.get_type().to_string()) - .context_menu(|ui| ContextMenu::ui(gui, ui, &pname, &sname, &s)); + .context_menu(|ui| context_menu::ContextMenu::ui(gui, ui, &song_info)); }); row.col(|ui| { ui.hyperlink_to(sname.clone(), s.get_url_str()) - .context_menu(|ui| ContextMenu::ui(gui, ui, &pname, &sname, &s)); + .context_menu(|ui| context_menu::ContextMenu::ui(gui, ui, &song_info)); }); row.response() - .context_menu(|ui| ContextMenu::ui(gui, ui, &pname, &sname, &s)); + .context_menu(|ui| context_menu::ContextMenu::ui(gui, ui, &song_info)); }); } }); }); }); + + check_if_needs_delete(gui); + } } + +fn check_if_needs_delete(gui: &mut crate::ui::gui::Gui) { + // Check for items that need to be deleted + let (id, resp, data) = gui.windows.get_window::(WindowIndex::Confirm).get_response(); + match (id.as_str(), resp) { + ("song_list_song_manifest_delete", Some(true)) => { + gui.manifest.remove_song(&data[0], &data[1]); + let _ = gui.manifest.save(None); + gui.windows.get_window::(WindowIndex::Confirm).reset(); + } + ("song_list_song_manifest_delete", Some(false)) => { + log::debug!("FALSE"); + gui.windows.get_window::(WindowIndex::Confirm).reset(); + } + _ => () + } + +} diff --git a/src/ui/gui/mod.rs b/src/ui/gui/mod.rs index 0477efd..2d6db98 100644 --- a/src/ui/gui/mod.rs +++ b/src/ui/gui/mod.rs @@ -51,7 +51,6 @@ impl Gui { Ok(()) } - #[allow(clippy::pedantic)] pub fn throw_error(&mut self, text: S) { let w = self.windows.get_window::(WindowIndex::Error); w.set_error_message(&text); @@ -94,5 +93,7 @@ impl eframe::App for Gui { }); }); }); + // Make sure we dont wait for any updates cause we depend on the gui code for downloads + ctx.request_repaint(); } } diff --git a/src/ui/gui/windows/confirm.rs b/src/ui/gui/windows/confirm.rs new file mode 100644 index 0000000..a1622d3 --- /dev/null +++ b/src/ui/gui/windows/confirm.rs @@ -0,0 +1,57 @@ +use egui::{Color32, Label, RichText, TextBuffer}; + +use super::{State, Window}; + + +#[allow(clippy::pedantic)] +#[derive(Debug, Default)] +pub struct ConfirmW { + id: String, + text: String, + response: Option, + data: Vec +} + + +impl Window for ConfirmW { + fn ui(&mut self, _: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()> { + let mut should_close = false; + egui::Window::new("Are you sure?").open(open).show(ctx, |ui| { + ui.vertical(|ui| { + ui.label(RichText::new("Are you sure you want to do this?").size(15.0).color(Color32::BLUE)); + ui.horizontal(|ui| { + ui.add(Label::new(self.text.clone()).wrap(true)); + }); + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + self.response = Some(false); + should_close = true; + } else if ui.button("Continue").clicked() { + self.response = Some(true); + should_close = true; + } + }) + }) + }); + if should_close { + *open = false; + } + Ok(()) + } +} + +impl ConfirmW { + pub fn set_message(&mut self, new_id: &S, text: &S, data: &Vec) { + self.text = text.to_string(); + self.id = new_id.to_string(); + self.data = data.clone(); + } + pub fn get_response(&self) -> (&String, &Option, &Vec) { + (&self.id, &self.response, &self.data) + } + pub fn reset(&mut self) { + self.id.clear(); + self.text.clear(); + self.response = None; + } +} diff --git a/src/ui/gui/windows/error.rs b/src/ui/gui/windows/error.rs index a10dd1c..406cc5d 100644 --- a/src/ui/gui/windows/error.rs +++ b/src/ui/gui/windows/error.rs @@ -16,7 +16,7 @@ impl Window for GuiError { .open(open) .show(ctx, |ui| { ui.vertical(|ui| { - ui.label(RichText::new("Error:").size(30.0).color(Color32::RED)); + ui.label(RichText::new("Error:").size(15.0).color(Color32::RED)); ui.horizontal(|ui| { ui.add(Label::new(self.text.clone()).wrap(true)); }) diff --git a/src/ui/gui/windows/mod.rs b/src/ui/gui/windows/mod.rs index 5eac44c..8df225d 100644 --- a/src/ui/gui/windows/mod.rs +++ b/src/ui/gui/windows/mod.rs @@ -5,6 +5,7 @@ pub mod song_edit; pub mod error; pub mod import_playlist; pub mod song_new; +pub mod confirm; pub trait Window: std::fmt::Debug { fn ui(&mut self, state: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()>; @@ -16,6 +17,7 @@ pub enum WindowIndex { ImportPlaylist, SongEdit, SongNew, + Confirm } @@ -38,6 +40,7 @@ impl WindowManager { windows.insert(WindowIndex::ImportPlaylist, Box::::default()); windows.insert(WindowIndex::SongEdit, Box::::default()); windows.insert(WindowIndex::SongNew, Box::::default()); + windows.insert(WindowIndex::Confirm, Box::::default()); Self { windows, ..Default::default() diff --git a/src/util.rs b/src/util.rs index d54884d..bf490be 100644 --- a/src/util.rs +++ b/src/util.rs @@ -70,4 +70,12 @@ pub fn get_song_path/*>*/(/*basepath: Option

,*/ pname: &S path.push(sname); path.set_extension(format.to_string()); path +} + +pub fn get_playlist_path(pname: &String) -> PathBuf { + let mut path = std::env::current_dir().unwrap_or_default(); + // TODO: Get this from cfg + path.push("out"); + path.push(pname); + path }