Compare commits

..

2 Commits

Author SHA1 Message Date
f26fd97809
Modularised components, moved everythign there 2024-09-22 01:07:34 +03:00
2cde24e7a8
Rewrite the structure of the gui and cli interfaces
Added a more modular way to add windows
2024-09-22 00:42:50 +03:00
18 changed files with 680 additions and 493 deletions

31
\ Normal file
View File

@ -0,0 +1,31 @@
use std::collections::HashMap;
use super::Gui;
mod song_edit;
trait Window {
fn show(&mut self, gui: &mut Gui, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()>;
fn name(&self) -> &'static str;
}
pub enum WindowIndex {
SongEdit
}
pub struct WindowManager {
windows: HashMap<WindowIndex, Box<dyn Window>>
}
impl WindowManager {
pub fn new() -> Self {
let mut windows = HashMap::new();
windows.instert(WindowIndex::SongEdit, Box::<song_edit::GuiSongEditor>::default());
Self {
windows: HashMap::from_iter([
(WindowIndex::SongEdit, Box::<song_edit::GuiSongEditor>::default())
])
}
}
}

View File

@ -1,251 +0,0 @@
mod nav_bar;
mod song_edit_window;
use egui::{Color32, RichText};
use egui_extras::{Column, TableBuilder};
use song_edit_window::{GuiError, GuiImportPlaylist, GuiNewSong};
use crate::{config::ConfigWrapper, downloader::Downloader, manifest::{song::{Song, SongType}, Manifest}};
use self::song_edit_window::GuiSongEditor;
#[derive(Debug, Default)]
pub struct Gui {
manifest: Manifest,
song_edit_w: GuiSongEditor,
new_song_w: GuiNewSong,
import_playlist_w: GuiImportPlaylist,
error_w: GuiError,
filter: String,
downloader: Downloader,
cfg: ConfigWrapper,
downloading: bool,
}
impl Gui {
fn new(_: &eframe::CreationContext<'_>, manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> Self {
Self {
manifest,
downloader,
cfg,
..Default::default()
}
}
pub fn start(manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> anyhow::Result<()> {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0]),
// .with_icon(
// // NOTE: Adding an icon is optional
// eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..])
// .expect("Failed to load icon"),
// ),
..Default::default()
};
if let Err(e) = eframe::run_native(
"eframe template",
native_options,
Box::new(|cc| Box::new(Gui::new(cc, manifest, downloader, cfg))),
) {
log::error!("Failed to create window: {e}");
};
Ok(())
}
pub fn throw_error(&mut self, text: impl ToString) {
self.error_w.is_open = true;
self.error_w.text = text.to_string();
}
}
impl eframe::App for Gui {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.draw_nav(ctx, frame);
self.draw_song_edit_window(ctx, frame);
self.draw_new_song_window(ctx, frame);
self.draw_import_playlist_window(ctx, frame);
self.draw_error_window(ctx, frame);
egui::CentralPanel::default().show(ctx, |ui| {
// The central panel the region left after adding TopPanel's and SidePanel's
//ui.heading(format!("Songs ({})", self.manifest.get_song_count()));
let fltr_by;
let filter_clean;
if self.filter.starts_with("playlist:") {
fltr_by = "playlist";
filter_clean = self.filter.strip_prefix("playlist:").unwrap_or("").to_string().to_lowercase();
} else if self.filter.starts_with("source:") {
fltr_by = "source";
filter_clean = self.filter.strip_prefix("source:").unwrap_or("").to_string().to_lowercase();
} else if self.filter.starts_with("url:") {
fltr_by = "url";
filter_clean = self.filter.strip_prefix("url:").unwrap_or("").to_string();
} else {
fltr_by = "";
filter_clean = self.filter.clone();
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.colored_label(Color32::from_hex("#4444aa").unwrap(), "Filter: ");
ui.text_edit_singleline(&mut self.filter);
});
});
ui.vertical(|ui| {
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.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());
let playlists = self.manifest.get_playlists().clone();
let songs = {
let mut songs = Vec::new();
for (pname, p) in playlists {
for (sname, s) in p {
songs.push((pname.clone(), sname, s))
}
}
songs
};
table.header(20.0, |mut header| {
// header.col(|_|{});
header.col(|ui| {
ui.strong("Playlist");
});
header.col(|ui| {
ui.strong("Source");
});
header.col(|ui| {
ui.strong("Name");
});
}).body(|mut body| {
for (pname, sname, s) in songs {
if fltr_by == "playlist" && !filter_clean.is_empty() {
if !pname.to_lowercase().contains(&filter_clean) {
continue;
}
} else if fltr_by == "type" && !filter_clean.is_empty(){
if !s.get_type().to_string().to_lowercase().contains(&filter_clean) {
continue;
}
} else if fltr_by == "url" && !filter_clean.is_empty(){
if !s.get_url_str().contains(&filter_clean) {
continue;
}
} else if !filter_clean.is_empty() {
if !sname.to_lowercase().contains(&filter_clean) {
continue;
}
}
body.row(18.0, |mut row| {
row.col(|ui| {
ui.label(pname.clone())
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
});
row.col(|ui| {
let color =
match s.get_type() {
SongType::Youtube => Color32::from_hex("#FF0000").unwrap(),
SongType::Spotify => Color32::from_hex("#1db954").unwrap(),
SongType::Soundcloud => Color32::from_hex("#F26F23").unwrap()
};
ui.colored_label(color, s.get_type().to_string())
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
});
row.col(|ui| {
ui.hyperlink_to(sname.clone(), s.get_url_str())
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
});
row.response()
.context_menu(|ui| context_menu(self, ui, &pname, &sname, &s));
fn context_menu(this: &mut Gui, ui: &mut egui::Ui, pname: &String, sname: &String, song: &Song) {
if ui.button("Edit").clicked() {
this.song_edit_w.song = (
pname.clone(),
sname.clone(),
);
this.song_edit_w.is_open = true;
this.song_edit_w.ed_name = sname.clone();
this.song_edit_w.ed_url = song.get_url_str().clone();
ui.close_menu()
}
if ui.button("Download").clicked() {
if let Err(e) = this.downloader.download_song_nb(&this.cfg, pname, sname, song, this.manifest.get_format()) {
log::error!("{e}");
this.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}");
this.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, this.manifest.get_format());
if !p.exists() {
this.throw_error(format!("Song does not exist on disk"));
} else if let Err(e) = open::that(p) {
log::error!("{e}");
this.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, this.manifest.get_format());
if p.exists() {
if let Err(e) = std::fs::remove_file(p) {
this.throw_error(format!("Failed to delete file: {e}"));
}
}
ui.close_menu();
}
if ui.button(RichText::new("Delete").color(Color32::RED)).clicked() {
this.throw_error("TODO");
ui.close_menu()
}
}
})
}
});
});
ui.separator();
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
egui::warn_if_debug_build(ui);
});
});
}
}

