Remote tabs: finishing touches
This commit is contained in:
@@ -68,6 +68,9 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
|
|||||||
let title = webView.title ?? ""
|
let title = webView.title ?? ""
|
||||||
BrowserHistory.shared.didNavigate(toURL: url, title: title)
|
BrowserHistory.shared.didNavigate(toURL: url, title: title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish Tabs
|
||||||
|
AttractorServer.shared.publishTabInfo(tabController.tabs.map { $0.tabInfo })
|
||||||
}
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void)
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void)
|
||||||
|
|||||||
@@ -145,12 +145,12 @@ class BrowserViewController: UIViewController
|
|||||||
tabPickerController.tabObserver = tabController.$tabs
|
tabPickerController.tabObserver = tabController.$tabs
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink(receiveValue: { (newTabs: [Tab]) in
|
.sink(receiveValue: { (newTabs: [Tab]) in
|
||||||
tabPickerController.setTabIdentifiers(newTabs.map { $0.identifier }, forHost: TabPickerViewController.localHostIdentifier)
|
tabPickerController.setTabInfos(newTabs.map { $0.tabInfo }, forHost: TabPickerViewController.localHostIdentifier)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set localhost tabs
|
// Set localhost tabs
|
||||||
let tabIdentifiers = tabController.tabs.map { $0.identifier }
|
let tabInfos = tabController.tabs.map { $0.tabInfo }
|
||||||
tabPickerController.setTabIdentifiers(tabIdentifiers, forHost: TabPickerViewController.localHostIdentifier)
|
tabPickerController.setTabInfos(tabInfos, forHost: TabPickerViewController.localHostIdentifier)
|
||||||
tabPickerController.selectedTabHost = TabPickerViewController.localHostIdentifier
|
tabPickerController.selectedTabHost = TabPickerViewController.localHostIdentifier
|
||||||
|
|
||||||
let remoteTabPickerController = TabPickerViewController()
|
let remoteTabPickerController = TabPickerViewController()
|
||||||
@@ -160,6 +160,20 @@ class BrowserViewController: UIViewController
|
|||||||
remoteTabPickerController.newTabButton.isEnabled = false
|
remoteTabPickerController.newTabButton.isEnabled = false
|
||||||
remoteTabPickerController.editButtonItem.isEnabled = false
|
remoteTabPickerController.editButtonItem.isEnabled = false
|
||||||
|
|
||||||
|
// Fetch tabs now
|
||||||
|
AttractorServer.shared.getTabInfos { [weak remoteTabPickerController] result in
|
||||||
|
guard let picker = remoteTabPickerController else { return }
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let tabInfos):
|
||||||
|
tabInfos.forEach { (key: String, value: [TabInfo]) in
|
||||||
|
picker.setTabInfos(value, forHost: key)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
picker.displayedError = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let tabBarController = UITabBarController(nibName: nil, bundle: nil)
|
let tabBarController = UITabBarController(nibName: nil, bundle: nil)
|
||||||
tabBarController.viewControllers = [
|
tabBarController.viewControllers = [
|
||||||
UINavigationController(rootViewController: tabPickerController),
|
UINavigationController(rootViewController: tabPickerController),
|
||||||
@@ -522,7 +536,7 @@ class BrowserViewController: UIViewController
|
|||||||
|
|
||||||
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
||||||
var findActions: [Selector] = []
|
var findActions: [Selector] = []
|
||||||
if #available(macCatalyst 16.0, *) {
|
if #available(macCatalyst 16.0, iOS 16.0, *) {
|
||||||
findActions = [
|
findActions = [
|
||||||
#selector(UIResponder.find(_:)),
|
#selector(UIResponder.find(_:)),
|
||||||
#selector(UIResponder.findNext(_:)),
|
#selector(UIResponder.findNext(_:)),
|
||||||
@@ -645,8 +659,15 @@ extension BrowserViewController: TabPickerViewControllerDelegate
|
|||||||
return tab.tabInfo
|
return tab.tabInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func tabPicker(_ picker: TabPickerViewController, didSelectTabIdentifier tabIdentifier: UUID) {
|
func tabPicker(_ picker: TabPickerViewController, didSelectTabInfo info: TabInfo, fromHost host: String) {
|
||||||
guard let tab = tabController.tab(forIdentifier: tabIdentifier) else { return }
|
var tab: Tab?
|
||||||
|
if host == TabPickerViewController.localHostIdentifier {
|
||||||
|
tab = tabController.tab(forIdentifier: info.identifier)
|
||||||
|
} else if let urlString = info.urlString {
|
||||||
|
tab = tabController.createNewTab(url: URL(string: urlString))
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let tab else { return }
|
||||||
|
|
||||||
self.tab = tab
|
self.tab = tab
|
||||||
picker.dismiss(animated: true, completion: nil)
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
|||||||
@@ -28,6 +28,30 @@ struct LabelContentConfiguration : UIContentConfiguration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TextFieldContentConfiguration : UIContentConfiguration
|
||||||
|
{
|
||||||
|
var text: String = ""
|
||||||
|
var placeholderText: String? = nil
|
||||||
|
var textChanged: ((String) -> Void)
|
||||||
|
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
let textField = UITextField(frame: .zero)
|
||||||
|
textField.borderStyle = .roundedRect
|
||||||
|
textField.autocorrectionType = .no
|
||||||
|
textField.autocapitalizationType = .none
|
||||||
|
|
||||||
|
return GenericContentView<UITextField, TextFieldContentConfiguration>(configuration: self, view: textField) { config, textField in
|
||||||
|
textField.text = config.text
|
||||||
|
textField.placeholder = config.placeholderText
|
||||||
|
textField.addAction(UIAction { _ in config.textChanged(textField.text ?? "") }, for: .editingChanged)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> TextFieldContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ButtonContentConfiguration : UIContentConfiguration
|
struct ButtonContentConfiguration : UIContentConfiguration
|
||||||
{
|
{
|
||||||
var menu: UIMenu
|
var menu: UIMenu
|
||||||
@@ -56,11 +80,13 @@ class GeneralSettingsViewController: UIViewController
|
|||||||
{
|
{
|
||||||
enum Section: String, CaseIterable {
|
enum Section: String, CaseIterable {
|
||||||
case searchEngine = "Search Engine"
|
case searchEngine = "Search Engine"
|
||||||
|
case syncServer = "Sync Server"
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Item = String
|
typealias Item = String
|
||||||
|
|
||||||
static let SearchProviderPopupItem = "searchProvider.popup"
|
static let SearchProviderPopupItem = "searchProvider.popup"
|
||||||
|
static let SyncServerItem = "syncServer.field"
|
||||||
|
|
||||||
let dataSource: UICollectionViewDiffableDataSource<Section, Item>
|
let dataSource: UICollectionViewDiffableDataSource<Section, Item>
|
||||||
let collectionView: UICollectionView
|
let collectionView: UICollectionView
|
||||||
@@ -109,7 +135,7 @@ class GeneralSettingsViewController: UIViewController
|
|||||||
if idiom == .mac {
|
if idiom == .mac {
|
||||||
return LabelContentConfiguration(
|
return LabelContentConfiguration(
|
||||||
text: sectionName + ": ",
|
text: sectionName + ": ",
|
||||||
insets: UIEdgeInsets(top: 0, left: 10.0, bottom: 0, right: 10.0),
|
insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0, right: 10.0),
|
||||||
textAlignment: .right
|
textAlignment: .right
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -141,11 +167,19 @@ class GeneralSettingsViewController: UIViewController
|
|||||||
})
|
})
|
||||||
|
|
||||||
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
|
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
|
||||||
|
} else if identifier == Self.SyncServerItem {
|
||||||
#if !targetEnvironment(macCatalyst)
|
cell.contentConfiguration = TextFieldContentConfiguration(
|
||||||
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
|
text: Settings.shared.syncServer,
|
||||||
#endif
|
placeholderText: "https://sync.server.com",
|
||||||
|
textChanged: { newString in
|
||||||
|
Settings.shared.syncServer = newString
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !targetEnvironment(macCatalyst)
|
||||||
|
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
let sectionHeaderRegistry = UICollectionView.SupplementaryRegistration<UICollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader, handler: {
|
let sectionHeaderRegistry = UICollectionView.SupplementaryRegistration<UICollectionViewCell>(elementKind: UICollectionView.elementKindSectionHeader, handler: {
|
||||||
@@ -189,6 +223,7 @@ class GeneralSettingsViewController: UIViewController
|
|||||||
// iOS
|
// iOS
|
||||||
// snapshot.appendItems(Settings.SearchProviderSetting.allCases.map { $0.rawValue }, toSection: .searchEngine)
|
// snapshot.appendItems(Settings.SearchProviderSetting.allCases.map { $0.rawValue }, toSection: .searchEngine)
|
||||||
snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine)
|
snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine)
|
||||||
|
snapshot.appendItems([ Self.SyncServerItem ], toSection: .syncServer)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false)
|
dataSource.apply(snapshot, animatingDifferences: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,4 +105,7 @@ class Settings
|
|||||||
|
|
||||||
@SettingProperty(key: "userStylesheet")
|
@SettingProperty(key: "userStylesheet")
|
||||||
public var userStylesheet: String = ""
|
public var userStylesheet: String = ""
|
||||||
|
|
||||||
|
@SettingProperty(key: "syncServer")
|
||||||
|
public var syncServer: String = "https://attractor.severnaya.net"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,6 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -8,5 +8,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.developer.web-browser</key>
|
<key>com.apple.developer.web-browser</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
76
App/Sync/AttractorServer.swift
Normal file
76
App/Sync/AttractorServer.swift
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
//
|
||||||
|
// AttractorServer.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by James Magahern on 8/5/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class AttractorServer
|
||||||
|
{
|
||||||
|
static let shared = AttractorServer()
|
||||||
|
|
||||||
|
private var endpointURL: URL {
|
||||||
|
get { URL(string: Settings.shared.syncServer) ?? URL(string: "http://localhost")! }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getHostname() -> String {
|
||||||
|
// Need an entitlement for this...
|
||||||
|
return UIDevice.current.name
|
||||||
|
}
|
||||||
|
|
||||||
|
public func publishTabInfo(_ tabInfos: [TabInfo]) {
|
||||||
|
let hostName = getHostname()
|
||||||
|
let rpcURL = endpointURL.appendingPathComponent("publishTabInfo")
|
||||||
|
var components = URLComponents(url: rpcURL, resolvingAgainstBaseURL: false)!
|
||||||
|
components.queryItems = [
|
||||||
|
URLQueryItem(name: "host", value: hostName)
|
||||||
|
]
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
if let bodyData = try? encoder.encode(tabInfos) {
|
||||||
|
var request = URLRequest(url: components.url!)
|
||||||
|
request.httpBody = bodyData
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
|
||||||
|
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
|
||||||
|
if let error {
|
||||||
|
print("Error publishing tab info: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTask.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getTabInfos(_ completion: @escaping(Result<[String: [TabInfo]], Error>) -> Void) {
|
||||||
|
let rpcURL = endpointURL.appendingPathComponent("getTabInfos")
|
||||||
|
let request = URLRequest(url: rpcURL)
|
||||||
|
let myHostname = getHostname()
|
||||||
|
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
|
||||||
|
if let error {
|
||||||
|
print("Error getting tab infos: \(error.localizedDescription)")
|
||||||
|
DispatchQueue.main.async { completion(.failure(error)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
if let data {
|
||||||
|
do {
|
||||||
|
let result = try decoder.decode([String: [TabInfo]].self, from: data)
|
||||||
|
.filter({ (host, tabInfo) in
|
||||||
|
// Filter out tabs from the same machine.
|
||||||
|
return host != myHostname
|
||||||
|
})
|
||||||
|
|
||||||
|
DispatchQueue.main.async { completion(.success(result)) }
|
||||||
|
} catch {
|
||||||
|
print("Error decoding tabs: \(error.localizedDescription)")
|
||||||
|
DispatchQueue.main.async { completion(.failure(error)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTask.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
|
|||||||
get {
|
get {
|
||||||
TabInfo(
|
TabInfo(
|
||||||
title: loadedWebView?.title,
|
title: loadedWebView?.title,
|
||||||
url: loadedWebView?.url ?? self.homeURL,
|
urlString: loadedWebView?.url?.absoluteString ?? self.homeURL?.absoluteString,
|
||||||
favicon: self.favicon,
|
faviconData: self.favicon?.pngData(),
|
||||||
identifier: self.identifier
|
identifier: self.identifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,15 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
|
|||||||
|
|
||||||
private var loadedWebView: WKWebView? = nil
|
private var loadedWebView: WKWebView? = nil
|
||||||
public var title: String? { get { tabInfo.title } }
|
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 {
|
public var javaScriptEnabled: Bool = false {
|
||||||
didSet { bridge.allowAllScripts = javaScriptEnabled }
|
didSet { bridge.allowAllScripts = javaScriptEnabled }
|
||||||
|
|||||||
@@ -8,14 +8,21 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
struct TabInfo
|
struct TabInfo: Codable, Hashable
|
||||||
{
|
{
|
||||||
public var title: String?
|
public var title: String?
|
||||||
public var url: URL?
|
public var urlString: String?
|
||||||
public var favicon: UIImage?
|
public var faviconData: Data?
|
||||||
public var identifier = UUID()
|
public var identifier = UUID()
|
||||||
|
|
||||||
public static func ==(lhs: TabInfo, rhs: TabInfo) -> Bool {
|
public static func ==(lhs: TabInfo, rhs: TabInfo) -> Bool {
|
||||||
return lhs.identifier == rhs.identifier
|
return lhs.identifier == rhs.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case title
|
||||||
|
case urlString = "url"
|
||||||
|
case faviconData
|
||||||
|
case identifier
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import Combine
|
|||||||
protocol TabPickerViewControllerDelegate: AnyObject
|
protocol TabPickerViewControllerDelegate: AnyObject
|
||||||
{
|
{
|
||||||
func tabPicker(_ picker: TabPickerViewController, createNewTabWithURL: URL?)
|
func tabPicker(_ picker: TabPickerViewController, createNewTabWithURL: URL?)
|
||||||
func tabPicker(_ picker: TabPickerViewController, didSelectTabIdentifier tab: UUID)
|
|
||||||
func tabPicker(_ picker: TabPickerViewController, closeTabWithIdentifier tab: UUID)
|
func tabPicker(_ picker: TabPickerViewController, closeTabWithIdentifier tab: UUID)
|
||||||
func tabPicker(_ picker: TabPickerViewController, tabInfoForIdentifier: UUID) -> TabInfo
|
func tabPicker(_ picker: TabPickerViewController, tabInfoForIdentifier: UUID) -> TabInfo
|
||||||
|
func tabPicker(_ picker: TabPickerViewController, didSelectTabInfo: TabInfo, fromHost: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
||||||
@@ -22,13 +22,16 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
|||||||
|
|
||||||
public static var localHostIdentifier = "__localhost__";
|
public static var localHostIdentifier = "__localhost__";
|
||||||
|
|
||||||
public var selectedTabIdentifier: UUID?
|
public var selectedTabIdentifier: TabID?
|
||||||
public var selectedTabHost: String? { didSet { didChangeSelectedTabHost(selectedTabHost!) } }
|
public var selectedTabHost: String? { didSet { didChangeSelectedTabHost(selectedTabHost!) } }
|
||||||
|
|
||||||
weak var delegate: TabPickerViewControllerDelegate?
|
weak var delegate: TabPickerViewControllerDelegate?
|
||||||
public var tabObserver: AnyCancellable?
|
public var tabObserver: AnyCancellable?
|
||||||
private var selectedTabIdentifiersForEditing: Set<UUID> = []
|
public var displayedError: Error? = nil { didSet { didSetDisplayedError(displayedError) } }
|
||||||
private var tabIdentifiersByHost: [String: [UUID]] = [:]
|
|
||||||
|
private var displayedErrorView: UITextView?
|
||||||
|
private var selectedTabIdentifiersForEditing: Set<TabInfo> = []
|
||||||
|
private var tabIdentifiersByHost: [String: [TabInfo]] = [:]
|
||||||
|
|
||||||
private var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
private var listConfiguration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
|
||||||
private lazy var listLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
|
private lazy var listLayout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
|
||||||
@@ -38,43 +41,41 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
|||||||
collectionView.delegate = self
|
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()
|
var config = listCell.defaultContentConfiguration()
|
||||||
|
|
||||||
if let tab = delegate?.tabPicker(self, tabInfoForIdentifier: item) {
|
if let title = tab.title, title.count > 0 {
|
||||||
if let title = tab.title, title.count > 0 {
|
config.text = title
|
||||||
config.text = title
|
config.secondaryText = tab.urlString
|
||||||
config.secondaryText = tab.url?.absoluteString
|
} else if let url = tab.urlString {
|
||||||
} else if let url = tab.url {
|
config.text = url
|
||||||
config.text = url.absoluteString
|
config.secondaryText = url
|
||||||
config.secondaryText = url.absoluteString
|
} else {
|
||||||
} else {
|
config.text = "New Tab"
|
||||||
config.text = "New Tab"
|
}
|
||||||
}
|
|
||||||
|
config.textProperties.numberOfLines = 1
|
||||||
config.textProperties.numberOfLines = 1
|
config.secondaryTextProperties.numberOfLines = 1
|
||||||
config.secondaryTextProperties.numberOfLines = 1
|
|
||||||
|
if let faviconData = tab.faviconData, let image = UIImage(data: faviconData) {
|
||||||
if let image = tab.favicon {
|
config.image = image
|
||||||
config.image = image
|
} else {
|
||||||
} else {
|
config.image = UIImage(systemName: "safari")
|
||||||
config.image = UIImage(systemName: "safari")
|
}
|
||||||
}
|
|
||||||
|
config.imageProperties.maximumSize = CGSize(width: 21.0, height: 21.0)
|
||||||
config.imageProperties.maximumSize = CGSize(width: 21.0, height: 21.0)
|
config.imageProperties.cornerRadius = 3.0
|
||||||
config.imageProperties.cornerRadius = 3.0
|
|
||||||
|
if let selectedTabIdentifier, selectedTabIdentifier == tab.identifier {
|
||||||
if let selectedTabIdentifier, selectedTabIdentifier == item {
|
listCell.accessories = [ .checkmark() ]
|
||||||
listCell.accessories = [ .checkmark() ]
|
} else {
|
||||||
} else {
|
listCell.accessories = []
|
||||||
listCell.accessories = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listCell.contentConfiguration = config
|
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
|
{ [unowned self] (collectionView, indexPath, item) -> UICollectionViewCell? in
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: cellRegistry, for: indexPath, item: item)
|
return collectionView.dequeueConfiguredReusableCell(using: cellRegistry, for: indexPath, item: item)
|
||||||
}
|
}
|
||||||
@@ -96,12 +97,14 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var hostPickerButton: UIButton = {
|
private lazy var hostPickerButton: UIButton = {
|
||||||
var buttonConfiguration = UIButton.Configuration.filled()
|
var buttonConfiguration = UIButton.Configuration.bordered()
|
||||||
buttonConfiguration.title = "Host"
|
buttonConfiguration.title = "Host"
|
||||||
|
|
||||||
let button = UIButton(configuration: buttonConfiguration)
|
let button = UIButton(configuration: buttonConfiguration)
|
||||||
button.changesSelectionAsPrimaryAction = true
|
button.changesSelectionAsPrimaryAction = true
|
||||||
button.showsMenuAsPrimaryAction = true
|
button.showsMenuAsPrimaryAction = true
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
button.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
return button
|
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
|
return UISwipeActionsConfiguration(actions: [ UIContextualAction(style: .destructive, title: "Close", handler: { [unowned self] (action, view, completionHandler) in
|
||||||
if let item = dataSource.itemIdentifier(for: indexPath) {
|
if let item = dataSource.itemIdentifier(for: indexPath) {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
delegate?.tabPicker(self, closeTabWithIdentifier: item)
|
delegate?.tabPicker(self, closeTabWithIdentifier: item.identifier)
|
||||||
|
|
||||||
snapshot.deleteItems([ item ])
|
snapshot.deleteItems([ item ])
|
||||||
dataSource.apply(snapshot, animatingDifferences: true)
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
@@ -136,14 +139,18 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
|||||||
configureNavigationButtons(forEditing: isEditing)
|
configureNavigationButtons(forEditing: isEditing)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setTabIdentifiers(_ identifiers: [UUID], forHost host: String) {
|
public func setTabInfos(_ infos: [TabInfo], forHost host: String) {
|
||||||
tabIdentifiersByHost[host] = identifiers
|
let wasEmpty = tabIdentifiersByHost.isEmpty
|
||||||
if host == selectedTabHost {
|
tabIdentifiersByHost[host] = infos
|
||||||
|
|
||||||
|
if wasEmpty {
|
||||||
|
selectedTabHost = host
|
||||||
|
} else if host == selectedTabHost {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.deleteAllItems()
|
snapshot.deleteAllItems()
|
||||||
snapshot.appendSections([ 0 ])
|
snapshot.appendSections([ 0 ])
|
||||||
snapshot.appendItems(identifiers)
|
snapshot.appendItems(infos)
|
||||||
dataSource.apply(snapshot)
|
dataSource.apply(snapshot) // crashing here...
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadHostPickerButtonMenu()
|
reloadHostPickerButtonMenu()
|
||||||
@@ -187,7 +194,7 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
|||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
for tab in selectedTabIdentifiersForEditing {
|
for tab in selectedTabIdentifiersForEditing {
|
||||||
snapshot.deleteItems([ tab ])
|
snapshot.deleteItems([ tab ])
|
||||||
delegate?.tabPicker(self, closeTabWithIdentifier: tab)
|
delegate?.tabPicker(self, closeTabWithIdentifier: tab.identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: true)
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
@@ -203,11 +210,50 @@ class TabPickerViewController: UIViewController, UICollectionViewDelegate
|
|||||||
dataSource.applySnapshotUsingReloadData(snapshot)
|
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) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard let tab = dataSource.itemIdentifier(for: indexPath) else { return }
|
guard let tab = dataSource.itemIdentifier(for: indexPath) else { return }
|
||||||
|
|
||||||
if !isEditing {
|
if !isEditing {
|
||||||
delegate?.tabPicker(self, didSelectTabIdentifier: tab)
|
delegate?.tabPicker(self, didSelectTabInfo: tab, fromHost: selectedTabHost!)
|
||||||
} else {
|
} else {
|
||||||
deleteTabButton.isEnabled = collectionView.indexPathsForSelectedItems?.count ?? 0 > 0
|
deleteTabButton.isEnabled = collectionView.indexPathsForSelectedItems?.count ?? 0 > 0
|
||||||
selectedTabIdentifiersForEditing.update(with: tab)
|
selectedTabIdentifiersForEditing.update(with: tab)
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
CDE6A30425F023BC00E912A4 /* AmberSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A30325F023BC00E912A4 /* AmberSettingsViewController.swift */; };
|
CDE6A30425F023BC00E912A4 /* AmberSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A30325F023BC00E912A4 /* AmberSettingsViewController.swift */; };
|
||||||
CDE6A30625F023EA00E912A4 /* AmberSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A30525F023EA00E912A4 /* AmberSettingsView.swift */; };
|
CDE6A30625F023EA00E912A4 /* AmberSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A30525F023EA00E912A4 /* AmberSettingsView.swift */; };
|
||||||
CDEDD8AA25D62ADB00862605 /* UITraitCollection+MacLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEDD8A925D62ADB00862605 /* UITraitCollection+MacLike.swift */; };
|
CDEDD8AA25D62ADB00862605 /* UITraitCollection+MacLike.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDEDD8A925D62ADB00862605 /* UITraitCollection+MacLike.swift */; };
|
||||||
|
CDF255FD289DD7CF0059F021 /* AttractorServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF255FC289DD7CF0059F021 /* AttractorServer.swift */; };
|
||||||
CDF3468E276C105900FB3141 /* SettingsSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF3468D276C105900FB3141 /* SettingsSceneDelegate.swift */; };
|
CDF3468E276C105900FB3141 /* SettingsSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF3468D276C105900FB3141 /* SettingsSceneDelegate.swift */; };
|
||||||
CDF34690276C14BD00FB3141 /* CodeEditorSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */; };
|
CDF34690276C14BD00FB3141 /* CodeEditorSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -181,6 +182,7 @@
|
|||||||
CDE6A30325F023BC00E912A4 /* AmberSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmberSettingsViewController.swift; sourceTree = "<group>"; };
|
CDE6A30325F023BC00E912A4 /* AmberSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmberSettingsViewController.swift; sourceTree = "<group>"; };
|
||||||
CDE6A30525F023EA00E912A4 /* AmberSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmberSettingsView.swift; sourceTree = "<group>"; };
|
CDE6A30525F023EA00E912A4 /* AmberSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmberSettingsView.swift; sourceTree = "<group>"; };
|
||||||
CDEDD8A925D62ADB00862605 /* UITraitCollection+MacLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITraitCollection+MacLike.swift"; sourceTree = "<group>"; };
|
CDEDD8A925D62ADB00862605 /* UITraitCollection+MacLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITraitCollection+MacLike.swift"; sourceTree = "<group>"; };
|
||||||
|
CDF255FC289DD7CF0059F021 /* AttractorServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttractorServer.swift; sourceTree = "<group>"; };
|
||||||
CDF3468D276C105900FB3141 /* SettingsSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSceneDelegate.swift; sourceTree = "<group>"; };
|
CDF3468D276C105900FB3141 /* SettingsSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditorSettingsViewController.swift; sourceTree = "<group>"; };
|
CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditorSettingsViewController.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@@ -297,6 +299,7 @@
|
|||||||
CDC5DA3C25DB7A5500BA8D99 /* Reader View */,
|
CDC5DA3C25DB7A5500BA8D99 /* Reader View */,
|
||||||
1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */,
|
1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */,
|
||||||
CDE6A30225F023A000E912A4 /* Settings */,
|
CDE6A30225F023A000E912A4 /* Settings */,
|
||||||
|
CDF255FB289DD7BD0059F021 /* Sync */,
|
||||||
1AB88F0324D3E1EC0006F850 /* Tabs */,
|
1AB88F0324D3E1EC0006F850 /* Tabs */,
|
||||||
1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */,
|
1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */,
|
||||||
1ADFF4C124CA6AE4006DC7AE /* Utilities */,
|
1ADFF4C124CA6AE4006DC7AE /* Utilities */,
|
||||||
@@ -473,6 +476,14 @@
|
|||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
CDF255FB289DD7BD0059F021 /* Sync */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CDF255FC289DD7CF0059F021 /* AttractorServer.swift */,
|
||||||
|
);
|
||||||
|
path = Sync;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -617,6 +628,7 @@
|
|||||||
CDE6A30625F023EA00E912A4 /* AmberSettingsView.swift in Sources */,
|
CDE6A30625F023EA00E912A4 /* AmberSettingsView.swift in Sources */,
|
||||||
CD7A7E9D2686A9A500E20BA3 /* SettingsViewController.swift in Sources */,
|
CD7A7E9D2686A9A500E20BA3 /* SettingsViewController.swift in Sources */,
|
||||||
1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */,
|
1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */,
|
||||||
|
CDF255FD289DD7CF0059F021 /* AttractorServer.swift in Sources */,
|
||||||
1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */,
|
1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */,
|
||||||
CD7F2135265DAD010001D042 /* MFMailComposeViewControllerFix.m in Sources */,
|
CD7F2135265DAD010001D042 /* MFMailComposeViewControllerFix.m in Sources */,
|
||||||
CDAD9CE8263A2DF200FF7199 /* DocumentControlsView.swift in Sources */,
|
CDAD9CE8263A2DF200FF7199 /* DocumentControlsView.swift in Sources */,
|
||||||
|
|||||||
Reference in New Issue
Block a user