diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index b06a710..f9012eb 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -19,7 +19,7 @@ impl ElyShell { cx: &mut Context, ) -> AnyElement { match tab.url().as_str() { - "ely://downloads" => self.render_downloads_page(snapshot), + "ely://downloads" => self.render_downloads_page(snapshot, cx), "ely://history" => self.render_history_page(snapshot, cx), "ely://archive" => self.render_archive_page(snapshot, cx), _ => render_default_page(tab), diff --git a/crates/ely_app/src/shell/internal_pages/downloads.rs b/crates/ely_app/src/shell/internal_pages/downloads.rs index 28242d0..1567481 100644 --- a/crates/ely_app/src/shell/internal_pages/downloads.rs +++ b/crates/ely_app/src/shell/internal_pages/downloads.rs @@ -5,14 +5,23 @@ use ely_domain::{ }; use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, px, rgb, + AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, + px, rgb, +}; +use gpui_component::{ + IconName, Sizable, StyledExt, + button::{Button, ButtonVariants}, + scroll::ScrollableElement, }; -use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; use super::{ElyShell, render_canvas_surface}; impl ElyShell { - pub(super) fn render_downloads_page(&mut self, snapshot: &BrowserSnapshot) -> AnyElement { + pub(super) fn render_downloads_page( + &mut self, + snapshot: &BrowserSnapshot, + cx: &mut Context, + ) -> AnyElement { render_canvas_surface( div() .size_full() @@ -66,120 +75,219 @@ impl ElyShell { ), ), ) - .child(render_downloads_list(snapshot)), + .child(self.render_downloads_list(snapshot, cx)), ) } -} -fn render_downloads_list(snapshot: &BrowserSnapshot) -> AnyElement { - if snapshot.download_entries.is_empty() { - return div() + fn render_downloads_list( + &mut self, + snapshot: &BrowserSnapshot, + cx: &mut Context, + ) -> AnyElement { + if snapshot.download_entries.is_empty() { + return div() + .flex_1() + .border_t_1() + .border_color(rgb(colors::HAIRLINE)) + .pt_5() + .text_sm() + .text_color(rgb(colors::MUTED)) + .child("Downloads are empty for this Profile.") + .into_any_element(); + } + + div() .flex_1() + .flex() + .flex_col() + .overflow_y_scrollbar() .border_t_1() .border_color(rgb(colors::HAIRLINE)) - .pt_5() - .text_sm() - .text_color(rgb(colors::MUTED)) - .child("Downloads are empty for this Profile.") - .into_any_element(); + .children( + snapshot + .download_entries + .iter() + .rev() + .enumerate() + .map(|(index, entry)| self.render_download_row(index, entry, cx)), + ) + .into_any_element() } - div() - .flex_1() - .flex() - .flex_col() - .overflow_y_scrollbar() - .border_t_1() - .border_color(rgb(colors::HAIRLINE)) - .children( - snapshot - .download_entries - .iter() - .rev() - .enumerate() - .map(|(index, entry)| render_download_row(index, entry)), - ) - .into_any_element() -} + fn render_download_row( + &mut self, + index: usize, + entry: &DownloadEntry, + cx: &mut Context, + ) -> AnyElement { + div() + .id(SharedString::from(format!("download-{index}"))) + .py_3() + .border_b_1() + .border_color(rgb(colors::HAIRLINE)) + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::MUTED)).child(IconName::File)) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .truncate() + .text_color(rgb(colors::INK)) + .child(entry.file_name().to_string()), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(entry.source_url().display_url()), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .text_xs() + .text_color(rgb(colors::MUTED)) + .child( + div() + .min_w_0() + .truncate() + .child(download_destination_label(entry.destination())), + ) + .when(entry.security().requires_prompt(), |this| { + this.child(render_security_prompt(entry.security())) + }), + ), + ), + ) + .child( + div() + .flex() + .items_center() + .justify_end() + .gap_3() + .child( + div() + .min_w(px(92.0)) + .flex() + .flex_col() + .items_end() + .gap_1() + .child( + div() + .text_xs() + .font_semibold() + .text_color(rgb(colors::BODY)) + .child(download_state_label(entry.state())), + ) + .child( + div() + .text_xs() + .text_color(rgb(colors::MUTED)) + .child(download_size_label(entry)), + ), + ) + .child(self.render_download_actions(index, entry, cx)), + ) + .into_any_element() + } -fn render_download_row(index: usize, entry: &DownloadEntry) -> AnyElement { - div() - .id(SharedString::from(format!("download-{index}"))) - .py_3() - .border_b_1() - .border_color(rgb(colors::HAIRLINE)) - .flex() - .items_center() - .justify_between() - .gap_4() - .child( - div() - .min_w_0() - .flex() - .items_center() - .gap_3() - .child(div().text_color(rgb(colors::MUTED)).child(IconName::File)) - .child( - div() - .min_w_0() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_sm() - .font_semibold() - .truncate() - .text_color(rgb(colors::INK)) - .child(entry.file_name().to_string()), - ) - .child( - div() - .text_xs() - .truncate() - .text_color(rgb(colors::MUTED)) - .child(entry.source_url().display_url()), - ) - .child( - div() - .flex() - .items_center() - .gap_2() - .text_xs() - .text_color(rgb(colors::MUTED)) - .child( - div() - .min_w_0() - .truncate() - .child(download_destination_label(entry.destination())), - ) - .when(entry.security().requires_prompt(), |this| { - this.child(render_security_prompt(entry.security())) - }), - ), - ), - ) - .child( - div() - .flex() - .flex_col() - .items_end() - .gap_1() - .child( - div() - .text_xs() - .font_semibold() - .text_color(rgb(colors::BODY)) - .child(download_state_label(entry.state())), + fn render_download_actions( + &mut self, + index: usize, + entry: &DownloadEntry, + cx: &mut Context, + ) -> AnyElement { + div() + .w(px(64.0)) + .flex() + .items_center() + .justify_end() + .gap_1() + .when(matches!(entry.state(), DownloadState::InProgress), |this| { + let pause_id = entry.id().clone(); + let cancel_id = entry.id().clone(); + + this.child( + download_action_button("pause", index, IconName::Minus, "Pause Download") + .on_click(cx.listener(move |shell, _, _, cx| { + shell.pause_download(&pause_id, cx); + })) + .into_any_element(), ) .child( - div() - .text_xs() - .text_color(rgb(colors::MUTED)) - .child(download_size_label(entry)), - ), - ) - .into_any_element() + download_action_button("cancel", index, IconName::Close, "Cancel Download") + .on_click(cx.listener(move |shell, _, _, cx| { + shell.cancel_download(&cancel_id, cx); + })) + .into_any_element(), + ) + }) + .when(matches!(entry.state(), DownloadState::Paused), |this| { + let resume_id = entry.id().clone(); + let cancel_id = entry.id().clone(); + + this.child( + download_action_button( + "resume", + index, + IconName::ArrowRight, + "Resume Download", + ) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.resume_download(&resume_id, cx); + })) + .into_any_element(), + ) + .child( + download_action_button("cancel", index, IconName::Close, "Cancel Download") + .on_click(cx.listener(move |shell, _, _, cx| { + shell.cancel_download(&cancel_id, cx); + })) + .into_any_element(), + ) + }) + .when( + matches!(entry.state(), DownloadState::Cancelled | DownloadState::Failed), + |this| { + let retry_id = entry.id().clone(); + + this.child( + download_action_button("retry", index, IconName::Undo2, "Retry Download") + .on_click(cx.listener(move |shell, _, _, cx| { + shell.retry_download(&retry_id, cx); + })) + .into_any_element(), + ) + }, + ) + .into_any_element() + } +} + +fn download_action_button( + action: &'static str, + index: usize, + icon: IconName, + tooltip: &'static str, +) -> Button { + Button::new((action, index)).ghost().xsmall().icon(icon).tooltip(tooltip) } fn download_state_label(state: &DownloadState) -> &'static str { diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 2997a55..885d118 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -2,7 +2,7 @@ mod internal_pages; mod render; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; -use ely_domain::{CommandIntent, SpaceId, TabId, UrlText}; +use ely_domain::{CommandIntent, DownloadId, SpaceId, TabId, UrlText}; use gpui::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscription, Window}; use gpui_component::input::{InputEvent, InputState, SelectAll}; @@ -215,6 +215,38 @@ impl ElyShell { } } + fn pause_download(&mut self, download_id: &DownloadId, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.pause_download(download_id).is_ok() + { + cx.notify(); + } + } + + fn resume_download(&mut self, download_id: &DownloadId, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.resume_download(download_id).is_ok() + { + cx.notify(); + } + } + + fn cancel_download(&mut self, download_id: &DownloadId, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.cancel_download(download_id).is_ok() + { + cx.notify(); + } + } + + fn retry_download(&mut self, download_id: &DownloadId, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state + && core.retry_download(download_id).is_ok() + { + cx.notify(); + } + } + fn on_close_current_tab( &mut self, _: &CloseCurrentTab,