Merge branch 'ios-candidate' into main
This commit is contained in:
commit
cc6449ed2a
@ -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))
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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")!
|
||||
}()
|
||||
|
@ -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? {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"/>
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
12
iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "redditContextMenu.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/redditContextMenu.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/contextMenuReddit.imageset/redditContextMenu.pdf
vendored
Normal file
Binary file not shown.
12
iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/Contents.json
vendored
Normal file
12
iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "twitterContextMenu.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/twitterContextMenu.pdf
vendored
Normal file
BIN
iOS/Resources/Assets.xcassets/contextMenuTwitter.imageset/twitterContextMenu.pdf
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user