View File

@ -1,223 +0,0 @@
use egui::{Color32, Label, RichText};
use crate::manifest::song::{Song, SongType};
use super::Gui;
#[derive(Debug, Default)]
pub struct GuiSongEditor {
pub is_open: bool,
pub song: (String, String),
pub ed_url: String,
pub ed_name: String,
}
#[derive(Debug, Default)]
pub struct GuiNewSong {
pub is_open: bool,
ed_type: SongType,
ed_name: String,
ed_playlist: Option<String>,
ed_url: String,
}
#[derive(Debug, Default)]
pub struct GuiImportPlaylist {
pub is_open: bool,
ed_name: String,
ed_url: String,
}
#[derive(Debug, Default)]
pub struct GuiError {
pub is_open: bool,
pub text: String,
}
impl Gui {
pub fn draw_song_edit_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
let mut save = false;
let (playlist, song_name) = self.song_edit_w.song.clone();
let Some(song) = self.manifest.get_song(&playlist, &song_name) else {
return;
};
let song = song.clone();
egui::Window::new("Song editor")
.open(&mut self.song_edit_w.is_open)
.show(ctx,
|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("[");
ui.hyperlink_to("link", song.get_url().unwrap());
ui.label("] ");
ui.colored_label(Color32::LIGHT_BLUE, &playlist);
ui.label(": ");
ui.label(&song_name)
});
ui.horizontal(|ui| {
ui.label("Type: ");
ui.label(&song.get_type().to_string());
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.song_edit_w.ed_name);
});
ui.horizontal(|ui| {
ui.label("Url: ");
ui.text_edit_singleline(&mut self.song_edit_w.ed_url);
});
if ui.button("Save").clicked() {
save = true;
}
});
if save {
{
let Some(song) = self.manifest.get_song_mut(&playlist, &song_name) else {
return;
};
*song.get_url_str_mut() = self.song_edit_w.ed_url.clone();
}
let Some(playlist) = self.manifest.get_playlist_mut(&playlist) else {
return;
};
playlist.remove_song(&song_name);
playlist.add_song(self.song_edit_w.ed_name.clone(), song);
self.song_edit_w.is_open = false;
let _ = self.manifest.save(None);
}
}
pub fn draw_new_song_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
let mut save = false;
egui::Window::new("New song")
.open(&mut self.new_song_w.is_open)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Type: ");
egui::ComboBox::from_id_source("new_song_window_type")
.selected_text(format!("{:?}", self.new_song_w.ed_type))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.new_song_w.ed_type, SongType::Youtube, "Youtube");
ui.selectable_value(&mut self.new_song_w.ed_type, SongType::Spotify, "Spotify");
ui.selectable_value(&mut self.new_song_w.ed_type, SongType::Soundcloud, "Soundcloud");
}
);
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.new_song_w.ed_name);
});
ui.horizontal(|ui| {
ui.label("Playlist: ");
egui::ComboBox::from_id_source("new_song_window_playlist")
.selected_text(format!("{}", self.new_song_w.ed_playlist.clone().unwrap_or("".to_string())))
.show_ui(ui, |ui| {
for p in self.manifest.get_playlists().keys() {
ui.selectable_value(&mut self.new_song_w.ed_playlist, Option::Some(p.clone()), p.as_str());
}
}
);
});
ui.horizontal(|ui| {
ui.label("Url: ");
ui.text_edit_singleline(&mut self.new_song_w.ed_url);
});
if ui.button("Save").clicked() {
save = true;
}
});
if save {
let Some(playlist) = self.manifest.get_playlist_mut(&self.new_song_w.ed_playlist.clone().unwrap()) else {
panic!("couldnt find playlist from a preset playlist list????????????");
};
playlist.add_song(
self.new_song_w.ed_name.clone(),
Song::from_url_str(self.new_song_w.ed_url.clone()).unwrap().set_type(self.new_song_w.ed_type.clone()).clone()
);
let _ = self.manifest.save(None);
self.new_song_w.is_open = false;
}
}
pub fn draw_import_playlist_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
let mut save = false;
egui::Window::new("Import Playlist")
.open(&mut self.import_playlist_w.is_open)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Type: Youtube");
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.import_playlist_w.ed_name);
});
ui.horizontal(|ui| {
ui.label("Url: ");
ui.text_edit_singleline(&mut self.import_playlist_w.ed_url);
});
if ui.button("Import").clicked() {
save = true;
}
});
if save {
let name = self.import_playlist_w.ed_name.clone();
let url = self.import_playlist_w.ed_url.clone();
if self.manifest.get_playlist(&name).is_some() {
log::error!("Playlist {name} already exists");
self.throw_error(format!("Playlist {name} already exists"));
}
let songs = self.downloader.download_playlist_nb(&self.cfg, &url, &name, &self.manifest.get_format()).unwrap();
self.manifest.add_playlist(name.clone());
let playlist = self.manifest.get_playlist_mut(&name).expect("Unreachable");
for (sname, song) in songs {
log::info!("Added: {sname}");
playlist.add_song(sname, song);
}
let _ = self.manifest.save(None);
self.import_playlist_w.is_open = false;
}
}
pub fn draw_error_window(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
egui::Window::new("ERROR!!!! D:")
.open(&mut self.error_w.is_open)
.show(ctx, |ui| {
ui.vertical(|ui| {
ui.label(RichText::new("Error:").size(30.0).color(Color32::RED));
ui.horizontal(|ui| {
ui.add(Label::new(self.error_w.text.clone()).wrap(true));
})
})
});
}
}

