Merge branch 'ios-candidate' into main

This commit is contained in:
Maurice Parker 2021-04-03 11:10:04 -05:00
commit cc6449ed2a
20 changed files with 268 additions and 178 deletions

View File

@ -53,9 +53,9 @@ public enum AccountType: Int, Codable {
} }
public enum FetchType { public enum FetchType {
case starred case starred(_: Int? = nil)
case unread case unread(_: Int? = nil)
case today case today(_: Int? = nil)
case folder(Folder, Bool) case folder(Folder, Bool)
case webFeed(WebFeed) case webFeed(WebFeed)
case articleIDs(Set<String>) case articleIDs(Set<String>)
@ -674,12 +674,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func fetchArticles(_ fetchType: FetchType) throws -> Set<Article> { public func fetchArticles(_ fetchType: FetchType) throws -> Set<Article> {
switch fetchType { switch fetchType {
case .starred: case .starred(let limit):
return try fetchStarredArticles() return try fetchStarredArticles(limit: limit)
case .unread: case .unread(let limit):
return try fetchUnreadArticles() return try fetchUnreadArticles(limit: limit)
case .today: case .today(let limit):
return try fetchTodayArticles() return try fetchTodayArticles(limit: limit)
case .folder(let folder, let readFilter): case .folder(let folder, let readFilter):
if readFilter { if readFilter {
return try fetchUnreadArticles(folder: folder) return try fetchUnreadArticles(folder: folder)
@ -699,12 +699,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) { public func fetchArticlesAsync(_ fetchType: FetchType, _ completion: @escaping ArticleSetResultBlock) {
switch fetchType { switch fetchType {
case .starred: case .starred(let limit):
fetchStarredArticlesAsync(completion) fetchStarredArticlesAsync(limit: limit, completion)
case .unread: case .unread(let limit):
fetchUnreadArticlesAsync(completion) fetchUnreadArticlesAsync(limit: limit, completion)
case .today: case .today(let limit):
fetchTodayArticlesAsync(completion) fetchTodayArticlesAsync(limit: limit, completion)
case .folder(let folder, let readFilter): case .folder(let folder, let readFilter):
if readFilter { if readFilter {
return fetchUnreadArticlesAsync(folder: folder, completion) return fetchUnreadArticlesAsync(folder: folder, completion)
@ -1046,28 +1046,28 @@ extension Account: WebFeedMetadataDelegate {
private extension Account { private extension Account {
func fetchStarredArticles() throws -> Set<Article> { func fetchStarredArticles(limit: Int?) throws -> Set<Article> {
return try database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs()) return try database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs(), limit)
} }
func fetchStarredArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { func fetchStarredArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
database.fetchedStarredArticlesAsync(flattenedWebFeeds().webFeedIDs(), completion) database.fetchedStarredArticlesAsync(flattenedWebFeeds().webFeedIDs(), limit, completion)
} }
func fetchUnreadArticles() throws -> Set<Article> { func fetchUnreadArticles(limit: Int?) throws -> Set<Article> {
return try fetchUnreadArticles(forContainer: self) return try fetchUnreadArticles(forContainer: self, limit: limit)
} }
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { func fetchUnreadArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
fetchUnreadArticlesAsync(forContainer: self, completion) fetchUnreadArticlesAsync(forContainer: self, limit: limit, completion)
} }
func fetchTodayArticles() throws -> Set<Article> { func fetchTodayArticles(limit: Int?) throws -> Set<Article> {
return try database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs()) return try database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs(), limit)
} }
func fetchTodayArticlesAsync(_ completion: @escaping ArticleSetResultBlock) { func fetchTodayArticlesAsync(limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
database.fetchTodayArticlesAsync(flattenedWebFeeds().webFeedIDs(), completion) database.fetchTodayArticlesAsync(flattenedWebFeeds().webFeedIDs(), limit, completion)
} }
func fetchArticles(folder: Folder) throws -> Set<Article> { func fetchArticles(folder: Folder) throws -> Set<Article> {
@ -1079,11 +1079,11 @@ private extension Account {
} }
func fetchUnreadArticles(folder: Folder) throws -> Set<Article> { func fetchUnreadArticles(folder: Folder) throws -> Set<Article> {
return try fetchUnreadArticles(forContainer: folder) return try fetchUnreadArticles(forContainer: folder, limit: nil)
} }
func fetchUnreadArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) { func fetchUnreadArticlesAsync(folder: Folder, _ completion: @escaping ArticleSetResultBlock) {
fetchUnreadArticlesAsync(forContainer: folder, completion) fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion)
} }
func fetchArticles(webFeed: WebFeed) throws -> Set<Article> { func fetchArticles(webFeed: WebFeed) throws -> Set<Article> {
@ -1129,7 +1129,7 @@ private extension Account {
} }
func fetchUnreadArticles(webFeed: WebFeed) throws -> Set<Article> { func fetchUnreadArticles(webFeed: WebFeed) throws -> Set<Article> {
let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID])) let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID]), nil)
validateUnreadCount(webFeed, articles) validateUnreadCount(webFeed, articles)
return articles return articles
} }
@ -1154,19 +1154,31 @@ private extension Account {
} }
} }
func fetchUnreadArticles(forContainer container: Container) throws -> Set<Article> { func fetchUnreadArticles(forContainer container: Container, limit: Int?) throws -> Set<Article> {
let feeds = container.flattenedWebFeeds() let feeds = container.flattenedWebFeeds()
let articles = try database.fetchUnreadArticles(feeds.webFeedIDs()) 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) validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
}
return 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() let webFeeds = container.flattenedWebFeeds()
database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs()) { [weak self] (articleSetResult) in database.fetchUnreadArticlesAsync(webFeeds.webFeedIDs(), limit) { [weak self] (articleSetResult) in
switch articleSetResult { switch articleSetResult {
case .success(let articles): case .success(let 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) self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles)
}
completion(.success(articles)) completion(.success(articles))
case .failure(let databaseError): case .failure(let databaseError):
completion(.failure(databaseError)) completion(.failure(databaseError))

View File

@ -102,16 +102,16 @@ public final class ArticlesDatabase {
return try articlesTable.fetchArticles(articleIDs: articleIDs) return try articlesTable.fetchArticles(articleIDs: articleIDs)
} }
public func fetchUnreadArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> { public func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try articlesTable.fetchUnreadArticles(webFeedIDs) return try articlesTable.fetchUnreadArticles(webFeedIDs, limit)
} }
public func fetchTodayArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> { public func fetchTodayArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate()) return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate(), limit)
} }
public func fetchStarredArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> { public func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try articlesTable.fetchStarredArticles(webFeedIDs) return try articlesTable.fetchStarredArticles(webFeedIDs, limit)
} }
public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> { public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> {
@ -136,16 +136,16 @@ public final class ArticlesDatabase {
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion) articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion)
} }
public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchUnreadArticlesAsync(webFeedIDs, completion) articlesTable.fetchUnreadArticlesAsync(webFeedIDs, limit, completion)
} }
public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), completion) articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), limit, completion)
} }
public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchStarredArticlesAsync(webFeedIDs, completion) articlesTable.fetchStarredArticlesAsync(webFeedIDs, limit, completion)
} }
public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { public func fetchArticlesMatchingAsync(_ searchString: String, _ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {

View File

@ -75,32 +75,32 @@ final class ArticlesTable: DatabaseTable {
// MARK: - Fetching Unread Articles // MARK: - Fetching Unread Articles
func fetchUnreadArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> { func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) } return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) }
} }
func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, $0) }, completion) fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, limit, $0) }, completion)
} }
// MARK: - Fetching Today Articles // MARK: - Fetching Today Articles
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date) throws -> Set<Article> { func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?) throws -> Set<Article> {
return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) } return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }
} }
func fetchArticlesSinceAsync(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ completion: @escaping ArticleSetResultBlock) { func fetchArticlesSinceAsync(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }, completion) fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }, completion)
} }
// MARK: - Fetching Starred Articles // MARK: - Fetching Starred Articles
func fetchStarredArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> { func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) } return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, limit, $0) }
} }
func fetchStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) { func fetchStarredArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, $0) }, completion) fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, limit, $0) }, completion)
} }
// MARK: - Fetching Search Articles // MARK: - Fetching Search Articles
@ -796,14 +796,17 @@ private extension ArticlesTable {
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
} }
func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> { func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0 // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and read=0
if webFeedIDs.isEmpty { if webFeedIDs.isEmpty {
return Set<Article>() return Set<Article>()
} }
let parameters = webFeedIDs.map { $0 as AnyObject } let parameters = webFeedIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! 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) return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
} }
@ -821,7 +824,7 @@ private extension ArticlesTable {
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
} }
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ database: FMDatabase) -> Set<Article> { func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?) // 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. // 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 parameters = webFeedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject]
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! 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) return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
} }
func fetchStarredArticles(_ webFeedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> { func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?, _ database: FMDatabase) -> Set<Article> {
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1; // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1;
if webFeedIDs.isEmpty { if webFeedIDs.isEmpty {
return Set<Article>() return Set<Article>()
} }
let parameters = webFeedIDs.map { $0 as AnyObject } let parameters = webFeedIDs.map { $0 as AnyObject }
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! 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) return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters)
} }

View File

@ -63,47 +63,81 @@ final class IconImage {
fileprivate enum ImageLuminanceType { fileprivate enum ImageLuminanceType {
case regular, bright, dark case regular, bright, dark
} }
extension CGImage { extension CGImage {
func isBright() -> Bool { func isBright() -> Bool {
guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { guard let luminanceType = getLuminanceType() else {
return false return false
} }
return luminanceType == .bright return luminanceType == .bright
} }
func isDark() -> Bool { func isDark() -> Bool {
guard let imageData = self.dataProvider?.data, let luminanceType = getLuminanceType(from: imageData) else { guard let luminanceType = getLuminanceType() else {
return false return false
} }
return luminanceType == .dark return luminanceType == .dark
} }
fileprivate func getLuminanceType(from data: CFData) -> ImageLuminanceType? { fileprivate func getLuminanceType() -> ImageLuminanceType? {
guard let ptr = CFDataGetBytePtr(data) else {
return nil // 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)
let length = CFDataGetLength(data)
var pixelCount = 0
var totalLuminance = 0.0 var totalLuminance = 0.0
for i in stride(from: 0, to: length, by: 4) { // 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 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)) let luminance = (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b))
if Double(a) > 0 {
totalLuminance += luminance totalLuminance += luminance
pixelCount += 1 }
} }
} let avgLuminance = totalLuminance / Double(totalPixels)
let avgLuminance = totalLuminance / Double(pixelCount)
if totalLuminance == 0 || avgLuminance < 40 { if totalLuminance == 0 || avgLuminance < 40 {
return .dark return .dark
} else if avgLuminance > 180 { } 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)
}
} }

View File

@ -21,7 +21,7 @@ struct StarredFeedDelegate: SmartFeedDelegate {
} }
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title") let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")
let fetchType: FetchType = .starred let fetchType: FetchType = .starred(nil)
var smallIcon: IconImage? { var smallIcon: IconImage? {
return AppAssets.starredFeedImage return AppAssets.starredFeedImage
} }

