From 33fef5ea1c60c500e58a3cd71d30f18df193a75d Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 25 Nov 2017 20:12:53 -0800 Subject: [PATCH] Add ImageDownloader. --- Evergreen.xcodeproj/project.pbxproj | 13 +++ Evergreen/AppDelegate.swift | 7 ++ Evergreen/Images/ImageDownloader.swift | 127 +++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 Evergreen/Images/ImageDownloader.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index e5c6a1f98..cab6f3f40 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 842E45E51ED8C6B7000A8B52 /* MainWindowSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */; }; 842E45E71ED8C747000A8B52 /* DB5.plist in Resources */ = {isa = PBXBuildFile; fileRef = 842E45E61ED8C747000A8B52 /* DB5.plist */; }; 84513F901FAA63950023A1A9 /* FeedListControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */; }; + 845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; }; 845A29091FC74B8E007B49E3 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; 845A291B1FC75AA6007B49E3 /* SeekingFavicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A291A1FC75AA6007B49E3 /* SeekingFavicon.swift */; }; 845A29221FC9251E007B49E3 /* SidebarCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */; }; @@ -407,6 +408,7 @@ 842E45E41ED8C6B7000A8B52 /* MainWindowSplitView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainWindowSplitView.swift; sourceTree = ""; }; 842E45E61ED8C747000A8B52 /* DB5.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = DB5.plist; path = Evergreen/Resources/DB5.plist; sourceTree = ""; }; 84513F8F1FAA63950023A1A9 /* FeedListControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListControlsView.swift; sourceTree = ""; }; + 845213221FCA5B10003B6E93 /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFaviconDownloader.swift; sourceTree = ""; }; 845A291A1FC75AA6007B49E3 /* SeekingFavicon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeekingFavicon.swift; sourceTree = ""; }; 845A29211FC9251E007B49E3 /* SidebarCellLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarCellLayout.swift; sourceTree = ""; }; @@ -558,6 +560,15 @@ path = Evergreen/MainWindow; sourceTree = ""; }; + 845213211FCA5B10003B6E93 /* Images */ = { + isa = PBXGroup; + children = ( + 845213221FCA5B10003B6E93 /* ImageDownloader.swift */, + ); + name = Images; + path = Evergreen/Images; + sourceTree = ""; + }; 845A29251FC928C7007B49E3 /* Cell */ = { isa = PBXGroup; children = ( @@ -785,6 +796,7 @@ 84F2D5341FC22FCB00998D64 /* PseudoFeeds */, 849A97561ED9EB0D007D329B /* Data */, 848F6AE31FC29CFA002D422E /* Favicons */, + 845213211FCA5B10003B6E93 /* Images */, 849A97961ED9EFAA007D329B /* Extensions */, 849A97991ED9EFB6007D329B /* Resources */, 84FB9A2C1EDCD6A4003D53B9 /* Frameworks */, @@ -1351,6 +1363,7 @@ 849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */, 849A97831ED9EC63007D329B /* StatusBarView.swift in Sources */, 84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */, + 845213231FCA5B11003B6E93 /* ImageDownloader.swift in Sources */, 849A97431ED9EAA9007D329B /* AddFolderWindowController.swift in Sources */, 849A97921ED9EF65007D329B /* IndeterminateProgressWindowController.swift in Sources */, 849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */, diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index 0c399785c..94c0b6eab 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -22,6 +22,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, var currentTheme: VSTheme! var faviconDownloader: FaviconDownloader! + var imageDownloader: ImageDownloader! var appName: String! var pseudoFeeds = [PseudoFeed]() @@ -130,11 +131,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let tempDirectory = NSTemporaryDirectory() let cacheFolder = (tempDirectory as NSString).appendingPathComponent("com.ranchero.evergreen") + let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons") let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder) try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil) faviconDownloader = FaviconDownloader(folder: faviconsFolder) + let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images") + let imagesFolderURL = URL(fileURLWithPath: imagesFolder) + try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil) + imageDownloader = ImageDownloader(folder: imagesFolder) + let todayFeed = SmartFeed(delegate: TodayFeedDelegate()) let unreadFeed = UnreadFeed() let starredFeed = SmartFeed(delegate: StarredFeedDelegate()) diff --git a/Evergreen/Images/ImageDownloader.swift b/Evergreen/Images/ImageDownloader.swift new file mode 100644 index 000000000..4af6f4a2d --- /dev/null +++ b/Evergreen/Images/ImageDownloader.swift @@ -0,0 +1,127 @@ +// +// ImageDownloader.swift +// Evergreen +// +// Created by Brent Simmons on 11/25/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import AppKit +import RSCore +import RSWeb + +extension Notification.Name { + + static let ImageDidBecomeAvailable = Notification.Name("ImageDidBecomeAvailableNotification") // ImageDownloader.UserInfoKey.imageURL +} + +final class ImageDownloader { + + private let folder: String + private var diskCache: BinaryDiskCache + private let queue: DispatchQueue + private var imageCache = [String: NSImage]() // url: image + + struct UserInfoKey { + static let imageURL = "imageURL" + } + + init(folder: String) { + + self.folder = folder + self.diskCache = BinaryDiskCache(folder: folder) + self.queue = DispatchQueue(label: "ImageDownloader serial queue - \(folder)") + } + + func image(for url: String) -> NSImage? { + + if let image = imageCache[url] { + return image + } + + findImage(url) + return nil + } +} + +private extension ImageDownloader { + + func cacheImage(_ url: String, _ image: NSImage) { + + imageCache[url] = image + postImageDidBecomeAvailableNotification(url) + } + + func findImage(_ url: String) { + + readFromDisk(url) { (image) in + + if let image = image { + self.cacheImage(url, image) + return + } + + self.downloadImage(url) { (image) in + + if let image = image { + self.cacheImage(url, image) + } + } + } + } + + func readFromDisk(_ url: String, _ callback: @escaping (NSImage?) -> Void) { + + queue.async { + + if let data = self.diskCache[self.diskKey(url)], !data.isEmpty { + NSImage.rs_image(with: data, imageResultBlock: callback) + return + } + + DispatchQueue.main.async { + callback(nil) + } + } + } + + func downloadImage(_ url: String, _ callback: @escaping (NSImage?) -> Void) { + + guard let url = URL(string: url) else { + callback(nil) + return + } + + downloadUsingCache(url) { (data, response, error) in + + if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { + self.saveToDisk(url.absoluteString, data) + NSImage.rs_image(with: data, imageResultBlock: callback) + return + } + + if let error = error { + appDelegate.logMessage("Error downloading image at \(url): \(error)", type: .warning) + } + + callback(nil) + } + } + + func saveToDisk(_ url: String, _ data: Data) { + + queue.async { + self.diskCache[self.diskKey(url)] = data + } + } + + func diskKey(_ url: String) -> String { + + return (url as NSString).rs_md5Hash() + } + + func postImageDidBecomeAvailableNotification(_ url: String) { + + NotificationCenter.default.post(name: .ImageDidBecomeAvailable, object: self, userInfo: [UserInfoKey.imageURL: url]) + } +}