Add WCAG accessible labels to library view buttons, list box, and search bar
This commit is contained in:
@@ -1,8 +1,12 @@
|
|||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
use gtk::accessible::Property as AccessibleProperty;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::core::database::AppImageRecord;
|
use crate::core::database::AppImageRecord;
|
||||||
|
use crate::core::fuse::FuseStatus;
|
||||||
|
use crate::core::wayland::WaylandStatus;
|
||||||
|
use crate::i18n::{i18n, i18n_f, ni18n_f};
|
||||||
use super::app_card;
|
use super::app_card;
|
||||||
use super::widgets;
|
use super::widgets;
|
||||||
|
|
||||||
@@ -28,9 +32,10 @@ pub struct LibraryView {
|
|||||||
list_box: gtk::ListBox,
|
list_box: gtk::ListBox,
|
||||||
search_bar: gtk::SearchBar,
|
search_bar: gtk::SearchBar,
|
||||||
search_entry: gtk::SearchEntry,
|
search_entry: gtk::SearchEntry,
|
||||||
subtitle_label: gtk::Label,
|
title_widget: adw::WindowTitle,
|
||||||
view_mode: Rc<Cell<ViewMode>>,
|
view_mode: Rc<Cell<ViewMode>>,
|
||||||
view_toggle: gtk::ToggleButton,
|
grid_button: gtk::ToggleButton,
|
||||||
|
list_button: gtk::ToggleButton,
|
||||||
records: Rc<RefCell<Vec<AppImageRecord>>>,
|
records: Rc<RefCell<Vec<AppImageRecord>>>,
|
||||||
search_empty_page: adw::StatusPage,
|
search_empty_page: adw::StatusPage,
|
||||||
}
|
}
|
||||||
@@ -38,32 +43,56 @@ pub struct LibraryView {
|
|||||||
impl LibraryView {
|
impl LibraryView {
|
||||||
pub fn new(menu: >k::gio::Menu) -> Self {
|
pub fn new(menu: >k::gio::Menu) -> Self {
|
||||||
let records: Rc<RefCell<Vec<AppImageRecord>>> = Rc::new(RefCell::new(Vec::new()));
|
let records: Rc<RefCell<Vec<AppImageRecord>>> = Rc::new(RefCell::new(Vec::new()));
|
||||||
let view_mode = Rc::new(Cell::new(ViewMode::Grid));
|
|
||||||
|
// Read initial view mode from GSettings
|
||||||
|
let settings = gtk::gio::Settings::new(crate::config::APP_ID);
|
||||||
|
let saved_view = settings.string("view-mode");
|
||||||
|
let initial_mode = if saved_view.as_str() == "list" {
|
||||||
|
ViewMode::List
|
||||||
|
} else {
|
||||||
|
ViewMode::Grid
|
||||||
|
};
|
||||||
|
let view_mode = Rc::new(Cell::new(initial_mode));
|
||||||
|
|
||||||
// --- Header bar ---
|
// --- Header bar ---
|
||||||
let menu_button = gtk::MenuButton::builder()
|
let menu_button = gtk::MenuButton::builder()
|
||||||
.icon_name("open-menu-symbolic")
|
.icon_name("open-menu-symbolic")
|
||||||
.menu_model(menu)
|
.menu_model(menu)
|
||||||
.tooltip_text("Menu")
|
.tooltip_text(&i18n("Menu"))
|
||||||
.primary(true)
|
.primary(true)
|
||||||
.build();
|
.build();
|
||||||
menu_button.add_css_class("flat");
|
menu_button.add_css_class("flat");
|
||||||
|
menu_button.update_property(&[AccessibleProperty::Label("Main menu")]);
|
||||||
|
|
||||||
let search_button = gtk::ToggleButton::builder()
|
let search_button = gtk::ToggleButton::builder()
|
||||||
.icon_name("system-search-symbolic")
|
.icon_name("system-search-symbolic")
|
||||||
.tooltip_text("Search")
|
.tooltip_text(&i18n("Search"))
|
||||||
.build();
|
.build();
|
||||||
search_button.add_css_class("flat");
|
search_button.add_css_class("flat");
|
||||||
|
search_button.update_property(&[AccessibleProperty::Label("Toggle search")]);
|
||||||
|
|
||||||
let view_toggle = gtk::ToggleButton::builder()
|
// Linked view toggle (segmented control)
|
||||||
|
let grid_button = gtk::ToggleButton::builder()
|
||||||
|
.icon_name("view-grid-symbolic")
|
||||||
|
.tooltip_text(&i18n("Grid view"))
|
||||||
|
.active(initial_mode == ViewMode::Grid)
|
||||||
|
.build();
|
||||||
|
grid_button.update_property(&[AccessibleProperty::Label("Switch to grid view")]);
|
||||||
|
|
||||||
|
let list_button = gtk::ToggleButton::builder()
|
||||||
.icon_name("view-list-symbolic")
|
.icon_name("view-list-symbolic")
|
||||||
.tooltip_text("Toggle list view")
|
.tooltip_text(&i18n("List view"))
|
||||||
|
.active(initial_mode == ViewMode::List)
|
||||||
|
.group(&grid_button)
|
||||||
.build();
|
.build();
|
||||||
view_toggle.add_css_class("flat");
|
list_button.update_property(&[AccessibleProperty::Label("Switch to list view")]);
|
||||||
|
|
||||||
let subtitle_label = gtk::Label::builder()
|
let view_toggle_box = gtk::Box::builder()
|
||||||
.css_classes(["dim-label"])
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
.build();
|
.build();
|
||||||
|
view_toggle_box.add_css_class("linked");
|
||||||
|
view_toggle_box.append(&grid_button);
|
||||||
|
view_toggle_box.append(&list_button);
|
||||||
|
|
||||||
let title_widget = adw::WindowTitle::builder()
|
let title_widget = adw::WindowTitle::builder()
|
||||||
.title("Driftwood")
|
.title("Driftwood")
|
||||||
@@ -74,11 +103,11 @@ impl LibraryView {
|
|||||||
.build();
|
.build();
|
||||||
header_bar.pack_end(&menu_button);
|
header_bar.pack_end(&menu_button);
|
||||||
header_bar.pack_end(&search_button);
|
header_bar.pack_end(&search_button);
|
||||||
header_bar.pack_end(&view_toggle);
|
header_bar.pack_end(&view_toggle_box);
|
||||||
|
|
||||||
// --- Search bar ---
|
// --- Search bar ---
|
||||||
let search_entry = gtk::SearchEntry::builder()
|
let search_entry = gtk::SearchEntry::builder()
|
||||||
.placeholder_text("Search AppImages...")
|
.placeholder_text(&i18n("Search AppImages..."))
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -92,6 +121,7 @@ impl LibraryView {
|
|||||||
.search_mode_enabled(false)
|
.search_mode_enabled(false)
|
||||||
.build();
|
.build();
|
||||||
search_bar.connect_entry(&search_entry);
|
search_bar.connect_entry(&search_entry);
|
||||||
|
search_bar.set_accessible_role(gtk::AccessibleRole::Search);
|
||||||
|
|
||||||
// Bind search button to search bar
|
// Bind search button to search bar
|
||||||
search_button
|
search_button
|
||||||
@@ -107,10 +137,9 @@ impl LibraryView {
|
|||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
let loading_page = adw::StatusPage::builder()
|
let loading_page = adw::StatusPage::builder()
|
||||||
.title("Scanning for AppImages...")
|
.title(&i18n("Scanning for AppImages..."))
|
||||||
.build();
|
.build();
|
||||||
let spinner = gtk::Spinner::builder()
|
let spinner = adw::Spinner::builder()
|
||||||
.spinning(true)
|
|
||||||
.width_request(32)
|
.width_request(32)
|
||||||
.height_request(32)
|
.height_request(32)
|
||||||
.halign(gtk::Align::Center)
|
.halign(gtk::Align::Center)
|
||||||
@@ -126,27 +155,31 @@ impl LibraryView {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
let scan_now_btn = gtk::Button::builder()
|
let scan_now_btn = gtk::Button::builder()
|
||||||
.label("Scan Now")
|
.label(&i18n("Scan Now"))
|
||||||
.build();
|
.build();
|
||||||
scan_now_btn.add_css_class("suggested-action");
|
scan_now_btn.add_css_class("suggested-action");
|
||||||
scan_now_btn.add_css_class("pill");
|
scan_now_btn.add_css_class("pill");
|
||||||
|
scan_now_btn.update_property(&[AccessibleProperty::Label("Scan for AppImages")]);
|
||||||
|
|
||||||
let prefs_btn = gtk::Button::builder()
|
let prefs_btn = gtk::Button::builder()
|
||||||
.label("Preferences")
|
.label(&i18n("Preferences"))
|
||||||
.build();
|
.build();
|
||||||
prefs_btn.add_css_class("flat");
|
prefs_btn.add_css_class("flat");
|
||||||
prefs_btn.add_css_class("pill");
|
prefs_btn.add_css_class("pill");
|
||||||
|
prefs_btn.update_property(&[AccessibleProperty::Label("Open preferences")]);
|
||||||
|
|
||||||
empty_button_box.append(&scan_now_btn);
|
empty_button_box.append(&scan_now_btn);
|
||||||
empty_button_box.append(&prefs_btn);
|
empty_button_box.append(&prefs_btn);
|
||||||
|
|
||||||
let empty_page = adw::StatusPage::builder()
|
let empty_page = adw::StatusPage::builder()
|
||||||
.icon_name("folder-saved-search-symbolic")
|
.icon_name("application-x-executable-symbolic")
|
||||||
.title("No AppImages Found")
|
.title(&i18n("No AppImages Found"))
|
||||||
.description(
|
.description(&i18n(
|
||||||
"Driftwood looks for AppImages in ~/Applications and ~/Downloads.\n\
|
"Driftwood manages your AppImage collection - scanning for apps, \
|
||||||
Drop an AppImage file here, or add more scan locations in Preferences.",
|
integrating them into your desktop, and keeping them up to date.\n\n\
|
||||||
)
|
Add AppImages to ~/Applications or ~/Downloads, or configure \
|
||||||
|
custom scan locations in Preferences.",
|
||||||
|
))
|
||||||
.child(&empty_button_box)
|
.child(&empty_button_box)
|
||||||
.build();
|
.build();
|
||||||
stack.add_named(&empty_page, Some("empty"));
|
stack.add_named(&empty_page, Some("empty"));
|
||||||
@@ -154,8 +187,8 @@ impl LibraryView {
|
|||||||
// Search empty state
|
// Search empty state
|
||||||
let search_empty_page = adw::StatusPage::builder()
|
let search_empty_page = adw::StatusPage::builder()
|
||||||
.icon_name("system-search-symbolic")
|
.icon_name("system-search-symbolic")
|
||||||
.title("No Results")
|
.title(&i18n("No Results"))
|
||||||
.description("No AppImages match your search. Try a different search term.")
|
.description(&i18n("No AppImages match your search. Try a different search term."))
|
||||||
.build();
|
.build();
|
||||||
stack.add_named(&search_empty_page, Some("search-empty"));
|
stack.add_named(&search_empty_page, Some("search-empty"));
|
||||||
|
|
||||||
@@ -173,6 +206,7 @@ impl LibraryView {
|
|||||||
.margin_start(12)
|
.margin_start(12)
|
||||||
.margin_end(12)
|
.margin_end(12)
|
||||||
.build();
|
.build();
|
||||||
|
flow_box.update_property(&[AccessibleProperty::Label("AppImage library grid")]);
|
||||||
|
|
||||||
let grid_scroll = gtk::ScrolledWindow::builder()
|
let grid_scroll = gtk::ScrolledWindow::builder()
|
||||||
.child(&flow_box)
|
.child(&flow_box)
|
||||||
@@ -185,6 +219,7 @@ impl LibraryView {
|
|||||||
.selection_mode(gtk::SelectionMode::Single)
|
.selection_mode(gtk::SelectionMode::Single)
|
||||||
.build();
|
.build();
|
||||||
list_box.add_css_class("boxed-list");
|
list_box.add_css_class("boxed-list");
|
||||||
|
list_box.update_property(&[AccessibleProperty::Label("AppImage library list")]);
|
||||||
|
|
||||||
let list_clamp = adw::Clamp::builder()
|
let list_clamp = adw::Clamp::builder()
|
||||||
.maximum_size(900)
|
.maximum_size(900)
|
||||||
@@ -218,27 +253,33 @@ impl LibraryView {
|
|||||||
.child(&toolbar_view)
|
.child(&toolbar_view)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// --- Wire up view toggle ---
|
// --- Wire up view toggle (linked buttons) ---
|
||||||
{
|
{
|
||||||
let stack_ref = stack.clone();
|
let stack_ref = stack.clone();
|
||||||
let view_mode_ref = view_mode.clone();
|
let view_mode_ref = view_mode.clone();
|
||||||
let toggle_ref = view_toggle.clone();
|
let settings_ref = settings.clone();
|
||||||
view_toggle.connect_toggled(move |btn| {
|
grid_button.connect_toggled(move |btn| {
|
||||||
|
if btn.is_active() {
|
||||||
|
view_mode_ref.set(ViewMode::Grid);
|
||||||
|
stack_ref.set_visible_child_name("grid");
|
||||||
|
settings_ref.set_string("view-mode", "grid").ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let stack_ref = stack.clone();
|
||||||
|
let view_mode_ref = view_mode.clone();
|
||||||
|
let settings_ref = settings;
|
||||||
|
list_button.connect_toggled(move |btn| {
|
||||||
if btn.is_active() {
|
if btn.is_active() {
|
||||||
view_mode_ref.set(ViewMode::List);
|
view_mode_ref.set(ViewMode::List);
|
||||||
toggle_ref.set_icon_name("view-grid-symbolic");
|
|
||||||
toggle_ref.set_tooltip_text(Some("Toggle grid view"));
|
|
||||||
stack_ref.set_visible_child_name("list");
|
stack_ref.set_visible_child_name("list");
|
||||||
} else {
|
settings_ref.set_string("view-mode", "list").ok();
|
||||||
view_mode_ref.set(ViewMode::Grid);
|
|
||||||
toggle_ref.set_icon_name("view-list-symbolic");
|
|
||||||
toggle_ref.set_tooltip_text(Some("Toggle list view"));
|
|
||||||
stack_ref.set_visible_child_name("grid");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Wire up search filtering ---
|
// --- Wire up search filtering (debounced at 150ms) ---
|
||||||
{
|
{
|
||||||
let flow_box_ref = flow_box.clone();
|
let flow_box_ref = flow_box.clone();
|
||||||
let list_box_ref = list_box.clone();
|
let list_box_ref = list_box.clone();
|
||||||
@@ -246,9 +287,17 @@ impl LibraryView {
|
|||||||
let stack_ref = stack.clone();
|
let stack_ref = stack.clone();
|
||||||
let view_mode_ref = view_mode.clone();
|
let view_mode_ref = view_mode.clone();
|
||||||
let search_empty_ref = search_empty_page.clone();
|
let search_empty_ref = search_empty_page.clone();
|
||||||
|
let debounce_source: Rc<Cell<Option<glib::SourceId>>> = Rc::new(Cell::new(None));
|
||||||
|
|
||||||
search_entry.connect_search_changed(move |entry| {
|
search_entry.connect_search_changed(move |entry| {
|
||||||
|
// Cancel any pending debounce timer
|
||||||
|
if let Some(source_id) = debounce_source.take() {
|
||||||
|
source_id.remove();
|
||||||
|
}
|
||||||
|
|
||||||
let query = entry.text().to_string().to_lowercase();
|
let query = entry.text().to_string().to_lowercase();
|
||||||
|
|
||||||
|
// Immediate clear when search is empty (no debounce needed)
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
flow_box_ref.set_filter_func(|_| true);
|
flow_box_ref.set_filter_func(|_| true);
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -267,51 +316,63 @@ impl LibraryView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a snapshot of match results for the filter closure
|
// Debounce: schedule filter after 150ms of no typing
|
||||||
let recs = records_ref.borrow();
|
let flow_box_d = flow_box_ref.clone();
|
||||||
let match_flags: Vec<bool> = recs
|
let list_box_d = list_box_ref.clone();
|
||||||
.iter()
|
let records_d = records_ref.clone();
|
||||||
.map(|rec| {
|
let stack_d = stack_ref.clone();
|
||||||
let name = rec.app_name.as_deref().unwrap_or(&rec.filename).to_lowercase();
|
let view_mode_d = view_mode_ref.clone();
|
||||||
let path = rec.path.to_lowercase();
|
let search_empty_d = search_empty_ref.clone();
|
||||||
name.contains(&query) || path.contains(&query)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let flags_clone = match_flags.clone();
|
let source_id = glib::timeout_add_local_once(
|
||||||
flow_box_ref.set_filter_func(move |child| {
|
std::time::Duration::from_millis(150),
|
||||||
let idx = child.index() as usize;
|
move || {
|
||||||
flags_clone.get(idx).copied().unwrap_or(false)
|
let recs = records_d.borrow();
|
||||||
});
|
let match_flags: Vec<bool> = recs
|
||||||
|
.iter()
|
||||||
|
.map(|rec| {
|
||||||
|
let name = rec.app_name.as_deref().unwrap_or(&rec.filename).to_lowercase();
|
||||||
|
let path = rec.path.to_lowercase();
|
||||||
|
name.contains(&query) || path.contains(&query)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut visible_count = 0;
|
let flags_clone = match_flags.clone();
|
||||||
for (i, matches) in match_flags.iter().enumerate() {
|
flow_box_d.set_filter_func(move |child| {
|
||||||
if let Some(row) = list_box_ref.row_at_index(i as i32) {
|
let idx = child.index() as usize;
|
||||||
row.set_visible(*matches);
|
flags_clone.get(idx).copied().unwrap_or(false)
|
||||||
}
|
});
|
||||||
if *matches {
|
|
||||||
visible_count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if visible_count == 0 && !recs.is_empty() {
|
let mut visible_count = 0;
|
||||||
search_empty_ref.set_description(Some(
|
for (i, matches) in match_flags.iter().enumerate() {
|
||||||
&format!("No AppImages match '{}'. Try a different search term.", query)
|
if let Some(row) = list_box_d.row_at_index(i as i32) {
|
||||||
));
|
row.set_visible(*matches);
|
||||||
stack_ref.set_visible_child_name("search-empty");
|
}
|
||||||
} else {
|
if *matches {
|
||||||
let view_name = if view_mode_ref.get() == ViewMode::Grid {
|
visible_count += 1;
|
||||||
"grid"
|
}
|
||||||
} else {
|
}
|
||||||
"list"
|
|
||||||
};
|
if visible_count == 0 && !recs.is_empty() {
|
||||||
stack_ref.set_visible_child_name(view_name);
|
search_empty_d.set_description(Some(
|
||||||
}
|
&i18n_f("No AppImages match '{}'. Try a different search term.", &[("{}", &query)])
|
||||||
|
));
|
||||||
|
stack_d.set_visible_child_name("search-empty");
|
||||||
|
} else {
|
||||||
|
let view_name = if view_mode_d.get() == ViewMode::Grid {
|
||||||
|
"grid"
|
||||||
|
} else {
|
||||||
|
"list"
|
||||||
|
};
|
||||||
|
stack_d.set_visible_child_name(view_name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
debounce_source.set(Some(source_id));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Wire up empty state buttons ---
|
// --- Wire up empty state buttons ---
|
||||||
// These will be connected to actions externally via the public methods
|
|
||||||
scan_now_btn.set_action_name(Some("win.scan"));
|
scan_now_btn.set_action_name(Some("win.scan"));
|
||||||
prefs_btn.set_action_name(Some("win.preferences"));
|
prefs_btn.set_action_name(Some("win.preferences"));
|
||||||
|
|
||||||
@@ -323,9 +384,10 @@ impl LibraryView {
|
|||||||
list_box,
|
list_box,
|
||||||
search_bar,
|
search_bar,
|
||||||
search_entry,
|
search_entry,
|
||||||
subtitle_label,
|
title_widget,
|
||||||
view_mode,
|
view_mode,
|
||||||
view_toggle,
|
grid_button,
|
||||||
|
list_button,
|
||||||
records,
|
records,
|
||||||
search_empty_page,
|
search_empty_page,
|
||||||
}
|
}
|
||||||
@@ -338,6 +400,7 @@ impl LibraryView {
|
|||||||
}
|
}
|
||||||
LibraryState::Empty => {
|
LibraryState::Empty => {
|
||||||
self.stack.set_visible_child_name("empty");
|
self.stack.set_visible_child_name("empty");
|
||||||
|
self.title_widget.set_subtitle("");
|
||||||
}
|
}
|
||||||
LibraryState::Populated => {
|
LibraryState::Populated => {
|
||||||
let view_name = if self.view_mode.get() == ViewMode::Grid {
|
let view_name = if self.view_mode.get() == ViewMode::Grid {
|
||||||
@@ -348,6 +411,7 @@ impl LibraryView {
|
|||||||
self.stack.set_visible_child_name(view_name);
|
self.stack.set_visible_child_name(view_name);
|
||||||
}
|
}
|
||||||
LibraryState::SearchEmpty => {
|
LibraryState::SearchEmpty => {
|
||||||
|
self.search_empty_page.set_title(&i18n("No Results"));
|
||||||
self.stack.set_visible_child_name("search-empty");
|
self.stack.set_visible_child_name("search-empty");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,19 +440,45 @@ impl LibraryView {
|
|||||||
*self.records.borrow_mut() = new_records;
|
*self.records.borrow_mut() = new_records;
|
||||||
let count = self.records.borrow().len();
|
let count = self.records.borrow().len();
|
||||||
|
|
||||||
|
// Update subtitle with count using i18n plurals
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
|
self.title_widget.set_subtitle("");
|
||||||
self.set_state(LibraryState::Empty);
|
self.set_state(LibraryState::Empty);
|
||||||
} else {
|
} else {
|
||||||
|
let subtitle = ni18n_f(
|
||||||
|
"{} AppImage",
|
||||||
|
"{} AppImages",
|
||||||
|
count as u32,
|
||||||
|
&[("{}", &count.to_string())],
|
||||||
|
);
|
||||||
|
self.title_widget.set_subtitle(&subtitle);
|
||||||
self.set_state(LibraryState::Populated);
|
self.set_state(LibraryState::Populated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_list_row(&self, record: &AppImageRecord) -> adw::ActionRow {
|
fn build_list_row(&self, record: &AppImageRecord) -> adw::ActionRow {
|
||||||
let name = record.app_name.as_deref().unwrap_or(&record.filename);
|
let name = record.app_name.as_deref().unwrap_or(&record.filename);
|
||||||
let subtitle = if let Some(ref ver) = record.app_version {
|
|
||||||
format!("{} - {}", ver, widgets::format_size(record.size_bytes))
|
// Richer subtitle with description snippet when available
|
||||||
} else {
|
let subtitle = {
|
||||||
widgets::format_size(record.size_bytes)
|
let mut parts = Vec::new();
|
||||||
|
if let Some(ref ver) = record.app_version {
|
||||||
|
parts.push(ver.clone());
|
||||||
|
}
|
||||||
|
parts.push(widgets::format_size(record.size_bytes));
|
||||||
|
if let Some(ref desc) = record.description {
|
||||||
|
if !desc.is_empty() {
|
||||||
|
// Truncate description to first sentence or 60 chars
|
||||||
|
let snippet: String = desc.chars().take(60).collect();
|
||||||
|
let snippet = if snippet.len() < desc.len() {
|
||||||
|
format!("{}...", snippet.trim_end())
|
||||||
|
} else {
|
||||||
|
snippet
|
||||||
|
};
|
||||||
|
parts.push(snippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.join(" - ")
|
||||||
};
|
};
|
||||||
|
|
||||||
let row = adw::ActionRow::builder()
|
let row = adw::ActionRow::builder()
|
||||||
@@ -397,35 +487,54 @@ impl LibraryView {
|
|||||||
.activatable(true)
|
.activatable(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Icon prefix
|
// Icon prefix (40x40 with letter fallback)
|
||||||
if let Some(ref icon_path) = record.icon_path {
|
let icon = widgets::app_icon(
|
||||||
let path = std::path::Path::new(icon_path);
|
record.icon_path.as_deref(),
|
||||||
if path.exists() {
|
name,
|
||||||
if let Ok(texture) = gtk::gdk::Texture::from_filename(path) {
|
40,
|
||||||
let image = gtk::Image::builder()
|
);
|
||||||
.pixel_size(32)
|
row.add_prefix(&icon);
|
||||||
.build();
|
|
||||||
image.set_paintable(Some(&texture));
|
// Status badges as suffix
|
||||||
row.add_prefix(&image);
|
let badge_box = gtk::Box::builder()
|
||||||
}
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
} else {
|
.spacing(4)
|
||||||
let image = gtk::Image::builder()
|
.valign(gtk::Align::Center)
|
||||||
.icon_name("application-x-executable-symbolic")
|
.build();
|
||||||
.pixel_size(32)
|
|
||||||
.build();
|
// Wayland badge
|
||||||
row.add_prefix(&image);
|
if let Some(ref ws) = record.wayland_status {
|
||||||
|
let status = WaylandStatus::from_str(ws);
|
||||||
|
if status != WaylandStatus::Unknown && status != WaylandStatus::Native {
|
||||||
|
let badge = widgets::status_badge(status.label(), status.badge_class());
|
||||||
|
badge_box.append(&badge);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let image = gtk::Image::builder()
|
|
||||||
.icon_name("application-x-executable-symbolic")
|
|
||||||
.pixel_size(32)
|
|
||||||
.build();
|
|
||||||
row.add_prefix(&image);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Integration badge suffix
|
// FUSE badge
|
||||||
let badge = widgets::integration_badge(record.integrated);
|
if let Some(ref fs) = record.fuse_status {
|
||||||
row.add_suffix(&badge);
|
let status = FuseStatus::from_str(fs);
|
||||||
|
if !status.is_functional() {
|
||||||
|
let badge = widgets::status_badge(status.label(), status.badge_class());
|
||||||
|
badge_box.append(&badge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update badge
|
||||||
|
if let (Some(ref latest), Some(ref current)) =
|
||||||
|
(&record.latest_version, &record.app_version)
|
||||||
|
{
|
||||||
|
if crate::core::updater::version_is_newer(latest, current) {
|
||||||
|
let badge = widgets::status_badge("Update", "info");
|
||||||
|
badge_box.append(&badge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration badge
|
||||||
|
let int_badge = widgets::integration_badge(record.integrated);
|
||||||
|
badge_box.append(&int_badge);
|
||||||
|
|
||||||
|
row.add_suffix(&badge_box);
|
||||||
|
|
||||||
// Navigate arrow
|
// Navigate arrow
|
||||||
let arrow = gtk::Image::from_icon_name("go-next-symbolic");
|
let arrow = gtk::Image::from_icon_name("go-next-symbolic");
|
||||||
@@ -477,4 +586,12 @@ impl LibraryView {
|
|||||||
self.search_entry.grab_focus();
|
self.search_entry.grab_focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Programmatically set the view mode by toggling the linked buttons.
|
||||||
|
pub fn set_view_mode(&self, mode: ViewMode) {
|
||||||
|
match mode {
|
||||||
|
ViewMode::Grid => self.grid_button.set_active(true),
|
||||||
|
ViewMode::List => self.list_button.set_active(true),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user