Add tags, export/import, and changelog features
- Tag editor in detail view with add/remove pill chips - Tag filter chips in library view for filtering by tag - Shared backup module for app list export/import (JSON v2) - CLI export/import refactored to use shared module - GUI export/import via file picker dialogs in hamburger menu - GitHub release history enrichment for catalog apps - Changelog preview in updates view with expandable rows - DB migration v19 for catalog release_history column
This commit is contained in:
@@ -262,11 +262,6 @@ fn populate_update_list(state: &Rc<UpdatesState>) {
|
||||
for record in &updatable {
|
||||
let name = record.app_name.as_deref().unwrap_or(&record.filename);
|
||||
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(name)
|
||||
.activatable(false)
|
||||
.build();
|
||||
|
||||
// Show version info: current -> latest (with size if available)
|
||||
let current = record.app_version.as_deref().unwrap_or("unknown");
|
||||
let latest = record.latest_version.as_deref().unwrap_or("unknown");
|
||||
@@ -275,12 +270,46 @@ fn populate_update_list(state: &Rc<UpdatesState>) {
|
||||
} else {
|
||||
format!("{} -> {}", current, latest)
|
||||
};
|
||||
row.set_subtitle(&subtitle);
|
||||
|
||||
// Try to find changelog from the app's own release_history or from catalog
|
||||
let changelog = find_changelog_for_version(
|
||||
&state.db, record, latest,
|
||||
);
|
||||
|
||||
// Use ExpanderRow if we have changelog, otherwise plain ActionRow
|
||||
let row: adw::ExpanderRow = adw::ExpanderRow::builder()
|
||||
.title(name)
|
||||
.subtitle(&subtitle)
|
||||
.show_enable_switch(false)
|
||||
.expanded(false)
|
||||
.build();
|
||||
|
||||
// App icon
|
||||
let icon = widgets::app_icon(record.icon_path.as_deref(), name, 32);
|
||||
row.add_prefix(&icon);
|
||||
|
||||
// "What's new" content inside the expander
|
||||
let changelog_text = match &changelog {
|
||||
Some(text) => text.clone(),
|
||||
None => i18n("Release notes not available"),
|
||||
};
|
||||
let changelog_label = gtk::Label::builder()
|
||||
.label(&changelog_text)
|
||||
.wrap(true)
|
||||
.xalign(0.0)
|
||||
.css_classes(if changelog.is_some() { vec!["body"] } else { vec!["dim-label", "caption"] })
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.margin_top(8)
|
||||
.margin_bottom(8)
|
||||
.selectable(true)
|
||||
.build();
|
||||
let changelog_row = adw::ActionRow::builder()
|
||||
.activatable(false)
|
||||
.child(&changelog_label)
|
||||
.build();
|
||||
row.add_row(&changelog_row);
|
||||
|
||||
// Individual update button
|
||||
let update_btn = gtk::Button::builder()
|
||||
.icon_name("software-update-available-symbolic")
|
||||
@@ -341,3 +370,81 @@ fn populate_update_list(state: &Rc<UpdatesState>) {
|
||||
state.list_box.append(&row);
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to find changelog/release notes for a specific version.
|
||||
/// Checks the installed app's release_history first, then falls back
|
||||
/// to the catalog app's release_history (populated by GitHub enrichment).
|
||||
fn find_changelog_for_version(
|
||||
db: &Database,
|
||||
record: &crate::core::database::AppImageRecord,
|
||||
target_version: &str,
|
||||
) -> Option<String> {
|
||||
// First check the installed app's own release_history
|
||||
if let Some(ref history_json) = record.release_history {
|
||||
if let Some(text) = extract_version_notes(history_json, target_version) {
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to catalog app's release_history
|
||||
let app_name = record.app_name.as_deref().unwrap_or(&record.filename);
|
||||
if let Ok(Some(ref history_json)) = db.get_catalog_release_history_by_name(app_name) {
|
||||
if let Some(text) = extract_version_notes(history_json, target_version) {
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse release_history JSON and extract the description for a given version.
|
||||
/// Format: [{"version": "1.0.0", "date": "2026-01-01", "description": "..."}]
|
||||
fn extract_version_notes(history_json: &str, target_version: &str) -> Option<String> {
|
||||
let releases: Vec<serde_json::Value> = serde_json::from_str(history_json).ok()?;
|
||||
|
||||
// Try exact match first
|
||||
for release in &releases {
|
||||
if let Some(ver) = release.get("version").and_then(|v| v.as_str()) {
|
||||
if ver == target_version {
|
||||
return release.get("description")
|
||||
.and_then(|d| d.as_str())
|
||||
.map(|s| truncate_changelog(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match, try matching without "v" prefix on both sides
|
||||
let clean_target = target_version.strip_prefix('v').unwrap_or(target_version);
|
||||
for release in &releases {
|
||||
if let Some(ver) = release.get("version").and_then(|v| v.as_str()) {
|
||||
let clean_ver = ver.strip_prefix('v').unwrap_or(ver);
|
||||
if clean_ver == clean_target {
|
||||
return release.get("description")
|
||||
.and_then(|d| d.as_str())
|
||||
.map(|s| truncate_changelog(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matching version found - show the latest entry's notes as a fallback
|
||||
// (the first entry is typically the newest release)
|
||||
if let Some(first) = releases.first() {
|
||||
if let Some(desc) = first.get("description").and_then(|d| d.as_str()) {
|
||||
let ver = first.get("version").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
return Some(format!("(v{}) {}", ver, truncate_changelog(desc)));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Truncate long changelog text to keep the UI compact.
|
||||
fn truncate_changelog(text: &str) -> String {
|
||||
let max_len = 500;
|
||||
let trimmed = text.trim();
|
||||
if trimmed.len() <= max_len {
|
||||
trimmed.to_string()
|
||||
} else {
|
||||
format!("{}...", &trimmed[..max_len])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user