From 10f4351904786cc6c01c66e2e5f49a11ea204588 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 22 Jan 2025 22:20:08 -0800 Subject: [PATCH] Fix lint issues. --- Shared/AccountType+Helpers.swift | 8 +- Shared/Activity/ActivityManager.swift | 104 +++++++++--------- .../ArticleExtractor/ArticleExtractor.swift | 42 +++---- .../ArticleExtractor/ExtractedArticle.swift | 4 +- Shared/ArticleRendering/ArticleRenderer.swift | 49 ++++----- Shared/ArticleRendering/ArticleTextSize.swift | 8 +- Shared/ArticleStyles/ArticleTheme.swift | 38 +++---- .../ArticleThemeDownloader.swift | 26 ++--- Shared/ArticleStyles/ArticleThemePlist.swift | 2 +- .../ArticleStyles/ArticleThemesManager.swift | 30 ++--- Shared/Commands/DeleteCommand.swift | 64 +++++------ Shared/Commands/MarkStatusCommand.swift | 14 +-- Shared/Constants.swift | 2 +- Shared/Exporters/OPMLExporter.swift | 2 +- .../SendToMarsEditCommand.swift | 4 +- .../SendToMicroBlogCommand.swift | 2 +- .../Extensions/AddFeedDefaultContainer.swift | 12 +- .../Extensions/ArticleStringFormatter.swift | 1 - Shared/Extensions/ArticleUtilities.swift | 68 ++++++------ Shared/Extensions/CacheCleaner.swift | 10 +- Shared/Extensions/IconImage.swift | 59 +++++----- .../NSAttributedString+NetNewsWire.swift | 11 +- Shared/Extensions/NSView-Extensions.swift | 2 +- Shared/Extensions/Node-Extensions.swift | 22 ++-- Shared/Extensions/RSImage-Extensions.swift | 8 +- Shared/Extensions/URL-Extensions.swift | 10 +- Shared/Favicons/ColorHash.swift | 14 +-- Shared/Favicons/FaviconDownloader.swift | 24 ++-- Shared/Favicons/FaviconGenerator.swift | 8 +- Shared/Favicons/SingleFaviconDownloader.swift | 11 +- Shared/IconImageCache.swift | 4 +- Shared/Images/AuthorAvatarDownloader.swift | 9 +- Shared/Images/FeedIconDownloader.swift | 2 +- Shared/Images/HTMLMetadata+Extension.swift | 6 +- Shared/Images/ImageUtilities.swift | 1 - Shared/Importers/DefaultFeedsImporter.swift | 7 +- .../ShareExtension/ExtensionContainers.swift | 12 +- .../ExtensionContainersFile.swift | 18 +-- .../ExtensionFeedAddRequest.swift | 4 +- .../ExtensionFeedAddRequestFile.swift | 62 +++++------ .../ShareDefaultContainer.swift | 10 +- Shared/SmartFeeds/PseudoFeed.swift | 2 +- Shared/SmartFeeds/SearchFeedDelegate.swift | 1 - Shared/SmartFeeds/SmartFeed.swift | 10 +- Shared/SmartFeeds/SmartFeedDelegate.swift | 2 +- .../SmartFeedPasteboardWriter.swift | 1 - Shared/SmartFeeds/SmartFeedsController.swift | 6 +- Shared/SmartFeeds/TodayFeedDelegate.swift | 5 +- Shared/SmartFeeds/UnreadFeed.swift | 12 +- Shared/Timeline/ArticleArray.swift | 19 ++-- Shared/Timeline/ArticleSorter.swift | 14 +-- Shared/Timeline/FetchRequestOperation.swift | 9 +- Shared/Timeline/FetchRequestQueue.swift | 6 +- Shared/Timer/AccountRefreshTimer.swift | 22 ++-- Shared/Timer/ArticleStatusSyncTimer.swift | 32 +++--- Shared/Timer/RefreshInterval.swift | 8 +- Shared/Tree/FeedTreeControllerDelegate.swift | 22 ++-- .../Tree/FolderTreeControllerDelegate.swift | 18 +-- Shared/UserInfoKey.swift | 4 +- .../UserNotificationManager.swift | 26 ++--- Shared/Widget/WidgetData.swift | 1 - Shared/Widget/WidgetDataDecoder.swift | 6 +- Shared/Widget/WidgetDataEncoder.swift | 25 ++--- Shared/Widget/WidgetDeepLinks.swift | 6 +- 64 files changed, 506 insertions(+), 545 deletions(-) diff --git a/Shared/AccountType+Helpers.swift b/Shared/AccountType+Helpers.swift index dd5d04083..809eacc0d 100644 --- a/Shared/AccountType+Helpers.swift +++ b/Shared/AccountType+Helpers.swift @@ -16,11 +16,11 @@ import UIKit import SwiftUI extension AccountType { - + // TODO: Move this to the Account Package. - + func localizedAccountName() -> String { - + switch self { case .onMyMac: let defaultName: String @@ -52,7 +52,7 @@ extension AccountType { return NSLocalizedString("The Old Reader", comment: "Account name") } } - + // MARK: - SwiftUI Images func image() -> Image { switch self { diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index b20488cf1..4ff43c4b3 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -16,7 +16,7 @@ import Intents import UniformTypeIdentifiers class ActivityManager { - + private var nextUnreadActivity: NSUserActivity? private var selectingActivity: NSUserActivity? private var readingActivity: NSUserActivity? @@ -26,11 +26,11 @@ class ActivityManager { if let activity = readingActivity { return activity } - + if let activity = selectingActivity { return activity } - + let activity = NSUserActivity(activityType: ActivityType.restoration.rawValue) #if os(iOS) activity.persistentIdentifier = UUID().uuidString @@ -38,40 +38,40 @@ class ActivityManager { activity.becomeCurrent() return activity } - + init() { NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) } - + func invalidateCurrentActivities() { invalidateReading() invalidateSelecting() invalidateNextUnread() } - + func selecting(feed: SidebarItem) { invalidateCurrentActivities() - + selectingActivity = makeSelectFeedActivity(feed: feed) - + if let feed = feed as? Feed { updateSelectingActivityFeedSearchAttributes(with: feed) } - + donate(selectingActivity!) } - + func invalidateSelecting() { selectingActivity?.invalidate() selectingActivity = nil } - + func selectingNextUnread() { guard nextUnreadActivity == nil else { return } nextUnreadActivity = NSUserActivity(activityType: ActivityType.nextUnread.rawValue) nextUnreadActivity!.title = NSLocalizedString("See first unread article", comment: "First Unread") - + #if os(iOS) nextUnreadActivity!.suggestedInvocationPhrase = nextUnreadActivity!.title nextUnreadActivity!.isEligibleForPrediction = true @@ -81,60 +81,60 @@ class ActivityManager { donate(nextUnreadActivity!) } - + func invalidateNextUnread() { nextUnreadActivity?.invalidate() nextUnreadActivity = nil } - + func reading(feed: SidebarItem?, article: Article?) { invalidateReading() invalidateNextUnread() - + guard let article = article else { return } readingActivity = makeReadArticleActivity(feed: feed, article: article) - + #if os(iOS) updateReadArticleSearchAttributes(with: article) #endif - + donate(readingActivity!) } - + func invalidateReading() { readingActivity?.invalidate() readingActivity = nil readingArticle = nil } - + #if os(iOS) static func cleanUp(_ account: Account) { var ids = [String]() - + if let folders = account.folders { for folder in folders { ids.append(identifier(for: folder)) } } - + for feed in account.flattenedFeeds() { ids.append(contentsOf: identifiers(for: feed)) } - + CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } - + static func cleanUp(_ folder: Folder) { var ids = [String]() ids.append(identifier(for: folder)) - + for feed in folder.flattenedFeeds() { ids.append(contentsOf: identifiers(for: feed)) } - + CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids) } - + static func cleanUp(_ feed: Feed) { CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: feed)) } @@ -144,13 +144,13 @@ class ActivityManager { guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.feedID] as? String else { return } - + #if os(iOS) if let article = readingArticle, activityFeedId == article.feedID { updateReadArticleSearchAttributes(with: article) } #endif - + if activityFeedId == feed.feedID { updateSelectingActivityFeedSearchAttributes(with: feed) } @@ -161,17 +161,17 @@ class ActivityManager { // MARK: Private private extension ActivityManager { - + func makeSelectFeedActivity(feed: SidebarItem) -> NSUserActivity { let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue) - + let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder") let title = NSString.localizedStringWithFormat(localizedText as NSString, feed.nameForDisplay) as String activity.title = title - + activity.keywords = Set(makeKeywords(title)) activity.isEligibleForSearch = true - + let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]() activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo] activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String }) @@ -186,11 +186,11 @@ private extension ActivityManager { return activity } - + func makeReadArticleActivity(feed: SidebarItem?, article: Article) -> NSUserActivity { let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue) activity.title = ArticleStringFormatter.truncatedTitle(article) - + if let feed = feed { let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]() let articlePathUserInfo = article.pathUserInfo @@ -199,9 +199,9 @@ private extension ActivityManager { activity.userInfo = [UserInfoKey.articlePath: article.pathUserInfo] } activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String }) - + activity.isEligibleForHandoff = true - + activity.persistentIdentifier = ActivityManager.identifier(for: article) #if os(iOS) @@ -212,13 +212,13 @@ private extension ActivityManager { #endif readingArticle = article - + return activity } - + #if os(iOS) func updateReadArticleSearchAttributes(with article: Article) { - + let attributeSet = CSSearchableItemAttributeSet(itemContentType: UTType.compositeContent.identifier) attributeSet.title = ArticleStringFormatter.truncatedTitle(article) attributeSet.contentDescription = article.summary @@ -228,25 +228,25 @@ private extension ActivityManager { if let iconImage = article.iconImage() { attributeSet.thumbnailData = iconImage.image.pngData() } - + readingActivity?.contentAttributeSet = attributeSet readingActivity?.needsSave = true - + } #endif - + func makeKeywords(_ article: Article) -> [String] { let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay) let articleTitleKeywords = makeKeywords(ArticleStringFormatter.truncatedTitle(article)) return feedNameKeywords + articleTitleKeywords } - + func makeKeywords(_ value: String?) -> [String] { return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? [] } - + func updateSelectingActivityFeedSearchAttributes(with feed: Feed) { - + let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.compositeContent) attributeSet.title = feed.nameForDisplay attributeSet.keywords = makeKeywords(feed.nameForDisplay) @@ -258,9 +258,9 @@ private extension ActivityManager { selectingActivity!.contentAttributeSet = attributeSet selectingActivity!.needsSave = true - + } - + func donate(_ activity: NSUserActivity) { // You have to put the search item in the index or the activity won't index // itself because the relatedUniqueIdentifier on the activity attributeset is populated. @@ -270,22 +270,22 @@ private extension ActivityManager { let searchableItem = CSSearchableItem(uniqueIdentifier: identifier, domainIdentifier: nil, attributeSet: tempAttributeSet) CSSearchableIndex.default().indexSearchableItems([searchableItem]) } - + activity.becomeCurrent() } - + static func identifier(for folder: Folder) -> String { return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)" } - + static func identifier(for feed: Feed) -> String { return "account_\(feed.account!.accountID)_feed_\(feed.feedID)" } - + static func identifier(for article: Article) -> String { return "account_\(article.accountID)_feed_\(article.feedID)_article_\(article.articleID)" } - + static func identifiers(for feed: Feed) -> [String] { var ids = [String]() ids.append(identifier(for: feed)) diff --git a/Shared/ArticleExtractor/ArticleExtractor.swift b/Shared/ArticleExtractor/ArticleExtractor.swift index 0eb09bc5f..e15bee5cc 100644 --- a/Shared/ArticleExtractor/ArticleExtractor.swift +++ b/Shared/ArticleExtractor/ArticleExtractor.swift @@ -24,23 +24,23 @@ protocol ArticleExtractorDelegate { } class ArticleExtractor { - - private var dataTask: URLSessionDataTask? = nil - + + private var dataTask: URLSessionDataTask? + var state: ArticleExtractorState! var article: ExtractedArticle? var delegate: ArticleExtractorDelegate? var articleLink: String? - + private var url: URL! - + public init?(_ articleLink: String) { self.articleLink = articleLink - + let clientURL = "https://extract.feedbin.com/parser" let username = SecretKey.mercuryClientID let signature = articleLink.hmacUsingSHA1(key: SecretKey.mercuryClientSecret) - + if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() { let fullURL = "\(clientURL)/\(username)/\(signature)?base64_url=\(base64URL)" if let url = URL(string: fullURL) { @@ -48,18 +48,18 @@ class ArticleExtractor { return } } - + return nil } - + public func process() { - + state = .processing - dataTask = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in - + dataTask = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in + guard let self = self else { return } - + if let error = error { self.state = .failedToParse DispatchQueue.main.async { @@ -67,7 +67,7 @@ class ArticleExtractor { } return } - + guard let data = data else { self.state = .failedToParse DispatchQueue.main.async { @@ -75,12 +75,12 @@ class ArticleExtractor { } return } - + do { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 self.article = try decoder.decode(ExtractedArticle.self, from: data) - + DispatchQueue.main.async { if self.article?.content == nil { self.state = .failedToParse @@ -96,16 +96,16 @@ class ArticleExtractor { self.delegate?.articleExtractionDidFail(with: error) } } - + } - + dataTask!.resume() - + } - + public func cancel() { state = .cancelled dataTask?.cancel() } - + } diff --git a/Shared/ArticleExtractor/ExtractedArticle.swift b/Shared/ArticleExtractor/ExtractedArticle.swift index 460302477..40c45820c 100644 --- a/Shared/ArticleExtractor/ExtractedArticle.swift +++ b/Shared/ArticleExtractor/ExtractedArticle.swift @@ -24,7 +24,7 @@ struct ExtractedArticle: Codable, Equatable { let direction: String? let totalPages: Int? let renderedPages: Int? - + enum CodingKeys: String, CodingKey { case title = "title" case author = "author" @@ -41,5 +41,5 @@ struct ExtractedArticle: Codable, Equatable { case totalPages = "total_pages" case renderedPages = "rendered_pages" } - + } diff --git a/Shared/ArticleRendering/ArticleRenderer.swift b/Shared/ArticleRendering/ArticleRenderer.swift index 6bc71b48d..788ff1cdc 100644 --- a/Shared/ArticleRendering/ArticleRenderer.swift +++ b/Shared/ArticleRendering/ArticleRenderer.swift @@ -17,12 +17,12 @@ import Account struct ArticleRenderer { typealias Rendering = (style: String, html: String, title: String, baseURL: String) - + struct Page { let url: URL let baseURL: URL let html: String - + init(name: String) { url = Bundle.main.url(forResource: name, withExtension: "html")! baseURL = url.deletingLastPathComponent() @@ -31,17 +31,17 @@ struct ArticleRenderer { } static var imageIconScheme = "nnwImageIcon" - + static var blank = Page(name: "blank") static var page = Page(name: "page") - + private let article: Article? private let extractedArticle: ExtractedArticle? private let articleTheme: ArticleTheme private let title: String private let body: String private let baseURL: String? - + private static let longDateTimeFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long @@ -140,7 +140,7 @@ struct ArticleRenderer { let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme) return (renderer.articleCSS, renderer.noSelectionHTML, renderer.title, renderer.baseURL ?? "") } - + static func noContentHTML(theme: ArticleTheme) -> Rendering { let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme) return (renderer.articleCSS, renderer.noContentHTML, renderer.title, renderer.baseURL ?? "") @@ -173,7 +173,7 @@ private extension ArticleRenderer { private var noContentHTML: String { return "" } - + private var articleCSS: String { return try! MacroProcessor.renderedText(withTemplate: styleString(), substitutions: styleSubstitutions()) } @@ -205,10 +205,10 @@ private extension ArticleRenderer { assertionFailure("Article should have been set before calling this function.") return d } - + d["title"] = title d["preferred_link"] = article.preferredLink ?? "" - + if let externalLink = article.externalLink, externalLink != article.preferredLink { d["external_link_label"] = NSLocalizedString("Link:", comment: "Link") d["external_link_stripped"] = externalLink.strippingHTTPOrHTTPSScheme @@ -218,23 +218,22 @@ private extension ArticleRenderer { d["external_link_stripped"] = "" d["external_link"] = "" } - + d["body"] = body - + #if os(macOS) d["text_size_class"] = AppDefaults.shared.articleTextSize.cssClass #endif - + var components = URLComponents() components.scheme = Self.imageIconScheme components.path = article.articleID if let imageIconURLString = components.string { d["avatar_src"] = imageIconURLString - } - else { + } else { d["avatar_src"] = "" } - + if self.title.isEmpty { d["dateline_style"] = "articleDatelineTitle" } else { @@ -273,7 +272,7 @@ private extension ArticleRenderer { return "" } } - + var byline = "" var isFirstAuthor = true @@ -283,27 +282,22 @@ private extension ArticleRenderer { } isFirstAuthor = false - var authorEmailAddress: String? = nil + var authorEmailAddress: String? if let emailAddress = author.emailAddress, !(emailAddress.contains("noreply@") || emailAddress.contains("no-reply@")) { authorEmailAddress = emailAddress } if let emailAddress = authorEmailAddress, emailAddress.contains(" ") { byline += emailAddress // probably name plus email address - } - else if let name = author.name, let url = author.url { + } else if let name = author.name, let url = author.url { byline += name.htmlByAddingLink(url) - } - else if let name = author.name, let emailAddress = authorEmailAddress { + } else if let name = author.name, let emailAddress = authorEmailAddress { byline += "\(name) <\(emailAddress)>" - } - else if let name = author.name { + } else if let name = author.name { byline += name - } - else if let emailAddress = authorEmailAddress { + } else if let emailAddress = authorEmailAddress { byline += "<\(emailAddress)>" // TODO: mailto link - } - else if let url = author.url { + } else if let url = author.url { byline += String.htmlWithLink(url) } } @@ -355,4 +349,3 @@ private extension Article { return url } } - diff --git a/Shared/ArticleRendering/ArticleTextSize.swift b/Shared/ArticleRendering/ArticleTextSize.swift index bbc433974..456bee65d 100644 --- a/Shared/ArticleRendering/ArticleTextSize.swift +++ b/Shared/ArticleRendering/ArticleTextSize.swift @@ -14,9 +14,9 @@ enum ArticleTextSize: Int, CaseIterable, Identifiable { case large = 3 case xlarge = 4 case xxlarge = 5 - + var id: String { description() } - + var cssClass: String { switch self { case .small: @@ -31,7 +31,7 @@ enum ArticleTextSize: Int, CaseIterable, Identifiable { return "xxLargeText" } } - + func description() -> String { switch self { case .small: @@ -46,5 +46,5 @@ enum ArticleTextSize: Int, CaseIterable, Identifiable { return NSLocalizedString("Extra Extra Large", comment: "XX-Large") } } - + } diff --git a/Shared/ArticleStyles/ArticleTheme.swift b/Shared/ArticleStyles/ArticleTheme.swift index 1d9eeea9b..61438bf4b 100644 --- a/Shared/ArticleStyles/ArticleTheme.swift +++ b/Shared/ArticleStyles/ArticleTheme.swift @@ -9,58 +9,58 @@ import Foundation struct ArticleTheme: Equatable { - + static let defaultTheme = ArticleTheme() static let nnwThemeSuffix = ".nnwtheme" - + private static let defaultThemeName = NSLocalizedString("Default", comment: "Default") private static let unknownValue = NSLocalizedString("Unknown", comment: "Unknown Value") - + let url: URL? let template: String? let css: String? let isAppTheme: Bool - + var name: String { guard let url else { return Self.defaultThemeName } return Self.themeNameForPath(url.path) } - + var creatorHomePage: String { return info?.creatorHomePage ?? Self.unknownValue } - + var creatorName: String { return info?.creatorName ?? Self.unknownValue } - + var version: String { return String(describing: info?.version ?? 0) } - + private let info: ArticleThemePlist? - + init() { self.url = nil self.info = ArticleThemePlist(name: "Article Theme", themeIdentifier: "com.ranchero.netnewswire.theme.article", creatorHomePage: "https://netnewswire.com/", creatorName: "Ranchero Software", version: 1) - + let corePath = Bundle.main.path(forResource: "core", ofType: "css")! let stylesheetPath = Bundle.main.path(forResource: "stylesheet", ofType: "css")! css = Self.stringAtPath(corePath)! + "\n" + Self.stringAtPath(stylesheetPath)! - + let templatePath = Bundle.main.path(forResource: "template", ofType: "html")! template = Self.stringAtPath(templatePath)! - + isAppTheme = true } - + init(url: URL, isAppTheme: Bool) throws { _ = url.startAccessingSecurityScopedResource() defer { url.stopAccessingSecurityScopedResource() } - + self.url = url let coreURL = Bundle.main.url(forResource: "core", withExtension: "css")! @@ -85,25 +85,25 @@ struct ArticleTheme: Equatable { if !FileManager.default.fileExists(atPath: f) { return nil } - + if let s = try? NSString(contentsOfFile: f, usedEncoding: nil) as String { return s } return nil } - + static func filenameWithThemeSuffixRemoved(_ filename: String) -> String { return filename.stripping(suffix: Self.nnwThemeSuffix) } - + static func themeNameForPath(_ f: String) -> String { let filename = (f as NSString).lastPathComponent return filenameWithThemeSuffixRemoved(filename) } - + static func pathIsPathForThemeName(_ themeName: String, path: String) -> Bool { let filename = (path as NSString).lastPathComponent return filenameWithThemeSuffixRemoved(filename) == themeName } - + } diff --git a/Shared/ArticleStyles/ArticleThemeDownloader.swift b/Shared/ArticleStyles/ArticleThemeDownloader.swift index d6714e169..c07095b1d 100644 --- a/Shared/ArticleStyles/ArticleThemeDownloader.swift +++ b/Shared/ArticleStyles/ArticleThemeDownloader.swift @@ -10,10 +10,10 @@ import Foundation import Zip public class ArticleThemeDownloader { - + public enum ArticleThemeDownloaderError: LocalizedError { case noThemeFile - + public var errorDescription: String? { switch self { case .noThemeFile: @@ -21,23 +21,22 @@ public class ArticleThemeDownloader { } } } - + public static let shared = ArticleThemeDownloader() private init() {} - + public func handleFile(at location: URL) throws { createDownloadDirectoryIfRequired() let movedFileLocation = try moveTheme(from: location) let unzippedFileLocation = try unzipFile(at: movedFileLocation) - NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil, userInfo: ["url" : unzippedFileLocation]) + NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil, userInfo: ["url": unzippedFileLocation]) } - - + /// Creates `Application Support/NetNewsWire/Downloads` if needed. private func createDownloadDirectoryIfRequired() { try? FileManager.default.createDirectory(at: downloadDirectory(), withIntermediateDirectories: true, attributes: nil) } - + /// Moves the downloaded `.tmp` file to the `downloadDirectory` and renames it a `.zip` /// - Parameter location: The temporary file location. /// - Returns: Destination `URL`. @@ -48,7 +47,7 @@ public class ArticleThemeDownloader { try FileManager.default.moveItem(at: location, to: fileUrl) return fileUrl } - + /// Unzips the zip file /// - Parameter location: Location of the zip archive. /// - Returns: Enclosed `.nnwtheme` file. @@ -67,8 +66,7 @@ public class ArticleThemeDownloader { throw error } } - - + /// Performs a deep search of the unzipped directory to find the theme file. /// - Parameter searchPath: directory to search /// - Returns: optional `String` @@ -80,16 +78,16 @@ public class ArticleThemeDownloader { } } } - + return nil } - + /// The download directory used by the theme downloader: `Application Support/NetNewsWire/Downloads` /// - Returns: `URL` private func downloadDirectory() -> URL { FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("NetNewsWire/Downloads", isDirectory: true) } - + /// Removes downloaded themes, where themes == folders, from `Application Support/NetNewsWire/Downloads`. public func cleanUp() { guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: downloadDirectory().path) else { diff --git a/Shared/ArticleStyles/ArticleThemePlist.swift b/Shared/ArticleStyles/ArticleThemePlist.swift index f324cdbde..306b1dc8a 100644 --- a/Shared/ArticleStyles/ArticleThemePlist.swift +++ b/Shared/ArticleStyles/ArticleThemePlist.swift @@ -14,7 +14,7 @@ public struct ArticleThemePlist: Codable, Equatable { public var creatorHomePage: String public var creatorName: String public var version: Int - + enum CodingKeys: String, CodingKey { case name = "Name" case themeIdentifier = "ThemeIdentifier" diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index 3075235d0..6fb4b42ca 100644 --- a/Shared/ArticleStyles/ArticleThemesManager.swift +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -54,49 +54,49 @@ final class ArticleThemesManager: NSObject, NSFilePresenter { self.currentTheme = ArticleTheme.defaultTheme super.init() - + do { try FileManager.default.createDirectory(atPath: folderPath, withIntermediateDirectories: true, attributes: nil) } catch { assertionFailure("Could not create folder for Themes.") abort() } - + updateThemeNames() updateCurrentTheme() NSFileCoordinator.addFilePresenter(self) } - + func presentedSubitemDidChange(at url: URL) { updateThemeNames() updateCurrentTheme() } // MARK: API - + func themeExists(filename: String) -> Bool { let filenameLastPathComponent = (filename as NSString).lastPathComponent let toFilename = (folderPath as NSString).appendingPathComponent(filenameLastPathComponent) return FileManager.default.fileExists(atPath: toFilename) } - + func importTheme(filename: String) throws { let filenameLastPathComponent = (filename as NSString).lastPathComponent let toFilename = (folderPath as NSString).appendingPathComponent(filenameLastPathComponent) - + if FileManager.default.fileExists(atPath: toFilename) { try FileManager.default.removeItem(atPath: toFilename) } - + try FileManager.default.copyItem(atPath: filename, toPath: toFilename) } - + func articleThemeWithThemeName(_ themeName: String) -> ArticleTheme? { if themeName == AppDefaults.defaultThemeName { return ArticleTheme.defaultTheme } - + let url: URL let isAppTheme: Bool if let appThemeURL = Bundle.main.url(forResource: themeName, withExtension: ArticleTheme.nnwThemeSuffix) { @@ -108,14 +108,14 @@ final class ArticleThemesManager: NSObject, NSFilePresenter { } else { return nil } - + do { return try ArticleTheme(url: url, isAppTheme: isAppTheme) } catch { NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) return nil } - + } func deleteTheme(themeName: String) { @@ -123,10 +123,10 @@ final class ArticleThemesManager: NSObject, NSFilePresenter { try? FileManager.default.removeItem(atPath: filename) } } - + } -// MARK : Private +// MARK: Private private extension ArticleThemesManager { @@ -137,7 +137,7 @@ private extension ArticleThemesManager { let installedThemeNames = Set(allThemePaths(folderPath).map { ArticleTheme.themeNameForPath($0) }) let allThemeNames = appThemeNames.union(installedThemeNames) - + let sortedThemeNames = allThemeNames.sorted(by: { $0.compare($1, options: .caseInsensitive) == .orderedAscending }) if sortedThemeNames != themeNames { themeNames = sortedThemeNames @@ -179,5 +179,5 @@ private extension ArticleThemesManager { } return nil } - + } diff --git a/Shared/Commands/DeleteCommand.swift b/Shared/Commands/DeleteCommand.swift index 5de96bb26..451e771d9 100644 --- a/Shared/Commands/DeleteCommand.swift +++ b/Shared/Commands/DeleteCommand.swift @@ -20,11 +20,11 @@ final class DeleteCommand: UndoableCommand { var redoActionName: String { return undoActionName } - let errorHandler: (Error) -> () + let errorHandler: (Error) -> Void private let itemSpecifiers: [SidebarItemSpecifier] - init?(nodesToDelete: [Node], treeController: TreeController? = nil, undoManager: UndoManager, errorHandler: @escaping (Error) -> ()) { + init?(nodesToDelete: [Node], treeController: TreeController? = nil, undoManager: UndoManager, errorHandler: @escaping (Error) -> Void) { guard DeleteCommand.canDelete(nodesToDelete) else { return nil @@ -38,7 +38,7 @@ final class DeleteCommand: UndoableCommand { self.undoManager = undoManager self.errorHandler = errorHandler - let itemSpecifiers = nodesToDelete.compactMap{ SidebarItemSpecifier(node: $0, errorHandler: errorHandler) } + let itemSpecifiers = nodesToDelete.compactMap { SidebarItemSpecifier(node: $0, errorHandler: errorHandler) } guard !itemSpecifiers.isEmpty else { return nil } @@ -46,21 +46,21 @@ final class DeleteCommand: UndoableCommand { } func perform() { - + let group = DispatchGroup() for itemSpecifier in itemSpecifiers { group.enter() - itemSpecifier.delete() { + itemSpecifier.delete { group.leave() } } - + group.notify(queue: DispatchQueue.main) { self.treeController?.rebuild() self.registerUndo() } } - + func undo() { for itemSpecifier in itemSpecifiers { itemSpecifier.restore() @@ -101,7 +101,7 @@ private struct SidebarItemSpecifier { private let folder: Folder? private let feed: Feed? private let path: ContainerPath - private let errorHandler: (Error) -> () + private let errorHandler: (Error) -> Void private var container: Container? { if let parentFolder = parentFolder { @@ -113,7 +113,7 @@ private struct SidebarItemSpecifier { return nil } - init?(node: Node, errorHandler: @escaping (Error) -> ()) { + init?(node: Node, errorHandler: @escaping (Error) -> Void) { var account: Account? @@ -123,13 +123,11 @@ private struct SidebarItemSpecifier { self.feed = feed self.folder = nil account = feed.account - } - else if let folder = node.representedObject as? Folder { + } else if let folder = node.representedObject as? Folder { self.feed = nil self.folder = folder account = folder.account - } - else { + } else { return nil } if account == nil { @@ -138,36 +136,36 @@ private struct SidebarItemSpecifier { self.account = account! self.path = ContainerPath(account: account!, folders: node.containingFolders()) - + self.errorHandler = errorHandler - + } func delete(completion: @escaping () -> Void) { if let feed = feed { - + guard let container = path.resolveContainer() else { completion() return } - + BatchUpdate.shared.start() account?.removeFeed(feed, from: container) { result in BatchUpdate.shared.end() completion() self.checkResult(result) } - + } else if let folder = folder { - + BatchUpdate.shared.start() account?.removeFolder(folder) { result in BatchUpdate.shared.end() completion() self.checkResult(result) } - + } } @@ -175,8 +173,7 @@ private struct SidebarItemSpecifier { if let _ = feed { restoreFeed() - } - else if let _ = folder { + } else if let _ = folder { restoreFolder() } } @@ -186,13 +183,13 @@ private struct SidebarItemSpecifier { guard let account = account, let feed = feed, let container = path.resolveContainer() else { return } - + BatchUpdate.shared.start() account.restoreFeed(feed, container: container) { result in BatchUpdate.shared.end() self.checkResult(result) } - + } private func restoreFolder() { @@ -200,17 +197,17 @@ private struct SidebarItemSpecifier { guard let account = account, let folder = folder else { return } - + BatchUpdate.shared.start() account.restoreFolder(folder) { result in BatchUpdate.shared.end() self.checkResult(result) } - + } private func checkResult(_ result: Result) { - + switch result { case .success: break @@ -219,11 +216,11 @@ private struct SidebarItemSpecifier { } } - + } private extension Node { - + func parentFolder() -> Folder? { guard let parentNode = self.parent else { @@ -246,8 +243,7 @@ private extension Node { while nomad != nil { if let folder = nomad!.representedObject as? Folder { folders += [folder] - } - else { + } else { break } nomad = nomad!.parent @@ -274,11 +270,9 @@ private struct DeleteActionName { for node in nodes { if let _ = node.representedObject as? Feed { numberOfFeeds += 1 - } - else if let _ = node.representedObject as? Folder { + } else if let _ = node.representedObject as? Folder { numberOfFolders += 1 - } - else { + } else { return nil // Delete only Feeds and Folders. } } diff --git a/Shared/Commands/MarkStatusCommand.swift b/Shared/Commands/MarkStatusCommand.swift index 79cf9777c..729fd6d51 100644 --- a/Shared/Commands/MarkStatusCommand.swift +++ b/Shared/Commands/MarkStatusCommand.swift @@ -13,17 +13,17 @@ import Articles // Mark articles read/unread, starred/unstarred, deleted/undeleted. final class MarkStatusCommand: UndoableCommand { - + let undoActionName: String let redoActionName: String let articles: Set
let undoManager: UndoManager let flag: Bool let statusKey: ArticleStatus.Key - var completion: (() -> Void)? = nil + var completion: (() -> Void)? init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) { - + // Filter out articles that already have the desired status or can't be marked. let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag) if articlesToMark.isEmpty { @@ -54,7 +54,7 @@ final class MarkStatusCommand: UndoableCommand { mark(statusKey, flag) registerUndo() } - + func undo() { mark(statusKey, !flag) registerRedo() @@ -62,7 +62,7 @@ final class MarkStatusCommand: UndoableCommand { } private extension MarkStatusCommand { - + func mark(_ statusKey: ArticleStatus.Key, _ flag: Bool) { markArticles(articles, statusKey: statusKey, flag: flag, completion: completion) completion = nil @@ -85,12 +85,12 @@ private extension MarkStatusCommand { static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] { - return articles.filter{ article in + return articles.filter { article in guard article.status.boolStatus(forKey: statusKey) != flag else { return false } guard statusKey == .read else { return true } guard !article.status.read || article.isAvailableToMarkUnread else { return false } return true } - + } } diff --git a/Shared/Constants.swift b/Shared/Constants.swift index 2d221cb43..392e40b7a 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -9,6 +9,6 @@ import Foundation struct Constants { - + static let prototypeText = "You are about to being reading Italo Calvino’s new novel, *If on a winter’s night a traveler*. Relax. Concentrate. Dispel every other thought. Let the world around you fade. Best to close the door; the TV is always on in the next room. Tell the others right away, “No, I don’t want to watch TV!” Raise your voice—they won’t hear you otherwise—“I’m reading! I don’t want to be disturbed!” Maybe they haven’t heard you, with all that racket; speak louder, yell: “I’m beginning to read Italo Calvino’s new novel!” Or if you prefer, don’t say anything; just hope they’ll leave you alone. Find the most comfortable position: seated, stretched out, curled up, or lying flat. Flat on your back, on your side, on your stomach. In an easy chair, on the sofa, in the rocker, the deck chair, on the hassock. In the hammock, if you have a hammock. On top of your bed, of course, or in the bed. You can even stand on your hands, head down, in the yoga position. With the book upside down, naturally." } diff --git a/Shared/Exporters/OPMLExporter.swift b/Shared/Exporters/OPMLExporter.swift index cba135a17..5cf027c9f 100644 --- a/Shared/Exporters/OPMLExporter.swift +++ b/Shared/Exporters/OPMLExporter.swift @@ -24,7 +24,7 @@ struct OPMLExporter { \(escapedTitle) - + """ let middleText = account.OPMLString(indentLevel: 0) diff --git a/Shared/ExtensionPoints/SendToMarsEditCommand.swift b/Shared/ExtensionPoints/SendToMarsEditCommand.swift index 49576b1c7..a027d623f 100644 --- a/Shared/ExtensionPoints/SendToMarsEditCommand.swift +++ b/Shared/ExtensionPoints/SendToMarsEditCommand.swift @@ -57,9 +57,9 @@ private extension SendToMarsEditCommand { let authorName = article.authors?.first?.name let sender = SendToBlogEditorApp(targetDescriptor: targetDescriptor, title: article.title, body: body, summary: article.summary, link: article.externalLink, permalink: article.link, subject: nil, creator: authorName, commentsURL: nil, guid: article.uniqueID, sourceName: article.feed?.nameForDisplay, sourceHomeURL: article.feed?.homePageURL, sourceFeedURL: article.feed?.url) - let _ = sender.send() + sender.send() } - + func appToUse() -> UserApp? { for app in marsEditApps { diff --git a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift index 342bec319..dbecdc77d 100644 --- a/Shared/ExtensionPoints/SendToMicroBlogCommand.swift +++ b/Shared/ExtensionPoints/SendToMicroBlogCommand.swift @@ -28,7 +28,7 @@ final class SendToMicroBlogCommand: SendToCommand { return true } - + func sendObject(_ object: Any?, selectedText: String?) { guard canSendObject(object, selectedText: selectedText) else { diff --git a/Shared/Extensions/AddFeedDefaultContainer.swift b/Shared/Extensions/AddFeedDefaultContainer.swift index 6bf7fa880..cd0c8abd6 100644 --- a/Shared/Extensions/AddFeedDefaultContainer.swift +++ b/Shared/Extensions/AddFeedDefaultContainer.swift @@ -10,9 +10,9 @@ import Foundation import Account struct AddFeedDefaultContainer { - + static var defaultContainer: Container? { - + if let accountID = AppDefaults.shared.addFeedAccountID, let account = AccountManager.shared.activeAccounts.first(where: { $0.accountID == accountID }) { if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) { return folder @@ -24,9 +24,9 @@ struct AddFeedDefaultContainer { } else { return nil } - + } - + static func saveDefaultContainer(_ container: Container) { AppDefaults.shared.addFeedAccountID = container.account?.accountID if let folder = container as? Folder { @@ -35,7 +35,7 @@ struct AddFeedDefaultContainer { AppDefaults.shared.addFeedFolderName = nil } } - + private static func substituteContainerIfNeeded(account: Account) -> Container? { if !account.behaviors.contains(.disallowFeedInRootFolder) { return account @@ -47,5 +47,5 @@ struct AddFeedDefaultContainer { } } } - + } diff --git a/Shared/Extensions/ArticleStringFormatter.swift b/Shared/Extensions/ArticleStringFormatter.swift index a331f4adf..784626d0f 100644 --- a/Shared/Extensions/ArticleStringFormatter.swift +++ b/Shared/Extensions/ArticleStringFormatter.swift @@ -116,4 +116,3 @@ struct ArticleStringFormatter { return dateFormatter.string(from: date) } } - diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift index 0f7272e18..c0548c351 100644 --- a/Shared/Extensions/ArticleUtilities.swift +++ b/Shared/Extensions/ArticleUtilities.swift @@ -14,11 +14,11 @@ import Account // These handle multiple accounts. func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) { - + let d: [String: Set
] = accountAndArticlesDictionary(articles) let group = DispatchGroup() - + for (accountID, accountArticles) in d { guard let account = AccountManager.shared.existingAccount(with: accountID) else { continue @@ -28,42 +28,42 @@ func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: group.leave() } } - + group.notify(queue: .main) { completion?() } } private func accountAndArticlesDictionary(_ articles: Set
) -> [String: Set
] { - + let d = Dictionary(grouping: articles, by: { $0.accountID }) - return d.mapValues{ Set($0) } + return d.mapValues { Set($0) } } extension Article { - + var feed: Feed? { return account?.existingFeed(withFeedID: feedID) } - + var url: URL? { return URL.reparingIfRequired(rawLink) } - + var externalURL: URL? { return URL.reparingIfRequired(rawExternalLink) } - + var imageURL: URL? { return URL.reparingIfRequired(rawImageLink) } - + var link: String? { // Prefer link from URL, if one can be created, as these are repaired if required. // Provide the raw link if URL creation fails. return url?.absoluteString ?? rawLink } - + var externalLink: String? { // Prefer link from externalURL, if one can be created, as these are repaired if required. // Provide the raw link if URL creation fails. @@ -85,19 +85,19 @@ extension Article { } return nil } - + var preferredURL: URL? { return url ?? externalURL } - + var body: String? { return contentHTML ?? contentText ?? summary } - + var logicalDatePublished: Date { return datePublished ?? dateModified ?? status.dateArrived } - + var isAvailableToMarkUnread: Bool { guard let markUnreadWindow = account?.behaviors.compactMap( { behavior -> Int? in switch behavior { @@ -109,7 +109,7 @@ extension Article { }).first else { return true } - + if logicalDatePublished.byAdding(days: markUnreadWindow) > Date() { return true } else { @@ -120,7 +120,7 @@ extension Article { func iconImage() -> IconImage? { return IconImageCache.shared.imageForArticle(self) } - + func iconImageUrl(feed: Feed) -> URL? { if let image = iconImage() { let fm = FileManager.default @@ -137,7 +137,7 @@ extension Article { return nil } } - + func byline() -> String { guard let authors = authors ?? feed?.authors, !authors.isEmpty else { return "" @@ -151,7 +151,7 @@ extension Article { return "" } } - + var byline = "" var isFirstAuthor = true @@ -160,32 +160,28 @@ extension Article { byline += ", " } isFirstAuthor = false - - var authorEmailAddress: String? = nil + + var authorEmailAddress: String? if let emailAddress = author.emailAddress, !(emailAddress.contains("noreply@") || emailAddress.contains("no-reply@")) { authorEmailAddress = emailAddress } if let emailAddress = authorEmailAddress, emailAddress.contains(" ") { byline += emailAddress // probably name plus email address - } - else if let name = author.name, let emailAddress = authorEmailAddress { + } else if let name = author.name, let emailAddress = authorEmailAddress { byline += "\(name) <\(emailAddress)>" - } - else if let name = author.name { + } else if let name = author.name { byline += name - } - else if let emailAddress = authorEmailAddress { + } else if let emailAddress = authorEmailAddress { byline += "<\(emailAddress)>" - } - else if let url = author.url { + } else if let url = author.url { byline += url } } return byline } - + } // MARK: Path @@ -199,7 +195,7 @@ struct ArticlePathKey { extension Article { - public var pathUserInfo: [AnyHashable : Any] { + public var pathUserInfo: [AnyHashable: Any] { return [ ArticlePathKey.accountID: accountID, ArticlePathKey.accountName: account?.nameForDisplay ?? "", @@ -213,21 +209,21 @@ extension Article { // MARK: SortableArticle extension Article: SortableArticle { - + var sortableName: String { return feed?.name ?? "" } - + var sortableDate: Date { return logicalDatePublished } - + var sortableArticleID: String { return articleID } - + var sortableFeedID: String { return feedID } - + } diff --git a/Shared/Extensions/CacheCleaner.swift b/Shared/Extensions/CacheCleaner.swift index fa00603e7..5fe39fe07 100644 --- a/Shared/Extensions/CacheCleaner.swift +++ b/Shared/Extensions/CacheCleaner.swift @@ -20,7 +20,7 @@ struct CacheCleaner { AppDefaults.shared.lastImageCacheFlushDate = Date() return } - + // If the image disk cache hasn't been flushed for 3 days and the network is available, delete it if flushDate.addingTimeInterval(3600 * 24 * 3) < Date() { if let reachability = try? Reachability(hostname: "apple.com") { @@ -41,13 +41,13 @@ struct CacheCleaner { os_log(.error, log: self.log, "Could not delete cache file: %@", error.localizedDescription) } } - + AppDefaults.shared.lastImageCacheFlushDate = Date() - + } } } - + } - + } diff --git a/Shared/Extensions/IconImage.swift b/Shared/Extensions/IconImage.swift index aa0a4f3e9..70aa691a6 100644 --- a/Shared/Extensions/IconImage.swift +++ b/Shared/Extensions/IconImage.swift @@ -15,15 +15,15 @@ import UIKit import RSCore final class IconImage { - + lazy var isDark: Bool = { return image.isDark() }() - + lazy var isBright: Bool = { return image.isBright() }() - + let image: RSImage let isSymbol: Bool let isBackgroundSuppressed: Bool @@ -35,7 +35,7 @@ final class IconImage { self.preferredColor = preferredColor self.isBackgroundSuppressed = isBackgroundSuppressed } - + } #if os(macOS) @@ -43,7 +43,7 @@ final class IconImage { func isDark() -> Bool { return self.cgImage(forProposedRect: nil, context: nil, hints: nil)?.isDark() ?? false } - + func isBright() -> Bool { return self.cgImage(forProposedRect: nil, context: nil, hints: nil)?.isBright() ?? false } @@ -53,14 +53,14 @@ final class IconImage { func isDark() -> Bool { return self.cgImage?.isDark() ?? false } - + func isBright() -> Bool { return self.cgImage?.isBright() ?? false } } #endif -fileprivate enum ImageLuminanceType { +private enum ImageLuminanceType { case regular, bright, dark } @@ -72,18 +72,18 @@ extension CGImage { } return luminanceType == .bright } - + func isDark() -> Bool { guard let luminanceType = getLuminanceType() else { return false } return luminanceType == .dark } - + fileprivate func getLuminanceType() -> ImageLuminanceType? { - + // This has been rewritten with information from https://christianselig.com/2021/04/efficient-average-color/ - + // First, resize the image. We do this for two reasons, 1) less pixels to deal with means faster // calculation and a resized image still has the "gist" of the colors, and 2) the image we're dealing // with may come in any of a variety of color formats (CMYK, ARGB, RGBA, etc.) which complicates things, @@ -91,32 +91,32 @@ extension CGImage { // 40x40 is a good size to resize to still preserve quite a bit of detail but not have too many pixels // to deal with. Aspect ratio is irrelevant for just finding average color. let size = CGSize(width: 40, height: 40) - + let width = Int(size.width) let height = Int(size.height) let totalPixels = width * height - + let colorSpace = CGColorSpaceCreateDeviceRGB() - + // ARGB format let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue - + // 8 bits for each color channel, we're doing ARGB so 32 bits (4 bytes) total, and thus if the image is n pixels wide, // and has 4 bytes per pixel, the total bytes per row is 4n. That gives us 2^8 = 256 color variations for each RGB channel // or 256 * 256 * 256 = ~16.7M color options in total. That seems like a lot, but lots of HDR movies are in 10 bit, which // is (2^10)^3 = 1 billion color options! guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil } - + // Draw our resized image context.draw(self, in: CGRect(origin: .zero, size: size)) - + guard let pixelBuffer = context.data else { return nil } - + // Bind the pixel buffer's memory location to a pointer we can use/access let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height) - + var totalLuminance = 0.0 - + // Column of pixels in image for x in 0 ..< width { // Row of pixels in image @@ -126,17 +126,17 @@ extension CGImage { // columns in to our "long row", we'd offset ourselves 15 times the width in pixels of the image, and // then offset by the amount of columns let pixel = pointer[(y * width) + x] - + let r = red(for: pixel) let g = green(for: pixel) let b = blue(for: pixel) - + let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) - + totalLuminance += luminance } } - + let avgLuminance = totalLuminance / Double(totalPixels) if totalLuminance == 0 || avgLuminance < 40 { return .dark @@ -146,27 +146,26 @@ extension CGImage { return .regular } } - + private func red(for pixelData: UInt32) -> UInt8 { return UInt8((pixelData >> 16) & 255) } - + private func green(for pixelData: UInt32) -> UInt8 { return UInt8((pixelData >> 8) & 255) } - + private func blue(for pixelData: UInt32) -> UInt8 { return UInt8((pixelData >> 0) & 255) } - -} +} enum IconSize: Int, CaseIterable { case small = 1 case medium = 2 case large = 3 - + private static let smallDimension = CGFloat(integerLiteral: 24) private static let mediumDimension = CGFloat(integerLiteral: 36) private static let largeDimension = CGFloat(integerLiteral: 48) diff --git a/Shared/Extensions/NSAttributedString+NetNewsWire.swift b/Shared/Extensions/NSAttributedString+NetNewsWire.swift index 4a65072ee..e023c99e1 100644 --- a/Shared/Extensions/NSAttributedString+NetNewsWire.swift +++ b/Shared/Extensions/NSAttributedString+NetNewsWire.swift @@ -40,7 +40,7 @@ extension NSAttributedString { let baseDescriptor = baseFont.fontDescriptor let baseSymbolicTraits = baseDescriptor.symbolicTraits - mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, stop: UnsafeMutablePointer) in + mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, _: UnsafeMutablePointer) in guard let font = font as? Font else { return } let currentDescriptor = font.fontDescriptor @@ -108,7 +108,7 @@ extension NSAttributedString { public convenience init(linkText: String, linkURL: URL) { let attrString = NSMutableAttributedString(string: linkText) let range = NSRange(location: 0, length: attrString.length) - + attrString.addAttribute(.font, value: NSFont.systemFont(ofSize: NSFont.systemFontSize), range: range) attrString.addAttribute(.cursor, value: NSCursor.pointingHand, range: range) attrString.addAttribute(.foregroundColor, value: NSColor.linkColor, range: range) @@ -186,20 +186,19 @@ extension NSAttributedString { } else { if char == "&" { var entity = "&" - var lastchar: Character? = nil + var lastchar: Character? while let entitychar = iterator.next() { if entitychar.isWhitespace { lastchar = entitychar - break; + break } entity.append(entitychar) - if (entitychar == ";") { break } + if entitychar == ";" { break } } - result.mutableString.append(entity.decodedEntity) if let lastchar = lastchar { result.mutableString.append(String(lastchar)) } diff --git a/Shared/Extensions/NSView-Extensions.swift b/Shared/Extensions/NSView-Extensions.swift index b2f50c8b3..d11ad2011 100644 --- a/Shared/Extensions/NSView-Extensions.swift +++ b/Shared/Extensions/NSView-Extensions.swift @@ -11,7 +11,7 @@ import AppKit extension NSView { func constraintsToMakeSubViewFullSize(_ subview: NSView) -> [NSLayoutConstraint] { - + let leadingConstraint = NSLayoutConstraint(item: subview, attribute: .leading, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .leading, multiplier: 1.0, constant: 0.0) let trailingConstraint = NSLayoutConstraint(item: subview, attribute: .trailing, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .trailing, multiplier: 1.0, constant: 0.0) let topConstraint = NSLayoutConstraint(item: subview, attribute: .top, relatedBy: .equal, toItem: self.safeAreaLayoutGuide, attribute: .top, multiplier: 1.0, constant: 0.0) diff --git a/Shared/Extensions/Node-Extensions.swift b/Shared/Extensions/Node-Extensions.swift index 14839f8e9..d7c1aadd5 100644 --- a/Shared/Extensions/Node-Extensions.swift +++ b/Shared/Extensions/Node-Extensions.swift @@ -27,41 +27,39 @@ extension Array where Element == Node { private extension Node { class func nodesSortedAlphabetically(_ nodes: [Node]) -> [Node] { - + return nodes.sorted { (node1, node2) -> Bool in - + guard let obj1 = node1.representedObject as? DisplayNameProvider, let obj2 = node2.representedObject as? DisplayNameProvider else { return false } - + let name1 = obj1.nameForDisplay let name2 = obj2.nameForDisplay - + return name1.localizedStandardCompare(name2) == .orderedAscending } } - + class func nodesSortedAlphabeticallyWithFoldersAtEnd(_ nodes: [Node]) -> [Node] { - + return nodes.sorted { (node1, node2) -> Bool in - + if node1.canHaveChildNodes != node2.canHaveChildNodes { if node1.canHaveChildNodes { return false } return true } - + guard let obj1 = node1.representedObject as? DisplayNameProvider, let obj2 = node2.representedObject as? DisplayNameProvider else { return false } - + let name1 = obj1.nameForDisplay let name2 = obj2.nameForDisplay - + return name1.localizedStandardCompare(name2) == .orderedAscending } } } - - diff --git a/Shared/Extensions/RSImage-Extensions.swift b/Shared/Extensions/RSImage-Extensions.swift index 94ae44892..05d879831 100644 --- a/Shared/Extensions/RSImage-Extensions.swift +++ b/Shared/Extensions/RSImage-Extensions.swift @@ -13,12 +13,10 @@ import AppKit import UIKit #endif -import RSCore - extension RSImage { - + static let maxIconSize = 48 - + static func scaledForIcon(_ data: Data, imageResultBlock: @escaping ImageResultBlock) { IconScalerQueue.shared.scaledForIcon(data, imageResultBlock) } @@ -34,7 +32,7 @@ extension RSImage { #else let size = NSSize(width: cgImage.width, height: cgImage.height) return RSImage(cgImage: cgImage, size: size) - #endif + #endif } } diff --git a/Shared/Extensions/URL-Extensions.swift b/Shared/Extensions/URL-Extensions.swift index dd96df60e..5c5d25c6d 100644 --- a/Shared/Extensions/URL-Extensions.swift +++ b/Shared/Extensions/URL-Extensions.swift @@ -9,12 +9,12 @@ import Foundation extension URL { - + /// Extracts email address from a `URL` with a `mailto` scheme, otherwise `nil`. var emailAddress: String? { scheme == "mailto" ? URLComponents(url: self, resolvingAgainstBaseURL: false)?.path : nil } - + /// Percent encoded `mailto` URL for use with `canOpenUrl`. If the URL doesn't contain the `mailto` scheme, this is `nil`. var percentEncodedEmailAddress: URL? { guard scheme == "mailto" else { @@ -25,7 +25,7 @@ extension URL { } return URL(string: urlString) } - + /// Reverse chronological list of release notes. static var releaseNotes = URL(string: "https://github.com/Ranchero-Software/NetNewsWire/releases/")! @@ -36,9 +36,9 @@ extension URL { return nil } return value - + } - + static func reparingIfRequired(_ link: String?) -> URL? { // If required, we replace any space characters to handle malformed links that are otherwise percent // encoded but contain spaces. For performance reasons, only try this if initial URL init fails. diff --git a/Shared/Favicons/ColorHash.swift b/Shared/Favicons/ColorHash.swift index 5cd55d05a..33f5b62d9 100644 --- a/Shared/Favicons/ColorHash.swift +++ b/Shared/Favicons/ColorHash.swift @@ -17,10 +17,10 @@ import AppKit #endif public class ColorHash { - + public static let defaultSaturation = [CGFloat(0.35), CGFloat(0.5), CGFloat(0.65)] public static let defaultBrightness = [CGFloat(0.5), CGFloat(0.65), CGFloat(0.80)] - + let seed = CGFloat(131.0) let seed2 = CGFloat(137.0) let maxSafeInteger = 9007199254740991.0 / CGFloat(137.0) @@ -29,13 +29,13 @@ public class ColorHash { public private(set) var str: String public private(set) var brightness: [CGFloat] public private(set) var saturation: [CGFloat] - + public init(_ str: String, _ saturation: [CGFloat] = defaultSaturation, _ brightness: [CGFloat] = defaultBrightness) { self.str = str self.saturation = saturation self.brightness = brightness } - + public var bkdrHash: CGFloat { var hash = CGFloat(0) for char in "\(str)x" { @@ -48,7 +48,7 @@ public class ColorHash { } return hash } - + public var HSB: (CGFloat, CGFloat, CGFloat) { var hash = CGFloat(bkdrHash) let H = hash.truncatingRemainder(dividingBy: (full - 1.0)) / full @@ -58,7 +58,7 @@ public class ColorHash { let B = brightness[Int((full * hash).truncatingRemainder(dividingBy: CGFloat(brightness.count)))] return (H, S, B) } - + #if os(iOS) || os(tvOS) || os(watchOS) public var color: UIColor { let (H, S, B) = HSB @@ -70,5 +70,5 @@ public class ColorHash { return NSColor(hue: H, saturation: S, brightness: B, alpha: 1.0) } #endif - + } diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index 12d357c37..47b5789a2 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -34,7 +34,7 @@ final class FaviconDownloader { private var remainingFaviconURLs = [String: ArraySlice]() // homePageURL: array of faviconURLs that haven't been checked yet private var currentHomePageHasOnlyFaviconICO = false - private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL + private var homePageToFaviconURLCache = [String: String]() // homePageURL: faviconURL private var homePageToFaviconURLCachePath: String private var homePageToFaviconURLCacheDirty = false { didSet { @@ -77,7 +77,7 @@ final class FaviconDownloader { func resetCache() { cache = [Feed: IconImage]() } - + func favicon(for feed: Feed) -> IconImage? { assert(Thread.isMainThread) @@ -103,13 +103,13 @@ final class FaviconDownloader { return nil } - + func faviconAsIcon(for feed: Feed) -> IconImage? { - + if let image = cache[feed] { return image } - + if let iconImage = favicon(for: feed), let imageData = iconImage.image.dataRepresentation() { if let scaledImage = RSImage.scaledForIcon(imageData) { let scaledIconImage = IconImage(scaledImage) @@ -117,7 +117,7 @@ final class FaviconDownloader { return scaledIconImage } } - + return nil } @@ -169,7 +169,7 @@ final class FaviconDownloader { self.currentHomePageHasOnlyFaviconICO = faviconURLs.count == 1 if let firstIconURL = faviconURLs.first { - let _ = self.favicon(with: firstIconURL, homePageURL: url) + _ = self.favicon(with: firstIconURL, homePageURL: url) self.remainingFaviconURLs[url] = faviconURLs.dropFirst() } } @@ -196,8 +196,8 @@ final class FaviconDownloader { guard let _ = singleFaviconDownloader.iconImage else { if let faviconURLs = remainingFaviconURLs[homePageURL] { if let nextIconURL = faviconURLs.first { - let _ = favicon(with: nextIconURL, homePageURL: singleFaviconDownloader.homePageURL) - remainingFaviconURLs[homePageURL] = faviconURLs.dropFirst(); + _ = favicon(with: nextIconURL, homePageURL: singleFaviconDownloader.homePageURL) + remainingFaviconURLs[homePageURL] = faviconURLs.dropFirst() } else { remainingFaviconURLs[homePageURL] = nil @@ -237,7 +237,7 @@ final class FaviconDownloader { saveHomePageToFaviconURLCache() } } - + @objc func saveHomePageURLsWithNoFaviconURLCacheIfNeeded() { if homePageURLsWithNoFaviconURLCacheDirty { saveHomePageURLsWithNoFaviconURLCache() @@ -277,7 +277,7 @@ private extension FaviconDownloader { func faviconDownloader(withURL faviconURL: String, homePageURL: String?) -> SingleFaviconDownloader { var firstTimeSeeingHomepageURL = false - + if let homePageURL = homePageURL, self.homePageToFaviconURLCache[homePageURL] == nil { self.homePageToFaviconURLCache[homePageURL] = faviconURL self.homePageToFaviconURLCacheDirty = true @@ -358,7 +358,7 @@ private extension FaviconDownloader { assertionFailure(error.localizedDescription) } } - + func saveHomePageURLsWithNoFaviconURLCache() { if Self.debugLoggingEnabled { diff --git a/Shared/Favicons/FaviconGenerator.swift b/Shared/Favicons/FaviconGenerator.swift index b405ea195..6c3cacb68 100644 --- a/Shared/Favicons/FaviconGenerator.swift +++ b/Shared/Favicons/FaviconGenerator.swift @@ -15,11 +15,11 @@ final class FaviconGenerator { private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage static func favicon(_ feed: Feed) -> IconImage { - + if let favicon = FaviconGenerator.faviconGeneratorCache[feed.url] { return favicon } - + let colorHash = ColorHash(feed.url) if let favicon = AppAssets.faviconTemplateImage.maskWithColor(color: colorHash.color.cgColor) { let iconImage = IconImage(favicon, isBackgroundSuppressed: true) @@ -28,7 +28,7 @@ final class FaviconGenerator { } else { return IconImage(AppAssets.faviconTemplateImage, isBackgroundSuppressed: true) } - + } - + } diff --git a/Shared/Favicons/SingleFaviconDownloader.swift b/Shared/Favicons/SingleFaviconDownloader.swift index 67e30dc19..f9fbba4c3 100644 --- a/Shared/Favicons/SingleFaviconDownloader.swift +++ b/Shared/Favicons/SingleFaviconDownloader.swift @@ -30,7 +30,7 @@ final class SingleFaviconDownloader { let homePageURL: String? private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SingleFaviconDownloader") - + private var lastDownloadAttemptDate: Date private var diskStatus = DiskStatus.unknown private var diskCache: BinaryDiskCache @@ -66,7 +66,7 @@ final class SingleFaviconDownloader { lastDownloadAttemptDate = Date() findFavicon() - + return true } } @@ -93,7 +93,7 @@ private extension SingleFaviconDownloader { } self.postDidLoadFaviconNotification() - + } } } @@ -127,8 +127,7 @@ private extension SingleFaviconDownloader { DispatchQueue.main.async { self.diskStatus = .onDisk } - } - catch {} + } catch {} } } @@ -160,5 +159,5 @@ private extension SingleFaviconDownloader { assert(Thread.isMainThread) NotificationCenter.default.post(name: .DidLoadFavicon, object: self) } - + } diff --git a/Shared/IconImageCache.swift b/Shared/IconImageCache.swift index f0f62beaa..0083c48c6 100644 --- a/Shared/IconImageCache.swift +++ b/Shared/IconImageCache.swift @@ -34,7 +34,7 @@ class IconImageCache { guard let feedID = feed.sidebarItemID else { return nil } - + if let smartFeed = feed as? PseudoFeed { return imageForSmartFeed(smartFeed, feedID) } @@ -68,7 +68,7 @@ class IconImageCache { } private extension IconImageCache { - + func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: SidebarItemIdentifier) -> IconImage? { if let iconImage = smartFeedIconImageCache[feedID] { return iconImage diff --git a/Shared/Images/AuthorAvatarDownloader.swift b/Shared/Images/AuthorAvatarDownloader.swift index 471325d05..78ff4fa78 100644 --- a/Shared/Images/AuthorAvatarDownloader.swift +++ b/Shared/Images/AuthorAvatarDownloader.swift @@ -31,21 +31,20 @@ final class AuthorAvatarDownloader { func resetCache() { cache = [String: IconImage]() } - + func image(for author: Author) -> IconImage? { guard let avatarURL = author.avatarURL else { return nil } - + if let cachedImage = cache[avatarURL] { return cachedImage } - + if let imageData = imageDownloader.image(for: avatarURL) { scaleAndCacheImageData(imageData, avatarURL) - } - else { + } else { waitingForAvatarURLs.insert(avatarURL) } diff --git a/Shared/Images/FeedIconDownloader.swift b/Shared/Images/FeedIconDownloader.swift index fe49b7757..f475cb3b8 100644 --- a/Shared/Images/FeedIconDownloader.swift +++ b/Shared/Images/FeedIconDownloader.swift @@ -70,7 +70,7 @@ public final class FeedIconDownloader { } } } - + func checkFeedIconURL() { if let iconURL = feed.iconURL { icon(forURL: iconURL, feed: feed) { (image) in diff --git a/Shared/Images/HTMLMetadata+Extension.swift b/Shared/Images/HTMLMetadata+Extension.swift index d4294779d..f32c37070 100644 --- a/Shared/Images/HTMLMetadata+Extension.swift +++ b/Shared/Images/HTMLMetadata+Extension.swift @@ -17,7 +17,7 @@ extension HTMLMetadata { return nil } - var bestImage: HTMLMetadataAppleTouchIcon? = nil + var bestImage: HTMLMetadataAppleTouchIcon? for image in icons { if let size = image.size { @@ -31,7 +31,7 @@ extension HTMLMetadata { } if let size = image.size, let bestImageSize = bestImage!.size { if size.height > bestImageSize.height && size.width > bestImageSize.width { - bestImage = image; + bestImage = image } } } @@ -46,7 +46,7 @@ extension HTMLMetadata { if let appleTouchIcon = largestAppleTouchIcon() { return appleTouchIcon } - + if let openGraphImageURL = openGraphProperties?.image?.url { return openGraphImageURL } diff --git a/Shared/Images/ImageUtilities.swift b/Shared/Images/ImageUtilities.swift index ecd3265d0..c1041bf9c 100644 --- a/Shared/Images/ImageUtilities.swift +++ b/Shared/Images/ImageUtilities.swift @@ -31,4 +31,3 @@ struct ImageUtilities { return false } } - diff --git a/Shared/Importers/DefaultFeedsImporter.swift b/Shared/Importers/DefaultFeedsImporter.swift index 8ed67d3b9..e6c03ad26 100644 --- a/Shared/Importers/DefaultFeedsImporter.swift +++ b/Shared/Importers/DefaultFeedsImporter.swift @@ -11,11 +11,10 @@ import Account import RSCore struct DefaultFeedsImporter { - + static func importDefaultFeeds(account: Account) { let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")! - AccountManager.shared.defaultAccount.importOPML(defaultFeedsURL) { result in } + AccountManager.shared.defaultAccount.importOPML(defaultFeedsURL) { _ in } } - -} +} diff --git a/Shared/ShareExtension/ExtensionContainers.swift b/Shared/ShareExtension/ExtensionContainers.swift index b283f177a..641c02c68 100644 --- a/Shared/ShareExtension/ExtensionContainers.swift +++ b/Shared/ShareExtension/ExtensionContainers.swift @@ -15,13 +15,13 @@ protocol ExtensionContainer: ContainerIdentifiable, Codable { } struct ExtensionContainers: Codable { - + enum CodingKeys: String, CodingKey { case accounts } let accounts: [ExtensionAccount] - + var flattened: [ExtensionContainer] { return accounts.reduce([ExtensionContainer](), { (containers, account) in var result = containers @@ -30,11 +30,11 @@ struct ExtensionContainers: Codable { return result }) } - + func findAccount(forName name: String) -> ExtensionAccount? { return accounts.first(where: { $0.name == name }) } - + } struct ExtensionAccount: ExtensionContainer { @@ -67,7 +67,7 @@ struct ExtensionAccount: ExtensionContainer { func findFolder(forName name: String) -> ExtensionFolder? { return folders.first(where: { $0.name == name }) } - + } struct ExtensionFolder: ExtensionContainer { @@ -90,5 +90,5 @@ struct ExtensionFolder: ExtensionContainer { self.name = folder.nameForDisplay self.containerID = folder.containerID } - + } diff --git a/Shared/ShareExtension/ExtensionContainersFile.swift b/Shared/ShareExtension/ExtensionContainersFile.swift index 13484dab2..74438009a 100644 --- a/Shared/ShareExtension/ExtensionContainersFile.swift +++ b/Shared/ShareExtension/ExtensionContainersFile.swift @@ -13,7 +13,7 @@ import Parser import Account final class ExtensionContainersFile { - + private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionContainersFile") private static var filePath: String = { @@ -21,7 +21,7 @@ final class ExtensionContainersFile { let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) return containerURL!.appendingPathComponent("extension_containers.plist").path }() - + private var isDirty = false { didSet { queueSaveToDiskIfNeeded() @@ -33,7 +33,7 @@ final class ExtensionContainersFile { if !FileManager.default.fileExists(atPath: ExtensionContainersFile.filePath) { save() } - + NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .UserDidAddAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .UserDidDeleteAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(markAsDirty), name: .AccountStateDidChange, object: nil) @@ -45,7 +45,7 @@ final class ExtensionContainersFile { let errorPointer: NSErrorPointer = nil let fileCoordinator = NSFileCoordinator() let fileURL = URL(fileURLWithPath: ExtensionContainersFile.filePath) - var extensionContainers: ExtensionContainers? = nil + var extensionContainers: ExtensionContainers? fileCoordinator.coordinate(readingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { readURL in if let fileData = try? Data(contentsOf: readURL) { @@ -53,14 +53,14 @@ final class ExtensionContainersFile { extensionContainers = try? decoder.decode(ExtensionContainers.self, from: fileData) } }) - + if let error = errorPointer?.pointee { os_log(.error, log: log, "Read from disk coordination failed: %@.", error.localizedDescription) } return extensionContainers } - + } private extension ExtensionContainersFile { @@ -68,7 +68,7 @@ private extension ExtensionContainersFile { @objc func markAsDirty() { isDirty = true } - + func queueSaveToDiskIfNeeded() { saveQueue.add(self, #selector(saveToDiskIfNeeded)) } @@ -87,7 +87,7 @@ private extension ExtensionContainersFile { let errorPointer: NSErrorPointer = nil let fileCoordinator = NSFileCoordinator() let fileURL = URL(fileURLWithPath: ExtensionContainersFile.filePath) - + fileCoordinator.coordinate(writingItemAt: fileURL, options: [], error: errorPointer, byAccessor: { writeURL in do { let extensionAccounts = AccountManager.shared.sortedActiveAccounts.map { ExtensionAccount(account: $0) } @@ -98,7 +98,7 @@ private extension ExtensionContainersFile { os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription) } }) - + if let error = errorPointer?.pointee { os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription) } diff --git a/Shared/ShareExtension/ExtensionFeedAddRequest.swift b/Shared/ShareExtension/ExtensionFeedAddRequest.swift index 86e54a7d6..99f0e00aa 100644 --- a/Shared/ShareExtension/ExtensionFeedAddRequest.swift +++ b/Shared/ShareExtension/ExtensionFeedAddRequest.swift @@ -10,7 +10,7 @@ import Foundation import Account struct ExtensionFeedAddRequest: Codable { - + enum CodingKeys: String, CodingKey { case name case feedURL @@ -20,5 +20,5 @@ struct ExtensionFeedAddRequest: Codable { let name: String? let feedURL: URL let destinationContainerID: ContainerIdentifier - + } diff --git a/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift b/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift index 2bbb4a6d4..b9529a7c0 100644 --- a/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift +++ b/Shared/ShareExtension/ExtensionFeedAddRequestFile.swift @@ -11,7 +11,7 @@ import os.log import Account final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { - + private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "extensionFeedAddRequestFile") private static var filePath: String = { @@ -19,23 +19,23 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) return containerURL!.appendingPathComponent("extension_feed_add_request.plist").path }() - + private let operationQueue: OperationQueue - + var presentedItemURL: URL? { return URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath) } - + var presentedItemOperationQueue: OperationQueue { return operationQueue } - + override init() { operationQueue = OperationQueue() operationQueue.maxConcurrentOperationCount = 1 - + super.init() - + NSFileCoordinator.addFilePresenter(self) process() } @@ -50,13 +50,13 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { NSFileCoordinator.addFilePresenter(self) process() } - + func suspend() { NSFileCoordinator.removeFilePresenter(self) } - + static func save(_ feedAddRequest: ExtensionFeedAddRequest) { - + let decoder = PropertyListDecoder() let encoder = PropertyListEncoder() encoder.outputFormat = .binary @@ -64,10 +64,10 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { let errorPointer: NSErrorPointer = nil let fileCoordinator = NSFileCoordinator() let fileURL = URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath) - + fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forMerging], error: errorPointer, byAccessor: { url in do { - + var requests: [ExtensionFeedAddRequest] if let fileData = try? Data(contentsOf: url), let decodedRequests = try? decoder.decode([ExtensionFeedAddRequest].self, from: fileData) { @@ -75,28 +75,28 @@ final class ExtensionFeedAddRequestFile: NSObject, NSFilePresenter { } else { requests = [ExtensionFeedAddRequest]() } - + requests.append(feedAddRequest) let data = try encoder.encode(requests) try data.write(to: url) - + } catch let error as NSError { os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription) } }) - + if let error = errorPointer?.pointee { os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription) } } - + } private extension ExtensionFeedAddRequestFile { - + func process() { - + let decoder = PropertyListDecoder() let encoder = PropertyListEncoder() encoder.outputFormat = .binary @@ -105,24 +105,24 @@ private extension ExtensionFeedAddRequestFile { let fileCoordinator = NSFileCoordinator(filePresenter: self) let fileURL = URL(fileURLWithPath: ExtensionFeedAddRequestFile.filePath) - var requests: [ExtensionFeedAddRequest]? = nil + var requests: [ExtensionFeedAddRequest]? fileCoordinator.coordinate(writingItemAt: fileURL, options: [.forMerging], error: errorPointer, byAccessor: { url in do { - + if let fileData = try? Data(contentsOf: url), let decodedRequests = try? decoder.decode([ExtensionFeedAddRequest].self, from: fileData) { requests = decodedRequests } - + let data = try encoder.encode([ExtensionFeedAddRequest]()) try data.write(to: url) - + } catch let error as NSError { os_log(.error, log: Self.log, "Save to disk failed: %@.", error.localizedDescription) } }) - + if let error = errorPointer?.pointee { os_log(.error, log: Self.log, "Save to disk coordination failed: %@.", error.localizedDescription) } @@ -133,9 +133,9 @@ private extension ExtensionFeedAddRequestFile { } } } - + func processRequest(_ request: ExtensionFeedAddRequest) { - var destinationAccountID: String? = nil + var destinationAccountID: String? switch request.destinationContainerID { case .account(let accountID): destinationAccountID = accountID @@ -144,21 +144,21 @@ private extension ExtensionFeedAddRequestFile { default: break } - + guard let accountID = destinationAccountID, let account = AccountManager.shared.existingAccount(with: accountID) else { return } - - var destinationContainer: Container? = nil + + var destinationContainer: Container? if account.containerID == request.destinationContainerID { destinationContainer = account } else { destinationContainer = account.folders?.first(where: { $0.containerID == request.destinationContainerID }) } - + guard let container = destinationContainer else { return } - + account.createFeed(url: request.feedURL.absoluteString, name: request.name, container: container, validateFeed: true) { _ in } } - + } diff --git a/Shared/ShareExtension/ShareDefaultContainer.swift b/Shared/ShareExtension/ShareDefaultContainer.swift index 5cc5ef24e..aabbe0c6e 100644 --- a/Shared/ShareExtension/ShareDefaultContainer.swift +++ b/Shared/ShareExtension/ShareDefaultContainer.swift @@ -9,9 +9,9 @@ import Foundation struct ShareDefaultContainer { - + static func defaultContainer(containers: ExtensionContainers) -> ExtensionContainer? { - + if let accountID = AppDefaults.shared.addFeedAccountID, let account = containers.accounts.first(where: { $0.accountID == accountID }) { if let folderName = AppDefaults.shared.addFeedFolderName, let folder = account.folders.first(where: { $0.name == folderName }) { return folder @@ -23,9 +23,9 @@ struct ShareDefaultContainer { } else { return nil } - + } - + static func saveDefaultContainer(_ container: ExtensionContainer) { AppDefaults.shared.addFeedAccountID = container.accountID if let folder = container as? ExtensionFolder { @@ -34,7 +34,7 @@ struct ShareDefaultContainer { AppDefaults.shared.addFeedFolderName = nil } } - + private static func substituteContainerIfNeeded(account: ExtensionAccount) -> ExtensionContainer? { if !account.disallowFeedInRootFolder { return account diff --git a/Shared/SmartFeeds/PseudoFeed.swift b/Shared/SmartFeeds/PseudoFeed.swift index 94436c66b..2126cd808 100644 --- a/Shared/SmartFeeds/PseudoFeed.swift +++ b/Shared/SmartFeeds/PseudoFeed.swift @@ -25,7 +25,7 @@ import Account import RSCore protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider { - + } #endif diff --git a/Shared/SmartFeeds/SearchFeedDelegate.swift b/Shared/SmartFeeds/SearchFeedDelegate.swift index cc8060725..a42843161 100644 --- a/Shared/SmartFeeds/SearchFeedDelegate.swift +++ b/Shared/SmartFeeds/SearchFeedDelegate.swift @@ -36,4 +36,3 @@ struct SearchFeedDelegate: SmartFeedDelegate { // TODO: after 5.0 } } - diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index ec7a65d3e..73c371f2b 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -14,7 +14,7 @@ import Account final class SmartFeed: PseudoFeed { - var account: Account? = nil + var account: Account? public var defaultReadFilterType: ReadFilterType { return .none @@ -39,7 +39,7 @@ final class SmartFeed: PseudoFeed { var smallIcon: IconImage? { return delegate.smallIcon } - + #if os(macOS) var pasteboardWriter: NSPasteboardWriting { return SmartFeedPasteboardWriter(smartFeed: self) @@ -63,7 +63,7 @@ final class SmartFeed: PseudoFeed { @objc func fetchUnreadCounts() { let activeAccounts = AccountManager.shared.activeAccounts - + // Remove any accounts that are no longer active or have been deleted let activeAccountIDs = activeAccounts.map { $0.accountID } for accountID in unreadCounts.keys { @@ -71,7 +71,7 @@ final class SmartFeed: PseudoFeed { unreadCounts.removeValue(forKey: accountID) } } - + if activeAccounts.isEmpty { updateUnreadCount() } else { @@ -80,7 +80,7 @@ final class SmartFeed: PseudoFeed { } } } - + } extension SmartFeed: ArticleFetcher { diff --git a/Shared/SmartFeeds/SmartFeedDelegate.swift b/Shared/SmartFeeds/SmartFeedDelegate.swift index 7f37a2ae6..ef5b10183 100644 --- a/Shared/SmartFeeds/SmartFeedDelegate.swift +++ b/Shared/SmartFeeds/SmartFeedDelegate.swift @@ -32,7 +32,7 @@ extension SmartFeedDelegate { } func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - fetchArticlesAsync{ articleSetResult in + fetchArticlesAsync { articleSetResult in switch articleSetResult { case .success(let articles): completion(.success(articles.unreadArticles())) diff --git a/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift b/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift index 139f120d9..e21b8f0e8 100644 --- a/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift +++ b/Shared/SmartFeeds/SmartFeedPasteboardWriter.swift @@ -40,4 +40,3 @@ import RSCore return plist } } - diff --git a/Shared/SmartFeeds/SmartFeedsController.swift b/Shared/SmartFeeds/SmartFeedsController.swift index 8ff75f510..fb578f117 100644 --- a/Shared/SmartFeeds/SmartFeedsController.swift +++ b/Shared/SmartFeeds/SmartFeedsController.swift @@ -11,7 +11,7 @@ import RSCore import Account final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable { - + var containerID: ContainerIdentifier? { return ContainerIdentifier.smartFeedController } @@ -27,7 +27,7 @@ final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable { private init() { self.smartFeeds = [todayFeed, unreadFeed, starredFeed] } - + func find(by identifier: SidebarItemIdentifier) -> PseudoFeed? { switch identifier { case .smartFeed(let stringIdentifer): @@ -45,5 +45,5 @@ final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable { return nil } } - + } diff --git a/Shared/SmartFeeds/TodayFeedDelegate.swift b/Shared/SmartFeeds/TodayFeedDelegate.swift index 06f215ef5..0f4c18b18 100644 --- a/Shared/SmartFeeds/TodayFeedDelegate.swift +++ b/Shared/SmartFeeds/TodayFeedDelegate.swift @@ -17,15 +17,14 @@ struct TodayFeedDelegate: SmartFeedDelegate { var sidebarItemID: SidebarItemIdentifier? { return SidebarItemIdentifier.smartFeed(String(describing: TodayFeedDelegate.self)) } - + let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title") let fetchType = FetchType.today(nil) var smallIcon: IconImage? { return AppAssets.todayFeedImage } - + func fetchUnreadCount(for account: Account, completion: @escaping SingleUnreadCountCompletionBlock) { account.fetchUnreadCountForToday(completion) } } - diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 540eb9620..c2f0af292 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -19,8 +19,8 @@ import ArticlesDatabase // This just shows the global unread count, which appDelegate already has. Easy. final class UnreadFeed: PseudoFeed { - - var account: Account? = nil + + var account: Account? public var defaultReadFilterType: ReadFilterType { return .alwaysRead @@ -32,7 +32,7 @@ final class UnreadFeed: PseudoFeed { let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title") let fetchType = FetchType.unread(nil) - + var unreadCount = 0 { didSet { if unreadCount != oldValue { @@ -44,13 +44,13 @@ final class UnreadFeed: PseudoFeed { var smallIcon: IconImage? { return AppAssets.unreadFeedImage } - + #if os(macOS) var pasteboardWriter: NSPasteboardWriting { return SmartFeedPasteboardWriter(smartFeed: self) } #endif - + init() { self.unreadCount = appDelegate.unreadCount @@ -65,7 +65,7 @@ final class UnreadFeed: PseudoFeed { } extension UnreadFeed: ArticleFetcher { - + func fetchArticles() throws -> Set
{ return try fetchUnreadArticles() } diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 104f06f8c..e65108cd6 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -23,10 +23,10 @@ extension Array where Element == Article { func orderedRowIndexes(fromIndex startIndex: Int, wrappingToTop wrapping: Bool) -> [Int] { if startIndex >= self.count { // Wrap around to the top if specified - return wrapping ? Array(0..(startIndex..(0.. Int? { @@ -45,15 +45,15 @@ extension Array where Element == Article { } func articlesForIndexes(_ indexes: IndexSet) -> Set
{ - return Set(indexes.compactMap{ (oneIndex) -> Article? in + return Set(indexes.compactMap { (oneIndex) -> Article? in return articleAtRow(oneIndex) }) } - + func sortedByDate(_ sortDirection: ComparisonResult, groupByFeed: Bool = false) -> ArticleArray { return ArticleSorter.sortedByDate(articles: self, sortDirection: sortDirection, groupByFeed: groupByFeed) } - + func canMarkAllAsRead() -> Bool { return anyArticleIsUnread() } @@ -84,7 +84,7 @@ extension Array where Element == Article { } func unreadArticles() -> [Article]? { - let articles = self.filter{ !$0.status.read } + let articles = self.filter { !$0.status.read } return articles.isEmpty ? nil : articles } @@ -107,7 +107,7 @@ extension Array where Element == Article { guard let position = firstIndex(of: article) else { return [] } return articlesAbove(position: position) } - + func articlesAbove(position: Int) -> [Article] { guard position < count else { return [] } let articlesAbove = self[.. [Article] { guard position < count else { return [] } var articlesBelow = Array(self[position...]) @@ -128,6 +128,5 @@ extension Array where Element == Article { articlesBelow.removeFirst() return articlesBelow } - -} +} diff --git a/Shared/Timeline/ArticleSorter.swift b/Shared/Timeline/ArticleSorter.swift index c7ba1a748..2561ddc4c 100644 --- a/Shared/Timeline/ArticleSorter.swift +++ b/Shared/Timeline/ArticleSorter.swift @@ -17,7 +17,7 @@ protocol SortableArticle { } struct ArticleSorter { - + static func sortedByDate(articles: [T], sortDirection: ComparisonResult, groupByFeed: Bool) -> [T] { @@ -27,9 +27,9 @@ struct ArticleSorter { return sortedByDate(articles: articles, sortDirection: sortDirection) } } - + // MARK: - - + private static func sortedByFeedName(articles: [T], sortByDateDirection: ComparisonResult) -> [T] { // Group articles by "feed-feedID" - feed ID is used to differentiate between @@ -39,11 +39,11 @@ struct ArticleSorter { .sorted { $0.key < $1.key } .flatMap { (tuple) -> [T] in let (_, articles) = tuple - + return sortedByDate(articles: articles, sortDirection: sortByDateDirection) } } - + private static func sortedByDate(articles: [T], sortDirection: ComparisonResult) -> [T] { return articles.sorted { (article1, article2) -> Bool in @@ -53,9 +53,9 @@ struct ArticleSorter { if sortDirection == .orderedDescending { return article1.sortableDate > article2.sortableDate } - + return article1.sortableDate < article2.sortableDate } } - + } diff --git a/Shared/Timeline/FetchRequestOperation.swift b/Shared/Timeline/FetchRequestOperation.swift index bfaab1439..726c4b115 100644 --- a/Shared/Timeline/FetchRequestOperation.swift +++ b/Shared/Timeline/FetchRequestOperation.swift @@ -61,14 +61,14 @@ final class FetchRequestOperation { let numberOfFetchers = fetchers.count var fetchersReturned = 0 var fetchedArticles = Set
() - + func process(_ articles: Set
) { precondition(Thread.isMainThread) guard !self.isCanceled else { callCompletionIfNeeded() return } - + assert(!self.isFinished) fetchedArticles.formUnion(articles) @@ -79,7 +79,7 @@ final class FetchRequestOperation { callCompletionIfNeeded() } } - + for fetcher in fetchers { if (fetcher as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true { fetcher.fetchUnreadArticlesAsync { articleSetResult in @@ -92,8 +92,7 @@ final class FetchRequestOperation { process(articles) } } - + } } } - diff --git a/Shared/Timeline/FetchRequestQueue.swift b/Shared/Timeline/FetchRequestQueue.swift index 688d5e6fd..957f19191 100644 --- a/Shared/Timeline/FetchRequestQueue.swift +++ b/Shared/Timeline/FetchRequestQueue.swift @@ -13,8 +13,8 @@ import Foundation final class FetchRequestQueue { private var pendingRequests = [FetchRequestOperation]() - private var currentRequest: FetchRequestOperation? = nil - + private var currentRequest: FetchRequestOperation? + var isAnyCurrentRequest: Bool { if let currentRequest = currentRequest { return !currentRequest.isCanceled @@ -57,6 +57,6 @@ private extension FetchRequestQueue { } func removeCanceledAndFinishedRequests() { - pendingRequests = pendingRequests.filter{ !$0.isCanceled && !$0.isFinished } + pendingRequests = pendingRequests.filter { !$0.isCanceled && !$0.isFinished } } } diff --git a/Shared/Timer/AccountRefreshTimer.swift b/Shared/Timer/AccountRefreshTimer.swift index bcf102ac0..c642a37ae 100644 --- a/Shared/Timer/AccountRefreshTimer.swift +++ b/Shared/Timer/AccountRefreshTimer.swift @@ -10,13 +10,13 @@ import Foundation import Account final class AccountRefreshTimer { - + var shuttingDown = false private var internalTimer: Timer? private var lastTimedRefresh: Date? private let launchTime = Date() - + func fireOldTimer() { if let timer = internalTimer { if timer.fireDate < Date() { @@ -26,7 +26,7 @@ final class AccountRefreshTimer { } } } - + func invalidate() { guard let timer = internalTimer else { return @@ -36,12 +36,12 @@ final class AccountRefreshTimer { } internalTimer = nil } - + func update() { guard !shuttingDown else { return } - + let refreshInterval = AppDefaults.shared.refreshInterval if refreshInterval == .manually { invalidate() @@ -56,23 +56,23 @@ final class AccountRefreshTimer { if let currentNextFireDate = internalTimer?.fireDate, currentNextFireDate == nextRefreshTime { return } - + invalidate() let timer = Timer(fireAt: nextRefreshTime, interval: 0, target: self, selector: #selector(timedRefresh(_:)), userInfo: nil, repeats: false) RunLoop.main.add(timer, forMode: .common) internalTimer = timer - + } - + @objc func timedRefresh(_ sender: Timer?) { - + guard !shuttingDown else { return } - + lastTimedRefresh = Date() update() - + AccountManager.shared.refreshAll() } } diff --git a/Shared/Timer/ArticleStatusSyncTimer.swift b/Shared/Timer/ArticleStatusSyncTimer.swift index 32e3cf604..2a246f5dc 100644 --- a/Shared/Timer/ArticleStatusSyncTimer.swift +++ b/Shared/Timer/ArticleStatusSyncTimer.swift @@ -10,15 +10,15 @@ import Foundation import Account class ArticleStatusSyncTimer { - + private static let intervalSeconds = Double(120) - + var shuttingDown = false - + private var internalTimer: Timer? private var lastTimedRefresh: Date? private let launchTime = Date() - + func fireOldTimer() { if let timer = internalTimer { if timer.fireDate < Date() { @@ -26,7 +26,7 @@ class ArticleStatusSyncTimer { } } } - + func invalidate() { guard let timer = internalTimer else { return @@ -36,13 +36,13 @@ class ArticleStatusSyncTimer { } internalTimer = nil } - + func update() { - + guard !shuttingDown else { return } - + let lastRefreshDate = lastTimedRefresh ?? launchTime var nextRefreshTime = lastRefreshDate.addingTimeInterval(ArticleStatusSyncTimer.intervalSeconds) if nextRefreshTime < Date() { @@ -51,25 +51,25 @@ class ArticleStatusSyncTimer { if let currentNextFireDate = internalTimer?.fireDate, currentNextFireDate == nextRefreshTime { return } - + invalidate() let timer = Timer(fireAt: nextRefreshTime, interval: 0, target: self, selector: #selector(timedRefresh(_:)), userInfo: nil, repeats: false) RunLoop.main.add(timer, forMode: .common) internalTimer = timer - + } - + @objc func timedRefresh(_ sender: Timer?) { - + guard !shuttingDown else { return } - + lastTimedRefresh = Date() update() - + AccountManager.shared.syncArticleStatusAll() - + } - + } diff --git a/Shared/Timer/RefreshInterval.swift b/Shared/Timer/RefreshInterval.swift index 603ed2cac..d313bfab6 100644 --- a/Shared/Timer/RefreshInterval.swift +++ b/Shared/Timer/RefreshInterval.swift @@ -16,7 +16,7 @@ enum RefreshInterval: Int, CaseIterable, Identifiable { case every2Hours = 5 case every4Hours = 6 case every8Hours = 7 - + func inSeconds() -> TimeInterval { switch self { case .manually: @@ -35,9 +35,9 @@ enum RefreshInterval: Int, CaseIterable, Identifiable { return 8 * 60 * 60 } } - + var id: String { description() } - + func description() -> String { switch self { case .manually: @@ -56,5 +56,5 @@ enum RefreshInterval: Int, CaseIterable, Identifiable { return NSLocalizedString("Every 8 Hours", comment: "Every 8 Hours") } } - + } diff --git a/Shared/Tree/FeedTreeControllerDelegate.swift b/Shared/Tree/FeedTreeControllerDelegate.swift index 80db8cdf0..9528c512f 100644 --- a/Shared/Tree/FeedTreeControllerDelegate.swift +++ b/Shared/Tree/FeedTreeControllerDelegate.swift @@ -15,15 +15,15 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate { private var filterExceptions = Set() var isReadFiltered = false - + func addFilterException(_ feedID: SidebarItemIdentifier) { filterExceptions.insert(feedID) } - + func resetFilterExceptions() { filterExceptions = Set() } - + func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { if node.isRoot { return childNodesForRootNode(node) @@ -36,11 +36,11 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate { } return nil - } + } } private extension FeedTreeControllerDelegate { - + func childNodesForRootNode(_ rootNode: Node) -> [Node]? { var topLevelNodes = [Node]() @@ -50,7 +50,7 @@ private extension FeedTreeControllerDelegate { topLevelNodes.append(smartFeedsNode) topLevelNodes.append(contentsOf: sortedAccountNodes(rootNode)) - + return topLevelNodes } @@ -65,13 +65,13 @@ private extension FeedTreeControllerDelegate { let container = containerNode.representedObject as! Container var children = [AnyObject]() - + for feed in container.topLevelFeeds { if let feedID = feed.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && feed.unreadCount == 0) { children.append(feed) } } - + if let folders = container.folders { for folder in folders { if let feedID = folder.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && folder.unreadCount == 0) { @@ -107,18 +107,18 @@ private extension FeedTreeControllerDelegate { if let folder = representedObject as? Folder { return createNode(folder: folder, parent: parent) } - + if let account = representedObject as? Account { return createNode(account: account, parent: parent) } return nil } - + func createNode(feed: Feed, parent: Node) -> Node { return parent.createChildNode(feed) } - + func createNode(folder: Folder, parent: Node) -> Node { let node = parent.createChildNode(folder) node.canHaveChildNodes = true diff --git a/Shared/Tree/FolderTreeControllerDelegate.swift b/Shared/Tree/FolderTreeControllerDelegate.swift index 2c1e60cf2..d3c5b97b4 100644 --- a/Shared/Tree/FolderTreeControllerDelegate.swift +++ b/Shared/Tree/FolderTreeControllerDelegate.swift @@ -13,7 +13,7 @@ import Articles import Account final class FolderTreeControllerDelegate: TreeControllerDelegate { - + func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { return node.isRoot ? childNodesForRootNode(node) : childNodes(node) @@ -21,27 +21,27 @@ final class FolderTreeControllerDelegate: TreeControllerDelegate { } private extension FolderTreeControllerDelegate { - + func childNodesForRootNode(_ node: Node) -> [Node]? { - + let accountNodes: [Node] = AccountManager.shared.sortedActiveAccounts.map { account in let accountNode = Node(representedObject: account, parent: node) accountNode.canHaveChildNodes = true return accountNode } return accountNodes - + } - + func childNodes(_ node: Node) -> [Node]? { - + guard let account = node.representedObject as? Account, let folders = account.folders else { return nil } - + let folderNodes: [Node] = folders.map { createNode($0, parent: node) } return folderNodes.sortedAlphabetically() - + } func createNode(_ folder: Folder, parent: Node) -> Node { @@ -49,5 +49,5 @@ private extension FolderTreeControllerDelegate { node.canHaveChildNodes = false return node } - + } diff --git a/Shared/UserInfoKey.swift b/Shared/UserInfoKey.swift index 35a392f25..ce200fbd5 100644 --- a/Shared/UserInfoKey.swift +++ b/Shared/UserInfoKey.swift @@ -14,7 +14,7 @@ struct UserInfoKey { static let url = "url" static let articlePath = "articlePath" static let feedIdentifier = "feedIdentifier" - + static let windowState = "windowState" static let windowFullScreenState = "windowFullScreenState" static let containerExpandedWindowState = "containerExpandedWindowState" @@ -25,5 +25,5 @@ struct UserInfoKey { static let selectedFeedsState = "selectedFeedsState" static let isShowingExtractedArticle = "isShowingExtractedArticle" static let articleWindowScrollY = "articleWindowScrollY" - + } diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 97cb2b4b1..15160e8a5 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -12,19 +12,19 @@ import Articles import UserNotifications final class UserNotificationManager: NSObject { - + override init() { super.init() NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) registerCategoriesAndActions() } - + @objc func accountDidDownloadArticles(_ note: Notification) { guard let articles = note.userInfo?[Account.UserInfoKey.newArticles] as? Set
else { return } - + for article in articles { if !article.status.read, let feed = article.feed, feed.isNotifyAboutNewArticles ?? false { sendNotification(feed: feed, article: article) @@ -38,7 +38,7 @@ final class UserNotificationManager: NSObject { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) return } - + if let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set, let statusKey = note.userInfo?[Account.UserInfoKey.statusKey] as? ArticleStatus.Key, let flag = note.userInfo?[Account.UserInfoKey.statusFlag] as? Bool, @@ -48,14 +48,14 @@ final class UserNotificationManager: NSObject { UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) } } - + } private extension UserNotificationManager { - + func sendNotification(feed: Feed, article: Article) { let content = UNMutableNotificationContent() - + content.title = feed.nameForDisplay if !ArticleStringFormatter.truncatedTitle(article).isEmpty { content.subtitle = ArticleStringFormatter.truncatedTitle(article) @@ -68,11 +68,11 @@ private extension UserNotificationManager { if let attachment = thumbnailAttachment(for: article, feed: feed) { content.attachments.append(attachment) } - + let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } - + /// Determine if there is an available icon for the article. This will then move it to the caches directory and make it avialble for the notification. /// - Parameters: /// - article: `Article` @@ -86,20 +86,20 @@ private extension UserNotificationManager { } return nil } - + func registerCategoriesAndActions() { let readAction = UNNotificationAction(identifier: "MARK_AS_READ", title: NSLocalizedString("Mark as Read", comment: "Mark as Read"), options: []) let starredAction = UNNotificationAction(identifier: "MARK_AS_STARRED", title: NSLocalizedString("Mark as Starred", comment: "Mark as Starred"), options: []) let openAction = UNNotificationAction(identifier: "OPEN_ARTICLE", title: NSLocalizedString("Open", comment: "Open"), options: [.foreground]) - + let newArticleCategory = UNNotificationCategory(identifier: "NEW_ARTICLE_NOTIFICATION_CATEGORY", actions: [openAction, readAction, starredAction], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: "", options: []) - + UNUserNotificationCenter.current().setNotificationCategories([newArticleCategory]) } - + } diff --git a/Shared/Widget/WidgetData.swift b/Shared/Widget/WidgetData.swift index ac71969f6..ebe2e5f90 100644 --- a/Shared/Widget/WidgetData.swift +++ b/Shared/Widget/WidgetData.swift @@ -30,4 +30,3 @@ struct LatestArticle: Codable, Identifiable { let pubDate: String } - diff --git a/Shared/Widget/WidgetDataDecoder.swift b/Shared/Widget/WidgetDataDecoder.swift index e2d03f53b..a7f57064e 100644 --- a/Shared/Widget/WidgetDataDecoder.swift +++ b/Shared/Widget/WidgetDataDecoder.swift @@ -9,7 +9,7 @@ import Foundation struct WidgetDataDecoder { - + static func decodeWidgetData() throws -> WidgetData { let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) @@ -21,7 +21,7 @@ struct WidgetDataDecoder { return WidgetData(currentUnreadCount: 0, currentTodayCount: 0, currentStarredCount: 0, unreadArticles: [], starredArticles: [], todayArticles: [], lastUpdateTime: Date()) } } - + static func sampleData() -> WidgetData { let pathToSample = Bundle.main.url(forResource: "widget-sample", withExtension: "json") do { @@ -32,5 +32,5 @@ struct WidgetDataDecoder { return WidgetData(currentUnreadCount: 0, currentTodayCount: 0, currentStarredCount: 0, unreadArticles: [], starredArticles: [], todayArticles: [], lastUpdateTime: Date()) } } - + } diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 63fc704a3..83dbf0da9 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -14,7 +14,6 @@ import RSCore import Articles import Account - public final class WidgetDataEncoder { private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") @@ -35,39 +34,39 @@ public final class WidgetDataEncoder { func encode() { isRunning = true - + flushSharedContainer() os_log(.debug, log: log, "Starting encoding widget data.") - + DispatchQueue.main.async { - self.encodeWidgetData() { latestData in + self.encodeWidgetData { latestData in guard let latestData = latestData else { self.isRunning = false return } - + let encodedData = try? JSONEncoder().encode(latestData) - + os_log(.debug, log: self.log, "Finished encoding widget data.") - + if self.fileExists() { try? FileManager.default.removeItem(at: self.dataURL!) os_log(.debug, log: self.log, "Removed widget data from container.") } - + if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { os_log(.debug, log: self.log, "Wrote widget data to container.") WidgetCenter.shared.reloadAllTimelines() } - + self.isRunning = false } } } - + private func encodeWidgetData(completion: @escaping (WidgetData?) -> Void) { let dispatchGroup = DispatchGroup() - var groupError: Error? = nil + var groupError: Error? var unread = [LatestArticle]() @@ -142,7 +141,7 @@ public final class WidgetDataEncoder { currentStarredCount: (try? AccountManager.shared.fetchCountForStarredArticles()) ?? 0, unreadArticles: unread, starredArticles: starred, - todayArticles:today, + todayArticles: today, lastUpdateTime: Date()) completion(latestData) } @@ -178,5 +177,3 @@ public final class WidgetDataEncoder { } } - - diff --git a/Shared/Widget/WidgetDeepLinks.swift b/Shared/Widget/WidgetDeepLinks.swift index 782efb1a6..df51e2012 100644 --- a/Shared/Widget/WidgetDeepLinks.swift +++ b/Shared/Widget/WidgetDeepLinks.swift @@ -9,7 +9,7 @@ import Foundation enum WidgetDeepLink { - + case unread case unreadArticle(id: String) case today @@ -17,7 +17,7 @@ enum WidgetDeepLink { case starred case starredArticle(id: String) case icon - + var url: URL { switch self { case .unread: @@ -42,5 +42,5 @@ enum WidgetDeepLink { return URL(string: "nnw://icon")! } } - + }