View File

@ -19,7 +19,7 @@ struct TodayFeedDelegate: SmartFeedDelegate {
} }
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title") let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")
let fetchType = FetchType.today let fetchType = FetchType.today(nil)
var smallIcon: IconImage? { var smallIcon: IconImage? {
return AppAssets.todayFeedImage return AppAssets.todayFeedImage
} }

View File

@ -29,7 +29,7 @@ final class UnreadFeed: PseudoFeed {
} }
let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title") let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title")
let fetchType = FetchType.unread let fetchType = FetchType.unread(nil)
var unreadCount = 0 { var unreadCount = 0 {
didSet { didSet {

View File

@ -12,11 +12,13 @@ import os.log
import UIKit import UIKit
import RSCore import RSCore
import Articles import Articles
import Account
public final class WidgetDataEncoder { public final class WidgetDataEncoder {
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
private let fetchLimit = 7
private var backgroundTaskID: UIBackgroundTaskIdentifier! private var backgroundTaskID: UIBackgroundTaskIdentifier!
private lazy var appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String 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.") os_log(.debug, log: log, "Starting encoding widget data.")
do { do {
let unreadArticles = Array(try SmartFeedsController.shared.unreadFeed.fetchArticles()).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 starredArticles = Array(try SmartFeedsController.shared.starredFeed.fetchArticles()).sortedByDate(.orderedDescending) let todayArticles = Array(try AccountManager.shared.fetchArticles(.today(fetchLimit))).sortedByDate(.orderedDescending)
let todayArticles = Array(try SmartFeedsController.shared.todayFeed.fetchUnreadArticles()).sortedByDate(.orderedDescending)
var unread = [LatestArticle]() var unread = [LatestArticle]()
var today = [LatestArticle]() var today = [LatestArticle]()
@ -47,9 +47,8 @@ public final class WidgetDataEncoder {
articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
articleSummary: article.summary, articleSummary: article.summary,
feedIcon: article.iconImage()?.image.dataRepresentation(), feedIcon: article.iconImage()?.image.dataRepresentation(),
pubDate: article.datePublished!.description) pubDate: article.datePublished?.description ?? "")
unread.append(latestArticle) unread.append(latestArticle)
if unread.count == 7 { break }
} }
for article in starredArticles { for article in starredArticles {
@ -58,9 +57,8 @@ public final class WidgetDataEncoder {
articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
articleSummary: article.summary, articleSummary: article.summary,
feedIcon: article.iconImage()?.image.dataRepresentation(), feedIcon: article.iconImage()?.image.dataRepresentation(),
pubDate: article.datePublished!.description) pubDate: article.datePublished?.description ?? "")
starred.append(latestArticle) starred.append(latestArticle)
if starred.count == 7 { break }
} }
for article in todayArticles { for article in todayArticles {
@ -69,13 +67,12 @@ public final class WidgetDataEncoder {
articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article),
articleSummary: article.summary, articleSummary: article.summary,
feedIcon: article.iconImage()?.image.dataRepresentation(), feedIcon: article.iconImage()?.image.dataRepresentation(),
pubDate: article.datePublished!.description) pubDate: article.datePublished?.description ?? "")
today.append(latestArticle) today.append(latestArticle)
if today.count == 7 { break }
} }
let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, 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, currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count,
unreadArticles: unread, unreadArticles: unread,
starredArticles: starred, starredArticles: starred,

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17147" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="fak-6k-FqE"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="fak-6k-FqE">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17120"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -15,7 +15,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="SFq-R0-gSo"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="SFq-R0-gSo">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections> <sections>
<tableViewSection id="Dp6-La-NeL"> <tableViewSection id="Dp6-La-NeL">
<cells> <cells>
@ -58,7 +58,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="The best posts on Reddit for you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="uaZ-4Q-FBS"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="The best posts on Reddit for you" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="uaZ-4Q-FBS">
<rect key="frame" x="20" y="32.5" width="198" height="16"/> <rect key="frame" x="20" y="32.5" width="197.5" height="16"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/> <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/> <nil key="textColor"/>
@ -143,7 +143,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="T93-wO-GIE"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="T93-wO-GIE">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="j8c-JM-nzm" style="IBUITableViewCellStyleDefault" id="vEE-Gx-Zgc" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="j8c-JM-nzm" style="IBUITableViewCellStyleDefault" id="vEE-Gx-Zgc" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="55.5" width="374" height="43.5"/> <rect key="frame" x="20" y="55.5" width="374" height="43.5"/>
@ -181,7 +181,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="76O-el-2DO"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="76O-el-2DO">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections> <sections>
<tableViewSection id="ZkR-cP-Kvy"> <tableViewSection id="ZkR-cP-Kvy">
<cells> <cells>
@ -229,7 +229,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fZZ-h8-KOR"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="fZZ-h8-KOR">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections> <sections>
<tableViewSection id="ExH-4T-drs"> <tableViewSection id="ExH-4T-drs">
<cells> <cells>
@ -272,7 +272,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="The most upvotes recently" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="l2u-CB-A9e"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="The most upvotes recently" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="l2u-CB-A9e">
<rect key="frame" x="20" y="32.5" width="161.5" height="16"/> <rect key="frame" x="20" y="32.5" width="161" height="16"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/> <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/> <nil key="textColor"/>
@ -344,7 +344,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Posts getting the most current activity" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="6AI-Dt-0ix"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Posts getting the most current activity" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="6AI-Dt-0ix">
<rect key="frame" x="20" y="32.5" width="232.5" height="16"/> <rect key="frame" x="20" y="32.5" width="232" height="16"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/> <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/> <nil key="textColor"/>
@ -385,8 +385,8 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<systemColor name="systemBackgroundColor"> <systemColor name="systemGroupedBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
</resources> </resources>
</document> </document>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17147" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="4Q4-Hi-Lic"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="4Q4-Hi-Lic">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17120"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -31,7 +31,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="SFq-R0-gSo"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="SFq-R0-gSo">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections> <sections>
<tableViewSection id="Dp6-La-NeL"> <tableViewSection id="Dp6-La-NeL">
<cells> <cells>
@ -159,7 +159,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="T93-wO-GIE"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="T93-wO-GIE">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="j8c-JM-nzm" style="IBUITableViewCellStyleDefault" id="vEE-Gx-Zgc" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="j8c-JM-nzm" style="IBUITableViewCellStyleDefault" id="vEE-Gx-Zgc" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="55.5" width="374" height="43.5"/> <rect key="frame" x="20" y="55.5" width="374" height="43.5"/>
@ -197,7 +197,7 @@
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="76O-el-2DO"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="76O-el-2DO">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections> <sections>
<tableViewSection id="ZkR-cP-Kvy"> <tableViewSection id="ZkR-cP-Kvy">
<cells> <cells>
@ -240,8 +240,8 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<systemColor name="systemBackgroundColor"> <systemColor name="systemGroupedBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor> </systemColor>
</resources> </resources>
</document> </document>

View File

@ -101,6 +101,14 @@ struct AppAssets {
return UIImage(named: "disclosure")! return UIImage(named: "disclosure")!
}() }()
static var contextMenuReddit: UIImage = {
return UIImage(named: "contextMenuReddit")!
}()
static var contextMenuTwitter: UIImage = {
return UIImage(named: "contextMenuTwitter")!
}()
static var copyImage: UIImage = { static var copyImage: UIImage = {
return UIImage(systemName: "doc.on.doc")! return UIImage(systemName: "doc.on.doc")!
}() }()
@ -133,6 +141,10 @@ struct AppAssets {
UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! UIImage(systemName: "line.horizontal.3.decrease.circle.fill")!
}() }()
static var folderOutlinePlus: UIImage = {
UIImage(systemName: "folder.badge.plus")!
}()
static var fullScreenBackgroundColor: UIColor = { static var fullScreenBackgroundColor: UIColor = {
return UIColor(named: "fullScreenBackgroundColor")! return UIColor(named: "fullScreenBackgroundColor")!
}() }()
@ -173,6 +185,10 @@ struct AppAssets {
return UIImage(systemName: "chevron.down.circle")! return UIImage(systemName: "chevron.down.circle")!
}() }()
static var plus: UIImage = {
UIImage(systemName: "plus")!
}()
static var prevArticleImage: UIImage = { static var prevArticleImage: UIImage = {
return UIImage(systemName: "chevron.up")! return UIImage(systemName: "chevron.up")!
}() }()

