Merge branch 'ios-release'
This commit is contained in:
commit
d0680bcc73
@ -695,56 +695,49 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
completion(nil)
|
completion(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let group = DispatchGroup()
|
|
||||||
var possibleError: DatabaseError? = nil
|
|
||||||
var newArticles = Set<Article>()
|
|
||||||
var updatedArticles = Set<Article>()
|
|
||||||
|
|
||||||
for (webFeedID, items) in webFeedIDsAndItems {
|
|
||||||
|
|
||||||
group.enter()
|
|
||||||
database.update(webFeedID: webFeedID, items: items, defaultRead: defaultRead) { updateArticlesResult in
|
|
||||||
|
|
||||||
switch updateArticlesResult {
|
database.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: defaultRead) { updateArticlesResult in
|
||||||
case .success(let newAndUpdatedArticles):
|
|
||||||
if let articles = newAndUpdatedArticles.newArticles {
|
func sendNotificationAbout(newArticles: Set<Article>?, updatedArticles: Set<Article>?) {
|
||||||
newArticles.formUnion(articles)
|
var webFeeds = Set<WebFeed>()
|
||||||
}
|
|
||||||
if let articles = newAndUpdatedArticles.updatedArticles {
|
if let newArticles = newArticles {
|
||||||
updatedArticles.formUnion(articles)
|
webFeeds.formUnion(Set(newArticles.compactMap { $0.webFeed }))
|
||||||
}
|
|
||||||
case .failure(let databaseError):
|
|
||||||
possibleError = databaseError
|
|
||||||
}
|
}
|
||||||
|
if let updatedArticles = updatedArticles {
|
||||||
group.leave()
|
webFeeds.formUnion(Set(updatedArticles.compactMap { $0.webFeed }))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
var shouldSendNotification = false
|
||||||
|
var userInfo = [String: Any]()
|
||||||
group.notify(queue: DispatchQueue.main) {
|
|
||||||
var userInfo = [String: Any]()
|
if let newArticles = newArticles, !newArticles.isEmpty {
|
||||||
var webFeeds = Set(newArticles.compactMap { $0.webFeed })
|
shouldSendNotification = true
|
||||||
webFeeds.formUnion(Set(updatedArticles.compactMap { $0.webFeed }))
|
userInfo[UserInfoKey.newArticles] = newArticles
|
||||||
|
self.updateUnreadCounts(for: webFeeds) {
|
||||||
if !newArticles.isEmpty {
|
NotificationCenter.default.post(name: .DownloadArticlesDidUpdateUnreadCounts, object: self, userInfo: nil)
|
||||||
self.updateUnreadCounts(for: webFeeds) {
|
}
|
||||||
NotificationCenter.default.post(name: .DownloadArticlesDidUpdateUnreadCounts, object: self, userInfo: nil)
|
}
|
||||||
|
|
||||||
|
if let updatedArticles = updatedArticles, !updatedArticles.isEmpty {
|
||||||
|
shouldSendNotification = true
|
||||||
|
userInfo[UserInfoKey.updatedArticles] = updatedArticles
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldSendNotification {
|
||||||
|
userInfo[UserInfoKey.webFeeds] = webFeeds
|
||||||
|
NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo)
|
||||||
}
|
}
|
||||||
userInfo[UserInfoKey.newArticles] = newArticles
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updatedArticles.isEmpty {
|
switch updateArticlesResult {
|
||||||
userInfo[UserInfoKey.updatedArticles] = updatedArticles
|
case .success(let newAndUpdatedArticles):
|
||||||
|
sendNotificationAbout(newArticles: newAndUpdatedArticles.newArticles, updatedArticles: newAndUpdatedArticles.updatedArticles)
|
||||||
|
completion(nil)
|
||||||
|
case .failure(let databaseError):
|
||||||
|
completion(databaseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfo[UserInfoKey.webFeeds] = webFeeds
|
|
||||||
NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo)
|
|
||||||
|
|
||||||
completion(possibleError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
|
@ -184,8 +184,8 @@ public final class ArticlesDatabase {
|
|||||||
// MARK: - Saving and Updating Articles
|
// MARK: - Saving and Updating Articles
|
||||||
|
|
||||||
/// Update articles and save new ones.
|
/// Update articles and save new ones.
|
||||||
public func update(webFeedID: String, items: Set<ParsedItem>, defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
public func update(webFeedIDsAndItems: [String: Set<ParsedItem>], defaultRead: Bool, completion: @escaping UpdateArticlesCompletionBlock) {
|
||||||
articlesTable.update(webFeedID, items, defaultRead, completion)
|
articlesTable.update(webFeedIDsAndItems, defaultRead, completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Status
|
// MARK: - Status
|
||||||
|
@ -169,8 +169,8 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
|
|
||||||
// MARK: - Updating
|
// MARK: - Updating
|
||||||
|
|
||||||
func update(_ webFeedID: String, _ items: Set<ParsedItem>, _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
func update(_ webFeedIDsAndItems: [String: Set<ParsedItem>], _ read: Bool, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||||
if items.isEmpty {
|
if webFeedIDsAndItems.isEmpty {
|
||||||
callUpdateArticlesCompletionBlock(nil, nil, completion)
|
callUpdateArticlesCompletionBlock(nil, nil, completion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -187,11 +187,15 @@ final class ArticlesTable: DatabaseTable {
|
|||||||
self.queue.runInTransaction { (databaseResult) in
|
self.queue.runInTransaction { (databaseResult) in
|
||||||
|
|
||||||
func makeDatabaseCalls(_ database: FMDatabase) {
|
func makeDatabaseCalls(_ database: FMDatabase) {
|
||||||
let articleIDs = items.articleIDs()
|
var articleIDs = Set<String>()
|
||||||
|
for (_, parsedItems) in webFeedIDsAndItems {
|
||||||
|
articleIDs.formUnion(parsedItems.articleIDs())
|
||||||
|
}
|
||||||
|
|
||||||
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
|
let statusesDictionary = self.statusesTable.ensureStatusesForArticleIDs(articleIDs, read, database) //1
|
||||||
assert(statusesDictionary.count == articleIDs.count)
|
assert(statusesDictionary.count == articleIDs.count)
|
||||||
|
|
||||||
let allIncomingArticles = Article.articlesWithWebFeedIDsAndItems(webFeedID, items, self.accountID, statusesDictionary) //2
|
let allIncomingArticles = Article.articlesWithWebFeedIDsAndItems(webFeedIDsAndItems, self.accountID, statusesDictionary) //2
|
||||||
if allIncomingArticles.isEmpty {
|
if allIncomingArticles.isEmpty {
|
||||||
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
|
self.callUpdateArticlesCompletionBlock(nil, nil, completion)
|
||||||
return
|
return
|
||||||
|
@ -112,9 +112,16 @@ extension Article {
|
|||||||
// return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) })
|
// return Set(parsedItems.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, feedID: feedID, status: statusesDictionary[$0.articleID]!) })
|
||||||
// }
|
// }
|
||||||
|
|
||||||
static func articlesWithWebFeedIDsAndItems(_ webFeedID: String, _ items: Set<ParsedItem>, _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
|
static func articlesWithWebFeedIDsAndItems(_ webFeedIDsAndItems: [String: Set<ParsedItem>], _ accountID: String, _ statusesDictionary: [String: ArticleStatus]) -> Set<Article> {
|
||||||
let maximumDateAllowed = Date().addingTimeInterval(60 * 60 * 24) // Allow dates up to about 24 hours ahead of now
|
let maximumDateAllowed = Date().addingTimeInterval(60 * 60 * 24) // Allow dates up to about 24 hours ahead of now
|
||||||
let feedArticles = Set(items.map{ Article(parsedItem: $0, maximumDateAllowed: maximumDateAllowed, accountID: accountID, webFeedID: webFeedID, status: statusesDictionary[$0.articleID]!) })
|
var feedArticles = Set<Article>()
|
||||||
|
for (webFeedID, parsedItems) in webFeedIDsAndItems {
|
||||||
|
for parsedItem in parsedItems {
|
||||||
|
let status = statusesDictionary[parsedItem.articleID]!
|
||||||
|
let article = Article(parsedItem: parsedItem, maximumDateAllowed: maximumDateAllowed, accountID: accountID, webFeedID: webFeedID, status: status)
|
||||||
|
feedArticles.insert(article)
|
||||||
|
}
|
||||||
|
}
|
||||||
return feedArticles
|
return feedArticles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import RSCore
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import AppKit
|
import AppKit
|
||||||
#else
|
#else
|
||||||
@ -18,13 +19,8 @@ extension RSImage {
|
|||||||
|
|
||||||
static let maxIconSize = 48
|
static let maxIconSize = 48
|
||||||
|
|
||||||
static func scaledForIcon(_ data: Data, imageResultBlock: @escaping (RSImage?) -> Void) {
|
static func scaledForIcon(_ data: Data, imageResultBlock: @escaping ImageResultBlock) {
|
||||||
DispatchQueue.global(qos: .default).async {
|
IconScalerQueue.shared.scaledForIcon(data, imageResultBlock)
|
||||||
let image = RSImage.scaledForIcon(data)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
imageResultBlock(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func scaledForIcon(_ data: Data) -> RSImage? {
|
static func scaledForIcon(_ data: Data) -> RSImage? {
|
||||||
@ -41,3 +37,26 @@ extension RSImage {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - IconScalerQueue
|
||||||
|
|
||||||
|
private class IconScalerQueue {
|
||||||
|
|
||||||
|
static let shared = IconScalerQueue()
|
||||||
|
|
||||||
|
private let queue: DispatchQueue = {
|
||||||
|
let q = DispatchQueue(label: "IconScaler", attributes: .initiallyInactive)
|
||||||
|
q.setTarget(queue: DispatchQueue.global(qos: .default))
|
||||||
|
q.activate()
|
||||||
|
return q
|
||||||
|
}()
|
||||||
|
|
||||||
|
func scaledForIcon(_ data: Data, _ imageResultBlock: @escaping ImageResultBlock) {
|
||||||
|
queue.async {
|
||||||
|
let image = RSImage.scaledForIcon(data)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
imageResultBlock(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -178,11 +178,6 @@ final class FaviconDownloader {
|
|||||||
|
|
||||||
remainingFaviconURLs[homePageURL] = nil
|
remainingFaviconURLs[homePageURL] = nil
|
||||||
|
|
||||||
if self.homePageToFaviconURLCache[homePageURL] == nil {
|
|
||||||
self.homePageToFaviconURLCache[homePageURL] = singleFaviconDownloader.faviconURL
|
|
||||||
self.homePageToFaviconURLCacheDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL)
|
postFaviconDidBecomeAvailableNotification(singleFaviconDownloader.faviconURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,8 +227,22 @@ private extension FaviconDownloader {
|
|||||||
|
|
||||||
func faviconDownloader(withURL faviconURL: String, homePageURL: String?) -> SingleFaviconDownloader {
|
func faviconDownloader(withURL faviconURL: String, homePageURL: String?) -> SingleFaviconDownloader {
|
||||||
|
|
||||||
|
var firstTimeSeeingHomepageURL = false
|
||||||
|
|
||||||
|
if let homePageURL = homePageURL, self.homePageToFaviconURLCache[homePageURL] == nil {
|
||||||
|
self.homePageToFaviconURLCache[homePageURL] = faviconURL
|
||||||
|
self.homePageToFaviconURLCacheDirty = true
|
||||||
|
firstTimeSeeingHomepageURL = true
|
||||||
|
}
|
||||||
|
|
||||||
if let downloader = singleFaviconDownloaderCache[faviconURL] {
|
if let downloader = singleFaviconDownloaderCache[faviconURL] {
|
||||||
downloader.downloadFaviconIfNeeded()
|
if firstTimeSeeingHomepageURL && !downloader.downloadFaviconIfNeeded() {
|
||||||
|
// This is to handle the scenario where we have different homepages, but the same favicon.
|
||||||
|
// This happens for Twitter and probably other sites like Blogger. Because the favicon
|
||||||
|
// is cached, we wouldn't send out a notification that it is now available unless we send
|
||||||
|
// it here.
|
||||||
|
postFaviconDidBecomeAvailableNotification(faviconURL)
|
||||||
|
}
|
||||||
return downloader
|
return downloader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,21 +48,23 @@ final class SingleFaviconDownloader {
|
|||||||
findFavicon()
|
findFavicon()
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadFaviconIfNeeded() {
|
func downloadFaviconIfNeeded() -> Bool {
|
||||||
|
|
||||||
// If we don’t have an image, and lastDownloadAttemptDate is a while ago, try again.
|
// If we don’t have an image, and lastDownloadAttemptDate is a while ago, try again.
|
||||||
|
|
||||||
if let _ = iconImage {
|
if let _ = iconImage {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let retryInterval: TimeInterval = 30 * 60 // 30 minutes
|
let retryInterval: TimeInterval = 30 * 60 // 30 minutes
|
||||||
if Date().timeIntervalSince(lastDownloadAttemptDate) < retryInterval {
|
if Date().timeIntervalSince(lastDownloadAttemptDate) < retryInterval {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDownloadAttemptDate = Date()
|
lastDownloadAttemptDate = Date()
|
||||||
findFavicon()
|
findFavicon()
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,13 +139,19 @@ private extension KeyboardManager {
|
|||||||
keys.append(KeyboardManager.createKeyCommand(title: goToStarredTitle, action: "goToStarred:", input: "3", modifiers: [.command]))
|
keys.append(KeyboardManager.createKeyCommand(title: goToStarredTitle, action: "goToStarred:", input: "3", modifiers: [.command]))
|
||||||
|
|
||||||
let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search")
|
let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search")
|
||||||
keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .shift]))
|
keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .alternate]))
|
||||||
|
|
||||||
let markAllAsReadTitle = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
|
let markAllAsReadTitle = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
|
||||||
keys.append(KeyboardManager.createKeyCommand(title: markAllAsReadTitle, action: "markAllAsRead:", input: "k", modifiers: [.command]))
|
keys.append(KeyboardManager.createKeyCommand(title: markAllAsReadTitle, action: "markAllAsRead:", input: "k", modifiers: [.command]))
|
||||||
|
|
||||||
let cleanUp = NSLocalizedString("Clean Up", comment: "Clean Up")
|
let cleanUp = NSLocalizedString("Clean Up", comment: "Clean Up")
|
||||||
keys.append(KeyboardManager.createKeyCommand(title: cleanUp, action: "cleanUp:", input: "h", modifiers: [.command, .shift]))
|
keys.append(KeyboardManager.createKeyCommand(title: cleanUp, action: "cleanUp:", input: "'", modifiers: [.command]))
|
||||||
|
|
||||||
|
let toggleReadFeedsFilter = NSLocalizedString("Toggle Read Feeds Filter", comment: "Toggle Read Feeds Filter")
|
||||||
|
keys.append(KeyboardManager.createKeyCommand(title: toggleReadFeedsFilter, action: "toggleReadFeedsFilter:", input: "f", modifiers: [.command, .shift]))
|
||||||
|
|
||||||
|
let toggleReadArticlesFilter = NSLocalizedString("Toggle Read Articles Filter", comment: "Toggle Read Articles Filter")
|
||||||
|
keys.append(KeyboardManager.createKeyCommand(title: toggleReadArticlesFilter, action: "toggleReadArticlesFilter:", input: "h", modifiers: [.command, .shift]))
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
@ -378,13 +378,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func toggleFilter(_ sender: Any) {
|
@IBAction func toggleFilter(_ sender: Any) {
|
||||||
if coordinator.isReadFeedsFiltered {
|
coordinator.toggleReadFeedsFilter()
|
||||||
setFilterButtonToInactive()
|
|
||||||
coordinator.showAllFeeds()
|
|
||||||
} else {
|
|
||||||
setFilterButtonToActive()
|
|
||||||
coordinator.hideReadFeeds()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func add(_ sender: UIBarButtonItem) {
|
@IBAction func add(_ sender: UIBarButtonItem) {
|
||||||
@ -509,6 +503,16 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateUI() {
|
||||||
|
if coordinator.isReadFeedsFiltered {
|
||||||
|
setFilterButtonToActive()
|
||||||
|
} else {
|
||||||
|
setFilterButtonToInactive()
|
||||||
|
}
|
||||||
|
refreshProgressView?.updateRefreshLabel()
|
||||||
|
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
func focus() {
|
func focus() {
|
||||||
becomeFirstResponder()
|
becomeFirstResponder()
|
||||||
}
|
}
|
||||||
@ -580,16 +584,6 @@ private extension MasterFeedViewController {
|
|||||||
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
toolbarItems?.insert(refreshProgressItemButton, at: 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI() {
|
|
||||||
if coordinator.isReadFeedsFiltered {
|
|
||||||
setFilterButtonToActive()
|
|
||||||
} else {
|
|
||||||
setFilterButtonToInactive()
|
|
||||||
}
|
|
||||||
refreshProgressView?.updateRefreshLabel()
|
|
||||||
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
func setFilterButtonToActive() {
|
func setFilterButtonToActive() {
|
||||||
filterButton?.image = AppAssets.filterActiveImage
|
filterButton?.image = AppAssets.filterActiveImage
|
||||||
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
|
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
|
||||||
|
@ -19,7 +19,6 @@ struct MasterTimelineAccessibilityCellLayout: MasterTimelineCellLayout {
|
|||||||
let summaryRect: CGRect
|
let summaryRect: CGRect
|
||||||
let feedNameRect: CGRect
|
let feedNameRect: CGRect
|
||||||
let dateRect: CGRect
|
let dateRect: CGRect
|
||||||
let separatorInsets: UIEdgeInsets
|
|
||||||
|
|
||||||
init(width: CGFloat, insets: UIEdgeInsets, cellData: MasterTimelineCellData) {
|
init(width: CGFloat, insets: UIEdgeInsets, cellData: MasterTimelineCellData) {
|
||||||
|
|
||||||
@ -34,9 +33,6 @@ struct MasterTimelineAccessibilityCellLayout: MasterTimelineCellLayout {
|
|||||||
// Start the point at the beginning position of the main block
|
// Start the point at the beginning position of the main block
|
||||||
currentPoint.x += MasterTimelineDefaultCellLayout.unreadCircleDimension + MasterTimelineDefaultCellLayout.unreadCircleMarginRight
|
currentPoint.x += MasterTimelineDefaultCellLayout.unreadCircleDimension + MasterTimelineDefaultCellLayout.unreadCircleMarginRight
|
||||||
|
|
||||||
// Separator Insets
|
|
||||||
self.separatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
||||||
|
|
||||||
// Icon Image
|
// Icon Image
|
||||||
if cellData.showIcon {
|
if cellData.showIcon {
|
||||||
self.iconImageRect = MasterTimelineAccessibilityCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
|
self.iconImageRect = MasterTimelineAccessibilityCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
|
||||||
|
@ -18,7 +18,6 @@ protocol MasterTimelineCellLayout {
|
|||||||
var summaryRect: CGRect {get}
|
var summaryRect: CGRect {get}
|
||||||
var feedNameRect: CGRect {get}
|
var feedNameRect: CGRect {get}
|
||||||
var dateRect: CGRect {get}
|
var dateRect: CGRect {get}
|
||||||
var separatorInsets: UIEdgeInsets {get}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,6 @@ struct MasterTimelineDefaultCellLayout: MasterTimelineCellLayout {
|
|||||||
let summaryRect: CGRect
|
let summaryRect: CGRect
|
||||||
let feedNameRect: CGRect
|
let feedNameRect: CGRect
|
||||||
let dateRect: CGRect
|
let dateRect: CGRect
|
||||||
let separatorInsets: UIEdgeInsets
|
|
||||||
|
|
||||||
init(width: CGFloat, insets: UIEdgeInsets, cellData: MasterTimelineCellData) {
|
init(width: CGFloat, insets: UIEdgeInsets, cellData: MasterTimelineCellData) {
|
||||||
|
|
||||||
@ -66,9 +65,6 @@ struct MasterTimelineDefaultCellLayout: MasterTimelineCellLayout {
|
|||||||
// Start the point at the beginning position of the main block
|
// Start the point at the beginning position of the main block
|
||||||
currentPoint.x += MasterTimelineDefaultCellLayout.unreadCircleDimension + MasterTimelineDefaultCellLayout.unreadCircleMarginRight
|
currentPoint.x += MasterTimelineDefaultCellLayout.unreadCircleDimension + MasterTimelineDefaultCellLayout.unreadCircleMarginRight
|
||||||
|
|
||||||
// Separator Insets
|
|
||||||
self.separatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
||||||
|
|
||||||
// Icon Image
|
// Icon Image
|
||||||
if cellData.showIcon {
|
if cellData.showIcon {
|
||||||
self.iconImageRect = MasterTimelineDefaultCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
|
self.iconImageRect = MasterTimelineDefaultCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
|
||||||
|
@ -79,7 +79,7 @@ class MasterTimelineTableViewCell: VibrantTableViewCell {
|
|||||||
feedNameView.setFrameIfNotEqual(layout.feedNameRect)
|
feedNameView.setFrameIfNotEqual(layout.feedNameRect)
|
||||||
dateView.setFrameIfNotEqual(layout.dateRect)
|
dateView.setFrameIfNotEqual(layout.dateRect)
|
||||||
|
|
||||||
separatorInset = layout.separatorInsets
|
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setIconImage(_ image: IconImage) {
|
func setIconImage(_ image: IconImage) {
|
||||||
|
@ -70,7 +70,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
tableView.dataSource = dataSource
|
tableView.dataSource = dataSource
|
||||||
numberOfTextLines = AppDefaults.timelineNumberOfLines
|
numberOfTextLines = AppDefaults.timelineNumberOfLines
|
||||||
iconSize = AppDefaults.timelineIconSize
|
iconSize = AppDefaults.timelineIconSize
|
||||||
tableView.rowHeight = calculateEstimatedRowHeight(forId: PrototypeFeedContent.feedId, withTitle: PrototypeFeedContent.longTitle, andFeed: PrototypeFeedContent.feedname)
|
resetEstimatedRowHeight()
|
||||||
|
|
||||||
if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView {
|
if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView {
|
||||||
navigationItem.titleView = titleView
|
navigationItem.titleView = titleView
|
||||||
@ -111,13 +111,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
|
|
||||||
// MARK: Actions
|
// MARK: Actions
|
||||||
@IBAction func toggleFilter(_ sender: Any) {
|
@IBAction func toggleFilter(_ sender: Any) {
|
||||||
if coordinator.isReadArticlesFiltered {
|
coordinator.toggleReadArticlesFilter()
|
||||||
setFilterButtonToInactive()
|
|
||||||
coordinator.showAllArticles()
|
|
||||||
} else {
|
|
||||||
setFilterButtonToActive()
|
|
||||||
coordinator.hideReadArticles()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func markAllAsRead(_ sender: Any) {
|
@IBAction func markAllAsRead(_ sender: Any) {
|
||||||
@ -443,7 +437,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
if numberOfTextLines != AppDefaults.timelineNumberOfLines || iconSize != AppDefaults.timelineIconSize {
|
if numberOfTextLines != AppDefaults.timelineNumberOfLines || iconSize != AppDefaults.timelineIconSize {
|
||||||
numberOfTextLines = AppDefaults.timelineNumberOfLines
|
numberOfTextLines = AppDefaults.timelineNumberOfLines
|
||||||
iconSize = AppDefaults.timelineIconSize
|
iconSize = AppDefaults.timelineIconSize
|
||||||
tableView.rowHeight = calculateEstimatedRowHeight(forId: PrototypeFeedContent.feedId, withTitle: PrototypeFeedContent.longTitle, andFeed: PrototypeFeedContent.feedname)
|
resetEstimatedRowHeight()
|
||||||
reloadAllVisibleCells()
|
reloadAllVisibleCells()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -487,21 +481,26 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
|
|
||||||
// MARK: Cell Configuring
|
// MARK: Cell Configuring
|
||||||
|
|
||||||
private func calculateEstimatedRowHeight(forId prototypeID: String, withTitle title: String, andFeed feedName: String) -> CGFloat {
|
private func resetEstimatedRowHeight() {
|
||||||
|
|
||||||
|
let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
||||||
|
|
||||||
|
let prototypeID = "prototype"
|
||||||
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, userDeleted: false, dateArrived: Date())
|
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, userDeleted: false, dateArrived: Date())
|
||||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: title, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, webFeedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
|
||||||
|
|
||||||
let prototypeCellData = MasterTimelineCellData(article: prototypeArticle, showFeedName: true, feedName: feedName, iconImage: nil, showIcon: false, featuredImage: nil, numberOfLines: numberOfTextLines, iconSize: iconSize)
|
let prototypeCellData = MasterTimelineCellData(article: prototypeArticle, showFeedName: true, feedName: "Prototype Feed Name", iconImage: nil, showIcon: false, featuredImage: nil, numberOfLines: numberOfTextLines, iconSize: iconSize)
|
||||||
|
|
||||||
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
|
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
|
||||||
let layout = MasterTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
let layout = MasterTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
||||||
return layout.height
|
tableView.estimatedRowHeight = layout.height
|
||||||
} else {
|
} else {
|
||||||
let layout = MasterTimelineDefaultCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
let layout = MasterTimelineDefaultCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
||||||
return layout.height
|
tableView.estimatedRowHeight = layout.height
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Searching
|
// MARK: Searching
|
||||||
@ -584,11 +583,13 @@ private extension MasterTimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if coordinator.isReadArticlesFiltered {
|
if coordinator.isReadArticlesFiltered {
|
||||||
setFilterButtonToActive()
|
filterButton?.image = AppAssets.filterActiveImage
|
||||||
|
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles")
|
||||||
} else {
|
} else {
|
||||||
setFilterButtonToInactive()
|
filterButton?.image = AppAssets.filterInactiveImage
|
||||||
|
filterButton?.accLabelText = NSLocalizedString("Filter Read Articles", comment: "Filter Read Articles")
|
||||||
}
|
}
|
||||||
|
|
||||||
tableView.selectRow(at: nil, animated: false, scrollPosition: .top)
|
tableView.selectRow(at: nil, animated: false, scrollPosition: .top)
|
||||||
if resetScroll && dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 {
|
if resetScroll && dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 {
|
||||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
||||||
@ -598,16 +599,6 @@ private extension MasterTimelineViewController {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setFilterButtonToActive() {
|
|
||||||
filterButton?.image = AppAssets.filterActiveImage
|
|
||||||
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setFilterButtonToInactive() {
|
|
||||||
filterButton?.image = AppAssets.filterInactiveImage
|
|
||||||
filterButton?.accLabelText = NSLocalizedString("Filter Read Articles", comment: "Filter Read Articles")
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateToolbar() {
|
func updateToolbar() {
|
||||||
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||||
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||||
@ -625,12 +616,16 @@ private extension MasterTimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
|
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
|
||||||
|
if coordinator.articles.count == 0 {
|
||||||
|
tableView.rowHeight = tableView.estimatedRowHeight
|
||||||
|
} else {
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
}
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Int, Article>()
|
var snapshot = NSDiffableDataSourceSnapshot<Int, Article>()
|
||||||
snapshot.appendSections([0])
|
snapshot.appendSections([0])
|
||||||
snapshot.appendItems(coordinator.articles, toSection: 0)
|
snapshot.appendItems(coordinator.articles, toSection: 0)
|
||||||
if coordinator.articles.count == 0 {
|
|
||||||
tableView.rowHeight = calculateEstimatedRowHeight(forId: PrototypeFeedContent.feedId, withTitle: PrototypeFeedContent.longTitle, andFeed: PrototypeFeedContent.feedname)
|
|
||||||
}
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in
|
dataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in
|
||||||
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
self?.restoreSelectionIfNecessary(adjustScroll: false)
|
||||||
completion?()
|
completion?()
|
||||||
@ -901,10 +896,3 @@ private extension MasterTimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct PrototypeFeedContent {
|
|
||||||
static let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
|
||||||
static let shortTitle = "prototype"
|
|
||||||
static let feedId = "feedId"
|
|
||||||
static let feedname = "prototype"
|
|
||||||
}
|
|
||||||
|
@ -169,6 +169,10 @@ code, pre {
|
|||||||
border: 1px solid var(--secondary-accent-color);
|
border: 1px solid var(--secondary-accent-color);
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
.nnw-overflow table table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
.nnw-overflow td, .nnw-overflow th {
|
.nnw-overflow td, .nnw-overflow th {
|
||||||
-webkit-hyphens: none;
|
-webkit-hyphens: none;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
|
@ -94,6 +94,14 @@ class RootSplitViewController: UISplitViewController {
|
|||||||
coordinator.cleanUp()
|
coordinator.cleanUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func toggleReadFeedsFilter(_ sender: Any?) {
|
||||||
|
coordinator.toggleReadFeedsFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func toggleReadArticlesFilter(_ sender: Any?) {
|
||||||
|
coordinator.toggleReadArticlesFilter()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func refresh(_ sender: Any?) {
|
@objc func refresh(_ sender: Any?) {
|
||||||
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
|
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
|
||||||
}
|
}
|
||||||
|
@ -541,7 +541,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func downloadArticlesDidUpdateUnreadCounts(_ note: Notification) {
|
@objc func downloadArticlesDidUpdateUnreadCounts(_ note: Notification) {
|
||||||
rebuildBackingStores()
|
rebuildBackingStoresWithMerge()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||||
@ -581,6 +581,30 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toggleReadFeedsFilter() {
|
||||||
|
if isReadFeedsFiltered {
|
||||||
|
treeControllerDelegate.isReadFiltered = false
|
||||||
|
} else {
|
||||||
|
treeControllerDelegate.isReadFiltered = true
|
||||||
|
}
|
||||||
|
rebuildBackingStores()
|
||||||
|
masterFeedViewController?.updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleReadArticlesFilter() {
|
||||||
|
guard let feedID = timelineFeed?.feedID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isReadArticlesFiltered {
|
||||||
|
readFilterEnabledTable[feedID] = false
|
||||||
|
} else {
|
||||||
|
readFilterEnabledTable[feedID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTimeline(resetScroll: false)
|
||||||
|
}
|
||||||
|
|
||||||
func shadowNodesFor(section: Int) -> [Node] {
|
func shadowNodesFor(section: Int) -> [Node] {
|
||||||
return shadowTable[section]
|
return shadowTable[section]
|
||||||
}
|
}
|
||||||
@ -617,30 +641,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAllFeeds() {
|
|
||||||
treeControllerDelegate.isReadFiltered = false
|
|
||||||
rebuildBackingStores()
|
|
||||||
}
|
|
||||||
|
|
||||||
func hideReadFeeds() {
|
|
||||||
treeControllerDelegate.isReadFiltered = true
|
|
||||||
rebuildBackingStores()
|
|
||||||
}
|
|
||||||
|
|
||||||
func showAllArticles() {
|
|
||||||
if let feedID = timelineFeed?.feedID {
|
|
||||||
readFilterEnabledTable[feedID] = false
|
|
||||||
}
|
|
||||||
refreshTimeline(resetScroll: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hideReadArticles() {
|
|
||||||
if let feedID = timelineFeed?.feedID {
|
|
||||||
readFilterEnabledTable[feedID] = true
|
|
||||||
}
|
|
||||||
refreshTimeline(resetScroll: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isExpanded(_ containerIdentifiable: ContainerIdentifiable) -> Bool {
|
func isExpanded(_ containerIdentifiable: ContainerIdentifiable) -> Bool {
|
||||||
if let containerID = containerIdentifiable.containerID {
|
if let containerID = containerIdentifiable.containerID {
|
||||||
return expandedTable.contains(containerID)
|
return expandedTable.contains(containerID)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user