Add Phase 5 enhancements: security, i18n, analysis, backup, notifications
- Database v8 migration: tags, pinned, avg_startup_ms columns - Security scanning with CVE matching and batch scan - Bundled library extraction and vulnerability reports - Desktop notification system for security alerts - Backup/restore system for AppImage configurations - i18n framework with gettext support - Runtime analysis and Wayland compatibility detection - AppStream metadata and Flatpak-style build support - File watcher module for live directory monitoring - Preferences panel with GSettings integration - CLI interface for headless operation - Detail view: tabbed layout with ViewSwitcher in title bar, health score, sandbox controls, changelog links - Library view: sort dropdown, context menu enhancements - Dashboard: system status, disk usage, launch history - Security report page with scan and export - Packaging: meson build, PKGBUILD, metainfo
This commit is contained in:
@@ -1,4 +1,59 @@
|
||||
use gtk::prelude::*;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Ensures the shared letter-icon CSS provider is registered on the default
|
||||
/// display exactly once. The provider defines `.letter-icon-a` through
|
||||
/// `.letter-icon-z` (and `.letter-icon-other`) with distinct hue-based
|
||||
/// background/foreground colors so that individual `build_letter_icon` calls
|
||||
/// never need to create their own CssProvider.
|
||||
fn ensure_letter_icon_css() {
|
||||
static REGISTERED: OnceLock<bool> = OnceLock::new();
|
||||
REGISTERED.get_or_init(|| {
|
||||
let provider = gtk::CssProvider::new();
|
||||
provider.load_from_string(&generate_letter_icon_css());
|
||||
if let Some(display) = gtk::gdk::Display::default() {
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
|
||||
);
|
||||
}
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
/// Generate CSS rules for `.letter-icon-a` through `.letter-icon-z` and a
|
||||
/// `.letter-icon-other` fallback. Each letter gets a unique hue evenly
|
||||
/// distributed around the color wheel (saturation 55%, lightness 45% for
|
||||
/// the background, lightness 97% for the foreground text) so that the 26
|
||||
/// letter icons are visually distinct while remaining legible.
|
||||
fn generate_letter_icon_css() -> String {
|
||||
let mut css = String::with_capacity(4096);
|
||||
for i in 0u32..26 {
|
||||
let letter = (b'a' + i as u8) as char;
|
||||
let hue = (i * 360) / 26;
|
||||
// HSL background: moderate saturation, medium lightness
|
||||
// HSL foreground: same hue, very light for contrast
|
||||
css.push_str(&format!(
|
||||
"label.letter-icon-{letter} {{ \
|
||||
background: hsl({hue}, 55%, 45%); \
|
||||
color: hsl({hue}, 100%, 97%); \
|
||||
border-radius: 50%; \
|
||||
font-weight: 700; \
|
||||
}}\n"
|
||||
));
|
||||
}
|
||||
// Fallback for non-alphabetic first characters
|
||||
css.push_str(
|
||||
"label.letter-icon-other { \
|
||||
background: hsl(0, 0%, 50%); \
|
||||
color: hsl(0, 0%, 97%); \
|
||||
border-radius: 50%; \
|
||||
font-weight: 700; \
|
||||
}\n"
|
||||
);
|
||||
css
|
||||
}
|
||||
|
||||
/// Create a status badge pill label with the given text and style class.
|
||||
/// Style classes: "success", "warning", "error", "info", "neutral"
|
||||
@@ -70,61 +125,47 @@ pub fn app_icon(icon_path: Option<&str>, app_name: &str, pixel_size: i32) -> gtk
|
||||
}
|
||||
|
||||
/// Build a colored circle with the first letter of the name as a fallback icon.
|
||||
///
|
||||
/// The color CSS classes (`.letter-icon-a` .. `.letter-icon-z`) are registered
|
||||
/// once via a shared CssProvider. This function only needs to pick the right
|
||||
/// class and set per-widget sizing, avoiding a new provider per icon.
|
||||
fn build_letter_icon(name: &str, size: i32) -> gtk::Widget {
|
||||
let letter = name
|
||||
// Ensure the shared CSS for all 26 letter classes is loaded
|
||||
ensure_letter_icon_css();
|
||||
|
||||
let first_char = name
|
||||
.chars()
|
||||
.find(|c| c.is_alphanumeric())
|
||||
.unwrap_or('?')
|
||||
.to_uppercase()
|
||||
.next()
|
||||
.unwrap_or('?');
|
||||
let letter_upper = first_char.to_uppercase().next().unwrap_or('?');
|
||||
|
||||
// Pick a color based on the name hash for consistency
|
||||
let color_index = name.bytes().fold(0u32, |acc, b| acc.wrapping_add(b as u32)) % 6;
|
||||
let bg_color = match color_index {
|
||||
0 => "@accent_bg_color",
|
||||
1 => "@success_bg_color",
|
||||
2 => "@warning_bg_color",
|
||||
3 => "@error_bg_color",
|
||||
4 => "@accent_bg_color",
|
||||
_ => "@success_bg_color",
|
||||
};
|
||||
let fg_color = match color_index {
|
||||
0 => "@accent_fg_color",
|
||||
1 => "@success_fg_color",
|
||||
2 => "@warning_fg_color",
|
||||
3 => "@error_fg_color",
|
||||
4 => "@accent_fg_color",
|
||||
_ => "@success_fg_color",
|
||||
// Determine the CSS class: letter-icon-a .. letter-icon-z, or letter-icon-other
|
||||
let css_class = if first_char.is_ascii_alphabetic() {
|
||||
format!("letter-icon-{}", first_char.to_ascii_lowercase())
|
||||
} else {
|
||||
"letter-icon-other".to_string()
|
||||
};
|
||||
|
||||
// Use a label styled as a circle with the letter
|
||||
// Font size scales with the icon (40% of the circle diameter).
|
||||
let font_size_pt = size * 4 / 10;
|
||||
|
||||
let label = gtk::Label::builder()
|
||||
.label(&letter.to_string())
|
||||
.use_markup(true)
|
||||
.halign(gtk::Align::Center)
|
||||
.valign(gtk::Align::Center)
|
||||
.width_request(size)
|
||||
.height_request(size)
|
||||
.build();
|
||||
|
||||
// Apply inline CSS via a provider on the display
|
||||
let css_provider = gtk::CssProvider::new();
|
||||
let unique_class = format!("letter-icon-{}", color_index);
|
||||
let css = format!(
|
||||
"label.{} {{ background: {}; color: {}; border-radius: 50%; min-width: {}px; min-height: {}px; font-size: {}px; font-weight: 700; }}",
|
||||
unique_class, bg_color, fg_color, size, size, size * 4 / 10
|
||||
// Use Pango markup to set the font size without a per-widget CssProvider.
|
||||
let markup = format!(
|
||||
"<span size='{}pt'>{}</span>",
|
||||
font_size_pt,
|
||||
glib::markup_escape_text(&letter_upper.to_string()),
|
||||
);
|
||||
css_provider.load_from_string(&css);
|
||||
label.set_markup(&markup);
|
||||
|
||||
if let Some(display) = gtk::gdk::Display::default() {
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&css_provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
|
||||
);
|
||||
}
|
||||
|
||||
label.add_css_class(&unique_class);
|
||||
label.add_css_class(&css_class);
|
||||
|
||||
label.upcast()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user