Remote tabs: finishing touches
This commit is contained in:
@@ -68,6 +68,9 @@ extension BrowserViewController: WKNavigationDelegate, WKUIDelegate
|
||||
let title = webView.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)
|
||||
|
||||
@@ -145,12 +145,12 @@ class BrowserViewController: UIViewController
|
||||
tabPickerController.tabObserver = tabController.$tabs
|
||||
.receive(on: RunLoop.main)
|
||||
.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
|
||||
let tabIdentifiers = tabController.tabs.map { $0.identifier }
|
||||
tabPickerController.setTabIdentifiers(tabIdentifiers, forHost: TabPickerViewController.localHostIdentifier)
|
||||
let tabInfos = tabController.tabs.map { $0.tabInfo }
|
||||
tabPickerController.setTabInfos(tabInfos, forHost: TabPickerViewController.localHostIdentifier)
|
||||
tabPickerController.selectedTabHost = TabPickerViewController.localHostIdentifier
|
||||
|
||||
let remoteTabPickerController = TabPickerViewController()
|
||||
@@ -160,6 +160,20 @@ class BrowserViewController: UIViewController
|
||||
remoteTabPickerController.newTabButton.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)
|
||||
tabBarController.viewControllers = [
|
||||
UINavigationController(rootViewController: tabPickerController),
|
||||
@@ -522,7 +536,7 @@ class BrowserViewController: UIViewController
|
||||
|
||||
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
||||
var findActions: [Selector] = []
|
||||
if #available(macCatalyst 16.0, *) {
|
||||
if #available(macCatalyst 16.0, iOS 16.0, *) {
|
||||
findActions = [
|
||||
#selector(UIResponder.find(_:)),
|
||||
#selector(UIResponder.findNext(_:)),
|
||||
@@ -645,8 +659,15 @@ extension BrowserViewController: TabPickerViewControllerDelegate
|
||||
return tab.tabInfo
|
||||
}
|
||||
|
||||
func tabPicker(_ picker: TabPickerViewController, didSelectTabIdentifier tabIdentifier: UUID) {
|
||||
guard let tab = tabController.tab(forIdentifier: tabIdentifier) else { return }
|
||||
func tabPicker(_ picker: TabPickerViewController, didSelectTabInfo info: TabInfo, fromHost host: String) {
|
||||
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
|
||||
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
|
||||
{
|
||||
var menu: UIMenu
|
||||
@@ -56,11 +80,13 @@ class GeneralSettingsViewController: UIViewController
|
||||
{
|
||||
enum Section: String, CaseIterable {
|
||||
case searchEngine = "Search Engine"
|
||||
case syncServer = "Sync Server"
|
||||
}
|
||||
|
||||
typealias Item = String
|
||||
|
||||
static let SearchProviderPopupItem = "searchProvider.popup"
|
||||
static let SyncServerItem = "syncServer.field"
|
||||
|
||||
let dataSource: UICollectionViewDiffableDataSource<Section, Item>
|
||||
let collectionView: UICollectionView
|
||||
@@ -109,7 +135,7 @@ class GeneralSettingsViewController: UIViewController
|
||||
if idiom == .mac {
|
||||
return LabelContentConfiguration(
|
||||
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
|
||||
)
|
||||
} else {
|
||||
@@ -141,11 +167,19 @@ class GeneralSettingsViewController: UIViewController
|
||||
})
|
||||
|
||||
cell.contentConfiguration = ButtonContentConfiguration(menu: menu)
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
cell.backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
|
||||
#endif
|
||||
} else if identifier == Self.SyncServerItem {
|
||||
cell.contentConfiguration = TextFieldContentConfiguration(
|
||||
text: Settings.shared.syncServer,
|
||||
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: {
|
||||
@@ -189,6 +223,7 @@ class GeneralSettingsViewController: UIViewController
|
||||
// iOS
|
||||
// snapshot.appendItems(Settings.SearchProviderSetting.allCases.map { $0.rawValue }, toSection: .searchEngine)
|
||||
snapshot.appendItems([ Self.SearchProviderPopupItem ], toSection: .searchEngine)
|
||||
snapshot.appendItems([ Self.SyncServerItem ], toSection: .syncServer)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
|
||||
@@ -105,4 +105,7 @@ class Settings
|
||||
|
||||
@SettingProperty(key: "userStylesheet")
|
||||
public var userStylesheet: String = ""
|
||||
|
||||
@SettingProperty(key: "syncServer")
|
||||
public var syncServer: String = "https://attractor.severnaya.net"
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
<true/>
|
||||
<key>com.apple.developer.web-browser</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</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 {
|
||||
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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
CDE6A30425F023BC00E912A4 /* AmberSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDE6A30325F023BC00E912A4 /* AmberSettingsViewController.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 */; };
|
||||
CDF255FD289DD7CF0059F021 /* AttractorServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF255FC289DD7CF0059F021 /* AttractorServer.swift */; };
|
||||
CDF3468E276C105900FB3141 /* SettingsSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF3468D276C105900FB3141 /* SettingsSceneDelegate.swift */; };
|
||||
CDF34690276C14BD00FB3141 /* CodeEditorSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -181,6 +182,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditorSettingsViewController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -297,6 +299,7 @@
|
||||
CDC5DA3C25DB7A5500BA8D99 /* Reader View */,
|
||||
1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */,
|
||||
CDE6A30225F023A000E912A4 /* Settings */,
|
||||
CDF255FB289DD7BD0059F021 /* Sync */,
|
||||
1AB88F0324D3E1EC0006F850 /* Tabs */,
|
||||
1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */,
|
||||
1ADFF4C124CA6AE4006DC7AE /* Utilities */,
|
||||
@@ -473,6 +476,14 @@
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CDF255FB289DD7BD0059F021 /* Sync */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CDF255FC289DD7CF0059F021 /* AttractorServer.swift */,
|
||||
);
|
||||
path = Sync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -617,6 +628,7 @@
|
||||
CDE6A30625F023EA00E912A4 /* AmberSettingsView.swift in Sources */,
|
||||
CD7A7E9D2686A9A500E20BA3 /* SettingsViewController.swift in Sources */,
|
||||
1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */,
|
||||
CDF255FD289DD7CF0059F021 /* AttractorServer.swift in Sources */,
|
||||
1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */,
|
||||
CD7F2135265DAD010001D042 /* MFMailComposeViewControllerFix.m in Sources */,
|
||||
CDAD9CE8263A2DF200FF7199 /* DocumentControlsView.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user