Merge branch 'master' into feature/feed-wrangler

This commit is contained in:
Jonathan Bennett 2019-11-24 20:41:43 -05:00
commit ce51e4e632
53 changed files with 1243 additions and 532 deletions

View File

@ -47,7 +47,7 @@ public enum FetchType {
case starred case starred
case unread case unread
case today case today
case unreadForFolder(Folder) case folder(Folder, Bool)
case webFeed(WebFeed) case webFeed(WebFeed)
case articleIDs(Set<String>) case articleIDs(Set<String>)
case search(String) case search(String)
@ -84,6 +84,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public var isDeleted = false public var isDeleted = false
public var containerID: ContainerIdentifier? {
return ContainerIdentifier.account(accountID)
}
public var account: Account? { public var account: Account? {
return self return self
} }
@ -594,8 +598,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return fetchUnreadArticles() return fetchUnreadArticles()
case .today: case .today:
return fetchTodayArticles() return fetchTodayArticles()
case .unreadForFolder(let folder): case .folder(let folder, let readFilter):
return fetchArticles(folder: folder) if readFilter {
return fetchUnreadArticles(folder: folder)
} else {
return fetchArticles(folder: folder)
}
case .webFeed(let webFeed): case .webFeed(let webFeed):
return fetchArticles(webFeed: webFeed) return fetchArticles(webFeed: webFeed)
case .articleIDs(let articleIDs): case .articleIDs(let articleIDs):
@ -615,8 +623,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fetchUnreadArticlesAsync(callback) fetchUnreadArticlesAsync(callback)
case .today: case .today:
fetchTodayArticlesAsync(callback) fetchTodayArticlesAsync(callback)
case .unreadForFolder(let folder): case .folder(let folder, let readFilter):
fetchArticlesAsync(folder: folder, callback) if readFilter {
return fetchUnreadArticlesAsync(folder: folder, callback)
} else {
return fetchArticlesAsync(folder: folder, callback)
}
case .webFeed(let webFeed): case .webFeed(let webFeed):
fetchArticlesAsync(webFeed: webFeed, callback) fetchArticlesAsync(webFeed: webFeed, callback)
case .articleIDs(let articleIDs): case .articleIDs(let articleIDs):
@ -892,10 +904,18 @@ private extension Account {
} }
func fetchArticles(folder: Folder) -> Set<Article> { func fetchArticles(folder: Folder) -> Set<Article> {
return fetchUnreadArticles(forContainer: folder) return fetchArticles(forContainer: folder)
} }
func fetchArticlesAsync(folder: Folder, _ callback: @escaping ArticleSetBlock) { func fetchArticlesAsync(folder: Folder, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync(forContainer: folder, callback)
}
func fetchUnreadArticles(folder: Folder) -> Set<Article> {
return fetchUnreadArticles(forContainer: folder)
}
func fetchUnreadArticlesAsync(folder: Folder, _ callback: @escaping ArticleSetBlock) {
fetchUnreadArticlesAsync(forContainer: folder, callback) fetchUnreadArticlesAsync(forContainer: folder, callback)
} }
@ -950,6 +970,21 @@ private extension Account {
} }
func fetchArticles(forContainer container: Container) -> Set<Article> {
let feeds = container.flattenedWebFeeds()
let articles = database.fetchArticles(feeds.webFeedIDs())
validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles)
return articles
}
func fetchArticlesAsync(forContainer container: Container, _ callback: @escaping ArticleSetBlock) {
let webFeeds = container.flattenedWebFeeds()
database.fetchArticlesAsync(webFeeds.webFeedIDs()) { [weak self] (articles) in
self?.validateUnreadCountsAfterFetchingUnreadArticles(webFeeds, articles)
callback(articles)
}
}
func fetchUnreadArticles(forContainer container: Container) -> Set<Article> { func fetchUnreadArticles(forContainer container: Container) -> Set<Article> {
let feeds = container.flattenedWebFeeds() let feeds = container.flattenedWebFeeds()
let articles = database.fetchUnreadArticles(feeds.webFeedIDs()) let articles = database.fetchUnreadArticles(feeds.webFeedIDs())

View File

@ -43,6 +43,7 @@
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; }; 5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; };
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; }; 51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */; };
51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; }; 51BC8FCC237EC055004F8B56 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC8FCB237EC055004F8B56 /* Feed.swift */; };
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BFDECD238B508D00216323 /* ContainerIdentifier.swift */; };
51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; }; 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; };
51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; }; 51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; };
51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; }; 51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; };
@ -255,6 +256,7 @@
518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = "<group>"; }; 518B2EA52351306200400001 /* Account_project_test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Account_project_test.xcconfig; sourceTree = "<group>"; };
51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; }; 51BB7B83233531BC008E8144 /* AccountBehaviors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountBehaviors.swift; sourceTree = "<group>"; };
51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; }; 51BC8FCB237EC055004F8B56 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerIdentifier.swift; sourceTree = "<group>"; };
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; }; 51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; }; 51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; }; 51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
@ -572,6 +574,7 @@
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */, 84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */, 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
8419740D1F6DD25F006346C4 /* Container.swift */, 8419740D1F6DD25F006346C4 /* Container.swift */,
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */,
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */, 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */,
84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */, 84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */,
51BC8FCB237EC055004F8B56 /* Feed.swift */, 51BC8FCB237EC055004F8B56 /* Feed.swift */,
@ -1123,6 +1126,7 @@
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */, 515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */, 844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */, 9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */,
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */,
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */, 9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */, 9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */,
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */, 84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,

View File

@ -49,11 +49,20 @@ extension WebFeed: ArticleFetcher {
extension Folder: ArticleFetcher { extension Folder: ArticleFetcher {
public func fetchArticles() -> Set<Article> { public func fetchArticles() -> Set<Article> {
return fetchUnreadArticles() guard let account = account else {
assertionFailure("Expected folder.account, but got nil.")
return Set<Article>()
}
return account.fetchArticles(.folder(self, false))
} }
public func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) { public func fetchArticlesAsync(_ callback: @escaping ArticleSetBlock) {
fetchUnreadArticlesAsync(callback) guard let account = account else {
assertionFailure("Expected folder.account, but got nil.")
callback(Set<Article>())
return
}
account.fetchArticlesAsync(.folder(self, false), callback)
} }
public func fetchUnreadArticles() -> Set<Article> { public func fetchUnreadArticles() -> Set<Article> {
@ -61,7 +70,7 @@ extension Folder: ArticleFetcher {
assertionFailure("Expected folder.account, but got nil.") assertionFailure("Expected folder.account, but got nil.")
return Set<Article>() return Set<Article>()
} }
return account.fetchArticles(.unreadForFolder(self)) return account.fetchArticles(.folder(self, true))
} }
public func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) { public func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
@ -70,6 +79,6 @@ extension Folder: ArticleFetcher {
callback(Set<Article>()) callback(Set<Article>())
return return
} }
account.fetchArticlesAsync(.unreadForFolder(self), callback) account.fetchArticlesAsync(.folder(self, true), callback)
} }
} }

View File

@ -16,7 +16,7 @@ extension Notification.Name {
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange") public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
} }
public protocol Container: class { public protocol Container: class, ContainerIdentifiable {
var account: Account? { get } var account: Account? { get }
var topLevelWebFeeds: Set<WebFeed> { get set } var topLevelWebFeeds: Set<WebFeed> { get set }

View File

@ -0,0 +1,19 @@
//
// ContainerIdentifier.swift
// Account
//
// Created by Maurice Parker on 11/24/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public protocol ContainerIdentifiable {
var containerID: ContainerIdentifier? { get }
}
public enum ContainerIdentifier: Hashable {
case smartFeedController
case account(String) // accountID
case folder(String, String) // accountID, folderName
}

View File

@ -9,6 +9,14 @@
import Foundation import Foundation
import RSCore import RSCore
public enum ReadFilterType {
case read
case none
case alwaysRead
}
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider { public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
var defaultReadFilterType: ReadFilterType { get }
} }

View File

@ -12,6 +12,18 @@ import RSCore
public final class Folder: Feed, Renamable, Container, Hashable { public final class Folder: Feed, Renamable, Container, Hashable {
public var defaultReadFilterType: ReadFilterType {
return .read
}
public var containerID: ContainerIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return ContainerIdentifier.folder(accountID, nameForDisplay)
}
public var feedID: FeedIdentifier? { public var feedID: FeedIdentifier? {
guard let accountID = account?.accountID else { guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.") assertionFailure("Expected feed.account, but got nil.")

View File

@ -13,6 +13,10 @@ import Articles
public final class WebFeed: Feed, Renamable, Hashable { public final class WebFeed: Feed, Renamable, Hashable {
public var defaultReadFilterType: ReadFilterType {
return .none
}
public var feedID: FeedIdentifier? { public var feedID: FeedIdentifier? {
guard let accountID = account?.accountID else { guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.") assertionFailure("Expected feed.account, but got nil.")

View File

@ -48,12 +48,16 @@ public final class ArticlesDatabase {
return articlesTable.fetchArticles(webFeedID) return articlesTable.fetchArticles(webFeedID)
} }
public func fetchArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticles(webFeedIDs)
}
public func fetchArticles(articleIDs: Set<String>) -> Set<Article> { public func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticles(articleIDs: articleIDs) return articlesTable.fetchArticles(articleIDs: articleIDs)
} }
public func fetchUnreadArticles(_ webFeedID: Set<String>) -> Set<Article> { public func fetchUnreadArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchUnreadArticles(webFeedID) return articlesTable.fetchUnreadArticles(webFeedIDs)
} }
public func fetchTodayArticles(_ webFeedIDs: Set<String>) -> Set<Article> { public func fetchTodayArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
@ -78,6 +82,10 @@ public final class ArticlesDatabase {
articlesTable.fetchArticlesAsync(webFeedID, callback) articlesTable.fetchArticlesAsync(webFeedID, callback)
} }
public func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchArticlesAsync(webFeedIDs, callback)
}
public func fetchArticlesAsync(articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) { public func fetchArticlesAsync(articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, callback) articlesTable.fetchArticlesAsync(articleIDs: articleIDs, callback)
} }

View File

@ -60,6 +60,25 @@ final class ArticlesTable: DatabaseTable {
return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits) return fetchArticlesWithWhereClause(database, whereClause: "articles.feedID = ?", parameters: [webFeedID as AnyObject], withLimits: withLimits)
} }
func fetchArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
return fetchArticles{ self.fetchArticles(webFeedIDs, $0) }
}
func fetchArticlesAsync(_ webFeedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchArticles(webFeedIDs, $0) }, callback)
}
private func fetchArticles(_ webFeedIDs: Set<String>, _ 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)"
return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
}
// MARK: - Fetching Articles by articleID // MARK: - Fetching Articles by articleID
func fetchArticles(articleIDs: Set<String>) -> Set<Article> { func fetchArticles(articleIDs: Set<String>) -> Set<Article> {

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15505"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Application--> <!--Application-->
@ -336,6 +336,12 @@
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO"> <menu key="submenu" title="View" id="HyV-fh-RgO">
<items> <items>
<menuItem title="Hide Read Articles" id="b10-sA-Yzi">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleReadArticlesFilter:" target="Ady-hI-5gd" id="YhV-0F-jrM"/>
</connections>
</menuItem>
<menuItem title="Sort Articles By" id="nLP-fa-KUi"> <menuItem title="Sort Articles By" id="nLP-fa-KUi">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Sort Articles By" id="OlJ-93-6OP"> <menu key="submenu" title="Sort Articles By" id="OlJ-93-6OP">
@ -362,6 +368,12 @@
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="dZt-2W-gxf"/> <menuItem isSeparatorItem="YES" id="dZt-2W-gxf"/>
<menuItem title="Hide Read Feeds" id="E9K-zV-nLv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleReadFeedsFilter:" target="Ady-hI-5gd" id="5pI-YT-xai"/>
</connections>
</menuItem>
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE"> <menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/> <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections> <connections>

View File

@ -221,7 +221,7 @@ private extension DetailWebViewController {
var render = "error();" var render = "error();"
if let data = try? encoder.encode(templateData) { if let data = try? encoder.encode(templateData) {
let json = String(data: data, encoding: .utf8)! let json = String(data: data, encoding: .utf8)!
render = "render(\(json));" render = "render(\(json), 0);"
} }
webView.evaluateJavaScript(render) webView.evaluateJavaScript(render)

View File

@ -237,6 +237,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
return currentSearchField != nil return currentSearchField != nil
} }
if item.action == #selector(toggleReadFeedsFilter(_:)) {
return validateToggleReadFeeds(item)
}
if item.action == #selector(toggleReadArticlesFilter(_:)) {
return validateToggleReadArticles(item)
}
if item.action == #selector(toggleSidebar(_:)) { if item.action == #selector(toggleSidebar(_:)) {
guard let splitViewItem = sidebarSplitViewItem else { guard let splitViewItem = sidebarSplitViewItem else {
return false return false
@ -438,6 +446,15 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
} }
window?.makeFirstResponder(searchField) window?.makeFirstResponder(searchField)
} }
@IBAction func toggleReadFeedsFilter(_ sender: Any?) {
sidebarViewController?.toggleReadFilter()
}
@IBAction func toggleReadArticlesFilter(_ sender: Any?) {
timelineContainerViewController?.toggleReadFilter()
}
} }
// MARK: - SidebarDelegate // MARK: - SidebarDelegate
@ -811,6 +828,30 @@ private extension MainWindowController {
return result return result
} }
func validateToggleReadFeeds(_ item: NSValidatedUserInterfaceItem) -> Bool {
guard let menuItem = item as? NSMenuItem else { return false }
let showCommand = NSLocalizedString("Show Read Feeds", comment: "Command")
let hideCommand = NSLocalizedString("Hide Read Feeds", comment: "Command")
menuItem.title = sidebarViewController?.isReadFiltered ?? false ? showCommand : hideCommand
return true
}
func validateToggleReadArticles(_ item: NSValidatedUserInterfaceItem) -> Bool {
guard let menuItem = item as? NSMenuItem else { return false }
let showCommand = NSLocalizedString("Show Read Articles", comment: "Command")
let hideCommand = NSLocalizedString("Hide Read Articles", comment: "Command")
if let isReadFiltered = timelineContainerViewController?.isReadFiltered {
menuItem.title = isReadFiltered ? showCommand : hideCommand
return true
} else {
menuItem.title = hideCommand
return false
}
}
// MARK: - Misc. // MARK: - Misc.
func goToNextUnreadInTimeline() { func goToNextUnreadInTimeline() {

View File

@ -30,6 +30,9 @@ protocol SidebarDelegate: class {
lazy var dataSource: SidebarOutlineDataSource = { lazy var dataSource: SidebarOutlineDataSource = {
return SidebarOutlineDataSource(treeController: treeController) return SidebarOutlineDataSource(treeController: treeController)
}() }()
var isReadFiltered: Bool {
return treeControllerDelegate.isReadFiltered
}
var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
private var animatingChanges = false private var animatingChanges = false
@ -334,6 +337,15 @@ protocol SidebarDelegate: class {
revealAndSelectRepresentedObject(feedNode.representedObject) revealAndSelectRepresentedObject(feedNode.representedObject)
} }
func toggleReadFilter() {
if treeControllerDelegate.isReadFiltered {
treeControllerDelegate.isReadFiltered = false
} else {
treeControllerDelegate.isReadFiltered = true
}
rebuildTreeAndRestoreSelection()
}
} }
// MARK: - NSUserInterfaceValidations // MARK: - NSUserInterfaceValidations

