feat: added progress bar on first launch screen; changed progress bar to reusable component (there were 4 duplicates) (#740)

This commit is contained in:
Maciek 2026-08-25 15:16:01 +02:00 • committed by GitHub
parent e659ee122a
commit 5cac70b026
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 60 additions and 50 deletions

View file

@ -24,6 +24,7 @@ mod package_row;
mod package_update_popup; mod package_update_popup;
mod pagination; mod pagination;
mod player_model; mod player_model;
mod progress_track;
mod recents_row; mod recents_row;
mod screenshot_viewer; mod screenshot_viewer;
mod scrollview; mod scrollview;
@ -66,6 +67,7 @@ pub use package_row::{CardLayout, PackageEntry, PackageRow, provider_badge};
pub use package_update_popup::PackageUpdatePopup; pub use package_update_popup::PackageUpdatePopup;
pub use pagination::Pagination; pub use pagination::Pagination;
pub use player_model::PlayerModel; pub use player_model::PlayerModel;
pub use progress_track::progress_track;
pub use recents_row::RecentsRow; pub use recents_row::RecentsRow;
pub use screenshot_viewer::ScreenshotViewer; pub use screenshot_viewer::ScreenshotViewer;
pub(crate) use scrollview::corrected_scroll; pub(crate) use scrollview::corrected_scroll;

View file

@ -6,7 +6,7 @@ use oneclient_events::Level;
use crate::{ use crate::{
ui::{divider, relative_time}, ui::{divider, relative_time},
components::{Button, ButtonVariant, Icon, IconType, OverlayPopup, ScrollArea}, components::{Button, ButtonVariant, Icon, IconType, OverlayPopup, ScrollArea, progress_track},
hooks::{use_dispatch, use_notifications_snapshot}, hooks::{use_dispatch, use_notifications_snapshot},
notifications::{InboxEntry, NotificationActionKind}, notifications::{InboxEntry, NotificationActionKind},
theme::colors, theme::colors,
@ -382,18 +382,12 @@ fn row_body(entry: &InboxEntry, dispatch: crate::Actions) -> impl IntoElement {
fn row_extra(entry: &InboxEntry, dispatch: crate::Actions) -> impl IntoElement { fn row_extra(entry: &InboxEntry, dispatch: crate::Actions) -> impl IntoElement {
if let Some((current, total)) = entry.progress.filter(|_| entry.is_loading) { if let Some((current, total)) = entry.progress.filter(|_| entry.is_loading) {
let frac = (current as f32 / total.max(1) as f32).clamp(0.0, 1.0); let frac = (current as f32 / total.max(1) as f32).clamp(0.0, 1.0);
let bar = rect() let bar = progress_track(
.width(Size::fill()) frac * 100.0,
.height(Size::px(6.)) 6.,
.corner_radius(CornerRadius::new_all(50.)) colors::brand(),
.background(colors::brand().with_a(128)) colors::brand().with_a(128),
.child( );
rect()
.width(Size::percent(frac * 100.0))
.height(Size::px(6.))
.corner_radius(CornerRadius::new_all(50.))
.background(colors::brand()),
);
return rect() return rect()
.vertical() .vertical()

View file

@ -0,0 +1,20 @@
use freya::prelude::*;
/// The app's one progress meter: a pill-shaped track with a filled portion.
///
/// `pct` is 0-100 and is clamped here, so callers can hand over a raw ratio
/// without guarding against a denominator that grew mid-flight.
pub fn progress_track(pct: f32, height: f32, fill: Color, bg: Color) -> Rect {
rect()
.width(Size::fill())
.height(Size::px(height))
.corner_radius(CornerRadius::new_all(height / 2.))
.background(bg)
.child(
rect()
.width(Size::percent(pct.clamp(0.0, 100.0)))
.height(Size::fill())
.corner_radius(CornerRadius::new_all(height / 2.))
.background(fill),
)
}

View file

@ -11,7 +11,7 @@ use oneclient_events::Level;
use crate::{ use crate::{
ui::{divider}, ui::{divider},
components::{Button, ButtonSize, Icon, IconType}, components::{Button, ButtonSize, Icon, IconType, progress_track},
hooks::{use_dispatch, use_notifications_snapshot}, hooks::{use_dispatch, use_notifications_snapshot},
notifications::{InboxEntry, NotificationActionKind}, notifications::{InboxEntry, NotificationActionKind},
theme::colors, theme::colors,
@ -440,19 +440,13 @@ fn progress_card(entry: &InboxEntry) -> Rect {
.color(colors::fg_secondary()), .color(colors::fg_secondary()),
) )
.child( .child(
rect() progress_track(
.width(Size::fill()) frac * 100.0,
.height(Size::px(6.)) 6.,
.margin(Gaps::new(8., 0., 0., 0.)) colors::brand(),
.corner_radius(CornerRadius::new_all(50.)) colors::brand().with_a(128),
.background(colors::brand().with_a(128)) )
.child( .margin(Gaps::new(8., 0., 0., 0.)),
rect()
.width(Size::percent(frac * 100.0))
.height(Size::px(6.))
.corner_radius(CornerRadius::new_all(50.))
.background(colors::brand()),
),
), ),
), ),
) )

View file

@ -1,6 +1,6 @@
use super::*; use super::*;
use crate::components::Button; use crate::components::{Button, progress_track};
use crate::theme::colors; use crate::theme::colors;
use crate::ui::border_all_color; use crate::ui::border_all_color;
use crate::utils::{format_duration_hms, format_size}; use crate::utils::{format_duration_hms, format_size};
@ -389,19 +389,3 @@ impl Component for ActivityIndicator {
) )
} }
} }
fn progress_track(pct: f32, height: f32, fill: Color, bg: Color) -> impl IntoElement {
rect()
.width(Size::fill())
.height(Size::px(height))
.corner_radius(CornerRadius::new_all(height / 2.))
.background(bg)
.child(
rect()
.width(Size::percent(pct.clamp(0.0, 100.0)))
.height(Size::fill())
.corner_radius(CornerRadius::new_all(height / 2.))
.background(fill),
)
.into_element()
}

