Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async fn download_to_file(
url: &str,
token: Option<&str>,
dest_path: &std::path::Path,
progress_tx: Option<futures::channel::mpsc::UnboundedSender<f32>>,
progress_tx: Option<futures::channel::mpsc::UnboundedSender<(u64, Option<u64>)>>,
) -> Result<()> {
let mut request = client.get(url);
if let Some(t) = token {
Expand Down Expand Up @@ -65,13 +65,31 @@ async fn download_to_file(
file.write_all(&chunk)?;
downloaded += chunk.len() as u64;

if let (Some(ref tx), Some(total)) = (&progress_tx, total) {
if total > 0 {
let pct = ((downloaded as f64 / total as f64) * 100.0) as u32;
if pct != last_pct {
last_pct = pct;
let _ = tx.unbounded_send(downloaded as f32 / total as f32);
if let Some(ref tx) = progress_tx {
// Throttle: send on whole-percent changes when the total is
// known, else every 256 KiB - not per network chunk.
let should_send = match total {
Some(total) if total > 0 => {
let pct = ((downloaded as f64 / total as f64) * 100.0) as u32;
if pct != last_pct {
last_pct = pct;
true
} else {
false
}
}
_ => {
let bucket = (downloaded / (256 * 1024)) as u32;
if bucket != last_pct {
last_pct = bucket;
true
} else {
false
}
}
};
if should_send {
let _ = tx.unbounded_send((downloaded, total));
}
}
}
Expand Down Expand Up @@ -309,7 +327,7 @@ pub struct AssetInstall {
pub async fn download_release_asset(
token: Option<String>,
install: AssetInstall,
progress_tx: Option<futures::channel::mpsc::UnboundedSender<f32>>,
progress_tx: Option<futures::channel::mpsc::UnboundedSender<(u64, Option<u64>)>>,
) -> Result<PathBuf> {
let AssetInstall {
repo_name,
Expand Down Expand Up @@ -444,7 +462,7 @@ pub async fn download_launcher_asset(
token: Option<String>,
tag: String,
filename: String,
progress_tx: Option<futures::channel::mpsc::UnboundedSender<f32>>,
progress_tx: Option<futures::channel::mpsc::UnboundedSender<(u64, Option<u64>)>>,
) -> Result<PathBuf> {
let temp_dir = colony_data_dir()?.join("update-staging");
std::fs::create_dir_all(&temp_dir)?;
Expand Down
37 changes: 36 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ impl App {
next_notification_id: 0,
app_icons: std::collections::HashMap::new(),
download_progress: None,
download_bytes: None,
download_speed: 0.0,
last_progress_sample: None,
download_abort: None,
downloading_repo: None,
favorites,
Expand Down Expand Up @@ -237,6 +240,7 @@ impl App {
update_queue: Vec::new(),
release_notes: std::collections::HashMap::new(),
fetching_notes: std::collections::HashSet::new(),
keyboard_cursor: None,
window_size: (
prefs.window_width.unwrap_or(1000.0).clamp(640.0, 7680.0),
prefs.window_height.unwrap_or(700.0).clamp(480.0, 4320.0),
Expand Down Expand Up @@ -315,7 +319,38 @@ impl App {
// Download progress bar with graphical bar and cancel button
if let Some((ref filename, progress)) = self.download_progress {
let pct = (progress * 100.0) as u32;
let bar_label = format!("\u{f019} {} — {}%", filename, pct);
// Size and speed when we have byte counters, plain % otherwise.
let bar_label = match self.download_bytes {
Some((done, Some(total))) if total > 0 => {
let speed = if self.download_speed > 1.0 {
format!(" · {}/s", state::human_bytes(self.download_speed as u64))
} else {
String::new()
};
format!(
"\u{f019} {} — {} / {} ({}%){}",
filename,
state::human_bytes(done),
state::human_bytes(total),
pct,
speed
)
}
Some((done, _)) => {
let speed = if self.download_speed > 1.0 {
format!(" · {}/s", state::human_bytes(self.download_speed as u64))
} else {
String::new()
};
format!(
"\u{f019} {} — {}{}",
filename,
state::human_bytes(done),
speed
)
}
None => format!("\u{f019} {} — {}%", filename, pct),
};
let cancel_btn = button(
text("\u{f00d}")
.size(self.sz(12))
Expand Down
5 changes: 4 additions & 1 deletion src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ pub enum Message {
GitHubRefreshRepos,
DownloadRelease(String, String), // (repo_name, platform_key)
#[allow(dead_code)]
DownloadProgress(String, f32), // (filename, progress 0.0..1.0)
/// (filename, downloaded bytes, total bytes when the server sent
/// Content-Length) - raw bytes so the UI can show size AND speed, not
/// just a bare percentage.
DownloadProgress(String, u64, Option<u64>),
DownloadCompleted(Result<(PathBuf, String, String), String>), // (path, repo_name, tag)
CancelDownload,
LaunchColonyApp(PathBuf),
Expand Down
48 changes: 48 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ pub struct App {
pub app_icons: std::collections::HashMap<String, iced::widget::image::Handle>,
// Download progress
pub download_progress: Option<(String, f32)>, // (filename, 0.0..1.0)
/// Raw transfer counters: (bytes downloaded, total when known).
pub download_bytes: Option<(u64, Option<u64>)>,
/// Smoothed transfer speed in bytes/second (EMA over progress samples).
pub download_speed: f32,
/// Last (instant, bytes) sample for the speed computation.
pub last_progress_sample: Option<(std::time::Instant, u64)>,
/// Abort handle for the in-flight download (app asset or launcher self-
/// update). Cancelling actually aborts the task instead of only clearing UI.
pub download_abort: Option<iced::task::Handle>,
Expand Down Expand Up @@ -244,6 +250,10 @@ pub struct App {
std::collections::HashMap<String, (String, Vec<crate::ui::markdown_blocks::DetailBlock>)>,
/// Repos whose release notes are currently being fetched.
pub fetching_notes: std::collections::HashSet<String>,
/// Keyboard-highlighted grid row, as a stable key ("repo:<name>" or
/// "app:<name>") so catalog refreshes and re-filters cannot silently move
/// the highlight to a different item. Cleared on search/section changes.
pub keyboard_cursor: Option<String>,
/// Live window size, persisted (debounced) so the next boot reopens at
/// the same dimensions. The pref fields existed but were never written.
pub window_size: (f32, f32),
Expand Down Expand Up @@ -271,6 +281,23 @@ impl App {
&self.colony_repo_list
}

/// Ordered stable keys of every row the grid currently shows (store repos
/// first, then local apps - the combined view's display order), for
/// keyboard traversal.
pub fn grid_keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self
.filtered_colony_repos()
.iter()
.map(|r| format!("repo:{}", r.name))
.collect();
keys.extend(
self.filtered_applications()
.iter()
.map(|a| format!("app:{}", a.name)),
);
keys
}

/// Resolve the repo whose detail page is open, surviving catalog refreshes
/// (`None` if the repo disappeared from the catalog).
pub fn active_repo(&self) -> Option<&ColonyRepo> {
Expand Down Expand Up @@ -427,6 +454,23 @@ impl App {
}
}

/// Human-readable byte size (KiB/MiB/GiB with one decimal).
pub fn human_bytes(bytes: u64) -> String {
const KIB: f64 = 1024.0;
const MIB: f64 = 1024.0 * 1024.0;
const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
let b = bytes as f64;
if b >= GIB {
format!("{:.1} GiB", b / GIB)
} else if b >= MIB {
format!("{:.1} MiB", b / MIB)
} else if b >= KIB {
format!("{:.0} KiB", b / KIB)
} else {
format!("{bytes} B")
}
}

pub fn capitalize_platform(p: &str) -> String {
match p.to_lowercase().as_str() {
"windows" => "Windows".to_string(),
Expand Down Expand Up @@ -459,6 +503,9 @@ impl App {
next_notification_id: 0,
app_icons: std::collections::HashMap::new(),
download_progress: None,
download_bytes: None,
download_speed: 0.0,
last_progress_sample: None,
download_abort: None,
downloading_repo: None,
favorites: Vec::new(),
Expand Down Expand Up @@ -497,6 +544,7 @@ impl App {
update_queue: Vec::new(),
release_notes: std::collections::HashMap::new(),
fetching_notes: std::collections::HashSet::new(),
keyboard_cursor: None,
window_size: (1000.0, 700.0),
window_save_gen: 0,
launcher_update_available: None,
Expand Down
24 changes: 19 additions & 5 deletions src/ui/app_grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,8 @@ impl App {
.align_y(iced::Alignment::Center)
.width(Fill);

let selected = self.active_colony_repo.as_deref() == Some(repo.name.as_str());
let selected = self.active_colony_repo.as_deref() == Some(repo.name.as_str())
|| self.keyboard_cursor.as_deref() == Some(&format!("repo:{}", repo.name));
self.cell_shell(
content.into(),
Message::ColonyRepoSelected(repo.name.clone()),
Expand All @@ -562,6 +563,7 @@ impl App {
&self,
content: Element<'a, Message>,
accent: Color,
selected: bool,
) -> Element<'a, Message> {
let bar = container(text(""))
.width(4)
Expand All @@ -586,10 +588,21 @@ impl App {
container(inner)
.width(Fill)
.height(Length::Fixed(self.sz(96)))
.style(|_theme| container::Style {
background: Some(Palette::BG_CARD().into()),
.style(move |_theme| container::Style {
background: Some(
if selected {
Palette::BG_SELECTED()
} else {
Palette::BG_CARD()
}
.into(),
),
border: iced::Border {
color: Palette::BORDER_SUBTLE(),
color: if selected {
Palette::ACCENT()
} else {
Palette::BORDER_SUBTLE()
},
width: 1.0,
radius: 12.0.into(),
},
Expand Down Expand Up @@ -679,6 +692,7 @@ impl App {
.align_y(iced::Alignment::Center)
.width(Fill);

self.cell_shell_static(content.into(), tint)
let selected = self.keyboard_cursor.as_deref() == Some(&format!("app:{}", app.name));
self.cell_shell_static(content.into(), tint, selected)
}
}
Loading
Loading