From 40b9be670948b40de3b1a8ebd762532f825c9426 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 14 Jun 2019 15:33:13 -0500 Subject: [PATCH] center smaller avatars on a solid color background image to make all avatars a consistent size --- Mac/AppAssets.swift | 9 ++- .../Timeline/TimelineViewController.swift | 16 +++++- .../Contents.json | 20 +++++++ .../Contents.json | 20 +++++++ NetNewsWire.xcodeproj/project.pbxproj | 6 +- .../Article Rendering/ArticleRenderer.swift | 6 +- Shared/Extensions/RSImage-Extensions.swift | 55 +++++++++++++++++-- Shared/Favicons/FaviconDownloader.swift | 21 +++++++ Shared/Favicons/SingleFaviconDownloader.swift | 4 +- Shared/Images/AuthorAvatarDownloader.swift | 4 ++ Shared/Images/FeedIconDownloader.swift | 4 ++ iOS/AppAssets.swift | 8 +++ .../MasterTimelineViewController.swift | 2 +- .../Contents.json | 20 +++++++ .../Contents.json | 20 +++++++ submodules/RSCore | 2 +- 16 files changed, 199 insertions(+), 18 deletions(-) create mode 100644 Mac/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index 86503c6eb..4362731c1 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -37,5 +37,12 @@ struct AppAssets { static var faviconTemplateImage: RSImage = { return RSImage(named: "faviconTemplateImage")! }() - + + static var avatarBackgroundColor: NSColor = { + return NSColor(named: NSColor.Name("avatarBackgroundColor"))! + }() + + static var avatarDarkBackgroundColor: NSColor = { + return NSColor(named: NSColor.Name("avatarDarkBackgroundColor"))! + }() } diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 3a2ca3ea9..60f76fa0f 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -11,6 +11,10 @@ import RSCore import Articles import Account +extension Notification.Name { + static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification") +} + protocol TimelineDelegate: class { func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?) } @@ -148,6 +152,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner { NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(calendarDayChanged(_:)), name: .NSCalendarDayChanged, object: nil) + DistributedNotificationCenter.default.addObserver(self, selector: #selector(appleInterfaceThemeChanged), name: .AppleInterfaceThemeChangedNotification, object: nil) didRegisterForNotifications = true } @@ -521,6 +526,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner { } } + @objc func appleInterfaceThemeChanged(_ note: Notification) { + appDelegate.authorAvatarDownloader.resetCache() + appDelegate.feedIconDownloader.resetCache() + appDelegate.faviconDownloader.resetCache() + performBlockAndRestoreSelection { + tableView.reloadData() + } + } + // MARK: - Reloading Data private func cellForRowView(_ rowView: NSView) -> NSView? { @@ -752,7 +766,7 @@ extension TimelineViewController: NSTableViewDelegate { return feedIcon } - if let favicon = appDelegate.faviconDownloader.favicon(for: feed) { + if let favicon = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) { return favicon } diff --git a/Mac/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json b/Mac/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json new file mode 100644 index 000000000..de7a620f3 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "56", + "alpha" : "1.000", + "blue" : "56", + "green" : "56" + } + } + } + ] +} \ No newline at end of file diff --git a/Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json b/Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json new file mode 100644 index 000000000..57a5cc49d --- /dev/null +++ b/Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "242", + "alpha" : "1.000", + "blue" : "242", + "green" : "242" + } + } + } + ] +} \ No newline at end of file diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index ab7336369..3dde7f1f6 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -2282,7 +2282,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# See https://github.com/Watson1978/kotori/commit/ffe320f2e058828f0af294b65ed88dfd7baaabff\n\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n codesign --verbose --force --deep -o runtime --sign \"Developer ID Application: Brent Simmons\" \"${CODESIGNING_FOLDER_PATH}/Contents/Frameworks/Sparkle.framework/Versions/A/Resources/AutoUpdate.app\"\nfi\n"; + shellScript = "# See https://github.com/Watson1978/kotori/commit/ffe320f2e058828f0af294b65ed88dfd7baaabff\n\nif [ \"${CONFIGURATION}\" = \"Release\" ]; then\n codesign --verbose --force --deep -o runtime --sign \"Developer ID Application: Vincode, Inc\" \"${CODESIGNING_FOLDER_PATH}/Contents/Frameworks/Sparkle.framework/Versions/A/Resources/AutoUpdate.app\"\nfi\n"; }; 84C987A52000AC9E0066B150 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -2823,7 +2823,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = iOS/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -2886,7 +2886,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = iOS/Resources/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.ranchero.NetNewsWire-Evergreen.iOS"; diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 7643b1091..833228120 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -210,11 +210,7 @@ private extension ArticleRenderer { } func base64String(forImage image: RSImage) -> String? { - #if os(macOS) - return image.tiffRepresentation?.base64EncodedString() - #else - return image.pngData()?.base64EncodedString() - #endif + return image.dataRepresentation()?.base64EncodedString() } func singleArticleSpecifiedAuthor() -> Author? { diff --git a/Shared/Extensions/RSImage-Extensions.swift b/Shared/Extensions/RSImage-Extensions.swift index 2e62704b3..3406a959b 100644 --- a/Shared/Extensions/RSImage-Extensions.swift +++ b/Shared/Extensions/RSImage-Extensions.swift @@ -21,16 +21,17 @@ extension RSImage { } } } -} - -private extension RSImage { static func scaledForAvatar(_ data: Data) -> RSImage? { let scaledMaxPixelSize = Int(ceil(CGFloat(RSImage.avatarSize) * RSScreen.mainScreenScale)) - guard let cgImage = RSImage.scaleImage(data, maxPixelSize: scaledMaxPixelSize) else { + guard var cgImage = RSImage.scaleImage(data, maxPixelSize: scaledMaxPixelSize) else { return nil } + if cgImage.width < avatarSize || cgImage.height < avatarSize { + cgImage = RSImage.compositeAvatar(cgImage) + } + #if os(iOS) return RSImage(cgImage: cgImage) #else @@ -41,3 +42,49 @@ private extension RSImage { } } + +private extension RSImage { + + #if os(iOS) + + static func compositeAvatar(_ avatar: CGImage) -> CGImage { + let rect = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize) + UIGraphicsBeginImageContext(rect.size) + if let context = UIGraphicsGetCurrentContext() { + context.setFillColor(AppAssets.avatarLightBackgroundColor.cgColor) + context.fill(rect) + context.translateBy(x: 0.0, y: CGFloat(integerLiteral: avatarSize)); + context.scaleBy(x: 1.0, y: -1.0) + let avatarRect = CGRect(x: (avatarSize - avatar.width) / 2, y: (avatarSize - avatar.height) / 2, width: avatar.width, height: avatar.height) + context.draw(avatar, in: avatarRect) + } + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return img!.cgImage! + } + + #else + + static func compositeAvatar(_ avatar: CGImage) -> CGImage { + var resultRect = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize) + let resultImage = NSImage(size: resultRect.size) + + resultImage.lockFocus() + if let context = NSGraphicsContext.current?.cgContext { + if NSApplication.shared.effectiveAppearance.isDarkMode { + context.setFillColor(AppAssets.avatarDarkBackgroundColor.cgColor) + } else { + context.setFillColor(AppAssets.avatarLightBackgroundColor.cgColor) + } + context.fill(resultRect) + let avatarRect = CGRect(x: (avatarSize - avatar.width) / 2, y: (avatarSize - avatar.height) / 2, width: avatar.width, height: avatar.height) + context.draw(avatar, in: avatarRect) + } + resultImage.unlockFocus() + + return resultImage.cgImage(forProposedRect: &resultRect, context: nil, hints: nil)! + } + + #endif + +} diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index 52c598d05..0cedd923c 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -24,6 +24,7 @@ final class FaviconDownloader { private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL private var homePageURLsWithNoFaviconURL = Set() private let queue: DispatchQueue + private var cache = [Feed: RSImage]() // faviconURL: RSImage struct UserInfoKey { static let faviconURL = "faviconURL" @@ -40,6 +41,10 @@ final class FaviconDownloader { // MARK: - API + func resetCache() { + cache = [Feed: RSImage]() + } + func favicon(for feed: Feed) -> RSImage? { assert(Thread.isMainThread) @@ -61,6 +66,22 @@ final class FaviconDownloader { return nil } + + func faviconAsAvatar(for feed: Feed) -> RSImage? { + + if let image = cache[feed] { + return image + } + + if let image = favicon(for: feed), let imageData = image.dataRepresentation() { + if let scaledImage = RSImage.scaledForAvatar(imageData) { + cache[feed] = scaledImage + return scaledImage + } + } + + return nil + } func favicon(with faviconURL: String) -> RSImage? { diff --git a/Shared/Favicons/SingleFaviconDownloader.swift b/Shared/Favicons/SingleFaviconDownloader.swift index 044b23761..57730a903 100644 --- a/Shared/Favicons/SingleFaviconDownloader.swift +++ b/Shared/Favicons/SingleFaviconDownloader.swift @@ -99,7 +99,7 @@ private extension SingleFaviconDownloader { queue.async { if let data = self.diskCache[self.diskKey], !data.isEmpty { - RSImage.scaledForAvatar(data, imageResultBlock: callback) + RSImage.rs_image(with: data, imageResultBlock: callback) return } @@ -134,7 +134,7 @@ private extension SingleFaviconDownloader { if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { self.saveToDisk(data) - RSImage.scaledForAvatar(data, imageResultBlock: callback) + RSImage.rs_image(with: data, imageResultBlock: callback) return } diff --git a/Shared/Images/AuthorAvatarDownloader.swift b/Shared/Images/AuthorAvatarDownloader.swift index e1629996b..ef0b04eba 100644 --- a/Shared/Images/AuthorAvatarDownloader.swift +++ b/Shared/Images/AuthorAvatarDownloader.swift @@ -27,6 +27,10 @@ final class AuthorAvatarDownloader { NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: imageDownloader) } + func resetCache() { + cache = [String: RSImage]() + } + func image(for author: Author) -> RSImage? { guard let avatarURL = author.avatarURL else { diff --git a/Shared/Images/FeedIconDownloader.swift b/Shared/Images/FeedIconDownloader.swift index 630a13b94..2fa08b0dc 100644 --- a/Shared/Images/FeedIconDownloader.swift +++ b/Shared/Images/FeedIconDownloader.swift @@ -31,6 +31,10 @@ public final class FeedIconDownloader { self.imageDownloader = imageDownloader } + func resetCache() { + cache = [Feed: RSImage]() + } + func icon(for feed: Feed) -> RSImage? { if let cachedImage = cache[feed] { diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 02c3836b4..30c9443b2 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -10,6 +10,14 @@ import RSCore struct AppAssets { + static var avatarDarkBackgroundColor: UIColor { + return UIColor(named: "avatarDarkBackgroundColor")! + } + + static var avatarLightBackgroundColor: UIColor { + return UIColor(named: "avatarLightBackgroundColor")! + } + static var circleClosedImage: RSImage = { return RSImage(named: "circleClosedImage")! }() diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 0ac2aa9cf..fb2383812 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -407,7 +407,7 @@ private extension MasterTimelineViewController { return feedIconImage } - if let feed = article.feed, let faviconImage = appDelegate.faviconDownloader.favicon(for: feed) { + if let feed = article.feed, let faviconImage = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) { return faviconImage } diff --git a/iOS/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json b/iOS/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json new file mode 100644 index 000000000..de7a620f3 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/avatarDarkBackgroundColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "56", + "alpha" : "1.000", + "blue" : "56", + "green" : "56" + } + } + } + ] +} \ No newline at end of file diff --git a/iOS/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json b/iOS/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json new file mode 100644 index 000000000..57a5cc49d --- /dev/null +++ b/iOS/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "242", + "alpha" : "1.000", + "blue" : "242", + "green" : "242" + } + } + } + ] +} \ No newline at end of file diff --git a/submodules/RSCore b/submodules/RSCore index aa7107080..111690033 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit aa7107080e90d5be11ae54fd41ee4dd192468e30 +Subproject commit 111690033354afc1cf57e37a326c344a0fe93b77