Add CloudKit syncing add account UI.

This commit is contained in:
Maurice Parker 2020-03-18 15:48:44 -05:00
parent 770206df60
commit e3d46960fd
11 changed files with 341 additions and 7 deletions

View File

@ -36,6 +36,7 @@ public extension Notification.Name {
public enum AccountType: Int, Codable {
// Raw values should not change since theyre stored on disk.
case onMyMac = 1
case cloudKit = 2
case feedly = 16
case feedbin = 17
case feedWrangler = 18
@ -232,6 +233,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
switch type {
case .onMyMac:
self.delegate = LocalAccountDelegate()
case .cloudKit:
self.delegate = CloudKitAccountDelegate()
case .feedbin:
self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport)
case .freshRSS:
@ -256,6 +259,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
switch type {
case .onMyMac:
defaultName = Account.defaultLocalAccountName
case .cloudKit:
defaultName = "iCloud"
case .feedly:
defaultName = "Feedly"
case .feedbin:

View File

@ -18,6 +18,7 @@
3B826DAE2385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA52385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift */; };
3B826DAF2385C81C00FC1ADB /* FeedWranglerGenericResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DA62385C81C00FC1ADB /* FeedWranglerGenericResult.swift */; };
3BC23AB92385ECB100371CBA /* FeedWranglerSubscriptionResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC23AB82385ECB100371CBA /* FeedWranglerSubscriptionResult.swift */; };
5103A9D92422546800410853 /* CloudKitAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103A9D82422546800410853 /* CloudKitAppDelegate.swift */; };
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
510BD111232C3801002692E4 /* AccountMetadataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510BD110232C3801002692E4 /* AccountMetadataFile.swift */; };
@ -230,6 +231,7 @@
3B826DA52385C81C00FC1ADB /* FeedWranglerSubscriptionsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerSubscriptionsRequest.swift; sourceTree = "<group>"; };
3B826DA62385C81C00FC1ADB /* FeedWranglerGenericResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerGenericResult.swift; sourceTree = "<group>"; };
3BC23AB82385ECB100371CBA /* FeedWranglerSubscriptionResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedWranglerSubscriptionResult.swift; sourceTree = "<group>"; };
5103A9D82422546800410853 /* CloudKitAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitAppDelegate.swift; sourceTree = "<group>"; };
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = "<group>"; };
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
@ -450,6 +452,14 @@
path = FeedWrangler;
sourceTree = "<group>";
};
5103A9D7242253DC00410853 /* CloudKit */ = {
isa = PBXGroup;
children = (
5103A9D82422546800410853 /* CloudKitAppDelegate.swift */,
);
path = CloudKit;
sourceTree = "<group>";
};
5111D71C2357534700737D45 /* Feedbin */ = {
isa = PBXGroup;
children = (
@ -606,6 +616,7 @@
3B826D9D2385C81C00FC1ADB /* FeedWrangler */,
552032EA229D5D5A009559E0 /* ReaderAPI */,
9EA31339231E368100268BA0 /* Feedly */,
5103A9D7242253DC00410853 /* CloudKit */,
848935031F62484F00CEBD24 /* AccountTests */,
848934F71F62484F00CEBD24 /* Products */,
8469F80F1F6DC3C10084783E /* Frameworks */,
@ -844,11 +855,11 @@
848934F51F62484F00CEBD24 = {
CreatedOnToolsVersion = 9.0;
LastSwiftMigration = 0900;
ProvisioningStyle = Manual;
ProvisioningStyle = Automatic;
};
848934FE1F62484F00CEBD24 = {
CreatedOnToolsVersion = 9.0;
ProvisioningStyle = Manual;
ProvisioningStyle = Automatic;
};
};
};
@ -1008,6 +1019,7 @@
84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */,
841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */,
510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */,
5103A9D92422546800410853 /* CloudKitAppDelegate.swift in Sources */,
5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */,
9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */,
9EEEF71F23545CB4009E9D80 /* FeedlySendArticleStatusesOperation.swift in Sources */,

View File

