UI redesign: onboarding card, browser tabs, accent colors, clean toolbar
This commit is contained in:
parent
500da04ce7
commit
46514298cd
7 changed files with 113 additions and 183 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue