From 2dcde1ab8e9f1fe8424493901967c7078ef0d3af Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 29 Apr 2019 07:07:57 -0500 Subject: [PATCH] Create generic feed icon and timeline avatar --- NetNewsWire.xcodeproj/project.pbxproj | 12 ++- Shared/Data/SmallIconProvider.swift | 3 +- Shared/Favicons/ColorHash.swift | 72 ++++++++++++++++++ Shared/Favicons/FaviconGenerator.swift | 33 ++++++++ iOS/AppAssets.swift | 4 + .../MasterTimelineViewController.swift | 2 +- .../Contents.json | 12 +++ .../faviconTemplateImage.pdf | Bin 0 -> 5858 bytes 8 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 Shared/Favicons/ColorHash.swift create mode 100644 Shared/Favicons/FaviconGenerator.swift create mode 100644 iOS/Resources/Assets.xcassets/faviconTemplateImage.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/faviconTemplateImage.imageset/faviconTemplateImage.pdf diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 1cc18e77a..898bb9eb0 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -106,6 +106,9 @@ 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */; }; + 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; + 51EF0F79227716380050506E /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; }; + 51EF0F7A22771B890050506E /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; }; 51F85BE5227217D000C787DC /* RefreshIntervalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */; }; 51F85BE7227245FC00C787DC /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BE6227245FC00C787DC /* AboutViewController.swift */; }; 51F85BEB22724CB600C787DC /* About.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 51F85BEA22724CB600C787DC /* About.rtf */; }; @@ -662,6 +665,8 @@ 51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; 51EC114B2149FE3300B296E3 /* FolderTreeMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FolderTreeMenu.swift; path = AddFeed/FolderTreeMenu.swift; sourceTree = ""; }; + 51EF0F76227716200050506E /* FaviconGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconGenerator.swift; sourceTree = ""; }; + 51EF0F78227716380050506E /* ColorHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHash.swift; sourceTree = ""; }; 51F85BDB2272162F00C787DC /* RefreshIntervalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalViewController.swift; sourceTree = ""; }; 51F85BE6227245FC00C787DC /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 51F85BEA22724CB600C787DC /* About.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = ""; }; @@ -1241,9 +1246,11 @@ 848F6AE31FC29CFA002D422E /* Favicons */ = { isa = PBXGroup; children = ( + 51EF0F78227716380050506E /* ColorHash.swift */, 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */, - 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */, + 51EF0F76227716200050506E /* FaviconGenerator.swift */, 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */, + 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */, ); path = Favicons; sourceTree = ""; @@ -2227,6 +2234,7 @@ 840D617F2029031C009BC708 /* AppDelegate.swift in Sources */, 512E08E72268801200BDCFDD /* FeedTreeControllerDelegate.swift in Sources */, 51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */, + 51EF0F79227716380050506E /* ColorHash.swift in Sources */, 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, 51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */, 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */, @@ -2248,6 +2256,7 @@ 51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, 5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */, + 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, 51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */, 5183CCEF227125970010922C /* SettingsViewController.swift in Sources */, 51F85BE5227217D000C787DC /* RefreshIntervalViewController.swift in Sources */, @@ -2318,6 +2327,7 @@ 84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */, 848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */, 84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */, + 51EF0F7A22771B890050506E /* ColorHash.swift in Sources */, 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */, D5907D972004B7EB005947E5 /* Account+Scriptability.swift in Sources */, 841ABA4E20145E7300980E11 /* NothingInspectorViewController.swift in Sources */, diff --git a/Shared/Data/SmallIconProvider.swift b/Shared/Data/SmallIconProvider.swift index a3db9d5be..2f3c366c7 100644 --- a/Shared/Data/SmallIconProvider.swift +++ b/Shared/Data/SmallIconProvider.swift @@ -25,7 +25,7 @@ extension Feed: SmallIconProvider { #if os(macOS) return AppImages.genericFeedImage #else - return AppAssets.feedImage + return FaviconGenerator.favicon(self) #endif } } @@ -39,4 +39,5 @@ extension Folder: SmallIconProvider { return AppAssets.masterFolderImage #endif } + } diff --git a/Shared/Favicons/ColorHash.swift b/Shared/Favicons/ColorHash.swift new file mode 100644 index 000000000..8849a0fe3 --- /dev/null +++ b/Shared/Favicons/ColorHash.swift @@ -0,0 +1,72 @@ +// +// ColorHash.swift +// ColorHash +// +// Created by Atsushi Nagase on 11/25/15. +// Copyright © 2015 LittleApps Inc. All rights reserved. +// +// Original Project: https://github.com/ngs/color-hash.swift + +import Foundation +#if os(iOS) || os(tvOS) +import UIKit +#elseif os(watchOS) +import WatchKit +#elseif os(OSX) +import Cocoa +#endif + +public class ColorHash { + + public static let defaultLS = [CGFloat(0.35), CGFloat(0.5), CGFloat(0.65)] + let seed = CGFloat(131.0) + let seed2 = CGFloat(137.0) + let maxSafeInteger = 9007199254740991.0 / CGFloat(137.0) + let full = CGFloat(360.0) + + public private(set) var str: String + public private(set) var brightness: [CGFloat] + public private(set) var saturation: [CGFloat] + + public init(_ str: String, _ saturation: [CGFloat] = defaultLS, _ brightness: [CGFloat] = defaultLS) { + self.str = str + self.saturation = saturation + self.brightness = brightness + } + + public var bkdrHash: CGFloat { + var hash = CGFloat(0) + for char in "\(str)x" { + if let scl = String(char).unicodeScalars.first?.value { + if hash > maxSafeInteger { + hash = hash / seed2 + } + hash = hash * seed + CGFloat(scl) + } + } + return hash + } + + public var HSB: (CGFloat, CGFloat, CGFloat) { + var hash = CGFloat(bkdrHash) + let H = hash.truncatingRemainder(dividingBy: (full - 1.0)) / full + hash /= full + let S = saturation[Int((full * hash).truncatingRemainder(dividingBy: CGFloat(saturation.count)))] + hash /= CGFloat(saturation.count) + let B = brightness[Int((full * hash).truncatingRemainder(dividingBy: CGFloat(brightness.count)))] + return (H, S, B) + } + + #if os(iOS) || os(tvOS) || os(watchOS) + public var color: UIColor { + let (H, S, B) = HSB + return UIColor(hue: H, saturation: S, brightness: B, alpha: 1.0) + } + #elseif os(OSX) + public var color: NSColor { + let (H, S, B) = HSB + return NSColor(hue: H, saturation: S, brightness: B, alpha: 1.0) + } + #endif + +} diff --git a/Shared/Favicons/FaviconGenerator.swift b/Shared/Favicons/FaviconGenerator.swift new file mode 100644 index 000000000..55c0daa09 --- /dev/null +++ b/Shared/Favicons/FaviconGenerator.swift @@ -0,0 +1,33 @@ +// +// FaviconGenerator.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 4/29/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Foundation +import RSCore +import Account + +final class FaviconGenerator { + + private static var faviconGeneratorCache = [String: RSImage]() // feedURL: RSImage + + static func favicon(_ feed: Feed) -> RSImage { + + if let favicon = FaviconGenerator.faviconGeneratorCache[feed.url] { + return favicon + } + + let colorHash = ColorHash(feed.url) + if let favicon = AppAssets.faviconTemplateImage.maskWithColor(color: colorHash.color) { + FaviconGenerator.faviconGeneratorCache[feed.url] = favicon + return favicon + } else { + return AppAssets.faviconTemplateImage + } + + } + +} diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 2edcde6ee..d67309528 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -32,6 +32,10 @@ struct AppAssets { return image.maskWithColor(color: AppAssets.chevronDisclosureColor)! }() + static var faviconTemplateImage: RSImage = { + return RSImage(named: "faviconTemplateImage")! + }() + static var feedColor: UIColor = { return UIColor(named: "feedColor")! }() diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 5070e23df..6df870d96 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -404,7 +404,7 @@ private extension MasterTimelineViewController { } } - return nil + return FaviconGenerator.favicon(feed) } diff --git a/iOS/Resources/Assets.xcassets/faviconTemplateImage.imageset/Contents.json b/iOS/Resources/Assets.xcassets/faviconTemplateImage.imageset/Contents.json new file mode 100644 index 000000000..5a1ae2f6a --- /dev/null +++ b/iOS/Resources/Assets.xcassets/faviconTemplateImage.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "faviconTemplateImage.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOS/Resources/Assets.xcassets/faviconTemplateImage.imageset/faviconTemplateImage.pdf b/iOS/Resources/Assets.xcassets/faviconTemplateImage.imageset/faviconTemplateImage.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d6bcb5b69b270394b8e33a787388fd7a3dddd7ee GIT binary patch literal 5858 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