@ -120,7 +120,7 @@ public final class AccountManager: UnreadCountProvider {
// MARK: - API
public func createAccount(type: AccountType) -> Account {
let accountID = UUID().uuidString
let accountID = type == .cloudKit ? "iCloud" : UUID().uuidString
let accountFolder = (accountsFolder as NSString).appendingPathComponent("\(type.rawValue)_\(accountID)")
do {

View File

@ -0,0 +1,216 @@
//
// CloudKitAppDelegate.swift
// Account
//
// Created by Maurice Parker on 3/18/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import RSParser
import Articles
import RSWeb
public enum CloudKitAccountDelegateError: String, Error {
case invalidParameter = "An invalid parameter was used."
}
final class CloudKitAccountDelegate: AccountDelegate {
let behaviors: AccountBehaviors = []
let isOPMLImportInProgress = false
let server: String? = nil
var credentials: Credentials?
var accountMetadata: AccountMetadata?
private let refresher = LocalAccountRefresher()
var refreshProgress: DownloadProgress {
return refresher.progress
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
refresher.refreshFeeds(account.flattenedWebFeeds()) {
account.metadata.lastArticleFetchEndTime = Date()
completion(.success(()))
}
}
func sendArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
completion(.success(()))
}
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
completion(.success(()))
}
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
var fileData: Data?
do {
fileData = try Data(contentsOf: opmlFile)
} catch {
completion(.failure(error))
return
}
guard let opmlData = fileData else {
completion(.success(()))
return
}
let parserData = ParserData(url: opmlFile.absoluteString, data: opmlData)
var opmlDocument: RSOPMLDocument?
do {
opmlDocument = try RSOPMLParser.parseOPML(with: parserData)
} catch {
completion(.failure(error))
return
}
guard let loadDocument = opmlDocument else {
completion(.success(()))
return
}
guard let children = loadDocument.children else {
return
}
BatchUpdate.shared.perform {
account.loadOPMLItems(children, parentFolder: nil)
}
completion(.success(()))
}
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
guard let url = URL(string: urlString) else {
completion(.failure(LocalAccountDelegateError.invalidParameter))
return
}
refreshProgress.addToNumberOfTasksAndRemaining(1)
FeedFinder.find(url: url) { result in
switch result {
case .success(let feedSpecifiers):
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers),
let url = URL(string: bestFeedSpecifier.urlString) else {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
return
}
if account.hasWebFeed(withURL: bestFeedSpecifier.urlString) {
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorAlreadySubscribed))
return
}
let feed = account.createWebFeed(with: nil, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
InitialFeedDownloader.download(url) { parsedFeed in
self.refreshProgress.completeTask()
if let parsedFeed = parsedFeed {
account.update(feed, with: parsedFeed, {_ in})
}
feed.editedName = name
container.addWebFeed(feed)
completion(.success(feed))
}
case .failure:
self.refreshProgress.completeTask()
completion(.failure(AccountError.createErrorNotFound))
}
}
}
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
feed.editedName = name
completion(.success(()))
}
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
container.removeWebFeed(feed)
completion(.success(()))
}
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
from.removeWebFeed(feed)
to.addWebFeed(feed)
completion(.success(()))
}
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
container.addWebFeed(feed)
completion(.success(()))
}
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
container.addWebFeed(feed)
completion(.success(()))
}
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
if let folder = account.ensureFolder(with: name) {
completion(.success(folder))
} else {
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
}
}
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
folder.name = name
completion(.success(()))
}
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
account.removeFolder(folder)
completion(.success(()))
}
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
account.addFolder(folder)
completion(.success(()))
}
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
return try? account.update(articles, statusKey: statusKey, flag: flag)
}
func accountDidInitialize(_ account: Account) {
}
func accountWillBeDeleted(_ account: Account) {
}
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result<Credentials?, Error>) -> Void) {
return completion(.success(nil))
}
// MARK: Suspend and Resume (for iOS)
func suspendNetwork() {
refresher.suspend()
}
func suspendDatabase() {
// Nothing to do
}
func resume() {
refresher.resume()
}
}

View File