View File

@ -18,7 +18,7 @@ lazy_static!(
static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new())); static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new()));
); );
#[derive(Debug, Default)] #[derive(Debug, Default, Clone)]
pub struct Downloader { pub struct Downloader {
count: usize, count: usize,
nb_initial_song_count: usize, nb_initial_song_count: usize,

View File

@ -1,3 +1,5 @@
#![feature(downcast_unchecked)]
use config::ConfigWrapper; use config::ConfigWrapper;
@ -6,11 +8,11 @@ mod manifest;
mod logger; mod logger;
mod downloader; mod downloader;
mod util; mod util;
mod commands;
mod prompt; mod prompt;
mod config; mod config;
mod constants; mod constants;
mod process_manager; mod process_manager;
mod ui;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -27,5 +29,5 @@ async fn main() {
}; };
let _ = commands::command_run(&cfg, &mut manifest).await; let _ = ui::cli::command_run(&cfg, &mut manifest).await;
} }

View File

@ -1,7 +1,6 @@
mod add; mod add;
pub mod gui;
use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest}; use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest, ui::gui};

View File

@ -0,0 +1,57 @@
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());
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(format!("Song does not exist on disk"));
} 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

@ -0,0 +1,14 @@
use super::Gui;
pub mod nav;
pub mod song_list;
pub mod context_menu;
pub trait Component {
fn ui(gui: &mut Gui, ctx: &egui::Context);
}
pub trait ComponentUi {
fn ui(gui: &mut Gui, ui: &mut egui::Ui);
}

