diff --git a/Cargo.lock b/Cargo.lock
index 9bc4082..eb93b2f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -559,6 +559,7 @@ dependencies = [
"log",
"notify",
"notify-rust",
+ "pulldown-cmark",
"quick-xml",
"rusqlite",
"serde",
@@ -876,6 +877,15 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "getopts"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+dependencies = [
+ "unicode-width",
+]
+
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -1804,6 +1814,25 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "pulldown-cmark"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
+dependencies = [
+ "bitflags 2.11.0",
+ "getopts",
+ "memchr",
+ "pulldown-cmark-escape",
+ "unicase",
+]
+
+[[package]]
+name = "pulldown-cmark-escape"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
+
[[package]]
name = "quick-xml"
version = "0.37.5"
@@ -2335,12 +2364,24 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
[[package]]
name = "unicode-xid"
version = "0.2.6"
diff --git a/Cargo.toml b/Cargo.toml
index 9f61955..537fee5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,5 +51,8 @@ notify-rust = "4"
# File system watching (inotify)
notify = "7"
+# Markdown parsing (for GitHub README rendering)
+pulldown-cmark = "0.12"
+
[build-dependencies]
glib-build-tools = "0.22"
diff --git a/data/resources/style.css b/data/resources/style.css
index 45f64c5..bace028 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -175,6 +175,58 @@ button.flat:not(.pill):not(.suggested-action):not(.destructive-action) {
min-height: 24px;
}
+/* ===== Category Filter Tiles ===== */
+.category-tile {
+ padding: 14px 18px;
+ min-height: 48px;
+ border-radius: 12px;
+ border: none;
+ font-weight: 600;
+ font-size: 0.9em;
+ color: white;
+}
+
+.category-tile image {
+ color: white;
+ opacity: 0.9;
+}
+
+/* Colored backgrounds per category */
+.cat-accent { background: alpha(@accent_bg_color, 0.7); }
+.cat-purple { background: alpha(@purple_3, 0.65); }
+.cat-red { background: alpha(@red_3, 0.6); }
+.cat-green { background: alpha(@success_bg_color, 0.55); }
+.cat-orange { background: alpha(@orange_3, 0.65); }
+.cat-blue { background: alpha(@blue_3, 0.6); }
+.cat-amber { background: alpha(@warning_bg_color, 0.6); }
+.cat-neutral { background: alpha(@window_fg_color, 0.2); }
+
+/* Hover: intensify the background */
+.cat-accent:hover { background: alpha(@accent_bg_color, 0.85); }
+.cat-purple:hover { background: alpha(@purple_3, 0.8); }
+.cat-red:hover { background: alpha(@red_3, 0.75); }
+.cat-green:hover { background: alpha(@success_bg_color, 0.7); }
+.cat-orange:hover { background: alpha(@orange_3, 0.8); }
+.cat-blue:hover { background: alpha(@blue_3, 0.75); }
+.cat-amber:hover { background: alpha(@warning_bg_color, 0.75); }
+.cat-neutral:hover { background: alpha(@window_fg_color, 0.3); }
+
+/* Checked: full-strength background + light border for emphasis */
+.cat-accent:checked { background: @accent_bg_color; }
+.cat-purple:checked { background: @purple_3; }
+.cat-red:checked { background: @red_3; }
+.cat-green:checked { background: @success_bg_color; }
+.cat-orange:checked { background: @orange_3; }
+.cat-blue:checked { background: @blue_3; }
+.cat-amber:checked { background: @warning_bg_color; }
+.cat-neutral:checked { background: alpha(@window_fg_color, 0.45); }
+
+/* Focus indicator on the tile itself */
+flowboxchild:focus-visible .category-tile {
+ outline: 2px solid @accent_bg_color;
+ outline-offset: 2px;
+}
+
/* ===== Catalog Tile Cards ===== */
.catalog-tile {
border: 1px solid alpha(@window_fg_color, 0.12);
diff --git a/src/core/catalog.rs b/src/core/catalog.rs
index 822f95a..588ec0b 100644
--- a/src/core/catalog.rs
+++ b/src/core/catalog.rs
@@ -20,6 +20,7 @@ pub struct CatalogSource {
#[derive(Debug, Clone, PartialEq)]
pub enum CatalogType {
AppImageHub,
+ OcsAppImageHub,
GitHubSearch,
Custom,
}
@@ -28,6 +29,7 @@ impl CatalogType {
pub fn as_str(&self) -> &str {
match self {
Self::AppImageHub => "appimage-hub",
+ Self::OcsAppImageHub => "ocs-appimagehub",
Self::GitHubSearch => "github-search",
Self::Custom => "custom",
}
@@ -36,6 +38,7 @@ impl CatalogType {
pub fn from_str(s: &str) -> Self {
match s {
"appimage-hub" => Self::AppImageHub,
+ "ocs-appimagehub" => Self::OcsAppImageHub,
"github-search" => Self::GitHubSearch,
_ => Self::Custom,
}
@@ -63,10 +66,138 @@ pub struct CatalogApp {
/// Default AppImageHub registry URL.
const APPIMAGEHUB_API_URL: &str = "https://appimage.github.io/feed.json";
+/// OCS API base URL for AppImageHub.com.
+const OCS_API_URL: &str = "https://api.appimagehub.com/ocs/v1/content/data";
+const OCS_PAGE_SIZE: u32 = 100;
+
+// --- OCS API response types ---
+
+/// Deserialize a JSON value that may be a number, a numeric string, or an empty string.
+/// The OCS API is loosely typed and sometimes returns "" instead of null for numeric fields.
+fn deserialize_lenient_i64<'de, D>(deserializer: D) -> Result