Compare commits
10 Commits
2ddb2b8a89
...
712e918e08
| Author | SHA1 | Date | |
|---|---|---|---|
| 712e918e08 | |||
| 787c01b9dc | |||
| e0201ccb50 | |||
| 7edb257e2f | |||
| dfc55837ee | |||
| 0435d33e58 | |||
| 193029d4e1 | |||
| 14c53d96c0 | |||
| 84ab965b9d | |||
| 2e57642aa3 |
37
.gitea/workflows/ci.yml
Normal file
37
.gitea/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
on: [push, pull_request]
|
||||
|
||||
name: Continuous integration
|
||||
|
||||
jobs:
|
||||
#check:
|
||||
# name: Check
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
# - run: cargo check
|
||||
|
||||
#test:
|
||||
# name: Test Suite
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
# - run: cargo test
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- run: rustup component add clippy
|
||||
- run: cargo clippy
|
||||
|
||||
build:
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- run: cargo build
|
||||
31
DEV.md
31
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,9 +47,10 @@ 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
|
||||
- removing playlists, single songs
|
||||
|
||||
### #9
|
||||
[BUG] - [utils](/src/util.rs)
|
||||
|
|
@ -61,12 +62,24 @@ Add an utility to detect if this is ran as a standalone application
|
|||
|
||||
### #11
|
||||
[FEAT] - [downloader](/src/downloader.rs)
|
||||
Refractor for better readability and usage
|
||||
Refractor downloader for better readability and usage
|
||||
|
||||
### #12
|
||||
[GIT]
|
||||
Add ci that runs clippy and builds in release mode
|
||||
|
||||
Constant todos:
|
||||
TODO: Run code through clippy and fix any errors
|
||||
### #13
|
||||
[FEAT] - [assets](/assets/)
|
||||
Make new icons for the app, preferably svg, except the app icon must be both svg and png
|
||||
|
||||
### #14
|
||||
[FEAT] - [manifest](/src/manifest/) [downloader](/src/downloader.rs)
|
||||
Add custom type for downloading, one for simple http downloads, and archived ones (zip, 7z, etc)
|
||||
|
||||
### #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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ The music is downloaded via [ytdlp](#Dependencies) and [spotdl](#Dependencies),
|
|||
|
||||
## Offline usage
|
||||
Your whole music library is downloaded to your music folder (unless its being ran in standalone mode). Saved in your selected format.
|
||||
All of the info required to download your songs is stored in 1 file (!). So all you need to backup all of your music is just 1 relatively small file AND you get the added benefit of easily moving your music between devices with just 1 manifest file, 1 executable (and 3 [dependencies](#dependencies)). Just press `download all` and see as all of your playlists appear in your hard drive, powereded by Open Source software.
|
||||
All of the info required to download your songs is stored in 1 file (!). So all you need to backup all of your music is just 1 relatively small file AND you get the added benefit of easily moving your music between devices with just 1 manifest file, 1 executable (and 3 [dependencies](#dependencies)). Just press `download all` and see as all of your playlists appear in your hard drive, powered by Open Source software.
|
||||
|
||||
## Dependencies
|
||||
[ffmpeg](https://ffmpeg.org/): To convert your music files to your desired format.
|
||||
|
|
|
|||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel="nightly"
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -168,10 +168,6 @@ impl Downloader {
|
|||
]);
|
||||
cmd
|
||||
}
|
||||
url => {
|
||||
log::error!("Unknown or unsupported hostname '{:?}'", url);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if log::max_level() < Level::Debug {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ use crate::ui::gui::{windows::WindowIndex, Gui};
|
|||
|
||||
use super::Component;
|
||||
|
||||
|
||||
#[allow(clippy::pedantic)]
|
||||
pub struct NavBar;
|
||||
#[warn(clippy::pedantic)]
|
||||
|
||||
impl Component for NavBar {
|
||||
fn ui(gui: &mut Gui, ctx: &egui::Context) {
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
use std::borrow::BorrowMut;
|
||||
|
||||
use egui::{Button, Color32, Label, RichText, Sense};
|
||||
|
||||
use super::ComponentUi;
|
||||
|
||||
|
||||
|
||||
pub struct SideNav;
|
||||
|
||||
impl ComponentUi for SideNav {
|
||||
fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui) {
|
||||
let mut playlist_names = gui.manifest
|
||||
.get_playlists()
|
||||
.keys().cloned().collect::<Vec<String>>();
|
||||
|
||||
playlist_names.sort_by_key(|name| name.to_lowercase());
|
||||
ui.with_layout(egui::Layout::top_down(egui::Align::TOP), |ui| {
|
||||
|
||||
for pname in playlist_names {
|
||||
if gui.current_playlist.is_empty() {
|
||||
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.horizontal(|ui| {
|
||||
let text;
|
||||
if gui.current_playlist == *pname {
|
||||
text = RichText::new(&pname).color(tint);
|
||||
} else {
|
||||
text = RichText::new(&pname);
|
||||
}
|
||||
|
||||
let button = Label::new(text).sense(Sense::click()).selectable(false);
|
||||
if ui.add(button).clicked() {
|
||||
gui.current_playlist = pname.to_string();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// #333377
|
||||
}
|
||||
55
src/ui/gui/components/side_nav/context_menu.rs
Normal file
55
src/ui/gui/components/side_nav/context_menu.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use egui::{Color32, RichText};
|
||||
|
||||
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",
|
||||
&[playlist_name.clone()]
|
||||
);
|
||||
gui.windows.open(WindowIndex::Confirm, true);
|
||||
ui.close_menu();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
72
src/ui/gui/components/side_nav/mod.rs
Normal file
72
src/ui/gui/components/side_nav/mod.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use egui::{Color32, Label, RichText, Sense};
|
||||
use crate::ui::gui::windows::{self, WindowIndex};
|
||||
|
||||
use super::{ComponentContextMenu, ComponentUi};
|
||||
|
||||
mod context_menu;
|
||||
|
||||
|
||||
|
||||
pub struct SideNav;
|
||||
|
||||
impl ComponentUi for SideNav {
|
||||
fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui) {
|
||||
let mut playlist_names = gui.manifest
|
||||
.get_playlists()
|
||||
.keys().cloned().collect::<Vec<String>>();
|
||||
|
||||
playlist_names.sort_by_key(|name| name.to_lowercase());
|
||||
ui.with_layout(egui::Layout::top_down(egui::Align::TOP), |ui| {
|
||||
|
||||
for pname in playlist_names {
|
||||
if gui.current_playlist.is_empty() {
|
||||
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))
|
||||
.context_menu(|ui| context_menu::ContextMenu::ui(gui, ui, &pname));
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let text = if gui.current_playlist == *pname {
|
||||
RichText::new(&pname).color(tint)
|
||||
} else {
|
||||
RichText::new(&pname)
|
||||
};
|
||||
|
||||
let button = Label::new(text).sense(Sense::click()).selectable(false);
|
||||
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();
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
|
||||
}
|
||||
96
src/ui/gui/components/song_list/context_menu.rs
Normal file
96
src/ui/gui/components/song_list/context_menu.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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: &str, sname: &str, song: &Song) -> Self {
|
||||
Self {
|
||||
pname: pname.to_string(),
|
||||
sname: sname.to_string(),
|
||||
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",
|
||||
&[data.playlist_name().clone(), data.song_name().clone()]
|
||||
);
|
||||
gui.windows.open(WindowIndex::Confirm, true);
|
||||
ui.close_menu();
|
||||
ui.close_menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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::<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();
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -51,10 +51,9 @@ 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);
|
||||
w.set_error_message(text);
|
||||
self.windows.open(WindowIndex::Error, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
src/ui/gui/windows/confirm.rs
Normal file
57
src/ui/gui/windows/confirm.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use egui::{Color32, Label, RichText};
|
||||
|
||||
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: &[String]) {
|
||||
self.text = text.to_string();
|
||||
self.id = new_id.to_string();
|
||||
self.data = data.to_vec();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
})
|
||||
|
|
@ -27,7 +27,7 @@ impl Window for GuiError {
|
|||
}
|
||||
|
||||
impl GuiError {
|
||||
pub fn set_error_message<S: ToString>(&mut self, text: &S) {
|
||||
pub fn set_error_message<S: ToString>(&mut self, text: S) {
|
||||
self.text = text.to_string();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::manifest::song::{Song, SongType};
|
||||
use crate::manifest::song::SongType;
|
||||
|
||||
use super::{State, Window};
|
||||
|
||||
|
|
@ -9,8 +9,8 @@ pub struct GuiImportPlaylist {
|
|||
ed_type: SongType,
|
||||
ed_name: String,
|
||||
ed_url: String,
|
||||
urls_to_add: Vec<String>,
|
||||
playlist_name: String,
|
||||
//urls_to_add: Vec<String>,
|
||||
// playlist_name: String,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -46,8 +46,8 @@ impl Window for GuiImportPlaylist {
|
|||
}
|
||||
});
|
||||
|
||||
if let Some(url) = self.urls_to_add.pop() {
|
||||
todo!();
|
||||
//if let Some(_) = self.urls_to_add.pop() {
|
||||
// todo!();
|
||||
//let client = reqwest::blocking::Client::new();
|
||||
// let song_name = crate::crawler::spotify::get_song_name(&client, url.clone())?;
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ impl Window for GuiImportPlaylist {
|
|||
// playlist.add_song(song_name, song);
|
||||
//}
|
||||
//let _ = state.manifest.save(None);
|
||||
}
|
||||
//}
|
||||
|
||||
|
||||
if save {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -70,4 +70,12 @@ pub fn get_song_path/*<P: TryInto<PathBuf>>*/(/*basepath: Option<P>,*/ 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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user