View File

@ -1,5 +1,5 @@
// //
// OpenInSafariActivity.swift // OpenInBrowserActivity.swift
// NetNewsWire-iOS // NetNewsWire-iOS
// //
// Created by Maurice Parker on 1/9/20. // Created by Maurice Parker on 1/9/20.
@ -8,16 +8,16 @@
import UIKit import UIKit
class OpenInSafariActivity: UIActivity { class OpenInBrowserActivity: UIActivity {
private var activityItems: [Any]? private var activityItems: [Any]?
override var activityTitle: String? { 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? { 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? { override var activityType: UIActivity.ActivityType? {

View File

@ -239,7 +239,7 @@ class WebViewController: UIViewController {
return 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 activityViewController.popoverPresentationController?.barButtonItem = popOverBarButtonItem
present(activityViewController, animated: true) present(activityViewController, animated: true)
} }

View File

@ -18,7 +18,7 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view hidden="YES" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="h1Q-FS-jlg" customClass="ArticleSearchBar" customModule="NetNewsWire" customModuleProvider="target"> <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="h1Q-FS-jlg" customClass="ArticleSearchBar" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="777" width="414" height="36"/> <rect key="frame" x="0.0" y="777" width="414" height="36"/>
<color key="backgroundColor" name="barBackgroundColor"/> <color key="backgroundColor" name="barBackgroundColor"/>
</view> </view>
@ -138,6 +138,9 @@
<userDefinedRuntimeAttributes> <userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Mark All as Read"/> <userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Mark All as Read"/>
</userDefinedRuntimeAttributes> </userDefinedRuntimeAttributes>
<connections>
<action selector="markAllAsRead:" destination="Kyk-vK-QRX" id="EVp-xb-0lW"/>
</connections>
</barButtonItem> </barButtonItem>
<barButtonItem style="plain" systemItem="flexibleSpace" id="53V-wq-bat"/> <barButtonItem style="plain" systemItem="flexibleSpace" id="53V-wq-bat"/>
<barButtonItem style="plain" systemItem="flexibleSpace" id="93y-8j-WBh"/> <barButtonItem style="plain" systemItem="flexibleSpace" id="93y-8j-WBh"/>

View File

@ -589,40 +589,50 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
@objc @objc
func configureContextMenu(_: Any? = nil) { func configureContextMenu(_: Any? = nil) {
if #available(iOS 14.0, *) { if #available(iOS 14.0, *) {
/*
Context Menu Order:
1. Add Web Feed
2. Add Reddit Feed
3. Add Twitter Feed
4. Add Folder
*/
var menuItems: [UIAction] = []
let addWebFeedActionTitle = NSLocalizedString("Add Web Feed", comment: "Add Web Feed") let addWebFeedActionTitle = NSLocalizedString("Add Web Feed", comment: "Add Web Feed")
let addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.faviconTemplateImage.withRenderingMode(.alwaysOriginal).withTintColor(.secondaryLabel)) { _ in let addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.plus) { _ in
self.coordinator.showAddWebFeed() self.coordinator.showAddWebFeed()
} }
menuItems.append(addWebFeedAction)
let addRedditFeedActionTitle = NSLocalizedString("Add Reddit Feed", comment: "Add Reddit Feed")
let addRedditFeedAction = UIAction(title: addRedditFeedActionTitle, image: AppAssets.redditOriginal) { _ in
self.coordinator.showAddRedditFeed()
}
let addTwitterFeedActionTitle = NSLocalizedString("Add Twitter Feed", comment: "Add Twitter Feed")
let addTwitterFeedAction = UIAction(title: addTwitterFeedActionTitle, image: AppAssets.twitterOriginal) { _ in
self.coordinator.showAddTwitterFeed()
}
let addWebFolderdActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
let addWebFolderAction = UIAction(title: addWebFolderdActionTitle, image: AppAssets.masterFolderImageNonIcon) { _ in
self.coordinator.showAddFolder()
}
var children = [addWebFolderAction, addWebFeedAction]
if AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit }) { if AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit }) {
if ExtensionPointManager.shared.isRedditEnabled { if ExtensionPointManager.shared.isRedditEnabled {
children.insert(addRedditFeedAction, at: 0) let addRedditFeedActionTitle = NSLocalizedString("Add Reddit Feed", comment: "Add Reddit Feed")
let addRedditFeedAction = UIAction(title: addRedditFeedActionTitle, image: AppAssets.contextMenuReddit.tinted(color: .label)) { _ in
self.coordinator.showAddRedditFeed()
}
menuItems.append(addRedditFeedAction)
} }
if ExtensionPointManager.shared.isTwitterEnabled { if ExtensionPointManager.shared.isTwitterEnabled {
children.insert(addTwitterFeedAction, at: 0) let addTwitterFeedActionTitle = NSLocalizedString("Add Twitter Feed", comment: "Add Twitter Feed")
let addTwitterFeedAction = UIAction(title: addTwitterFeedActionTitle, image: AppAssets.contextMenuTwitter.tinted(color: .label)) { _ in
self.coordinator.showAddTwitterFeed()
}
menuItems.append(addTwitterFeedAction)
} }
} }
let menu = UIMenu(title: "Add Item", image: nil, identifier: nil, options: [], children: children)
self.addNewItemButton.menu = menu let addWebFolderActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
let addWebFolderAction = UIAction(title: addWebFolderActionTitle, image: AppAssets.folderOutlinePlus) { _ in
self.coordinator.showAddFolder()
}
menuItems.append(addWebFolderAction)
let contextMenu = UIMenu(title: NSLocalizedString("Add Item", comment: "Add Item"), image: nil, identifier: nil, options: [], children: menuItems.reversed())
self.addNewItemButton.menu = contextMenu
} }
} }
@ -1225,7 +1235,7 @@ private extension MasterFeedViewController {
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
if let articles = try? account.fetchArticles(.unread) { if let articles = try? account.fetchArticles(.unread()) {
self?.coordinator.markAllAsRead(Array(articles)) self?.coordinator.markAllAsRead(Array(articles))
} }
} }

