mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'main' of https://github.com/Ranchero-Software/NetNewsWire
This commit is contained in:
@@ -34,7 +34,7 @@ final class FeedlyLogoutOperation: FeedlyOperation {
|
||||
assert(Thread.isMainThread)
|
||||
switch result {
|
||||
case .success:
|
||||
os_log("Logged out of %{public}@ account.", "\(account.type)")
|
||||
os_log("Logged out of %{public}@ account.", log: log, "\(account.type)")
|
||||
do {
|
||||
try account.removeCredentials(type: .oauthAccessToken)
|
||||
try account.removeCredentials(type: .oauthRefreshToken)
|
||||
@@ -44,7 +44,7 @@ final class FeedlyLogoutOperation: FeedlyOperation {
|
||||
didFinish()
|
||||
|
||||
case .failure(let error):
|
||||
os_log("Logout failed because %{public}@.", error as NSError)
|
||||
os_log("Logout failed because %{public}@.", log: log, error as NSError)
|
||||
didFinish(with: error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
static let mainWindow = "mainWindow"
|
||||
}
|
||||
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||
|
||||
var userNotificationManager: UserNotificationManager!
|
||||
var faviconDownloader: FaviconDownloader!
|
||||
var imageDownloader: ImageDownloader!
|
||||
@@ -199,7 +201,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
AppDefaults.shared.registerDefaults()
|
||||
let isFirstRun = AppDefaults.shared.isFirstRun
|
||||
if isFirstRun {
|
||||
os_log(.debug, "Is first run.")
|
||||
os_log(.debug, log: log, "Is first run.")
|
||||
}
|
||||
let localAccount = AccountManager.shared.defaultAccount
|
||||
|
||||
@@ -1010,12 +1012,12 @@ private extension AppDelegate {
|
||||
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
os_log(.debug, log: log, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
os_log(.debug, log: log, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
|
||||
@@ -1029,12 +1031,12 @@ private extension AppDelegate {
|
||||
}
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
os_log(.debug, log: log, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
os_log(.debug, log: log, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .starred, flag: true) { _ in }
|
||||
|
||||
@@ -940,7 +940,7 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
return [action]
|
||||
|
||||
@unknown default:
|
||||
os_log(.error, "Unknown table row edge: %ld", edge.rawValue)
|
||||
print("Unknown table row edge: \(edge.rawValue)")
|
||||
}
|
||||
|
||||
return []
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
"repositoryURL": "https://github.com/microsoft/plcrashreporter.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "d747ab5de269cd44022bbe96ff9609d8626694ab",
|
||||
"version": "1.9.0"
|
||||
"revision": "6b27393cad517c067dceea85fadf050e70c4ceaa",
|
||||
"version": "1.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -105,8 +105,8 @@
|
||||
"repositoryURL": "https://github.com/Ranchero-Software/Sparkle-Binary.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "67cd26321bdf4e77954cf6de7d9e6a20544f2030",
|
||||
"version": "2.0.0"
|
||||
"revision": "d1a8b3c98d96c601453f2e4230f1dd65b60d0581",
|
||||
"version": "2.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -58,6 +58,10 @@ struct AppAssets {
|
||||
static var appBadgeImage: UIImage = {
|
||||
return UIImage(systemName: "app.badge")!
|
||||
}()
|
||||
|
||||
static var articleAppearanceImage: UIImage = {
|
||||
return UIImage(systemName: "textformat.size")!
|
||||
}()
|
||||
|
||||
static var articleExtractorError: UIImage = {
|
||||
return UIImage(named: "articleExtractorError")!
|
||||
|
||||
@@ -408,11 +408,11 @@ private extension AppDelegate {
|
||||
|
||||
// set expiration handler
|
||||
task.expirationHandler = { [weak task] in
|
||||
DispatchQueue.main.sync {
|
||||
self.suspendApplication()
|
||||
}
|
||||
os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info)
|
||||
task?.setTaskCompleted(success: false)
|
||||
DispatchQueue.main.async {
|
||||
self.suspendApplication()
|
||||
task?.setTaskCompleted(success: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,12 +431,12 @@ private extension AppDelegate {
|
||||
resumeDatabaseProcessingIfNecessary()
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
os_log(.debug, log: self.log, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
os_log(.debug, log: self.log, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
|
||||
@@ -459,12 +459,12 @@ private extension AppDelegate {
|
||||
resumeDatabaseProcessingIfNecessary()
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
os_log(.debug, "No account found from notification.")
|
||||
os_log(.debug, log: self.log, "No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
os_log(.debug, "No article found from search using %@", articleID)
|
||||
os_log(.debug, log: self.log, "No article found from search using %@", articleID)
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .starred, flag: true) { _ in }
|
||||
|
||||
@@ -162,7 +162,6 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
searchBar.delegate = self
|
||||
view.bringSubviewToFront(searchBar)
|
||||
|
||||
configureAppearanceMenu()
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@@ -234,12 +233,20 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
starBarButtonItem.accLabelText = NSLocalizedString("Star Article", comment: "Star Article")
|
||||
}
|
||||
|
||||
configureAppearanceMenu()
|
||||
configureArticleExtractorMenu()
|
||||
|
||||
}
|
||||
|
||||
override func contentScrollView(for edge: NSDirectionalRectEdge) -> UIScrollView? {
|
||||
return currentWebViewController?.webView?.scrollView
|
||||
}
|
||||
|
||||
|
||||
/// The appearance menu is different on iPhone and iPad.
|
||||
/// On iPad, it's only the theme selector. On iPhone, the appearance menu
|
||||
/// contains the the theme selector and full screen options.
|
||||
/// - Parameter sender: `Any?`
|
||||
@objc
|
||||
func configureAppearanceMenu(_ sender: Any? = nil) {
|
||||
|
||||
@@ -266,6 +273,12 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
|
||||
let themeMenu = UIMenu(title: "Theme", image: AppAssets.themeImage, identifier: nil, options: .singleSelection, children: [ defaultThemeMenu, customThemeMenu])
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
appearanceBarButtonItem.image = AppAssets.themeImage
|
||||
appearanceBarButtonItem.menu = themeMenu
|
||||
return
|
||||
}
|
||||
|
||||
var appearanceChildren: [UIMenuElement] = [themeMenu]
|
||||
|
||||
if let currentWebViewController = currentWebViewController {
|
||||
@@ -291,8 +304,15 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
}
|
||||
}
|
||||
|
||||
var feedManagementChildren = [UIMenuElement]()
|
||||
let appearanceMenu = UIMenu(title: NSLocalizedString("Article Appearance", comment: "Appearance"), image: nil, identifier: nil, options: .displayInline, children: appearanceChildren)
|
||||
|
||||
let menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: [appearanceMenu])
|
||||
|
||||
appearanceBarButtonItem.image = AppAssets.articleAppearanceImage
|
||||
appearanceBarButtonItem.menu = menu
|
||||
}
|
||||
|
||||
private func configureArticleExtractorMenu() {
|
||||
if let feed = article?.webFeed {
|
||||
let extractorOn = feed.isArticleExtractorAlwaysOn ?? false
|
||||
let readerAction = UIAction(title: NSLocalizedString("Always Use Reader View", comment: "Always Use Reader View"),
|
||||
@@ -303,37 +323,16 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
state: extractorOn ? .on : .off) { [weak self] _ in
|
||||
if feed.isArticleExtractorAlwaysOn == nil {
|
||||
feed.isArticleExtractorAlwaysOn = true
|
||||
self?.currentWebViewController?.toggleArticleExtractor()
|
||||
} else {
|
||||
feed.isArticleExtractorAlwaysOn?.toggle()
|
||||
}
|
||||
self?.configureAppearanceMenu()
|
||||
self?.configureArticleExtractorMenu()
|
||||
}
|
||||
feedManagementChildren.append(readerAction)
|
||||
|
||||
let notifyOn = feed.isNotifyAboutNewArticles ?? false
|
||||
let notifyAction = UIAction(title: NSLocalizedString("Notify About New Articles", comment: "Notify About New Articles"),
|
||||
image: AppAssets.appBadgeImage,
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: notifyOn ? .on : .off) { [weak self] _ in
|
||||
if feed.isNotifyAboutNewArticles == nil {
|
||||
feed.isNotifyAboutNewArticles = true
|
||||
} else {
|
||||
feed.isNotifyAboutNewArticles?.toggle()
|
||||
}
|
||||
self?.configureAppearanceMenu()
|
||||
}
|
||||
feedManagementChildren.append(notifyAction)
|
||||
let menu = UIMenu(title: feed.nameForDisplay, image: AppAssets.articleExtractorOffSF, identifier: nil, options: .displayInline, children: [readerAction])
|
||||
articleExtractorButton.menu = menu
|
||||
articleExtractorButton.showsMenuAsPrimaryAction = false
|
||||
}
|
||||
|
||||
let appearanceMenu = UIMenu(title: NSLocalizedString("Article Appearance", comment: "Appearance"), image: nil, identifier: nil, options: .displayInline, children: appearanceChildren)
|
||||
let feedMgmtMenu = UIMenu(title: NSLocalizedString("Feed Management", comment: "Feed Management"), image: nil , identifier: nil, options: .displayInline, children: feedManagementChildren)
|
||||
|
||||
let menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: [appearanceMenu, feedMgmtMenu])
|
||||
|
||||
appearanceBarButtonItem.menu = menu
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -416,6 +415,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
|
||||
@IBAction func toggleArticleExtractor(_ sender: Any) {
|
||||
currentWebViewController?.toggleArticleExtractor()
|
||||
configureArticleExtractorMenu()
|
||||
}
|
||||
|
||||
@IBAction func nextUnread(_ sender: Any) {
|
||||
|
||||
@@ -563,6 +563,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
|
||||
}
|
||||
}
|
||||
|
||||
if let rowChanges = changes.rowChanges {
|
||||
for rowChange in rowChanges {
|
||||
if let reloads = rowChange.reloadIndexPaths, !reloads.isEmpty {
|
||||
tableView.reloadRows(at: reloads, with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion?()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ struct ShadowTableChanges {
|
||||
var section: Int
|
||||
var deletes: Set<Int>?
|
||||
var inserts: Set<Int>?
|
||||
var reloads: Set<Int>?
|
||||
var moves: Set<ShadowTableChanges.Move>?
|
||||
|
||||
var isEmpty: Bool {
|
||||
@@ -41,15 +42,21 @@ struct ShadowTableChanges {
|
||||
return inserts.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
var reloadIndexPaths: [IndexPath]? {
|
||||
guard let reloads = reloads else { return nil }
|
||||
return reloads.map { IndexPath(row: $0, section: section) }
|
||||
}
|
||||
|
||||
var moveIndexPaths: [(IndexPath, IndexPath)]? {
|
||||
guard let moves = moves else { return nil }
|
||||
return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) }
|
||||
}
|
||||
|
||||
init(section: Int, deletes: Set<Int>?, inserts: Set<Int>?, moves: Set<Move>?) {
|
||||
init(section: Int, deletes: Set<Int>?, inserts: Set<Int>?, reloads: Set<Int>?, moves: Set<Move>?) {
|
||||
self.section = section
|
||||
self.deletes = deletes
|
||||
self.inserts = inserts
|
||||
self.reloads = reloads
|
||||
self.moves = moves
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,16 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
private var fetchSerialNumber = 0
|
||||
private let fetchRequestQueue = FetchRequestQueue()
|
||||
|
||||
// Which Containers are expanded
|
||||
private var expandedTable = Set<ContainerIdentifier>()
|
||||
|
||||
// Which Containers used to be expanded. Reset by rebuilding the Shadow Table.
|
||||
private var lastExpandedTable = Set<ContainerIdentifier>()
|
||||
|
||||
// Which Feeds have the Read Articles Filter enabled
|
||||
private var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
||||
|
||||
// Flattened tree structure for the Sidebar
|
||||
private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]()
|
||||
|
||||
private(set) var preSearchTimelineFeed: Feed?
|
||||
@@ -675,8 +683,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
rebuildBackingStores()
|
||||
}
|
||||
|
||||
/// This is a special function that expects the caller to change the disclosure arrow state outside this function.
|
||||
/// Failure to do so will get the Sidebar into an invalid state.
|
||||
func expand(_ node: Node) {
|
||||
guard let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { return }
|
||||
lastExpandedTable.insert(containerID)
|
||||
expand(containerID)
|
||||
}
|
||||
|
||||
@@ -698,8 +709,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
clearTimelineIfNoLongerAvailable()
|
||||
}
|
||||
|
||||
/// This is a special function that expects the caller to change the disclosure arrow state outside this function.
|
||||
/// Failure to do so will get the Sidebar into an invalid state.
|
||||
func collapse(_ node: Node) {
|
||||
guard let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { return }
|
||||
lastExpandedTable.remove(containerID)
|
||||
collapse(containerID)
|
||||
}
|
||||
|
||||
@@ -1080,7 +1094,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
||||
|
||||
rebuildBackingStores(initialLoad: initialLoad, completion: {
|
||||
self.treeControllerDelegate.resetFilterExceptions()
|
||||
self.selectFeed(webFeed, animations: animations, completion: completion)
|
||||
self.selectFeed(nil) {
|
||||
self.selectFeed(webFeed, animations: animations, completion: completion)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -1383,6 +1399,7 @@ private extension SceneCoordinator {
|
||||
let toolbarAppearance = UIToolbarAppearance()
|
||||
navController.toolbar.standardAppearance = toolbarAppearance
|
||||
navController.toolbar.compactAppearance = toolbarAppearance
|
||||
navController.toolbar.scrollEdgeAppearance = toolbarAppearance
|
||||
navController.toolbar.tintColor = AppAssets.primaryAccentColor
|
||||
}
|
||||
|
||||
@@ -1512,9 +1529,10 @@ private extension SceneCoordinator {
|
||||
currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject)
|
||||
}
|
||||
|
||||
// Compute the differences in the shadow table rows
|
||||
// Compute the differences in the shadow table rows and the expanded table entries
|
||||
var changes = [ShadowTableChanges.RowChanges]()
|
||||
|
||||
let expandedTableDifference = lastExpandedTable.symmetricDifference(expandedTable)
|
||||
|
||||
for (section, newSectionRows) in newShadowTable.enumerated() {
|
||||
var moves = Set<ShadowTableChanges.Move>()
|
||||
var inserts = Set<Int>()
|
||||
@@ -1540,9 +1558,22 @@ private extension SceneCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, moves: moves))
|
||||
// We need to reload the difference in expanded rows to get the disclosure arrows correct when programmatically changing their state
|
||||
var reloads = Set<Int>()
|
||||
|
||||
for (index, newFeedNode) in newSectionRows.feedNodes.enumerated() {
|
||||
if let newFeedNodeContainerID = (newFeedNode.node.representedObject as? Container)?.containerID {
|
||||
if expandedTableDifference.contains(newFeedNodeContainerID) {
|
||||
reloads.insert(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, reloads: reloads, moves: moves))
|
||||
}
|
||||
|
||||
lastExpandedTable = expandedTable
|
||||
|
||||
// Compute the difference in the shadow table sections
|
||||
var moves = Set<ShadowTableChanges.Move>()
|
||||
var inserts = Set<Int>()
|
||||
|
||||
Reference in New Issue
Block a user