Add UX enhancements: carousel, filter chips, command palette, and more
- Replace featured section Stack with AdwCarousel + indicator dots - Convert category grid to horizontal scrollable filter chips - Add grid/list view toggle for catalog with compact row layout - Add quick launch button on library list rows - Add stale catalog banner when data is older than 7 days - Add command palette (Ctrl+K) for quick app search and launch - Show specific app names in update notifications - Add per-app auto-update toggle (skip updates switch) - Add keyboard shortcut hints to button tooltips - Add source trust badges (AppImageHub/Community) on catalog tiles - Add undo-based uninstall with toast and record restoration - Add type-to-search in library view - Use human-readable catalog source labels - Show Launch button for installed apps in catalog detail - Replace external browser link with inline AppImage explainer dialog
This commit is contained in:
@@ -6,7 +6,8 @@ use super::widgets;
|
||||
/// Build a catalog tile for the browse grid.
|
||||
/// Left-aligned layout: icon (48px) at top, name, description, category badge.
|
||||
/// Card fills its entire FlowBoxChild cell.
|
||||
pub fn build_catalog_tile(app: &CatalogApp) -> gtk::FlowBoxChild {
|
||||
/// If `installed` is true, an "Installed" badge is shown on the card.
|
||||
pub fn build_catalog_tile(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChild {
|
||||
let card = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(6)
|
||||
@@ -75,10 +76,11 @@ pub fn build_catalog_tile(app: &CatalogApp) -> gtk::FlowBoxChild {
|
||||
desc_label.set_height_request(desc_label.preferred_size().1.height().max(36));
|
||||
inner.append(&desc_label);
|
||||
|
||||
// Stats row (stars + version) - only if data exists
|
||||
// Stats row (downloads + stars + version) - only if data exists
|
||||
let has_downloads = app.ocs_downloads.is_some_and(|d| d > 0);
|
||||
let has_stars = app.github_stars.is_some_and(|s| s > 0);
|
||||
let has_version = app.latest_version.is_some();
|
||||
if has_stars || has_version {
|
||||
if has_downloads || has_stars || has_version {
|
||||
let stats_row = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.spacing(12)
|
||||
@@ -86,6 +88,22 @@ pub fn build_catalog_tile(app: &CatalogApp) -> gtk::FlowBoxChild {
|
||||
.build();
|
||||
stats_row.add_css_class("catalog-stats-row");
|
||||
|
||||
if let Some(downloads) = app.ocs_downloads.filter(|&d| d > 0) {
|
||||
let dl_box = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.spacing(4)
|
||||
.build();
|
||||
let dl_icon = gtk::Image::from_icon_name("folder-download-symbolic");
|
||||
dl_icon.set_pixel_size(12);
|
||||
dl_box.append(&dl_icon);
|
||||
let dl_label = gtk::Label::new(Some(&widgets::format_count(downloads)));
|
||||
dl_label.add_css_class("caption");
|
||||
dl_label.add_css_class("dim-label");
|
||||
dl_box.append(&dl_label);
|
||||
dl_box.set_tooltip_text(Some(&format!("{} downloads", downloads)));
|
||||
stats_row.append(&dl_box);
|
||||
}
|
||||
|
||||
if let Some(stars) = app.github_stars.filter(|&s| s > 0) {
|
||||
let star_box = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
@@ -135,6 +153,21 @@ pub fn build_catalog_tile(app: &CatalogApp) -> gtk::FlowBoxChild {
|
||||
}
|
||||
}
|
||||
|
||||
// Installed badge
|
||||
if installed {
|
||||
let installed_badge = widgets::status_badge("Installed", "success");
|
||||
installed_badge.set_halign(gtk::Align::Start);
|
||||
installed_badge.set_margin_top(4);
|
||||
inner.append(&installed_badge);
|
||||
}
|
||||
|
||||
// Source badge - show which source this app came from
|
||||
let source_label = if app.ocs_id.is_some() { "AppImageHub" } else { "Community" };
|
||||
let source_badge = widgets::status_badge(source_label, "neutral");
|
||||
source_badge.set_halign(gtk::Align::Start);
|
||||
source_badge.set_margin_top(2);
|
||||
inner.append(&source_badge);
|
||||
|
||||
card.append(&inner);
|
||||
|
||||
let child = gtk::FlowBoxChild::builder()
|
||||
@@ -146,6 +179,92 @@ pub fn build_catalog_tile(app: &CatalogApp) -> gtk::FlowBoxChild {
|
||||
child
|
||||
}
|
||||
|
||||
/// Build a compact list-row tile for the browse grid in list mode.
|
||||
/// Horizontal layout: icon (32px) | name | description snippet | stats.
|
||||
pub fn build_catalog_row(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChild {
|
||||
let row = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.spacing(12)
|
||||
.halign(gtk::Align::Fill)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
row.add_css_class("card");
|
||||
row.add_css_class("catalog-row");
|
||||
|
||||
let inner = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.spacing(12)
|
||||
.margin_top(8)
|
||||
.margin_bottom(8)
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.hexpand(true)
|
||||
.build();
|
||||
|
||||
// Icon (32px)
|
||||
let icon = widgets::app_icon(None, &app.name, 32);
|
||||
icon.set_valign(gtk::Align::Center);
|
||||
inner.append(&icon);
|
||||
|
||||
// Name
|
||||
let name_label = gtk::Label::builder()
|
||||
.label(&app.name)
|
||||
.css_classes(["heading"])
|
||||
.ellipsize(gtk::pango::EllipsizeMode::End)
|
||||
.max_width_chars(18)
|
||||
.xalign(0.0)
|
||||
.width_chars(14)
|
||||
.build();
|
||||
inner.append(&name_label);
|
||||
|
||||
// Description (single line)
|
||||
let plain = app.ocs_summary.as_deref()
|
||||
.filter(|d| !d.is_empty())
|
||||
.or(app.github_description.as_deref().filter(|d| !d.is_empty()))
|
||||
.or(app.description.as_deref().filter(|d| !d.is_empty()))
|
||||
.map(|d| strip_html(d))
|
||||
.unwrap_or_default();
|
||||
let desc_label = gtk::Label::builder()
|
||||
.label(&plain)
|
||||
.css_classes(["dim-label"])
|
||||
.ellipsize(gtk::pango::EllipsizeMode::End)
|
||||
.hexpand(true)
|
||||
.xalign(0.0)
|
||||
.build();
|
||||
inner.append(&desc_label);
|
||||
|
||||
// Stats (compact)
|
||||
if let Some(downloads) = app.ocs_downloads.filter(|&d| d > 0) {
|
||||
let dl_label = gtk::Label::builder()
|
||||
.label(&format!("{} dl", widgets::format_count(downloads)))
|
||||
.css_classes(["caption", "dim-label"])
|
||||
.build();
|
||||
inner.append(&dl_label);
|
||||
} else if let Some(stars) = app.github_stars.filter(|&s| s > 0) {
|
||||
let star_label = gtk::Label::builder()
|
||||
.label(&format!("{} stars", widgets::format_count(stars)))
|
||||
.css_classes(["caption", "dim-label"])
|
||||
.build();
|
||||
inner.append(&star_label);
|
||||
}
|
||||
|
||||
// Installed badge
|
||||
if installed {
|
||||
let badge = widgets::status_badge("Installed", "success");
|
||||
badge.set_valign(gtk::Align::Center);
|
||||
inner.append(&badge);
|
||||
}
|
||||
|
||||
row.append(&inner);
|
||||
|
||||
let child = gtk::FlowBoxChild::builder()
|
||||
.child(&row)
|
||||
.build();
|
||||
child.add_css_class("activatable");
|
||||
widgets::set_pointer_cursor(&child);
|
||||
child
|
||||
}
|
||||
|
||||
/// Build a featured banner card for the carousel.
|
||||
/// Layout: screenshot preview on top, then icon + name + description + badge below.
|
||||
/// Width is set dynamically by the carousel layout.
|
||||
@@ -241,7 +360,7 @@ pub fn build_featured_tile(app: &CatalogApp) -> gtk::Box {
|
||||
text_box.append(&desc_label);
|
||||
}
|
||||
|
||||
// Badge row: category + stars
|
||||
// Badge row: category + downloads/stars
|
||||
let badge_row = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.spacing(6)
|
||||
@@ -262,7 +381,15 @@ pub fn build_featured_tile(app: &CatalogApp) -> gtk::Box {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(stars) = app.github_stars.filter(|&s| s > 0) {
|
||||
if let Some(downloads) = app.ocs_downloads.filter(|&d| d > 0) {
|
||||
let dl_badge = widgets::status_badge_with_icon(
|
||||
"folder-download-symbolic",
|
||||
&widgets::format_count(downloads),
|
||||
"neutral",
|
||||
);
|
||||
dl_badge.set_halign(gtk::Align::Start);
|
||||
badge_row.append(&dl_badge);
|
||||
} else if let Some(stars) = app.github_stars.filter(|&s| s > 0) {
|
||||
let star_badge = widgets::status_badge_with_icon(
|
||||
"starred-symbolic",
|
||||
&widgets::format_count(stars),
|
||||
|
||||
Reference in New Issue
Block a user