From f0922e3c03916ec0cdfc4e2fbda053121ece77de Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sun, 29 Mar 2026 17:40:59 -0400 Subject: [PATCH] Implement Linux data models (C/Vala) - Add FeedItem, Feed, FeedSubscription models - Add SearchResult, SearchFilters, SearchQuery models - Add NotificationPreferences, ReadingPreferences models - Implement JSON serialization/deserialization for all models - Add equality comparison methods - Following GNOME HIG naming conventions - Build system configured with Meson/Ninja --- native-route/linux/meson.build | 33 ++ native-route/linux/src/models/feed-item.vala | 313 +++++++++++++ .../linux/src/models/feed-subscription.vala | 259 +++++++++++ native-route/linux/src/models/feed.vala | 282 ++++++++++++ native-route/linux/src/models/namespaces.vala | 5 + .../src/models/notification-preferences.vala | 190 ++++++++ .../linux/src/models/reading-preferences.vala | 168 +++++++ .../linux/src/models/search-filters.vala | 435 ++++++++++++++++++ .../linux/src/models/search-result.vala | 208 +++++++++ 9 files changed, 1893 insertions(+) create mode 100644 native-route/linux/meson.build create mode 100644 native-route/linux/src/models/feed-item.vala create mode 100644 native-route/linux/src/models/feed-subscription.vala create mode 100644 native-route/linux/src/models/feed.vala create mode 100644 native-route/linux/src/models/namespaces.vala create mode 100644 native-route/linux/src/models/notification-preferences.vala create mode 100644 native-route/linux/src/models/reading-preferences.vala create mode 100644 native-route/linux/src/models/search-filters.vala create mode 100644 native-route/linux/src/models/search-result.vala diff --git a/native-route/linux/meson.build b/native-route/linux/meson.build new file mode 100644 index 0000000..76d4852 --- /dev/null +++ b/native-route/linux/meson.build @@ -0,0 +1,33 @@ +project('rssuper-linux', 'vala', 'c', + version: '0.1.0', + default_options: [ + 'c_std=c11', + 'warning_level=3', + 'werror=false', + ] +) + +vala = find_program('valac') +meson_version_check = run_command(vala, '--version', check: true) + +# Dependencies +glib_dep = dependency('glib-2.0', version: '>= 2.58') +gio_dep = dependency('gio-2.0', version: '>= 2.58') +json_dep = dependency('json-glib-1.0', version: '>= 1.4') + +# Source files +models = files( + 'src/models/feed-item.vala', + 'src/models/feed.vala', + 'src/models/feed-subscription.vala', + 'src/models/search-result.vala', + 'src/models/search-filters.vala', + 'src/models/notification-preferences.vala', + 'src/models/reading-preferences.vala', +) + +# Main library +models_lib = library('rssuper-models', models, + dependencies: [glib_dep, gio_dep, json_dep], + install: false +) diff --git a/native-route/linux/src/models/feed-item.vala b/native-route/linux/src/models/feed-item.vala new file mode 100644 index 0000000..0cc2bc8 --- /dev/null +++ b/native-route/linux/src/models/feed-item.vala @@ -0,0 +1,313 @@ +/* + * FeedItem.vala + * + * Represents a single RSS/Atom feed item (article, episode, etc.) + * Following GNOME HIG naming conventions and Vala/GObject patterns. + */ + +/** + * Enclosure metadata for media attachments (podcasts, videos, etc.) + */ +public struct RSSuper.Enclosure { + public string url { get; set; } + public string item_type { get; set; } + public string? length { get; set; } + + public Enclosure(string url, string type, string? length = null) { + this.url = url; + this.item_type = type; + this.length = length; + } +} + +/** + * FeedItem - Represents a single RSS/Atom entry + */ +public class RSSuper.FeedItem : Object { + public string id { get; set; } + public string title { get; set; } + public string? link { get; set; } + public string? description { get; set; } + public string? content { get; set; } + public string? author { get; set; } + public string? published { get; set; } + public string? updated { get; set; } + public string[] categories { get; set; } + public string? enclosure_url { get; set; } + public string? enclosure_type { get; set; } + public string? enclosure_length { get; set; } + public string? guid { get; set; } + public string? subscription_title { get; set; } + + /** + * Default constructor + */ + public FeedItem() { + this.id = ""; + this.title = ""; + this.categories = {}; + } + + /** + * Constructor with initial values + */ + public FeedItem.with_values(string id, string title, string? link = null, + string? description = null, string? content = null, + string? author = null, string? published = null, + string? updated = null, string[]? categories = null, + string? enclosure_url = null, string? enclosure_type = null, + string? enclosure_length = null, string? guid = null, + string? subscription_title = null) { + this.id = id; + this.title = title; + this.link = link; + this.description = description; + this.content = content; + this.author = author; + this.published = published; + this.updated = updated; + this.categories = categories; + this.enclosure_url = enclosure_url; + this.enclosure_type = enclosure_type; + this.enclosure_length = enclosure_length; + this.guid = guid; + this.subscription_title = subscription_title; + } + + /** + * Get enclosure as struct + */ + public Enclosure? get_enclosure() { + if (this.enclosure_url == null) { + return null; + } + return Enclosure(this.enclosure_url, this.enclosure_type ?? "", this.enclosure_length); + } + + /** + * Set enclosure from struct + */ + public void set_enclosure(Enclosure? enclosure) { + if (enclosure == null) { + this.enclosure_url = null; + this.enclosure_type = null; + this.enclosure_length = null; + } else { + this.enclosure_url = enclosure.url; + this.enclosure_type = enclosure_type; + this.enclosure_length = enclosure.length; + } + } + + /** + * Serialize to JSON string + */ + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{"); + sb.append("\"id\":\""); + sb.append(this.id); + sb.append("\",\"title\":\""); + sb.append(this.title); + sb.append("\""); + + if (this.link != null) { + sb.append(",\"link\":\""); + sb.append(this.link); + sb.append("\""); + } + if (this.description != null) { + sb.append(",\"description\":\""); + sb.append(this.description); + sb.append("\""); + } + if (this.content != null) { + sb.append(",\"content\":\""); + sb.append(this.content); + sb.append("\""); + } + if (this.author != null) { + sb.append(",\"author\":\""); + sb.append(this.author); + sb.append("\""); + } + if (this.published != null) { + sb.append(",\"published\":\""); + sb.append(this.published); + sb.append("\""); + } + if (this.updated != null) { + sb.append(",\"updated\":\""); + sb.append(this.updated); + sb.append("\""); + } + if (this.categories.length > 0) { + sb.append(",\"categories\":["); + for (var i = 0; i < this.categories.length; i++) { + if (i > 0) sb.append(","); + sb.append("\""); + sb.append(this.categories[i]); + sb.append("\""); + } + sb.append("]"); + } + if (this.enclosure_url != null) { + sb.append(",\"enclosure\":{\"url\":\""); + sb.append(this.enclosure_url); + sb.append("\""); + if (this.enclosure_type != null) { + sb.append(",\"type\":\""); + sb.append(this.enclosure_type); + sb.append("\""); + } + if (this.enclosure_length != null) { + sb.append(",\"length\":\""); + sb.append(this.enclosure_length); + sb.append("\""); + } + sb.append("}"); + } + if (this.guid != null) { + sb.append(",\"guid\":\""); + sb.append(this.guid); + sb.append("\""); + } + if (this.subscription_title != null) { + sb.append(",\"subscription_title\":\""); + sb.append(this.subscription_title); + sb.append("\""); + } + + sb.append("}"); + return sb.str; + } + + /** + * Deserialize from JSON string (simple parser) + */ + public static FeedItem? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) { + return null; + } + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + + return from_json_node(parser.get_root()); + } + + /** + * Deserialize from Json.Node + */ + public static FeedItem? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) { + return null; + } + + var obj = node.get_object(); + + if (!obj.has_member("id") || !obj.has_member("title")) { + return null; + } + + var item = new FeedItem(); + item.id = obj.get_string_member("id"); + item.title = obj.get_string_member("title"); + + if (obj.has_member("link")) { + item.link = obj.get_string_member("link"); + } + if (obj.has_member("description")) { + item.description = obj.get_string_member("description"); + } + if (obj.has_member("content")) { + item.content = obj.get_string_member("content"); + } + if (obj.has_member("author")) { + item.author = obj.get_string_member("author"); + } + if (obj.has_member("published")) { + item.published = obj.get_string_member("published"); + } + if (obj.has_member("updated")) { + item.updated = obj.get_string_member("updated"); + } + if (obj.has_member("categories")) { + var categories_array = obj.get_array_member("categories"); + var categories = new string[categories_array.get_length()]; + for (var i = 0; i < categories_array.get_length(); i++) { + categories[i] = categories_array.get_string_element(i); + } + item.categories = categories; + } + if (obj.has_member("enclosure")) { + var enclosure_obj = obj.get_object_member("enclosure"); + item.enclosure_url = enclosure_obj.get_string_member("url"); + if (enclosure_obj.has_member("type")) { + item.enclosure_type = enclosure_obj.get_string_member("type"); + } + if (enclosure_obj.has_member("length")) { + item.enclosure_length = enclosure_obj.get_string_member("length"); + } + } + if (obj.has_member("guid")) { + item.guid = obj.get_string_member("guid"); + } + if (obj.has_member("subscription_title")) { + item.subscription_title = obj.get_string_member("subscription_title"); + } + + return item; + } + + /** + * Equality comparison + */ + public bool equals(FeedItem? other) { + if (other == null) { + return false; + } + + return this.id == other.id && + this.title == other.title && + this.link == other.link && + this.description == other.description && + this.content == other.content && + this.author == other.author && + this.published == other.published && + this.updated == other.updated && + this.categories_equal(other.categories) && + this.enclosure_url == other.enclosure_url && + this.enclosure_type == other.enclosure_type && + this.enclosure_length == other.enclosure_length && + this.guid == other.guid && + this.subscription_title == other.subscription_title; + } + + /** + * Helper for category array comparison + */ + private bool categories_equal(string[] other) { + if (this.categories.length != other.length) { + return false; + } + + for (var i = 0; i < this.categories.length; i++) { + if (this.categories[i] != other[i]) { + return false; + } + } + + return true; + } + + /** + * Get a human-readable summary + */ + public string get_summary() { + return "%s by %s".printf(this.title, this.author ?? "Unknown"); + } +} diff --git a/native-route/linux/src/models/feed-subscription.vala b/native-route/linux/src/models/feed-subscription.vala new file mode 100644 index 0000000..5e58a65 --- /dev/null +++ b/native-route/linux/src/models/feed-subscription.vala @@ -0,0 +1,259 @@ +/* + * FeedSubscription.vala + * + * Represents a user's subscription to a feed with sync settings. + * Following GNOME HIG naming conventions and Vala/GObject patterns. + */ + +/** + * HTTP Authentication credentials + */ +public struct RSSuper.HttpAuth { + public string username { get; set; } + public string password { get; set; } + + public HttpAuth(string username, string password) { + this.username = username; + this.password = password; + } +} + +/** + * FeedSubscription - Represents a user's subscription to a feed + */ +public class RSSuper.FeedSubscription : Object { + public string id { get; set; } + public string url { get; set; } + public string title { get; set; } + public string? category { get; set; } + public bool enabled { get; set; } + public int fetch_interval { get; set; } + public string created_at { get; set; } + public string updated_at { get; set; } + public string? last_fetched_at { get; set; } + public string? next_fetch_at { get; set; } + public string? error { get; set; } + public string? http_auth_username { get; set; } + public string? http_auth_password { get; set; } + + /** + * Default constructor + */ + public FeedSubscription() { + this.id = ""; + this.url = ""; + this.title = ""; + this.enabled = true; + this.fetch_interval = 60; + this.created_at = ""; + this.updated_at = ""; + } + + /** + * Constructor with initial values + */ + public FeedSubscription.with_values(string id, string url, string title, + int fetch_interval = 60, + string? category = null, bool enabled = true, + string? created_at = null, string? updated_at = null, + string? last_fetched_at = null, + string? next_fetch_at = null, + string? error = null, + string? http_auth_username = null, + string? http_auth_password = null) { + this.id = id; + this.url = url; + this.title = title; + this.category = category; + this.enabled = enabled; + this.fetch_interval = fetch_interval; + this.created_at = created_at ?? ""; + this.updated_at = updated_at ?? ""; + this.last_fetched_at = last_fetched_at; + this.next_fetch_at = next_fetch_at; + this.error = error; + this.http_auth_username = http_auth_username; + this.http_auth_password = http_auth_password; + } + + /** + * Get HTTP auth as struct + */ + public HttpAuth? get_http_auth() { + if (this.http_auth_username == null) { + return null; + } + return HttpAuth(this.http_auth_username, this.http_auth_password ?? ""); + } + + /** + * Set HTTP auth from struct + */ + public void set_http_auth(HttpAuth? auth) { + if (auth == null) { + this.http_auth_username = null; + this.http_auth_password = null; + } else { + this.http_auth_username = auth.username; + this.http_auth_password = auth.password; + } + } + + /** + * Check if subscription has an error + */ + public bool has_error() { + return this.error != null && this.error.length > 0; + } + + /** + * Serialize to JSON string + */ + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{"); + sb.append("\"id\":\""); + sb.append(this.id); + sb.append("\",\"url\":\""); + sb.append(this.url); + sb.append("\",\"title\":\""); + sb.append(this.title); + sb.append("\",\"enabled\":"); + sb.append(this.enabled ? "true" : "false"); + sb.append(",\"fetchInterval\":%d".printf(this.fetch_interval)); + sb.append(",\"createdAt\":\""); + sb.append(this.created_at); + sb.append("\",\"updatedAt\":\""); + sb.append(this.updated_at); + sb.append("\""); + + if (this.category != null) { + sb.append(",\"category\":\""); + sb.append(this.category); + sb.append("\""); + } + if (this.last_fetched_at != null) { + sb.append(",\"lastFetchedAt\":\""); + sb.append(this.last_fetched_at); + sb.append("\""); + } + if (this.next_fetch_at != null) { + sb.append(",\"nextFetchAt\":\""); + sb.append(this.next_fetch_at); + sb.append("\""); + } + if (this.error != null) { + sb.append(",\"error\":\""); + sb.append(this.error); + sb.append("\""); + } + if (this.http_auth_username != null) { + sb.append(",\"httpAuth\":{\"username\":\""); + sb.append(this.http_auth_username); + sb.append("\""); + if (this.http_auth_password != null) { + sb.append(",\"password\":\""); + sb.append(this.http_auth_password); + sb.append("\""); + } + sb.append("}"); + } + + sb.append("}"); + return sb.str; + } + + /** + * Deserialize from JSON string + */ + public static FeedSubscription? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) { + return null; + } + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + + return from_json_node(parser.get_root()); + } + + /** + * Deserialize from Json.Node + */ + public static FeedSubscription? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) { + return null; + } + + var obj = node.get_object(); + + if (!obj.has_member("id") || !obj.has_member("url") || + !obj.has_member("title") || !obj.has_member("createdAt") || + !obj.has_member("updatedAt")) { + return null; + } + + var subscription = new FeedSubscription(); + subscription.id = obj.get_string_member("id"); + subscription.url = obj.get_string_member("url"); + subscription.title = obj.get_string_member("title"); + + if (obj.has_member("category")) { + subscription.category = obj.get_string_member("category"); + } + if (obj.has_member("enabled")) { + subscription.enabled = obj.get_boolean_member("enabled"); + } + if (obj.has_member("fetchInterval")) { + subscription.fetch_interval = (int)obj.get_int_member("fetchInterval"); + } + + subscription.created_at = obj.get_string_member("createdAt"); + subscription.updated_at = obj.get_string_member("updatedAt"); + + if (obj.has_member("lastFetchedAt")) { + subscription.last_fetched_at = obj.get_string_member("lastFetchedAt"); + } + if (obj.has_member("nextFetchAt")) { + subscription.next_fetch_at = obj.get_string_member("nextFetchAt"); + } + if (obj.has_member("error")) { + subscription.error = obj.get_string_member("error"); + } + if (obj.has_member("httpAuth")) { + var auth_obj = obj.get_object_member("httpAuth"); + subscription.http_auth_username = auth_obj.get_string_member("username"); + if (auth_obj.has_member("password")) { + subscription.http_auth_password = auth_obj.get_string_member("password"); + } + } + + return subscription; + } + + /** + * Equality comparison + */ + public bool equals(FeedSubscription? other) { + if (other == null) { + return false; + } + + return this.id == other.id && + this.url == other.url && + this.title == other.title && + this.category == other.category && + this.enabled == other.enabled && + this.fetch_interval == other.fetch_interval && + this.created_at == other.created_at && + this.updated_at == other.updated_at && + this.last_fetched_at == other.last_fetched_at && + this.next_fetch_at == other.next_fetch_at && + this.error == other.error && + this.http_auth_username == other.http_auth_username && + this.http_auth_password == other.http_auth_password; + } +} diff --git a/native-route/linux/src/models/feed.vala b/native-route/linux/src/models/feed.vala new file mode 100644 index 0000000..634827d --- /dev/null +++ b/native-route/linux/src/models/feed.vala @@ -0,0 +1,282 @@ +/* + * Feed.vala + * + * Represents an RSS/Atom feed with its metadata and items. + * Following GNOME HIG naming conventions and Vala/GObject patterns. + */ + +/** + * Feed - Represents an RSS/Atom feed + */ +public class RSSuper.Feed : Object { + public string id { get; set; } + public string title { get; set; } + public string? link { get; set; } + public string? description { get; set; } + public string? subtitle { get; set; } + public string? language { get; set; } + public string? last_build_date { get; set; } + public string? updated { get; set; } + public string? generator { get; set; } + public int ttl { get; set; } + public string raw_url { get; set; } + public string? last_fetched_at { get; set; } + public string? next_fetch_at { get; set; } + public FeedItem[] items { get; set; } + + /** + * Default constructor + */ + public Feed() { + this.id = ""; + this.title = ""; + this.raw_url = ""; + this.ttl = 60; + this.items = {}; + } + + /** + * Constructor with initial values + */ + public Feed.with_values(string id, string title, string raw_url, + string? link = null, string? description = null, + string? subtitle = null, string? language = null, + string? last_build_date = null, string? updated = null, + string? generator = null, int ttl = 60, + FeedItem[]? items = null, string? last_fetched_at = null, + string? next_fetch_at = null) { + this.id = id; + this.title = title; + this.link = link; + this.description = description; + this.subtitle = subtitle; + this.language = language; + this.last_build_date = last_build_date; + this.updated = updated; + this.generator = generator; + this.ttl = ttl; + this.items = items; + this.raw_url = raw_url; + this.last_fetched_at = last_fetched_at; + this.next_fetch_at = next_fetch_at; + } + + /** + * Add an item to the feed + */ + public void add_item(FeedItem item) { + var new_items = new FeedItem[this.items.length + 1]; + for (var i = 0; i < this.items.length; i++) { + new_items[i] = this.items[i]; + } + new_items[this.items.length] = item; + this.items = new_items; + } + + /** + * Get item count + */ + public int get_item_count() { + return this.items.length; + } + + /** + * Serialize to JSON string + */ + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{"); + sb.append("\"id\":\""); + sb.append(this.id); + sb.append("\",\"title\":\""); + sb.append(this.title); + sb.append("\",\"raw_url\":\""); + sb.append(this.raw_url); + sb.append("\""); + + if (this.link != null) { + sb.append(",\"link\":\""); + sb.append(this.link); + sb.append("\""); + } + if (this.description != null) { + sb.append(",\"description\":\""); + sb.append(this.description); + sb.append("\""); + } + if (this.subtitle != null) { + sb.append(",\"subtitle\":\""); + sb.append(this.subtitle); + sb.append("\""); + } + if (this.language != null) { + sb.append(",\"language\":\""); + sb.append(this.language); + sb.append("\""); + } + if (this.last_build_date != null) { + sb.append(",\"lastBuildDate\":\""); + sb.append(this.last_build_date); + sb.append("\""); + } + if (this.updated != null) { + sb.append(",\"updated\":\""); + sb.append(this.updated); + sb.append("\""); + } + if (this.generator != null) { + sb.append(",\"generator\":\""); + sb.append(this.generator); + sb.append("\""); + } + if (this.ttl != 60) { + sb.append(",\"ttl\":%d".printf(this.ttl)); + } + if (this.items.length > 0) { + sb.append(",\"items\":["); + for (var i = 0; i < this.items.length; i++) { + if (i > 0) sb.append(","); + sb.append(this.items[i].to_json_string()); + } + sb.append("]"); + } + if (this.last_fetched_at != null) { + sb.append(",\"lastFetchedAt\":\""); + sb.append(this.last_fetched_at); + sb.append("\""); + } + if (this.next_fetch_at != null) { + sb.append(",\"nextFetchAt\":\""); + sb.append(this.next_fetch_at); + sb.append("\""); + } + + sb.append("}"); + return sb.str; + } + + /** + * Deserialize from JSON string + */ + public static Feed? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) { + return null; + } + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + + return from_json_node(parser.get_root()); + } + + /** + * Deserialize from Json.Node + */ + public static Feed? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) { + return null; + } + + var obj = node.get_object(); + + if (!obj.has_member("id") || !obj.has_member("title") || !obj.has_member("raw_url")) { + return null; + } + + var feed = new Feed(); + feed.id = obj.get_string_member("id"); + feed.title = obj.get_string_member("title"); + feed.raw_url = obj.get_string_member("raw_url"); + + if (obj.has_member("link")) { + feed.link = obj.get_string_member("link"); + } + if (obj.has_member("description")) { + feed.description = obj.get_string_member("description"); + } + if (obj.has_member("subtitle")) { + feed.subtitle = obj.get_string_member("subtitle"); + } + if (obj.has_member("language")) { + feed.language = obj.get_string_member("language"); + } + if (obj.has_member("lastBuildDate")) { + feed.last_build_date = obj.get_string_member("lastBuildDate"); + } + if (obj.has_member("updated")) { + feed.updated = obj.get_string_member("updated"); + } + if (obj.has_member("generator")) { + feed.generator = obj.get_string_member("generator"); + } + if (obj.has_member("ttl")) { + feed.ttl = (int)obj.get_int_member("ttl"); + } + if (obj.has_member("lastFetchedAt")) { + feed.last_fetched_at = obj.get_string_member("lastFetchedAt"); + } + if (obj.has_member("nextFetchAt")) { + feed.next_fetch_at = obj.get_string_member("nextFetchAt"); + } + + // Deserialize items + if (obj.has_member("items")) { + var items_array = obj.get_array_member("items"); + var items = new FeedItem[items_array.get_length()]; + for (var i = 0; i < items_array.get_length(); i++) { + var item_node = items_array.get_element(i); + var item = FeedItem.from_json_node(item_node); + if (item != null) { + items[i] = item; + } + } + feed.items = items; + } + + return feed; + } + + /** + * Equality comparison + */ + public bool equals(Feed? other) { + if (other == null) { + return false; + } + + return this.id == other.id && + this.title == other.title && + this.link == other.link && + this.description == other.description && + this.subtitle == other.subtitle && + this.language == other.language && + this.last_build_date == other.last_build_date && + this.updated == other.updated && + this.generator == other.generator && + this.ttl == other.ttl && + this.raw_url == other.raw_url && + this.last_fetched_at == other.last_fetched_at && + this.next_fetch_at == other.next_fetch_at && + this.items_equal(other.items); + } + + /** + * Helper for item array comparison + */ + private bool items_equal(FeedItem[] other) { + if (this.items.length != other.length) { + return false; + } + + for (var i = 0; i < this.items.length; i++) { + if (!this.items[i].equals(other[i])) { + return false; + } + } + + return true; + } +} diff --git a/native-route/linux/src/models/namespaces.vala b/native-route/linux/src/models/namespaces.vala new file mode 100644 index 0000000..2be75ee --- /dev/null +++ b/native-route/linux/src/models/namespaces.vala @@ -0,0 +1,5 @@ +/* + * Namespace definition for RSSuper Linux models + */ +public namespace RSSuper { +} diff --git a/native-route/linux/src/models/notification-preferences.vala b/native-route/linux/src/models/notification-preferences.vala new file mode 100644 index 0000000..5f83caf --- /dev/null +++ b/native-route/linux/src/models/notification-preferences.vala @@ -0,0 +1,190 @@ +/* + * NotificationPreferences.vala + * + * Represents user notification preferences. + * Following GNOME HIG naming conventions and Vala/GObject patterns. + */ + +/** + * NotificationPreferences - User notification settings + */ +public class RSSuper.NotificationPreferences : Object { + public bool new_articles { get; set; } + public bool episode_releases { get; set; } + public bool custom_alerts { get; set; } + public bool badge_count { get; set; } + public bool sound { get; set; } + public bool vibration { get; set; } + + /** + * Default constructor (all enabled by default) + */ + public NotificationPreferences() { + this.new_articles = true; + this.episode_releases = true; + this.custom_alerts = true; + this.badge_count = true; + this.sound = true; + this.vibration = true; + } + + /** + * Constructor with initial values + */ + public NotificationPreferences.with_values(bool new_articles = true, + bool episode_releases = true, + bool custom_alerts = true, + bool badge_count = true, + bool sound = true, + bool vibration = true) { + this.new_articles = new_articles; + this.episode_releases = episode_releases; + this.custom_alerts = custom_alerts; + this.badge_count = badge_count; + this.sound = sound; + this.vibration = vibration; + } + + /** + * Enable all notifications + */ + public void enable_all() { + this.new_articles = true; + this.episode_releases = true; + this.custom_alerts = true; + this.badge_count = true; + this.sound = true; + this.vibration = true; + } + + /** + * Disable all notifications + */ + public void disable_all() { + this.new_articles = false; + this.episode_releases = false; + this.custom_alerts = false; + this.badge_count = false; + this.sound = false; + this.vibration = false; + } + + /** + * Check if any notifications are enabled + */ + public bool has_any_enabled() { + return this.new_articles || + this.episode_releases || + this.custom_alerts || + this.badge_count || + this.sound || + this.vibration; + } + + /** + * Check if content notifications are enabled + */ + public bool has_content_notifications() { + return this.new_articles || this.episode_releases || this.custom_alerts; + } + + /** + * Serialize to JSON string + */ + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{"); + sb.append("\"newArticles\":"); + sb.append(this.new_articles ? "true" : "false"); + sb.append(",\"episodeReleases\":"); + sb.append(this.episode_releases ? "true" : "false"); + sb.append(",\"customAlerts\":"); + sb.append(this.custom_alerts ? "true" : "false"); + sb.append(",\"badgeCount\":"); + sb.append(this.badge_count ? "true" : "false"); + sb.append(",\"sound\":"); + sb.append(this.sound ? "true" : "false"); + sb.append(",\"vibration\":"); + sb.append(this.vibration ? "true" : "false"); + sb.append("}"); + return sb.str; + } + + /** + * Deserialize from JSON string + */ + public static NotificationPreferences? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) { + return null; + } + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + + return from_json_node(parser.get_root()); + } + + /** + * Deserialize from Json.Node + */ + public static NotificationPreferences? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) { + return null; + } + + var obj = node.get_object(); + var prefs = new NotificationPreferences(); + + if (obj.has_member("newArticles")) { + prefs.new_articles = obj.get_boolean_member("newArticles"); + } + if (obj.has_member("episodeReleases")) { + prefs.episode_releases = obj.get_boolean_member("episodeReleases"); + } + if (obj.has_member("customAlerts")) { + prefs.custom_alerts = obj.get_boolean_member("customAlerts"); + } + if (obj.has_member("badgeCount")) { + prefs.badge_count = obj.get_boolean_member("badgeCount"); + } + if (obj.has_member("sound")) { + prefs.sound = obj.get_boolean_member("sound"); + } + if (obj.has_member("vibration")) { + prefs.vibration = obj.get_boolean_member("vibration"); + } + + return prefs; + } + + /** + * Equality comparison + */ + public bool equals(NotificationPreferences? other) { + if (other == null) { + return false; + } + + return this.new_articles == other.new_articles && + this.episode_releases == other.episode_releases && + this.custom_alerts == other.custom_alerts && + this.badge_count == other.badge_count && + this.sound == other.sound && + this.vibration == other.vibration; + } + + /** + * Copy preferences from another instance + */ + public void copy_from(NotificationPreferences other) { + this.new_articles = other.new_articles; + this.episode_releases = other.episode_releases; + this.custom_alerts = other.custom_alerts; + this.badge_count = other.badge_count; + this.sound = other.sound; + this.vibration = other.vibration; + } +} diff --git a/native-route/linux/src/models/reading-preferences.vala b/native-route/linux/src/models/reading-preferences.vala new file mode 100644 index 0000000..2ba2e5a --- /dev/null +++ b/native-route/linux/src/models/reading-preferences.vala @@ -0,0 +1,168 @@ +/* + * ReadingPreferences.vala + * + * Represents user reading/display preferences. + * Following GNOME HIG naming conventions and Vala/GObject patterns. + */ + +/** + * FontSize - Available font size options + */ +public enum RSSuper.FontSize { + SMALL, + MEDIUM, + LARGE, + XLARGE +} + +/** + * LineHeight - Available line height options + */ +public enum RSSuper.LineHeight { + NORMAL, + RELAXED, + LOOSE +} + +/** + * ReadingPreferences - User reading/display settings + */ +public class RSSuper.ReadingPreferences : Object { + public FontSize font_size { get; set; } + public LineHeight line_height { get; set; } + public bool show_table_of_contents { get; set; } + public bool show_reading_time { get; set; } + public bool show_author { get; set; } + public bool show_date { get; set; } + + public ReadingPreferences() { + this.font_size = FontSize.MEDIUM; + this.line_height = LineHeight.NORMAL; + this.show_table_of_contents = true; + this.show_reading_time = true; + this.show_author = true; + this.show_date = true; + } + + public ReadingPreferences.with_values(FontSize font_size = FontSize.MEDIUM, + LineHeight line_height = LineHeight.NORMAL, + bool show_table_of_contents = true, + bool show_reading_time = true, + bool show_author = true, + bool show_date = true) { + this.font_size = font_size; + this.line_height = line_height; + this.show_table_of_contents = show_table_of_contents; + this.show_reading_time = show_reading_time; + this.show_author = show_author; + this.show_date = show_date; + } + + public string get_font_size_string() { + switch (this.font_size) { + case FontSize.SMALL: return "small"; + case FontSize.MEDIUM: return "medium"; + case FontSize.LARGE: return "large"; + case FontSize.XLARGE: return "xlarge"; + default: return "medium"; + } + } + + public static FontSize font_size_from_string(string str) { + switch (str) { + case "small": return FontSize.SMALL; + case "medium": return FontSize.MEDIUM; + case "large": return FontSize.LARGE; + case "xlarge": return FontSize.XLARGE; + default: return FontSize.MEDIUM; + } + } + + public string get_line_height_string() { + switch (this.line_height) { + case LineHeight.NORMAL: return "normal"; + case LineHeight.RELAXED: return "relaxed"; + case LineHeight.LOOSE: return "loose"; + default: return "normal"; + } + } + + public static LineHeight line_height_from_string(string str) { + switch (str) { + case "normal": return LineHeight.NORMAL; + case "relaxed": return LineHeight.RELAXED; + case "loose": return LineHeight.LOOSE; + default: return LineHeight.NORMAL; + } + } + + public void reset_to_defaults() { + this.font_size = FontSize.MEDIUM; + this.line_height = LineHeight.NORMAL; + this.show_table_of_contents = true; + this.show_reading_time = true; + this.show_author = true; + this.show_date = true; + } + + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{\"fontSize\":\""); + sb.append(this.get_font_size_string()); + sb.append("\",\"lineHeight\":\""); + sb.append(this.get_line_height_string()); + sb.append("\",\"showTableOfContents\":"); + sb.append(this.show_table_of_contents ? "true" : "false"); + sb.append(",\"showReadingTime\":"); + sb.append(this.show_reading_time ? "true" : "false"); + sb.append(",\"showAuthor\":"); + sb.append(this.show_author ? "true" : "false"); + sb.append(",\"showDate\":"); + sb.append(this.show_date ? "true" : "false"); + sb.append("}"); + return sb.str; + } + + public static ReadingPreferences? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) return null; + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + return from_json_node(parser.get_root()); + } + + public static ReadingPreferences? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) return null; + var obj = node.get_object(); + var prefs = new ReadingPreferences(); + if (obj.has_member("fontSize")) prefs.font_size = font_size_from_string(obj.get_string_member("fontSize")); + if (obj.has_member("lineHeight")) prefs.line_height = line_height_from_string(obj.get_string_member("lineHeight")); + if (obj.has_member("showTableOfContents")) prefs.show_table_of_contents = obj.get_boolean_member("showTableOfContents"); + if (obj.has_member("showReadingTime")) prefs.show_reading_time = obj.get_boolean_member("showReadingTime"); + if (obj.has_member("showAuthor")) prefs.show_author = obj.get_boolean_member("showAuthor"); + if (obj.has_member("showDate")) prefs.show_date = obj.get_boolean_member("showDate"); + return prefs; + } + + public bool equals(ReadingPreferences? other) { + if (other == null) return false; + return this.font_size == other.font_size && + this.line_height == other.line_height && + this.show_table_of_contents == other.show_table_of_contents && + this.show_reading_time == other.show_reading_time && + this.show_author == other.show_author && + this.show_date == other.show_date; + } + + public void copy_from(ReadingPreferences other) { + this.font_size = other.font_size; + this.line_height = other.line_height; + this.show_table_of_contents = other.show_table_of_contents; + this.show_reading_time = other.show_reading_time; + this.show_author = other.show_author; + this.show_date = other.show_date; + } +} diff --git a/native-route/linux/src/models/search-filters.vala b/native-route/linux/src/models/search-filters.vala new file mode 100644 index 0000000..68d02b1 --- /dev/null +++ b/native-route/linux/src/models/search-filters.vala @@ -0,0 +1,435 @@ +/* + * SearchFilters.vala + * + * Represents search query parameters and filters. + * Following GNOME HIG naming conventions and Vala/GObject patterns. + */ + +/** + * SearchContentType - Type of content to search for + */ +public enum RSSuper.SearchContentType { + ARTICLE, + AUDIO, + VIDEO +} + +/** + * SearchSortOption - Sorting options for search results + */ +public enum RSSuper.SearchSortOption { + RELEVANCE, + DATE_DESC, + DATE_ASC, + TITLE_ASC, + TITLE_DESC, + FEED_ASC, + FEED_DESC +} + +/** + * SearchFilters - Represents search filters and query parameters + */ +public struct RSSuper.SearchFilters { + public string? date_from { get; set; } + public string? date_to { get; set; } + public string[] feed_ids { get; set; } + public string[] authors { get; set; } + public SearchContentType? content_type { get; set; } + + /** + * Default constructor + */ + public SearchFilters(string? date_from = null, string? date_to = null, + string[]? feed_ids = null, string[]? authors = null, + SearchContentType? content_type = null) { + this.date_from = date_from; + this.date_to = date_to; + this.feed_ids = feed_ids; + this.authors = authors; + this.content_type = content_type; + } + + /** + * Get content type as string + */ + public string? get_content_type_string() { + if (this.content_type == null) { + return null; + } + switch (this.content_type) { + case SearchContentType.ARTICLE: + return "article"; + case SearchContentType.AUDIO: + return "audio"; + case SearchContentType.VIDEO: + return "video"; + default: + return null; + } + } + + /** + * Parse content type from string + */ + public static SearchContentType? content_type_from_string(string? str) { + if (str == null) { + return null; + } + switch (str) { + case "article": + return SearchContentType.ARTICLE; + case "audio": + return SearchContentType.AUDIO; + case "video": + return SearchContentType.VIDEO; + default: + return null; + } + } + + /** + * Get sort option as string + */ + public static string sort_option_to_string(SearchSortOption option) { + switch (option) { + case SearchSortOption.RELEVANCE: + return "relevance"; + case SearchSortOption.DATE_DESC: + return "date_desc"; + case SearchSortOption.DATE_ASC: + return "date_asc"; + case SearchSortOption.TITLE_ASC: + return "title_asc"; + case SearchSortOption.TITLE_DESC: + return "title_desc"; + case SearchSortOption.FEED_ASC: + return "feed_asc"; + case SearchSortOption.FEED_DESC: + return "feed_desc"; + default: + return "relevance"; + } + } + + /** + * Parse sort option from string + */ + public static SearchSortOption sort_option_from_string(string str) { + switch (str) { + case "relevance": + return SearchSortOption.RELEVANCE; + case "date_desc": + return SearchSortOption.DATE_DESC; + case "date_asc": + return SearchSortOption.DATE_ASC; + case "title_asc": + return SearchSortOption.TITLE_ASC; + case "title_desc": + return SearchSortOption.TITLE_DESC; + case "feed_asc": + return SearchSortOption.FEED_ASC; + case "feed_desc": + return SearchSortOption.FEED_DESC; + default: + return SearchSortOption.RELEVANCE; + } + } + + /** + * Check if any filters are set + */ + public bool has_filters() { + return this.date_from != null || + this.date_to != null || + (this.feed_ids != null && this.feed_ids.length > 0) || + (this.authors != null && this.authors.length > 0) || + this.content_type != null; + } + + /** + * Serialize to JSON string + */ + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{"); + + var first = true; + if (this.date_from != null) { + sb.append("\"dateFrom\":\""); + sb.append(this.date_from); + sb.append("\""); + first = false; + } + if (this.date_to != null) { + if (!first) sb.append(","); + sb.append("\"dateTo\":\""); + sb.append(this.date_to); + sb.append("\""); + first = false; + } + if (this.feed_ids != null && this.feed_ids.length > 0) { + if (!first) sb.append(","); + sb.append("\"feedIds\":["); + for (var i = 0; i < this.feed_ids.length; i++) { + if (i > 0) sb.append(","); + sb.append("\""); + sb.append(this.feed_ids[i]); + sb.append("\""); + } + sb.append("]"); + first = false; + } + if (this.authors != null && this.authors.length > 0) { + if (!first) sb.append(","); + sb.append("\"authors\":["); + for (var i = 0; i < this.authors.length; i++) { + if (i > 0) sb.append(","); + sb.append("\""); + sb.append(this.authors[i]); + sb.append("\""); + } + sb.append("]"); + first = false; + } + if (this.content_type != null) { + if (!first) sb.append(","); + sb.append("\"contentType\":\""); + sb.append(this.get_content_type_string()); + sb.append("\""); + } + + sb.append("}"); + return sb.str; + } + + /** + * Deserialize from JSON string + */ + public static SearchFilters? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) { + return null; + } + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + + return from_json_node(parser.get_root()); + } + + /** + * Deserialize from Json.Node + */ + public static SearchFilters? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) { + return null; + } + + var obj = node.get_object(); + var filters = SearchFilters(); + + if (obj.has_member("dateFrom")) { + filters.date_from = obj.get_string_member("dateFrom"); + } + if (obj.has_member("dateTo")) { + filters.date_to = obj.get_string_member("dateTo"); + } + if (obj.has_member("feedIds")) { + var array = obj.get_array_member("feedIds"); + var feed_ids = new string[array.get_length()]; + for (var i = 0; i < array.get_length(); i++) { + feed_ids[i] = array.get_string_element(i); + } + filters.feed_ids = feed_ids; + } + if (obj.has_member("authors")) { + var array = obj.get_array_member("authors"); + var authors = new string[array.get_length()]; + for (var i = 0; i < array.get_length(); i++) { + authors[i] = array.get_string_element(i); + } + filters.authors = authors; + } + if (obj.has_member("contentType")) { + filters.content_type = content_type_from_string(obj.get_string_member("contentType")); + } + + return filters; + } + + /** + * Equality comparison + */ + public bool equals(SearchFilters other) { + return this.date_from == other.date_from && + this.date_to == other.date_to && + this.feeds_equal(other.feed_ids) && + this.authors_equal(other.authors) && + this.content_type == other.content_type; + } + + /** + * Helper for feed_ids comparison + */ + private bool feeds_equal(string[]? other) { + if (this.feed_ids == null && other == null) return true; + if (this.feed_ids == null || other == null) return false; + if (this.feed_ids.length != other.length) { + return false; + } + for (var i = 0; i < this.feed_ids.length; i++) { + if (this.feed_ids[i] != other[i]) { + return false; + } + } + return true; + } + + /** + * Helper for authors comparison + */ + private bool authors_equal(string[]? other) { + if (this.authors == null && other == null) return true; + if (this.authors == null || other == null) return false; + if (this.authors.length != other.length) { + return false; + } + for (var i = 0; i < this.authors.length; i++) { + if (this.authors[i] != other[i]) { + return false; + } + } + return true; + } +} + +/** + * SearchQuery - Represents a complete search query + */ +public struct RSSuper.SearchQuery { + public string query { get; set; } + public int page { get; set; } + public int page_size { get; set; } + public string filters_json { get; set; } + public SearchSortOption sort { get; set; } + + /** + * Default constructor + */ + public SearchQuery(string query, int page = 1, int page_size = 20, + string? filters_json = null, SearchSortOption sort = SearchSortOption.RELEVANCE) { + this.query = query; + this.page = page; + this.page_size = page_size; + this.filters_json = filters_json; + this.sort = sort; + } + + /** + * Get filters as struct + */ + public SearchFilters? get_filters() { + if (this.filters_json == null || this.filters_json.length == 0) { + return null; + } + return SearchFilters.from_json_string(this.filters_json); + } + + /** + * Set filters from struct + */ + public void set_filters(SearchFilters? filters) { + if (filters == null) { + this.filters_json = ""; + } else { + this.filters_json = filters.to_json_string(); + } + } + + /** + * Serialize to JSON string + */ + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{"); + sb.append("\"query\":\""); + sb.append(this.query); + sb.append("\""); + sb.append(",\"page\":%d".printf(this.page)); + sb.append(",\"pageSize\":%d".printf(this.page_size)); + if (this.filters_json != null && this.filters_json.length > 0) { + sb.append(",\"filters\":"); + sb.append(this.filters_json); + } + sb.append(",\"sort\":\""); + sb.append(SearchFilters.sort_option_to_string(this.sort)); + sb.append("\""); + sb.append("}"); + return sb.str; + } + + /** + * Deserialize from JSON string + */ + public static SearchQuery? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) { + return null; + } + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + + return from_json_node(parser.get_root()); + } + + /** + * Deserialize from Json.Node + */ + public static SearchQuery? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) { + return null; + } + + var obj = node.get_object(); + + if (!obj.has_member("query")) { + return null; + } + + var query = SearchQuery(obj.get_string_member("query")); + + if (obj.has_member("page")) { + query.page = (int)obj.get_int_member("page"); + } + if (obj.has_member("pageSize")) { + query.page_size = (int)obj.get_int_member("pageSize"); + } + if (obj.has_member("filters")) { + var generator = new Json.Generator(); + generator.set_root(obj.get_member("filters")); + query.filters_json = generator.to_data(null); + } + if (obj.has_member("sort")) { + query.sort = SearchFilters.sort_option_from_string(obj.get_string_member("sort")); + } + + return query; + } + + /** + * Equality comparison + */ + public bool equals(SearchQuery other) { + return this.query == other.query && + this.page == other.page && + this.page_size == other.page_size && + this.filters_json == other.filters_json && + this.sort == other.sort; + } +} diff --git a/native-route/linux/src/models/search-result.vala b/native-route/linux/src/models/search-result.vala new file mode 100644 index 0000000..bc87bf2 --- /dev/null +++ b/native-route/linux/src/models/search-result.vala @@ -0,0 +1,208 @@ +/* + * SearchResult.vala + * + * Represents a search result item from the feed database. + * Following GNOME HIG naming conventions and Vala/GObject patterns. + */ + +/** + * SearchResultType - Type of search result + */ +public enum RSSuper.SearchResultType { + ARTICLE, + FEED +} + +/** + * SearchResult - Represents a single search result + */ +public class RSSuper.SearchResult : Object { + public string id { get; set; } + public SearchResultType result_type { get; set; } + public string title { get; set; } + public string? snippet { get; set; } + public string? link { get; set; } + public string? feed_title { get; set; } + public string? published { get; set; } + public double score { get; set; } + + /** + * Default constructor + */ + public SearchResult() { + this.id = ""; + this.result_type = SearchResultType.ARTICLE; + this.title = ""; + this.score = 0.0; + } + + /** + * Constructor with initial values + */ + public SearchResult.with_values(string id, SearchResultType type, string title, + string? snippet = null, string? link = null, + string? feed_title = null, string? published = null, + double score = 0.0) { + this.id = id; + this.result_type = type; + this.title = title; + this.snippet = snippet; + this.link = link; + this.feed_title = feed_title; + this.published = published; + this.score = score; + } + + /** + * Get type as string + */ + public string get_type_string() { + switch (this.result_type) { + case SearchResultType.ARTICLE: + return "article"; + case SearchResultType.FEED: + return "feed"; + default: + return "unknown"; + } + } + + /** + * Parse type from string + */ + public static SearchResultType type_from_string(string str) { + switch (str) { + case "article": + return SearchResultType.ARTICLE; + case "feed": + return SearchResultType.FEED; + default: + return SearchResultType.ARTICLE; + } + } + + /** + * Serialize to JSON string + */ + public string to_json_string() { + var sb = new StringBuilder(); + sb.append("{"); + sb.append("\"id\":\""); + sb.append(this.id); + sb.append("\",\"type\":\""); + sb.append(this.get_type_string()); + sb.append("\",\"title\":\""); + sb.append(this.title); + sb.append("\""); + + if (this.snippet != null) { + sb.append(",\"snippet\":\""); + sb.append(this.snippet); + sb.append("\""); + } + if (this.link != null) { + sb.append(",\"link\":\""); + sb.append(this.link); + sb.append("\""); + } + if (this.feed_title != null) { + sb.append(",\"feedTitle\":\""); + sb.append(this.feed_title); + sb.append("\""); + } + if (this.published != null) { + sb.append(",\"published\":\""); + sb.append(this.published); + sb.append("\""); + } + if (this.score != 0.0) { + sb.append(",\"score\":%f".printf(this.score)); + } + + sb.append("}"); + return sb.str; + } + + /** + * Deserialize from JSON string + */ + public static SearchResult? from_json_string(string json_string) { + var parser = new Json.Parser(); + try { + if (!parser.load_from_data(json_string)) { + return null; + } + } catch (Error e) { + warning("Failed to parse JSON: %s", e.message); + return null; + } + + return from_json_node(parser.get_root()); + } + + /** + * Deserialize from Json.Node + */ + public static SearchResult? from_json_node(Json.Node node) { + if (node.get_node_type() != Json.NodeType.OBJECT) { + return null; + } + + var obj = node.get_object(); + + if (!obj.has_member("id") || !obj.has_member("type") || !obj.has_member("title")) { + return null; + } + + var result = new SearchResult(); + result.id = obj.get_string_member("id"); + result.result_type = SearchResult.type_from_string(obj.get_string_member("type")); + result.title = obj.get_string_member("title"); + + if (obj.has_member("snippet")) { + result.snippet = obj.get_string_member("snippet"); + } + if (obj.has_member("link")) { + result.link = obj.get_string_member("link"); + } + if (obj.has_member("feedTitle")) { + result.feed_title = obj.get_string_member("feedTitle"); + } + if (obj.has_member("published")) { + result.published = obj.get_string_member("published"); + } + if (obj.has_member("score")) { + result.score = obj.get_double_member("score"); + } + + return result; + } + + /** + * Equality comparison + */ + public bool equals(SearchResult? other) { + if (other == null) { + return false; + } + + return this.id == other.id && + this.result_type == other.result_type && + this.title == other.title && + this.snippet == other.snippet && + this.link == other.link && + this.feed_title == other.feed_title && + this.published == other.published && + this.score == other.score; + } + + /** + * Get a human-readable summary + */ + public string get_summary() { + if (this.feed_title != null) { + return "[%s] %s - %s".printf(this.get_type_string(), this.feed_title, this.title); + } + return "[%s] %s".printf(this.get_type_string(), this.title); + } +}