View File

@ -1,8 +1,13 @@
use super::Gui; use crate::ui::gui::{windows::WindowIndex, Gui};
impl Gui { use super::Component;
pub fn draw_nav(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
pub struct NavBar;
impl Component for NavBar {
fn ui(gui: &mut Gui, ctx: &egui::Context) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
// The top panel is often a good place for a menu bar: // The top panel is often a good place for a menu bar:
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
@ -11,7 +16,7 @@ impl Gui {
ctx.open_url(egui::OpenUrl::new_tab("https://git.mcorangehq.xyz/XOR64/music")); ctx.open_url(egui::OpenUrl::new_tab("https://git.mcorangehq.xyz/XOR64/music"));
} }
if ui.button("Save").clicked() { if ui.button("Save").clicked() {
if let Err(e) = self.manifest.save(None) { if let Err(e) = gui.manifest.save(None) {
log::error!("Failed to save manifest: {e}"); log::error!("Failed to save manifest: {e}");
} }
} }
@ -22,19 +27,19 @@ impl Gui {
ui.menu_button("Song", |ui| { ui.menu_button("Song", |ui| {
if ui.button("Add New").clicked() { if ui.button("Add New").clicked() {
self.new_song_w.is_open = true; gui.windows.open(WindowIndex::SongNew, true);
} }
}); });
ui.menu_button("Playlist", |ui| { ui.menu_button("Playlist", |ui| {
if ui.button("Import").clicked() { if ui.button("Import").clicked() {
self.import_playlist_w.is_open = true; gui.windows.open(WindowIndex::ImportPlaylist, true);
} }
}); });
ui.menu_button("Downloader", |ui| { ui.menu_button("Downloader", |ui| {
if ui.button("Download All").clicked() { if ui.button("Download All").clicked() {
if let Err(e) = self.downloader.download_all_nb(&self.manifest, &self.cfg) { if let Err(e) = gui.downloader.download_all_nb(&gui.manifest, &gui.cfg) {
log::error!("Err: {e}"); log::error!("Err: {e}");
} }
} }
@ -42,21 +47,22 @@ impl Gui {
ui.add_space(16.0); ui.add_space(16.0);
ui.with_layout(egui::Layout::bottom_up(egui::Align::RIGHT), |ui| { ui.with_layout(egui::Layout::bottom_up(egui::Align::RIGHT), |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if self.downloader.get_songs_left_nb() > 0 { if gui.downloader.get_songs_left_nb() > 0 {
self.downloading = true; gui.downloading = true;
ui.label(format!("Downloading: {}/{}", self.downloader.get_songs_left_nb(), self.downloader.get_initial_song_count_nb())); ui.label(format!("Downloading: {}/{}", gui.downloader.get_songs_left_nb(), gui.downloader.get_initial_song_count_nb()));
} else if self.downloading { } else if gui.downloading {
let _ = notify_rust::Notification::new() let _ = notify_rust::Notification::new()
.summary("Done downloading") .summary("Done downloading")
.body("Your music has been downloaded") .body("Your music has been downloaded")
.show(); .show();
self.downloading = false; gui.downloading = false;
} }
let _ = self.downloader.download_all_nb_poll(&self.cfg); let _ = gui.downloader.download_all_nb_poll(&gui.cfg);
egui::widgets::global_dark_light_mode_buttons(ui); egui::widgets::global_dark_light_mode_buttons(ui);
}); });
}); });
}); });
}); });
}
}
} }

View File

