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
This commit is contained in:
2026-03-29 17:40:59 -04:00
parent fdd4fd8a46
commit f0922e3c03
9 changed files with 1893 additions and 0 deletions

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,5 @@
/*
* Namespace definition for RSSuper Linux models
*/
public namespace RSSuper {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}