From d7d6faeeb10223af1077a667c8ce9c11aeb98495 Mon Sep 17 00:00:00 2001 From: TomZz Date: Sun, 14 Jun 2026 23:58:46 +0800 Subject: [PATCH] feat: optimize transfer history dialog UI and logic - Add an independent scrollbar track to prevent it from overlapping with the content list - Add a "Clear History" button with text label, placed seamlessly next to the close button in the custom header - Implement a 100-record limit for transfer history and display this limit in the header - Persist the cleared transfer history into the session config immediately after clearing - Disable default dialog close button and reconstruct header to avoid state update collision and panic Resolves #25 --- locales/en.yml | 2 + locales/zh-CN.yml | 2 + src/app/dialogs.rs | 136 ++++++++++++++++++++++++++++++++++----------- src/app/mod.rs | 3 + 4 files changed, 111 insertions(+), 32 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index c7acae4..9f7b973 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -123,3 +123,5 @@ settings_group_font: "Font" settings_group_other: "Other" theme: "Theme" theme_mode: "Theme Mode" +clear_transfers: "Clear History" +transfers_limit: " (Max 100 records)" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 0eb4623..ae25d27 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -126,3 +126,5 @@ settings_group_font: "字体" settings_group_other: "其它" theme: "主题" theme_mode: "主题模式" +clear_transfers: "清空记录" +transfers_limit: " (最多保留 100 条)" diff --git a/src/app/dialogs.rs b/src/app/dialogs.rs index 1ef0143..d636f16 100644 --- a/src/app/dialogs.rs +++ b/src/app/dialogs.rs @@ -398,11 +398,78 @@ impl Ashell { let view = cx.entity(); window.open_dialog(cx, move |dialog: Dialog, _window, _| { dialog - .title(t!("transfers").to_string()) .w(px(600.)) + .close_button(false) .content({ let view = view.clone(); move |content, window, cx| { + let can_clear = view.read(cx).transfers.iter().any(|t| { + !matches!( + t.state, + crate::terminal::TransferState::Running + | crate::terminal::TransferState::Paused + ) + }); + + let clear_btn = if can_clear { + Some( + Button::new("clear_transfers_btn") + .small() + .ghost() + .icon(IconName::Delete) + .label(t!("clear_transfers").to_string()) + .on_click(window.listener_for(&view, |this, _, _, cx| { + this.transfers.retain(|t| { + matches!( + t.state, + crate::terminal::TransferState::Running + | crate::terminal::TransferState::Paused + ) + }); + this.config.set_transfers(this.transfers.clone()); + cx.notify(); + })), + ) + } else { + None + }; + + let header = h_flex() + .w_full() + .justify_between() + .items_center() + .child( + h_flex() + .items_baseline() + .child( + div() + .text_lg() + .font_weight(FontWeight::SEMIBOLD) + .child(t!("transfers").to_string()), + ) + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .ml_2() + .child(t!("transfers_limit").to_string()), + ), + ) + .child( + h_flex() + .gap_2() + .children(clear_btn) + .child( + Button::new("close_dialog") + .small() + .ghost() + .icon(IconName::Close) + .on_click(|_, window, cx| { + window.close_dialog(cx); + }), + ), + ); + let mut transfers = view.read(cx).transfers.clone(); transfers.sort_by_key(|t| match t.state { crate::terminal::TransferState::Running @@ -412,14 +479,15 @@ impl Ashell { if transfers.is_empty() { return content.child( - div() - .p_4() - .text_center() - .text_color(cx.theme().muted_foreground) - .child(t!("no_transfers_yet").to_string()), + v_flex().gap_2().child(header).child( + div() + .p_4() + .text_center() + .text_color(cx.theme().muted_foreground) + .child(t!("no_transfers_yet").to_string()), + ), ); } - let list = v_flex().gap_2().children(transfers.into_iter().map(|t| { let (icon, _color) = match t.info.kind { crate::terminal::TransferType::Upload => { @@ -629,6 +697,7 @@ impl Ashell { ) })); + let scroll_handle = window .use_keyed_state("transfers-scroll", cx, |_, _| { gpui::ScrollHandle::default() @@ -637,31 +706,34 @@ impl Ashell { .clone(); content.child( - div() - .w_full() - .relative() - .child( - div() - .w_full() - .max_h(px(400.)) - .flex_col() - .id("transfers-scroll-view") - .track_scroll(&scroll_handle) - .overflow_y_scroll() - .child(list), - ) - .child( - div() - .absolute() - .top_0() - .right_0() - .bottom_0() - .w(px(16.)) - .child( - Scrollbar::vertical(&scroll_handle) - .scrollbar_show(ScrollbarShow::Always), - ), - ), + v_flex().gap_2().child(header).child( + div() + .w_full() + .relative() + .child( + div() + .w_full() + .max_h(px(400.)) + .flex_col() + .id("transfers-scroll-view") + .track_scroll(&scroll_handle) + .overflow_y_scroll() + .pr(px(14.)) + .child(list), + ) + .child( + div() + .absolute() + .top_0() + .right_0() + .bottom_0() + .w(px(16.)) + .child( + Scrollbar::vertical(&scroll_handle) + .scrollbar_show(ScrollbarShow::Always), + ), + ), + ) ) } }) diff --git a/src/app/mod.rs b/src/app/mod.rs index c04370b..69b895a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -718,6 +718,9 @@ impl Ashell { state: crate::terminal::TransferState::Running, }, ); + if self.transfers.len() > 100 { + self.transfers.truncate(100); + } transfers_changed = true; } BackendEvent::SftpHome { tab_id, home } => {