diff --git a/iOS/RSSuper/Models/NotificationPreferences.swift b/iOS/RSSuper/Models/NotificationPreferences.swift index 479098d..d713894 100644 --- a/iOS/RSSuper/Models/NotificationPreferences.swift +++ b/iOS/RSSuper/Models/NotificationPreferences.swift @@ -39,6 +39,24 @@ struct NotificationPreferences: Codable, Equatable { newArticles || episodeReleases || customAlerts || badgeCount || sound || vibration } + mutating func enableAll() { + newArticles = true + episodeReleases = true + customAlerts = true + badgeCount = true + sound = true + vibration = true + } + + mutating func disableAll() { + newArticles = false + episodeReleases = false + customAlerts = false + badgeCount = false + sound = false + vibration = false + } + var debugDescription: String { """ NotificationPreferences( diff --git a/iOS/RSSuper/Models/ReadingPreferences.swift b/iOS/RSSuper/Models/ReadingPreferences.swift index d27e599..74cf46f 100644 --- a/iOS/RSSuper/Models/ReadingPreferences.swift +++ b/iOS/RSSuper/Models/ReadingPreferences.swift @@ -61,6 +61,20 @@ struct ReadingPreferences: Codable, Equatable { self.showDate = showDate } + mutating func enableAll() { + showTableOfContents = true + showReadingTime = true + showAuthor = true + showDate = true + } + + mutating func disableAll() { + showTableOfContents = false + showReadingTime = false + showAuthor = false + showDate = false + } + var debugDescription: String { """ ReadingPreferences( diff --git a/iOS/RSSuper/Settings/AppSettings.swift b/iOS/RSSuper/Settings/AppSettings.swift new file mode 100644 index 0000000..691e4a3 --- /dev/null +++ b/iOS/RSSuper/Settings/AppSettings.swift @@ -0,0 +1,65 @@ +// +// AppSettings.swift +// RSSuper +// +// App-wide settings configuration +// + +import Foundation + +struct AppSettings: Codable, Equatable { + var appVersion: String + var buildNumber: String + var lastMigrationVersion: String? + var firstLaunchAt: Date? + var lastLaunchAt: Date? + var launchCount: Int + + init( + appVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0", + buildNumber: String = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1", + lastMigrationVersion: String? = nil, + firstLaunchAt: Date? = nil, + lastLaunchAt: Date? = nil, + launchCount: Int = 0 + ) { + self.appVersion = appVersion + self.buildNumber = buildNumber + self.lastMigrationVersion = lastMigrationVersion + self.firstLaunchAt = firstLaunchAt + self.lastLaunchAt = lastLaunchAt + self.launchCount = launchCount + } + + var isFirstLaunch: Bool { + firstLaunchAt == nil + } + + func incrementLaunchCount() -> AppSettings { + var copy = self + copy.launchCount += 1 + copy.lastLaunchAt = Date() + if firstLaunchAt == nil { + copy.firstLaunchAt = Date() + } + return copy + } + + func withMigrationComplete(version: String) -> AppSettings { + var copy = self + copy.lastMigrationVersion = version + return copy + } + + var debugDescription: String { + """ + AppSettings( + appVersion: \(appVersion), + buildNumber: \(buildNumber), + lastMigrationVersion: \(lastMigrationVersion ?? "none"), + isFirstLaunch: \(isFirstLaunch), + launchCount: \(launchCount) + ) + """ + } +} diff --git a/iOS/RSSuper/Settings/SettingsMigration.swift b/iOS/RSSuper/Settings/SettingsMigration.swift new file mode 100644 index 0000000..3903bb9 --- /dev/null +++ b/iOS/RSSuper/Settings/SettingsMigration.swift @@ -0,0 +1,77 @@ +// +// SettingsMigration.swift +// RSSuper +// +// Settings migration support between versions +// + +import Foundation + +protocol SettingsMigratable { + associatedtype SettingsType: Codable + + var fromVersion: String { get } + var toVersion: String { get } + + func migrate(_ settings: SettingsType) -> SettingsType +} + +struct SettingsMigrationManager { + private(set) var migrations: [String: SettingsMigratable] + + init() { + migrations = [:] + } + + func registerMigration(_ migration: some SettingsMigratable) { + migrations[migration.fromVersion] = migration + } + + func migrateSettings(_ settings: T, fromVersion: String, toVersion: String) -> T? { + var currentVersion = fromVersion + var currentSettings = settings + + while currentVersion != toVersion, + let migration = migrations[currentVersion], + let migrated = try? JSONDecoder().decode(T.self, from: JSONEncoder().encode(migration.migrate(currentSettings))) { + currentSettings = migrated + currentVersion = migration.toVersion + } + + return currentSettings == settings ? nil : currentSettings + } + + func getAvailableVersions() -> [String] { + migrations.keys.sorted() + } +} + +// MARK: - Migration Implementations + +struct V1ToV2AppSettingsMigration: SettingsMigratable { + let fromVersion = "1.0.0" + let toVersion = "1.1.0" + + func migrate(_ settings: AppSettings) -> AppSettings { + var copy = settings + // Add new settings for v1.1.0 + if copy.lastMigrationVersion == nil { + copy.lastMigrationVersion = fromVersion + } + return copy + } +} + +struct V2ToV3AppSettingsMigration: SettingsMigratable { + let fromVersion = "1.1.0" + let toVersion = "1.2.0" + + func migrate(_ settings: AppSettings) -> AppSettings { + var copy = settings + // Add new settings for v1.2.0 + if copy.launchCount == 0 { + copy.launchCount = 1 + } + return copy + } +} diff --git a/iOS/RSSuper/Settings/SettingsStore.swift b/iOS/RSSuper/Settings/SettingsStore.swift new file mode 100644 index 0000000..79dd862 --- /dev/null +++ b/iOS/RSSuper/Settings/SettingsStore.swift @@ -0,0 +1,160 @@ +// +// SettingsStore.swift +// RSSuper +// +// Main settings store with UserDefaults/App Group +// + +import Foundation + +final class SettingsStore { + static let shared = SettingsStore() + + private let userDefaults: UserDefaults + private let appGroupDefaults: UserDefaults? + + private init() { + // Main app defaults + userDefaults = UserDefaults.standard + + // App Group defaults for widget/shared content + if let groupID = Bundle.main.object(forInfoDictionaryKey: "AppGroupID") as? String { + appGroupDefaults = UserDefaults(suiteName: groupID) + } else { + appGroupDefaults = nil + } + } + + // MARK: - App Settings + + private enum AppSettingsKey: String { + case appVersion = "appVersion" + case buildNumber = "buildNumber" + case lastMigrationVersion = "lastMigrationVersion" + case firstLaunchAt = "firstLaunchAt" + case lastLaunchAt = "lastLaunchAt" + case launchCount = "launchCount" + } + + func getAppSettings() -> AppSettings { + guard let data = userDefaults.data(forKey: AppSettingsKey.appVersion.rawValue), + let settings = try? JSONDecoder().decode(AppSettings.self, from: data) else { + return AppSettings() + } + return settings + } + + func save(_ settings: AppSettings) { + do { + let data = try JSONEncoder().encode(settings) + userDefaults.set(data, forKey: AppSettingsKey.appVersion.rawValue) + } catch { + print("Failed to save app settings: \(error)") + } + } + + // MARK: - User Preferences + + private enum UserPreferencesKey: String { + case reading = "readingPreferences" + case notification = "notificationPreferences" + } + + func getReadingPreferences() -> ReadingPreferences { + guard let data = userDefaults.data(forKey: UserPreferencesKey.reading), + let prefs = try? JSONDecoder().decode(ReadingPreferences.self, from: data) else { + return ReadingPreferences() + } + return prefs + } + + func save(_ reading: ReadingPreferences) { + do { + let data = try JSONEncoder().encode(reading) + userDefaults.set(data, forKey: UserPreferencesKey.reading) + } catch { + print("Failed to save reading preferences: \(error)") + } + } + + func getNotificationPreferences() -> NotificationPreferences { + guard let data = userDefaults.data(forKey: UserPreferencesKey.notification), + let prefs = try? JSONDecoder().decode(NotificationPreferences.self, from: data) else { + return NotificationPreferences() + } + return prefs + } + + func save(_ notification: NotificationPreferences) { + do { + let data = try JSONEncoder().encode(notification) + userDefaults.set(data, forKey: UserPreferencesKey.notification) + } catch { + print("Failed to save notification preferences: \(error)") + } + } + + func getUserPreferences() -> UserPreferences { + UserPreferences( + reading: getReadingPreferences(), + notification: getNotificationPreferences() + ) + } + + func save(_ preferences: UserPreferences) { + save(preferences.reading) + save(preferences.notification) + } + + // MARK: - App Group Sync + + func syncToAppGroup() { + guard let groupDefaults = appGroupDefaults else { return } + + if let appSettingsData = userDefaults.data(forKey: AppSettingsKey.appVersion.rawValue) { + groupDefaults.set(appSettingsData, forKey: AppSettingsKey.appVersion.rawValue) + } + + if let readingData = userDefaults.data(forKey: UserPreferencesKey.reading) { + groupDefaults.set(readingData, forKey: UserPreferencesKey.reading) + } + + if let notificationData = userDefaults.data(forKey: UserPreferencesKey.notification) { + groupDefaults.set(notificationData, forKey: UserPreferencesKey.notification) + } + } + + func syncFromAppGroup() { + guard let groupDefaults = appGroupDefaults else { return } + + if let appSettingsData = groupDefaults.data(forKey: AppSettingsKey.appVersion.rawValue) { + userDefaults.set(appSettingsData, forKey: AppSettingsKey.appVersion.rawValue) + } + + if let readingData = groupDefaults.data(forKey: UserPreferencesKey.reading) { + userDefaults.set(readingData, forKey: UserPreferencesKey.reading) + } + + if let notificationData = groupDefaults.data(forKey: UserPreferencesKey.notification) { + userDefaults.set(notificationData, forKey: UserPreferencesKey.notification) + } + } + + // MARK: - Notifications + + private let settingsChangedNotification = NotificationCenter.default + + func startObservingSettingsChanges() { + settingsChangedNotification.addObserver( + forName: UserDefaults.didChangeNotification, + object: userDefaults, + queue: .main + ) { [weak self] _ in + self?.syncToAppGroup() + } + } + + func stopObservingSettingsChanges() { + settingsChangedNotification.removeObserver(self) + } +} diff --git a/iOS/RSSuper/Settings/UserPreferences.swift b/iOS/RSSuper/Settings/UserPreferences.swift new file mode 100644 index 0000000..1cbf574 --- /dev/null +++ b/iOS/RSSuper/Settings/UserPreferences.swift @@ -0,0 +1,32 @@ +// +// UserPreferences.swift +// RSSuper +// +// User preferences keys and defaults +// + +import Foundation + +struct UserPreferences: Codable, Equatable { + var reading: ReadingPreferences + var notification: NotificationPreferences + + init( + reading: ReadingPreferences = ReadingPreferences(), + notification: NotificationPreferences = NotificationPreferences() + ) { + self.reading = reading + self.notification = notification + } + + static let `default': UserPreferences = UserPreferences() + + var debugDescription: String { + """ + UserPreferences( + reading: \(reading.debugDescription), + notification: \(notification.debugDescription) + ) + """ + } +} diff --git a/native-route/linux/gsettings/org.rssuper.notification.preferences.gschema.xml b/native-route/linux/gsettings/org.rssuper.notification.preferences.gschema.xml new file mode 100644 index 0000000..f13eaef --- /dev/null +++ b/native-route/linux/gsettings/org.rssuper.notification.preferences.gschema.xml @@ -0,0 +1,74 @@ + + + rssuper + + + + + + + + + + + + + + + + + + + + + + + + New Article Notifications + true + Enable notifications for new articles + + + + Episode Release Notifications + true + Enable notifications for episode releases + + + + Custom Alert Notifications + true + Enable notifications for custom alerts + + + + Badge Count + true + Show badge count in app header + + + + Sound + true + Play sound on notification + + + + Vibration + true + Vibrate device on notification + + + + All Preferences + { + "newArticles": true, + "episodeReleases": true, + "customAlerts": true, + "badgeCount": true, + "sound": true, + "vibration": true + } + All notification preferences as JSON + + \ No newline at end of file diff --git a/native-route/linux/src/notification-manager.vala b/native-route/linux/src/notification-manager.vala new file mode 100644 index 0000000..906a2f8 --- /dev/null +++ b/native-route/linux/src/notification-manager.vala @@ -0,0 +1,373 @@ +/* + * notification-manager.vala + * + * Notification manager for RSSuper on Linux. + * Coordinates notifications, badge management, and tray integration. + */ + +using Gio; +using GLib; +using Gtk; + +namespace RSSuper { + +/** + * NotificationManager - Manager for coordinating notifications + */ +public class NotificationManager : Object { + + // Singleton instance + private static NotificationManager? _instance; + + // Notification service + private NotificationService? _notification_service; + + // Badge reference + private Gtk.Badge? _badge; + + // Tray icon reference + private Gtk.TrayIcon? _tray_icon; + + // App reference + private Gtk.App? _app; + + // Current unread count + private int _unread_count = 0; + + // Badge visibility + private bool _badge_visible = true; + + /** + * Get singleton instance + */ + public static NotificationManager? get_instance() { + if (_instance == null) { + _instance = new NotificationManager(); + } + return _instance; + } + + /** + * Get the instance + */ + private NotificationManager() { + _notification_service = NotificationService.get_instance(); + _app = Gtk.App.get_active(); + } + + /** + * Initialize the notification manager + */ + public void initialize() { + // Set up badge + _badge = Gtk.Badge.new(); + _badge.set_visible(_badge_visible); + _badge.set_halign(Gtk.Align.START); + + // Connect badge changed signal + _badge.changed.connect(_on_badge_changed); + + // Set up tray icon + _tray_icon = Gtk.TrayIcon.new(); + _tray_icon.set_icon_name("rssuper"); + _tray_icon.set_tooltip_text("RSSuper - Press for notifications"); + + // Connect tray icon clicked signal + _tray_icon.clicked.connect(_on_tray_clicked); + + // Set up tray icon popup menu + var popup = new PopupMenu(); + popup.add_item(new Gtk.Label("Notifications: " + _unread_count.toString())); + popup.add_item(new Gtk.Separator()); + popup.add_item(new Gtk.Label("Mark all as read")); + popup.add_item(new Gtk.Separator()); + popup.add_item(new Gtk.Label("Settings")); + popup.add_item(new Gtk.Label("Exit")); + popup.connect_closed(_on_tray_closed); + + _tray_icon.set_popup(popup); + + // Connect tray icon popup menu signal + popup.menu_closed.connect(_on_tray_popup_closed); + + // Set up tray icon popup handler + _tray_icon.set_popup_handler(_on_tray_popup); + + // Set up tray icon tooltip + _tray_icon.set_tooltip_text("RSSuper - Press for notifications"); + } + + /** + * Set up the badge in the app header + */ + public void set_up_badge() { + _badge.set_visible(_badge_visible); + _badge.set_halign(Gtk.Align.START); + + // Set up badge changed signal + _badge.changed.connect(_on_badge_changed); + } + + /** + * Set up the tray icon + */ + public void set_up_tray_icon() { + _tray_icon.set_icon_name("rssuper"); + _tray_icon.set_tooltip_text("RSSuper - Press for notifications"); + + // Connect tray icon clicked signal + _tray_icon.clicked.connect(_on_tray_clicked); + + // Set up tray icon popup menu + var popup = new PopupMenu(); + popup.add_item(new Gtk.Label("Notifications: " + _unread_count.toString())); + popup.add_item(new Gtk.Separator()); + popup.add_item(new Gtk.Label("Mark all as read")); + popup.add_item(new Gtk.Separator()); + popup.add_item(new Gtk.Label("Settings")); + popup.add_item(new Gtk.Label("Exit")); + popup.connect_closed(_on_tray_closed); + + _tray_icon.set_popup(popup); + + // Connect tray icon popup menu signal + popup.menu_closed.connect(_on_tray_popup_closed); + + // Set up tray icon popup handler + _tray_icon.set_popup_handler(_on_tray_popup); + + // Set up tray icon tooltip + _tray_icon.set_tooltip_text("RSSuper - Press for notifications"); + } + + /** + * Show badge + */ + public void show_badge() { + _badge.set_visible(_badge_visible); + } + + /** + * Hide badge + */ + public void hide_badge() { + _badge.set_visible(false); + } + + /** + * Show badge with count + */ + public void show_badge_with_count(int count) { + _badge.set_visible(_badge_visible); + _badge.set_label(count.toString()); + } + + /** + * Set unread count + */ + public void set_unread_count(int count) { + _unread_count = count; + + // Update badge + if (_badge != null) { + _badge.set_label(count.toString()); + } + + // Update tray icon popup + if (_tray_icon != null) { + var popup = _tray_icon.get_popup(); + if (popup != null) { + popup.set_label("Notifications: " + count.toString()); + } + } + + // Show badge if count > 0 + if (count > 0) { + show_badge(); + } + } + + /** + * Clear unread count + */ + public void clear_unread_count() { + _unread_count = 0; + hide_badge(); + + // Update tray icon popup + if (_tray_icon != null) { + var popup = _tray_icon.get_popup(); + if (popup != null) { + popup.set_label("Notifications: 0"); + } + } + } + + /** + * Get unread count + */ + public int get_unread_count() { + return _unread_count; + } + + /** + * Get badge reference + */ + public Gtk.Badge? get_badge() { + return _badge; + } + + /** + * Get tray icon reference + */ + public Gtk.TrayIcon? get_tray_icon() { + return _tray_icon; + } + + /** + * Get app reference + */ + public Gtk.App? get_app() { + return _app; + } + + /** + * Check if badge should be visible + */ + public bool should_show_badge() { + return _unread_count > 0 && _badge_visible; + } + + /** + * Set badge visibility + */ + public void set_badge_visibility(bool visible) { + _badge_visible = visible; + + if (_badge != null) { + _badge.set_visible(visible); + } + } + + /** + * Show notification with badge + */ + public void show_with_badge(string title, string body, + string icon = null, + Urgency urgency = Urgency.NORMAL) { + + var notification = _notification_service.create(title, body, icon, urgency); + notification.show_with_timeout(5000); + + // Show badge + if (_unread_count == 0) { + show_badge_with_count(1); + } + } + + /** + * Show notification without badge + */ + public void show_without_badge(string title, string body, + string icon = null, + Urgency urgency = Urgency.NORMAL) { + + var notification = _notification_service.create(title, body, icon, urgency); + notification.show_with_timeout(5000); + } + + /** + * Show critical notification + */ + public void show_critical(string title, string body, + string icon = null) { + show_with_badge(title, body, icon, Urgency.CRITICAL); + } + + /** + * Show low priority notification + */ + public void show_low(string title, string body, + string icon = null) { + show_with_badge(title, body, icon, Urgency.LOW); + } + + /** + * Show normal notification + */ + public void show_normal(string title, string body, + string icon = null) { + show_with_badge(title, body, icon, Urgency.NORMAL); + } + + /** + * Handle badge changed signal + */ + private void _on_badge_changed(Gtk.Badge badge) { + var count = badge.get_label(); + if (!string.IsNullOrEmpty(count)) { + _unread_count = int.Parse(count); + } + } + + /** + * Handle tray icon clicked signal + */ + private void _on_tray_clicked(Gtk.TrayIcon tray) { + show_notifications_panel(); + } + + /** + * Handle tray icon popup closed signal + */ + private void _on_tray_popup_closed(Gtk.Popup popup) { + // Popup closed, hide icon + if (_tray_icon != null) { + _tray_icon.hide(); + } + } + + /** + * Handle tray icon popup open signal + */ + private void _on_tray_popup(Gtk.TrayIcon tray, Gtk.MenuItem menu) { + // Show icon when popup is opened + if (_tray_icon != null) { + _tray_icon.show(); + } + } + + /** + * Handle tray icon closed signal + */ + private void _on_tray_closed(Gtk.App app) { + // App closed, hide tray icon + if (_tray_icon != null) { + _tray_icon.hide(); + } + } + + /** + * Show notifications panel + */ + private void show_notifications_panel() { + // TODO: Show notifications panel + print("Notifications panel requested"); + } + + /** + * Get notification service + */ + public NotificationService? get_notification_service() { + return _notification_service; + } + + /** + * Check if notification manager is available + */ + public bool is_available() { + return _notification_service != null && _notification_service.is_available(); + } +} + +} \ No newline at end of file diff --git a/native-route/linux/src/notification-preferences-store.vala b/native-route/linux/src/notification-preferences-store.vala new file mode 100644 index 0000000..ffecbde --- /dev/null +++ b/native-route/linux/src/notification-preferences-store.vala @@ -0,0 +1,258 @@ +/* + * notification-preferences-store.vala + * + * Store for notification preferences. + * Provides persistent storage for user notification settings. + */ + +using GLib; + +namespace RSSuper { + +/** + * NotificationPreferencesStore - Persistent storage for notification preferences + * + * Uses GSettings for persistent storage following freedesktop.org conventions. + */ +public class NotificationPreferencesStore : Object { + + // Singleton instance + private static NotificationPreferencesStore? _instance; + + // GSettings schema key + private const string SCHEMA_KEY = "org.rssuper.notification.preferences"; + + // Preferences schema + private GSettings? _settings; + + // Preferences object + private NotificationPreferences? _preferences; + + /** + * Get singleton instance + */ + public static NotificationPreferencesStore? get_instance() { + if (_instance == null) { + _instance = new NotificationPreferencesStore(); + } + return _instance; + } + + /** + * Get the instance + */ + private NotificationPreferencesStore() { + _settings = GSettings.new(SCHEMA_KEY); + + // Load initial preferences + _preferences = NotificationPreferences.from_json_string(_settings.get_string("preferences")); + + if (_preferences == null) { + // Set default preferences if none exist + _preferences = new NotificationPreferences(); + _settings.set_string("preferences", _preferences.to_json_string()); + } + + // Listen for settings changes + _settings.changed.connect(_on_settings_changed); + } + + /** + * Get notification preferences + */ + public NotificationPreferences? get_preferences() { + return _preferences; + } + + /** + * Set notification preferences + */ + public void set_preferences(NotificationPreferences prefs) { + _preferences = prefs; + + // Save to GSettings + _settings.set_string("preferences", prefs.to_json_string()); + } + + /** + * Get new articles preference + */ + public bool get_new_articles() { + return _preferences != null ? _preferences.new_articles : true; + } + + /** + * Set new articles preference + */ + public void set_new_articles(bool enabled) { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.new_articles = enabled; + _settings.set_boolean("newArticles", enabled); + } + + /** + * Get episode releases preference + */ + public bool get_episode_releases() { + return _preferences != null ? _preferences.episode_releases : true; + } + + /** + * Set episode releases preference + */ + public void set_episode_releases(bool enabled) { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.episode_releases = enabled; + _settings.set_boolean("episodeReleases", enabled); + } + + /** + * Get custom alerts preference + */ + public bool get_custom_alerts() { + return _preferences != null ? _preferences.custom_alerts : true; + } + + /** + * Set custom alerts preference + */ + public void set_custom_alerts(bool enabled) { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.custom_alerts = enabled; + _settings.set_boolean("customAlerts", enabled); + } + + /** + * Get badge count preference + */ + public bool get_badge_count() { + return _preferences != null ? _preferences.badge_count : true; + } + + /** + * Set badge count preference + */ + public void set_badge_count(bool enabled) { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.badge_count = enabled; + _settings.set_boolean("badgeCount", enabled); + } + + /** + * Get sound preference + */ + public bool get_sound() { + return _preferences != null ? _preferences.sound : true; + } + + /** + * Set sound preference + */ + public void set_sound(bool enabled) { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.sound = enabled; + _settings.set_boolean("sound", enabled); + } + + /** + * Get vibration preference + */ + public bool get_vibration() { + return _preferences != null ? _preferences.vibration : true; + } + + /** + * Set vibration preference + */ + public void set_vibration(bool enabled) { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.vibration = enabled; + _settings.set_boolean("vibration", enabled); + } + + /** + * Enable all notifications + */ + public void enable_all() { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.enable_all(); + + // Save to GSettings + _settings.set_string("preferences", _preferences.to_json_string()); + } + + /** + * Disable all notifications + */ + public void disable_all() { + _preferences = _preferences ?? new NotificationPreferences(); + _preferences.disable_all(); + + // Save to GSettings + _settings.set_string("preferences", _preferences.to_json_string()); + } + + /** + * Get all preferences as dictionary + */ + public Dictionary get_all_preferences() { + if (_preferences == null) { + return new Dictionary(); + } + + var prefs = new Dictionary(); + prefs["new_articles"] = _preferences.new_articles; + prefs["episode_releases"] = _preferences.episode_releases; + prefs["custom_alerts"] = _preferences.custom_alerts; + prefs["badge_count"] = _preferences.badge_count; + prefs["sound"] = _preferences.sound; + prefs["vibration"] = _preferences.vibration; + + return prefs; + } + + /** + * Set all preferences from dictionary + */ + public void set_all_preferences(Dictionary prefs) { + _preferences = new NotificationPreferences(); + + if (prefs.containsKey("new_articles")) { + _preferences.new_articles = prefs["new_articles"] as bool; + } + if (prefs.containsKey("episode_releases")) { + _preferences.episode_releases = prefs["episode_releases"] as bool; + } + if (prefs.containsKey("custom_alerts")) { + _preferences.custom_alerts = prefs["custom_alerts"] as bool; + } + if (prefs.containsKey("badge_count")) { + _preferences.badge_count = prefs["badge_count"] as bool; + } + if (prefs.containsKey("sound")) { + _preferences.sound = prefs["sound"] as bool; + } + if (prefs.containsKey("vibration")) { + _preferences.vibration = prefs["vibration"] as bool; + } + + // Save to GSettings + _settings.set_string("preferences", _preferences.to_json_string()); + } + + /** + * Handle settings changed signal + */ + private void _on_settings_changed(GSettings settings) { + // Settings changed, reload preferences + _preferences = NotificationPreferences.from_json_string(settings.get_string("preferences")); + + if (_preferences == null) { + // Set defaults on error + _preferences = new NotificationPreferences(); + settings.set_string("preferences", _preferences.to_json_string()); + } + } +} + +} \ No newline at end of file diff --git a/native-route/linux/src/notification-service.vala b/native-route/linux/src/notification-service.vala new file mode 100644 index 0000000..e23ad2c --- /dev/null +++ b/native-route/linux/src/notification-service.vala @@ -0,0 +1,232 @@ +/* + * notification-service.vala + * + * Main notification service for RSSuper on Linux. + * Implements Gio.Notification API following freedesktop.org spec. + */ + +using Gio; +using GLib; + +namespace RSSuper { + +/** + * NotificationService - Main notification service for Linux + * + * Handles desktop notifications using Gio.Notification. + * Follows freedesktop.org notify-send specification. + */ +public class NotificationService : Object { + + // Singleton instance + private static NotificationService? _instance; + + // Gio.Notification instance + private Gio.Notification? _notification; + + // Tray icon reference + private Gtk.App? _app; + + // Default title + private string _default_title = "RSSuper"; + + // Default urgency + private Urgency _default_urgency = Urgency.NORMAL; + + /** + * Get singleton instance + */ + public static NotificationService? get_instance() { + if (_instance == null) { + _instance = new NotificationService(); + } + return _instance; + } + + /** + * Get the instance (for singleton pattern) + */ + private NotificationService() { + _app = Gtk.App.get_active(); + _default_title = _app != null ? _app.get_name() : "RSSuper"; + _default_urgency = Urgency.NORMAL; + } + + /** + * Check if notification service is available + */ + public bool is_available() { + return Gio.Notification.is_available(); + } + + /** + * Create a new notification + * + * @param title The notification title + * @param body The notification body + * @param urgency Urgency level (NORMAL, CRITICAL, LOW) + * @param timestamp Optional timestamp (defaults to now) + */ + public Notification create(string title, string body, + Urgency urgency = Urgency.NORMAL, + DateTime timestamp = null) { + + _notification = Gio.Notification.new(_default_title); + _notification.set_body(body); + _notification.set_urgency(urgency); + + if (timestamp == null) { + _notification.set_time_now(); + } else { + _notification.set_time(timestamp); + } + + return _notification; + } + + /** + * Create a notification with summary and icon + */ + public Notification create(string title, string body, string icon, + Urgency urgency = Urgency.NORMAL, + DateTime timestamp = null) { + + _notification = Gio.Notification.new(title); + _notification.set_body(body); + _notification.set_urgency(urgency); + + if (timestamp == null) { + _notification.set_time_now(); + } else { + _notification.set_time(timestamp); + } + + // Set icon + try { + _notification.set_icon(icon); + } catch (Error e) { + warning("Failed to set icon: %s", e.message); + } + + return _notification; + } + + /** + * Create a notification with summary, body, and icon + */ + public Notification create(string summary, string body, string icon, + Urgency urgency = Urgency.NORMAL, + DateTime timestamp = null) { + + _notification = Gio.Notification.new(summary); + _notification.set_body(body); + _notification.set_urgency(urgency); + + if (timestamp == null) { + _notification.set_time_now(); + } else { + _notification.set_time(timestamp); + } + + // Set icon + try { + _notification.set_icon(icon); + } catch (Error e) { + warning("Failed to set icon: %s", e.message); + } + + return _notification; + } + + /** + * Show the notification + */ + public void show() { + if (_notification == null) { + warning("Cannot show null notification"); + return; + } + + try { + _notification.show(); + } catch (Error e) { + warning("Failed to show notification: %s", e.message); + } + } + + /** + * Show the notification with timeout + * + * @param timeout_seconds Timeout in seconds (default: 5) + */ + public void show_with_timeout(int timeout_seconds = 5) { + if (_notification == null) { + warning("Cannot show null notification"); + return; + } + + try { + _notification.show_with_timeout(timeout_seconds * 1000); + } catch (Error e) { + warning("Failed to show notification with timeout: %s", e.message); + } + } + + /** + * Get the notification instance + */ + public Gio.Notification? get_notification() { + return _notification; + } + + /** + * Set the default title + */ + public void set_default_title(string title) { + _default_title = title; + } + + /** + * Set the default urgency + */ + public void set_default_urgency(Urgency urgency) { + _default_urgency = urgency; + } + + /** + * Get the default title + */ + public string get_default_title() { + return _default_title; + } + + /** + * Get the default urgency + */ + public Urgency get_default_urgency() { + return _default_urgency; + } + + /** + * Get the app reference + */ + public Gtk.App? get_app() { + return _app; + } + + /** + * Check if the notification can be shown + */ + public bool can_show() { + return _notification != null && _notification.can_show(); + } + + /** + * Get available urgency levels + */ + public static List get_available_urgencies() { + return Urgency.get_available(); + } +} + +} \ No newline at end of file