View File

@ -19,15 +19,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
private var refreshProgressView: RefreshProgressView? private var refreshProgressView: RefreshProgressView?
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem! { @IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
didSet {
if #available(iOS 14, *) {
markAllAsReadButton.primaryAction = nil
} else {
markAllAsReadButton.action = #selector(MasterTimelineViewController.markAllAsRead(_:))
}
}
}
private var filterButton: UIBarButtonItem! private var filterButton: UIBarButtonItem!
private var firstUnreadButton: UIBarButtonItem! private var firstUnreadButton: UIBarButtonItem!
@ -666,25 +658,6 @@ private extension MasterTimelineViewController {
setToolbarItems(items, animated: false) setToolbarItems(items, animated: false)
} }
} }
if #available(iOS 14, *) {
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
var markAsReadAction: UIAction!
if AppDefaults.shared.confirmMarkAllAsRead {
markAsReadAction = UIAction(title: title, image: AppAssets.markAllAsReadImage, discoverabilityTitle: "in \(self.title!)") { [weak self] action in
self?.coordinator.markAllAsReadInTimeline()
}
let settingsAction = UIAction(title: NSLocalizedString("Settings", comment: "Settings"), image: UIImage(systemName: "gear")!, discoverabilityTitle: NSLocalizedString("You can turn this confirmation off in Settings.", comment: "You can turn this confirmation off in Settings.")) { [weak self] action in
self?.coordinator.showSettings(scrollToArticlesSection: true)
}
markAllAsReadButton.menu = UIMenu(title: NSLocalizedString(title, comment: title), image: nil, identifier: nil, children: [settingsAction, markAsReadAction])
markAllAsReadButton.action = nil
} else {
markAllAsReadButton.action = #selector(MasterTimelineViewController.markAllAsRead(_:))
}
}
} }
func updateTitleUnreadCount() { func updateTitleUnreadCount() {

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "redditContextMenu.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "twitterContextMenu.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}