View File

@ -30,6 +30,11 @@ final class TimelineContainerViewController: NSViewController {
weak var delegate: TimelineContainerViewControllerDelegate? weak var delegate: TimelineContainerViewControllerDelegate?
var isReadFiltered: Bool? {
guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return nil }
return regularTimelineViewController.isReadFiltered
}
lazy var regularTimelineViewController = { lazy var regularTimelineViewController = {
return TimelineViewController(delegate: self) return TimelineViewController(delegate: self)
}() }()
@ -79,6 +84,11 @@ final class TimelineContainerViewController: NSViewController {
} }
return true return true
} }
func toggleReadFilter() {
regularTimelineViewController.toggleReadFilter()
}
} }
extension TimelineContainerViewController: TimelineDelegate { extension TimelineContainerViewController: TimelineDelegate {

View File

@ -20,6 +20,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
@IBOutlet var tableView: TimelineTableView! @IBOutlet var tableView: TimelineTableView!
private var articleReadFilterType: ReadFilterType?
var isReadFiltered: Bool? {
guard let articleReadFilterType = articleReadFilterType, articleReadFilterType != .alwaysRead else { return nil}
return articleReadFilterType != .none
}
var representedObjects: [AnyObject]? { var representedObjects: [AnyObject]? {
didSet { didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) { if !representedObjectArraysAreEqual(oldValue, representedObjects) {
@ -36,6 +42,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
showFeedNames = false showFeedNames = false
} }
determineReadFilterType()
selectionDidChange(nil) selectionDidChange(nil)
if showsSearchResults { if showsSearchResults {
fetchAndReplaceArticlesAsync() fetchAndReplaceArticlesAsync()
@ -213,6 +220,19 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return representedObjects.first! === object return representedObjects.first! === object
} }
func toggleReadFilter() {
guard let filterType = articleReadFilterType else { return }
switch filterType {
case .alwaysRead:
break
case .read:
articleReadFilterType = ReadFilterType.none
case .none:
articleReadFilterType = ReadFilterType.read
}
fetchAndReplaceArticlesAsync()
}
// MARK: - Actions // MARK: - Actions
@objc func openArticleInBrowser(_ sender: Any?) { @objc func openArticleInBrowser(_ sender: Any?) {
@ -945,6 +965,14 @@ private extension TimelineViewController {
// MARK: - Fetching Articles // MARK: - Fetching Articles
func determineReadFilterType() {
if representedObjects?.count ?? 0 == 1, let feed = representedObjects?.first as? Feed {
articleReadFilterType = feed.defaultReadFilterType
} else {
articleReadFilterType = .read
}
}
func fetchAndReplaceArticlesSync() { func fetchAndReplaceArticlesSync() {
// To be called when the user has made a change of selection in the sidebar. // To be called when the user has made a change of selection in the sidebar.
// It blocks the main thread, so that theres no async delay, // It blocks the main thread, so that theres no async delay,
@ -990,7 +1018,11 @@ private extension TimelineViewController {
var fetchedArticles = Set<Article>() var fetchedArticles = Set<Article>()
for articleFetcher in articleFetchers { for articleFetcher in articleFetchers {
fetchedArticles.formUnion(articleFetcher.fetchArticles()) if articleReadFilterType != ReadFilterType.none {
fetchedArticles.formUnion(articleFetcher.fetchUnreadArticles())
} else {
fetchedArticles.formUnion(articleFetcher.fetchArticles())
}
} }
return fetchedArticles return fetchedArticles
} }
@ -1000,7 +1032,8 @@ private extension TimelineViewController {
// if its been superseded by a newer fetch, or the timeline was emptied, etc., it wont get called. // if its been superseded by a newer fetch, or the timeline was emptied, etc., it wont get called.
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in let readFilter = articleReadFilterType != ReadFilterType.none
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: readFilter, representedObjects: representedObjects) { [weak self] (articles, operation) in
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
return return

View File

@ -39,7 +39,6 @@
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; };
512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97611ED9EB96007D329B /* WebFeedTreeControllerDelegate.swift */; }; 512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97611ED9EB96007D329B /* WebFeedTreeControllerDelegate.swift */; };
512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */; }; 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */; };
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */; };
512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C9C1FAE83C600ECDEDB /* DeleteCommand.swift */; }; 512E094D2268B8AB00BDCFDD /* DeleteCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C9C1FAE83C600ECDEDB /* DeleteCommand.swift */; };
5131463E235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 5131463E235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
51314668235A7E4600387FDC /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51314666235A7E4600387FDC /* IntentHandler.swift */; }; 51314668235A7E4600387FDC /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51314666235A7E4600387FDC /* IntentHandler.swift */; };
@ -95,6 +94,10 @@
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; 515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; };
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; }; 515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; }; 515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; };
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; };
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */; };
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */; };
516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */; }; 516A093723609A3600EAE89B /* SettingsAccountTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */; };
516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; }; 516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; };
516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; }; 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */; };
@ -217,7 +220,6 @@
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; };
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; }; 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; };
51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */; };
51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; }; 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; };
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; }; 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; }; 51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; };
@ -1239,7 +1241,6 @@
512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageHeaderView.swift; sourceTree = "<group>"; }; 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageHeaderView.swift; sourceTree = "<group>"; };
512AF9DC236F05230066F8BE /* InteractiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveLabel.swift; sourceTree = "<group>"; }; 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveLabel.swift; sourceTree = "<group>"; };
512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeader.swift; sourceTree = "<group>"; }; 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeader.swift; sourceTree = "<group>"; };
512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISplitViewController-Extensions.swift"; sourceTree = "<group>"; };
51314617235A797400387FDC /* NetNewsWire_iOSintentextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSintentextension_target.xcconfig; sourceTree = "<group>"; }; 51314617235A797400387FDC /* NetNewsWire_iOSintentextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSintentextension_target.xcconfig; sourceTree = "<group>"; };
51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Intents Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Intents Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
51314665235A7E4600387FDC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 51314665235A7E4600387FDC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -1274,6 +1275,10 @@
515D4FCB2325815A00EE1167 /* SafariExt.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SafariExt.js; sourceTree = "<group>"; }; 515D4FCB2325815A00EE1167 /* SafariExt.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SafariExt.js; sourceTree = "<group>"; };
515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = "<group>"; }; 515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = "<group>"; };
515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = "<group>"; }; 515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = "<group>"; };
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = "<group>"; };
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = "<group>"; };
51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; };
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingPreviewParameters.swift; sourceTree = "<group>"; };
516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsAccountTableViewCell.xib; sourceTree = "<group>"; }; 516A091D23609A3600EAE89B /* SettingsAccountTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsAccountTableViewCell.xib; sourceTree = "<group>"; };
516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountTableViewCell.swift; sourceTree = "<group>"; }; 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountTableViewCell.swift; sourceTree = "<group>"; };
516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = "<group>"; }; 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = "<group>"; };
@ -1334,7 +1339,6 @@
51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = "<group>"; }; 51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = "<group>"; };
51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; }; 51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = "<group>"; };
51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = "<group>"; };
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = "<group>"; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = "<group>"; };
51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = "<group>"; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = "<group>"; };
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
@ -1858,24 +1862,24 @@
children = ( children = (
51F85BFA2275D85000C787DC /* Array-Extensions.swift */, 51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
51F85BF42273625800C787DC /* Bundle-Extensions.swift */, 51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */,
512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */, 512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */,
512AF9DC236F05230066F8BE /* InteractiveLabel.swift */, 512AF9DC236F05230066F8BE /* InteractiveLabel.swift */,
51934CC1230F5963006127BE /* InteractiveNavigationController.swift */,
51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */,
51EAED95231363EF00A9EEE3 /* NonIntrinsicButton.swift */, 51EAED95231363EF00A9EEE3 /* NonIntrinsicButton.swift */,
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */, 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */,
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */, 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */,
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */,
512363372369155100951F16 /* RoundedProgressView.swift */, 512363372369155100951F16 /* RoundedProgressView.swift */,
51C45250226506F400C03939 /* String-Extensions.swift */, 51C45250226506F400C03939 /* String-Extensions.swift */,
51934CC1230F5963006127BE /* InteractiveNavigationController.swift */,
5108F6D723763094001ABC45 /* TickMarkSlider.swift */, 5108F6D723763094001ABC45 /* TickMarkSlider.swift */,
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */, 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */,
51F85BF622749FA100C787DC /* UIFont-Extensions.swift */, 51F85BF622749FA100C787DC /* UIFont-Extensions.swift */,
512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */,
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */, 51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */,
51FFF0C3235EE8E5002762AA /* VibrantButton.swift */, 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */,
5186A634235EF3A800C97195 /* VibrantLabel.swift */, 5186A634235EF3A800C97195 /* VibrantLabel.swift */,
5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */, 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */,
51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */,
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */,
); );
path = "UIKit Extensions"; path = "UIKit Extensions";
sourceTree = "<group>"; sourceTree = "<group>";
@ -1884,7 +1888,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
51C45264226508F600C03939 /* MasterFeedViewController.swift */, 51C45264226508F600C03939 /* MasterFeedViewController.swift */,
51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */, 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */,
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */,
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */,
51CE1C0A23622006005548FC /* RefreshProgressView.swift */, 51CE1C0A23622006005548FC /* RefreshProgressView.swift */,
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */, 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */,
51C45260226508F600C03939 /* Cell */, 51C45260226508F600C03939 /* Cell */,
@ -3962,7 +3968,6 @@
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */, 51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */, 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */,
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */,
51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */, 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */,
51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */, 51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */,
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */, 51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
@ -3971,6 +3976,7 @@
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */, 51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */, 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */,
5141E7562374A2890013FF27 /* ArticleIconSchemeHandler.swift in Sources */, 5141E7562374A2890013FF27 /* ArticleIconSchemeHandler.swift in Sources */,
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */,
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */, 512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */,
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */, 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */, 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */,
@ -3991,6 +3997,7 @@
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */,
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */, 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */, 5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */,
@ -4016,6 +4023,7 @@
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */, 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */, 5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
518651DA235621840078E021 /* ImageTransition.swift in Sources */, 518651DA235621840078E021 /* ImageTransition.swift in Sources */,
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
@ -4027,12 +4035,12 @@
51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */,
5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */, 5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */,
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */,
51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */,
FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */, FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */,
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */,
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */, 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */,
51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */,
51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */, 51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */,
513228FC233037630033D4ED /* Reachability.swift in Sources */, 513228FC233037630033D4ED /* Reachability.swift in Sources */,
51C45259226508D300C03939 /* AppDefaults.swift in Sources */, 51C45259226508D300C03939 /* AppDefaults.swift in Sources */,

View File

