Adds history browser view
This commit is contained in:
@@ -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> = 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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
20
App/History UI/HistoryBrowserViewController.swift
Normal file
20
App/History UI/HistoryBrowserViewController.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// HistoryBrowserViewController.swift
|
||||
// App
|
||||
//
|
||||
// Created by James Magahern on 1/20/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
class HistoryBrowserViewController: UIHostingController<HistoryView> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
72
App/History UI/HistoryView.swift
Normal file
72
App/History UI/HistoryView.swift
Normal file
@@ -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<HistoryItem.ID>()
|
||||
@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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
CD01D5A4254A10BB00189CDC /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = "<group>"; };
|
||||
CD01D5AA254A206D00189CDC /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = "<group>"; };
|
||||
CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryBrowserViewController.swift; sourceTree = "<group>"; };
|
||||
CD153330297B6806009A7F3A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||
CD16844C269E709400B8F8A5 /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
||||
CD19576C268BE95900E8089B /* GenericContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericContentView.swift; sourceTree = "<group>"; };
|
||||
CD361CF4271A3718006E9CA5 /* SBRScriptPolicy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SBRScriptPolicy.h; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
CD15332D297B6784009A7F3A /* History UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CD15332E297B6798009A7F3A /* HistoryBrowserViewController.swift */,
|
||||
CD153330297B6806009A7F3A /* HistoryView.swift */,
|
||||
);
|
||||
path = "History UI";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user