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:
lashman
2026-03-01 01:01:43 +02:00
parent 79519c500a
commit abb69dc753
9 changed files with 901 additions and 215 deletions

View File

@@ -265,6 +265,72 @@ pub fn enrich_app_release_info(
Ok(remaining)
}
/// A GitHub release with body text for changelog display.
#[derive(Debug, serde::Deserialize)]
struct GitHubRelease {
tag_name: String,
published_at: Option<String>,
body: Option<String>,
}
/// Fetch up to 10 recent releases for a repo.
fn fetch_recent_releases(owner: &str, repo: &str, token: &str) -> Result<(Vec<GitHubRelease>, u32), String> {
let url = format!("https://api.github.com/repos/{}/{}/releases?per_page=10", owner, repo);
let (body, remaining) = github_get(&url, token)?;
let releases: Vec<GitHubRelease> = serde_json::from_str(&body)
.map_err(|e| format!("Parse error: {}", e))?;
Ok((releases, remaining))
}
/// Enrich a catalog app with release history (version, date, description for last 10 releases).
/// Only populates if the existing release_history is empty.
pub fn enrich_app_release_history(
db: &Database,
app_id: i64,
owner: &str,
repo: &str,
token: &str,
) -> Result<u32, String> {
// Check if release_history is already populated (from AppStream or prior enrichment)
if let Ok(Some(app)) = db.get_catalog_app(app_id) {
if app.release_history.as_ref().is_some_and(|h| !h.is_empty()) {
return Ok(u32::MAX); // already has data, skip
}
}
let (releases, remaining) = fetch_recent_releases(owner, repo, token)?;
if releases.is_empty() {
return Ok(remaining);
}
// Convert to the same JSON format used by AppStream: [{version, date, description}]
let history: Vec<serde_json::Value> = releases.iter().map(|r| {
let version = r.tag_name.strip_prefix('v')
.unwrap_or(&r.tag_name)
.to_string();
let date = r.published_at.as_deref()
.and_then(|d| d.split('T').next())
.unwrap_or("");
let mut obj = serde_json::json!({
"version": version,
"date": date,
});
if let Some(ref body) = r.body {
if !body.is_empty() {
obj["description"] = serde_json::Value::String(body.clone());
}
}
obj
}).collect();
let json = serde_json::to_string(&history)
.map_err(|e| format!("JSON error: {}", e))?;
db.update_catalog_app_release_history(app_id, &json)
.map_err(|e| format!("DB error: {}", e))?;
Ok(remaining)
}
/// Fetch and store the README for a catalog app.
pub fn enrich_app_readme(
db: &Database,
@@ -310,6 +376,9 @@ pub fn background_enrich_batch(
Ok(remaining) => {
enriched += 1;
// Also fetch release history (changelog data)
let _ = enrich_app_release_history(db, app.id, owner, repo, token);
// Report progress
if let Ok((done, total)) = db.catalog_enrichment_progress() {
on_progress(done, total);