@ -30,11 +30,11 @@ function error() {
document.body.innerHTML = "error"; document.body.innerHTML = "error";
} }
function render(data) { function render(data, scrollY) {
document.getElementsByTagName("style")[0].innerHTML = data.style; document.getElementsByTagName("style")[0].innerHTML = data.style;
document.body.innerHTML = data.body; document.body.innerHTML = data.body;
window.scrollTo(0, 0); window.scrollTo(0, scrollY);
wrapFrames() wrapFrames()
stripStyles() stripStyles()

View File

@ -89,7 +89,7 @@ struct ArticleStringFormatter {
return cachedBody return cachedBody
} }
var s = body.rsparser_stringByDecodingHTMLEntities() var s = body.rsparser_stringByDecodingHTMLEntities()
s = s.rs_string(byStrippingHTML: 150) s = s.rs_string(byStrippingHTML: 250)
s = s.rs_stringByTrimmingWhitespace() s = s.rs_stringByTrimmingWhitespace()
s = s.rs_stringWithCollapsedWhitespace() s = s.rs_stringWithCollapsedWhitespace()
if s == "Comments" { // Hacker News. if s == "Comments" { // Hacker News.

View File

@ -13,6 +13,10 @@ import Account
final class SmartFeed: PseudoFeed { final class SmartFeed: PseudoFeed {
public var defaultReadFilterType: ReadFilterType {
return .none
}
var feedID: FeedIdentifier? { var feedID: FeedIdentifier? {
delegate.feedID delegate.feedID
} }

View File

@ -8,13 +8,18 @@
import Foundation import Foundation
import RSCore import RSCore
import Account
final class SmartFeedsController: DisplayNameProvider { final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable {
var containerID: ContainerIdentifier? {
return ContainerIdentifier.smartFeedController
}
public static let shared = SmartFeedsController() public static let shared = SmartFeedsController()
let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title") let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title")
var smartFeeds = [AnyObject]() var smartFeeds = [Feed]()
let todayFeed = SmartFeed(delegate: TodayFeedDelegate()) let todayFeed = SmartFeed(delegate: TodayFeedDelegate())
let unreadFeed = UnreadFeed() let unreadFeed = UnreadFeed()
let starredFeed = SmartFeed(delegate: StarredFeedDelegate()) let starredFeed = SmartFeed(delegate: StarredFeedDelegate())
@ -23,14 +28,19 @@ final class SmartFeedsController: DisplayNameProvider {
self.smartFeeds = [todayFeed, unreadFeed, starredFeed] self.smartFeeds = [todayFeed, unreadFeed, starredFeed]
} }
func find(by identifier: String) -> PseudoFeed? { func find(by identifier: FeedIdentifier) -> PseudoFeed? {
switch identifier { switch identifier {
case String(describing: TodayFeedDelegate.self): case .smartFeed(let stringIdentifer):
return todayFeed switch stringIdentifer {
case String(describing: UnreadFeed.self): case String(describing: TodayFeedDelegate.self):
return unreadFeed return todayFeed
case String(describing: StarredFeedDelegate.self): case String(describing: UnreadFeed.self):
return starredFeed return unreadFeed
case String(describing: StarredFeedDelegate.self):
return starredFeed
default:
return nil
}
default: default:
return nil return nil
} }

View File

@ -19,6 +19,10 @@ import Articles
final class UnreadFeed: PseudoFeed { final class UnreadFeed: PseudoFeed {
public var defaultReadFilterType: ReadFilterType {
return .alwaysRead
}
var feedID: FeedIdentifier? { var feedID: FeedIdentifier? {
return FeedIdentifier.smartFeed(String(describing: UnreadFeed.self)) return FeedIdentifier.smartFeed(String(describing: UnreadFeed.self))
} }

View File

@ -19,14 +19,16 @@ typealias FetchRequestOperationResultBlock = (Set<Article>, FetchRequestOperatio
final class FetchRequestOperation { final class FetchRequestOperation {
let id: Int let id: Int
let readFilter: Bool
let resultBlock: FetchRequestOperationResultBlock let resultBlock: FetchRequestOperationResultBlock
var isCanceled = false var isCanceled = false
var isFinished = false var isFinished = false
private let representedObjects: [Any] private let representedObjects: [Any]
init(id: Int, representedObjects: [Any], resultBlock: @escaping FetchRequestOperationResultBlock) { init(id: Int, readFilter: Bool, representedObjects: [Any], resultBlock: @escaping FetchRequestOperationResultBlock) {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
self.id = id self.id = id
self.readFilter = readFilter
self.representedObjects = representedObjects self.representedObjects = representedObjects
self.resultBlock = resultBlock self.resultBlock = resultBlock
} }
@ -60,25 +62,38 @@ final class FetchRequestOperation {
let numberOfFetchers = articleFetchers.count let numberOfFetchers = articleFetchers.count
var fetchersReturned = 0 var fetchersReturned = 0
var fetchedArticles = Set<Article>() var fetchedArticles = Set<Article>()
func process(articles: Set<Article>) {
precondition(Thread.isMainThread)
guard !self.isCanceled else {
callCompletionIfNeeded()
return
}
assert(!self.isFinished)
fetchedArticles.formUnion(articles)
fetchersReturned += 1
if fetchersReturned == numberOfFetchers {
self.isFinished = true
self.resultBlock(fetchedArticles, self)
callCompletionIfNeeded()
}
}
for articleFetcher in articleFetchers { for articleFetcher in articleFetchers {
articleFetcher.fetchArticlesAsync { (articles) in if readFilter {
precondition(Thread.isMainThread) articleFetcher.fetchUnreadArticlesAsync { (articles) in
guard !self.isCanceled else { process(articles: articles)
callCompletionIfNeeded()
return
} }
} else {
assert(!self.isFinished) articleFetcher.fetchArticlesAsync { (articles) in
process(articles: articles)
fetchedArticles.formUnion(articles)
fetchersReturned += 1
if fetchersReturned == numberOfFetchers {
self.isFinished = true
self.resultBlock(fetchedArticles, self)
callCompletionIfNeeded()
} }
} }
} }
} }
} }

View File

@ -13,8 +13,9 @@ import Account
final class WebFeedTreeControllerDelegate: TreeControllerDelegate { final class WebFeedTreeControllerDelegate: TreeControllerDelegate {
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? { var isReadFiltered = false
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
if node.isRoot { if node.isRoot {
return childNodesForRootNode(node) return childNodesForRootNode(node)
} }
@ -32,29 +33,47 @@ final class WebFeedTreeControllerDelegate: TreeControllerDelegate {
private extension WebFeedTreeControllerDelegate { private extension WebFeedTreeControllerDelegate {
func childNodesForRootNode(_ rootNode: Node) -> [Node]? { func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
var topLevelNodes = [Node]()
// The top-level nodes are Smart Feeds and accounts. // Check to see if we should show the SmartFeeds top level by checking the unreadFeed
if !(isReadFiltered && SmartFeedsController.shared.unreadFeed.unreadCount == 0) {
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared)
smartFeedsNode.canHaveChildNodes = true
smartFeedsNode.isGroupItem = true
topLevelNodes.append(smartFeedsNode)
}
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared) topLevelNodes.append(contentsOf: sortedAccountNodes(rootNode))
smartFeedsNode.canHaveChildNodes = true
smartFeedsNode.isGroupItem = true
return [smartFeedsNode] + sortedAccountNodes(rootNode) return topLevelNodes
} }
func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] { func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] {
return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in
return SmartFeedsController.shared.smartFeeds.map { parentNode.existingOrNewChildNode(with: $0) } if isReadFiltered && feed.unreadCount == 0 {
return nil
}
return parentNode.existingOrNewChildNode(with: feed as AnyObject)
}
} }
func childNodesForContainerNode(_ containerNode: Node) -> [Node]? { func childNodesForContainerNode(_ containerNode: Node) -> [Node]? {
let container = containerNode.representedObject as! Container let container = containerNode.representedObject as! Container
var children = [AnyObject]() var children = [AnyObject]()
children.append(contentsOf: Array(container.topLevelWebFeeds))
for webFeed in container.topLevelWebFeeds {
if !(isReadFiltered && webFeed.unreadCount == 0) {
children.append(webFeed)
}
}
if let folders = container.folders { if let folders = container.folders {
children.append(contentsOf: Array(folders)) for folder in folders {
if !(isReadFiltered && folder.unreadCount == 0) {
children.append(folder)
}
}
} }
var updatedChildNodes = [Node]() var updatedChildNodes = [Node]()
@ -77,13 +96,14 @@ private extension WebFeedTreeControllerDelegate {
} }
func createNode(representedObject: Any, parent: Node) -> Node? { func createNode(representedObject: Any, parent: Node) -> Node? {
if let webFeed = representedObject as? WebFeed { if let webFeed = representedObject as? WebFeed {
return createNode(webFeed: webFeed, parent: parent) return createNode(webFeed: webFeed, parent: parent)
} }
if let folder = representedObject as? Folder { if let folder = representedObject as? Folder {
return createNode(folder: folder, parent: parent) return createNode(folder: folder, parent: parent)
} }
if let account = representedObject as? Account { if let account = representedObject as? Account {
return createNode(account: account, parent: parent) return createNode(account: account, parent: parent)
} }
@ -92,19 +112,16 @@ private extension WebFeedTreeControllerDelegate {
} }
func createNode(webFeed: WebFeed, parent: Node) -> Node { func createNode(webFeed: WebFeed, parent: Node) -> Node {
return parent.createChildNode(webFeed) return parent.createChildNode(webFeed)
} }
func createNode(folder: Folder, parent: Node) -> Node { func createNode(folder: Folder, parent: Node) -> Node {
let node = parent.createChildNode(folder) let node = parent.createChildNode(folder)
node.canHaveChildNodes = true node.canHaveChildNodes = true
return node return node
} }
func createNode(account: Account, parent: Node) -> Node { func createNode(account: Account, parent: Node) -> Node {
let node = parent.createChildNode(account) let node = parent.createChildNode(account)
node.canHaveChildNodes = true node.canHaveChildNodes = true
node.isGroupItem = true node.isGroupItem = true
@ -112,8 +129,10 @@ private extension WebFeedTreeControllerDelegate {
} }
func sortedAccountNodes(_ parent: Node) -> [Node] { func sortedAccountNodes(_ parent: Node) -> [Node] {
let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in
let nodes = AccountManager.shared.sortedActiveAccounts.map { (account) -> Node in if isReadFiltered && account.unreadCount == 0 {
return nil
}
let accountNode = parent.existingOrNewChildNode(with: account) let accountNode = parent.existingOrNewChildNode(with: account)
accountNode.canHaveChildNodes = true accountNode.canHaveChildNodes = true
accountNode.isGroupItem = true accountNode.isGroupItem = true
@ -123,7 +142,6 @@ private extension WebFeedTreeControllerDelegate {
} }
func nodeInArrayRepresentingObject(_ nodes: [Node], _ representedObject: AnyObject) -> Node? { func nodeInArrayRepresentingObject(_ nodes: [Node], _ representedObject: AnyObject) -> Node? {
for oneNode in nodes { for oneNode in nodes {
if oneNode.representedObject === representedObject { if oneNode.representedObject === representedObject {
return oneNode return oneNode

View File

@ -44,7 +44,7 @@ class FeedbinAccountViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

View File

@ -36,7 +36,7 @@ class LocalAccountViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

View File

@ -89,6 +89,14 @@ struct AppAssets {
return RSImage(named: "faviconTemplateImage")! return RSImage(named: "faviconTemplateImage")!
}() }()
static var filterInactiveImage: UIImage = {
UIImage(systemName: "line.horizontal.3.decrease.circle")!
}()
static var filterActiveImage: UIImage = {
UIImage(systemName: "line.horizontal.3.decrease.circle.fill")!
}()
static var fullScreenBackgroundColor: UIColor = { static var fullScreenBackgroundColor: UIColor = {
return UIColor(named: "fullScreenBackgroundColor")! return UIColor(named: "fullScreenBackgroundColor")!
}() }()

View File

@ -23,6 +23,7 @@ struct AppDefaults {
static let timelineNumberOfLines = "timelineNumberOfLines" static let timelineNumberOfLines = "timelineNumberOfLines"
static let timelineIconSize = "timelineIconSize" static let timelineIconSize = "timelineIconSize"
static let timelineSortDirection = "timelineSortDirection" static let timelineSortDirection = "timelineSortDirection"
static let articleFullscreenEnabled = "articleFullscreenEnabled"
static let displayUndoAvailableTip = "displayUndoAvailableTip" static let displayUndoAvailableTip = "displayUndoAvailableTip"
static let lastRefresh = "lastRefresh" static let lastRefresh = "lastRefresh"
static let addWebFeedAccountID = "addWebFeedAccountID" static let addWebFeedAccountID = "addWebFeedAccountID"
@ -92,6 +93,15 @@ struct AppDefaults {
} }
} }
static var articleFullscreenEnabled: Bool {
get {
return bool(for: Key.articleFullscreenEnabled)
}
set {
setBool(for: Key.articleFullscreenEnabled, newValue)
}
}
static var displayUndoAvailableTip: Bool { static var displayUndoAvailableTip: Bool {
get { get {
return bool(for: Key.displayUndoAvailableTip) return bool(for: Key.displayUndoAvailableTip)
@ -135,6 +145,7 @@ struct AppDefaults {
Key.timelineNumberOfLines: 2, Key.timelineNumberOfLines: 2,
Key.timelineIconSize: IconSize.medium.rawValue, Key.timelineIconSize: IconSize.medium.rawValue,
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
Key.articleFullscreenEnabled: false,
Key.displayUndoAvailableTip: true] Key.displayUndoAvailableTip: true]
AppDefaults.shared.register(defaults: defaults) AppDefaults.shared.register(defaults: defaults)
} }

View File

@ -36,6 +36,8 @@ class ArticleViewController: UIViewController {
@IBOutlet private weak var webViewContainer: UIView! @IBOutlet private weak var webViewContainer: UIView!
@IBOutlet private weak var showNavigationView: UIView! @IBOutlet private weak var showNavigationView: UIView!
@IBOutlet private weak var showToolbarView: UIView! @IBOutlet private weak var showToolbarView: UIView!
@IBOutlet private weak var showNavigationViewConstraint: NSLayoutConstraint!
@IBOutlet private weak var showToolbarViewConstraint: NSLayoutConstraint!
private var articleExtractorButton: ArticleExtractorButton = { private var articleExtractorButton: ArticleExtractorButton = {
let button = ArticleExtractorButton(type: .system) let button = ArticleExtractorButton(type: .system)
@ -59,6 +61,8 @@ class ArticleViewController: UIViewController {
} }
} }
var restoreOffset = 0
var currentArticle: Article? { var currentArticle: Article? {
switch state { switch state {
case .article(let article): case .article(let article):
@ -131,11 +135,23 @@ class ArticleViewController: UIViewController {
} }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if AppDefaults.articleFullscreenEnabled {
hideBars()
}
}
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
coordinator.isArticleViewControllerPending = false coordinator.isArticleViewControllerPending = false
} }
override func viewSafeAreaInsetsDidChange() {
// This will animate if the show/hide bars animation is happening.
view.layoutIfNeeded()
}
func updateUI() { func updateUI() {
guard let article = currentArticle else { guard let article = currentArticle else {
@ -190,9 +206,11 @@ class ArticleViewController: UIViewController {
var render = "error();" var render = "error();"
if let data = try? encoder.encode(templateData) { if let data = try? encoder.encode(templateData) {
let json = String(data: data, encoding: .utf8)! let json = String(data: data, encoding: .utf8)!
render = "render(\(json));" render = "render(\(json), \(restoreOffset));"
} }
restoreOffset = 0
ArticleViewControllerWebViewProvider.shared.articleIconSchemeHandler.currentArticle = currentArticle ArticleViewControllerWebViewProvider.shared.articleIconSchemeHandler.currentArticle = currentArticle
webView?.scrollView.setZoomScale(1.0, animated: false) webView?.scrollView.setZoomScale(1.0, animated: false)
webView?.evaluateJavaScript(render) webView?.evaluateJavaScript(render)
@ -231,7 +249,10 @@ class ArticleViewController: UIViewController {
} }
@objc func willEnterForeground(_ note: Notification) { @objc func willEnterForeground(_ note: Notification) {
showBars() // The toolbar will come back on you if you don't hide it again
if AppDefaults.articleFullscreenEnabled {
hideBars()
}
} }
// MARK: Actions // MARK: Actions
@ -319,6 +340,21 @@ class ArticleViewController: UIViewController {
webView?.evaluateJavaScript("showClickedImage();") webView?.evaluateJavaScript("showClickedImage();")
} }
func fullReload() {
if let offset = webView?.scrollView.contentOffset.y {
restoreOffset = Int(offset)
webView?.reload()
}
}
}
// MARK: InteractiveNavigationControllerTappable
extension ArticleViewController: InteractiveNavigationControllerTappable {
func didTapNavigationBar() {
hideBars()
}
} }
// MARK: WKNavigationDelegate // MARK: WKNavigationDelegate
@ -357,14 +393,6 @@ extension ArticleViewController: WKNavigationDelegate {
} }
// MARK: InteractiveNavigationControllerTappable
extension ArticleViewController: InteractiveNavigationControllerTappable {
func didTapNavigationBar() {
hideBars()
}
}
// MARK: WKUIDelegate // MARK: WKUIDelegate
extension ArticleViewController: WKUIDelegate { extension ArticleViewController: WKUIDelegate {
@ -466,7 +494,10 @@ private extension ArticleViewController {
func showBars() { func showBars() {
if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed { if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed {
AppDefaults.articleFullscreenEnabled = false
coordinator.showStatusBar() coordinator.showStatusBar()
showNavigationViewConstraint.constant = 0
showToolbarViewConstraint.constant = 0
navigationController?.setNavigationBarHidden(false, animated: true) navigationController?.setNavigationBarHidden(false, animated: true)
navigationController?.setToolbarHidden(false, animated: true) navigationController?.setToolbarHidden(false, animated: true)
} }
@ -474,7 +505,10 @@ private extension ArticleViewController {
func hideBars() { func hideBars() {
if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed { if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed {
AppDefaults.articleFullscreenEnabled = true
coordinator.hideStatusBar() coordinator.hideStatusBar()
showNavigationViewConstraint.constant = 44.0
showToolbarViewConstraint.constant = 44.0
navigationController?.setNavigationBarHidden(true, animated: true) navigationController?.setNavigationBarHidden(true, animated: true)
navigationController?.setToolbarHidden(true, animated: true) navigationController?.setToolbarHidden(true, animated: true)
} }

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Cqo-6I-B1A"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Cqo-6I-B1A">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15508"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -70,7 +70,7 @@
</toolbarItems> </toolbarItems>
<navigationItem key="navigationItem" title="Feeds" largeTitleDisplayMode="never" id="lE1-xw-gjH"> <navigationItem key="navigationItem" title="Feeds" largeTitleDisplayMode="never" id="lE1-xw-gjH">
<barButtonItem key="leftBarButtonItem" title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/> <barButtonItem key="leftBarButtonItem" title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/>
<barButtonItem key="rightBarButtonItem" title="Edit" id="Khk-Hd-iNS"/> <barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="Khk-Hd-iNS"/>
</navigationItem> </navigationItem>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/> <simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
</tableViewController> </tableViewController>
@ -114,5 +114,6 @@
</scenes> </scenes>
<resources> <resources>
<image name="gear" catalog="system" width="64" height="58"/> <image name="gear" catalog="system" width="64" height="58"/>
<image name="line.horizontal.3.decrease.circle" catalog="system" width="64" height="60"/>
</resources> </resources>
</document> </document>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Cqo-6I-B1A"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Cqo-6I-B1A">
<device id="retina6_1" orientation="portrait" appearance="light"/> <device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15508"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
@ -70,7 +70,7 @@
</toolbarItems> </toolbarItems>
<navigationItem key="navigationItem" title="Feeds" largeTitleDisplayMode="always" id="lE1-xw-gjH"> <navigationItem key="navigationItem" title="Feeds" largeTitleDisplayMode="always" id="lE1-xw-gjH">
<barButtonItem key="leftBarButtonItem" title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/> <barButtonItem key="leftBarButtonItem" title="Item" image="gear" catalog="system" id="AK3-N5-4ke"/>
<barButtonItem key="rightBarButtonItem" title="Edit" id="Khk-Hd-iNS"/> <barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="Khk-Hd-iNS"/>
</navigationItem> </navigationItem>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/> <simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
</tableViewController> </tableViewController>
@ -114,5 +114,6 @@
</scenes> </scenes>
<resources> <resources>
<image name="gear" catalog="system" width="64" height="58"/> <image name="gear" catalog="system" width="64" height="58"/>
<image name="line.horizontal.3.decrease.circle" catalog="system" width="64" height="60"/>
</resources> </resources>
</document> </document>

View File

@ -21,14 +21,14 @@
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
</view> </view>
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iEi-hX-TYy"> <view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iEi-hX-TYy">
<rect key="frame" x="0.0" y="769" width="414" height="100"/> <rect key="frame" x="0.0" y="813" width="414" height="100"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="100" id="xX2-AK-xJX"/> <constraint firstAttribute="height" constant="100" id="xX2-AK-xJX"/>
</constraints> </constraints>
</view> </view>
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="A7j-8T-DqE"> <view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="A7j-8T-DqE">
<rect key="frame" x="0.0" y="32" width="414" height="100"/> <rect key="frame" x="0.0" y="-12" width="414" height="100"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints> <constraints>
<constraint firstAttribute="height" constant="100" id="3HX-Dm-bA6"/> <constraint firstAttribute="height" constant="100" id="3HX-Dm-bA6"/>
@ -37,14 +37,14 @@
</subviews> </subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints> <constraints>
<constraint firstItem="VUw-jc-0yf" firstAttribute="bottom" secondItem="iEi-hX-TYy" secondAttribute="top" constant="44" id="4fZ-pn-fmB"/> <constraint firstItem="VUw-jc-0yf" firstAttribute="bottom" secondItem="iEi-hX-TYy" secondAttribute="top" id="4fZ-pn-fmB"/>
<constraint firstItem="DNb-lt-KzC" firstAttribute="top" secondItem="VUw-jc-0yf" secondAttribute="top" id="Bfh-RL-m4d"/> <constraint firstItem="DNb-lt-KzC" firstAttribute="top" secondItem="VUw-jc-0yf" secondAttribute="top" id="Bfh-RL-m4d"/>
<constraint firstItem="A7j-8T-DqE" firstAttribute="trailing" secondItem="VUw-jc-0yf" secondAttribute="trailing" id="Feu-hj-K01"/> <constraint firstItem="A7j-8T-DqE" firstAttribute="trailing" secondItem="VUw-jc-0yf" secondAttribute="trailing" id="Feu-hj-K01"/>
<constraint firstItem="DNb-lt-KzC" firstAttribute="bottom" secondItem="VUw-jc-0yf" secondAttribute="bottom" id="FfW-6G-Bcp"/> <constraint firstItem="DNb-lt-KzC" firstAttribute="bottom" secondItem="VUw-jc-0yf" secondAttribute="bottom" id="FfW-6G-Bcp"/>
<constraint firstItem="VUw-jc-0yf" firstAttribute="trailing" secondItem="iEi-hX-TYy" secondAttribute="trailing" id="Ij6-ri-sBN"/> <constraint firstItem="VUw-jc-0yf" firstAttribute="trailing" secondItem="iEi-hX-TYy" secondAttribute="trailing" id="Ij6-ri-sBN"/>
<constraint firstItem="iEi-hX-TYy" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="Muc-gr-S7o"/> <constraint firstItem="iEi-hX-TYy" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="Muc-gr-S7o"/>
<constraint firstItem="DNb-lt-KzC" firstAttribute="trailing" secondItem="VUw-jc-0yf" secondAttribute="trailing" id="QJ5-Ne-ndd"/> <constraint firstItem="DNb-lt-KzC" firstAttribute="trailing" secondItem="VUw-jc-0yf" secondAttribute="trailing" id="QJ5-Ne-ndd"/>
<constraint firstItem="A7j-8T-DqE" firstAttribute="bottom" secondItem="VUw-jc-0yf" secondAttribute="top" constant="44" id="b2h-zZ-xwi"/> <constraint firstItem="A7j-8T-DqE" firstAttribute="bottom" secondItem="VUw-jc-0yf" secondAttribute="top" id="b2h-zZ-xwi"/>
<constraint firstItem="DNb-lt-KzC" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="ezE-0p-35X"/> <constraint firstItem="DNb-lt-KzC" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="ezE-0p-35X"/>
<constraint firstItem="A7j-8T-DqE" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="wny-M6-akA"/> <constraint firstItem="A7j-8T-DqE" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="wny-M6-akA"/>
</constraints> </constraints>
@ -121,7 +121,9 @@
<outlet property="prevArticleBarButtonItem" destination="v4j-fq-23N" id="Gny-Oh-cQa"/> <outlet property="prevArticleBarButtonItem" destination="v4j-fq-23N" id="Gny-Oh-cQa"/>
<outlet property="readBarButtonItem" destination="hy0-LS-MzE" id="BzM-x9-tuj"/> <outlet property="readBarButtonItem" destination="hy0-LS-MzE" id="BzM-x9-tuj"/>
<outlet property="showNavigationView" destination="A7j-8T-DqE" id="D59-3C-HmS"/> <outlet property="showNavigationView" destination="A7j-8T-DqE" id="D59-3C-HmS"/>
<outlet property="showNavigationViewConstraint" destination="b2h-zZ-xwi" id="CaG-8F-5kF"/>
<outlet property="showToolbarView" destination="iEi-hX-TYy" id="zoa-h3-H8b"/> <outlet property="showToolbarView" destination="iEi-hX-TYy" id="zoa-h3-H8b"/>
<outlet property="showToolbarViewConstraint" destination="4fZ-pn-fmB" id="ayD-Mq-kft"/>
<outlet property="starBarButtonItem" destination="wU4-eH-wC9" id="Z8Q-Lt-dKk"/> <outlet property="starBarButtonItem" destination="wU4-eH-wC9" id="Z8Q-Lt-dKk"/>
<outlet property="webViewContainer" destination="DNb-lt-KzC" id="Fc1-Ae-pWK"/> <outlet property="webViewContainer" destination="DNb-lt-KzC" id="Fc1-Ae-pWK"/>
</connections> </connections>
@ -167,10 +169,17 @@
</connections> </connections>
</barButtonItem> </barButtonItem>
</toolbarItems> </toolbarItems>
<navigationItem key="navigationItem" title="Timeline" largeTitleDisplayMode="never" id="wcC-1L-ug4"/> <navigationItem key="navigationItem" title="Timeline" largeTitleDisplayMode="never" id="wcC-1L-ug4">
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="af2-lj-EcA">
<connections>
<action selector="toggleFilter:" destination="Kyk-vK-QRX" id="jxP-b2-V1n"/>
</connections>
</barButtonItem>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" translucent="NO" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" translucent="NO" prompted="NO"/>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics" translucent="NO"/> <simulatedToolbarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
<connections> <connections>
<outlet property="filterButton" destination="af2-lj-EcA" id="uGR-n0-YKf"/>
<outlet property="firstUnreadButton" destination="2v2-jD-C9k" id="8NP-Uc-3Fn"/> <outlet property="firstUnreadButton" destination="2v2-jD-C9k" id="8NP-Uc-3Fn"/>
<outlet property="markAllAsReadButton" destination="fTv-eX-72r" id="12S-lN-Sxa"/> <outlet property="markAllAsReadButton" destination="fTv-eX-72r" id="12S-lN-Sxa"/>
</connections> </connections>
@ -214,9 +223,17 @@
<action selector="settings:" destination="7bK-jq-Zjz" id="Y8a-lz-Im7"/> <action selector="settings:" destination="7bK-jq-Zjz" id="Y8a-lz-Im7"/>
</connections> </connections>
</barButtonItem> </barButtonItem>
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="ZJu-oJ-c1R">
<connections>
<action selector="toggleFilter:" destination="7bK-jq-Zjz" id="7lh-Bz-nfD"/>
</connections>
</barButtonItem>
</navigationItem> </navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/> <simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="filterButton" destination="ZJu-oJ-c1R" id="jiO-wg-qrG"/>
</connections>
</tableViewController> </tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
</objects> </objects>
@ -289,6 +306,7 @@
<image name="chevron.up" catalog="system" width="64" height="36"/> <image name="chevron.up" catalog="system" width="64" height="36"/>
<image name="circle" catalog="system" width="64" height="60"/> <image name="circle" catalog="system" width="64" height="60"/>
<image name="gear" catalog="system" width="64" height="58"/> <image name="gear" catalog="system" width="64" height="58"/>
<image name="line.horizontal.3.decrease.circle" catalog="system" width="64" height="60"/>
<image name="multiply.circle.fill" catalog="system" width="64" height="60"/> <image name="multiply.circle.fill" catalog="system" width="64" height="60"/>
<image name="square.and.arrow.up" catalog="system" width="56" height="64"/> <image name="square.and.arrow.up" catalog="system" width="56" height="64"/>
<image name="square.and.arrow.up.fill" catalog="system" width="56" height="64"/> <image name="square.and.arrow.up.fill" catalog="system" width="56" height="64"/>

View File

@ -128,7 +128,7 @@ extension AccountInspectorViewController {
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

View File

@ -78,7 +78,7 @@ class WebFeedInspectorViewController: UITableViewController {
extension WebFeedInspectorViewController { extension WebFeedInspectorViewController {
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? 64.0 : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

View File

@ -98,7 +98,7 @@ struct MasterFeedTableViewCellLayout {
} }
} }
let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height // Determine cell height
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding) let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding)
@ -117,6 +117,12 @@ struct MasterFeedTableViewCellLayout {
rDisclosure = MasterFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds) rDisclosure = MasterFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds)
} }
// Small fonts and the Favicon need centered if we hit the minimum row height
if cellHeight == MasterFeedTableViewCellLayout.minRowHeight {
rLabel = MasterFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
rFavicon = MasterFeedTableViewCellLayout.centerVertically(rFavicon, newBounds)
}
// Separator Insets // Separator Insets
let separatorInset = MasterFeedTableViewCellLayout.disclosureButtonSize.width let separatorInset = MasterFeedTableViewCellLayout.disclosureButtonSize.width
separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5) separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5)

View File

@ -57,7 +57,7 @@ struct MasterFeedTableViewSectionHeaderLayout {
labelWidth = cellWidth - (rLabelx + MasterFeedTableViewSectionHeaderLayout.labelMarginRight + maxUnreadCountSize.width + MasterFeedTableViewSectionHeaderLayout.unreadCountMarginRight) labelWidth = cellWidth - (rLabelx + MasterFeedTableViewSectionHeaderLayout.labelMarginRight + maxUnreadCountSize.width + MasterFeedTableViewSectionHeaderLayout.unreadCountMarginRight)
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height // Determine cell height
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewSectionHeaderLayout.verticalPadding) let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewSectionHeaderLayout.verticalPadding)
@ -74,6 +74,11 @@ struct MasterFeedTableViewSectionHeaderLayout {
} }
rDisclosure = MasterFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds) rDisclosure = MasterFeedTableViewCellLayout.centerVertically(rDisclosure, newBounds)
// Small fonts need centered if we hit the minimum row height
if cellHeight == MasterFeedTableViewSectionHeaderLayout.minRowHeight {
rLabel = MasterFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
}
// Assign the properties // Assign the properties
self.height = cellHeight self.height = cellHeight
self.unreadCountRect = rUnread self.unreadCountRect = rUnread

View File

@ -13,16 +13,6 @@ import Account
class MasterFeedDataSource: UITableViewDiffableDataSource<Node, Node> { class MasterFeedDataSource: UITableViewDiffableDataSource<Node, Node> {
private var coordinator: SceneCoordinator!
private var errorHandler: ((Error) -> ())!
init(coordinator: SceneCoordinator, errorHandler: @escaping (Error) -> (), tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<Node, Node>.CellProvider) {
super.init(tableView: tableView, cellProvider: cellProvider)
self.coordinator = coordinator
self.errorHandler = errorHandler
self.defaultRowAnimation = .middle
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
guard let node = itemIdentifier(for: indexPath), !(node.representedObject is PseudoFeed) else { guard let node = itemIdentifier(for: indexPath), !(node.representedObject is PseudoFeed) else {
return false return false
@ -30,140 +20,4 @@ class MasterFeedDataSource: UITableViewDiffableDataSource<Node, Node> {
return true return true
} }
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
guard let node = itemIdentifier(for: indexPath) else {
return false
}
return node.representedObject is WebFeed
}
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
guard let sourceNode = itemIdentifier(for: sourceIndexPath), let webFeed = sourceNode.representedObject as? WebFeed else {
return
}
// Based on the drop we have to determine a node to start looking for a parent container.
let destNode: Node = {
if destinationIndexPath.row == 0 {
return coordinator.rootNode.childAtIndex(destinationIndexPath.section)!
} else {
let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0
let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section)
return itemIdentifier(for: adjustedDestIndexPath)!
}
}()
// Now we start looking for the parent container
let destParentNode: Node? = {
if destNode.representedObject is Container {
return destNode
} else {
if destNode.parent?.representedObject is Container {
return destNode.parent!
} else {
return nil
}
}
}()
// Move the Web Feed
guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else {
return
}
if sameAccount(sourceNode, destParentNode!) {
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
} else {
moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
}
}
private func sameAccount(_ node: Node, _ parentNode: Node) -> Bool {
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
if accountID == parentAccountID {
return true
}
}
return false
}
private func nodeAccount(_ node: Node) -> Account? {
if let account = node.representedObject as? Account {
return account
} else if let folder = node.representedObject as? Folder {
return folder.account
} else if let webFeed = node.representedObject as? WebFeed {
return webFeed.account
} else {
return nil
}
}
private func nodeAccountID(_ node: Node) -> String? {
return nodeAccount(node)?.accountID
}
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
BatchUpdate.shared.start()
sourceContainer.account?.moveWebFeed(feed, from: sourceContainer, to: destinationContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.errorHandler(error)
}
}
}
func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {
BatchUpdate.shared.start()
destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in
switch result {
case .success:
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.errorHandler(error)
}
}
case .failure(let error):
BatchUpdate.shared.end()
self.errorHandler(error)
}
}
} else {
BatchUpdate.shared.start()
destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
switch result {
case .success:
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.errorHandler(error)
}
}
case .failure(let error):
BatchUpdate.shared.end()
self.errorHandler(error)
}
}
}
}
} }

View File

@ -0,0 +1,33 @@
//
// MasterFeedViewController+Drag.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 11/20/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import MobileCoreServices
import Account
extension MasterFeedViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let node = dataSource.itemIdentifier(for: indexPath), let webFeed = node.representedObject as? WebFeed else {
return [UIDragItem]()
}
let data = webFeed.url.data(using: .utf8)
let itemProvider = NSItemProvider()
itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .ownProcess) { completion in
completion(data, nil)
return nil
}
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = node
return [dragItem]
}
}

View File

@ -0,0 +1,195 @@
//
// MasterFeedViewController+Drop.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 11/20/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
import Account
import RSTree
extension MasterFeedViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
return session.localDragSession != nil
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
guard let destIndexPath = destinationIndexPath,
destIndexPath.section > 0,
tableView.hasActiveDrag,
let destNode = dataSource.itemIdentifier(for: destIndexPath),
let destCell = tableView.cellForRow(at: destIndexPath) else {
return UITableViewDropProposal(operation: .forbidden)
}
if destNode.representedObject is Folder {
if session.location(in: destCell).y >= 0 {
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
} else {
return UITableViewDropProposal(operation: .move, intent: .unspecified)
}
} else {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
}
func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) {
guard let dragItem = dropCoordinator.items.first?.dragItem,
let sourceNode = dragItem.localObject as? Node,
let webFeed = sourceNode.representedObject as? WebFeed,
let destIndexPath = dropCoordinator.destinationIndexPath else {
return
}
let isFolderDrop: Bool = {
if let propDestNode = dataSource.itemIdentifier(for: destIndexPath), let propCell = tableView.cellForRow(at: destIndexPath) {
return propDestNode.representedObject is Folder && dropCoordinator.session.location(in: propCell).y >= 0
}
return false
}()
// Based on the drop we have to determine a node to start looking for a parent container.
let destNode: Node? = {
if destIndexPath.row == 0 {
return coordinator.rootNode.childAtIndex(destIndexPath.section)!
} else {
if isFolderDrop {
return dataSource.itemIdentifier(for: destIndexPath)
} else {
if destIndexPath.row > 0 {
return dataSource.itemIdentifier(for: IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section))
} else {
return nil
}
}
}
}()
// Now we start looking for the parent container
let destParentNode: Node? = {
if destNode?.representedObject is Container {
return destNode
} else {
if destNode?.parent?.representedObject is Container {
return destNode!.parent!
} else {
return nil
}
}
}()
// Move the Web Feed
guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else {
return
}
if sameAccount(sourceNode, destParentNode!) {
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
} else {
moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
}
}
private func sameAccount(_ node: Node, _ parentNode: Node) -> Bool {
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
if accountID == parentAccountID {
return true
}
}
return false
}
private func nodeAccount(_ node: Node) -> Account? {
if let account = node.representedObject as? Account {
return account
} else if let folder = node.representedObject as? Folder {
return folder.account
} else if let webFeed = node.representedObject as? WebFeed {
return webFeed.account
} else {
return nil
}
}
private func nodeAccountID(_ node: Node) -> String? {
return nodeAccount(node)?.accountID
}
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
guard sourceContainer !== destinationContainer else { return }
BatchUpdate.shared.start()
sourceContainer.account?.moveWebFeed(feed, from: sourceContainer, to: destinationContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.presentError(error)
}
}
}
func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {
BatchUpdate.shared.start()
destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in
switch result {
case .success:
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.presentError(error)
}
}
case .failure(let error):
BatchUpdate.shared.end()
self.presentError(error)
}
}
} else {
BatchUpdate.shared.start()
destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
switch result {
case .success:
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.presentError(error)
}
}
case .failure(let error):
BatchUpdate.shared.end()
self.presentError(error)
}
}
}
}
}

