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
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:
@@ -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] {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
97
iOS/RSSuper/UI/SearchView.swift
Normal file
97
iOS/RSSuper/UI/SearchView.swift
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user