diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 2db24eb0d..fb57fdd1c 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -53,9 +53,9 @@ public enum AccountType: Int, Codable { } public enum FetchType { - case starred - case unread - case today + case starred(_: Int? = nil) + case unread(_: Int? = nil) + case today(_: Int? = nil) case folder(Folder, Bool) case webFeed(WebFeed) case articleIDs(Set) @@ -674,12 +674,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ switch fetchType { - case .starred: - return try fetchStarredArticles() - case .unread: - return try fetchUnreadArticles() - case .today: - return try fetchTodayArticles() + case .starred(let limit): + return try fetchStarredArticles(limit: limit) + case .unread(let limit): + return try fetchUnreadArticles(limit: limit) + case .today(let limit): + return try fetchTodayArticles(limit: limit) case .folder(let folder, let readFilter): if readFilter { return try fetchUnreadArticles(folder: folder) @@ -699,12 +699,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) { switch fetchType { - case .starred: - fetchStarredArticlesAsync(completion) - case .unread: - fetchUnreadArticlesAsync(completion) - case .today: - fetchTodayArticlesAsync(completion) + case .starred(let limit): + fetchStarredArticlesAsync(limit: limit, completion) + case .unread(let limit): + fetchUnreadArticlesAsync(limit: limit, completion) + case .today(let limit): + fetchTodayArticlesAsync(limit: limit, completion) case .folder(let folder, let readFilter): if readFilter { return fetchUnreadArticlesAsync(folder: folder, completion) @@ -1046,28 +1046,28 @@ extension Account: WebFeedMetadataDelegate { private extension Account { - func fetchStarredArticles() throws -> Set
{ - return try database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs()) + func fetchStarredArticles(limit: Int?) throws -> Set
{ + return try database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs(), limit) } - func fetchStarredArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - database.fetchedStarredArticlesAsync(flattenedWebFeeds().webFeedIDs(), completion) + func fetchStarredArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + database.fetchedStarredArticlesAsync(flattenedWebFeeds().webFeedIDs(), limit, completion) } - func fetchUnreadArticles() throws -> Set
{ - return try fetchUnreadArticles(forContainer: self) + func fetchUnreadArticles(limit: Int?) throws -> Set
{ + return try fetchUnreadArticles(forContainer: self, limit: limit) } - func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - fetchUnreadArticlesAsync(forContainer: self, completion) + func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion) } - func fetchTodayArticles() throws -> Set
{ - return try database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs()) + func fetchTodayArticles(limit: Int?) throws -> Set
{ + return try database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs(), limit) } - func fetchTodayArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { - database.fetchTodayArticlesAsync(flattenedWebFeeds().webFeedIDs(), completion) + func fetchTodayArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + database.fetchTodayArticlesAsync(flattenedWebFeeds().webFeedIDs(), limit, completion) } func fetchArticles(folder: Folder) throws -> Set
{ @@ -1079,11 +1079,11 @@ private extension Account { } func fetchUnreadArticles(folder: Folder) throws -> Set
{ - return try fetchUnreadArticles(forContainer: folder) + return try fetchUnreadArticles(forContainer: folder, limit: nil) } func fetchUnreadArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) { - fetchUnreadArticlesAsync(forContainer: folder, completion) + fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion) } func fetchArticles(webFeed: WebFeed) throws -> Set
{ @@ -1129,7 +1129,7 @@ private extension Account { } func fetchUnreadArticles(webFeed: WebFeed) throws -> Set
{ - let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID])) + let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID]), nil) validateUnreadCount(webFeed, articles) return articles } @@ -1154,19 +1154,31 @@ private extension Account { } } - func fetchUnreadArticles(forContainer container: Container) throws -> Set
{ + func fetchUnreadArticles(forContainer container: Container, limit: Int?) throws -> Set
{ let feeds = container.flattenedWebFeeds() - let articles = try database.fetchUnreadArticles(feeds.webFeedIDs()) - validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + let articles = try database.fetchUnreadArticles(feeds.webFeedIDs(), limit) + + // We don't validate limit queries because they, by definition, won't correctly match the + // complete unread state for the given container. + if limit == nil { + validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) + } + return articles } - func fetchUnreadArticlesAsync(forContainer container: Container, _ completion: @escaping ArticleSetResultBlock) { + func fetchUnreadArticlesAsync(forContainer container: Container, limit: Int?, _ completion: @escaping ArticleSetResultBlock) { let webFeeds = container.flattenedWebFeeds() - database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs()) { [weak self] (articleSetResult) in + database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs(), limit) { [weak self] (articleSetResult) in switch articleSetResult { case .success(let articles): - self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles) + + // We don't validate limit queries because they, by definition, won't correctly match the + // complete unread state for the given container. + if limit == nil { + self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles) + } + completion(.success(articles)) case .failure(let databaseError): completion(.failure(databaseError)) diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index c68f04679..5a7355201 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -102,16 +102,16 @@ public final class ArticlesDatabase { return try articlesTable.fetchArticles(articleIDs: articleIDs) } - public func fetchUnreadArticles(_ webFeedIDs: Set) throws -> Set
{ - return try articlesTable.fetchUnreadArticles(webFeedIDs) + public func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try articlesTable.fetchUnreadArticles(webFeedIDs, limit) } - public func fetchTodayArticles(_ webFeedIDs: Set) throws -> Set
{ - return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate()) + public func fetchTodayArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate(), limit) } - public func fetchStarredArticles(_ webFeedIDs: Set) throws -> Set
{ - return try articlesTable.fetchStarredArticles(webFeedIDs) + public func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try articlesTable.fetchStarredArticles(webFeedIDs, limit) } public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set) throws -> Set
{ @@ -136,16 +136,16 @@ public final class ArticlesDatabase { articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion) } - public func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - articlesTable.fetchUnreadArticlesAsync(webFeedIDs, completion) + public func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + articlesTable.fetchUnreadArticlesAsync(webFeedIDs, limit, completion) } - public func fetchTodayArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), completion) + public func fetchTodayArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), limit, completion) } - public func fetchedStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - articlesTable.fetchStarredArticlesAsync(webFeedIDs, completion) + public func fetchedStarredArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + articlesTable.fetchStarredArticlesAsync(webFeedIDs, limit, completion) } public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift index cdfdccf14..ab7655474 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -75,32 +75,32 @@ final class ArticlesTable: DatabaseTable { // MARK: - Fetching Unread Articles - func fetchUnreadArticles(_ webFeedIDs: Set) throws -> Set
{ - return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) } + func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) } } - func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, $0) }, completion) + func fetchUnreadArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, limit, $0) }, completion) } // MARK: - Fetching Today Articles - func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date) throws -> Set
{ - return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) } + func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ limit: Int?) throws -> Set
{ + return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) } } - func fetchArticlesSinceAsync(_ webFeedIDs: Set, _ cutoffDate: Date, _ completion: @escaping ArticleSetResultBlock) { - fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }, completion) + func fetchArticlesSinceAsync(_ webFeedIDs: Set, _ cutoffDate: Date, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }, completion) } // MARK: - Fetching Starred Articles - func fetchStarredArticles(_ webFeedIDs: Set) throws -> Set
{ - return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) } + func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?) throws -> Set
{ + return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, limit, $0) } } - func fetchStarredArticlesAsync(_ webFeedIDs: Set, _ completion: @escaping ArticleSetResultBlock) { - fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, $0) }, completion) + func fetchStarredArticlesAsync(_ webFeedIDs: Set, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) { + fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, limit, $0) }, completion) } // MARK: - Fetching Search Articles @@ -796,14 +796,17 @@ private extension ArticlesTable { return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } - func fetchUnreadArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + func fetchUnreadArticles(_ webFeedIDs: Set, _ limit: Int?, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 if webFeedIDs.isEmpty { return Set
() } let parameters = webFeedIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and read=0" + var whereClause = "feedID in \(placeholders) and read=0" + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } @@ -821,7 +824,7 @@ private extension ArticlesTable { return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } - func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ database: FMDatabase) -> Set
{ + func fetchArticlesSince(_ webFeedIDs: Set, _ cutoffDate: Date, _ limit: Int?, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?) // // datePublished may be nil, so we fall back to dateArrived. @@ -830,18 +833,24 @@ private extension ArticlesTable { } let parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject] let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))" + var whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?))" + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } - func fetchStarredArticles(_ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ + func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1; if webFeedIDs.isEmpty { return Set
() } let parameters = webFeedIDs.map { $0 as AnyObject } let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - let whereClause = "feedID in \(placeholders) and starred=1" + var whereClause = "feedID in \(placeholders) and starred=1" + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } diff --git a/Shared/Extensions/IconImage.swift b/Shared/Extensions/IconImage.swift index dd11e0933..e460e4b6b 100644 --- a/Shared/Extensions/IconImage.swift +++ b/Shared/Extensions/IconImage.swift @@ -63,47 +63,81 @@ final class IconImage { fileprivate enum ImageLuminanceType { case regular, bright, dark } + extension CGImage { func isBright() -> Bool { - guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { + guard let luminanceType = getLuminanceType() else { return false } return luminanceType == .bright } func isDark() -> Bool { - guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { + guard let luminanceType = getLuminanceType() else { return false } return luminanceType == .dark } - fileprivate func getLuminanceType(from data: CFData) -> ImageLuminanceType? { - guard let ptr = CFDataGetBytePtr(data) else { - return nil - } - - let length = CFDataGetLength(data) - var pixelCount = 0 + fileprivate func getLuminanceType() -> ImageLuminanceType? { + + // This has been rewritten with information from https://christianselig.com/2021/04/efficient-average-color/ + + // First, resize the image. We do this for two reasons, 1) less pixels to deal with means faster + // calculation and a resized image still has the "gist" of the colors, and 2) the image we're dealing + // with may come in any of a variety of color formats (CMYK, ARGB, RGBA, etc.) which complicates things, + // and redrawing it normalizes that into a base color format we can deal with. + // 40x40 is a good size to resize to still preserve quite a bit of detail but not have too many pixels + // to deal with. Aspect ratio is irrelevant for just finding average color. + let size = CGSize(width: 40, height: 40) + + let width = Int(size.width) + let height = Int(size.height) + let totalPixels = width * height + + let colorSpace = CGColorSpaceCreateDeviceRGB() + + // ARGB format + let bitmapInfo: UInt32 = CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue + + // 8 bits for each color channel, we're doing ARGB so 32 bits (4 bytes) total, and thus if the image is n pixels wide, + // and has 4 bytes per pixel, the total bytes per row is 4n. That gives us 2^8 = 256 color variations for each RGB channel + // or 256 * 256 * 256 = ~16.7M color options in total. That seems like a lot, but lots of HDR movies are in 10 bit, which + // is (2^10)^3 = 1 billion color options! + guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo) else { return nil } + + // Draw our resized image + context.draw(self, in: CGRect(origin: .zero, size: size)) + + guard let pixelBuffer = context.data else { return nil } + + // Bind the pixel buffer's memory location to a pointer we can use/access + let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height) + var totalLuminance = 0.0 - for i in stride(from: 0, to: length, by: 4) { - - let r = ptr[i] - let g = ptr[i + 1] - let b = ptr[i + 2] - let a = ptr[i + 3] - let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) - - if Double(a) > 0 { + // Column of pixels in image + for x in 0 ..< width { + // Row of pixels in image + for y in 0 ..< height { + // To get the pixel location just think of the image as a grid of pixels, but stored as one long row + // rather than columns and rows, so for instance to map the pixel from the grid in the 15th row and 3 + // columns in to our "long row", we'd offset ourselves 15 times the width in pixels of the image, and + // then offset by the amount of columns + let pixel = pointer[(y * width) + x] + + let r = red(for: pixel) + let g = green(for: pixel) + let b = blue(for: pixel) + + let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b)) + totalLuminance += luminance - pixelCount += 1 } - } - let avgLuminance = totalLuminance / Double(pixelCount) + let avgLuminance = totalLuminance / Double(totalPixels) if totalLuminance == 0 || avgLuminance < 40 { return .dark } else if avgLuminance > 180 { @@ -113,6 +147,18 @@ extension CGImage { } } + private func red(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 16) & 255) + } + + private func green(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 8) & 255) + } + + private func blue(for pixelData: UInt32) -> UInt8 { + return UInt8((pixelData >> 0) & 255) + } + } diff --git a/Shared/SmartFeeds/StarredFeedDelegate.swift b/Shared/SmartFeeds/StarredFeedDelegate.swift index 251e72cd5..55770fe36 100644 --- a/Shared/SmartFeeds/StarredFeedDelegate.swift +++ b/Shared/SmartFeeds/StarredFeedDelegate.swift @@ -21,7 +21,7 @@ struct StarredFeedDelegate: SmartFeedDelegate { } let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title") - let fetchType: FetchType = .starred + let fetchType: FetchType = .starred(nil) var smallIcon: IconImage? { return AppAssets.starredFeedImage } diff --git a/Shared/SmartFeeds/TodayFeedDelegate.swift b/Shared/SmartFeeds/TodayFeedDelegate.swift index 085a43777..ad6e47977 100644 --- a/Shared/SmartFeeds/TodayFeedDelegate.swift +++ b/Shared/SmartFeeds/TodayFeedDelegate.swift @@ -19,7 +19,7 @@ struct TodayFeedDelegate: SmartFeedDelegate { } let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title") - let fetchType = FetchType.today + let fetchType = FetchType.today(nil) var smallIcon: IconImage? { return AppAssets.todayFeedImage } diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index 4ef22635b..f8ce3660c 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -29,7 +29,7 @@ final class UnreadFeed: PseudoFeed { } let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title") - let fetchType = FetchType.unread + let fetchType = FetchType.unread(nil) var unreadCount = 0 { didSet { diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 2a38cdf48..86c46be3b 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -12,11 +12,13 @@ import os.log import UIKit import RSCore import Articles +import Account public final class WidgetDataEncoder { private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") + private let fetchLimit = 7 private var backgroundTaskID: UIBackgroundTaskIdentifier! private lazy var appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String @@ -31,11 +33,9 @@ public final class WidgetDataEncoder { os_log(.debug, log: log, "Starting encoding widget data.") do { - let unreadArticles = Array(try SmartFeedsController.shared.unreadFeed.fetchArticles()).sortedByDate(.orderedDescending) - - let starredArticles = Array(try SmartFeedsController.shared.starredFeed.fetchArticles()).sortedByDate(.orderedDescending) - - let todayArticles = Array(try SmartFeedsController.shared.todayFeed.fetchUnreadArticles()).sortedByDate(.orderedDescending) + let unreadArticles = Array(try AccountManager.shared.fetchArticles(.unread(fetchLimit))).sortedByDate(.orderedDescending) + let starredArticles = Array(try AccountManager.shared.fetchArticles(.starred(fetchLimit))).sortedByDate(.orderedDescending) + let todayArticles = Array(try AccountManager.shared.fetchArticles(.today(fetchLimit))).sortedByDate(.orderedDescending) var unread = [LatestArticle]() var today = [LatestArticle]() @@ -47,9 +47,8 @@ public final class WidgetDataEncoder { articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished!.description) + pubDate: article.datePublished?.description ?? "") unread.append(latestArticle) - if unread.count == 7 { break } } for article in starredArticles { @@ -58,9 +57,8 @@ public final class WidgetDataEncoder { articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished!.description) + pubDate: article.datePublished?.description ?? "") starred.append(latestArticle) - if starred.count == 7 { break } } for article in todayArticles { @@ -69,13 +67,12 @@ public final class WidgetDataEncoder { articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished!.description) + pubDate: article.datePublished?.description ?? "") today.append(latestArticle) - if today.count == 7 { break } } let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, - currentTodayCount: try! SmartFeedsController.shared.todayFeed.fetchUnreadArticles().count, + currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count, unreadArticles: unread, starredArticles: starred, diff --git a/iOS/Add/Reddit/RedditAdd.storyboard b/iOS/Add/Reddit/RedditAdd.storyboard index 1add6b8b2..738c51e87 100644 --- a/iOS/Add/Reddit/RedditAdd.storyboard +++ b/iOS/Add/Reddit/RedditAdd.storyboard @@ -1,9 +1,9 @@ - + - + @@ -15,7 +15,7 @@ - + @@ -58,7 +58,7 @@ diff --git a/iOS/Add/Twitter/TwitterAdd.storyboard b/iOS/Add/Twitter/TwitterAdd.storyboard index 87c3d69cd..f6b2db210 100644 --- a/iOS/Add/Twitter/TwitterAdd.storyboard +++ b/iOS/Add/Twitter/TwitterAdd.storyboard @@ -1,9 +1,9 @@ - + - + @@ -31,7 +31,7 @@ - + @@ -159,7 +159,7 @@ - + @@ -197,7 +197,7 @@ - + @@ -240,8 +240,8 @@ - - + + diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index d2fa7bad8..2a5ed0659 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -101,6 +101,14 @@ struct AppAssets { return UIImage(named: "disclosure")! }() + static var contextMenuReddit: UIImage = { + return UIImage(named: "contextMenuReddit")! + }() + + static var contextMenuTwitter: UIImage = { + return UIImage(named: "contextMenuTwitter")! + }() + static var copyImage: UIImage = { return UIImage(systemName: "doc.on.doc")! }() @@ -133,6 +141,10 @@ struct AppAssets { UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! }() + static var folderOutlinePlus: UIImage = { + UIImage(systemName: "folder.badge.plus")! + }() + static var fullScreenBackgroundColor: UIColor = { return UIColor(named: "fullScreenBackgroundColor")! }() @@ -173,6 +185,10 @@ struct AppAssets { return UIImage(systemName: "chevron.down.circle")! }() + static var plus: UIImage = { + UIImage(systemName: "plus")! + }() + static var prevArticleImage: UIImage = { return UIImage(systemName: "chevron.up")! }() diff --git a/iOS/Article/OpenInSafariActivity.swift b/iOS/Article/OpenInSafariActivity.swift index 9b5b5ed8f..2c9ae9eab 100644 --- a/iOS/Article/OpenInSafariActivity.swift +++ b/iOS/Article/OpenInSafariActivity.swift @@ -1,5 +1,5 @@ // -// OpenInSafariActivity.swift +// OpenInBrowserActivity.swift // NetNewsWire-iOS // // Created by Maurice Parker on 1/9/20. @@ -8,16 +8,16 @@ import UIKit -class OpenInSafariActivity: UIActivity { +class OpenInBrowserActivity: UIActivity { private var activityItems: [Any]? override var activityTitle: String? { - return NSLocalizedString("Open in Safari", comment: "Open in Safari") + return NSLocalizedString("Open in Browser", comment: "Open in Browser") } override var activityImage: UIImage? { - return UIImage(systemName: "safari", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) + return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) } override var activityType: UIActivity.ActivityType? { diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index e00898eb5..74c8404c6 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -239,7 +239,7 @@ class WebViewController: UIViewController { return } - let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInSafariActivity()]) + let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()]) activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem present(activityViewController, animated: true) } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 7a89c6297..82dc391e2 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -18,7 +18,7 @@ -