feat(ui): browser grid tags and install button on package cards
This commit is contained in:
parent
6e25ea2bfd
commit
fb90eb7bb8
4 changed files with 101 additions and 54 deletions
|
|
@ -53,6 +53,7 @@ pub struct Button {
|
|||
|
||||
enabled: bool,
|
||||
focusable: bool,
|
||||
alt: Option<Box<str>>,
|
||||
on_press: Option<EventHandler<Event<PressEventData>>>,
|
||||
|
||||
elements: Vec<Element>,
|
||||
|
|
@ -110,6 +111,7 @@ impl Button {
|
|||
variant,
|
||||
enabled: true,
|
||||
focusable: true,
|
||||
alt: None,
|
||||
on_press: None,
|
||||
elements: Vec::new(),
|
||||
key: DiffKey::None,
|
||||
|
|
@ -196,6 +198,13 @@ impl Button {
|
|||
self
|
||||
}
|
||||
|
||||
/// The name a button carrying only an icon goes by, since there is no label
|
||||
/// for accessibility to read out.
|
||||
pub fn alt(mut self, alt: impl Into<Box<str>>) -> Self {
|
||||
self.alt = Some(alt.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cursor_icon(mut self, cursor_icon: CursorIcon) -> Self {
|
||||
self.cursor_icon = cursor_icon;
|
||||
self
|
||||
|
|
@ -284,6 +293,7 @@ impl Component for Button {
|
|||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(enabled() && self.focusable)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.map(self.alt.clone(), |rect, alt| rect.a11y_alt(alt))
|
||||
.overflow(Overflow::Clip)
|
||||
.layout(self.layout.clone())
|
||||
.text_style(self.text_style.clone())
|
||||
|
|
|
|||
|
|
@ -17,8 +17,16 @@ use crate::ui::border_all_color;
|
|||
|
||||
type InstalledMap = HashMap<(ProviderId, String), Installed>;
|
||||
|
||||
fn installed_for(installed: &InstalledMap, item: &ProjectSummary) -> Option<Installed> {
|
||||
installed.get(&(item.provider, item.id.clone())).cloned()
|
||||
/// How tall a card's footer row is kept, so the row sits at the same height
|
||||
/// whether it holds the install button or only the download count.
|
||||
const CARD_FOOTER_H: f32 = 30.;
|
||||
/// Diameter of the round install button.
|
||||
const INSTALL_BUTTON: f32 = 30.;
|
||||
|
||||
fn installed_for(installed: &InstalledMap, item: &ProjectSummary) -> Option<InstallSource> {
|
||||
installed
|
||||
.get(&(item.provider, item.id.clone()))
|
||||
.map(|installed| installed.source)
|
||||
}
|
||||
|
||||
pub(super) fn grid_row(
|
||||
|
|
@ -29,7 +37,7 @@ pub(super) fn grid_row(
|
|||
) -> impl IntoElement {
|
||||
let package_type = package_type.to_string();
|
||||
let fill = GRID_COLUMNS - row.len();
|
||||
let installed: Vec<Option<Installed>> = row
|
||||
let installed: Vec<Option<InstallSource>> = row
|
||||
.iter()
|
||||
.map(|item| installed_for(installed, item))
|
||||
.collect();
|
||||
|
|
@ -70,7 +78,7 @@ struct PackageCard {
|
|||
item: ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<Installed>,
|
||||
installed: Option<InstallSource>,
|
||||
}
|
||||
|
||||
impl PackageCard {
|
||||
|
|
@ -78,7 +86,7 @@ impl PackageCard {
|
|||
item: ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<Installed>,
|
||||
installed: Option<InstallSource>,
|
||||
) -> Self {
|
||||
Self {
|
||||
item,
|
||||
|
|
@ -135,7 +143,17 @@ impl Component for PackageCard {
|
|||
..Default::default()
|
||||
})
|
||||
.overflow(Overflow::Clip)
|
||||
.child(PackageBanner::new(icon_url, BANNER_H)),
|
||||
.child(PackageBanner::new(icon_url, BANNER_H))
|
||||
// Sat in the banner's top-left corner rather than down in
|
||||
// the footer: it is the first thing worth knowing about a
|
||||
// result, and it leaves the footer to the install button.
|
||||
.maybe_child(self.installed.map(|installed| {
|
||||
rect()
|
||||
.position(Position::new_absolute().top(8.).left(8.))
|
||||
.layer(Layer::Relative(7))
|
||||
.child(installed_badge_overlay(installed))
|
||||
.into_element()
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
|
|
@ -184,6 +202,9 @@ impl Component for PackageCard {
|
|||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
// Held at the button's height so the download count
|
||||
// doesn't shift down on cards without one.
|
||||
.height(Size::px(CARD_FOOTER_H))
|
||||
.cross_align(Alignment::Center)
|
||||
.content(Content::Flex)
|
||||
.child(downloads_row(self.item.downloads))
|
||||
|
|
@ -193,16 +214,14 @@ impl Component for PackageCard {
|
|||
.width(Size::flex(1.0))
|
||||
.cross_align(Alignment::Center)
|
||||
.main_align(Alignment::End)
|
||||
.spacing(6.)
|
||||
.maybe_child(self.installed.as_ref().map(|installed| {
|
||||
installed_badge(installed.source, 10.).into_element()
|
||||
}))
|
||||
.child(InstallButton::new(
|
||||
&self.item,
|
||||
self.cluster_id,
|
||||
&self.package_type,
|
||||
self.installed.clone(),
|
||||
)),
|
||||
.maybe_child(self.installed.is_none().then(|| {
|
||||
InstallButton::new(
|
||||
&self.item,
|
||||
self.cluster_id,
|
||||
&self.package_type,
|
||||
)
|
||||
.into_element()
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -228,7 +247,7 @@ struct ListRow {
|
|||
item: ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<Installed>,
|
||||
installed: Option<InstallSource>,
|
||||
}
|
||||
|
||||
impl Component for ListRow {
|
||||
|
|
@ -311,44 +330,34 @@ impl Component for ListRow {
|
|||
)
|
||||
.maybe_child(
|
||||
self.installed
|
||||
.as_ref()
|
||||
.map(|installed| installed_badge(installed.source, 11.).into_element()),
|
||||
.map(|installed| installed_badge(installed, 11.).into_element()),
|
||||
)
|
||||
.child(downloads_row(item.downloads))
|
||||
.child(InstallButton::new(
|
||||
&self.item,
|
||||
self.cluster_id,
|
||||
&self.package_type,
|
||||
self.installed.clone(),
|
||||
))
|
||||
.maybe_child(self.installed.is_none().then(|| {
|
||||
InstallButton::new(&self.item, self.cluster_id, &self.package_type).into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Installs a listing entry's latest compatible version without making the user
|
||||
/// open its page first. Same version pick, same dispatch and same in-flight
|
||||
/// handling as the install button on the package page.
|
||||
/// open its page first. Same version pick and same dispatch as the install
|
||||
/// button on the package page. Only rendered for entries the cluster doesn't
|
||||
/// already have — an installed or bundled one wears its badge instead.
|
||||
#[derive(PartialEq)]
|
||||
struct InstallButton {
|
||||
provider: ProviderId,
|
||||
project_id: String,
|
||||
cluster_id: i64,
|
||||
content_type: ContentType,
|
||||
installed: Option<Installed>,
|
||||
}
|
||||
|
||||
impl InstallButton {
|
||||
fn new(
|
||||
item: &ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: &str,
|
||||
installed: Option<Installed>,
|
||||
) -> Self {
|
||||
fn new(item: &ProjectSummary, cluster_id: i64, package_type: &str) -> Self {
|
||||
Self {
|
||||
provider: item.provider,
|
||||
project_id: item.id.clone(),
|
||||
cluster_id,
|
||||
content_type: content_type_for_slug(package_type),
|
||||
installed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -382,13 +391,9 @@ impl Component for InstallButton {
|
|||
));
|
||||
let latest = versions.first().map(|v| v.version_id.clone());
|
||||
|
||||
// Nothing to start twice while the install that was just kicked off is
|
||||
// still running, and nothing to start before the version list arrives.
|
||||
let installing = use_installs_snapshot().is_installing(cluster_id, provider, &project_id);
|
||||
// Nothing to do when the cluster already has exactly this version, and
|
||||
// nothing to do twice while the install that was just started is running.
|
||||
let have_latest = match (&self.installed, &latest) {
|
||||
(Some(installed), Some(latest)) => installed.is_version(latest),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
rect()
|
||||
// The card behind this is one big press target. A press on the
|
||||
|
|
@ -397,9 +402,13 @@ impl Component for InstallButton {
|
|||
.on_all_press(|e: Event<PressEventData>| e.stop_propagation())
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.small()
|
||||
.enabled(latest.is_some() && !have_latest && !installing)
|
||||
.primary()
|
||||
.width(Size::px(INSTALL_BUTTON))
|
||||
.height(Size::px(INSTALL_BUTTON))
|
||||
.padding(Gaps::new_all(0.))
|
||||
.corner_radius(CornerRadius::new_all(INSTALL_BUTTON / 2.))
|
||||
.alt(if installing { "Installing" } else { "Install" })
|
||||
.enabled(latest.is_some() && !installing)
|
||||
.on_press(move |_| {
|
||||
if let Some(version_id) = latest.clone() {
|
||||
dispatch.install_package(
|
||||
|
|
@ -410,13 +419,15 @@ impl Component for InstallButton {
|
|||
);
|
||||
}
|
||||
})
|
||||
.text(if installing {
|
||||
"Installing..."
|
||||
} else if have_latest {
|
||||
"Installed"
|
||||
} else {
|
||||
"Install"
|
||||
}),
|
||||
.child(
|
||||
Icon::new(if installing {
|
||||
IconType::Loading02
|
||||
} else {
|
||||
IconType::Download01
|
||||
})
|
||||
.size(15.)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use oneclient_core::settings::ViewLayout;
|
|||
use crate::components::{
|
||||
Dropdown, Icon, IconType, Pagination, ScrollArea, Segment, SegmentedControl, TextInput,
|
||||
};
|
||||
use crate::hooks::use_cluster;
|
||||
use crate::hooks::{
|
||||
BROWSE_PAGE_SIZE, BrowserUiState, bundles_with_status_items, category_list,
|
||||
cluster_content_items, content_type_for_slug, pick_version_metadata, search_items,
|
||||
|
|
@ -25,9 +26,11 @@ use crate::hooks::{
|
|||
};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
use crate::hooks::use_cluster;
|
||||
|
||||
use super::{Installed, PackageBanner, Thumbnail, installed_badge, installed_map};
|
||||
use super::{
|
||||
InstallSource, Installed, PackageBanner, Thumbnail, installed_badge, installed_badge_overlay,
|
||||
installed_map,
|
||||
};
|
||||
use crate::utils::{abbreviate_number, sort_clusters_for_home};
|
||||
|
||||
mod cards;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use oneclient_core::{BundleFileKind, BundleWithUpdateStatus, LinkedArtifactInfo}
|
|||
use crate::components::{Icon, IconType};
|
||||
use crate::hooks::{loaded_image, use_cached_image};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
const BANNER_BG: Color = Color::from_rgb(21, 28, 34);
|
||||
|
||||
|
|
@ -105,6 +106,27 @@ pub(crate) fn installed_map(
|
|||
}
|
||||
|
||||
pub(crate) fn installed_badge(installed: InstallSource, font_size: f32) -> impl IntoElement {
|
||||
badge(installed, font_size, installed.color().with_a(38), None)
|
||||
}
|
||||
|
||||
/// The same badge sat on top of a card's artwork. The faint tint the in-card
|
||||
/// badge uses has nothing to read against there, so this one brings its own
|
||||
/// backdrop and outlines itself to stay legible over whatever the image is.
|
||||
pub(crate) fn installed_badge_overlay(installed: InstallSource) -> impl IntoElement {
|
||||
badge(
|
||||
installed,
|
||||
10.,
|
||||
BANNER_BG.with_a(225),
|
||||
Some(installed.color().with_a(110)),
|
||||
)
|
||||
}
|
||||
|
||||
fn badge(
|
||||
installed: InstallSource,
|
||||
font_size: f32,
|
||||
background: Color,
|
||||
border: Option<Color>,
|
||||
) -> impl IntoElement {
|
||||
let color = installed.color();
|
||||
|
||||
rect()
|
||||
|
|
@ -113,7 +135,8 @@ pub(crate) fn installed_badge(installed: InstallSource, font_size: f32) -> impl
|
|||
.spacing(4.)
|
||||
.padding(Gaps::new_symmetric(2., 8.))
|
||||
.corner_radius(CornerRadius::new_all(999.))
|
||||
.background(color.with_a(38))
|
||||
.background(background)
|
||||
.map(border, |el, border| el.border(border_all_color(1., border)))
|
||||
.child(
|
||||
Icon::new(IconType::CheckCircle)
|
||||
.size(font_size)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue