conflicting pathing
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 11:46:15 -04:00
parent ba1e2e96e7
commit 199c711dd4
23 changed files with 3439 additions and 378 deletions

View File

@@ -127,6 +127,7 @@ final class DatabaseManager {
subscription_id TEXT NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE,
subscription_title TEXT,
read INTEGER NOT NULL DEFAULT 0,
starred INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
)
"""
@@ -463,25 +464,48 @@ extension DatabaseManager {
return executeQuery(sql: selectSQL, bindParams: [limit], rowMapper: rowToFeedItem)
}
func updateFeedItem(_ item: FeedItem, read: Bool? = nil) throws -> FeedItem {
guard let read = read else { return item }
func updateFeedItem(itemId: String, read: Bool? = nil, starred: Bool? = nil) throws -> FeedItem {
var sqlParts: [String] = []
var bindings: [Any] = []
let updateSQL = "UPDATE feed_items SET read = ? WHERE id = ?"
if let read = read {
sqlParts.append("read = ?")
bindings.append(read ? 1 : 0)
}
if let starred = starred {
sqlParts.append("starred = ?")
bindings.append(starred ? 1 : 0)
}
guard !sqlParts.isEmpty else {
throw DatabaseError.saveFailed(DatabaseError.objectNotFound)
}
let updateSQL = "UPDATE feed_items SET \(sqlParts.joined(separator: ", ")) WHERE id = ?"
bindings.append(itemId)
guard let statement = prepareStatement(sql: updateSQL) else {
throw DatabaseError.saveFailed(DatabaseError.objectNotFound)
}
defer { sqlite3_finalize(statement) }
sqlite3_bind_int(statement, 1, read ? 1 : 0)
sqlite3_bind_text(statement, 2, (item.id as NSString).utf8String, -1, nil)
for (index, binding) in bindings.enumerated() {
if let value = binding as? Int {
sqlite3_bind_int(statement, Int32(index + 1), value)
} else if let value = binding as? String {
sqlite3_bind_text(statement, Int32(index + 1), (value as NSString).utf8String, -1, nil)
}
}
if sqlite3_step(statement) != SQLITE_DONE {
throw DatabaseError.saveFailed(DatabaseError.objectNotFound)
}
var updatedItem = item
updatedItem.read = read
if let read = read { updatedItem.read = read }
if let starred = starred { updatedItem.starred = starred }
return updatedItem
}
@@ -742,28 +766,15 @@ extension DatabaseManager {
}
func markItemAsRead(itemId: String) throws {
guard let item = try fetchFeedItem(id: itemId) else {
throw DatabaseError.objectNotFound
}
_ = try updateFeedItem(item, read: true)
_ = try updateFeedItem(itemId, read: true)
}
func markItemAsStarred(itemId: String) throws {
guard let item = try fetchFeedItem(id: itemId) else {
throw DatabaseError.objectNotFound
}
var updatedItem = item
updatedItem.starred = true
_ = try updateFeedItem(updatedItem, read: nil)
_ = try updateFeedItem(itemId, read: nil, starred: true)
}
func unstarItem(itemId: String) throws {
guard let item = try fetchFeedItem(id: itemId) else {
throw DatabaseError.objectNotFound
}
var updatedItem = item
updatedItem.starred = false
_ = try updateFeedItem(updatedItem, read: nil)
_ = try updateFeedItem(itemId, read: nil, starred: false)
}
func getStarredItems() throws -> [FeedItem] {

View File

@@ -22,6 +22,7 @@ struct FeedItem: Identifiable, Codable, Equatable {
var subscriptionId: String
var subscriptionTitle: String?
var read: Bool = false
var starred: Bool = false
enum CodingKeys: String, CodingKey {
case id
@@ -38,6 +39,7 @@ struct FeedItem: Identifiable, Codable, Equatable {
case subscriptionId = "subscription_id"
case subscriptionTitle = "subscription_title"
case read
case starred
}
init(
@@ -54,7 +56,8 @@ struct FeedItem: Identifiable, Codable, Equatable {
guid: String? = nil,
subscriptionId: String,
subscriptionTitle: String? = nil,
read: Bool = false
read: Bool = false,
starred: Bool = false
) {
self.id = id
self.title = title
@@ -70,6 +73,7 @@ struct FeedItem: Identifiable, Codable, Equatable {
self.subscriptionId = subscriptionId
self.subscriptionTitle = subscriptionTitle
self.read = read
self.starred = starred
}
var debugDescription: String {

View File

@@ -16,14 +16,13 @@ struct FeedDetailView: View {
feedItem.read
}
private func toggleRead() {
private func toggleRead() {
let success = feedService.markItemAsRead(itemId: feedItem.id)
if !success {
errorMessage = "Failed to update read status"
showError = true
}
}
}
private func close() {
// Dismiss the view

View File

@@ -0,0 +1,97 @@
import SwiftUI
struct SearchView: View {
@StateObject private var viewModel: SearchViewModel
@State private var searchQuery: String = ""
@State private var isSearching: Bool = false
@State private var showError: Bool = false
@State private var errorMessage: String = ""
private let searchService: SearchServiceProtocol
init(searchService: SearchServiceProtocol = SearchService()) {
self.searchService = searchService
_viewModel = StateObject(wrappedValue: SearchViewModel(searchService: searchService))
}
var body: some View {
NavigationView {
VStack {
HStack {
Image(systemName: "magnifyingglass")
TextField("Search feeds...", text: $searchQuery)
.onSubmit {
performSearch()
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
.padding(.horizontal)
if isSearching {
ProgressView("Searching...")
.padding()
}
if showError {
Text(errorMessage)
.foregroundColor(.red)
.font(.caption)
.padding()
}
List {
ForEach(viewModel.searchResults) { result in
NavigationLink(destination: FeedDetailView(feedItem: result.item, feedService: searchService)) {
VStack(alignment: .leading) {
Text(result.item.title)
.font(.headline)
Text(result.item.subscriptionTitle ?? "Unknown")
.font(.subheadline)
.foregroundColor(.secondary)
if let published = result.item.published {
Text(published, format: Date.FormatStyle(date: .abbreviated, time: .shortened))
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
}
.listStyle(PlainListStyle())
}
.navigationTitle("Search")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Clear") {
searchQuery = ""
viewModel.clearSearch()
}
}
}
}
}
private func performSearch() {
guard !searchQuery.isEmpty else { return }
isSearching = true
showError = false
Task {
do {
try await viewModel.search(query: searchQuery)
isSearching = false
} catch {
errorMessage = error.localizedDescription
showError = true
isSearching = false
}
}
}
}
#Preview {
SearchView()
}

View File

@@ -24,7 +24,7 @@ class FeedViewModel: ObservableObject {
private let feedService: FeedServiceProtocol
private var cancellables = Set<AnyCancellable>()
private var currentSubscriptionId: String?
var currentSubscriptionId: String?
init(feedService: FeedServiceProtocol = FeedService()) {
self.feedService = feedService