refactor: Go back to cli-only app

Signed-off-by: SindreKjelsrud <sindre@kjelsrud.dev>
This commit is contained in:
SindreKjelsrud 2026-01-13 22:00:16 +01:00
parent 758076fe32
commit f946dbb9ce
Signed by: sidski
GPG key ID: D2BBDF3EDE6BA9A6
6 changed files with 29 additions and 2446 deletions

2107
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -46,16 +46,6 @@ id3 = "1.13"
# UUID generation
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]
mockito = "1.0"
tokio-test = "0.4"

View file

@ -11,31 +11,9 @@ pkgs.mkShell {
pkg-config # useful for C-dependencies
openssl # if crates are dependent on openssl
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 = 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}"
shellHook = ''
echo "NaviPod Development Environment"
# Verify Rust is available
@ -50,7 +28,12 @@ pkgs.mkShell {
echo ""
echo "Welcome to NaviPod development shell!"
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 './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"
'';
}

View file

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

View file

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

View file

@ -1,6 +1,5 @@
pub mod config;
pub mod error;
pub mod gui;
pub mod ipod;
pub mod navidrome;
pub mod sync;