diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index a5e0d096a..dea0ced23 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; + 51126DA5225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; 5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; }; 5127B239222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; }; 5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; @@ -747,6 +749,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = ""; }; 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailKeyboardDelegate.swift; sourceTree = ""; }; 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = ""; }; 519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = ""; }; @@ -1457,6 +1460,7 @@ children = ( 849A97971ED9EFAA007D329B /* Node-Extensions.swift */, 8405DD9B22153BD7008CE1BF /* NSView-Extensions.swift */, + 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */, ); name = Extensions; path = NetNewsWire/Extensions; @@ -2497,6 +2501,7 @@ 840F7C4A21BDA4B40057E851 /* RSHTMLMetadata+Extension.swift in Sources */, 840F7C4B21BDA4B40057E851 /* SendToMarsEditCommand.swift in Sources */, 842AE5BC2241F37B004A742C /* AccountsPreferencesViewController.swift in Sources */, + 51126DA5225FDE2F00722696 /* RSImage-Extensions.swift in Sources */, 840F7C4C21BDA4B40057E851 /* ScriptingObjectContainer.swift in Sources */, 840F7C4D21BDA4B40057E851 /* ArticleStylesManager.swift in Sources */, 840F7C4E21BDA4B40057E851 /* SharingServiceDelegate.swift in Sources */, @@ -2682,6 +2687,7 @@ 844B5B591FE9FE4F00C7C76A /* SidebarKeyboardDelegate.swift in Sources */, 51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */, 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */, + 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */, 84A1500320048D660046AD9A /* SendToCommand.swift in Sources */, 845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */, 849A97851ED9ECCD007D329B /* PreferencesWindowController.swift in Sources */, diff --git a/NetNewsWire/Extensions/RSImage-Extensions.swift b/NetNewsWire/Extensions/RSImage-Extensions.swift new file mode 100644 index 000000000..39251498c --- /dev/null +++ b/NetNewsWire/Extensions/RSImage-Extensions.swift @@ -0,0 +1,41 @@ +// +// RSImage-Extensions.swift +// RSCore +// +// Created by Maurice Parker on 4/11/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation +import RSCore + +extension RSImage { + + static let avatarSize = 48 + + static func scaledForAvatar(_ data: Data, imageResultBlock: @escaping (RSImage?) -> Void) { + DispatchQueue.global().async { + let image = RSImage.scaledForAvatar(data) + DispatchQueue.main.async { + imageResultBlock(image) + } + } + } + + static func scaledForAvatar(_ data: Data) -> RSImage? { + + let scaledMaxPixelSize = Int(ceil(CGFloat(RSImage.avatarSize) * RSScreen.mainScreenScale)) + guard let cgImage = RSImage.scaleImage(data, maxPixelSize: scaledMaxPixelSize) else { + return nil + } + + #if os(iOS) + return RSImage(cgImage: cgImage) + #else + let size = NSSize(width: scaledMaxPixelSize, height: scaledMaxPixelSize) + return RSImage(cgImage: cgImage, size: size) + #endif + + } + +} diff --git a/NetNewsWire/Favicons/FaviconDownloader.swift b/NetNewsWire/Favicons/FaviconDownloader.swift index 454f2b374..52c598d05 100644 --- a/NetNewsWire/Favicons/FaviconDownloader.swift +++ b/NetNewsWire/Favicons/FaviconDownloader.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Ranchero Software. All rights reserved. // -import AppKit +import Foundation import Articles import Account import RSCore @@ -40,7 +40,7 @@ final class FaviconDownloader { // MARK: - API - func favicon(for feed: Feed) -> NSImage? { + func favicon(for feed: Feed) -> RSImage? { assert(Thread.isMainThread) @@ -62,13 +62,13 @@ final class FaviconDownloader { return nil } - func favicon(with faviconURL: String) -> NSImage? { + func favicon(with faviconURL: String) -> RSImage? { let downloader = faviconDownloader(withURL: faviconURL) return downloader.image } - func favicon(withHomePageURL homePageURL: String) -> NSImage? { + func favicon(withHomePageURL homePageURL: String) -> RSImage? { let url = homePageURL.rs_normalizedURL() if homePageURLsWithNoFaviconURL.contains(url) { diff --git a/NetNewsWire/Favicons/SingleFaviconDownloader.swift b/NetNewsWire/Favicons/SingleFaviconDownloader.swift index be7ffed08..9a6e1c44a 100644 --- a/NetNewsWire/Favicons/SingleFaviconDownloader.swift +++ b/NetNewsWire/Favicons/SingleFaviconDownloader.swift @@ -25,7 +25,7 @@ final class SingleFaviconDownloader { } let faviconURL: String - var image: NSImage? + var image: RSImage? private var lastDownloadAttemptDate: Date private var diskStatus = DiskStatus.unknown @@ -89,7 +89,7 @@ private extension SingleFaviconDownloader { } } - func readFromDisk(_ callback: @escaping (NSImage?) -> Void) { + func readFromDisk(_ callback: @escaping (RSImage?) -> Void) { guard diskStatus != .notOnDisk else { callback(nil) @@ -99,7 +99,7 @@ private extension SingleFaviconDownloader { queue.async { if let data = self.diskCache[self.diskKey], !data.isEmpty { - NSImage.rs_image(with: data, imageResultBlock: callback) + RSImage.scaledForAvatar(data, imageResultBlock: callback) return } @@ -123,7 +123,7 @@ private extension SingleFaviconDownloader { } } - func downloadFavicon(_ callback: @escaping (NSImage?) -> Void) { + func downloadFavicon(_ callback: @escaping (RSImage?) -> Void) { guard let url = URL(string: faviconURL) else { callback(nil) @@ -134,7 +134,7 @@ private extension SingleFaviconDownloader { if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { self.saveToDisk(data) - NSImage.rs_image(with: data, imageResultBlock: callback) + RSImage.scaledForAvatar(data, imageResultBlock: callback) return } @@ -151,4 +151,5 @@ private extension SingleFaviconDownloader { assert(Thread.isMainThread) NotificationCenter.default.post(name: .DidLoadFavicon, object: self) } + } diff --git a/NetNewsWire/Images/AuthorAvatarDownloader.swift b/NetNewsWire/Images/AuthorAvatarDownloader.swift index 009c75a95..9eedf5f0a 100644 --- a/NetNewsWire/Images/AuthorAvatarDownloader.swift +++ b/NetNewsWire/Images/AuthorAvatarDownloader.swift @@ -6,8 +6,9 @@ // Copyright © 2017 Ranchero Software. All rights reserved. // -import AppKit +import Foundation import Articles +import RSCore extension Notification.Name { @@ -17,7 +18,7 @@ extension Notification.Name { final class AuthorAvatarDownloader { private let imageDownloader: ImageDownloader - private var cache = [String: NSImage]() // avatarURL: NSImage + private var cache = [String: RSImage]() // avatarURL: RSImage private var waitingForAvatarURLs = Set() init(imageDownloader: ImageDownloader) { @@ -26,19 +27,22 @@ final class AuthorAvatarDownloader { NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: imageDownloader) } - func image(for author: Author) -> NSImage? { + func image(for author: Author) -> RSImage? { guard let avatarURL = author.avatarURL else { return nil } + if let cachedImage = cache[avatarURL] { return cachedImage } - if let image = imageDownloader.image(for: avatarURL) { - handleImageDidBecomeAvailable(avatarURL, image) - return image - } - else { + + if let imageData = imageDownloader.image(for: avatarURL) { + if let image = RSImage.scaledForAvatar(imageData) { + handleImageDidBecomeAvailable(avatarURL, image) + return image + } + } else { waitingForAvatarURLs.insert(avatarURL) } @@ -50,20 +54,24 @@ final class AuthorAvatarDownloader { guard let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { return } + guard waitingForAvatarURLs.contains(avatarURL) else { return } - guard let image = imageDownloader.image(for: avatarURL) else { + + guard let imageData = imageDownloader.image(for: avatarURL), + let image = RSImage.scaledForAvatar(imageData) else { return } handleImageDidBecomeAvailable(avatarURL, image) + } } private extension AuthorAvatarDownloader { - func handleImageDidBecomeAvailable(_ avatarURL: String, _ image: NSImage) { + func handleImageDidBecomeAvailable(_ avatarURL: String, _ image: RSImage) { if cache[avatarURL] == nil { cache[avatarURL] = image diff --git a/NetNewsWire/Images/FeaturedImageDownloader.swift b/NetNewsWire/Images/FeaturedImageDownloader.swift index e67361ac3..618cf07de 100644 --- a/NetNewsWire/Images/FeaturedImageDownloader.swift +++ b/NetNewsWire/Images/FeaturedImageDownloader.swift @@ -6,8 +6,9 @@ // Copyright © 2017 Ranchero Software. All rights reserved. // -import AppKit +import Foundation import Articles +import RSCore import RSParser final class FeaturedImageDownloader { @@ -22,7 +23,7 @@ final class FeaturedImageDownloader { self.imageDownloader = imageDownloader } - func image(for article: Article) -> NSImage? { + func image(for article: Article) -> RSImage? { if let url = article.imageURL { return image(forFeaturedImageURL: url) @@ -33,7 +34,7 @@ final class FeaturedImageDownloader { return nil } - func image(forArticleURL articleURL: String) -> NSImage? { + func image(forArticleURL articleURL: String) -> RSImage? { if articleURLsWithNoFeaturedImage.contains(articleURL) { return nil @@ -46,10 +47,13 @@ final class FeaturedImageDownloader { return nil } - func image(forFeaturedImageURL featuredImageURL: String) -> NSImage? { - - return imageDownloader.image(for: featuredImageURL) + func image(forFeaturedImageURL featuredImageURL: String) -> RSImage? { + if let data = imageDownloader.image(for: featuredImageURL) { + return RSImage(data: data) + } + return nil } + } private extension FeaturedImageDownloader { diff --git a/NetNewsWire/Images/FeedIconDownloader.swift b/NetNewsWire/Images/FeedIconDownloader.swift index 13f2c2ea0..847c5775b 100644 --- a/NetNewsWire/Images/FeedIconDownloader.swift +++ b/NetNewsWire/Images/FeedIconDownloader.swift @@ -9,6 +9,7 @@ import AppKit import Articles import Account +import RSCore import RSWeb import RSParser @@ -23,14 +24,14 @@ public final class FeedIconDownloader { private var homePageToIconURLCache = [String: String]() private var homePagesWithNoIconURL = Set() private var urlsInProgress = Set() - private var cache = [Feed: NSImage]() + private var cache = [Feed: RSImage]() init(imageDownloader: ImageDownloader) { self.imageDownloader = imageDownloader } - func icon(for feed: Feed) -> NSImage? { + func icon(for feed: Feed) -> RSImage? { if let cachedImage = cache[feed] { return cachedImage @@ -58,7 +59,7 @@ public final class FeedIconDownloader { private extension FeedIconDownloader { - func icon(forHomePageURL homePageURL: String) -> NSImage? { + func icon(forHomePageURL homePageURL: String) -> RSImage? { if homePagesWithNoIconURL.contains(homePageURL) { return nil @@ -72,9 +73,11 @@ private extension FeedIconDownloader { return nil } - func icon(forURL url: String) -> NSImage? { - - return imageDownloader.image(for: url) + func icon(forURL url: String) -> RSImage? { + if let imageData = imageDownloader.image(for: url), let image = RSImage.scaledForAvatar(imageData) { + return image + } + return nil } func postFeedIconDidBecomeAvailableNotification(_ feed: Feed) { diff --git a/NetNewsWire/Images/ImageDownloader.swift b/NetNewsWire/Images/ImageDownloader.swift index f5fd3ef5a..299aba887 100644 --- a/NetNewsWire/Images/ImageDownloader.swift +++ b/NetNewsWire/Images/ImageDownloader.swift @@ -6,7 +6,7 @@ // Copyright © 2017 Ranchero Software. All rights reserved. // -import AppKit +import Foundation import RSCore import RSWeb @@ -20,7 +20,7 @@ final class ImageDownloader { private let folder: String private var diskCache: BinaryDiskCache private let queue: DispatchQueue - private var imageCache = [String: NSImage]() // url: image + private var imageCache = [String: Data]() // url: image private var urlsInProgress = Set() private var badURLs = Set() // That return a 404 or whatever. Just skip them in the future. @@ -32,10 +32,10 @@ final class ImageDownloader { } @discardableResult - func image(for url: String) -> NSImage? { + func image(for url: String) -> Data? { - if let image = imageCache[url] { - return image + if let data = imageCache[url] { + return data } findImage(url) @@ -45,7 +45,7 @@ final class ImageDownloader { private extension ImageDownloader { - func cacheImage(_ url: String, _ image: NSImage) { + func cacheImage(_ url: String, _ image: Data) { imageCache[url] = image postImageDidBecomeAvailableNotification(url) @@ -76,12 +76,14 @@ private extension ImageDownloader { } } - func readFromDisk(_ url: String, _ callback: @escaping (NSImage?) -> Void) { + func readFromDisk(_ url: String, _ callback: @escaping (Data?) -> Void) { queue.async { if let data = self.diskCache[self.diskKey(url)], !data.isEmpty { - NSImage.rs_image(with: data, imageResultBlock: callback) + DispatchQueue.main.async { + callback(data) + } return } @@ -91,7 +93,7 @@ private extension ImageDownloader { } } - func downloadImage(_ url: String, _ callback: @escaping (NSImage?) -> Void) { + func downloadImage(_ url: String, _ callback: @escaping (Data?) -> Void) { guard let imageURL = URL(string: url) else { callback(nil) @@ -102,7 +104,7 @@ private extension ImageDownloader { if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { self.saveToDisk(url, data) - NSImage.rs_image(with: data, imageResultBlock: callback) + callback(data) return } diff --git a/NetNewsWire/MainWindow/Timeline/TimelineViewController.swift b/NetNewsWire/MainWindow/Timeline/TimelineViewController.swift index e69473f54..5c0f3719b 100644 --- a/NetNewsWire/MainWindow/Timeline/TimelineViewController.swift +++ b/NetNewsWire/MainWindow/Timeline/TimelineViewController.swift @@ -692,13 +692,16 @@ extension TimelineViewController: NSTableViewDelegate { private func featuredImageFor(_ article: Article) -> NSImage? { if let url = article.imageURL { - return appDelegate.imageDownloader.image(for: url) + if let imageData = appDelegate.imageDownloader.image(for: url) { + return NSImage(data: imageData) + } } + return nil + } private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) { - cell.objectValue = nil cell.cellData = TimelineCellData() }