From c6a27b3fd9e7eee566338335526ae85e11e38d3b Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 14 Aug 2020 20:05:36 -0700 Subject: [PATCH] Started working on history tracking --- App/Backend/History/BrowserHistory.swift | 88 +++++++++++++++++++ .../History.xcdatamodel/contents | 13 +++ App/Backend/History/HistoryItem.swift | 21 +++++ App/Browser View/BrowserViewController.swift | 17 +++- SBrowser.xcodeproj/project.pbxproj | 33 +++++++ 5 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 App/Backend/History/BrowserHistory.swift create mode 100644 App/Backend/History/History.xcdatamodeld/History.xcdatamodel/contents create mode 100644 App/Backend/History/HistoryItem.swift diff --git a/App/Backend/History/BrowserHistory.swift b/App/Backend/History/BrowserHistory.swift new file mode 100644 index 0000000..2cc7e95 --- /dev/null +++ b/App/Backend/History/BrowserHistory.swift @@ -0,0 +1,88 @@ +// +// BrowserHistory.swift +// App +// +// Created by James Magahern on 8/14/20. +// + +import Foundation +import CoreData + +class BrowserHistory +{ + 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)") + } + } + + public func allHistory() -> [HistoryItem] { + let dataContext = persistentContainer.viewContext + + let fetchRequest: NSFetchRequest = HistoryItemEntity.fetchRequest() + 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.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "host CONTAINS %@ OR title contains %@", matching, matching) + + let entities: [HistoryItemEntity] = (try? dataContext.fetch(fetchRequest)) ?? [] + let allItems: [HistoryItem] = entities.map { HistoryItem(entity: $0) } + + var topLevelItems: [URL: (HistoryItem, Int)] = [:] + allItems.forEach { item in + let topLevelURL = item.url.topLevelURL() + var topLevelItem = topLevelItems[topLevelURL] ?? (item, 0) + topLevelItem.0.url = topLevelURL + topLevelItems[topLevelURL] = topLevelItem + } + + return topLevelItems.values.map { return $0.0 }.sorted { (item1, item2) -> Bool in + return topLevelItems[item1.url]!.1 < topLevelItems[item2.url]!.1 + } + } +} + +extension URL +{ + public func topLevelURL() -> URL { + if var components = URLComponents(url: self, resolvingAgainstBaseURL: false) { + components.path = "" + components.query = "" + components.queryItems = [] + if let url = components.url { + return url + } + } + + return self + } +} diff --git a/App/Backend/History/History.xcdatamodeld/History.xcdatamodel/contents b/App/Backend/History/History.xcdatamodeld/History.xcdatamodel/contents new file mode 100644 index 0000000..dbfc4e2 --- /dev/null +++ b/App/Backend/History/History.xcdatamodeld/History.xcdatamodel/contents @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/App/Backend/History/HistoryItem.swift b/App/Backend/History/HistoryItem.swift new file mode 100644 index 0000000..dc29222 --- /dev/null +++ b/App/Backend/History/HistoryItem.swift @@ -0,0 +1,21 @@ +// +// HistoryItem.swift +// App +// +// Created by James Magahern on 8/14/20. +// + +import Foundation + +struct HistoryItem: Hashable +{ + var url: URL + var title: String + var lastVisited: Date + + init(entity: HistoryItemEntity) { + self.url = entity.url ?? URL(string: "about:blank")! + self.lastVisited = entity.lastVisited ?? Date() + self.title = entity.title ?? "" + } +} diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index d107ebf..69e3a98 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -273,14 +273,20 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, updateTitleAndURL(forWebView: webView) - // Start requesting favicon if let url = webView.url { + // Start requesting favicon tab.updateFaviconForURL(url) } } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { toolbarController.urlBar.loadProgress = .complete + + // Update history + if let url = webView.url { + let title = webView.title ?? "" + BrowserHistory.shared.didNavigate(toURL: url, title: title) + } } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) @@ -305,6 +311,15 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, // MARK: UITextField Delegate + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let text = textField.text { + let matches = BrowserHistory.shared.visitedToplevelHistoryItems(matching: text) + print(matches) + } + + return true + } + func textFieldShouldReturn(_ textField: UITextField) -> Bool { if let text = textField.text?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) { diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index 49be5ba..c594edb 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -36,6 +36,9 @@ 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */; }; 1ADFF4CD24CBB0C8006DC7AE /* ScriptPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */; }; 1ADFF4D024CBBCD1006DC7AE /* ScriptPolicyControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */; }; + CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */; }; + CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; }; + CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -101,6 +104,9 @@ 1ADFF4CA24CB8278006DC7AE /* ScriptControllerIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptControllerIconView.swift; sourceTree = ""; }; 1ADFF4CC24CBB0C8006DC7AE /* ScriptPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyViewController.swift; sourceTree = ""; }; 1ADFF4CF24CBBCD1006DC7AE /* ScriptPolicyControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptPolicyControl.swift; sourceTree = ""; }; + CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserHistory.swift; sourceTree = ""; }; + CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = ""; }; + CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -224,6 +230,7 @@ 1ADFF47A24C7E176006DC7AE /* Backend */ = { isa = PBXGroup; children = ( + CD853BD224E77BEF00D2BDCC /* History */, 1ADFF4AD24C8ED32006DC7AE /* ResourcePolicyManager.swift */, ); path = Backend; @@ -278,6 +285,16 @@ path = "Script Policy UI"; sourceTree = ""; }; + CD853BD224E77BEF00D2BDCC /* History */ = { + isa = PBXGroup; + children = ( + CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */, + CD853BCD24E7763900D2BDCC /* BrowserHistory.swift */, + CD853BD324E77BF900D2BDCC /* HistoryItem.swift */, + ); + path = History; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -384,14 +401,17 @@ 1A03811024E71CF000826501 /* ReliefButton.swift in Sources */, 1A03811224E71EAA00826501 /* GradientView.swift in Sources */, 1ADFF4C024CA6964006DC7AE /* URLBar.swift in Sources */, + CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */, 1ADFF4C724CA6DEB006DC7AE /* UIEdgeInsets+Layout.swift in Sources */, 1ADFF4AE24C8ED32006DC7AE /* ResourcePolicyManager.swift in Sources */, 1ADFF47424C7DE9C006DC7AE /* BrowserViewController.swift in Sources */, 1ADFF4D024CBBCD1006DC7AE /* ScriptPolicyControl.swift in Sources */, 1A03810D24E71CA700826501 /* ToolbarView.swift in Sources */, + CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */, 1ADFF48D24C8C176006DC7AE /* SBRProcessBundleBridge.m in Sources */, 1AB88F0624D4D3A90006F850 /* UIGestureRecognizer+Actions.swift in Sources */, 1ADFF46224C7DE53006DC7AE /* SceneDelegate.swift in Sources */, + CD853BCE24E7763900D2BDCC /* BrowserHistory.swift in Sources */, 1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */, 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, 1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */, @@ -676,6 +696,19 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + CD853BD024E778B800D2BDCC /* History.xcdatamodel */, + ); + currentVersion = CD853BD024E778B800D2BDCC /* History.xcdatamodel */; + path = History.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = 1ADFF45424C7DE53006DC7AE /* Project object */; }