UI redesign: onboarding card, browser tabs, accent colors, clean toolbar

This commit is contained in:
loki5512344 2026-06-15 21:32:53 +02:00
parent 500da04ce7
commit 46514298cd
7 changed files with 113 additions and 183 deletions

View file

@ -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<RemoteRegistry>,
@ -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 {

View file

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

View file

@ -193,8 +193,9 @@ pub fn trigger_list(
let path = state.tabs[tab_idx].remote_path.clone();
let registry = registry.clone();
let result: Arc<std::sync::Mutex<Option<Result<Vec<FileEntry>, String>>>> =
Arc::new(std::sync::Mutex::new(None));
let result = Arc::new(std::sync::Mutex::new(
None::<Result<Vec<FileEntry>, String>>,
));
let result_clone = result.clone();
rt_handle.spawn(async move {

View file

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

View file

@ -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<usize> = 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;
}

View file

@ -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<RemoteRegistry>,
_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<String> {
let idx = state.active_tab.min(state.tabs.len().saturating_sub(1));
state.tabs.get(idx).and_then(|t| t.remote_selected.clone())
}

View file

@ -35,6 +35,9 @@ pub struct AppState {
pub local_selected: Option<String>,
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(),