refactor: Go back to cli-only app
Signed-off-by: SindreKjelsrud <sindre@kjelsrud.dev>
This commit is contained in:
parent
758076fe32
commit
f946dbb9ce
6 changed files with 29 additions and 2446 deletions
2107
Cargo.lock
generated
2107
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
|
@ -46,16 +46,6 @@ id3 = "1.13"
|
||||||
# UUID generation
|
# UUID generation
|
||||||
uuid = { version = "1.0", features = ["v4"] }
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
|
|
||||||
# GUI
|
|
||||||
eframe = { version = "0.27", default-features = false, features = ["default_fonts", "glow"] }
|
|
||||||
egui = "0.27"
|
|
||||||
rfd = "0.14"
|
|
||||||
home = "0.5.11" # Pin to version compatible with Rust 1.86
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "navipod-gui"
|
|
||||||
path = "src/gui_main.rs"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockito = "1.0"
|
mockito = "1.0"
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
|
|
||||||
31
shell.nix
31
shell.nix
|
|
@ -11,31 +11,9 @@ pkgs.mkShell {
|
||||||
pkg-config # useful for C-dependencies
|
pkg-config # useful for C-dependencies
|
||||||
openssl # if crates are dependent on openssl
|
openssl # if crates are dependent on openssl
|
||||||
openssl.dev # openssl development headers
|
openssl.dev # openssl development headers
|
||||||
# GUI dependencies - X11
|
|
||||||
libGL
|
|
||||||
xorg.libX11
|
|
||||||
xorg.libXcursor
|
|
||||||
xorg.libXi
|
|
||||||
xorg.libXrandr
|
|
||||||
libxkbcommon
|
|
||||||
# GUI dependencies - Wayland (for Wayland support)
|
|
||||||
wayland
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# Set up library paths for runtime
|
shellHook = ''
|
||||||
shellHook = let
|
|
||||||
libPath = pkgs.lib.makeLibraryPath (with pkgs; [
|
|
||||||
libGL
|
|
||||||
xorg.libX11
|
|
||||||
xorg.libXcursor
|
|
||||||
xorg.libXi
|
|
||||||
xorg.libXrandr
|
|
||||||
libxkbcommon
|
|
||||||
wayland
|
|
||||||
]);
|
|
||||||
in ''
|
|
||||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${libPath}"
|
|
||||||
|
|
||||||
echo "NaviPod Development Environment"
|
echo "NaviPod Development Environment"
|
||||||
|
|
||||||
# Verify Rust is available
|
# Verify Rust is available
|
||||||
|
|
@ -50,7 +28,12 @@ pkgs.mkShell {
|
||||||
echo ""
|
echo ""
|
||||||
echo "Welcome to NaviPod development shell!"
|
echo "Welcome to NaviPod development shell!"
|
||||||
echo "Run 'cargo build' to build the project."
|
echo "Run 'cargo build' to build the project."
|
||||||
|
echo "Run 'cargo run' to run the CLI."
|
||||||
echo "Run 'cargo check' to check for compilation errors."
|
echo "Run 'cargo check' to check for compilation errors."
|
||||||
echo "Run './target/debug/navipod-gui' to launch the GUI (from within this shell)."
|
echo ""
|
||||||
|
echo "Available commands:"
|
||||||
|
echo " navipod sync [--album ALBUM] [--artist ARTIST] - Sync content to iPod"
|
||||||
|
echo " navipod list-albums - List available albums"
|
||||||
|
echo " navipod check - Check iPod connection"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
297
src/gui.rs
297
src/gui.rs
|
|
@ -1,297 +0,0 @@
|
||||||
use crate::config::{Config, FirmwareType, IpodConfig, NavidromeConfig, SyncConfig};
|
|
||||||
use crate::sync;
|
|
||||||
use eframe::egui;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
|
|
||||||
pub struct NaviPodApp {
|
|
||||||
// Configuration inputs
|
|
||||||
navidrome_url: String,
|
|
||||||
navidrome_username: String,
|
|
||||||
navidrome_password: String,
|
|
||||||
firmware_type: FirmwareType,
|
|
||||||
mount_point: String,
|
|
||||||
|
|
||||||
// Status
|
|
||||||
status: Arc<Mutex<SyncStatus>>,
|
|
||||||
runtime: Option<Runtime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NaviPodApp {
|
|
||||||
fn default() -> Self {
|
|
||||||
// Try to load config from file
|
|
||||||
let (url, username, password, firmware, mount) = if let Ok(config) = crate::config::Config::load() {
|
|
||||||
(
|
|
||||||
config.navidrome.url,
|
|
||||||
config.navidrome.username,
|
|
||||||
config.navidrome.password,
|
|
||||||
config.ipod.firmware,
|
|
||||||
config.ipod.mount_point.to_string_lossy().to_string(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
String::new(),
|
|
||||||
FirmwareType::Rockbox,
|
|
||||||
String::new(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
navidrome_url: url,
|
|
||||||
navidrome_username: username,
|
|
||||||
navidrome_password: password,
|
|
||||||
firmware_type: firmware,
|
|
||||||
mount_point: mount,
|
|
||||||
status: Arc::new(Mutex::new(SyncStatus::Idle)),
|
|
||||||
runtime: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum SyncStatus {
|
|
||||||
Idle,
|
|
||||||
Connecting,
|
|
||||||
Syncing {
|
|
||||||
current_album: String,
|
|
||||||
current_song: String,
|
|
||||||
albums_total: usize,
|
|
||||||
albums_done: usize,
|
|
||||||
},
|
|
||||||
Success {
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
Error {
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SyncStatus {
|
|
||||||
fn default() -> Self {
|
|
||||||
SyncStatus::Idle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NaviPodApp {
|
|
||||||
fn start_sync(&mut self) {
|
|
||||||
if self.runtime.is_some() {
|
|
||||||
return; // Already syncing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate inputs
|
|
||||||
if self.navidrome_url.is_empty() {
|
|
||||||
*self.status.lock().unwrap() = SyncStatus::Error {
|
|
||||||
message: "Navidrome URL is required".to_string(),
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.navidrome_username.is_empty() {
|
|
||||||
*self.status.lock().unwrap() = SyncStatus::Error {
|
|
||||||
message: "Navidrome username is required".to_string(),
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.navidrome_password.is_empty() {
|
|
||||||
*self.status.lock().unwrap() = SyncStatus::Error {
|
|
||||||
message: "Navidrome password is required".to_string(),
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.mount_point.is_empty() {
|
|
||||||
*self.status.lock().unwrap() = SyncStatus::Error {
|
|
||||||
message: "iPod mount point is required".to_string(),
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create runtime for async operations
|
|
||||||
let rt = Runtime::new().unwrap();
|
|
||||||
self.runtime = Some(rt);
|
|
||||||
|
|
||||||
// Build config
|
|
||||||
let config = Config {
|
|
||||||
navidrome: NavidromeConfig {
|
|
||||||
url: self.navidrome_url.clone(),
|
|
||||||
username: self.navidrome_username.clone(),
|
|
||||||
password: self.navidrome_password.clone(),
|
|
||||||
},
|
|
||||||
ipod: IpodConfig {
|
|
||||||
mount_point: PathBuf::from(&self.mount_point),
|
|
||||||
firmware: self.firmware_type,
|
|
||||||
},
|
|
||||||
sync: SyncConfig {
|
|
||||||
albums: true,
|
|
||||||
playlists: false,
|
|
||||||
format: "mp3".to_string(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let status_clone_progress = self.status.clone();
|
|
||||||
let status_clone_final = self.status.clone();
|
|
||||||
let runtime = self.runtime.as_ref().unwrap();
|
|
||||||
|
|
||||||
*self.status.lock().unwrap() = SyncStatus::Connecting;
|
|
||||||
|
|
||||||
// Create progress callback
|
|
||||||
let progress_callback: sync::ProgressCallback = Arc::new(move |album, song, albums_done, albums_total| {
|
|
||||||
*status_clone_progress.lock().unwrap() = SyncStatus::Syncing {
|
|
||||||
current_album: album,
|
|
||||||
current_song: song,
|
|
||||||
albums_total,
|
|
||||||
albums_done,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spawn sync task
|
|
||||||
runtime.spawn(async move {
|
|
||||||
match sync::sync_content_with_progress(config, None, None, Some(progress_callback)).await {
|
|
||||||
Ok(_) => {
|
|
||||||
*status_clone_final.lock().unwrap() = SyncStatus::Success {
|
|
||||||
message: "Sync completed successfully!".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
*status_clone_final.lock().unwrap() = SyncStatus::Error {
|
|
||||||
message: format!("Sync failed: {}", e),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop_sync(&mut self) {
|
|
||||||
self.runtime = None;
|
|
||||||
*self.status.lock().unwrap() = SyncStatus::Idle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for NaviPodApp {
|
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
|
||||||
ui.heading("NaviPod - Sync Navidrome to iPod");
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
// Configuration section
|
|
||||||
ui.group(|ui| {
|
|
||||||
ui.label("Navidrome Configuration");
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("URL:");
|
|
||||||
ui.text_edit_singleline(&mut self.navidrome_url);
|
|
||||||
});
|
|
||||||
if self.navidrome_url.is_empty() {
|
|
||||||
ui.label(egui::RichText::new(" (e.g., https://music.example.com)").weak());
|
|
||||||
}
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Username:");
|
|
||||||
ui.text_edit_singleline(&mut self.navidrome_username);
|
|
||||||
});
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Password:");
|
|
||||||
ui.add(egui::TextEdit::singleline(&mut self.navidrome_password).password(true));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
// iPod Configuration
|
|
||||||
ui.group(|ui| {
|
|
||||||
ui.label("iPod Configuration");
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Firmware:");
|
|
||||||
egui::ComboBox::from_id_source("firmware")
|
|
||||||
.selected_text(match self.firmware_type {
|
|
||||||
FirmwareType::Rockbox => "Rockbox",
|
|
||||||
FirmwareType::Stock => "Stock",
|
|
||||||
})
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
ui.selectable_value(&mut self.firmware_type, FirmwareType::Rockbox, "Rockbox");
|
|
||||||
ui.selectable_value(&mut self.firmware_type, FirmwareType::Stock, "Stock");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.label("Mount Point:");
|
|
||||||
ui.text_edit_singleline(&mut self.mount_point);
|
|
||||||
if ui.button("Browse...").clicked() {
|
|
||||||
// Use native file dialog to select directory
|
|
||||||
if let Some(path) = rfd::FileDialog::new()
|
|
||||||
.set_directory("/")
|
|
||||||
.pick_folder()
|
|
||||||
{
|
|
||||||
self.mount_point = path.to_string_lossy().to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if self.mount_point.is_empty() {
|
|
||||||
ui.label(egui::RichText::new(" (e.g., /run/media/user/IPOD)").weak());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
// Sync button
|
|
||||||
let is_syncing = matches!(
|
|
||||||
*self.status.lock().unwrap(),
|
|
||||||
SyncStatus::Connecting | SyncStatus::Syncing { .. }
|
|
||||||
);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if is_syncing {
|
|
||||||
if ui.button("Stop Sync").clicked() {
|
|
||||||
self.stop_sync();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ui.button("Start Sync").clicked() {
|
|
||||||
self.start_sync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
// Status display
|
|
||||||
ui.group(|ui| {
|
|
||||||
ui.label("Status:");
|
|
||||||
let status = self.status.lock().unwrap().clone();
|
|
||||||
match status {
|
|
||||||
SyncStatus::Idle => {
|
|
||||||
ui.label(egui::RichText::new("Ready").color(egui::Color32::GRAY));
|
|
||||||
}
|
|
||||||
SyncStatus::Connecting => {
|
|
||||||
ui.label(egui::RichText::new("Connecting to Navidrome...").color(egui::Color32::BLUE));
|
|
||||||
ui.spinner();
|
|
||||||
}
|
|
||||||
SyncStatus::Syncing {
|
|
||||||
current_album,
|
|
||||||
current_song,
|
|
||||||
albums_total,
|
|
||||||
albums_done,
|
|
||||||
} => {
|
|
||||||
ui.label(egui::RichText::new(format!(
|
|
||||||
"Syncing: {} / {} albums",
|
|
||||||
albums_done, albums_total
|
|
||||||
)).color(egui::Color32::BLUE));
|
|
||||||
ui.label(format!("Album: {}", current_album));
|
|
||||||
ui.label(format!("Song: {}", current_song));
|
|
||||||
ui.spinner();
|
|
||||||
}
|
|
||||||
SyncStatus::Success { message } => {
|
|
||||||
ui.label(egui::RichText::new(&message).color(egui::Color32::GREEN));
|
|
||||||
}
|
|
||||||
SyncStatus::Error { message } => {
|
|
||||||
ui.label(egui::RichText::new(&message).color(egui::Color32::RED));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request repaint to update status
|
|
||||||
ctx.request_repaint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
use eframe::egui;
|
|
||||||
use navipod::gui::NaviPodApp;
|
|
||||||
|
|
||||||
fn main() -> eframe::Result<()> {
|
|
||||||
// Force X11 backend - set before any winit code runs
|
|
||||||
// This must be set before eframe initializes
|
|
||||||
if std::env::var("WINIT_UNIX_BACKEND").is_err() {
|
|
||||||
std::env::set_var("WINIT_UNIX_BACKEND", "x11");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also try setting WAYLAND_DISPLAY to empty to prevent Wayland detection
|
|
||||||
if std::env::var("WAYLAND_DISPLAY").is_ok() {
|
|
||||||
std::env::remove_var("WAYLAND_DISPLAY");
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = eframe::NativeOptions {
|
|
||||||
viewport: egui::ViewportBuilder::default()
|
|
||||||
.with_inner_size([600.0, 500.0])
|
|
||||||
.with_title("NaviPod - Sync Navidrome to iPod"),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
eframe::run_native(
|
|
||||||
"NaviPod",
|
|
||||||
options,
|
|
||||||
Box::new(|_cc| Box::new(NaviPodApp::default())),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod gui;
|
|
||||||
pub mod ipod;
|
pub mod ipod;
|
||||||
pub mod navidrome;
|
pub mod navidrome;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue