Add basic ExtensionPoint support.

This commit is contained in:
Maurice Parker
2020-04-07 15:25:33 -05:00
parent f8667be32b
commit 49cff8eb8e
29 changed files with 299 additions and 140 deletions

View File

@@ -0,0 +1,51 @@
//
// AddWebFeedDefaultContainer.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 11/16/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import Account
struct AddWebFeedDefaultContainer {
static var defaultContainer: Container? {
if let accountID = AppDefaults.addWebFeedAccountID, let account = AccountManager.shared.activeAccounts.first(where: { $0.accountID == accountID }) {
if let folderName = AppDefaults.addWebFeedFolderName, let folder = account.existingFolder(withDisplayName: folderName) {
return folder
} else {
return substituteContainerIfNeeded(account: account)
}
} else if let account = AccountManager.shared.sortedActiveAccounts.first {
return substituteContainerIfNeeded(account: account)
} else {
return nil
}
}
static func saveDefaultContainer(_ container: Container) {
AppDefaults.addWebFeedAccountID = container.account?.accountID
if let folder = container as? Folder {
AppDefaults.addWebFeedFolderName = folder.nameForDisplay
} else {
AppDefaults.addWebFeedFolderName = nil
}
}
private static func substituteContainerIfNeeded(account: Account) -> Container? {
if !account.behaviors.contains(.disallowFeedInRootFolder) {
return account
} else {
if let folder = account.sortedFolders?.first {
return folder
} else {
return nil
}
}
}
}

View File

@@ -0,0 +1,109 @@
//
// ArticleStringFormatter.swift
// NetNewsWire
//
// Created by Brent Simmons on 8/31/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Articles
import RSParser
struct ArticleStringFormatter {
private static var feedNameCache = [String: String]()
private static var titleCache = [String: String]()
private static var summaryCache = [String: String]()
private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
return formatter
}()
private static let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter
}()
static func emptyCaches() {
feedNameCache = [String: String]()
titleCache = [String: String]()
summaryCache = [String: String]()
}
static func truncatedFeedName(_ feedName: String) -> String {
if let cachedFeedName = feedNameCache[feedName] {
return cachedFeedName
}
let maxFeedNameLength = 100
if feedName.count < maxFeedNameLength {
feedNameCache[feedName] = feedName
return feedName
}
let s = (feedName as NSString).substring(to: maxFeedNameLength)
feedNameCache[feedName] = s
return s
}
static func truncatedTitle(_ article: Article) -> String {
guard let title = article.title else {
return ""
}
if let cachedTitle = titleCache[title] {
return cachedTitle
}
var s = title.replacingOccurrences(of: "\n", with: "")
s = s.replacingOccurrences(of: "\r", with: "")
s = s.replacingOccurrences(of: "\t", with: "")
s = s.rsparser_stringByDecodingHTMLEntities()
s = s.trimmingWhitespace
s = s.collapsingWhitespace
let maxLength = 1000
if s.count < maxLength {
titleCache[title] = s
return s
}
s = (s as NSString).substring(to: maxLength)
titleCache[title] = s
return s
}
static func truncatedSummary(_ article: Article) -> String {
guard let body = article.body else {
return ""
}
let key = article.articleID + article.accountID
if let cachedBody = summaryCache[key] {
return cachedBody
}
var s = body.rsparser_stringByDecodingHTMLEntities()
s = s.strippingHTML(maxCharacters: 250)
s = s.trimmingWhitespace
s = s.collapsingWhitespace
if s == "Comments" { // Hacker News.
s = ""
}
summaryCache[key] = s
return s
}
static func dateString(_ date: Date) -> String {
if Calendar.dateIsToday(date) {
return timeFormatter.string(from: date)
}
return dateFormatter.string(from: date)
}
}

View File

