Fix lint issues.

This commit is contained in:
Brent Simmons 2025-01-22 22:20:08 -08:00
parent bbef99f2d3
commit 10f4351904
64 changed files with 506 additions and 545 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Void, Error>) {
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.
}
}

View File

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

View File

@ -9,6 +9,6 @@
import Foundation
struct Constants {
static let prototypeText = "You are about to being reading Italo Calvinos new novel, *If on a winters 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 dont want to watch TV!” Raise your voice—they wont hear you otherwise—“Im reading! I dont want to be disturbed!” Maybe they havent heard you, with all that racket; speak louder, yell: “Im beginning to read Italo Calvinos new novel!” Or if you prefer, dont say anything; just hope theyll 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."
}

View File

@ -24,7 +24,7 @@ struct OPMLExporter {
<title>\(escapedTitle)</title>
</head>
<body>
"""
let middleText = account.OPMLString(indentLevel: 0)

View File

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

View File

@ -28,7 +28,7 @@ final class SendToMicroBlogCommand: SendToCommand {
return true
}
func sendObject(_ object: Any?, selectedText: String?) {
guard canSendObject(object, selectedText: selectedText) else {

View File

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

View File

@ -116,4 +116,3 @@ struct ArticleStringFormatter {
return dateFormatter.string(from: date)
}
}

View File

@ -14,11 +14,11 @@ import Account
// These handle multiple accounts.
func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
let d: [String: Set<Article>] = 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<Article>, statusKey: ArticleStatus.Key, flag:
group.leave()
}
}
group.notify(queue: .main) {
completion?()
}
}
private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String: Set<Article>] {
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
}
}

View File

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

View File

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

View File

@ -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<ObjCBool>) in
mutable.enumerateAttribute(.font, in: fullRange, options: []) { (font: Any?, range: NSRange, _: UnsafeMutablePointer<ObjCBool>) 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)) }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ final class FaviconDownloader {
private var remainingFaviconURLs = [String: ArraySlice<String>]() // 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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -70,7 +70,7 @@ public final class FeedIconDownloader {
}
}
}
func checkFeedIconURL() {
if let iconURL = feed.iconURL {
icon(forURL: iconURL, feed: feed) { (image) in

View File

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

View File

@ -31,4 +31,3 @@ struct ImageUtilities {
return false
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ import Account
import RSCore
protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider {
}
#endif

View File

@ -36,4 +36,3 @@ struct SearchFeedDelegate: SmartFeedDelegate {
// TODO: after 5.0
}
}

View File

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

View File

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

View File

@ -40,4 +40,3 @@ import RSCore
return plist
}
}

View File

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

View File

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

View File

@ -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<Article> {
return try fetchUnreadArticles()
}

View File

@ -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<Int>(0..<self.count) : []
return wrapping ? [Int](0..<self.count) : []
} else {
// Start at the selection and wrap around to the beginning
return Array<Int>(startIndex..<self.count) + (wrapping ? Array<Int>(0..<startIndex) : [])
return [Int](startIndex..<self.count) + (wrapping ? [Int](0..<startIndex) : [])
}
}
func rowOfNextUnreadArticle(_ selectedRow: Int, wrappingToTop wrapping: Bool) -> Int? {
@ -45,15 +45,15 @@ extension Array where Element == Article {
}
func articlesForIndexes(_ indexes: IndexSet) -> Set<Article> {
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[..<position]
@ -118,7 +118,7 @@ extension Array where Element == Article {
guard let position = firstIndex(of: article) else { return [] }
return articlesBelow(position: position)
}
func articlesBelow(position: Int) -> [Article] {
guard position < count else { return [] }
var articlesBelow = Array(self[position...])
@ -128,6 +128,5 @@ extension Array where Element == Article {
articlesBelow.removeFirst()
return articlesBelow
}
}
}

View File

@ -17,7 +17,7 @@ protocol SortableArticle {
}
struct ArticleSorter {
static func sortedByDate<T: SortableArticle>(articles: [T],
sortDirection: ComparisonResult,
groupByFeed: Bool) -> [T] {
@ -27,9 +27,9 @@ struct ArticleSorter {
return sortedByDate(articles: articles, sortDirection: sortDirection)
}
}
// MARK: -
private static func sortedByFeedName<T: SortableArticle>(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<T: SortableArticle>(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
}
}
}

View File

@ -61,14 +61,14 @@ final class FetchRequestOperation {
let numberOfFetchers = fetchers.count
var fetchersReturned = 0
var fetchedArticles = Set<Article>()
func process(_ articles: Set<Article>) {
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)
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -15,15 +15,15 @@ final class FeedTreeControllerDelegate: TreeControllerDelegate {
private var filterExceptions = Set<SidebarItemIdentifier>()
var isReadFiltered = false
func addFilterException(_ feedID: SidebarItemIdentifier) {
filterExceptions.insert(feedID)
}
func resetFilterExceptions() {
filterExceptions = Set<SidebarItemIdentifier>()
}
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

View File

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

View File

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

View File

@ -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<Article> 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<String>,
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])
}
}

View File

@ -30,4 +30,3 @@ struct LatestArticle: Codable, Identifiable {
let pubDate: String
}

View File

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

View File

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

View File

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