@ -21,6 +21,10 @@ struct AppAssets {
return RSImage(named: .timelineStar)
}()
static var accountCloudKit: RSImage! = {
return RSImage(named: "accountCloudKit")
}()
static var accountLocal: RSImage! = {
return RSImage(named: "accountLocal")
}()
@ -129,6 +133,8 @@ struct AppAssets {
switch accountType {
case .onMyMac:
return AppAssets.accountLocal
case .cloudKit:
return AppAssets.accountCloudKit
case .feedbin:
return AppAssets.accountFeedbin
case .feedly:

View File

@ -0,0 +1,42 @@
//
// AccountsAddCloudKitWindowController.swift
// NetNewsWire
//
// Created by Maurice Parker on 3/18/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
class AccountsAddCloudKitWindowController: NSWindowController {
private weak var hostWindow: NSWindow?
convenience init() {
self.init(windowNibName: NSNib.Name("AccountsAddCloudKit"))
}
override func windowDidLoad() {
super.windowDidLoad()
}
// MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) {
self.hostWindow = hostWindow
hostWindow.beginSheet(window!, completionHandler: completion)
}
// MARK: Actions
@IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
}
@IBAction func create(_ sender: Any) {
_ = AccountManager.shared.createAccount(type: .cloudKit)
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
}
}

View File