@@ -0,0 +1,202 @@
//
// ArticleUtilities.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/25/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import Articles
import Account
// These handle multiple accounts.
@discardableResult
func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
var updatedArticles = Set<Article>()
for (accountID, accountArticles) in d {
guard let account = AccountManager.shared.existingAccount(with: accountID) else {
continue
}
if let accountUpdatedArticles = account.markArticles(accountArticles, statusKey: statusKey, flag: flag) {
updatedArticles.formUnion(accountUpdatedArticles)
}
}
return updatedArticles
}
private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String: Set<Article>] {
let d = Dictionary(grouping: articles, by: { $0.accountID })
return d.mapValues{ Set($0) }
}
extension Article {
var webFeed: WebFeed? {
return account?.existingWebFeed(withWebFeedID: webFeedID)
}
var preferredLink: String? {
if let url = url, !url.isEmpty {
return url
}
if let externalURL = externalURL, !externalURL.isEmpty {
return externalURL
}
return nil
}
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 {
case .disallowMarkAsUnreadAfterPeriod(let days):
return days
default:
return nil
}
}).first else {
return true
}
if logicalDatePublished.byAdding(days: markUnreadWindow) > Date() {
return true
} else {
return false
}
}
func iconImage() -> IconImage? {
if let authors = authors, authors.count == 1, let author = authors.first {
if let image = appDelegate.authorAvatarDownloader.image(for: author) {
return image
}
}
if let authors = webFeed?.authors, authors.count == 1, let author = authors.first {
if let image = appDelegate.authorAvatarDownloader.image(for: author) {
return image
}
}
guard let webFeed = webFeed else {
return nil
}
let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed)
if feedIconImage != nil {
return feedIconImage
}
if let faviconImage = appDelegate.faviconDownloader.faviconAsIcon(for: webFeed) {
return faviconImage
}
return FaviconGenerator.favicon(webFeed)
}
func byline() -> String {
guard let authors = authors ?? webFeed?.authors, !authors.isEmpty else {
return ""
}
// If the author's name is the same as the feed, then we don't want to display it.
// This code assumes that multiple authors would never match the feed name so that
// if there feed owner has an article co-author all authors are given the byline.
if authors.count == 1, let author = authors.first {
if author.name == webFeed?.nameForDisplay {
return ""
}
}
var byline = ""
var isFirstAuthor = true
for author in authors {
if !isFirstAuthor {
byline += ", "
}
isFirstAuthor = false
if let emailAddress = author.emailAddress, emailAddress.contains(" ") {
byline += emailAddress // probably name plus email address
}
else if let name = author.name, let emailAddress = author.emailAddress {
byline += "\(name) <\(emailAddress)>"
}
else if let name = author.name {
byline += name
}
else if let emailAddress = author.emailAddress {
byline += "<\(emailAddress)>"
}
else if let url = author.url {
byline += url
}
}
return byline
}
}
// MARK: Path
struct ArticlePathKey {
static let accountID = "accountID"
static let accountName = "accountName"
static let webFeedID = "webFeedID"
static let articleID = "articleID"
}
extension Article {
public var pathUserInfo: [AnyHashable : Any] {
return [
ArticlePathKey.accountID: accountID,
ArticlePathKey.accountName: account?.nameForDisplay ?? "",
ArticlePathKey.webFeedID: webFeedID,
ArticlePathKey.articleID: articleID
]
}
}
// MARK: SortableArticle
extension Article: SortableArticle {
var sortableName: String {
return webFeed?.name ?? ""
}
var sortableDate: Date {
return logicalDatePublished
}
var sortableArticleID: String {
return articleID
}
var sortableWebFeedID: String {
return webFeedID
}
}

View File

@@ -0,0 +1,51 @@
//
// CacheCleaner.swift
// NetNewsWire
//
// Created by Maurice Parker on 11/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import os.log
struct CacheCleaner {
static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CacheCleaner")
static func purgeIfNecessary() {
guard let flushDate = AppDefaults.lastImageCacheFlushDate else {
AppDefaults.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") {
if reachability.connection != .unavailable {
let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
let imagesFolderURL = tempDir.appendingPathComponent("Images")
let homePageToIconURL = tempDir.appendingPathComponent("HomePageToIconURLCache.plist")
let homePagesWithNoIconURL = tempDir.appendingPathComponent("HomePagesWithNoIconURLCache.plist")
for tempItem in [faviconsFolderURL, imagesFolderURL, homePageToIconURL, homePagesWithNoIconURL] {
do {
os_log(.info, log: self.log, "Removing cache file: %@", tempItem.absoluteString)
try FileManager.default.removeItem(at: tempItem)
} catch {
os_log(.error, log: self.log, "Could not delete cache file: %@", error.localizedDescription)
}
}
AppDefaults.lastImageCacheFlushDate = Date()
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
//
// SmallIconProvider.swift
// NetNewsWire
//
// Created by Brent Simmons on 12/16/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import Articles
import Account
import RSCore
protocol SmallIconProvider {
var smallIcon: IconImage? { get }
}
extension Account: SmallIconProvider {
var smallIcon: IconImage? {
if let image = AppAssets.image(for: type) {
return IconImage(image)
}
return nil
}
}
extension WebFeed: SmallIconProvider {
var smallIcon: IconImage? {
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
return iconImage
}
return FaviconGenerator.favicon(self)
}
}
extension Folder: SmallIconProvider {
var smallIcon: IconImage? {
AppAssets.masterFolderImage
}
}