@ -0,0 +1,130 @@
use egui::{Color32, RichText};
use egui_extras::{Column, TableBuilder};
use crate::manifest::song::SongType;
use super::{context_menu::ContextMenu, ComponentUi};
pub struct SongList;
impl ComponentUi for SongList {
fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui) {
let fltr_by;
let filter_clean;
if gui.filter.starts_with("playlist:") {
fltr_by = "playlist";
filter_clean = gui.filter.strip_prefix("playlist:").unwrap_or("").to_string().to_lowercase();
} else if gui.filter.starts_with("source:") {
fltr_by = "source";
filter_clean = gui.filter.strip_prefix("source:").unwrap_or("").to_string().to_lowercase();
} else if gui.filter.starts_with("url:") {
fltr_by = "url";
filter_clean = gui.filter.strip_prefix("url:").unwrap_or("").to_string();
} else {
fltr_by = "";
filter_clean = gui.filter.clone();
}
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.colored_label(Color32::from_hex("#4444aa").unwrap(), "Filter: ");
ui.text_edit_singleline(&mut gui.filter);
});
});
ui.vertical(|ui| {
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.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());
let playlists = gui.manifest.get_playlists().clone();
let songs = {
let mut songs = Vec::new();
for (pname, p) in playlists {
for (sname, s) in p {
songs.push((pname.clone(), sname, s))
}
}
songs
};
table.header(20.0, |mut header| {
// header.col(|_|{});
header.col(|ui| {
ui.strong("Playlist");
});
header.col(|ui| {
ui.strong("Source");
});
header.col(|ui| {
ui.strong("Name");
});
}).body(|mut body| {
for (pname, sname, s) in songs {
if fltr_by == "playlist" && !filter_clean.is_empty() {
if !pname.to_lowercase().contains(&filter_clean) {
continue;
}
} else if fltr_by == "type" && !filter_clean.is_empty(){
if !s.get_type().to_string().to_lowercase().contains(&filter_clean) {
continue;
}
} else if fltr_by == "url" && !filter_clean.is_empty(){
if !s.get_url_str().contains(&filter_clean) {
continue;
}
} else if !filter_clean.is_empty() {
if !sname.to_lowercase().contains(&filter_clean) {
continue;
}
}
body.row(18.0, |mut row| {
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() {
SongType::Youtube => Color32::from_hex("#FF0000").unwrap(),
SongType::Spotify => Color32::from_hex("#1db954").unwrap(),
SongType::Soundcloud => Color32::from_hex("#F26F23").unwrap()
};
ui.colored_label(color, s.get_type().to_string())
.context_menu(|ui| ContextMenu::ui(gui, ui, &pname, &sname, &s));
});
row.col(|ui| {
ui.hyperlink_to(sname.clone(), s.get_url_str())
.context_menu(|ui| ContextMenu::ui(gui, ui, &pname, &sname, &s));
});
row.response()
.context_menu(|ui| ContextMenu::ui(gui, ui, &pname, &sname, &s));
})
}
});
});
}
}

86
src/ui/gui/mod.rs Normal file
View File

@ -0,0 +1,86 @@
mod windows;
mod components;
use components::{Component, ComponentUi};
use windows::{State, WindowIndex, WindowManager};
use crate::{config::ConfigWrapper, downloader::Downloader, manifest::Manifest};
#[derive(Debug, Default)]
pub struct Gui {
windows: WindowManager,
manifest: Manifest,
filter: String,
downloader: Downloader,
cfg: ConfigWrapper,
downloading: bool,
}
impl Gui {
fn new(_: &eframe::CreationContext<'_>, manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> Self {
Self {
manifest,
downloader,
cfg,
windows: windows::WindowManager::new(),
..Default::default()
}
}
pub fn start(manifest: Manifest, downloader: Downloader, cfg: ConfigWrapper) -> anyhow::Result<()> {
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0]),
// .with_icon(
// // NOTE: Adding an icon is optional
// eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..])
// .expect("Failed to load icon"),
// ),
..Default::default()
};
if let Err(e) = eframe::run_native(
"eframe template",
native_options,
Box::new(|cc| Box::new(Gui::new(cc, manifest, downloader, cfg))),
) {
log::error!("Failed to create window: {e}");
};
Ok(())
}
pub fn throw_error(&mut self, text: impl ToString) {
let w = self.windows.get_window::<windows::error::GuiError>(WindowIndex::Error);
w.set_error_message(text);
self.windows.open(WindowIndex::Error, true);
}
}
impl eframe::App for Gui {
fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) {
components::nav::NavBar::ui(self, ctx);
{
let mut state = State {
cfg: self.cfg.clone(),
downloader: self.downloader.clone(),
manifest: self.manifest.clone(),
};
self.windows.ui(&mut state, ctx).unwrap();
self.cfg = state.cfg;
self.downloader = state.downloader;
self.manifest = state.manifest;
}
egui::CentralPanel::default().show(ctx, |ui| {
// The central panel the region left after adding TopPanel's and SidePanel's
//ui.heading(format!("Songs ({})", self.manifest.get_song_count()));
components::song_list::SongList::ui(self, ui);
ui.separator();
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
egui::warn_if_debug_build(ui);
});
});
}
}

