From e8c6111592930975297835ed84a7b7c98da9c9ec Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 5 Aug 2022 18:55:19 -0700 Subject: [PATCH] Remote tabs: finishing touches --- ...BrowserViewController+WebKitDelegate.swift | 3 + App/Browser View/BrowserViewController.swift | 33 ++++- .../GeneralSettingsViewController.swift | 45 +++++- App/Settings/Settings.swift | 3 + App/Supporting Files/Info.plist | 2 - App/Supporting Files/SBrowser.entitlements | 2 + App/Sync/AttractorServer.swift | 76 ++++++++++ App/Tabs/Tab.swift | 14 +- App/Tabs/TabInfo.swift | 13 +- App/Tabs/TabPickerViewController.swift | 132 ++++++++++++------ SBrowser.xcodeproj/project.pbxproj | 12 ++ 11 files changed, 273 insertions(+), 62 deletions(-) create mode 100644 App/Sync/AttractorServer.swift diff --git a/App/Browser View/BrowserViewController+WebKitDelegate.swift b/App/Browser View/BrowserViewController+WebKitDelegate.swift index 472fc62..6ef785a 100644 --- a/App/Browser View/BrowserViewController+WebKitDelegate.swift +++ b/App/Browser View/BrowserViewController+WebKitDelegate.swift @@ -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) diff --git a/App/Browser View/BrowserViewController.swift b/App/Browser View/BrowserViewController.swift index c1c624d..80160f0 100644 --- a/App/Browser View/BrowserViewController.swift +++ b/App/Browser View/BrowserViewController.swift @@ -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) diff --git a/App/Settings/GeneralSettingsViewController.swift b/App/Settings/GeneralSettingsViewController.swift index 96e65c2..23ff06b 100644 --- a/App/Settings/GeneralSettingsViewController.swift +++ b/App/Settings/GeneralSettingsViewController.swift @@ -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(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 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(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) } diff --git a/App/Settings/Settings.swift b/App/Settings/Settings.swift index 8605b6f..192ce91 100644 --- a/App/Settings/Settings.swift +++ b/App/Settings/Settings.swift @@ -105,4 +105,7 @@ class Settings @SettingProperty(key: "userStylesheet") public var userStylesheet: String = "" + + @SettingProperty(key: "syncServer") + public var syncServer: String = "https://attractor.severnaya.net" } diff --git a/App/Supporting Files/Info.plist b/App/Supporting Files/Info.plist index 2e42c72..baa99bb 100644 --- a/App/Supporting Files/Info.plist +++ b/App/Supporting Files/Info.plist @@ -40,8 +40,6 @@ NSAllowsArbitraryLoads - NSAllowsArbitraryLoadsInWebContent - UIApplicationSceneManifest diff --git a/App/Supporting Files/SBrowser.entitlements b/App/Supporting Files/SBrowser.entitlements index f033e7a..44b0952 100644 --- a/App/Supporting Files/SBrowser.entitlements +++ b/App/Supporting Files/SBrowser.entitlements @@ -8,5 +8,7 @@ com.apple.developer.web-browser + com.apple.developer.device-information.user-assigned-device-name + diff --git a/App/Sync/AttractorServer.swift b/App/Sync/AttractorServer.swift new file mode 100644 index 0000000..6aed49e --- /dev/null +++ b/App/Sync/AttractorServer.swift @@ -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() + } +} diff --git a/App/Tabs/Tab.swift b/App/Tabs/Tab.swift index 2b4f7cc..0a5f92e 100644 --- a/App/Tabs/Tab.swift +++ b/App/Tabs/Tab.swift @@ -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 } diff --git a/App/Tabs/TabInfo.swift b/App/Tabs/TabInfo.swift index 8b8d45c..0d5e569 100644 --- a/App/Tabs/TabInfo.swift +++ b/App/Tabs/TabInfo.swift @@ -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 + } } diff --git a/App/Tabs/TabPickerViewController.swift b/App/Tabs/TabPickerViewController.swift index 23f812d..b3318d3 100644 --- a/App/Tabs/TabPickerViewController.swift +++ b/App/Tabs/TabPickerViewController.swift @@ -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 = [] - private var tabIdentifiersByHost: [String: [UUID]] = [:] + public var displayedError: Error? = nil { didSet { didSetDisplayedError(displayedError) } } + + private var displayedErrorView: UITextView? + private var selectedTabIdentifiersForEditing: Set = [] + 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 { [unowned self] (listCell, indexPath, item) in + private lazy var cellRegistry = UICollectionView.CellRegistration { [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(collectionView: collectionView) + private lazy var dataSource = UICollectionViewDiffableDataSource(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) diff --git a/SBrowser.xcodeproj/project.pbxproj b/SBrowser.xcodeproj/project.pbxproj index e78cc14..57c305f 100644 --- a/SBrowser.xcodeproj/project.pbxproj +++ b/SBrowser.xcodeproj/project.pbxproj @@ -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 = ""; }; CDE6A30525F023EA00E912A4 /* AmberSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmberSettingsView.swift; sourceTree = ""; }; CDEDD8A925D62ADB00862605 /* UITraitCollection+MacLike.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITraitCollection+MacLike.swift"; sourceTree = ""; }; + CDF255FC289DD7CF0059F021 /* AttractorServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttractorServer.swift; sourceTree = ""; }; CDF3468D276C105900FB3141 /* SettingsSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSceneDelegate.swift; sourceTree = ""; }; CDF3468F276C14BD00FB3141 /* CodeEditorSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeEditorSettingsViewController.swift; sourceTree = ""; }; /* 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 = ""; }; + CDF255FB289DD7BD0059F021 /* Sync */ = { + isa = PBXGroup; + children = ( + CDF255FC289DD7CF0059F021 /* AttractorServer.swift */, + ); + path = Sync; + sourceTree = ""; + }; /* 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 */,