View File

@ -14,10 +14,11 @@ import RSTree
class MasterFeedViewController: UITableViewController, UndoableCommandRunner { class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
@IBOutlet weak var filterButton: UIBarButtonItem!
private var refreshProgressView: RefreshProgressView? private var refreshProgressView: RefreshProgressView?
private var addNewItemButton: UIBarButtonItem! private var addNewItemButton: UIBarButtonItem!
private lazy var dataSource = makeDataSource() lazy var dataSource = makeDataSource()
var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
weak var coordinator: SceneCoordinator! weak var coordinator: SceneCoordinator!
@ -38,11 +39,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
navigationController?.navigationBar.prefersLargeTitles = true navigationController?.navigationBar.prefersLargeTitles = true
} }
navigationItem.rightBarButtonItem = editButtonItem
// Set the bar button item so that it doesn't show on the timeline view
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
// If you don't have an empty table header, UIKit tries to help out by putting one in for you // If you don't have an empty table header, UIKit tries to help out by putting one in for you
// that makes a gap between the first section header and the navigation bar // that makes a gap between the first section header and the navigation bar
var frame = CGRect.zero var frame = CGRect.zero
@ -51,6 +47,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
tableView.dataSource = dataSource tableView.dataSource = dataSource
tableView.dragDelegate = self
tableView.dropDelegate = self
tableView.dragInteractionEnabled = true
resetEstimatedRowHeight() resetEstimatedRowHeight()
tableView.separatorStyle = .none tableView.separatorStyle = .none
@ -192,7 +191,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
} }
headerView.tag = section headerView.tag = section
headerView.disclosureExpanded = sectionNode.isExpanded headerView.disclosureExpanded = coordinator.isExpanded(sectionNode)
if section == tableView.numberOfSections - 1 { if section == tableView.numberOfSections - 1 {
headerView.isLastSection = true headerView.isLastSection = true
@ -292,12 +291,23 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return nil return nil
} }
if node.representedObject is WebFeed { if node.representedObject is WebFeed {
return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true) return makeFeedContextMenu(node: node, indexPath: indexPath, includeDeleteRename: true)
} else { } else {
return makeFolderContextMenu(indexPath: indexPath) return makeFolderContextMenu(node: node, indexPath: indexPath)
} }
} }
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let nodeUniqueId = configuration.identifier as? Int,
let node = coordinator.rootNode.descendantNode(where: { $0.uniqueID == nodeUniqueId }),
let indexPath = dataSource.indexPath(for: node),
let cell = tableView.cellForRow(at: indexPath) else {
return nil
}
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
becomeFirstResponder() becomeFirstResponder()
coordinator.selectFeed(indexPath, animated: true) coordinator.selectFeed(indexPath, animated: true)
@ -324,9 +334,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
} }
// If this is a folder and isn't expanded or doesn't have any entries, let the users drop on it // If this is a folder and isn't expanded or doesn't have any entries, let the users drop on it
if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !destNode.isExpanded) { if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !coordinator.isExpanded(destNode)) {
let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0 return proposedDestinationIndexPath
return IndexPath(row: destIndexPath.row + movementAdjustment, section: destIndexPath.section)
} }
// If we are dragging around in the same container, just return the original source // If we are dragging around in the same container, just return the original source
@ -352,13 +361,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
sortedNodes.remove(at: index) sortedNodes.remove(at: index)
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0 if index >= sortedNodes.count {
let adjustedIndex = index - movementAdjustment
if adjustedIndex >= sortedNodes.count {
let lastSortedIndexPath = dataSource.indexPath(for: sortedNodes[sortedNodes.count - 1])! let lastSortedIndexPath = dataSource.indexPath(for: sortedNodes[sortedNodes.count - 1])!
return IndexPath(row: lastSortedIndexPath.row + 1, section: lastSortedIndexPath.section) let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0
return IndexPath(row: lastSortedIndexPath.row + movementAdjustment, section: lastSortedIndexPath.section)
} else { } else {
return dataSource.indexPath(for: sortedNodes[adjustedIndex])! let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
return dataSource.indexPath(for: sortedNodes[index - movementAdjustment])!
} }
} }
@ -371,6 +380,16 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
coordinator.showSettings() coordinator.showSettings()
} }
@IBAction func toggleFilter(_ sender: Any) {
if coordinator.isUnreadFeedsFiltered {
filterButton.image = AppAssets.filterInactiveImage
coordinator.showAllFeeds()
} else {
filterButton.image = AppAssets.filterActiveImage
coordinator.hideUnreadFeeds()
}
}
@IBAction func add(_ sender: UIBarButtonItem) { @IBAction func add(_ sender: UIBarButtonItem) {
coordinator.showAdd(.feed) coordinator.showAdd(.feed)
} }
@ -384,7 +403,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return return
} }
if sectionNode.isExpanded { if coordinator.isExpanded(sectionNode) {
headerView.disclosureExpanded = false headerView.disclosureExpanded = false
coordinator.collapse(sectionNode) coordinator.collapse(sectionNode)
self.applyChanges(animated: true) self.applyChanges(animated: true)
@ -501,7 +520,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return return
} }
if !sectionNode.isExpanded { if !coordinator.isExpanded(sectionNode) {
coordinator.expand(sectionNode) coordinator.expand(sectionNode)
self.applyChanges(animated: true) { self.applyChanges(animated: true) {
completion?() completion?()
@ -561,12 +580,22 @@ extension MasterFeedViewController: UIContextMenuInteractionDelegate {
return nil return nil
} }
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { suggestedActions in return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { suggestedActions in
let accountInfoAction = self.getAccountInfoAction(account: account) let accountInfoAction = self.getAccountInfoAction(account: account)
let deactivateAction = self.deactivateAccountAction(account: account) let deactivateAction = self.deactivateAccountAction(account: account)
return UIMenu(title: "", children: [accountInfoAction, deactivateAction]) return UIMenu(title: "", children: [accountInfoAction, deactivateAction])
} }
} }
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let sectionIndex = configuration.identifier as? Int,
let cell = tableView.headerView(forSection: sectionIndex) else {
return nil
}
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
}
} }
// MARK: MasterTableViewCellDelegate // MARK: MasterTableViewCellDelegate
@ -630,11 +659,13 @@ private extension MasterFeedViewController {
} }
func makeDataSource() -> UITableViewDiffableDataSource<Node, Node> { func makeDataSource() -> UITableViewDiffableDataSource<Node, Node> {
return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in let dataSource = MasterFeedDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
self?.configure(cell, node) self?.configure(cell, node)
return cell return cell
}) })
dataSource.defaultRowAnimation = .middle
return dataSource
} }
func resetEstimatedRowHeight() { func resetEstimatedRowHeight() {
@ -656,7 +687,7 @@ private extension MasterFeedViewController {
} else { } else {
cell.indentationLevel = 1 cell.indentationLevel = 1
} }
cell.setDisclosure(isExpanded: node.isExpanded, animated: false) cell.setDisclosure(isExpanded: coordinator.isExpanded(node), animated: false)
cell.isDisclosureAvailable = node.canHaveChildNodes cell.isDisclosureAvailable = node.canHaveChildNodes
cell.name = nameFor(node) cell.name = nameFor(node)
@ -772,8 +803,8 @@ private extension MasterFeedViewController {
} }
} }
func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { func makeFeedContextMenu(node: Node, indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in return UIContextMenuConfiguration(identifier: node.uniqueID as NSCopying, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
guard let self = self else { return nil } guard let self = self else { return nil }
@ -806,8 +837,8 @@ private extension MasterFeedViewController {
} }
func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration { func makeFolderContextMenu(node: Node, indexPath: IndexPath) -> UIContextMenuConfiguration {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] suggestedActions in return UIContextMenuConfiguration(identifier: node.uniqueID as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in
guard let self = self else { return nil } guard let self = self else { return nil }

View File

@ -10,13 +10,6 @@ import UIKit
class MasterTimelineDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { class MasterTimelineDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {
private var coordinator: SceneCoordinator!
init(coordinator: SceneCoordinator, tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider) {
super.init(tableView: tableView, cellProvider: cellProvider)
self.coordinator = coordinator
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true return true
} }

View File

@ -13,10 +13,11 @@ import Articles
class MasterTimelineViewController: UITableViewController, UndoableCommandRunner { class MasterTimelineViewController: UITableViewController, UndoableCommandRunner {
private var titleView: MasterTimelineTitleView?
private var numberOfTextLines = 0 private var numberOfTextLines = 0
private var iconSize = IconSize.medium private var iconSize = IconSize.medium
private lazy var feedTapGestureRecognizer = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:)))
@IBOutlet weak var filterButton: UIBarButtonItem!
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem! @IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
@IBOutlet weak var firstUnreadButton: UIBarButtonItem! @IBOutlet weak var firstUnreadButton: UIBarButtonItem!
@ -68,12 +69,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
iconSize = AppDefaults.timelineIconSize iconSize = AppDefaults.timelineIconSize
resetEstimatedRowHeight() resetEstimatedRowHeight()
if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView {
navigationItem.titleView = titleView
}
resetUI() resetUI()
applyChanges(animated: false) applyChanges(animated: false)
// Set the bar button item so that it doesn't show on the article view
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
// Restore the scroll position if we have one stored // Restore the scroll position if we have one stored
if let restoreIndexPath = coordinator.timelineMiddleIndexPath { if let restoreIndexPath = coordinator.timelineMiddleIndexPath {
tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false) tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false)
@ -81,12 +83,37 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
override func viewWillAppear(_ animated: Bool) {
// If the nav bar is hidden, fade it in to avoid it showing stuff as it is getting laid out
if navigationController?.navigationBar.isHidden ?? false {
navigationController?.navigationBar.alpha = 0
}
}
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
coordinator.isTimelineViewControllerPending = false coordinator.isTimelineViewControllerPending = false
if navigationController?.navigationBar.alpha == 0 {
UIView.animate(withDuration: 0.5) {
self.navigationController?.navigationBar.alpha = 1
}
}
} }
// MARK: Actions // MARK: Actions
@IBAction func toggleFilter(_ sender: Any) {
switch coordinator.articleReadFilterType {
case .none:
filterButton.image = AppAssets.filterActiveImage
coordinator.hideUnreadArticles()
case .read:
filterButton.image = AppAssets.filterInactiveImage
coordinator.showAllArticles()
case .alwaysRead:
break
}
}
@IBAction func markAllAsRead(_ sender: Any) { @IBAction func markAllAsRead(_ sender: Any) {
if coordinator.displayUndoAvailableTip { if coordinator.displayUndoAvailableTip {
@ -129,10 +156,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// MARK: API // MARK: API
func restoreTimelinePosition() {
}
func restoreSelectionIfNecessary(adjustScroll: Bool) { func restoreSelectionIfNecessary(adjustScroll: Bool) {
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
if adjustScroll { if adjustScroll {
@ -263,7 +286,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil } guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] suggestedActions in return UIContextMenuConfiguration(identifier: indexPath.row as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in
guard let self = self else { return nil } guard let self = self else { return nil }
@ -294,6 +317,15 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
guard let row = configuration.identifier as? Int,
let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0)) else {
return nil
}
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
becomeFirstResponder() becomeFirstResponder()
let article = dataSource.itemIdentifier(for: indexPath) let article = dataSource.itemIdentifier(for: indexPath)
@ -328,7 +360,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) { @objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
titleView?.iconView.iconImage = coordinator.timelineIconImage
if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
titleView.iconView.iconImage = coordinator.timelineIconImage
}
guard let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else { guard let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
return return
} }
@ -359,7 +395,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
@objc func faviconDidBecomeAvailable(_ note: Notification) { @objc func faviconDidBecomeAvailable(_ note: Notification) {
titleView?.iconView.iconImage = coordinator.timelineIconImage if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
titleView.iconView.iconImage = coordinator.timelineIconImage
}
if coordinator.showIcons { if coordinator.showIcons {
queueReloadAvailableCells() queueReloadAvailableCells()
} }
@ -379,7 +417,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} }
@objc func displayNameDidChange(_ note: Notification) { @objc func displayNameDidChange(_ note: Notification) {
titleView?.label.text = coordinator.timelineFeed?.nameForDisplay if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
}
} }
@objc func scrollPositionDidChange() { @objc func scrollPositionDidChange() {
@ -466,24 +506,34 @@ extension MasterTimelineViewController: UISearchBarDelegate {
private extension MasterTimelineViewController { private extension MasterTimelineViewController {
func resetUI() { func resetUI() {
title = coordinator.timelineFeed?.nameForDisplay
if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView { title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline"
self.titleView = titleView
if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
titleView.iconView.iconImage = coordinator.timelineIconImage titleView.iconView.iconImage = coordinator.timelineIconImage
titleView.label.text = coordinator.timelineFeed?.nameForDisplay titleView.label.text = coordinator.timelineFeed?.nameForDisplay
updateTitleUnreadCount() updateTitleUnreadCount()
if coordinator.timelineFeed is WebFeed { if coordinator.timelineFeed is WebFeed {
titleView.heightAnchor.constraint(equalToConstant: 44.0).isActive = true titleView.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
let tap = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:))) titleView.addGestureRecognizer(feedTapGestureRecognizer)
titleView.addGestureRecognizer(tap) } else {
titleView.removeGestureRecognizer(feedTapGestureRecognizer)
} }
navigationItem.titleView = titleView navigationItem.titleView = titleView
} }
switch coordinator.articleReadFilterType {
case .none:
filterButton.isHidden = false
filterButton.image = AppAssets.filterInactiveImage
case .read:
filterButton.isHidden = false
filterButton.image = AppAssets.filterActiveImage
case .alwaysRead:
filterButton.isHidden = true
}
tableView.selectRow(at: nil, animated: false, scrollPosition: .top) tableView.selectRow(at: nil, animated: false, scrollPosition: .top)
if dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 { if dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 {
@ -505,7 +555,9 @@ private extension MasterTimelineViewController {
} }
func updateTitleUnreadCount() { func updateTitleUnreadCount() {
self.titleView?.unreadCountView.unreadCount = coordinator.unreadCount if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
titleView.unreadCountView.unreadCount = coordinator.unreadCount
}
} }
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) { func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
@ -521,12 +573,12 @@ private extension MasterTimelineViewController {
func makeDataSource() -> UITableViewDiffableDataSource<Int, Article> { func makeDataSource() -> UITableViewDiffableDataSource<Int, Article> {
let dataSource: UITableViewDiffableDataSource<Int, Article> = let dataSource: UITableViewDiffableDataSource<Int, Article> =
MasterTimelineDataSource(coordinator: coordinator, tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in MasterTimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell
self?.configure(cell, article: article) self?.configure(cell, article: article)
return cell return cell
}) })
dataSource.defaultRowAnimation = .left dataSource.defaultRowAnimation = .middle
return dataSource return dataSource
} }

View File

@ -67,6 +67,7 @@ body .headerTable {
border-bottom: 1px solid var(--header-table-border-color); border-bottom: 1px solid var(--header-table-border-color);
} }
body .header { body .header {
font: -apple-system-body;
color: var(--header-color); color: var(--header-color);
} }
body .header a:link, body .header a:visited { body .header a:link, body .header a:visited {

View File

@ -22,9 +22,7 @@ class RootSplitViewController: UISplitViewController {
} }
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UIApplication.shared.applicationState != .background { self.coordinator.configurePanelMode(for: size)
self.coordinator.configureThreePanelMode(for: size)
}
super.viewWillTransition(to: size, with: coordinator) super.viewWillTransition(to: size, with: coordinator)
} }

View File

@ -13,6 +13,11 @@ import Articles
import RSCore import RSCore
import RSTree import RSTree
enum PanelMode {
case unset
case three
case standard
}
enum SearchScope: Int { enum SearchScope: Int {
case timeline = 0 case timeline = 0
case global = 1 case global = 1
@ -25,6 +30,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return rootSplitViewController.undoManager return rootSplitViewController.undoManager
} }
private var panelMode: PanelMode = .unset
private var activityManager = ActivityManager() private var activityManager = ActivityManager()
private var isShowingExtractedArticle = false private var isShowingExtractedArticle = false
@ -34,10 +41,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private var masterNavigationController: UINavigationController! private var masterNavigationController: UINavigationController!
private var masterFeedViewController: MasterFeedViewController! private var masterFeedViewController: MasterFeedViewController!
private var masterTimelineViewController: MasterTimelineViewController? private var masterTimelineViewController: MasterTimelineViewController?
private var subSplitViewController: UISplitViewController?
private var subSplitViewController: UISplitViewController? {
return rootSplitViewController.children.last as? UISplitViewController
}
private var articleViewController: ArticleViewController? { private var articleViewController: ArticleViewController? {
if let detail = masterNavigationController.viewControllers.last as? ArticleViewController { if let detail = masterNavigationController.viewControllers.last as? ArticleViewController {
@ -55,11 +59,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return nil return nil
} }
private var wasRootSplitViewControllerCollapsed = false
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
private var fetchSerialNumber = 0 private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue() private let fetchRequestQueue = FetchRequestQueue()
private var animatingChanges = false private var animatingChanges = false
private var expandedTable = Set<ContainerIdentifier>()
private var shadowTable = [[Node]]() private var shadowTable = [[Node]]()
private var lastSearchString = "" private var lastSearchString = ""
private var lastSearchScope: SearchScope? = nil private var lastSearchScope: SearchScope? = nil
@ -103,9 +110,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
var isThreePanelMode: Bool { var isThreePanelMode: Bool {
return subSplitViewController != nil return panelMode == .three
} }
var isUnreadFeedsFiltered: Bool {
return treeControllerDelegate.isReadFiltered
}
var articleReadFilterType: ReadFilterType = .none
var rootNode: Node { var rootNode: Node {
return treeController.rootNode return treeController.rootNode
} }
@ -257,8 +270,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
super.init() super.init()
for section in treeController.rootNode.childNodes { for sectionNode in treeController.rootNode.childNodes {
section.isExpanded = true markExpanded(sectionNode)
shadowTable.append([Node]()) shadowTable.append([Node]())
} }
@ -297,7 +310,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: true) let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: true)
rootSplitViewController.showDetailViewController(detailNavigationController, sender: self) rootSplitViewController.showDetailViewController(detailNavigationController, sender: self)
configureThreePanelMode(for: size) configurePanelMode(for: size)
return rootSplitViewController return rootSplitViewController
} }
@ -325,19 +338,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
handleReadArticle(userInfo) handleReadArticle(userInfo)
} }
func configureThreePanelMode(for size: CGSize) { func configurePanelMode(for size: CGSize) {
guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad && !rootSplitViewController.isCollapsed else { guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad else {
return return
} }
if (size.width / size.height) > 1.2 { if (size.width / size.height) > 1.2 {
if !isThreePanelMode { if panelMode == .unset || panelMode == .standard {
transitionToThreePanelMode() panelMode = .three
configureThreePanelMode()
} }
} else { } else {
if isThreePanelMode { if panelMode == .unset || panelMode == .three {
transitionFromThreePanelMode() panelMode = .standard
configureStandardPanelMode()
} }
} }
wasRootSplitViewControllerCollapsed = rootSplitViewController.isCollapsed
} }
func selectFirstUnreadInAllUnread() { func selectFirstUnreadInAllUnread() {
@ -363,7 +381,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
@objc func containerChildrenDidChange(_ note: Notification) { @objc func containerChildrenDidChange(_ note: Notification) {
if timelineFetcherContainsAnyPseudoFeed() || timelineFetcherContainsAnyFolder() { if timelineFetcherContainsAnyPseudoFeed() || timelineFetcherContainsAnyFolder() {
fetchAndReplaceArticlesAsync() {} refreshTimeline()
} }
rebuildBackingStores() rebuildBackingStores()
} }
@ -377,60 +395,58 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
@objc func accountStateDidChange(_ note: Notification) { @objc func accountStateDidChange(_ note: Notification) {
let rebuildAndExpand = {
guard let account = note.userInfo?[Account.UserInfoKey.account] as? Account else {
assertionFailure()
return
}
self.rebuildBackingStores() {
// If we are activating an account, then automatically expand it
if account.isActive, let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
node.isExpanded = true
}
}
}
if timelineFetcherContainsAnyPseudoFeed() { if timelineFetcherContainsAnyPseudoFeed() {
fetchAndReplaceArticlesAsync { fetchAndReplaceArticlesAsync(animated: true) {
rebuildAndExpand() self.masterTimelineViewController?.reinitializeArticles()
self.rebuildBackingStores()
} }
} else { } else {
rebuildAndExpand() rebuildBackingStores()
} }
} }
@objc func userDidAddAccount(_ note: Notification) { @objc func userDidAddAccount(_ note: Notification) {
let expandNewAccount = {
let rebuildAndExpand = { if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
self.rebuildBackingStores() { let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
// Automatically expand any new accounts self.markExpanded(node)
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
node.isExpanded = true
}
} }
} }
if timelineFetcherContainsAnyPseudoFeed() { if timelineFetcherContainsAnyPseudoFeed() {
fetchAndReplaceArticlesAsync { fetchAndReplaceArticlesAsync(animated: true) {
rebuildAndExpand() self.masterTimelineViewController?.reinitializeArticles()
self.rebuildBackingStores() {
expandNewAccount()
}
} }
} else { } else {
rebuildAndExpand() rebuildBackingStores() {
expandNewAccount()
}
} }
} }
@objc func userDidDeleteAccount(_ note: Notification) { @objc func userDidDeleteAccount(_ note: Notification) {
let cleanupAccount = {
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
self.unmarkExpanded(node)
}
}
if timelineFetcherContainsAnyPseudoFeed() { if timelineFetcherContainsAnyPseudoFeed() {
fetchAndReplaceArticlesAsync { fetchAndReplaceArticlesAsync(animated: true) {
self.rebuildBackingStores() self.masterTimelineViewController?.reinitializeArticles()
self.rebuildBackingStores() {
cleanupAccount()
}
} }
} else { } else {
rebuildBackingStores() rebuildBackingStores() {
cleanupAccount()
}
} }
} }
@ -474,8 +490,53 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return 0 return 0
} }
func refreshTimeline() {
fetchAndReplaceArticlesAsync(animated: true) {
self.masterTimelineViewController?.reinitializeArticles()
}
}
func showAllFeeds() {
treeControllerDelegate.isReadFiltered = false
rebuildBackingStores()
}
func hideUnreadFeeds() {
treeControllerDelegate.isReadFiltered = true
rebuildBackingStores()
}
func showAllArticles() {
articleReadFilterType = .none
refreshTimeline()
}
func hideUnreadArticles() {
articleReadFilterType = .read
refreshTimeline()
}
func markExpanded(_ node: Node) {
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID {
expandedTable.insert(containerID)
}
}
func unmarkExpanded(_ node: Node) {
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID {
expandedTable.remove(containerID)
}
}
func isExpanded(_ node: Node) -> Bool {
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID {
return expandedTable.contains(containerID)
}
return false
}
func expand(_ node: Node) { func expand(_ node: Node) {
node.isExpanded = true markExpanded(node)
animatingChanges = true animatingChanges = true
rebuildShadowTable() rebuildShadowTable()
animatingChanges = false animatingChanges = false
@ -483,10 +544,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
func expandAllSectionsAndFolders() { func expandAllSectionsAndFolders() {
for sectionNode in treeController.rootNode.childNodes { for sectionNode in treeController.rootNode.childNodes {
sectionNode.isExpanded = true markExpanded(sectionNode)
for topLevelNode in sectionNode.childNodes { for topLevelNode in sectionNode.childNodes {
if topLevelNode.representedObject is Folder { if topLevelNode.representedObject is Folder {
topLevelNode.isExpanded = true markExpanded(topLevelNode)
} }
} }
} }
@ -496,7 +557,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} }
func collapse(_ node: Node) { func collapse(_ node: Node) {
node.isExpanded = false unmarkExpanded(node)
animatingChanges = true animatingChanges = true
rebuildShadowTable() rebuildShadowTable()
animatingChanges = false animatingChanges = false
@ -504,10 +565,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
func collapseAllFolders() { func collapseAllFolders() {
for sectionNode in treeController.rootNode.childNodes { for sectionNode in treeController.rootNode.childNodes {
sectionNode.isExpanded = true unmarkExpanded(sectionNode)
for topLevelNode in sectionNode.childNodes { for topLevelNode in sectionNode.childNodes {
if topLevelNode.representedObject is Folder { if topLevelNode.representedObject is Folder {
topLevelNode.isExpanded = true unmarkExpanded(topLevelNode)
} }
} }
} }
@ -523,7 +584,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return indexPathFor(node) return indexPathFor(node)
} }
func selectFeed(_ indexPath: IndexPath?, animated: Bool, completion: (() -> Void)? = nil) { func selectFeed(_ indexPath: IndexPath?, animated: Bool, deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
guard indexPath != currentFeedIndexPath else { guard indexPath != currentFeedIndexPath else {
completion?() completion?()
return return
@ -533,19 +594,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
masterFeedViewController.updateFeedSelection(animated: animated) masterFeedViewController.updateFeedSelection(animated: animated)
emptyTheTimeline() emptyTheTimeline()
selectArticle(nil) if deselectArticle {
selectArticle(nil)
}
if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed { if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
self.activityManager.selecting(feed: feed) self.activityManager.selecting(feed: feed)
self.installTimelineControllerIfNecessary(animated: animated) self.installTimelineControllerIfNecessary(animated: animated)
setTimelineFeed(feed) { setTimelineFeed(feed, animated: false) {
completion?() completion?()
} }
} else { } else {
setTimelineFeed(nil) { setTimelineFeed(nil, animated: false) {
self.activityManager.invalidateSelecting() self.activityManager.invalidateSelecting()
if self.rootSplitViewController.isCollapsed && self.navControllerForTimeline().viewControllers.last is MasterTimelineViewController { if self.rootSplitViewController.isCollapsed && self.navControllerForTimeline().viewControllers.last is MasterTimelineViewController {
self.navControllerForTimeline().popViewController(animated: animated) self.navControllerForTimeline().popViewController(animated: animated)
@ -608,9 +671,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
let currentArticleViewController: ArticleViewController let currentArticleViewController: ArticleViewController
if articleViewController == nil { if articleViewController == nil {
currentArticleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) currentArticleViewController = installArticleController(animated: animated)
currentArticleViewController.coordinator = self
installArticleController(currentArticleViewController, animated: animated)
} else { } else {
currentArticleViewController = articleViewController! currentArticleViewController = articleViewController!
} }
@ -632,7 +693,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
isSearching = true isSearching = true
savedSearchArticles = articles savedSearchArticles = articles
savedSearchArticleIds = Set(articles.map { $0.articleID }) savedSearchArticleIds = Set(articles.map { $0.articleID })
setTimelineFeed(nil) setTimelineFeed(nil, animated: true)
selectArticle(nil) selectArticle(nil)
} }
@ -640,9 +701,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
if let ip = currentFeedIndexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed { if let ip = currentFeedIndexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
timelineFeed = feed timelineFeed = feed
masterTimelineViewController?.reinitializeArticles() masterTimelineViewController?.reinitializeArticles()
replaceArticles(with: savedSearchArticles!, animate: true) replaceArticles(with: savedSearchArticles!, animated: true)
} else { } else {
setTimelineFeed(nil) setTimelineFeed(nil, animated: true)
} }
lastSearchString = "" lastSearchString = ""
@ -658,7 +719,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
guard isSearching else { return } guard isSearching else { return }
if searchString.count < 3 { if searchString.count < 3 {
setTimelineFeed(nil) setTimelineFeed(nil, animated: true)
return return
} }
@ -666,9 +727,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
switch searchScope { switch searchScope {
case .global: case .global:
setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString))) setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString)), animated: true)
case .timeline: case .timeline:
setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!))) setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!)), animated: true)
} }
lastSearchString = searchString lastSearchString = searchString
@ -724,9 +785,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return return
} }
selectNextUnreadFeedFetcher() selectNextUnreadFeed() {
if selectNextUnreadArticleInTimeline() { if self.selectNextUnreadArticleInTimeline() {
activityManager.selectingNextUnread() self.activityManager.selectingNextUnread()
}
} }
} }
@ -963,15 +1025,36 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
extension SceneCoordinator: UISplitViewControllerDelegate { extension SceneCoordinator: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool { func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
guard !isThreePanelMode else {
return true
}
if let articleViewController = (secondaryViewController as? UINavigationController)?.topViewController as? ArticleViewController {
masterNavigationController.pushViewController(articleViewController, animated: false)
return false
}
return currentArticle == nil return currentArticle == nil
} }
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? { func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
if currentArticle == nil { guard !isThreePanelMode else {
return subSplitViewController
}
guard currentArticle != nil else {
let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
articleViewController.coordinator = self articleViewController.coordinator = self
return articleViewController let controller = addNavControllerIfNecessary(articleViewController, showButton: true)
return controller
} }
if let articleViewController = masterNavigationController.viewControllers.last as? ArticleViewController {
masterNavigationController.popViewController(animated: false)
let controller = addNavControllerIfNecessary(articleViewController, showButton: true)
return controller
}
return nil return nil
} }
@ -987,7 +1070,6 @@ extension SceneCoordinator: UINavigationControllerDelegate {
return return
} }
// If we are showing the Feeds and only the feeds start clearing stuff // If we are showing the Feeds and only the feeds start clearing stuff
if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending { if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending {
activityManager.invalidateCurrentActivities() activityManager.invalidateCurrentActivities()
@ -1073,10 +1155,10 @@ private extension SceneCoordinator {
var result = [Node]() var result = [Node]()
let sectionNode = treeController.rootNode.childAtIndex(i)! let sectionNode = treeController.rootNode.childAtIndex(i)!
if sectionNode.isExpanded { if isExpanded(sectionNode) {
for node in sectionNode.childNodes { for node in sectionNode.childNodes {
result.append(node) result.append(node)
if node.isExpanded { if isExpanded(node) {
for child in node.childNodes { for child in node.childNodes {
result.append(child) result.append(child)
} }
@ -1112,11 +1194,12 @@ private extension SceneCoordinator {
return indexPathFor(node) return indexPathFor(node)
} }
func setTimelineFeed(_ feed: Feed?, completion: (() -> Void)? = nil) { func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) {
timelineFeed = feed timelineFeed = feed
timelineMiddleIndexPath = nil timelineMiddleIndexPath = nil
articleReadFilterType = feed?.defaultReadFilterType ?? .none
fetchAndReplaceArticlesAsync { fetchAndReplaceArticlesAsync(animated: animated) {
self.masterTimelineViewController?.reinitializeArticles() self.masterTimelineViewController?.reinitializeArticles()
completion?() completion?()
} }
@ -1234,7 +1317,7 @@ private extension SceneCoordinator {
return true return true
} }
if node.isExpanded { if isExpanded(node) {
continue continue
} }
@ -1289,7 +1372,7 @@ private extension SceneCoordinator {
} }
func selectNextUnreadFeedFetcher() { func selectNextUnreadFeed(completion: @escaping () -> Void) {
let indexPath: IndexPath = { let indexPath: IndexPath = {
if currentFeedIndexPath == nil { if currentFeedIndexPath == nil {
@ -1312,15 +1395,19 @@ private extension SceneCoordinator {
} }
}() }()
if selectNextUnreadFeedFetcher(startingWith: nextIndexPath) { selectNextUnreadFeed(startingWith: nextIndexPath) { found in
return if !found {
self.selectNextUnreadFeed(startingWith: IndexPath(row: 0, section: 0)) { _ in
completion()
}
} else {
completion()
}
} }
selectNextUnreadFeedFetcher(startingWith: IndexPath(row: 0, section: 0))
} }
@discardableResult func selectNextUnreadFeed(startingWith indexPath: IndexPath, completion: @escaping (Bool) -> Void) {
func selectNextUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool {
for i in indexPath.section..<shadowTable.count { for i in indexPath.section..<shadowTable.count {
@ -1337,23 +1424,27 @@ private extension SceneCoordinator {
let nextIndexPath = IndexPath(row: j, section: i) let nextIndexPath = IndexPath(row: j, section: i)
guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else { guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {
assertionFailure() assertionFailure()
return true completion(false)
return
} }
if node.isExpanded { if isExpanded(node) {
continue continue
} }
if unreadCountProvider.unreadCount > 0 { if unreadCountProvider.unreadCount > 0 {
selectFeed(nextIndexPath, animated: true) selectFeed(nextIndexPath, animated: false, deselectArticle: false) {
return true self.currentArticle = nil
completion(true)
}
return
} }
} }
} }
return false completion(false)
} }
@ -1377,25 +1468,25 @@ private extension SceneCoordinator {
func emptyTheTimeline() { func emptyTheTimeline() {
if !articles.isEmpty { if !articles.isEmpty {
replaceArticles(with: Set<Article>(), animate: false) replaceArticles(with: Set<Article>(), animated: false)
} }
} }
func sortParametersDidChange() { func sortParametersDidChange() {
replaceArticles(with: Set(articles), animate: true) replaceArticles(with: Set(articles), animated: true)
} }
func replaceArticles(with unsortedArticles: Set<Article>, animate: Bool) { func replaceArticles(with unsortedArticles: Set<Article>, animated: Bool) {
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed) let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
replaceArticles(with: sortedArticles, animate: animate) replaceArticles(with: sortedArticles, animated: animated)
} }
func replaceArticles(with sortedArticles: ArticleArray, animate: Bool) { func replaceArticles(with sortedArticles: ArticleArray, animated: Bool) {
if articles != sortedArticles { if articles != sortedArticles {
articles = sortedArticles articles = sortedArticles
updateShowNamesAndIcons() updateShowNamesAndIcons()
updateUnreadCount() updateUnreadCount()
masterTimelineViewController?.reloadArticles(animated: animate) masterTimelineViewController?.reloadArticles(animated: animated)
} }
} }
@ -1422,7 +1513,7 @@ private extension SceneCoordinator {
} }
} }
strongSelf.replaceArticles(with: updatedArticles, animate: true) strongSelf.replaceArticles(with: updatedArticles, animated: true)
} }
} }
@ -1432,7 +1523,7 @@ private extension SceneCoordinator {
fetchRequestQueue.cancelAllRequests() fetchRequestQueue.cancelAllRequests()
} }
func fetchAndReplaceArticlesAsync(completion: @escaping () -> Void) { func fetchAndReplaceArticlesAsync(animated: Bool, completion: @escaping () -> Void) {
// To be called when we need to do an entire fetch, but an async delay is okay. // To be called when we need to do an entire fetch, but an async delay is okay.
// Example: we have the Today feed selected, and the calendar day just changed. // Example: we have the Today feed selected, and the calendar day just changed.
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
@ -1443,7 +1534,7 @@ private extension SceneCoordinator {
} }
fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (articles) in fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (articles) in
self?.replaceArticles(with: articles, animate: true) self?.replaceArticles(with: articles, animated: animated)
completion() completion()
} }
@ -1455,10 +1546,10 @@ private extension SceneCoordinator {
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, representedObjects: representedObjects) { [weak self] (articles, operation) in let readFilter = articleReadFilterType != .none
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: readFilter, representedObjects: representedObjects) { [weak self] (articles, operation) in
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
callback(Set<Article>())
return return
} }
callback(articles) callback(articles)
@ -1507,37 +1598,50 @@ private extension SceneCoordinator {
func installTimelineControllerIfNecessary(animated: Bool) { func installTimelineControllerIfNecessary(animated: Bool) {
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 { if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
isTimelineViewControllerPending = true isTimelineViewControllerPending = true
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self masterTimelineViewController!.coordinator = self
navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: animated) navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: animated)
masterTimelineViewController?.reloadArticles(animated: false)
} }
} }
func installArticleController(_ articleController: UIViewController, animated: Bool) { @discardableResult
func installArticleController(_ recycledArticleController: ArticleViewController? = nil, animated: Bool) -> ArticleViewController {
isArticleViewControllerPending = true isArticleViewControllerPending = true
let articleController: ArticleViewController = {
if let controller = recycledArticleController {
return controller
} else {
let controller = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
controller.coordinator = self
return controller
}
}()
if let subSplit = subSplitViewController { if let subSplit = subSplitViewController {
let controller = addNavControllerIfNecessary(articleController, showButton: false) let controller = addNavControllerIfNecessary(articleController, showButton: false)
subSplit.showDetailViewController(controller, sender: self) subSplit.showDetailViewController(controller, sender: self)
} else if rootSplitViewController.isCollapsed { } else if rootSplitViewController.isCollapsed || wasRootSplitViewControllerCollapsed {
let controller = addNavControllerIfNecessary(articleController, showButton: false) masterNavigationController.pushViewController(articleController, animated: animated)
masterNavigationController.pushViewController(controller, animated: animated)
} else { } else {
let controller = addNavControllerIfNecessary(articleController, showButton: true) let controller = addNavControllerIfNecessary(articleController, showButton: true)
rootSplitViewController.showDetailViewController(controller, sender: self) rootSplitViewController.showDetailViewController(controller, sender: self)
} }
// We have to do a full reload when installing an article controller. We may have changed color contexts
// and need to update the article colors. An example is in dark mode. Split screen doesn't use true black
// like darkmode usually does.
articleController.fullReload()
return articleController
} }
func addNavControllerIfNecessary(_ controller: UIViewController, showButton: Bool) -> UIViewController { func addNavControllerIfNecessary(_ controller: UIViewController, showButton: Bool) -> UIViewController {
if rootSplitViewController.traitCollection.horizontalSizeClass == .compact { // You will sometimes get a compact horizontal size class while in three panel mode. Dunno why it lies.
if rootSplitViewController.traitCollection.horizontalSizeClass == .compact && !isThreePanelMode {
return controller return controller
@ -1560,14 +1664,16 @@ private extension SceneCoordinator {
} }
func configureDoubleSplit() { func installSubSplit() {
rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.30 rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.30
let subSplit = UISplitViewController.template() subSplitViewController = UISplitViewController()
subSplit.preferredDisplayMode = .allVisible subSplitViewController!.preferredDisplayMode = .allVisible
subSplit.preferredPrimaryColumnWidthFraction = 0.4285 subSplitViewController!.viewControllers = [InteractiveNavigationController.template()]
subSplitViewController!.preferredPrimaryColumnWidthFraction = 0.4285
rootSplitViewController.showDetailViewController(subSplit, sender: self) rootSplitViewController.showDetailViewController(subSplitViewController!, sender: self)
rootSplitViewController.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .regular), forChild: subSplitViewController!)
} }
func navControllerForTimeline() -> UINavigationController { func navControllerForTimeline() -> UINavigationController {
@ -1578,67 +1684,50 @@ private extension SceneCoordinator {
} }
} }
@discardableResult func configureThreePanelMode() {
func transitionToThreePanelMode() -> UIViewController { let recycledArticleController = articleViewController
defer { defer {
masterNavigationController.viewControllers = [masterFeedViewController] masterNavigationController.viewControllers = [masterFeedViewController]
} }
let controller: UIViewController = {
if let result = articleViewController {
return result
} else {
let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
articleViewController.coordinator = self
return articleViewController
}
}()
configureDoubleSplit() if rootSplitViewController.viewControllers.last is InteractiveNavigationController {
_ = rootSplitViewController.viewControllers.popLast()
}
installSubSplit()
installTimelineControllerIfNecessary(animated: false) installTimelineControllerIfNecessary(animated: false)
masterTimelineViewController?.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem masterTimelineViewController?.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
masterTimelineViewController?.navigationItem.leftItemsSupplementBackButton = true masterTimelineViewController?.navigationItem.leftItemsSupplementBackButton = true
// Create the new sub split controller and add the timeline in the primary position installArticleController(recycledArticleController, animated: false)
let masterTimelineNavController = subSplitViewController!.viewControllers.first as! UINavigationController
masterTimelineNavController.viewControllers = [masterTimelineViewController!]
// Put the detail or no selection controller in the secondary (or detail) position of the sub split
let navController = addNavControllerIfNecessary(controller, showButton: false)
subSplitViewController!.showDetailViewController(navController, sender: self)
masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true) masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true)
masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false) masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false)
// We made sure this was there above when we called configureDoubleSplit
return subSplitViewController!
} }
func transitionFromThreePanelMode() { func configureStandardPanelMode() {
let recycledArticleController = articleViewController
rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension
if let subSplit = rootSplitViewController.viewControllers.last as? UISplitViewController { // Set the is Pending flags early to prevent the navigation controller delegate from thinking that we
// swiping around in the user interface
// Push a new timeline on to the master navigation controller. For some reason recycling the timeline can freak isTimelineViewControllerPending = true
// the system out and throw it into an infinite loop. isArticleViewControllerPending = true
if currentFeedIndexPath != nil {
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self
masterNavigationController.pushViewController(masterTimelineViewController!, animated: false)
}
// Pull the detail or no selection controller out of the sub split second position and move it to the root split controller
// secondary (detail) position.
if let detailNav = subSplit.viewControllers.last as? UINavigationController, let topController = detailNav.topViewController {
let newNav = addNavControllerIfNecessary(topController, showButton: true)
rootSplitViewController.showDetailViewController(newNav, sender: self)
}
masterNavigationController.viewControllers = [masterFeedViewController]
if rootSplitViewController.viewControllers.last is UISplitViewController {
subSplitViewController = nil
_ = rootSplitViewController.viewControllers.popLast()
} }
if currentFeedIndexPath != nil {
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self
masterNavigationController.pushViewController(masterTimelineViewController!, animated: false)
}
installArticleController(recycledArticleController, animated: false)
} }
// MARK: NSUserActivity // MARK: NSUserActivity
@ -1646,14 +1735,14 @@ private extension SceneCoordinator {
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
guard let userInfo = userInfo, guard let userInfo = userInfo,
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any],
let articleFetcherType = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
return return
} }
switch articleFetcherType { switch feedIdentifier {
case .smartFeed(let identifier): case .smartFeed:
guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return } guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return }
if let indexPath = indexPathFor(smartFeed) { if let indexPath = indexPathFor(smartFeed) {
selectFeed(indexPath, animated: false) selectFeed(indexPath, animated: false)
} }
@ -1706,14 +1795,14 @@ private extension SceneCoordinator {
func restoreFeed(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool { func restoreFeed(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any], guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any],
let articleFetcherType = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
return false return false
} }
switch articleFetcherType { switch feedIdentifier {
case .smartFeed(let identifier): case .smartFeed:
guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return false } guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false }
if smartFeed.fetchArticles().contains(accountID: accountID, articleID: articleID) { if smartFeed.fetchArticles().contains(accountID: accountID, articleID: articleID) {
if let indexPath = indexPathFor(smartFeed) { if let indexPath = indexPathFor(smartFeed) {
selectFeed(indexPath, animated: false) { selectFeed(indexPath, animated: false) {

View File

@ -58,7 +58,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func sceneWillEnterForeground(_ scene: UIScene) { func sceneWillEnterForeground(_ scene: UIScene) {
appDelegate.prepareAccountsForForeground() appDelegate.prepareAccountsForForeground()
self.coordinator.configureThreePanelMode(for: window!.frame.size) self.coordinator.configurePanelMode(for: window!.frame.size)
} }
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {

View File

@ -19,7 +19,7 @@
<tableViewSection headerTitle="Notifications, Badge, Data, &amp; More" id="Bmb-Oi-RZK"> <tableViewSection headerTitle="Notifications, Badge, Data, &amp; More" id="Bmb-Oi-RZK">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="F9H-Kr-npj" style="IBUITableViewCellStyleDefault" id="zvg-7C-BlH" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="F9H-Kr-npj" style="IBUITableViewCellStyleDefault" id="zvg-7C-BlH" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="55.5" width="374" height="44"/> <rect key="frame" x="0.0" y="55.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="zvg-7C-BlH" id="Tqk-Tu-E6K"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="zvg-7C-BlH" id="Tqk-Tu-E6K">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -184,7 +184,7 @@
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/> <rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Customize Layout" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6C6-JQ-lfQ"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Timeline Layout" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="6C6-JQ-lfQ">
<rect key="frame" x="20" y="0.0" width="315" height="44"/> <rect key="frame" x="20" y="0.0" width="315" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
@ -196,17 +196,51 @@
</tableViewCell> </tableViewCell>
</cells> </cells>
</tableViewSection> </tableViewSection>
<tableViewSection headerTitle="About" id="TkH-4v-yhk"> <tableViewSection headerTitle="Articles" id="TRr-Ew-IvU">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="2o6-8W-nyK" style="IBUITableViewCellStyleDefault" id="he9-Ql-yfa" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="WR6-xo-ty2" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="631.5" width="374" height="44"/> <rect key="frame" x="20" y="631.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="he9-Ql-yfa" id="q6L-C8-H9a"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="WR6-xo-ty2" id="zX8-l2-bVH">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="About NetNewsWire" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="2o6-8W-nyK"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show Articles in Fullscreen" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="79e-5s-vd0">
<rect key="frame" x="20" y="0.0" width="315" height="44"/> <rect key="frame" x="20" y="11.5" width="206" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="2Md-2E-7Z4">
<rect key="frame" x="305" y="6.5" width="51" height="31"/>
<color key="onTintColor" name="secondaryAccentColor"/>
<connections>
<action selector="switchFullscreenArticles:" destination="a0p-rk-skQ" eventType="valueChanged" id="5fa-Ad-e0j"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstItem="2Md-2E-7Z4" firstAttribute="centerY" secondItem="zX8-l2-bVH" secondAttribute="centerY" id="1ae-Z0-Rxf"/>
<constraint firstAttribute="trailing" secondItem="2Md-2E-7Z4" secondAttribute="trailing" constant="20" symbolic="YES" id="ELH-06-H2j"/>
<constraint firstItem="79e-5s-vd0" firstAttribute="centerY" secondItem="zX8-l2-bVH" secondAttribute="centerY" id="FoL-fO-aDw"/>
<constraint firstItem="2Md-2E-7Z4" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="79e-5s-vd0" secondAttribute="trailing" constant="8" id="lUn-8D-X20"/>
<constraint firstItem="79e-5s-vd0" firstAttribute="leading" secondItem="zX8-l2-bVH" secondAttribute="leadingMargin" id="tdZ-30-ACC"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Help" id="TkH-4v-yhk">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="40W-2p-ne4" style="IBUITableViewCellStyleDefault" id="Om7-lH-RUh" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="731.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Om7-lH-RUh" id="vrJ-nE-HMP">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="NetNewsWire Help" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="40W-2p-ne4">
<rect key="frame" x="20" y="0.0" width="334" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/> <nil key="textColor"/>
@ -216,7 +250,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="lOk-Dh-GfZ" style="IBUITableViewCellStyleDefault" id="GWZ-jk-qU6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="lOk-Dh-GfZ" style="IBUITableViewCellStyleDefault" id="GWZ-jk-qU6" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="675.5" width="374" height="44"/> <rect key="frame" x="20" y="775.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GWZ-jk-qU6" id="ZgS-bo-xDl"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GWZ-jk-qU6" id="ZgS-bo-xDl">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -233,7 +267,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Pm8-6D-fdE" style="IBUITableViewCellStyleDefault" id="3cU-BG-6kK" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Pm8-6D-fdE" style="IBUITableViewCellStyleDefault" id="3cU-BG-6kK" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="719.5" width="374" height="44"/> <rect key="frame" x="20" y="819.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3cU-BG-6kK" id="Qm0-SY-0vx"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3cU-BG-6kK" id="Qm0-SY-0vx">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -250,7 +284,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="TEA-EG-V6d" style="IBUITableViewCellStyleDefault" id="4yc-ig-I61" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="TEA-EG-V6d" style="IBUITableViewCellStyleDefault" id="4yc-ig-I61" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="763.5" width="374" height="44"/> <rect key="frame" x="20" y="863.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4yc-ig-I61" id="uQl-VP-9p9"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="4yc-ig-I61" id="uQl-VP-9p9">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -267,7 +301,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Q9a-Pi-uCc" style="IBUITableViewCellStyleDefault" id="mSW-A7-8lf" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Q9a-Pi-uCc" style="IBUITableViewCellStyleDefault" id="mSW-A7-8lf" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="807.5" width="374" height="44"/> <rect key="frame" x="20" y="907.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mSW-A7-8lf" id="shF-ro-Zpx"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mSW-A7-8lf" id="shF-ro-Zpx">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -284,7 +318,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dWz-1o-EpJ" style="IBUITableViewCellStyleDefault" id="2MG-qn-idJ" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dWz-1o-EpJ" style="IBUITableViewCellStyleDefault" id="2MG-qn-idJ" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="851.5" width="374" height="44"/> <rect key="frame" x="20" y="951.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2MG-qn-idJ" id="gP9-ry-keC"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2MG-qn-idJ" id="gP9-ry-keC">
<rect key="frame" x="0.0" y="0.0" width="374" height="44"/> <rect key="frame" x="0.0" y="0.0" width="374" height="44"/>
@ -300,6 +334,23 @@
</subviews> </subviews>
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="2o6-8W-nyK" style="IBUITableViewCellStyleDefault" id="he9-Ql-yfa" customClass="VibrantTableViewCell" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="20" y="995.5" width="374" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="he9-Ql-yfa" id="q6L-C8-H9a">
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="About NetNewsWire" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="2o6-8W-nyK">
<rect key="frame" x="20" y="0.0" width="315" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells> </cells>
</tableViewSection> </tableViewSection>
</sections> </sections>
@ -317,6 +368,7 @@
</navigationItem> </navigationItem>
<connections> <connections>
<outlet property="groupByFeedSwitch" destination="JNi-Wz-RbU" id="TwH-Kd-o6N"/> <outlet property="groupByFeedSwitch" destination="JNi-Wz-RbU" id="TwH-Kd-o6N"/>
<outlet property="showFullscreenArticlesSwitch" destination="2Md-2E-7Z4" id="lEN-VP-wEO"/>
<outlet property="timelineSortOrderSwitch" destination="Keq-Np-l9O" id="Zm7-HG-r5h"/> <outlet property="timelineSortOrderSwitch" destination="Keq-Np-l9O" id="Zm7-HG-r5h"/>
</connections> </connections>
</tableViewController> </tableViewController>

View File

@ -17,6 +17,7 @@ class SettingsViewController: UITableViewController {
@IBOutlet weak var timelineSortOrderSwitch: UISwitch! @IBOutlet weak var timelineSortOrderSwitch: UISwitch!
@IBOutlet weak var groupByFeedSwitch: UISwitch! @IBOutlet weak var groupByFeedSwitch: UISwitch!
@IBOutlet weak var showFullscreenArticlesSwitch: UISwitch!
weak var presentingParentController: UIViewController? weak var presentingParentController: UIViewController?
@ -50,10 +51,16 @@ class SettingsViewController: UITableViewController {
groupByFeedSwitch.isOn = false groupByFeedSwitch.isOn = false
} }
if AppDefaults.articleFullscreenEnabled {
showFullscreenArticlesSwitch.isOn = true
} else {
showFullscreenArticlesSwitch.isOn = false
}
let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 20.0, y: 0.0, width: 0.0, height: 0.0)) let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 20.0, y: 0.0, width: 0.0, height: 0.0))
buildLabel.font = UIFont.systemFont(ofSize: 11.0) buildLabel.font = UIFont.systemFont(ofSize: 11.0)
buildLabel.textColor = UIColor.gray buildLabel.textColor = UIColor.gray
buildLabel.text = "\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))" buildLabel.text = "\(Bundle.main.appName) \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))"
buildLabel.sizeToFit() buildLabel.sizeToFit()
buildLabel.translatesAutoresizingMaskIntoConstraints = false buildLabel.translatesAutoresizingMaskIntoConstraints = false
@ -71,24 +78,42 @@ class SettingsViewController: UITableViewController {
// MARK: UITableView // MARK: UITableView
override func numberOfSections(in tableView: UITableView) -> Int {
var sections = super.numberOfSections(in: tableView)
if traitCollection.userInterfaceIdiom != .phone {
sections = sections - 1
}
return sections
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section { var adjustedSection = section
if traitCollection.userInterfaceIdiom != .phone && section > 3 {
adjustedSection = adjustedSection + 1
}
switch adjustedSection {
case 1: case 1:
return AccountManager.shared.accounts.count + 1 return AccountManager.shared.accounts.count + 1
case 2: case 2:
let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section) let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: adjustedSection)
if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) { if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasFeedWithURL(appNewsURLString) {
return defaultNumberOfRows - 1 return defaultNumberOfRows - 1
} }
return defaultNumberOfRows return defaultNumberOfRows
default: default:
return super.tableView(tableView, numberOfRowsInSection: section) return super.tableView(tableView, numberOfRowsInSection: adjustedSection)
} }
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var adjustedSection = indexPath.section
if traitCollection.userInterfaceIdiom != .phone && adjustedSection > 3 {
adjustedSection = adjustedSection + 1
}
let cell: UITableViewCell let cell: UITableViewCell
switch indexPath.section { switch adjustedSection {
case 1: case 1:
let sortedAccounts = AccountManager.shared.sortedAccounts let sortedAccounts = AccountManager.shared.sortedAccounts
@ -105,8 +130,8 @@ class SettingsViewController: UITableViewController {
} }
default: default:
let adjustedIndexPath = IndexPath(row: indexPath.row, section: adjustedSection)
cell = super.tableView(tableView, cellForRowAt: indexPath) cell = super.tableView(tableView, cellForRowAt: adjustedIndexPath)
} }
@ -114,7 +139,12 @@ class SettingsViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.section { var adjustedSection = indexPath.section
if traitCollection.userInterfaceIdiom != .phone && adjustedSection > 3 {
adjustedSection = adjustedSection + 1
}
switch adjustedSection {
case 0: case 0:
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!) UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
@ -156,11 +186,11 @@ class SettingsViewController: UITableViewController {
default: default:
break break
} }
case 4: case 5:
switch indexPath.row { switch indexPath.row {
case 0: case 0:
let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self) openURL("https://ranchero.com/netnewswire/help/ios/5.0/en/")
self.navigationController?.pushViewController(timeline, animated: true) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
case 1: case 1:
openURL("https://ranchero.com/netnewswire/") openURL("https://ranchero.com/netnewswire/")
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
@ -176,6 +206,9 @@ class SettingsViewController: UITableViewController {
case 5: case 5:
openURL("https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes") openURL("https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes")
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
case 6:
let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self)
self.navigationController?.pushViewController(timeline, animated: true)
default: default:
break break
} }
@ -197,19 +230,11 @@ class SettingsViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 1 { return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1))
return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1))
} else {
return super.tableView(tableView, heightForRowAt: indexPath)
}
} }
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
if indexPath.section == 1 { return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
} else {
return super.tableView(tableView, indentationLevelForRowAt: indexPath)
}
} }
// MARK: Actions // MARK: Actions
@ -234,6 +259,14 @@ class SettingsViewController: UITableViewController {
} }
} }
@IBAction func switchFullscreenArticles(_ sender: Any) {
if showFullscreenArticlesSwitch.isOn {
AppDefaults.articleFullscreenEnabled = true
} else {
AppDefaults.articleFullscreenEnabled = false
}
}
// MARK: Notifications // MARK: Notifications
@objc func contentSizeCategoryDidChange() { @objc func contentSizeCategoryDidChange() {

View File

@ -0,0 +1,20 @@
//
// CroppingPreviewParameters.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 11/23/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
class CroppingPreviewParameters: UIPreviewParameters {
init(view: UIView) {
super.init()
let newBounds = CGRect(x: 1, y: 1, width: view.bounds.width - 2, height: view.bounds.height - 2)
let visiblePath = UIBezierPath(roundedRect: newBounds, cornerRadius: 10)
self.visiblePath = visiblePath
}
}

View File

@ -10,6 +10,8 @@ import UIKit
class ImageHeaderView: UITableViewHeaderFooterView { class ImageHeaderView: UITableViewHeaderFooterView {
static let rowHeight = CGFloat(integerLiteral: 88)
var imageView = UIImageView() var imageView = UIImageView()
override init(reuseIdentifier: String?) { override init(reuseIdentifier: String?) {

View File

@ -13,9 +13,12 @@ class TickMarkSlider: UISlider {
private var enableFeedback = false private var enableFeedback = false
private let feedbackGenerator = UISelectionFeedbackGenerator() private let feedbackGenerator = UISelectionFeedbackGenerator()
private var roundedValue: Float?
override var value: Float { override var value: Float {
didSet { didSet {
if enableFeedback && value.truncatingRemainder(dividingBy: 1) == 0 { let testValue = value.rounded()
if testValue != roundedValue && enableFeedback && value.truncatingRemainder(dividingBy: 1) == 0 {
roundedValue = testValue
feedbackGenerator.selectionChanged() feedbackGenerator.selectionChanged()
} }
} }
@ -67,6 +70,12 @@ class TickMarkSlider: UISlider {
} }
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let result = super.continueTracking(touch, with: event)
value = value.rounded()
return result
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) { override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
value = value.rounded() value = value.rounded()
} }

View File

@ -1,20 +0,0 @@
//
// UISplitViewController-Extensions.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/18/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
extension UISplitViewController {
static func template() -> UISplitViewController {
let splitViewController = UISplitViewController()
splitViewController.preferredDisplayMode = .automatic
splitViewController.viewControllers = [InteractiveNavigationController.template()]
return splitViewController
}
}

@ -1 +1 @@
Subproject commit 3dc1c288bb4e15fedf17fa8fc43c1d5cec36af5e Subproject commit 2fc9b9cff60032a272303ff6d6df5b39ec297179