From 46514298cde5e7d33704283f02a0a971ebfd186b Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 15 Jun 2026 21:32:53 +0200 Subject: [PATCH] UI redesign: onboarding card, browser tabs, accent colors, clean toolbar --- src/ui/app.rs | 94 ++++++++++++++++++------ src/ui/panels/queue.rs | 20 +++--- src/ui/panels/remote_pane.rs | 5 +- src/ui/panels/status_bar.rs | 13 +++- src/ui/panels/tabs.rs | 24 +++---- src/ui/panels/toolbar.rs | 134 +---------------------------------- src/ui/state.rs | 6 ++ 7 files changed, 113 insertions(+), 183 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index d1577dc..315beab 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -15,6 +15,8 @@ use crate::ui::panels::status_bar; use crate::ui::panels::tabs; use crate::ui::panels::toolbar; +const ACCENT: egui::Color32 = egui::Color32::from_rgb(100, 80, 220); + pub struct FileManagerApp { pub state: crate::ui::state::AppState, pub registry: Arc, @@ -84,6 +86,9 @@ impl FileManagerApp { self.state.status_message = "Connected".into(); self.state.connect_loading = false; self.state.show_connect_dialog = false; + self.state.onboarding_host.clear(); + self.state.onboarding_user.clear(); + self.state.onboarding_pass.clear(); } Err(e) => { self.state.status_message = format!("Connection failed: {}", e); @@ -147,6 +152,12 @@ impl eframe::App for FileManagerApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { if self.first_frame { self.first_frame = false; + + let mut style = (*ctx.style()).clone(); + style.visuals.selection.stroke.color = ACCENT; + style.visuals.widgets.active.bg_fill = ACCENT; + ctx.set_style(style); + ctx.request_repaint(); } @@ -164,13 +175,7 @@ impl eframe::App for FileManagerApp { }); TopBottomPanel::top("toolbar").show(ctx, |ui| { - toolbar::render( - ui, - &mut self.state, - &self.queue, - &self.registry, - self.rt.handle(), - ); + toolbar::render(ui, &mut self.state); }); let has_connection = self.state.active_tab_ref().is_some() @@ -204,36 +209,83 @@ impl eframe::App for FileManagerApp { }); } else { CentralPanel::default().show(ctx, |ui| { + let available = ui.available_height(); ui.vertical_centered(|ui| { - ui.add_space(ui.available_height() * 0.3); + ui.add_space(available * 0.15); + ui.heading("LoFlum"); ui.label("FTP/SFTP client"); - ui.add_space(20.0); - if ui.button("New Connection").clicked() { - self.state.show_connect_dialog = true; - } - if !self.state.tabs.is_empty() { - ui.add_space(10.0); - ui.label("Select a connection tab above or create a new one"); - } + ui.add_space(24.0); + + let card_frame = egui::Frame::window(&ctx.style()); + card_frame.show(ui, |ui| { + ui.set_min_width(320.0); + ui.vertical_centered(|ui| { + ui.label("Connect to a server"); + ui.add_space(12.0); + + ui.horizontal(|ui| { + ui.label("Host:"); + ui.add( + egui::TextEdit::singleline(&mut self.state.onboarding_host) + .id("onb_host".into()) + .desired_width(200.0) + .hint_text("hostname"), + ); + }); + + ui.horizontal(|ui| { + ui.label("User:"); + ui.add( + egui::TextEdit::singleline(&mut self.state.onboarding_user) + .id("onb_user".into()) + .desired_width(200.0) + .hint_text("username"), + ); + }); + + ui.horizontal(|ui| { + ui.label("Pass:"); + ui.add( + egui::TextEdit::singleline(&mut self.state.onboarding_pass) + .password(true) + .id("onb_pass".into()) + .desired_width(200.0) + .hint_text("password"), + ); + }); + + ui.add_space(12.0); + let conn_btn = egui::Button::new("Connect") + .fill(ACCENT) + .min_size(egui::vec2(200.0, 32.0)); + if ui.add(conn_btn).clicked() { + self.state.connect_host = self.state.onboarding_host.clone(); + self.state.connect_user = self.state.onboarding_user.clone(); + self.state.connect_pass = self.state.onboarding_pass.clone(); + self.state.connect_port = "22".into(); + self.state.connect_protocol = 0; + self.state.show_connect_dialog = true; + } + }); + }); }); }); } - let has_tasks = !self.state.queue_tasks.is_empty(); let tasks = self.state.queue_tasks.clone(); - if has_tasks || self.state.show_queue { + if self.state.show_queue || !tasks.is_empty() { TopBottomPanel::bottom("queue_panel") .resizable(true) - .default_height(150.0) - .min_height(80.0) + .default_height(130.0) + .min_height(60.0) .show(ctx, |ui| { queue::render(ui, &mut self.state, &tasks); }); } TopBottomPanel::bottom("status_bar").show(ctx, |ui| { - status_bar::render(ui, &self.state); + status_bar::render(ui, &mut self.state); }); if self.state.show_connect_dialog { diff --git a/src/ui/panels/queue.rs b/src/ui/panels/queue.rs index 6ea8271..56bdd06 100644 --- a/src/ui/panels/queue.rs +++ b/src/ui/panels/queue.rs @@ -1,15 +1,12 @@ use crate::domain::transfer::TransferTask; use crate::ui::panels::file_pane::format_size; use crate::ui::state::AppState; -use egui::Color32; -pub fn render(ui: &mut egui::Ui, state: &mut AppState, tasks: &[TransferTask]) { +pub fn render(ui: &mut egui::Ui, _state: &mut AppState, tasks: &[TransferTask]) { ui.horizontal(|ui| { - ui.heading("\u{1F4E6} Transfer Queue"); + ui.strong("Transfer Queue"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.small_button("\u{2715}").clicked() { - state.show_queue = false; - } + ui.label(format!("{} tasks", tasks.len())); }); }); @@ -27,13 +24,15 @@ pub fn render(ui: &mut egui::Ui, state: &mut AppState, tasks: &[TransferTask]) { for task in tasks { let bg = match &task.state { crate::domain::transfer::TaskState::Completed => { - Color32::from_rgb(220, 255, 220) + egui::Color32::from_rgb(220, 255, 220) } crate::domain::transfer::TaskState::Failed(_) => { - Color32::from_rgb(255, 220, 220) + egui::Color32::from_rgb(255, 220, 220) } - crate::domain::transfer::TaskState::Running => Color32::from_rgb(220, 220, 255), - _ => Color32::TRANSPARENT, + crate::domain::transfer::TaskState::Running => { + egui::Color32::from_rgb(220, 220, 255) + } + _ => egui::Color32::TRANSPARENT, }; egui::Frame::none() @@ -59,6 +58,7 @@ pub fn render(ui: &mut egui::Ui, state: &mut AppState, tasks: &[TransferTask]) { let pct = task.progress_pct(); let pb = egui::ProgressBar::new(pct as f32 / 100.0) .text(format!("{:.1}%", pct)) + .fill(egui::Color32::from_rgb(100, 80, 220)) .desired_width(ui.available_width() * 0.6); ui.add(pb); diff --git a/src/ui/panels/remote_pane.rs b/src/ui/panels/remote_pane.rs index 6c0d7bc..e874a6b 100644 --- a/src/ui/panels/remote_pane.rs +++ b/src/ui/panels/remote_pane.rs @@ -193,8 +193,9 @@ pub fn trigger_list( let path = state.tabs[tab_idx].remote_path.clone(); let registry = registry.clone(); - let result: Arc, String>>>> = - Arc::new(std::sync::Mutex::new(None)); + let result = Arc::new(std::sync::Mutex::new( + None::, String>>, + )); let result_clone = result.clone(); rt_handle.spawn(async move { diff --git a/src/ui/panels/status_bar.rs b/src/ui/panels/status_bar.rs index 34156a6..d08af5e 100644 --- a/src/ui/panels/status_bar.rs +++ b/src/ui/panels/status_bar.rs @@ -1,12 +1,19 @@ use crate::domain::connection::ConnectionStatus; use crate::ui::state::AppState; -pub fn render(ui: &mut egui::Ui, state: &AppState) { +pub fn render(ui: &mut egui::Ui, state: &mut AppState) { ui.horizontal(|ui| { ui.label(&state.status_message); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let queued = state.queue_tasks.len(); - ui.label(format!("Queue: {}", queued)); + let queue_label = if state.show_queue { + format!("Queue ({}) \u{25BC}", queued) + } else { + format!("Queue ({}) \u{25B2}", queued) + }; + if ui.button(queue_label).clicked() { + state.show_queue = !state.show_queue; + } ui.separator(); let connected = state .tabs @@ -14,7 +21,7 @@ pub fn render(ui: &mut egui::Ui, state: &AppState) { .filter(|t| t.status == ConnectionStatus::Connected) .count(); let total = state.tabs.len(); - ui.label(format!("Connections: {}/{}", connected, total)); + ui.label(format!("{}/{}", connected, total)); }); }); } diff --git a/src/ui/panels/tabs.rs b/src/ui/panels/tabs.rs index 309dd7c..f7a4594 100644 --- a/src/ui/panels/tabs.rs +++ b/src/ui/panels/tabs.rs @@ -1,16 +1,10 @@ use crate::domain::connection::ConnectionStatus; use crate::ui::state::AppState; +const ACCENT: egui::Color32 = egui::Color32::from_rgb(100, 80, 220); + pub fn render(ui: &mut egui::Ui, state: &mut AppState) { ui.horizontal(|ui| { - if state.tabs.is_empty() { - ui.label("No connections"); - if ui.button("+ New Connection").clicked() { - state.show_connect_dialog = true; - } - return; - } - let mut to_close: Option = None; for (i, tab) in state.tabs.iter().enumerate() { @@ -23,18 +17,18 @@ pub fn render(ui: &mut egui::Ui, state: &mut AppState) { ConnectionStatus::Error(_) => "\u{2716}", }; - let label = format!("{} {}", dot, tab.label); + let label = format!("{} {} ", dot, tab.label); - let mut tab_button = egui::Button::new(&label); + let mut tab_btn = egui::Button::new(&label) + .min_size(egui::vec2(60.0, 28.0)); if is_active { - tab_button = tab_button.fill(egui::Color32::from_rgb(60, 60, 80)); + tab_btn = tab_btn.fill(ACCENT); } - let response = ui.add(tab_button); - if response.clicked() { + if ui.add(tab_btn).clicked() { state.active_tab = i; } - if ui.small_button("\u{2715}").clicked() { + if ui.small_button("x").clicked() { to_close = Some(i); } } @@ -46,8 +40,6 @@ pub fn render(ui: &mut egui::Ui, state: &mut AppState) { } } - ui.separator(); - if ui.button("+").clicked() { state.show_connect_dialog = true; } diff --git a/src/ui/panels/toolbar.rs b/src/ui/panels/toolbar.rs index e9589e8..0b45d5c 100644 --- a/src/ui/panels/toolbar.rs +++ b/src/ui/panels/toolbar.rs @@ -1,139 +1,11 @@ -use crate::domain::connection::ConnectionStatus; -use crate::domain::file_entry::EntryKind; -use crate::domain::transfer::{TransferKind, TransferTask}; -use crate::fs::remote::RemoteRegistry; -use crate::transfer::queue::TransferQueue; use crate::ui::state::AppState; -use std::sync::Arc; -pub fn render( - ui: &mut egui::Ui, - state: &mut AppState, - queue: &TransferQueue, - registry: &Arc, - _rt_handle: &tokio::runtime::Handle, -) { +pub fn render(ui: &mut egui::Ui, state: &mut AppState) { ui.horizontal(|ui| { - let add_btn = egui::Button::new("+ New Connection"); + let add_btn = egui::Button::new("+ New Connection") + .fill(egui::Color32::from_rgb(80, 60, 180)); if ui.add(add_btn).clicked() { state.show_connect_dialog = true; } - - ui.separator(); - - let has_local_selection = state.local_selected.is_some(); - let has_remote_selection = state - .active_tab_ref() - .and_then(|t| t.remote_selected.as_ref()) - .is_some(); - let (is_connected, tab_exists) = { - let active = state.active_tab_ref(); - ( - active - .map(|t| t.status == ConnectionStatus::Connected) - .unwrap_or(false), - active.is_some(), - ) - }; - - let tab_id = state.active_tab_ref().map(|t| t.id.clone()); - let remote_path = state.active_tab_ref().map(|t| t.remote_path.clone()); - - let upload_btn = egui::Button::new("\u{2191} Upload") - .min_size(egui::vec2(100.0, 24.0)); - ui.add_enabled_ui(is_connected && has_local_selection && tab_exists, |ui| { - if ui.add(upload_btn).clicked() - && let Some(sel) = state.local_selected.clone() - && let Some(entry) = state.local_entries.iter().find(|e| e.name == sel) - { - if entry.kind == EntryKind::File { - let file_name = entry.name.clone(); - let remote_base = remote_path.clone().unwrap_or_else(|| "/".into()); - let remote_path_str = - format!("{}/{}", remote_base.trim_end_matches('/'), file_name); - let cid = tab_id.clone().unwrap_or_default(); - let task = TransferTask::new( - TransferKind::Upload, - cid, - entry.path.clone(), - remote_path_str, - file_name, - entry.size.unwrap_or(0), - ); - if registry.get(&task.connection_id).is_some() { - queue.push(task); - state.status_message = "Upload queued".into(); - } else { - state.status_message = "No connection".into(); - } - } else { - state.status_message = "Cannot upload directory".into(); - } - } - }); - - let download_btn = egui::Button::new("\u{2193} Download") - .min_size(egui::vec2(100.0, 24.0)); - ui.add_enabled_ui(is_connected && has_remote_selection && tab_exists, |ui| { - if ui.add(download_btn).clicked() - && let Some(sel) = tab_remote_selected(state) - { - let file_name = sel.clone(); - let remote_base = remote_path.clone().unwrap_or_else(|| "/".into()); - let remote_path_str = - format!("{}/{}", remote_base.trim_end_matches('/'), file_name); - let local_path_str = - format!("{}/{}", state.local_path.trim_end_matches('/'), file_name); - let size = state - .active_tab_ref() - .map(|t| { - t.remote_entries - .iter() - .find(|e| e.name == sel) - .and_then(|e| e.size) - .unwrap_or(0) - }) - .unwrap_or(0); - let cid = tab_id.clone().unwrap_or_default(); - let task = TransferTask::new( - TransferKind::Download, - cid, - local_path_str, - remote_path_str, - file_name, - size, - ); - if registry.get(&task.connection_id).is_some() { - queue.push(task); - state.status_message = "Download queued".into(); - } else { - state.status_message = "No connection".into(); - } - } - }); - - ui.separator(); - - let queue_label = if state.show_queue { - "Queue \u{25BC}" - } else { - "Queue \u{25B2}" - }; - if ui.button(queue_label).clicked() { - state.show_queue = !state.show_queue; - } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - ui.separator(); - let task_count = state.queue_tasks.len(); - if task_count > 0 { - ui.label(format!("{} tasks", task_count)); - } - }); }); } - -fn tab_remote_selected(state: &AppState) -> Option { - let idx = state.active_tab.min(state.tabs.len().saturating_sub(1)); - state.tabs.get(idx).and_then(|t| t.remote_selected.clone()) -} diff --git a/src/ui/state.rs b/src/ui/state.rs index b720e7c..2c0e640 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -35,6 +35,9 @@ pub struct AppState { pub local_selected: Option, pub local_tree_open: bool, pub show_connect_dialog: bool, + pub onboarding_host: String, + pub onboarding_user: String, + pub onboarding_pass: String, pub connect_label: String, pub connect_host: String, pub connect_port: String, @@ -66,6 +69,9 @@ impl Default for AppState { local_selected: None, local_tree_open: false, show_connect_dialog: false, + onboarding_host: String::new(), + onboarding_user: String::new(), + onboarding_pass: String::new(), connect_label: String::new(), connect_host: String::new(), connect_port: "22".into(),