Files
RSSuper/native-route/ios/RSSuper/Services/FeedItemStore.swift
Michael Freno 199c711dd4
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
conflicting pathing
2026-03-31 11:46:15 -04:00

191 lines
6.1 KiB
Swift

/*
* FeedItemStore.swift
*
* CRUD operations for feed items with FTS search support.
*/
import Foundation
import CoreData
/// FeedItemStore - Manages feed item persistence with FTS search
class FeedItemStore: NSObject {
private let db: CoreDataDatabase
/// Signal emitted when an item is added
var itemAdded: ((FeedItem) -> Void)?
/// Signal emitted when an item is updated
var itemUpdated: ((FeedItem) -> Void)?
/// Signal emitted when an item is deleted
var itemDeleted: ((String) -> Void)?
/// Create a new feed item store
init(db: CoreDataDatabase) {
self.db = db
super.init()
}
/// Add a new feed item
func add(_ item: FeedItem) async throws -> FeedItem {
try await db.insertFeedItem(item)
itemAdded?(item)
return item
}
/// Add multiple items in a batch
func addBatch(_ items: [FeedItem]) async throws {
try await db.insertFeedItems(items)
}
/// Get an item by ID
func get_BY_ID(_ id: String) async throws -> FeedItem? {
return try await db.getFeedItemById(id)
}
/// Get items by subscription ID
func get_BY_SUBSCRIPTION(_ subscriptionId: String) async throws -> [FeedItem] {
return try await db.getFeedItems(subscriptionId)
}
/// Get all items
func get_ALL() async throws -> [FeedItem] {
return try await db.getFeedItems(nil)
}
/// Search items using FTS
func search(_ query: String, filters: SearchFilters? = nil, limit: Int = 50) async throws -> [SearchResult] {
return try await searchFTS(query: query, filters: filters, limit: limit)
}
/// Search items using FTS with custom limit
func searchFTS(query: String, filters: SearchFilters? = nil, limit: Int) async throws -> [SearchResult] {
let fullTextSearch = FullTextSearch(db: db)
// Perform FTS search
var results = try await fullTextSearch.search(query: query, filters: filters, limit: limit)
// Rank results by relevance
results = try rankResults(query: query, results: results)
return results
}
/// Apply search filters to a search result
func applyFilters(_ result: SearchResult, filters: SearchFilters) -> Bool {
// Date filters
if let dateFrom = filters.dateFrom, result.published != nil {
let published = result.published.map { Date(string: $0) } ?? Date.distantPast
if published < dateFrom {
return false
}
}
if let dateTo = filters.dateTo, result.published != nil {
let published = result.published.map { Date(string: $0) } ?? Date.distantFuture
if published > dateTo {
return false
}
}
// Feed ID filters
if let feedIds = filters.feedIds, !feedIds.isEmpty {
// For now, we can't filter by feedId without additional lookup
// This would require joining with feed_subscriptions
}
// Author filters - not directly supported in current schema
// Would require adding author to FTS index
// Content type filters - not directly supported
// Would require adding enclosure_type to FTS index
return true
}
/// Rank search results by relevance
func rankResults(query: String, results: [SearchResult]) async throws -> [SearchResult] {
let queryWords = query.components(separatedBy: .whitespaces)
var ranked: [SearchResult?] = results.map { $0 }
for result in ranked {
guard let result = result else { continue }
var score = result.score
// Boost score for exact title matches
if let title = result.title {
for word in queryWords {
let word = word.trimmingCharacters(in: .whitespaces)
if !word.isEmpty && title.lowercased().contains(word.lowercased()) {
score += 0.5
}
}
}
// Boost score for feed title matches
if let feedTitle = result.feedTitle {
for word in queryWords {
let word = word.trimmingCharacters(in: .whitespaces)
if !word.isEmpty && feedTitle.lowercased().contains(word.lowercased()) {
score += 0.3
}
}
}
result.score = score
ranked.append(result)
}
// Sort by score (descending)
ranked.sort { $0?.score ?? 0 > $1?.score ?? 0 }
return ranked.compactMap { $0 }
}
/// Mark an item as read
func markAsRead(_ id: String) async throws {
try await db.markFeedItemAsRead(id)
}
/// Mark an item as unread
func markAsUnread(_ id: String) async throws {
try await db.markFeedItemAsUnread(id)
}
/// Mark an item as starred
func markAsStarred(_ id: String) async throws {
try await db.markFeedItemAsStarred(id)
}
/// Unmark an item from starred
func unmarkStarred(_ id: String) async throws {
try await db.unmarkFeedItemAsStarred(id)
}
/// Get unread items
func get_UNREAD() async throws -> [FeedItem] {
return try await db.getFeedItems(nil).filter { $0.isRead == false }
}
/// Get starred items
func get_STARRED() async throws -> [FeedItem] {
return try await db.getFeedItems(nil).filter { $0.isStarred == true }
}
/// Delete an item by ID
func delete(_ id: String) async throws {
try await db.deleteFeedItem(id)
itemDeleted?(id)
}
/// Delete items by subscription ID
func deleteBySubscription(_ subscriptionId: String) async throws {
try await db.deleteFeedItems(subscriptionId)
}
/// Delete old items (keep last N items per subscription)
func cleanupOldItems(keepCount: Int = 100) async throws {
try await db.cleanupOldItems(keepCount: keepCount)
}
}