Did #0 (added context menu to side_nav), fixed some spelling errors, implemented deleting from manifest for both side_nav and song_list

This commit is contained in:
Gvidas Juknevičius 2024-10-10 22:42:04 +03:00
parent 84ab965b9d
commit 14c53d96c0
Signed by: MCorange
GPG Key ID: 12B1346D720B7FBB
16 changed files with 303 additions and 101 deletions

16
DEV.md
View File

@ -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

View File

@ -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)
}

View File

@ -66,6 +66,12 @@ impl Manifest {
pub fn get_playlists_mut(&mut self) -> &mut HashMap<String, playlist::Playlist> {
&mut self.playlists
}
pub fn remove_playlist(&mut self, playlist_name: &String) -> Option<playlist::Playlist> {
self.playlists.remove(playlist_name)
}
pub fn remove_song(&mut self, playlist_name: &String, song_name: &String) -> Option<Song> {
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() {

View File

@ -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<usize> {
pub fn wait_for_procs_until(max: usize) -> anyhow::Result<usize> {
// 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;

View File

@ -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(())

View File

@ -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::<GuiSongEditor>(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();
}
}
}

View File

@ -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);
}

View File

@ -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::<windows::confirm::ConfirmW>(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();
}
}
}

View File

@ -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::<windows::confirm::ConfirmW>(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::<windows::confirm::ConfirmW>(WindowIndex::Confirm).reset();
}
("side_nav_playlist_manifest_delete", Some(false)) => {
log::debug!("FALSE");
gui.windows.get_window::<windows::confirm::ConfirmW>(WindowIndex::Confirm).reset();
}
_ => ()
}
}

View File

@ -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::<GuiSongEditor>(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::<windows::confirm::ConfirmW>(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();
}
}
}

View File

@ -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| {
let song_info = context_menu::SongInfo::new(&pname, &sname, &s);
//row.col(|ui| {
// ui.label(pname.clone())
// .context_menu(|ui| ContextMenu::ui(gui, ui, &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::<windows::confirm::ConfirmW>(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::<windows::confirm::ConfirmW>(WindowIndex::Confirm).reset();
}
("song_list_song_manifest_delete", Some(false)) => {
log::debug!("FALSE");
gui.windows.get_window::<windows::confirm::ConfirmW>(WindowIndex::Confirm).reset();
}
_ => ()
}
}

View File

@ -51,7 +51,6 @@ impl Gui {
Ok(())
}
#[allow(clippy::pedantic)]
pub fn throw_error<S: ToString>(&mut self, text: S) {
let w = self.windows.get_window::<windows::error::GuiError>(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();
}
}

View File

@ -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<bool>,
data: Vec<String>
}
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<S: ToString>(&mut self, new_id: &S, text: &S, data: &Vec<String>) {
self.text = text.to_string();
self.id = new_id.to_string();
self.data = data.clone();
}
pub fn get_response(&self) -> (&String, &Option<bool>, &Vec<String>) {
(&self.id, &self.response, &self.data)
}
pub fn reset(&mut self) {
self.id.clear();
self.text.clear();
self.response = None;
}
}

View File

@ -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));
})

View File

@ -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::<import_playlist::GuiImportPlaylist>::default());
windows.insert(WindowIndex::SongEdit, Box::<song_edit::GuiSongEditor>::default());
windows.insert(WindowIndex::SongNew, Box::<song_new::GuiNewSong>::default());
windows.insert(WindowIndex::Confirm, Box::<confirm::ConfirmW>::default());
Self {
windows,
..Default::default()

View File

@ -71,3 +71,11 @@ pub fn get_song_path/*<P: TryInto<PathBuf>>*/(/*basepath: Option<P>,*/ pname: &S
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
}