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
|
[BUG] \[loc\](/src/...) - Bugfix, mandatory location
|
||||||
[GIT] \[loc\](/src/...) - Git related feature, optional 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
|
### #0
|
||||||
[FEAT] - [side_nav](/src/ui/gui/components/side_nav.rs)
|
**DONE** ~~[FEAT] - [side_nav](/src/ui/gui/components/mod.rs)
|
||||||
Add dropdown menu for `side_nav` playlist
|
Add dropdown menu for `side_nav` playlist~~
|
||||||
|
|
||||||
### #1
|
### #1
|
||||||
[FEAT] - [gui](/src/ui/gui/)
|
[FEAT] - [gui](/src/ui/gui/)
|
||||||
|
|
@ -25,7 +25,7 @@ Better styling
|
||||||
Add music player footer
|
Add music player footer
|
||||||
|
|
||||||
### #4
|
### #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
|
Add numbers to `song_list` table
|
||||||
|
|
||||||
### #5
|
### #5
|
||||||
|
|
@ -33,7 +33,7 @@ Add numbers to `song_list` table
|
||||||
Add music player logic
|
Add music player logic
|
||||||
|
|
||||||
### #6
|
### #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
|
Add support for images by possibly storing the images in json or custom format
|
||||||
|
|
||||||
### #7
|
### #7
|
||||||
|
|
@ -47,9 +47,10 @@ standalone one, moving default paths and using [#10](#10):
|
||||||
| music-output | `~/Music/mcmg/*` | `%userprofile%/Music/mcmg/*` |
|
| music-output | `~/Music/mcmg/*` | `%userprofile%/Music/mcmg/*` |
|
||||||
|
|
||||||
### #8
|
### #8
|
||||||
[FEAT] - [cli](/src/ui/cli/)
|
[FEAT] - [cli](/src/ui/cli/mod.rs)
|
||||||
add missing commands that are available via gui
|
add missing commands that are available via gui
|
||||||
- Downloading single songs, from the manifest and standalone as an utility
|
- Downloading single songs, from the manifest and standalone as an utility
|
||||||
|
- removing playlists, single songs
|
||||||
|
|
||||||
### #9
|
### #9
|
||||||
[BUG] - [utils](/src/util.rs)
|
[BUG] - [utils](/src/util.rs)
|
||||||
|
|
@ -61,12 +62,24 @@ Add an utility to detect if this is ran as a standalone application
|
||||||
|
|
||||||
### #11
|
### #11
|
||||||
[FEAT] - [downloader](/src/downloader.rs)
|
[FEAT] - [downloader](/src/downloader.rs)
|
||||||
Refractor for better readability and usage
|
Refractor downloader for better readability and usage
|
||||||
|
|
||||||
### #12
|
### #12
|
||||||
[GIT]
|
[GIT]
|
||||||
Add ci that runs clippy and builds in release mode
|
Add ci that runs clippy and builds in release mode
|
||||||
|
|
||||||
Constant todos:
|
### #13
|
||||||
TODO: Run code through clippy and fix any errors
|
[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
|
## Offline usage
|
||||||
Your whole music library is downloaded to your music folder (unless its being ran in standalone mode). Saved in your selected format.
|
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
|
## Dependencies
|
||||||
[ffmpeg](https://ffmpeg.org/): To convert your music files to your desired format.
|
[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 (name, playlist) in manifest.get_playlists() {
|
||||||
for (song_name, song) in playlist.get_songs() {
|
for (song_name, song) in playlist.get_songs() {
|
||||||
self.download_song(cfg, song_name, song, name, format)?;
|
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)
|
Ok(self.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,10 +168,6 @@ impl Downloader {
|
||||||
]);
|
]);
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
url => {
|
|
||||||
log::error!("Unknown or unsupported hostname '{:?}'", url);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if log::max_level() < Level::Debug {
|
if log::max_level() < Level::Debug {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,12 @@ impl Manifest {
|
||||||
pub fn get_playlists_mut(&mut self) -> &mut HashMap<String, playlist::Playlist> {
|
pub fn get_playlists_mut(&mut self) -> &mut HashMap<String, playlist::Playlist> {
|
||||||
&mut self.playlists
|
&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 {
|
pub fn get_song_count(&self) -> usize {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for v in self.playlists.values() {
|
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`
|
/// 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: 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
|
// NOTE: So its also kinda really slow
|
||||||
let mut finish_count = 0;
|
let mut finish_count = 0;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ pub fn song(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downl
|
||||||
|
|
||||||
if should_download {
|
if should_download {
|
||||||
downloader.download_song(cfg, name, &song, playlist, manifest.get_format())?;
|
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(())
|
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 nav;
|
||||||
pub mod song_list;
|
pub mod song_list;
|
||||||
pub mod context_menu;
|
|
||||||
pub mod side_nav;
|
pub mod side_nav;
|
||||||
pub mod search_bar;
|
pub mod search_bar;
|
||||||
|
|
||||||
|
|
@ -18,3 +17,8 @@ pub trait ComponentUi {
|
||||||
pub trait ComponentUiMut {
|
pub trait ComponentUiMut {
|
||||||
fn ui(&mut self, gui: &mut Gui, ui: &mut egui::Ui);
|
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;
|
use super::Component;
|
||||||
|
|
||||||
|
|
||||||
#[allow(clippy::pedantic)]
|
|
||||||
pub struct NavBar;
|
pub struct NavBar;
|
||||||
#[warn(clippy::pedantic)]
|
|
||||||
|
|
||||||
impl Component for NavBar {
|
impl Component for NavBar {
|
||||||
fn ui(gui: &mut Gui, ctx: &egui::Context) {
|
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::Color32;
|
||||||
use egui_extras::{Column, TableBuilder};
|
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)]
|
#[derive(Debug, Default)]
|
||||||
pub struct SongList {
|
pub struct SongList;
|
||||||
}
|
|
||||||
|
|
||||||
impl ComponentUi for SongList {
|
impl ComponentUi for SongList {
|
||||||
fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui) {
|
fn ui(gui: &mut crate::ui::gui::Gui, ui: &mut egui::Ui) {
|
||||||
|
|
@ -25,17 +26,8 @@ impl ComponentUi for SongList {
|
||||||
.striped(true)
|
.striped(true)
|
||||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||||
.resizable(true)
|
.resizable(true)
|
||||||
//.column(Column::auto())
|
|
||||||
//.column(Column::auto())
|
|
||||||
//.column(
|
|
||||||
// Column::remainder()
|
|
||||||
// .at_least(40.0)
|
|
||||||
// .clip(true)
|
|
||||||
// .resizable(true),
|
|
||||||
//)
|
|
||||||
.column(Column::auto())
|
.column(Column::auto())
|
||||||
.column(Column::remainder())
|
.column(Column::remainder())
|
||||||
//.column(Column::remainder())
|
|
||||||
.min_scrolled_height(0.0)
|
.min_scrolled_height(0.0)
|
||||||
.max_scroll_height(available_height)
|
.max_scroll_height(available_height)
|
||||||
.sense(egui::Sense::click());
|
.sense(egui::Sense::click());
|
||||||
|
|
@ -55,10 +47,6 @@ impl ComponentUi for SongList {
|
||||||
};
|
};
|
||||||
|
|
||||||
table.header(20.0, |mut header| {
|
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| {
|
header.col(|ui| {
|
||||||
ui.strong("Source");
|
ui.strong("Source");
|
||||||
});
|
});
|
||||||
|
|
@ -97,11 +85,8 @@ impl ComponentUi for SongList {
|
||||||
(SearchType::Url, _) => (),
|
(SearchType::Url, _) => (),
|
||||||
}
|
}
|
||||||
body.row(18.0, |mut row| {
|
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| {
|
row.col(|ui| {
|
||||||
let color =
|
let color =
|
||||||
match s.get_type() {
|
match s.get_type() {
|
||||||
|
|
@ -111,20 +96,41 @@ impl ComponentUi for SongList {
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.colored_label(color, s.get_type().to_string())
|
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| {
|
row.col(|ui| {
|
||||||
ui.hyperlink_to(sname.clone(), s.get_url_str())
|
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()
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::pedantic)]
|
|
||||||
pub fn throw_error<S: ToString>(&mut self, text: S) {
|
pub fn throw_error<S: ToString>(&mut self, text: S) {
|
||||||
let w = self.windows.get_window::<windows::error::GuiError>(WindowIndex::Error);
|
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);
|
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)
|
.open(open)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.vertical(|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.horizontal(|ui| {
|
||||||
ui.add(Label::new(self.text.clone()).wrap(true));
|
ui.add(Label::new(self.text.clone()).wrap(true));
|
||||||
})
|
})
|
||||||
|
|
@ -27,7 +27,7 @@ impl Window for GuiError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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();
|
self.text = text.to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::manifest::song::{Song, SongType};
|
use crate::manifest::song::SongType;
|
||||||
|
|
||||||
use super::{State, Window};
|
use super::{State, Window};
|
||||||
|
|
||||||
|
|
@ -9,8 +9,8 @@ pub struct GuiImportPlaylist {
|
||||||
ed_type: SongType,
|
ed_type: SongType,
|
||||||
ed_name: String,
|
ed_name: String,
|
||||||
ed_url: String,
|
ed_url: String,
|
||||||
urls_to_add: Vec<String>,
|
//urls_to_add: Vec<String>,
|
||||||
playlist_name: String,
|
// playlist_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -46,8 +46,8 @@ impl Window for GuiImportPlaylist {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(url) = self.urls_to_add.pop() {
|
//if let Some(_) = self.urls_to_add.pop() {
|
||||||
todo!();
|
// todo!();
|
||||||
//let client = reqwest::blocking::Client::new();
|
//let client = reqwest::blocking::Client::new();
|
||||||
// let song_name = crate::crawler::spotify::get_song_name(&client, url.clone())?;
|
// 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);
|
// playlist.add_song(song_name, song);
|
||||||
//}
|
//}
|
||||||
//let _ = state.manifest.save(None);
|
//let _ = state.manifest.save(None);
|
||||||
}
|
//}
|
||||||
|
|
||||||
|
|
||||||
if save {
|
if save {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pub mod song_edit;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod import_playlist;
|
pub mod import_playlist;
|
||||||
pub mod song_new;
|
pub mod song_new;
|
||||||
|
pub mod confirm;
|
||||||
|
|
||||||
pub trait Window: std::fmt::Debug {
|
pub trait Window: std::fmt::Debug {
|
||||||
fn ui(&mut self, state: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()>;
|
fn ui(&mut self, state: &mut State, ctx: &egui::Context, open: &mut bool) -> anyhow::Result<()>;
|
||||||
|
|
@ -16,6 +17,7 @@ pub enum WindowIndex {
|
||||||
ImportPlaylist,
|
ImportPlaylist,
|
||||||
SongEdit,
|
SongEdit,
|
||||||
SongNew,
|
SongNew,
|
||||||
|
Confirm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -38,6 +40,7 @@ impl WindowManager {
|
||||||
windows.insert(WindowIndex::ImportPlaylist, Box::<import_playlist::GuiImportPlaylist>::default());
|
windows.insert(WindowIndex::ImportPlaylist, Box::<import_playlist::GuiImportPlaylist>::default());
|
||||||
windows.insert(WindowIndex::SongEdit, Box::<song_edit::GuiSongEditor>::default());
|
windows.insert(WindowIndex::SongEdit, Box::<song_edit::GuiSongEditor>::default());
|
||||||
windows.insert(WindowIndex::SongNew, Box::<song_new::GuiNewSong>::default());
|
windows.insert(WindowIndex::SongNew, Box::<song_new::GuiNewSong>::default());
|
||||||
|
windows.insert(WindowIndex::Confirm, Box::<confirm::ConfirmW>::default());
|
||||||
Self {
|
Self {
|
||||||
windows,
|
windows,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
||||||
|
|
@ -70,4 +70,12 @@ pub fn get_song_path/*<P: TryInto<PathBuf>>*/(/*basepath: Option<P>,*/ pname: &S
|
||||||
path.push(sname);
|
path.push(sname);
|
||||||
path.set_extension(format.to_string());
|
path.set_extension(format.to_string());
|
||||||
path
|
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