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

@@ -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)

View File

@@ -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)

View File

@@ -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)
} }

View File

@@ -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"
} }

View File

@@ -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>

View File

@@ -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>

View 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()
}
}

View File

@@ -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 }

View File

@@ -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
}
} }

View File

@@ -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 image = tab.favicon { if let faviconData = tab.faviconData, let image = UIImage(data: faviconData) {
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 == item { if let selectedTabIdentifier, selectedTabIdentifier == tab.identifier {
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)

View File

@@ -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 */,