View File

@ -0,0 +1,32 @@
use egui::{Color32, Label, RichText};
use super::{State, Window};
#[derive(Debug, Default)]
pub struct GuiError {
text: String,
}
impl Window for GuiError {
fn ui(&mut self, _: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()> {
egui::Window::new("ERROR!!!! D:")
.open(open)
.show(ctx, |ui| {
ui.vertical(|ui| {
ui.label(RichText::new("Error:").size(30.0).color(Color32::RED));
ui.horizontal(|ui| {
ui.add(Label::new(self.text.clone()).wrap(true));
})
})
});
Ok(())
}
}
impl GuiError {
pub fn set_error_message<S: ToString>(&mut self, text: S) {
self.text = text.to_string();
}
}

View File

@ -0,0 +1,59 @@
use super::{State, Window};
#[derive(Debug, Default)]
pub struct GuiImportPlaylist {
ed_name: String,
ed_url: String,
}
impl Window for GuiImportPlaylist {
fn ui(&mut self, state: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()> {
let mut save = false;
egui::Window::new("Import Playlist")
.open(open)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Type: Youtube");
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.ed_name);
});
ui.horizontal(|ui| {
ui.label("Url: ");
ui.text_edit_singleline(&mut self.ed_url);
});
if ui.button("Import").clicked() {
save = true;
}
});
if save {
let name = self.ed_name.clone();
let url = self.ed_url.clone();
if state.manifest.get_playlist(&name).is_some() {
log::error!("Playlist {name} already exists");
}
let songs = state.downloader.download_playlist_nb(&state.cfg, &url, &name, &state.manifest.get_format()).unwrap();
state.manifest.add_playlist(name.clone());
let playlist = state.manifest.get_playlist_mut(&name).expect("Unreachable");
for (sname, song) in songs {
log::info!("Added: {sname}");
playlist.add_song(sname, song);
}
let _ = state.manifest.save(None);
*open = false;
}
Ok(())
}
}

76
src/ui/gui/windows/mod.rs Normal file
View File

@ -0,0 +1,76 @@
use std::collections::HashMap;
use crate::{config::ConfigWrapper, downloader::Downloader, manifest::Manifest};
pub mod song_edit;
pub mod error;
pub mod import_playlist;
pub mod song_new;
pub trait Window: std::fmt::Debug {
fn ui(&mut self, state: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()>;
}
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
pub enum WindowIndex {
Error,
ImportPlaylist,
SongEdit,
SongNew,
}
#[derive(Debug,Default)]
pub struct WindowManager {
opened: HashMap<WindowIndex, bool>,
windows: HashMap<WindowIndex, Box<dyn Window>>
}
pub struct State {
pub downloader: Downloader,
pub manifest: Manifest,
pub cfg: ConfigWrapper,
}
impl WindowManager {
pub fn new() -> Self {
let mut windows: HashMap<WindowIndex, Box<dyn Window>> = HashMap::new();
windows.insert(WindowIndex::Error, Box::<error::GuiError>::default());
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());
Self {
windows,
..Default::default()
}
}
#[allow(dead_code)]
pub fn is_open(&self, id: &WindowIndex) -> bool {
*self.opened.get(id).unwrap()
}
pub fn open(&mut self, id: WindowIndex, open: bool) {
self.opened.insert(id, open);
}
pub fn ui(&mut self, state: &mut State, ctx: &egui::Context) -> anyhow::Result<()> {
for (id, window) in &mut self.windows {
if !self.opened.contains_key(&id) {
self.opened.insert(*id, false);
}
let open = self.opened.get_mut(id).unwrap();
if let Err(e) = window.ui(state, ctx, open) {
log::error!("Window {id:?} errored: {e}");
}
}
Ok(())
}
pub fn get_window<T: Window + 'static>(&mut self, id: WindowIndex) -> &mut Box<T> {
let w = self.windows.get_mut(&id).unwrap();
unsafe {
crate::util::as_any_mut(w).downcast_mut_unchecked()
}
}
}

