From 53efb5389e57f645dfcd1137e916b94978854de4 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 20 Jan 2023 17:28:15 -0800 Subject: [PATCH] Adds history browser view --- App/Backend/History/BrowserHistory.swift | 11 ++- App/Backend/History/HistoryItem.swift | 12 +++- .../BrowserViewController+Keyboard.swift | 14 ++++ App/Browser View/BrowserViewController.swift | 17 +++++ .../DocumentControlViewController.swift | 2 + .../HistoryBrowserViewController.swift | 20 ++++++ App/History UI/HistoryView.swift | 72 +++++++++++++++++++ App/KeyboardShortcuts.swift | 13 ++++ SBrowser.xcodeproj/project.pbxproj | 18 +++++ 9 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 App/History UI/HistoryBrowserViewController.swift create mode 100644 App/History UI/HistoryView.swift diff --git a/App/Backend/History/BrowserHistory.swift b/App/Backend/History/BrowserHistory.swift index cd2afc8..0f3ad97 100644 --- a/App/Backend/History/BrowserHistory.swift +++ b/App/Backend/History/BrowserHistory.swift @@ -37,10 +37,19 @@ class BrowserHistory } } - public func allHistory() -> [HistoryItem] { + public func allHistory(limit: Int? = nil) -> [HistoryItem] { let dataContext = persistentContainer.viewContext let fetchRequest: NSFetchRequest = HistoryItemEntity.fetchRequest() + fetchRequest.sortDescriptors = [ + // Sort by date + NSSortDescriptor(keyPath: \HistoryItemEntity.lastVisited, ascending: false) + ] + + if let limit { + fetchRequest.fetchLimit = limit + } + let entities: [HistoryItemEntity] = (try? dataContext.fetch(fetchRequest)) ?? [] return entities.map { (entity) -> HistoryItem in diff --git a/App/Backend/History/HistoryItem.swift b/App/Backend/History/HistoryItem.swift index dc29222..2014fb2 100644 --- a/App/Backend/History/HistoryItem.swift +++ b/App/Backend/History/HistoryItem.swift @@ -7,15 +7,25 @@ import Foundation -struct HistoryItem: Hashable +struct HistoryItem: Hashable, Identifiable { var url: URL var title: String var lastVisited: Date + var id: ObjectIdentifier init(entity: HistoryItemEntity) { self.url = entity.url ?? URL(string: "about:blank")! self.lastVisited = entity.lastVisited ?? Date() self.title = entity.title ?? "" + self.id = entity.id + } + + // For testing/previews + public init(url: URL, title: String, lastVisited: Date) { + self.url = url + self.title = title + self.lastVisited = lastVisited + self.id = ObjectIdentifier(NSUUID()) } } diff --git a/App/Browser View/BrowserViewController+Keyboard.swift b/App/Browser View/BrowserViewController+Keyboard.swift index c3663d3..5ae8d04 100644 --- a/App/Browser View/BrowserViewController+Keyboard.swift +++ b/App/Browser View/BrowserViewController+Keyboard.swift @@ -156,6 +156,10 @@ extension BrowserViewController: ShortcutResponder showSettingsWindow() } + func showHistory(_ sender: Any?) { + showHistoryWindow() + } + func toggleDarkMode(_ sender: Any?) { self.darkModeEnabled = !self.darkModeEnabled } @@ -163,4 +167,14 @@ extension BrowserViewController: ShortcutResponder func openInReaderMode(_ sender: Any?) { showReaderWindow() } + + func handleOpenURL(_ sender: Any?, url: URL?) { + guard let url else { return } + + if tab.url == nil { + tab.beginLoadingURL(url) + } else { + createNewTab(withURL: url) + } + } } diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index b800e38..83f1e61 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -333,6 +333,12 @@ class BrowserViewController: UIViewController showShareSheetForCurrentURL(fromViewController: documentControls) }, for: .touchUpInside) + // History + documentControls.historyView.addAction(UIAction { [unowned self] action in + documentControls.dismiss(animated: false, completion: nil) + showHistory(action) + }, for: .touchUpInside) + present(documentControls, animated: true, completion: nil) }), for: .touchUpInside) @@ -463,6 +469,17 @@ class BrowserViewController: UIViewController } } + internal func showHistoryWindow() { + let historyViewController = HistoryBrowserViewController() + historyViewController.title = "History" + historyViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: UIAction { _ in + historyViewController.dismiss(animated: true) + }) + + let navigationController = UINavigationController(rootViewController: historyViewController) + present(navigationController, animated: true) + } + internal func updateLoadProgress(forWebView webView: WKWebView) { if let loadError = tab.loadError { toolbarController.urlBar.loadProgress = .error(error: loadError) diff --git a/App/Document Controls UI/DocumentControlViewController.swift b/App/Document Controls UI/DocumentControlViewController.swift index 0716612..fdd67d1 100644 --- a/App/Document Controls UI/DocumentControlViewController.swift +++ b/App/Document Controls UI/DocumentControlViewController.swift @@ -20,6 +20,7 @@ class DocumentControlViewController: UIViewController let archiveView = DocumentControlItemView().title("Archive.today") .symbol("shippingbox") let emailView = DocumentControlItemView().title("Email") .symbol("envelope") let sharingView = DocumentControlItemView().title("Share") .symbol("square.and.arrow.up") + let historyView = DocumentControlItemView().title("History") .symbol("clock.arrow.circlepath") let darkModeView = DocumentControlItemView().title("Dark Mode") var observations: [NSKeyValueObservation] = [] @@ -46,6 +47,7 @@ class DocumentControlViewController: UIViewController documentControlsView.stackView.addArrangedSubview(darkModeView) documentControlsView.stackView.addArrangedSubview(readabilityView) documentControlsView.stackView.addArrangedSubview(archiveView) + documentControlsView.stackView.addArrangedSubview(historyView) documentControlsView.stackView.addArrangedSubview(settingsView) diff --git a/App/History UI/HistoryBrowserViewController.swift b/App/History UI/HistoryBrowserViewController.swift new file mode 100644 index 0000000..b05689f --- /dev/null +++ b/App/History UI/HistoryBrowserViewController.swift @@ -0,0 +1,20 @@ +// +// HistoryBrowserViewController.swift +// App +// +// Created by James Magahern on 1/20/23. +// + +import SwiftUI +import UIKit + +@MainActor +class HistoryBrowserViewController: UIHostingController { + public init() { + super.init(rootView: HistoryView(historyItems: BrowserHistory.shared.allHistory(limit: 500))) + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/App/History UI/HistoryView.swift b/App/History UI/HistoryView.swift new file mode 100644 index 0000000..3cffbb6 --- /dev/null +++ b/App/History UI/HistoryView.swift @@ -0,0 +1,72 @@ +// +// HistoryView.swift +// App +// +// Created by James Magahern on 1/20/23. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct HistoryView: View { + var historyItems: [HistoryItem] + + private let dateFormatter: DateFormatter + @State public var selectedItems = Set() + @Environment(\.dismiss) private var dismissAction + + init(historyItems: [HistoryItem]) { + self.historyItems = historyItems + + let formatter = DateFormatter() + formatter.locale = Locale.current + formatter.dateStyle = .medium + formatter.timeStyle = .short + self.dateFormatter = formatter + } + + var body: some View { + Table(historyItems, selection: $selectedItems) { + TableColumn("Title", value: \.title) + + TableColumn("URL") { item in + Text(item.url.absoluteString) + } + + TableColumn("Last Visited") { item in + Text(dateFormatter.string(from: item.lastVisited)) + } + } + .contextMenu(forSelectionType: HistoryItem.ID.self, menu: { items in + if let firstItem: HistoryItem.ID = items.first, + let historyItem = historyItems.first { $0.id == firstItem } + { + Button("Copy") { + UIPasteboard.general.addItems([ + [ UTType.url.identifier : historyItem.url ] + ]) + } + + // TODO: Delete? + } + }, primaryAction: { items in + if let firstItem: HistoryItem.ID = items.first, + let historyItem = historyItems.first(where: { $0.id == firstItem }) + { + UIApplication.shared.open(historyItem.url) + dismissAction() + } + }) + } +} + +struct HistoryViewPreviewProvider: PreviewProvider { + static var previews: some View { + HistoryView(historyItems: [ + HistoryItem(url: URL(string: "https://apple.com")!, title: "Apple", lastVisited: Date.now), + HistoryItem(url: URL(string: "https://google.com")!, title: "Google", lastVisited: Date.now) + ]) + .previewLayout(.fixed(width: 480.0, height: 800.0)) + } +} + diff --git a/App/KeyboardShortcuts.swift b/App/KeyboardShortcuts.swift index 9abb6b5..6ff5655 100644 --- a/App/KeyboardShortcuts.swift +++ b/App/KeyboardShortcuts.swift @@ -50,6 +50,12 @@ protocol ShortcutResponder: AnyObject { @objc optional func openInReaderMode(_ sender: Any?) + + @objc + optional func showHistory(_ sender: Any?) + + @objc + optional func handleOpenURL(_ sender: Any?, url: URL?) } fileprivate extension Array { @@ -166,6 +172,13 @@ public class KeyboardShortcuts { title: "Go Forward", action: #selector(ShortcutResponder.goForward) ), + + UIKeyCommand( + modifiers: [.command, .shift], + input: "h", + title: "Show History…", + action: #selector(ShortcutResponder.showHistory) + ), ]), // Tab Navigation diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index cf37142..2c0cae6 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ 1ADFF4CD24CBB0C8006DC7AE /* ScriptOriginPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADFF4CC24CBB0C8006DC7AE /* ScriptOriginPolicyViewController.swift */; }; CD01D5A5254A10BB00189CDC /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD01D5A4254A10BB00189CDC /* TabBarView.swift */; }; CD01D5AB254A206D00189CDC /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD01D5AA254A206D00189CDC /* TabBarViewController.swift */; }; + CD15332F297B6798009A7F3A /* HistoryBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */; }; + CD153331297B6806009A7F3A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD153330297B6806009A7F3A /* HistoryView.swift */; }; CD16844D269E709400B8F8A5 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD16844C269E709400B8F8A5 /* Box.swift */; }; CD19576D268BE95900E8089B /* GenericContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD19576C268BE95900E8089B /* GenericContentView.swift */; }; CD361CF6271A3718006E9CA5 /* SBRScriptPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = CD361CF5271A3718006E9CA5 /* SBRScriptPolicy.m */; }; @@ -150,6 +152,8 @@ 1ADFF4CC24CBB0C8006DC7AE /* ScriptOriginPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptOriginPolicyViewController.swift; sourceTree = ""; }; CD01D5A4254A10BB00189CDC /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; CD01D5AA254A206D00189CDC /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = ""; }; + CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryBrowserViewController.swift; sourceTree = ""; }; + CD153330297B6806009A7F3A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; CD16844C269E709400B8F8A5 /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; CD19576C268BE95900E8089B /* GenericContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericContentView.swift; sourceTree = ""; }; CD361CF4271A3718006E9CA5 /* SBRScriptPolicy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRScriptPolicy.h; sourceTree = ""; }; @@ -302,6 +306,7 @@ CDCE2662251AA7FC007FE92A /* Document Controls UI */, 1AD3104125254FA300A4A952 /* Find on Page */, CD7F2133265DACEE0001D042 /* Hacks */, + CD15332D297B6784009A7F3A /* History UI */, CDC5DA3C25DB7A5500BA8D99 /* Reader View */, CDB6807B28B4456B007D787E /* Scene Delegates */, 1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */, @@ -406,6 +411,15 @@ path = "Script Policy UI"; sourceTree = ""; }; + CD15332D297B6784009A7F3A /* History UI */ = { + isa = PBXGroup; + children = ( + CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */, + CD153330297B6806009A7F3A /* HistoryView.swift */, + ); + path = "History UI"; + sourceTree = ""; + }; CD7A7E9B2686A99600E20BA3 /* Amber */ = { isa = PBXGroup; children = ( @@ -634,6 +648,7 @@ CDB6807D28B446FC007D787E /* ReaderSceneDelegate.swift in Sources */, CDD0522425F8055700DD1771 /* SearchProvider.swift in Sources */, CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */, + CD15332F297B6798009A7F3A /* HistoryBrowserViewController.swift in Sources */, 1ADFF48D24C8C176006DC7AE /* SBRProcessBundleBridge.m in Sources */, 1AB88F0624D4D3A90006F850 /* UIGestureRecognizer+Actions.swift in Sources */, CD7313E4270534B800053347 /* ScriptPolicyViewControllerDelegate.swift in Sources */, @@ -663,6 +678,7 @@ CD01D5AB254A206D00189CDC /* TabBarViewController.swift in Sources */, 1ADFF47924C7DFF8006DC7AE /* BrowserView.swift in Sources */, CDCE2664251AA80F007FE92A /* DocumentControlViewController.swift in Sources */, + CD153331297B6806009A7F3A /* HistoryView.swift in Sources */, CDF3468E276C105900FB3141 /* SettingsSceneDelegate.swift in Sources */, 1AB88EFF24D3BBA50006F850 /* TabPickerViewController.swift in Sources */, CD19576D268BE95900E8089B /* GenericContentView.swift in Sources */, @@ -834,6 +850,7 @@ CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = "App/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -861,6 +878,7 @@ CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = YES; INFOPLIST_FILE = "App/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks",