From 2704ebb3165324f87c6142bb0036e9df1da7c07d Mon Sep 17 00:00:00 2001 From: lashman Date: Sat, 28 Feb 2026 00:17:55 +0200 Subject: [PATCH] Add similar app recommendations from shared categories --- src/core/database.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ src/ui/detail_view.rs | 32 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/src/core/database.rs b/src/core/database.rs index 73d2e17..79f7ce3 100644 --- a/src/core/database.rs +++ b/src/core/database.rs @@ -1869,6 +1869,51 @@ impl Database { ) } + // --- Similar apps --- + + /// Find AppImages from the user's library that share categories with the given app. + pub fn find_similar_apps( + &self, + categories: &str, + exclude_id: i64, + limit: i32, + ) -> SqlResult)>> { + // Split categories and match any overlap + let cats: Vec<&str> = categories.split(';').filter(|s| !s.is_empty()).collect(); + if cats.is_empty() { + return Ok(Vec::new()); + } + + // Build LIKE conditions for each category + let conditions: Vec = cats.iter() + .map(|c| format!("categories LIKE '%{}%'", c.replace('\'', "''"))) + .collect(); + let where_clause = conditions.join(" OR "); + + let sql = format!( + "SELECT id, COALESCE(app_name, filename) AS name, icon_path + FROM appimages + WHERE id != ?1 AND ({}) + LIMIT ?2", + where_clause + ); + + let mut stmt = self.conn.prepare(&sql)?; + let rows = stmt.query_map(params![exclude_id, limit], |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>(2)?, + )) + })?; + + let mut results = Vec::new(); + for row in rows { + results.push(row?); + } + Ok(results) + } + // --- System modification tracking --- pub fn register_modification( diff --git a/src/ui/detail_view.rs b/src/ui/detail_view.rs index 05b7ab2..99bc3b1 100644 --- a/src/ui/detail_view.rs +++ b/src/ui/detail_view.rs @@ -868,6 +868,38 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc) -> gtk::Box { } inner.append(&info_group); + // "You might also like" - similar apps from the user's library + if let Some(ref cats) = record.categories { + if let Ok(similar) = db.find_similar_apps(cats, record.id, 4) { + if !similar.is_empty() { + let similar_group = adw::PreferencesGroup::builder() + .title("You might also like") + .build(); + + for (id, name, icon_path) in &similar { + let row = adw::ActionRow::builder() + .title(name.as_str()) + .activatable(true) + .build(); + + let icon = widgets::app_icon( + icon_path.as_deref(), + name, + 32, + ); + row.add_prefix(&icon); + + // Store the record ID in the widget name for navigation + row.set_widget_name(&format!("similar-{}", id)); + + similar_group.add(&row); + } + + inner.append(&similar_group); + } + } + } + clamp.set_child(Some(&inner)); tab.append(&clamp); tab