Remote tabs: finishing touches

This commit is contained in:
James Magahern
2022-08-05 18:55:19 -07:00
parent 61773b97db
commit e8c6111592
11 changed files with 273 additions and 62 deletions

View File

@@ -21,8 +21,8 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
get {
TabInfo(
title: loadedWebView?.title,
url: loadedWebView?.url ?? self.homeURL,
favicon: self.favicon,
urlString: loadedWebView?.url?.absoluteString ?? self.homeURL?.absoluteString,
faviconData: self.favicon?.pngData(),
identifier: self.identifier
)
}
@@ -45,7 +45,15 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
private var loadedWebView: WKWebView? = nil
public var title: String? { get { tabInfo.title } }
public var url: URL? { get { tabInfo.url } }
public var url: URL? {
get {
if let urlString = tabInfo.urlString {
return URL(string: urlString)
}
return nil
}
}
public var javaScriptEnabled: Bool = false {
didSet { bridge.allowAllScripts = javaScriptEnabled }

View File

@@ -8,14 +8,21 @@
import Foundation
import Combine
struct TabInfo
struct TabInfo: Codable, Hashable
{
public var title: String?
public var url: URL?
public var favicon: UIImage?
public var urlString: String?
public var faviconData: Data?
public var identifier = UUID()
public static func ==(lhs: TabInfo, rhs: TabInfo) -> Bool {
return lhs.identifier == rhs.identifier
}
enum CodingKeys: String, CodingKey {
case title
case urlString = "url"
case faviconData
case identifier
}
}

View File

@@ -11,9 +11,9 @@ import Combine
protocol TabPickerViewControllerDelegate: AnyObject
{
func tabPicker(_ picker: TabPickerViewController, createNewTabWithURL: URL?)
func tabPicker(_ picker: TabPickerViewController, didSelectTabIdentifier tab: UUID)
func tabPicker(_ picker: TabPickerViewController, closeTabWithIdentifier tab: UUID)
func tabPicker(_ picker: TabPickerViewController, tabInfoForIdentifier: UUID) -> TabInfo
func tabPicker(_ picker: TabPickerViewController, didSelectTabInfo: TabInfo, fromHost: String)
}
class TabPickerViewController: UIViewController, UICollectionViewDelegate
@@ -22,13 +22,16 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
public static var localHostIdentifier = "__localhost__";
public var selectedTabIdentifier: UUID?
public var selectedTabIdentifier: TabID?
public var selectedTabHost: String? { didSet { didChangeSelectedTabHost(selectedTabHost!) } }
weak var delegate: TabPickerViewControllerDelegate?
public var tabObserver: AnyCancellable?
private var selectedTabIdentifiersForEditing: Set<UUID> = []
private var tabIdentifiersByHost: [String: [UUID]] = [:]
public var displayedError: Error? = nil { didSet { didSetDisplayedError(displayedError) } }
private var displayedErrorView: UITextView?
private var selectedTabIdentifiersForEditing: Set<TabInfo> = []
private var tabIdentifiersByHost: [String: [TabInfo]] = [:]
private var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
private lazy var listLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
@@ -38,43 +41,41 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
collectionView.delegate = self
}
private lazy var cellRegistry = UICollectionView.CellRegistration<UICollectionViewListCell, TabID> { [unowned self] (listCell, indexPath, item) in
private lazy var cellRegistry = UICollectionView.CellRegistration<UICollectionViewListCell, TabInfo> { [unowned self] (listCell, indexPath, tab) in
var config = listCell.defaultContentConfiguration()
if let tab = delegate?.tabPicker(self, tabInfoForIdentifier: item) {
if let title = tab.title, title.count > 0 {
config.text = title
config.secondaryText = tab.url?.absoluteString
} else if let url = tab.url {
config.text = url.absoluteString
config.secondaryText = url.absoluteString
} else {
config.text = "New Tab"
}
config.textProperties.numberOfLines = 1
config.secondaryTextProperties.numberOfLines = 1
if let image = tab.favicon {
config.image = image
} else {
config.image = UIImage(systemName: "safari")
}
config.imageProperties.maximumSize = CGSize(width: 21.0, height: 21.0)
config.imageProperties.cornerRadius = 3.0
if let selectedTabIdentifier, selectedTabIdentifier == item {
listCell.accessories = [ .checkmark() ]
} else {
listCell.accessories = []
}
if let title = tab.title, title.count > 0 {
config.text = title
config.secondaryText = tab.urlString
} else if let url = tab.urlString {
config.text = url
config.secondaryText = url
} else {
config.text = "New Tab"
}
config.textProperties.numberOfLines = 1
config.secondaryTextProperties.numberOfLines = 1
if let faviconData = tab.faviconData, let image = UIImage(data: faviconData) {
config.image = image
} else {
config.image = UIImage(systemName: "safari")
}
config.imageProperties.maximumSize = CGSize(width: 21.0, height: 21.0)
config.imageProperties.cornerRadius = 3.0
if let selectedTabIdentifier, selectedTabIdentifier == tab.identifier {
listCell.accessories = [ .checkmark() ]
} else {
listCell.accessories = []
}
listCell.contentConfiguration = config
}
private lazy var dataSource = UICollectionViewDiffableDataSource<Int, TabID>(collectionView: collectionView)
private lazy var dataSource = UICollectionViewDiffableDataSource<Int, TabInfo>(collectionView: collectionView)
{ [unowned self] (collectionView, indexPath, item) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistry, for: indexPath, item: item)
}
@@ -96,12 +97,14 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
}()
private lazy var hostPickerButton: UIButton = {
var buttonConfiguration = UIButton.Configuration.filled()
var buttonConfiguration = UIButton.Configuration.bordered()
buttonConfiguration.title = "Host"
let button = UIButton(configuration: buttonConfiguration)
button.changesSelectionAsPrimaryAction = true
button.showsMenuAsPrimaryAction = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setContentCompressionResistancePriority(.required, for: .horizontal)
return button
}()
@@ -123,7 +126,7 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
return UISwipeActionsConfiguration(actions: [ UIContextualAction(style: .destructive, title: "Close", handler: { [unowned self] (action, view, completionHandler) in
if let item = dataSource.itemIdentifier(for: indexPath) {
var snapshot = dataSource.snapshot()
delegate?.tabPicker(self, closeTabWithIdentifier: item)
delegate?.tabPicker(self, closeTabWithIdentifier: item.identifier)
snapshot.deleteItems([ item ])
dataSource.apply(snapshot, animatingDifferences: true)
@@ -136,14 +139,18 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
configureNavigationButtons(forEditing: isEditing)
}
public func setTabIdentifiers(_ identifiers: [UUID], forHost host: String) {
tabIdentifiersByHost[host] = identifiers
if host == selectedTabHost {
public func setTabInfos(_ infos: [TabInfo], forHost host: String) {
let wasEmpty = tabIdentifiersByHost.isEmpty
tabIdentifiersByHost[host] = infos
if wasEmpty {
selectedTabHost = host
} else if host == selectedTabHost {
var snapshot = dataSource.snapshot()
snapshot.deleteAllItems()
snapshot.appendSections([ 0 ])
snapshot.appendItems(identifiers)
dataSource.apply(snapshot)
snapshot.appendItems(infos)
dataSource.apply(snapshot) // crashing here...
}
reloadHostPickerButtonMenu()
@@ -187,7 +194,7 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
var snapshot = dataSource.snapshot()
for tab in selectedTabIdentifiersForEditing {
snapshot.deleteItems([ tab ])
delegate?.tabPicker(self, closeTabWithIdentifier: tab)
delegate?.tabPicker(self, closeTabWithIdentifier: tab.identifier)
}
dataSource.apply(snapshot, animatingDifferences: true)
@@ -203,11 +210,50 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
dataSource.applySnapshotUsingReloadData(snapshot)
}
private func didSetDisplayedError(_ displayedError: Error?) {
if let displayedError {
// Clear items first
var snapshot = dataSource.snapshot()
snapshot.deleteAllItems()
dataSource.applySnapshotUsingReloadData(snapshot)
if displayedErrorView == nil {
let errorView = UITextView(frame: .zero)
errorView.isUserInteractionEnabled = false
errorView.translatesAutoresizingMaskIntoConstraints = false
errorView.isScrollEnabled = false
collectionView.addSubview(errorView)
let guide = collectionView.layoutMarginsGuide
NSLayoutConstraint.activate([
errorView.leadingAnchor .constraint(equalTo: guide.leadingAnchor),
errorView.trailingAnchor .constraint(equalTo: guide.trailingAnchor),
errorView.centerYAnchor .constraint(equalTo: guide.centerYAnchor),
])
self.displayedErrorView = errorView
}
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
var attributedString = try! AttributedString(markdown: "**Error loading tabs**: \(displayedError.localizedDescription)")
attributedString.foregroundColor = UIColor.secondaryLabel
attributedString.paragraphStyle = paragraphStyle
attributedString.font = UIFont.preferredFont(forTextStyle: .body)
displayedErrorView?.attributedText = NSAttributedString(attributedString)
} else {
displayedErrorView?.removeFromSuperview()
displayedErrorView = nil
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let tab = dataSource.itemIdentifier(for: indexPath) else { return }
if !isEditing {
delegate?.tabPicker(self, didSelectTabIdentifier: tab)
delegate?.tabPicker(self, didSelectTabInfo: tab, fromHost: selectedTabHost!)
} else {
deleteTabButton.isEnabled = collectionView.indexPathsForSelectedItems?.count ?? 0 > 0
selectedTabIdentifiersForEditing.update(with: tab)