fix readme repo diagram, add agents.md
Some checks failed
CI - Multi-Platform Native / Build iOS (RSSuper) (push) Has been cancelled
CI - Multi-Platform Native / Build macOS (push) Has been cancelled
CI - Multi-Platform Native / Build Android (push) Has been cancelled
CI - Multi-Platform Native / Build Linux (push) Has been cancelled
CI - Multi-Platform Native / Integration Tests (push) Has been cancelled
CI - Multi-Platform Native / Build Summary (push) Has been cancelled

This commit is contained in:
2026-03-31 12:54:01 -04:00
parent 6a7efebdfc
commit b79e6e7aa2
7 changed files with 629 additions and 9 deletions

175
AGENTS.md Normal file
View File

@@ -0,0 +1,175 @@
# AGENTS.md - RSSuper Development Guide
This file provides guidelines for AI agents working on the RSSuper codebase.
## Project Overview
RSSuper is a native multi-platform RSS reader:
- **iOS/macOS**: Swift + SwiftUI + Xcode
- **Android**: Kotlin + Jetpack Compose + Gradle
- **Linux**: Vala + GTK4 + Libadwaita + Meson
- **Windows**: Planned (C# + WinUI3)
## Build Commands
### Main Build Script (all platforms)
```bash
./scripts/build.sh # Build all platforms (debug)
./scripts/build.sh -p ios,android # Build specific platforms
./scripts/build.sh -t release # Release build
./scripts/build.sh --test # Build and test
./scripts/build.sh -a clean # Clean all
```
### iOS/macOS
```bash
./scripts/build-ios.sh # Debug build
./scripts/build-ios.sh Release iOS build # Release build
./scripts/build-ios.sh Debug iOS test # Run tests
./scripts/build-ios.sh Debug iOS clean # Clean
# Single test file (in Xcode)
xcodebuild test -scheme RSSuper -destination 'platform=iOS Simulator,name=iPhone 15' \
-only-testing:RSSuperTests/SearchQueryTests
```
### Android
```bash
cd android && ./gradlew assembleDebug # Debug APK
cd android && ./gradlew assembleRelease # Release APK
cd android && ./gradlew test # Run all unit tests
cd android && ./gradlew test --tests "com.rssuper.search.SearchQueryTest" # Single test class
cd android && ./gradlew clean # Clean
# Single test via Gradle
cd android && ./gradlew test --tests "com.rssuper.search.SearchQueryTest.testParse*"
```
### Linux
```bash
./scripts/build-linux.sh debug build # Debug build
./scripts/build-linux.sh release test # Run tests
./scripts/build-linux.sh debug clean # Clean
```
## Code Style Guidelines
### General Principles
- Write clean, readable code with minimal comments (only when logic is complex)
- Follow existing patterns in each platform's codebase
- Use dependency injection for testability
- Prefer explicit over implicit
### Kotlin (Android)
- **Formatting**: 4 spaces indentation, 140 char line limit
- **Naming**: `camelCase` for functions/variables, `PascalCase` for classes
- **Imports**: Grouped: standard library → Android → third-party → project
- **Types**: Use Kotlin null safety, prefer `val` over `var`
- **Error Handling**: Use `Result<T>` or sealed classes for errors (see `SearchQuery.kt`)
- **Coroutines**: Use `viewModelScope` in ViewModels, structured concurrency
- **Tests**: JUnit 4 with Mockito, Robolectric for Android-specific tests
### Swift (iOS/macOS)
- **Formatting**: 4 spaces, 120 char line limit
- **Naming**: `camelCase`, `PascalCase` for types/protocols
- **Imports**: Foundation → SwiftUI/AppKit → third-party → project
- **Error Handling**: Use `Result<T, Error>` and `async/await`
- **Protocols**: Use protocols for dependency injection (see `FeedServiceProtocol`)
- **Tests**: XCTest, arrange/act/assert pattern
### Vala (Linux)
- **Formatting**: 4 spaces indentation
- **Naming**: `snake_case` for methods/variables, `PascalCase` for classes
- **Memory**: Use GLib's reference counting (`ref`/`unref`)
- **Error Handling**: Use `GLib.Error` and `try/catch`
### Android-specific
- **ViewModels**: Use `androidx.lifecycle.ViewModel` with `StateFlow`
- **Database**: Room with Kotlin coroutines
- **Background Work**: WorkManager for sync tasks
### iOS-specific
- **State Management**: `@Observable` macro (iOS 17+) or `ObservableObject`
- **Persistence**: SQLite via SQLite.swift
- **Background**: BGTaskScheduler for background refresh
## Architecture Patterns
### Clean Architecture Layers
1. **UI Layer**: SwiftUI Views / Jetpack Compose
2. **ViewModel/ViewModel**: Business logic, state management
3. **Service Layer**: `FeedService`, `SearchService`, `BookmarkStore`
4. **Data Layer**: Database managers, network clients
### Dependency Injection
- Android: Manual DI in constructors (no framework)
- iOS: Protocol-based DI with default implementations
## Testing Guidelines
### Unit Tests Location
- **Android**: `android/src/test/java/com/rssuper/`
- **iOS**: `iOS/RSSuperTests/`
- **Linux**: Test files alongside source in `linux/src/`
### Test Naming Convention
- `MethodName_State_ExpectedResult` (e.g., `SearchQueryTest.parseEmptyQueryReturnsNull`)
### Test Data
- Fixtures in `tests/fixtures/` (sample RSS/Atom feeds)
## File Organization
```
RSSuper/
├── android/ # Kotlin Android library
│ ├── src/main/java/com/rssuper/
│ │ ├── model/ # Data models
│ │ ├── parsing/ # Feed parsers
│ │ ├── state/ # State management
│ │ ├── viewmodel/ # ViewModels
│ │ ├── search/ # Search service
│ │ └── database/ # Room database
│ └── src/test/ # Unit tests
├── iOS/RSSuper/ # Swift iOS app
│ ├── Models/ # Data models
│ ├── Parsing/ # Feed parsers
│ ├── Services/ # Business logic
│ ├── ViewModels/ # ViewModels
│ ├── Networking/ # HTTP client
│ ├── Settings/ # User preferences
│ └── Database/ # SQLite layer
├── iOS/RSSuperTests/ # Unit tests
├── linux/ # Vala GTK app
│ └── src/ # Source + tests
├── scripts/ # Build scripts
└── tests/fixtures/ # Test data
```
## Common Tasks
### Running a Single Android Test
```bash
cd android
./gradlew test --tests "com.rssuper.search.SearchQueryTest"
```
### Running a Single iOS Test
```bash
./scripts/build-ios.sh Debug iOS test
# Or in Xcode: Product > Run Tests (select specific test)
```
### Adding a New Platform Module
1. Create directory under `native-route/`
2. Add build command to `./scripts/build.sh`
3. Add test configuration to CI workflow
4. Update this AGENTS.md
## Important Notes
- The Android project uses a library module structure (not application)
- iOS minimum deployment: iOS 16.0+
- Android minimum SDK: 24 (Android 7.0)
- Linux requires GTK4, Libadwaita, SQLite3 development headers
- All platform modules share similar business logic patterns

View File

@@ -39,11 +39,10 @@ RSSuper uses a native-first approach, building truly native applications for eac
```
RSSuper/
├── native-route/ # Native platform projects
│ ├── ios/ # iOS/macOS Xcode project
│ ├── android/ # Android Gradle project
│ ├── linux/ # Linux Meson project
│ └── windows/ # Windows project (planned)
ios/ # iOS/macOS Xcode project
android/ # Android Gradle project
linux/ # Linux Meson project
windows/ # Windows project (planned)
├── scripts/ # Build scripts
│ ├── build.sh # Main build orchestrator
│ ├── build-ios.sh # iOS/macOS builder

View File

@@ -329,8 +329,10 @@ class FeedIntegrationTest {
val fetchResult = feedFetcher.fetch(feedUrl)
assertTrue("Fetch should succeed", fetchResult.isSuccess())
assertThrows<Exception> {
try {
feedParser.parse(fetchResult.getOrNull()!!.feedXml, feedUrl)
fail("Parsing invalid XML should throw exception")
} catch (e: Exception) {
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.rssuper.app.settings" path="/org/rssuper/app/settings/">
<key type="s" name="reading-prefs-file">
<default>'reading_preferences.json'</default>
<description>Reading preferences file name</description>
</key>
<key type="s" name="sync-prefs-file">
<default>'sync_preferences.json'</default>
<description>Sync preferences file name</description>
</key>
<key type="b" name="background-sync-enabled">
<default>false</default>
<description>Enable background sync</description>
</key>
<key type="i" name="sync-interval-minutes">
<default>15</default>
<description>Sync interval in minutes</description>
</key>
</schema>
</schemalist>

View File

@@ -20,6 +20,19 @@ xml_dep = dependency('libxml-2.0', version: '>= 2.0')
soup_dep = dependency('libsoup-3.0', version: '>= 3.0')
gtk_dep = dependency('gtk4', version: '>= 4.0')
# GSettings schemas
schema_files = [
'gsettings/org.rssuper.notification.preferences.gschema.xml',
'gsettings/org.rssuper.app.settings.gschema.xml',
]
compile_schemas = custom_target('Compile GSettings schemas',
output: 'schemas.glibdir',
command: glib_dep.get_variable(name: 'glib-mkenums') + ' --targetdir=build --schema-dir=gsettings schemas.glibdir',
dependencies: glib_dep,
install: false
)
# Source files
models = files(
'src/models/feed-item.vala',
@@ -32,6 +45,13 @@ models = files(
'src/models/bookmark.vala',
)
# Settings files
settings = files(
'src/settings-store.vala',
'src/app-settings.vala',
'src/notification-preferences-store.vala',
)
# Database files
database = files(
'src/database/db-error.vala',
@@ -76,6 +96,13 @@ models_lib = library('rssuper-models', models,
install: false
)
# Settings library
settings_lib = library('rssuper-settings', settings,
dependencies: [glib_dep, gio_dep, json_dep],
link_with: [models_lib],
install: false
)
# Database library
database_lib = library('rssuper-database', database,
dependencies: [glib_dep, gio_dep, json_dep, sqlite_dep, gobject_dep],
@@ -153,9 +180,19 @@ notification_manager_test_exe = executable('notification-manager-tests',
install: false
)
# Settings store test executable
settings_store_test_exe = executable('settings-store-tests',
'src/tests/settings-store-tests.vala',
dependencies: [glib_dep, gio_dep, json_dep],
link_with: [models_lib, settings_lib],
vala_args: ['--vapidir', '.', '--pkg', 'gio-2.0'],
install: false
)
# Test definitions
test('database tests', test_exe)
test('parser tests', parser_test_exe)
test('feed fetcher tests', fetcher_test_exe)
test('notification service tests', notification_service_test_exe)
test('notification manager tests', notification_manager_test_exe)
test('settings store tests', settings_store_test_exe)

162
linux/src/app-settings.vala Normal file
View File

@@ -0,0 +1,162 @@
/*
* app-settings.vala
*
* Application settings interface and utilities.
* Provides a unified interface for accessing application settings.
*/
using GLib;
namespace RSSuper {
/**
* AppSettings - Application settings interface
*
* Provides access to all application-level settings including:
* - Reading preferences
* - Sync preferences
* - Theme settings
* - Language settings
*/
public class AppSettings : Object {
// Singleton instance
private static AppSettings? _instance;
// Settings store
private SettingsStore? _settings_store;
// Theme
private Theme _theme;
// Language
private string _language;
// GSettings schema key
private const string SCHEMA_KEY = "org.rssuper.app.settings";
/**
* Get singleton instance
*/
public static AppSettings? get_instance() {
if (_instance == null) {
_instance = new AppSettings();
}
return _instance;
}
/**
* Constructor
*/
private AppSettings() {
_settings_store = SettingsStore.get_instance();
// Load theme
_theme = Theme.SYSTEM;
_language = "en";
}
/**
* Get reading preferences
*/
public ReadingPreferences? get_reading_preferences() {
return _settings_store.get_reading_preferences();
}
/**
* Set reading preferences
*/
public void set_reading_preferences(ReadingPreferences prefs) {
_settings_store.set_reading_preferences(prefs);
}
/**
* Get background sync enabled
*/
public bool get_background_sync_enabled() {
return _settings_store.get_background_sync_enabled();
}
/**
* Set background sync enabled
*/
public void set_background_sync_enabled(bool enabled) {
_settings_store.set_background_sync_enabled(enabled);
}
/**
* Get sync interval in minutes
*/
public int get_sync_interval_minutes() {
return _settings_store.get_sync_interval_minutes();
}
/**
* Set sync interval in minutes
*/
public void set_sync_interval_minutes(int minutes) {
_settings_store.set_sync_interval_minutes(minutes);
}
/**
* Get theme
*/
public Theme get_theme() {
return _theme;
}
/**
* Set theme
*/
public void set_theme(Theme theme) {
_theme = theme;
}
/**
* Get language
*/
public string get_language() {
return _language;
}
/**
* Set language
*/
public void set_language(string language) {
_language = language;
}
/**
* Get all settings as dictionary
*/
public Dictionary<string, object> get_all_settings() {
return _settings_store.get_all_settings();
}
/**
* Set all settings from dictionary
*/
public void set_all_settings(Dictionary<string, object> settings) {
_settings_store.set_all_settings(settings);
}
/**
* Reset all settings to defaults
*/
public void reset_to_defaults() {
_settings_store.reset_to_defaults();
_theme = Theme.SYSTEM;
_language = "en";
}
}
/**
* Theme - Available theme options
*/
public enum RSSuper.Theme {
SYSTEM,
LIGHT,
DARK
}
}

View File

@@ -0,0 +1,224 @@
/*
* settings-store-tests.vala
*
* Unit tests for settings store.
*/
using GLib;
namespace RSSuper {
public void main(string[] args) {
print("Running Settings Store Tests\n");
print("============================\n\n");
// Test ReadingPreferences
test_reading_preferences();
// Test SettingsStore
test_settings_store();
// Test AppSettings
test_app_settings();
print("\nAll tests passed!\n");
}
/**
* Test ReadingPreferences
*/
private void test_reading_preferences() {
print("Testing ReadingPreferences...\n");
// Test default constructor
var prefs = new ReadingPreferences();
assert(prefs.font_size == FontSize.MEDIUM);
assert(prefs.line_height == LineHeight.NORMAL);
assert(prefs.show_table_of_contents == true);
assert(prefs.show_reading_time == true);
assert(prefs.show_author == true);
assert(prefs.show_date == true);
print(" ✓ Default constructor sets correct defaults\n");
// Test with_values constructor
var prefs2 = new ReadingPreferences.with_values(
FontSize.LARGE,
LineHeight.RELAXED,
false,
false,
false,
false
);
assert(prefs2.font_size == FontSize.LARGE);
assert(prefs2.line_height == LineHeight.RELAXED);
assert(prefs2.show_table_of_contents == false);
assert(prefs2.show_reading_time == false);
assert(prefs2.show_author == false);
assert(prefs2.show_date == false);
print(" ✓ with_values constructor sets correct values\n");
// Test to_json_string
var json_str = prefs.to_json_string();
assert(json_str.contains("fontSize"));
assert(json_str.contains("lineHeight"));
assert(json_str.contains("showTableOfContents"));
print(" ✓ to_json_string produces valid JSON\n");
// Test from_json_string
var prefs3 = ReadingPreferences.from_json_string(json_str);
assert(prefs3 != null);
assert(prefs3.font_size == FontSize.MEDIUM);
print(" ✓ from_json_string parses JSON correctly\n");
// Test font_size_from_string
assert(ReadingPreferences.font_size_from_string("small") == FontSize.SMALL);
assert(ReadingPreferences.font_size_from_string("medium") == FontSize.MEDIUM);
assert(ReadingPreferences.font_size_from_string("large") == FontSize.LARGE);
assert(ReadingPreferences.font_size_from_string("xlarge") == FontSize.XLARGE);
assert(ReadingPreferences.font_size_from_string("invalid") == FontSize.MEDIUM);
print(" ✓ font_size_from_string parses correctly\n");
// Test line_height_from_string
assert(ReadingPreferences.line_height_from_string("normal") == LineHeight.NORMAL);
assert(ReadingPreferences.line_height_from_string("relaxed") == LineHeight.RELAXED);
assert(ReadingPreferences.line_height_from_string("loose") == LineHeight.LOOSE);
assert(ReadingPreferences.line_height_from_string("invalid") == LineHeight.NORMAL);
print(" ✓ line_height_from_string parses correctly\n");
// Test equals
assert(prefs.equals(prefs2) == false);
var prefs4 = new ReadingPreferences();
assert(prefs.equals(prefs4) == true);
print(" ✓ equals works correctly\n");
// Test copy_from
var prefs5 = new ReadingPreferences();
prefs5.copy_from(prefs2);
assert(prefs5.font_size == FontSize.LARGE);
assert(prefs5.line_height == LineHeight.RELAXED);
print(" ✓ copy_from copies values correctly\n");
// Test reset_to_defaults
prefs.font_size = FontSize.LARGE;
prefs.reset_to_defaults();
assert(prefs.font_size == FontSize.MEDIUM);
print(" ✓ reset_to_defaults resets to defaults\n");
print("ReadingPreferences tests passed!\n\n");
}
/**
* Test SettingsStore
*/
private void test_settings_store() {
print("Testing SettingsStore...\n");
// Test singleton pattern
var store1 = SettingsStore.get_instance();
var store2 = SettingsStore.get_instance();
assert(store1 == store2);
print(" ✓ Singleton pattern works correctly\n");
// Test reading preferences
var prefs = new ReadingPreferences.with_values(FontSize.LARGE);
store1.set_reading_preferences(prefs);
var retrieved_prefs = store1.get_reading_preferences();
assert(retrieved_prefs != null);
assert(retrieved_prefs.font_size == FontSize.LARGE);
print(" ✓ Reading preferences stored and retrieved correctly\n");
// Test background sync enabled
store1.set_background_sync_enabled(true);
assert(store1.get_background_sync_enabled() == true);
store1.set_background_sync_enabled(false);
assert(store1.get_background_sync_enabled() == false);
print(" ✓ Background sync enabled works correctly\n");
// Test sync interval
store1.set_sync_interval_minutes(30);
assert(store1.get_sync_interval_minutes() == 30);
store1.set_sync_interval_minutes(15);
assert(store1.get_sync_interval_minutes() == 15);
print(" ✓ Sync interval works correctly\n");
// Test get_all_settings
var all_settings = store1.get_all_settings();
assert(all_settings.containsKey("fontSize"));
assert(all_settings.containsKey("backgroundSyncEnabled"));
assert(all_settings.containsKey("syncIntervalMinutes"));
print(" ✓ get_all_settings returns correct keys\n");
// Test set_all_settings
var new_settings = new Dictionary<string, object>();
new_settings["fontSize"] = "large";
new_settings["backgroundSyncEnabled"] = true;
new_settings["syncIntervalMinutes"] = 60;
store1.set_all_settings(new_settings);
assert(store1.get_reading_preferences().font_size == FontSize.LARGE);
assert(store1.get_background_sync_enabled() == true);
assert(store1.get_sync_interval_minutes() == 60);
print(" ✓ set_all_settings sets all values correctly\n");
// Test reset_to_defaults
store1.reset_to_defaults();
assert(store1.get_reading_preferences().font_size == FontSize.MEDIUM);
assert(store1.get_background_sync_enabled() == false);
assert(store1.get_sync_interval_minutes() == 15);
print(" ✓ reset_to_defaults resets all values\n");
print("SettingsStore tests passed!\n\n");
}
/**
* Test AppSettings
*/
private void test_app_settings() {
print("Testing AppSettings...\n");
// Test singleton pattern
var settings1 = AppSettings.get_instance();
var settings2 = AppSettings.get_instance();
assert(settings1 == settings2);
print(" ✓ Singleton pattern works correctly\n");
// Test theme
settings1.set_theme(Theme.DARK);
assert(settings1.get_theme() == Theme.DARK);
settings1.set_theme(Theme.LIGHT);
assert(settings1.get_theme() == Theme.LIGHT);
settings1.set_theme(Theme.SYSTEM);
assert(settings1.get_theme() == Theme.SYSTEM);
print(" ✓ Theme works correctly\n");
// Test language
settings1.set_language("en");
assert(settings1.get_language() == "en");
settings1.set_language("es");
assert(settings1.get_language() == "es");
print(" ✓ Language works correctly\n");
// Test reading preferences through AppSettings
var prefs = new ReadingPreferences.with_values(FontSize.XLARGE);
settings1.set_reading_preferences(prefs);
var retrieved_prefs = settings1.get_reading_preferences();
assert(retrieved_prefs != null);
assert(retrieved_prefs.font_size == FontSize.XLARGE);
print(" ✓ Reading preferences through AppSettings works\n");
// Test sync settings through AppSettings
settings1.set_background_sync_enabled(true);
assert(settings1.get_background_sync_enabled() == true);
settings1.set_sync_interval_minutes(45);
assert(settings1.get_sync_interval_minutes() == 45);
print(" ✓ Sync settings through AppSettings works\n");
// Test reset_to_defaults
settings1.reset_to_defaults();
assert(settings1.get_theme() == Theme.SYSTEM);
assert(settings1.get_language() == "en");
print(" ✓ reset_to_defaults works correctly\n");
print("AppSettings tests passed!\n\n");
}
}