Files
Attractor/App/Backend/History/BrowserHistory.swift

214 lines
8.5 KiB
Swift
Raw Normal View History

2020-08-14 20:05:36 -07:00
//
// BrowserHistory.swift
// App
//
// Created by James Magahern on 8/14/20.
//
import Foundation
import CoreData
import Combine
2020-08-14 20:05:36 -07:00
class BrowserHistory
{
class ViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
@Published var searchQuery: String = ""
@Published private(set) var historyItems: [HistoryItem] = []
fileprivate let historyController: BrowserHistory
fileprivate var searchQueryObserver: AnyCancellable? = nil
fileprivate var fetchedResultsController: NSFetchedResultsController<HistoryItemEntity> {
didSet { fetchedResultsController.delegate = self; performInitialFetch() }
}
fileprivate init(fetchedResultsController: NSFetchedResultsController<HistoryItemEntity>, historyController: BrowserHistory) {
self.fetchedResultsController = fetchedResultsController
self.historyController = historyController
super.init()
searchQueryObserver = $searchQuery.sink { [unowned self] newValue in
self.fetchedResultsController = historyController.fetchRequestController(forQuery: newValue)
}
}
public func item(forIdentifier identifier: HistoryItem.ID) -> HistoryItem? {
if let object = fetchedResultsController.managedObjectContext.object(with: identifier) as? HistoryItemEntity {
return HistoryItem(entity: object)
}
return nil
}
public func deleteItems(_ items: Set<HistoryItem.ID>) {
items.forEach { identifier in
historyController.deleteItem(withIdentifier: identifier)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if let objects = controller.fetchedObjects as? [HistoryItemEntity] {
self.historyItems = objects.map { HistoryItem(entity: $0) }
}
}
fileprivate func performInitialFetch() {
try? fetchedResultsController.performFetch()
if let objects = fetchedResultsController.fetchedObjects {
self.historyItems = objects.map { HistoryItem(entity: $0) }
}
}
}
2020-08-14 20:05:36 -07:00
static public let shared = BrowserHistory()
lazy fileprivate var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "History")
container.loadPersistentStores { description, error in
assert(error == nil)
}
return container
}()
public func didNavigate(toURL url: URL, title: String) {
let dataContext = persistentContainer.viewContext
let entity = HistoryItemEntity(context: dataContext)
entity.url = url
entity.lastVisited = Date()
entity.title = title
entity.host = url.host
do { try dataContext.save() }
catch {
let nserror = error as NSError
fatalError("Failed saving persistent entity to store: \(nserror), \(nserror.userInfo)")
}
}
2023-01-25 16:13:09 -08:00
public func viewModel(forFilterString filterString: String? = nil, limit: Int = 500) -> ViewModel {
let resultsController = fetchRequestController(forQuery: filterString, limit: limit)
return ViewModel(fetchedResultsController: resultsController, historyController: self)
}
public func historyItem(forIdentifier: HistoryItem.ID) -> HistoryItem? {
if let entity = try? persistentContainer.viewContext.existingObject(with: forIdentifier) as? HistoryItemEntity {
return HistoryItem(entity: entity)
2023-01-20 17:28:15 -08:00
}
return nil
}
public func deleteItem(withIdentifier identifier: HistoryItem.ID) {
let dataContext = persistentContainer.viewContext
if let object = try? dataContext.existingObject(with: identifier) {
dataContext.delete(object)
try? dataContext.save()
}
}
2023-09-26 16:23:35 -07:00
public func clearAllHistory() {
let dataContext = persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = HistoryItemEntity.fetchRequest()
let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do { try dataContext.execute(request) }
catch { print("Error clearing history: \(error.localizedDescription)") }
}
2023-01-25 16:13:09 -08:00
public func allHistory(filteredBy filterString: String? = nil, limit: Int = 500) -> [HistoryItem] {
let dataContext = persistentContainer.viewContext
let fetchRequest = fetchRequest(forStringContaining: filterString, limit: limit)
2020-08-14 20:05:36 -07:00
let entities: [HistoryItemEntity] = (try? dataContext.fetch(fetchRequest)) ?? []
return entities.map { (entity) -> HistoryItem in
HistoryItem(entity: entity)
}
}
public func visitedToplevelHistoryItems(matching: String) -> [HistoryItem] {
let dataContext = persistentContainer.viewContext
let fetchRequest: NSFetchRequest<HistoryItemEntity> = HistoryItemEntity.fetchRequest()
2023-01-20 16:20:36 -08:00
fetchRequest.predicate = NSPredicate(format: """
host CONTAINS[cd] %@
OR title CONTAINS[cd] %@
OR url ENDSWITH[cd] %@
""", matching, matching, matching)
fetchRequest.fetchLimit = 200
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "visitCount", ascending: false) ]
2020-08-14 20:05:36 -07:00
let entities: [HistoryItemEntity] = (try? dataContext.fetch(fetchRequest)) ?? []
let allItems: [HistoryItem] = entities.map { HistoryItem(entity: $0) }
var topLevelItems: [URL: (HistoryItem, Int)] = [:]
for item in allItems {
if item.url.pathComponents.count <= 2 {
var score = 1
let topLevelURL = item.url.topLevelURL()
var topLevelItem = topLevelItems[topLevelURL] ?? (item, 0)
topLevelItem.0.url = topLevelURL
if item.url.path == "/" || item.url.path == "" {
score += 10
topLevelItem.0.title = item.title
}
topLevelItem.1 += score
topLevelItems[topLevelURL] = topLevelItem
if topLevelItems.count == 20 {
break
}
2020-09-21 17:56:22 -07:00
}
2020-08-14 20:05:36 -07:00
}
return topLevelItems.values.map { return $0.0 }.sorted { (item1, item2) -> Bool in
2020-09-21 17:56:22 -07:00
return topLevelItems[item1.url]!.1 > topLevelItems[item2.url]!.1
2020-08-14 20:05:36 -07:00
}
}
}
extension BrowserHistory
{
2023-01-25 16:13:09 -08:00
fileprivate func fetchRequestController(forQuery query: String? = nil, limit: Int = 500) -> NSFetchedResultsController<HistoryItemEntity> {
let fetchRequest = fetchRequest(forStringContaining: query, limit: limit)
return NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: persistentContainer.viewContext,
sectionNameKeyPath: nil, cacheName: nil)
}
2023-01-25 16:13:09 -08:00
fileprivate func fetchRequest(forStringContaining filterString: String? = nil, limit: Int = 500) -> NSFetchRequest<HistoryItemEntity> {
let fetchRequest: NSFetchRequest<HistoryItemEntity> = HistoryItemEntity.fetchRequest()
2023-01-25 16:13:09 -08:00
fetchRequest.fetchLimit = limit
fetchRequest.sortDescriptors = [
// Sort by date
NSSortDescriptor(keyPath: \HistoryItemEntity.lastVisited, ascending: false)
]
if let filterString, filterString.count > 0 {
fetchRequest.predicate = NSPredicate(format: """
host CONTAINS[cd] %@
OR title CONTAINS[cd] %@
OR url CONTAINS[cd] %@
""", filterString, filterString, filterString)
}
return fetchRequest
}
}
2020-08-14 20:05:36 -07:00
extension URL
{
public func topLevelURL() -> URL {
if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) {
2020-09-21 17:56:22 -07:00
components.query = nil
components.queryItems = nil
components.fragment = nil
2020-08-14 20:05:36 -07:00
if let url = components.url {
return url
}
}
return self
}
}