View file

@ -8,9 +8,13 @@ use crate::hooks::{
use_splash, use_terms, use_splash, use_terms,
}; };
use crate::hooks::use_dispatch; use crate::hooks::use_dispatch;
use crate::components::Button; use crate::components::{Button, progress_track};
use crate::theme::colors; use crate::theme::colors;
/// Startup is a sequence of boolean gates, not measurable work, so the bar steps
/// once per gate the launcher clears instead of tracking bytes
const STARTUP_STEPS: u8 = 3;
#[derive(PartialEq)] #[derive(PartialEq)]
struct RecoveryActions { struct RecoveryActions {
snapshots: usize, snapshots: usize,
@ -124,11 +128,13 @@ impl Component for Startup {
let active = notifications.inbox.iter().find(|entry| entry.is_loading); let active = notifications.inbox.iter().find(|entry| entry.is_loading);
let (message, detail): (String, Option<String>) = let (message, detail, step): (String, Option<String>, u8) =
if let Some(err) = launcher.error.as_deref() { if let Some(err) = launcher.error.as_deref() {
let step = if launcher.ready { 2 } else { 1 };
( (
"Couldn't start OneClient".to_string(), "Couldn't start OneClient".to_string(),
Some(err.to_string()), Some(err.to_string()),
step,
) )
} else if let Some(entry) = active { } else if let Some(entry) = active {
let detail = entry let detail = entry
@ -137,11 +143,11 @@ impl Component for Startup {
.find(|t| t.total == 0 || t.current < t.total) .find(|t| t.total == 0 || t.current < t.total)
.map(|t| t.label.clone()) .map(|t| t.label.clone())
.or_else(|| entry.progress.map(|(c, t)| format!("{c} / {t}"))); .or_else(|| entry.progress.map(|(c, t)| format!("{c} / {t}")));
(entry.title.clone(), detail) (entry.title.clone(), detail, 3)
} else if launcher.ready { } else if launcher.ready {
("Fetching versions and bundles...".to_string(), None) ("Fetching versions and bundles...".to_string(), None, 2)
} else { } else {
("Starting OneClient...".to_string(), None) ("Starting OneClient...".to_string(), None, 1)
}; };
let is_error = launcher.error.is_some(); let is_error = launcher.error.is_some();
@ -156,6 +162,15 @@ impl Component for Startup {
colors::brand() colors::brand()
}; };
let filled = f32::from(step) / f32::from(STARTUP_STEPS + 1) * 100.;
let progress = rect().width(Size::px(288.)).child(progress_track(
filled,
4.,
dot_color,
colors::fg_primary().with_a(30),
));
let loading_row = rect() let loading_row = rect()
.horizontal() .horizontal()
.cross_align(Alignment::Center) .cross_align(Alignment::Center)
@ -192,7 +207,8 @@ impl Component for Startup {
.color(colors::fg_primary().with_a(140)), .color(colors::fg_primary().with_a(140)),
) )
.child(rect().height(Size::px(14.))) .child(rect().height(Size::px(14.)))
.child(loading_row); .child(loading_row)
.child(progress);
if let Some(detail) = detail { if let Some(detail) = detail {
content = content.child( content = content.child(