mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Add basic ExtensionPoint support.
This commit is contained in:
51
Shared/Extensions/AddWebFeedDefaultContainer.swift
Normal file
51
Shared/Extensions/AddWebFeedDefaultContainer.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
109
Shared/Extensions/ArticleStringFormatter.swift
Normal file
109
Shared/Extensions/ArticleStringFormatter.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
202
Shared/Extensions/ArticleUtilities.swift
Normal file
202
Shared/Extensions/ArticleUtilities.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
51
Shared/Extensions/CacheCleaner.swift
Normal file
51
Shared/Extensions/CacheCleaner.swift
Normal 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()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
42
Shared/Extensions/SmallIconProvider.swift
Normal file
42
Shared/Extensions/SmallIconProvider.swift
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user