Render downloads internal page

This commit is contained in:
2026-05-07 21:30:39 -04:00
parent d40e33c206
commit f93943ccfe
8 changed files with 361 additions and 3 deletions
@@ -1,3 +1,5 @@
mod downloads;
use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing};
use ely_domain::{ArchiveSource, ArchivedTab, BrowserTab, HistoryEntry};
@@ -17,6 +19,7 @@ impl ElyShell {
cx: &mut Context<Self>,
) -> AnyElement {
match tab.url().as_str() {
"ely://downloads" => self.render_downloads_page(snapshot),
"ely://history" => self.render_history_page(snapshot, cx),
"ely://archive" => self.render_archive_page(snapshot, cx),
_ => render_default_page(tab),
@@ -0,0 +1,183 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors;
use ely_domain::{DownloadEntry, DownloadState};
use gpui::{
AnyElement, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, px, rgb,
};
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 {
render_canvas_surface(
div()
.size_full()
.p_8()
.flex()
.flex_col()
.gap_5()
.child(
div()
.flex()
.items_end()
.justify_between()
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_size(px(26.0))
.text_color(rgb(colors::INK))
.child("Downloads"),
)
.child(
div()
.text_sm()
.text_color(rgb(colors::MUTED))
.child(snapshot.active_profile_name.clone()),
),
)
.child(
div()
.text_xs()
.text_color(rgb(colors::MUTED))
.child(format!("{} downloads", snapshot.download_entries.len())),
),
)
.child(render_downloads_list(snapshot)),
)
}
}
fn render_downloads_list(snapshot: &BrowserSnapshot) -> 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))
.children(
snapshot
.download_entries
.iter()
.rev()
.enumerate()
.map(|(index, entry)| render_download_row(index, entry)),
)
.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()
.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)),
),
)
.into_any_element()
}
fn download_state_label(state: &DownloadState) -> &'static str {
match state {
DownloadState::InProgress => "In progress",
DownloadState::Paused => "Paused",
DownloadState::Completed => "Complete",
DownloadState::Cancelled => "Cancelled",
DownloadState::Failed => "Failed",
}
}
fn download_size_label(entry: &DownloadEntry) -> String {
match entry.total_bytes() {
Some(total_bytes) => {
format!("{} of {}", format_bytes(entry.received_bytes()), format_bytes(total_bytes))
}
None => format_bytes(entry.received_bytes()),
}
}
fn format_bytes(bytes: u64) -> String {
const KIB: u64 = 1024;
const MIB: u64 = KIB * 1024;
const GIB: u64 = MIB * 1024;
if bytes >= GIB {
return format!("{:.1} GB", bytes as f64 / GIB as f64);
}
if bytes >= MIB {
return format!("{:.1} MB", bytes as f64 / MIB as f64);
}
if bytes >= KIB {
return format!("{:.1} KB", bytes as f64 / KIB as f64);
}
format!("{bytes} B")
}
+7 -2
View File
@@ -1,13 +1,14 @@
use std::collections::BTreeMap;
use ely_domain::{
ArchivedTab, BrowserTab, DomainError, HistoryEntry, Profile, ProfileId, ProfileKind, Space,
SpaceId, TabId, UrlText,
ArchivedTab, BrowserTab, DomainError, DownloadEntry, HistoryEntry, Profile, ProfileId,
ProfileKind, Space, SpaceId, TabId, UrlText,
};
use crate::CoreError;
mod commands;
mod downloads;
mod history;
mod profiles;
mod tabs;
@@ -37,6 +38,7 @@ pub struct BrowserSnapshot {
pub favorites: Vec<BrowserTab>,
pub pinned_tabs: Vec<BrowserTab>,
pub archived_tabs: Vec<ArchivedTab>,
pub download_entries: Vec<DownloadEntry>,
pub history_entries: Vec<HistoryEntry>,
pub spaces: Vec<Space>,
pub active_tab_id: TabId,
@@ -52,6 +54,7 @@ pub struct BrowserCore {
profiles: Vec<Profile>,
tabs: Vec<BrowserTab>,
archived_tabs: Vec<ArchivedTab>,
download_entries: Vec<DownloadEntry>,
history_entries: Vec<HistoryEntry>,
active_space_id: SpaceId,
active_profile_id: ProfileId,
@@ -93,6 +96,7 @@ impl BrowserCore {
profiles: vec![profile],
tabs: vec![tab],
archived_tabs: Vec::new(),
download_entries: Vec::new(),
history_entries: Vec::new(),
command_query: String::new(),
new_tab_url,
@@ -179,6 +183,7 @@ impl BrowserCore {
favorites: self.favorites(),
pinned_tabs: self.pinned_tabs(),
archived_tabs: self.archived_tabs.clone(),
download_entries: self.visible_downloads(),
history_entries: self.visible_history(),
spaces: self.spaces.clone(),
tabs: self.visible_tabs(),
@@ -0,0 +1,35 @@
use std::time::SystemTime;
use ely_domain::{DownloadEntry, DownloadId, UrlText};
use crate::CoreError;
use super::BrowserCore;
impl BrowserCore {
pub fn record_download_started(
&mut self,
source_url: UrlText,
file_name: impl Into<String>,
total_bytes: Option<u64>,
) -> Result<DownloadId, CoreError> {
let entry = DownloadEntry::started(
self.active_profile_id.clone(),
source_url,
file_name,
total_bytes,
SystemTime::now(),
)?;
let download_id = entry.id().clone();
self.download_entries.push(entry);
Ok(download_id)
}
pub(super) fn visible_downloads(&self) -> Vec<DownloadEntry> {
self.download_entries
.iter()
.filter(|entry| entry.profile_id() == &self.active_profile_id)
.cloned()
.collect()
}
}
@@ -0,0 +1,38 @@
use std::error::Error;
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{ProfileKind, UrlText};
#[test]
fn download_entries_stay_with_active_profile() -> Result<(), Box<dyn Error>> {
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
let default_profile_id = core.active_tab()?.profile_id().clone();
core.record_download_started(
UrlText::parse("https://example.com/report.pdf")?,
"report.pdf",
Some(2048),
)?;
let default_snapshot = core.snapshot()?;
assert_eq!(default_snapshot.download_entries.len(), 1);
assert_eq!(default_snapshot.download_entries[0].file_name(), "report.pdf");
assert_eq!(default_snapshot.download_entries[0].profile_id(), &default_profile_id);
core.create_profile("Personal", 0xf54e00, ProfileKind::Standard)?;
let personal_snapshot = core.snapshot()?;
assert!(personal_snapshot.download_entries.is_empty());
core.record_download_started(
UrlText::parse("https://example.com/archive.zip")?,
"archive.zip",
None,
)?;
assert_eq!(core.snapshot()?.download_entries.len(), 1);
core.select_profile(&default_profile_id)?;
let default_snapshot = core.snapshot()?;
assert_eq!(default_snapshot.download_entries.len(), 1);
assert_eq!(default_snapshot.download_entries[0].file_name(), "report.pdf");
Ok(())
}
+91
View File
@@ -0,0 +1,91 @@
use std::time::SystemTime;
use crate::{DomainError, DownloadId, ProfileId, UrlText};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DownloadState {
InProgress,
Paused,
Completed,
Cancelled,
Failed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DownloadEntry {
id: DownloadId,
profile_id: ProfileId,
source_url: UrlText,
file_name: String,
state: DownloadState,
received_bytes: u64,
total_bytes: Option<u64>,
started_at: SystemTime,
}
impl DownloadEntry {
pub fn started(
profile_id: ProfileId,
source_url: UrlText,
file_name: impl Into<String>,
total_bytes: Option<u64>,
started_at: SystemTime,
) -> Result<Self, DomainError> {
let file_name = file_name.into();
let file_name = file_name.trim();
if file_name.is_empty() {
return Err(DomainError::EmptyField { field: "file_name" });
}
Ok(Self {
id: DownloadId::new(),
profile_id,
source_url,
file_name: file_name.to_string(),
state: DownloadState::InProgress,
received_bytes: 0,
total_bytes,
started_at,
})
}
#[must_use]
pub fn id(&self) -> &DownloadId {
&self.id
}
#[must_use]
pub fn profile_id(&self) -> &ProfileId {
&self.profile_id
}
#[must_use]
pub fn source_url(&self) -> &UrlText {
&self.source_url
}
#[must_use]
pub fn file_name(&self) -> &str {
&self.file_name
}
#[must_use]
pub fn state(&self) -> &DownloadState {
&self.state
}
#[must_use]
pub fn received_bytes(&self) -> u64 {
self.received_bytes
}
#[must_use]
pub fn total_bytes(&self) -> Option<u64> {
self.total_bytes
}
#[must_use]
pub fn started_at(&self) -> SystemTime {
self.started_at
}
}
+1
View File
@@ -38,3 +38,4 @@ entity_id!(SpaceId, "space");
entity_id!(ProfileId, "profile");
entity_id!(SplitId, "split");
entity_id!(WebViewId, "webview");
entity_id!(DownloadId, "download");
+3 -1
View File
@@ -1,5 +1,6 @@
mod archive;
mod command;
mod download;
mod error;
mod history;
mod identifiers;
@@ -11,9 +12,10 @@ mod url_text;
pub use archive::{ArchiveSource, ArchivedTab};
pub use command::{CommandIntent, CommandScope};
pub use download::{DownloadEntry, DownloadState};
pub use error::DomainError;
pub use history::HistoryEntry;
pub use identifiers::{ProfileId, SpaceId, SplitId, TabId, WebViewId};
pub use identifiers::{DownloadId, ProfileId, SpaceId, SplitId, TabId, WebViewId};
pub use profile::{Profile, ProfileKind};
pub use space::{ArchivePolicy, Space};
pub use split::{SplitAxis, SplitLayout, SplitPane};