View File

@ -0,0 +1,95 @@
use anyhow::{Result, bail};
use egui::Color32;
use crate::ui::gui::Gui;
use super::{State, Window};
#[derive(Debug, Default)]
pub struct GuiSongEditor {
song: (String, String),
ed_url: String,
ed_name: String,
}
impl Window for GuiSongEditor {
fn ui(&mut self, state: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()> {
let mut save = false;
let (playlist, song_name) = self.song.clone();
if playlist.is_empty() {
return Ok(());
}
let Some(song) = state.manifest.get_song(&playlist, &song_name) else {
bail!("Failed to get song (1)");
};
let song = song.clone();
egui::Window::new("Song editor")
.open(open)
.show(ctx,
|ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("[");
ui.hyperlink_to("link", song.get_url().unwrap());
ui.label("] ");
ui.colored_label(Color32::LIGHT_BLUE, &playlist);
ui.label(": ");
ui.label(&song_name)
});
ui.horizontal(|ui| {
ui.label("Type: ");
ui.label(&song.get_type().to_string());
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.ed_name);
});
ui.horizontal(|ui| {
ui.label("Url: ");
ui.text_edit_singleline(&mut self.ed_url);
});
if ui.button("Save").clicked() {
save = true;
}
});
if save {
{
let Some(song) = state.manifest.get_song_mut(&playlist, &song_name) else {
bail!("Failed to get song (2)");
};
*song.get_url_str_mut() = self.ed_url.clone();
}
let Some(playlist) = state.manifest.get_playlist_mut(&playlist) else {
bail!("Failed to get playlist");
};
playlist.remove_song(&song_name);
playlist.add_song(self.ed_name.clone(), song);
*open = false;
let _ = state.manifest.save(None);
}
Ok(())
}
}
impl GuiSongEditor {
pub fn set_active_song(&mut self, pname: &String, sname: &String, url: &String) {
self.song.0 = pname.clone();
self.song.1 = sname.clone();
self.ed_name = sname.clone();
self.ed_url = url.clone();
}
}

View File

@ -0,0 +1,72 @@
use crate::manifest::song::{Song, SongType};
use super::{State, Window};
#[derive(Debug, Default)]
pub struct GuiNewSong {
ed_type: SongType,
ed_name: String,
ed_playlist: Option<String>,
ed_url: String,
}
impl Window for GuiNewSong {
fn ui(&mut self, state: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()> {
let mut save = false;
egui::Window::new("New song")
.open(open)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Type: ");
egui::ComboBox::from_id_source("new_song_window_type")
.selected_text(format!("{:?}", self.ed_type))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.ed_type, SongType::Youtube, "Youtube");
ui.selectable_value(&mut self.ed_type, SongType::Spotify, "Spotify");
ui.selectable_value(&mut self.ed_type, SongType::Soundcloud, "Soundcloud");
}
);
});
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut self.ed_name);
});
ui.horizontal(|ui| {
ui.label("Playlist: ");
egui::ComboBox::from_id_source("new_song_window_playlist")
.selected_text(format!("{}", self.ed_playlist.clone().unwrap_or("".to_string())))
.show_ui(ui, |ui| {
for p in state.manifest.get_playlists().keys() {
ui.selectable_value(&mut self.ed_playlist, Option::Some(p.clone()), p.as_str());
}
}
);
});
ui.horizontal(|ui| {
ui.label("Url: ");
ui.text_edit_singleline(&mut self.ed_url);
});
if ui.button("Save").clicked() {
save = true;
}
});
if save {
let Some(playlist) = state.manifest.get_playlist_mut(&self.ed_playlist.clone().unwrap()) else {
panic!("couldnt find playlist from a preset playlist list????????????");
};
playlist.add_song(
self.ed_name.clone(),
Song::from_url_str(self.ed_url.clone()).unwrap().set_type(self.ed_type.clone()).clone()
);
let _ = state.manifest.save(None);
*open = false;
}
Ok(())
}
}

2
src/ui/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod gui;
pub mod cli;