@ -16,7 +16,11 @@ class AccountsAddViewController: NSViewController {
private var accountsAddWindowController: NSWindowController?
private let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .freshRSS]
#if DEBUG
private var addableAccountTypes: [AccountType] = [.onMyMac, .cloudKit, .feedbin, .feedly, .feedWrangler, .freshRSS]
#else
private var addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly]
#endif
init() {
super.init(nibName: "AccountsAdd", bundle: nil)
@ -27,12 +31,10 @@ class AccountsAddViewController: NSViewController {
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
removeCloudKitIfNecessary()
}
}
@ -63,6 +65,9 @@ extension AccountsAddViewController: NSTableViewDelegate {
case .onMyMac:
cell.accountNameLabel?.stringValue = Account.defaultLocalAccountName
cell.accountImageView?.image = AppAssets.accountLocal
case .cloudKit:
cell.accountNameLabel?.stringValue = NSLocalizedString("iCloud", comment: "iCloud")
cell.accountImageView?.image = AppAssets.accountCloudKit
case .feedbin:
cell.accountNameLabel?.stringValue = NSLocalizedString("Feedbin", comment: "Feedbin")
cell.accountImageView?.image = AppAssets.accountFeedbin
@ -95,6 +100,15 @@ extension AccountsAddViewController: NSTableViewDelegate {
let accountsAddLocalWindowController = AccountsAddLocalWindowController()
accountsAddLocalWindowController.runSheetOnWindow(self.view.window!)
accountsAddWindowController = accountsAddLocalWindowController
case .cloudKit:
let accountsAddCloudKitWindowController = AccountsAddCloudKitWindowController()
accountsAddCloudKitWindowController.runSheetOnWindow(self.view.window!) { response in
if response == NSApplication.ModalResponse.OK {
self.removeCloudKitIfNecessary()
self.tableView.reloadData()
}
}
accountsAddWindowController = accountsAddCloudKitWindowController
case .feedbin:
let accountsFeedbinWindowController = AccountsFeedbinWindowController()
accountsFeedbinWindowController.runSheetOnWindow(self.view.window!)
@ -142,3 +156,13 @@ extension AccountsAddViewController: OAuthAccountAuthorizationOperationDelegate
view.window?.presentError(error)
}
}
// MARK: Private
private extension AccountsAddViewController {
func removeCloudKitIfNecessary() {
if let index = AccountManager.shared.activeAccounts.firstIndex(where: { $0.type == .cloudKit }) {
addableAccountTypes.remove(at: index)
}
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "icloud.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

View File

@ -159,6 +159,8 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
switch self.account.type {
case .onMyMac:
osType = "Locl"
case .cloudKit:
osType = "Clkt"
case .feedly:
osType = "Fdly"
case .feedbin:

View File

@ -17,6 +17,10 @@
5103A9982421643300410853 /* blank.html in Resources */ = {isa = PBXBuildFile; fileRef = 5103A9972421643300410853 /* blank.html */; };
5103A9992421643300410853 /* blank.html in Resources */ = {isa = PBXBuildFile; fileRef = 5103A9972421643300410853 /* blank.html */; };
5103A9B424216A4200410853 /* blank.html in Resources */ = {isa = PBXBuildFile; fileRef = 5103A9B324216A4200410853 /* blank.html */; };
5103A9F4242258C600410853 /* AccountsAddCloudKit.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5103A9DA242258C600410853 /* AccountsAddCloudKit.xib */; };
5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5103A9DA242258C600410853 /* AccountsAddCloudKit.xib */; };
5103A9F724225E4C00410853 /* AccountsAddCloudKitWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103A9F624225E4C00410853 /* AccountsAddCloudKitWindowController.swift */; };
5103A9F824225E4C00410853 /* AccountsAddCloudKitWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5103A9F624225E4C00410853 /* AccountsAddCloudKitWindowController.swift */; };
5108F6B62375E612001ABC45 /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; };
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; };
5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6D12375EED2001ABC45 /* TimelineCustomizerViewController.swift */; };
@ -1255,6 +1259,8 @@
49F40DEF2335B71000552BF4 /* newsfoot.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = newsfoot.js; sourceTree = "<group>"; };
5103A9972421643300410853 /* blank.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = blank.html; sourceTree = "<group>"; };
5103A9B324216A4200410853 /* blank.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = blank.html; sourceTree = "<group>"; };
5103A9DA242258C600410853 /* AccountsAddCloudKit.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsAddCloudKit.xib; sourceTree = "<group>"; };
5103A9F624225E4C00410853 /* AccountsAddCloudKitWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddCloudKitWindowController.swift; sourceTree = "<group>"; };
5108F6B52375E612001ABC45 /* CacheCleaner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheCleaner.swift; sourceTree = "<group>"; };
5108F6D12375EED2001ABC45 /* TimelineCustomizerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineCustomizerViewController.swift; sourceTree = "<group>"; };
5108F6D32375EEEF001ABC45 /* TimelinePreviewTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePreviewTableViewController.swift; sourceTree = "<group>"; };
@ -2616,6 +2622,8 @@
55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */,
5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */,
5144EA372279FC6200D19003 /* AccountsAddLocalWindowController.swift */,
5103A9DA242258C600410853 /* AccountsAddCloudKit.xib */,
5103A9F624225E4C00410853 /* AccountsAddCloudKitWindowController.swift */,
);
path = Accounts;
sourceTree = "<group>";
@ -3468,6 +3476,7 @@
65ED405B235DEF6C0081F399 /* KeyboardShortcuts.html in Resources */,
65ED405C235DEF6C0081F399 /* ImportOPMLSheet.xib in Resources */,
65ED405D235DEF6C0081F399 /* SidebarKeyboardShortcuts.plist in Resources */,
5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */,
65ED405E235DEF6C0081F399 /* DefaultFeeds.opml in Resources */,
65ED405F235DEF6C0081F399 /* Preferences.storyboard in Resources */,
65ED4060235DEF6C0081F399 /* (null) in Resources */,
@ -3556,6 +3565,7 @@
84C9FC8C22629E8F00D921D6 /* KeyboardShortcuts.html in Resources */,
5144EA3B227A379E00D19003 /* ImportOPMLSheet.xib in Resources */,
844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */,
5103A9F4242258C600410853 /* AccountsAddCloudKit.xib in Resources */,
84A3EE5F223B667F00557320 /* DefaultFeeds.opml in Resources */,
849C78902362AAFC009A71E4 /* ExportOPMLSheet.xib in Resources */,
84C9FC8222629E4800D921D6 /* Preferences.storyboard in Resources */,
@ -3778,6 +3788,7 @@
65ED3FD6235DEF6C0081F399 /* MarkStatusCommand.swift in Sources */,
65ED3FD7235DEF6C0081F399 /* NSApplication+Scriptability.swift in Sources */,
65ED3FD8235DEF6C0081F399 /* NSView-Extensions.swift in Sources */,
5103A9F824225E4C00410853 /* AccountsAddCloudKitWindowController.swift in Sources */,
65ED3FD9235DEF6C0081F399 /* SidebarCell.swift in Sources */,
65ED3FDA235DEF6C0081F399 /* ArticleStatusSyncTimer.swift in Sources */,
65ED3FDB235DEF6C0081F399 /* WebFeedTreeControllerDelegate.swift in Sources */,
@ -4129,6 +4140,7 @@
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */,
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */,
5103A9F724225E4C00410853 /* AccountsAddCloudKitWindowController.swift in Sources */,
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */,
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */,
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,