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
191 lines
6.1 KiB
Swift
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)
|
|
}
|
|
}
|