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 {
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<String>)
@ -674,12 +674,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public func fetchArticles(_ fetchType: FetchType) throws -> Set<Article> {
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<Article> {
return try database.fetchStarredArticles(flattenedWebFeeds().webFeedIDs())
func fetchStarredArticles(limit: Int?) throws -> Set<Article> {
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<Article> {
return try fetchUnreadArticles(forContainer: self)
func fetchUnreadArticles(limit: Int?) throws -> Set<Article> {
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<Article> {
return try database.fetchTodayArticles(flattenedWebFeeds().webFeedIDs())
func fetchTodayArticles(limit: Int?) throws -> Set<Article> {
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<Article> {
@ -1079,11 +1079,11 @@ private extension Account {
}
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) {
fetchUnreadArticlesAsync(forContainer: folder, completion)
fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion)
}
func fetchArticles(webFeed: WebFeed) throws -> Set<Article> {
@ -1129,7 +1129,7 @@ private extension Account {
}
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)
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 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))

View File

@ -102,16 +102,16 @@ public final class ArticlesDatabase {
return try articlesTable.fetchArticles(articleIDs: articleIDs)
}
public func fetchUnreadArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchUnreadArticles(webFeedIDs)
public func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try articlesTable.fetchUnreadArticles(webFeedIDs, limit)
}
public func fetchTodayArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate())
public func fetchTodayArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try articlesTable.fetchArticlesSince(webFeedIDs, todayCutoffDate(), limit)
}
public func fetchStarredArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try articlesTable.fetchStarredArticles(webFeedIDs)
public func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try articlesTable.fetchStarredArticles(webFeedIDs, limit)
}
public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set<String>) throws -> Set<Article> {
@ -136,16 +136,16 @@ public final class ArticlesDatabase {
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, completion)
}
public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchUnreadArticlesAsync(webFeedIDs, completion)
public func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchUnreadArticlesAsync(webFeedIDs, limit, completion)
}
public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), completion)
public func fetchTodayArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchArticlesSinceAsync(webFeedIDs, todayCutoffDate(), limit, completion)
}
public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchStarredArticlesAsync(webFeedIDs, completion)
public func fetchedStarredArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
articlesTable.fetchStarredArticlesAsync(webFeedIDs, limit, completion)
}
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
func fetchUnreadArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, $0) }
func fetchUnreadArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try fetchArticles{ self.fetchUnreadArticles(webFeedIDs, limit, $0) }
}
func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, $0) }, completion)
func fetchUnreadArticlesAsync(_ webFeedIDs: Set<String>, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchUnreadArticles(webFeedIDs, limit, $0) }, completion)
}
// MARK: - Fetching Today Articles
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date) throws -> Set<Article> {
return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }
func fetchArticlesSince(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?) throws -> Set<Article> {
return try fetchArticles{ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }
}
func fetchArticlesSinceAsync(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, $0) }, completion)
func fetchArticlesSinceAsync(_ webFeedIDs: Set<String>, _ cutoffDate: Date, _ limit: Int?, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchArticlesSince(webFeedIDs, cutoffDate, limit, $0) }, completion)
}
// MARK: - Fetching Starred Articles
func fetchStarredArticles(_ webFeedIDs: Set<String>) throws -> Set<Article> {
return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, $0) }
func fetchStarredArticles(_ webFeedIDs: Set<String>, _ limit: Int?) throws -> Set<Article> {
return try fetchArticles{ self.fetchStarredArticles(webFeedIDs, limit, $0) }
}
func fetchStarredArticlesAsync(_ webFeedIDs: Set<String>, _ completion: @escaping ArticleSetResultBlock) {
fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, $0) }, completion)
func fetchStarredArticlesAsync(_ webFeedIDs: Set<String>, _ 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<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
if webFeedIDs.isEmpty {
return Set<Article>()
}
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<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 > ?)
//
// 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<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;
if webFeedIDs.isEmpty {
return Set<Article>()
}
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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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,

View File

@ -1,9 +1,9 @@
<?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"/>
<dependencies>
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</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">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections>
<tableViewSection id="Dp6-La-NeL">
<cells>
@ -58,7 +58,7 @@
<nil key="highlightedColor"/>
</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">
<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"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<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">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<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">
<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">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections>
<tableViewSection id="ZkR-cP-Kvy">
<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">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections>
<tableViewSection id="ExH-4T-drs">
<cells>
@ -272,7 +272,7 @@
<nil key="highlightedColor"/>
</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">
<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"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
@ -344,7 +344,7 @@
<nil key="highlightedColor"/>
</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">
<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"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
@ -385,8 +385,8 @@
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<systemColor name="systemGroupedBackgroundColor">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@ -1,9 +1,9 @@
<?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"/>
<dependencies>
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</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">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections>
<tableViewSection id="Dp6-La-NeL">
<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">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<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">
<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">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
<sections>
<tableViewSection id="ZkR-cP-Kvy">
<cells>
@ -240,8 +240,8 @@
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<systemColor name="systemGroupedBackgroundColor">
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@ -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")!
}()

View File

@ -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? {

View File

@ -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)
}

View File

@ -18,7 +18,7 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<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"/>
<color key="backgroundColor" name="barBackgroundColor"/>
</view>
@ -138,6 +138,9 @@
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="accLabelText" value="Mark All as Read"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="markAllAsRead:" destination="Kyk-vK-QRX" id="EVp-xb-0lW"/>
</connections>
</barButtonItem>
<barButtonItem style="plain" systemItem="flexibleSpace" id="53V-wq-bat"/>
<barButtonItem style="plain" systemItem="flexibleSpace" id="93y-8j-WBh"/>

View File

@ -589,40 +589,50 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
@objc
func configureContextMenu(_: Any? = nil) {
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 addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.faviconTemplateImage.withRenderingMode(.alwaysOriginal).withTintColor(.secondaryLabel)) { _ in
let addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.plus) { _ in
self.coordinator.showAddWebFeed()
}
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]
menuItems.append(addWebFeedAction)
if AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit }) {
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 {
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)
let addWebFolderActionTitle = NSLocalizedString("Add Folder", comment: "Add Folder")
let addWebFolderAction = UIAction(title: addWebFolderActionTitle, image: AppAssets.folderOutlinePlus) { _ in
self.coordinator.showAddFolder()
}
self.addNewItemButton.menu = menu
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 action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action 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))
}
}

View File

@ -19,15 +19,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
private var refreshProgressView: RefreshProgressView?
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem! {
didSet {
if #available(iOS 14, *) {
markAllAsReadButton.primaryAction = nil
} else {
markAllAsReadButton.action = #selector(MasterTimelineViewController.markAllAsRead(_:))
}
}
}
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
private var filterButton: UIBarButtonItem!
private var firstUnreadButton: UIBarButtonItem!
@ -666,25 +658,6 @@ private extension MasterTimelineViewController {
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() {

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
}
}