mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-02 12:06:58 +01:00
Merge branch 'master' into feature/feed-wrangler
This commit is contained in:
commit
ce51e4e632
@ -47,7 +47,7 @@ public enum FetchType {
|
||||
case starred
|
||||
case unread
|
||||
case today
|
||||
case unreadForFolder(Folder)
|
||||
case folder(Folder, Bool)
|
||||
case webFeed(WebFeed)
|
||||
case articleIDs(Set<String>)
|
||||
case search(String)
|
||||
@ -84,6 +84,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
public var isDeleted = false
|
||||
|
||||
public var containerID: ContainerIdentifier? {
|
||||
return ContainerIdentifier.account(accountID)
|
||||
}
|
||||
|
||||
public var account: Account? {
|
||||
return self
|
||||
}
|
||||
@ -594,8 +598,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return fetchUnreadArticles()
|
||||
case .today:
|
||||
return fetchTodayArticles()
|
||||
case .unreadForFolder(let folder):
|
||||
return fetchArticles(folder: folder)
|
||||
case .folder(let folder, let readFilter):
|
||||
if readFilter {
|
||||
return fetchUnreadArticles(folder: folder)
|
||||
} else {
|
||||
return fetchArticles(folder: folder)
|
||||
}
|
||||
case .webFeed(let webFeed):
|
||||
return fetchArticles(webFeed: webFeed)
|
||||
case .articleIDs(let articleIDs):
|
||||
@ -615,8 +623,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
fetchUnreadArticlesAsync(callback)
|
||||
case .today:
|
||||
fetchTodayArticlesAsync(callback)
|
||||
case .unreadForFolder(let folder):
|
||||
fetchArticlesAsync(folder: folder, callback)
|
||||
case .folder(let folder, let readFilter):
|
||||
if readFilter {
|
||||
return fetchUnreadArticlesAsync(folder: folder, callback)
|
||||
} else {
|
||||
return fetchArticlesAsync(folder: folder, callback)
|
||||
}
|
||||
case .webFeed(let webFeed):
|
||||
fetchArticlesAsync(webFeed: webFeed, callback)
|
||||
case .articleIDs(let articleIDs):
|
||||
@ -892,10 +904,18 @@ private extension Account {
|
||||
}
|
||||
|
||||
func fetchArticles(folder: Folder) -> Set<Article> {
|
||||
return fetchUnreadArticles(forContainer: folder)
|
||||
return fetchArticles(forContainer: folder)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
let feeds = container.flattenedWebFeeds()
|
||||
let articles = database.fetchUnreadArticles(feeds.webFeedIDs())
|
||||
|
@ -43,6 +43,7 @@
|
||||
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; };
|
||||
51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7B83233531BC008E8144 /* AccountBehaviors.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 */; };
|
||||
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 */; };
|
||||
@ -255,6 +256,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -572,6 +574,7 @@
|
||||
84F73CF0202788D80000BCEF /* ArticleFetcher.swift */,
|
||||
84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */,
|
||||
8419740D1F6DD25F006346C4 /* Container.swift */,
|
||||
51BFDECD238B508D00216323 /* ContainerIdentifier.swift */,
|
||||
84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */,
|
||||
84C8B3F31F89DE430053CCA6 /* DataExtensions.swift */,
|
||||
51BC8FCB237EC055004F8B56 /* Feed.swift */,
|
||||
@ -1123,6 +1126,7 @@
|
||||
515E4EB52324FF8C0057B0E7 /* CredentialsManager.swift in Sources */,
|
||||
844B297F210CE37E004020B3 /* UnreadCountProvider.swift in Sources */,
|
||||
9E1773D5234570E30056A5A8 /* FeedlyEntryParser.swift in Sources */,
|
||||
51BFDECE238B508D00216323 /* ContainerIdentifier.swift in Sources */,
|
||||
9E1D1555233431A600F4944C /* FeedlyOperation.swift in Sources */,
|
||||
9E1AF38B2353D41A008BD1D5 /* FeedlySetStarredArticlesOperation.swift in Sources */,
|
||||
84F1F06E2243524700DA0616 /* AccountMetadata.swift in Sources */,
|
||||
|
@ -49,11 +49,20 @@ extension WebFeed: ArticleFetcher {
|
||||
extension Folder: ArticleFetcher {
|
||||
|
||||
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) {
|
||||
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> {
|
||||
@ -61,7 +70,7 @@ extension Folder: ArticleFetcher {
|
||||
assertionFailure("Expected folder.account, but got nil.")
|
||||
return Set<Article>()
|
||||
}
|
||||
return account.fetchArticles(.unreadForFolder(self))
|
||||
return account.fetchArticles(.folder(self, true))
|
||||
}
|
||||
|
||||
public func fetchUnreadArticlesAsync(_ callback: @escaping ArticleSetBlock) {
|
||||
@ -70,6 +79,6 @@ extension Folder: ArticleFetcher {
|
||||
callback(Set<Article>())
|
||||
return
|
||||
}
|
||||
account.fetchArticlesAsync(.unreadForFolder(self), callback)
|
||||
account.fetchArticlesAsync(.folder(self, true), callback)
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ extension Notification.Name {
|
||||
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
|
||||
}
|
||||
|
||||
public protocol Container: class {
|
||||
public protocol Container: class, ContainerIdentifiable {
|
||||
|
||||
var account: Account? { get }
|
||||
var topLevelWebFeeds: Set<WebFeed> { get set }
|
||||
|
19
Frameworks/Account/ContainerIdentifier.swift
Normal file
19
Frameworks/Account/ContainerIdentifier.swift
Normal 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
|
||||
}
|
@ -9,6 +9,14 @@
|
||||
import Foundation
|
||||
import RSCore
|
||||
|
||||
public enum ReadFilterType {
|
||||
case read
|
||||
case none
|
||||
case alwaysRead
|
||||
}
|
||||
|
||||
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
|
||||
|
||||
var defaultReadFilterType: ReadFilterType { get }
|
||||
|
||||
}
|
||||
|
@ -12,6 +12,18 @@ import RSCore
|
||||
|
||||
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? {
|
||||
guard let accountID = account?.accountID else {
|
||||
assertionFailure("Expected feed.account, but got nil.")
|
||||
|
@ -13,6 +13,10 @@ import Articles
|
||||
|
||||
public final class WebFeed: Feed, Renamable, Hashable {
|
||||
|
||||
public var defaultReadFilterType: ReadFilterType {
|
||||
return .none
|
||||
}
|
||||
|
||||
public var feedID: FeedIdentifier? {
|
||||
guard let accountID = account?.accountID else {
|
||||
assertionFailure("Expected feed.account, but got nil.")
|
||||
|
@ -48,12 +48,16 @@ public final class ArticlesDatabase {
|
||||
return articlesTable.fetchArticles(webFeedID)
|
||||
}
|
||||
|
||||
public func fetchArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchArticles(webFeedIDs)
|
||||
}
|
||||
|
||||
public func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchArticles(articleIDs: articleIDs)
|
||||
}
|
||||
|
||||
public func fetchUnreadArticles(_ webFeedID: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchUnreadArticles(webFeedID)
|
||||
public func fetchUnreadArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
|
||||
return articlesTable.fetchUnreadArticles(webFeedIDs)
|
||||
}
|
||||
|
||||
public func fetchTodayArticles(_ webFeedIDs: Set<String>) -> Set<Article> {
|
||||
@ -78,6 +82,10 @@ public final class ArticlesDatabase {
|
||||
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) {
|
||||
articlesTable.fetchArticlesAsync(articleIDs: articleIDs, callback)
|
||||
}
|
||||
|
@ -60,6 +60,25 @@ final class ArticlesTable: DatabaseTable {
|
||||
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
|
||||
|
||||
func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?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>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15504"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15505"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
@ -336,6 +336,12 @@
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
<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">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Sort Articles By" id="OlJ-93-6OP">
|
||||
@ -362,6 +368,12 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<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">
|
||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||
<connections>
|
||||
|
@ -221,7 +221,7 @@ private extension DetailWebViewController {
|
||||
var render = "error();"
|
||||
if let data = try? encoder.encode(templateData) {
|
||||
let json = String(data: data, encoding: .utf8)!
|
||||
render = "render(\(json));"
|
||||
render = "render(\(json), 0);"
|
||||
}
|
||||
|
||||
webView.evaluateJavaScript(render)
|
||||
|
@ -237,6 +237,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
return currentSearchField != nil
|
||||
}
|
||||
|
||||
if item.action == #selector(toggleReadFeedsFilter(_:)) {
|
||||
return validateToggleReadFeeds(item)
|
||||
}
|
||||
|
||||
if item.action == #selector(toggleReadArticlesFilter(_:)) {
|
||||
return validateToggleReadArticles(item)
|
||||
}
|
||||
|
||||
if item.action == #selector(toggleSidebar(_:)) {
|
||||
guard let splitViewItem = sidebarSplitViewItem else {
|
||||
return false
|
||||
@ -438,6 +446,15 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
}
|
||||
window?.makeFirstResponder(searchField)
|
||||
}
|
||||
|
||||
@IBAction func toggleReadFeedsFilter(_ sender: Any?) {
|
||||
sidebarViewController?.toggleReadFilter()
|
||||
}
|
||||
|
||||
@IBAction func toggleReadArticlesFilter(_ sender: Any?) {
|
||||
timelineContainerViewController?.toggleReadFilter()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - SidebarDelegate
|
||||
@ -810,6 +827,30 @@ private extension MainWindowController {
|
||||
|
||||
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.
|
||||
|
||||
|
@ -30,6 +30,9 @@ protocol SidebarDelegate: class {
|
||||
lazy var dataSource: SidebarOutlineDataSource = {
|
||||
return SidebarOutlineDataSource(treeController: treeController)
|
||||
}()
|
||||
var isReadFiltered: Bool {
|
||||
return treeControllerDelegate.isReadFiltered
|
||||
}
|
||||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
private var animatingChanges = false
|
||||
@ -333,7 +336,16 @@ protocol SidebarDelegate: class {
|
||||
}
|
||||
revealAndSelectRepresentedObject(feedNode.representedObject)
|
||||
}
|
||||
|
||||
|
||||
func toggleReadFilter() {
|
||||
if treeControllerDelegate.isReadFiltered {
|
||||
treeControllerDelegate.isReadFiltered = false
|
||||
} else {
|
||||
treeControllerDelegate.isReadFiltered = true
|
||||
}
|
||||
rebuildTreeAndRestoreSelection()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSUserInterfaceValidations
|
||||
|
@ -30,6 +30,11 @@ final class TimelineContainerViewController: NSViewController {
|
||||
|
||||
weak var delegate: TimelineContainerViewControllerDelegate?
|
||||
|
||||
var isReadFiltered: Bool? {
|
||||
guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return nil }
|
||||
return regularTimelineViewController.isReadFiltered
|
||||
}
|
||||
|
||||
lazy var regularTimelineViewController = {
|
||||
return TimelineViewController(delegate: self)
|
||||
}()
|
||||
@ -79,6 +84,11 @@ final class TimelineContainerViewController: NSViewController {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func toggleReadFilter() {
|
||||
regularTimelineViewController.toggleReadFilter()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TimelineContainerViewController: TimelineDelegate {
|
||||
|
@ -20,6 +20,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
@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]? {
|
||||
didSet {
|
||||
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
|
||||
@ -36,6 +42,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
showFeedNames = false
|
||||
}
|
||||
|
||||
determineReadFilterType()
|
||||
selectionDidChange(nil)
|
||||
if showsSearchResults {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
@ -213,6 +220,19 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
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
|
||||
|
||||
@objc func openArticleInBrowser(_ sender: Any?) {
|
||||
@ -944,6 +964,14 @@ private extension TimelineViewController {
|
||||
}
|
||||
|
||||
// MARK: - Fetching Articles
|
||||
|
||||
func determineReadFilterType() {
|
||||
if representedObjects?.count ?? 0 == 1, let feed = representedObjects?.first as? Feed {
|
||||
articleReadFilterType = feed.defaultReadFilterType
|
||||
} else {
|
||||
articleReadFilterType = .read
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAndReplaceArticlesSync() {
|
||||
// To be called when the user has made a change of selection in the sidebar.
|
||||
@ -990,7 +1018,11 @@ private extension TimelineViewController {
|
||||
|
||||
var fetchedArticles = Set<Article>()
|
||||
for articleFetcher in articleFetchers {
|
||||
fetchedArticles.formUnion(articleFetcher.fetchArticles())
|
||||
if articleReadFilterType != ReadFilterType.none {
|
||||
fetchedArticles.formUnion(articleFetcher.fetchUnreadArticles())
|
||||
} else {
|
||||
fetchedArticles.formUnion(articleFetcher.fetchArticles())
|
||||
}
|
||||
}
|
||||
return fetchedArticles
|
||||
}
|
||||
@ -1000,7 +1032,8 @@ private extension TimelineViewController {
|
||||
// if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called.
|
||||
precondition(Thread.isMainThread)
|
||||
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)
|
||||
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
|
||||
return
|
||||
|
@ -39,7 +39,6 @@
|
||||
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; };
|
||||
512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97611ED9EB96007D329B /* WebFeedTreeControllerDelegate.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 */; };
|
||||
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 */; };
|
||||
@ -95,6 +94,10 @@
|
||||
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.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 */; };
|
||||
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 */; };
|
||||
516A09392360A2AE00EAE89B /* SettingsAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsAccountTableViewCell.swift */; };
|
||||
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 */; };
|
||||
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; };
|
||||
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 */; };
|
||||
51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1274,6 +1275,10 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1334,7 +1339,6 @@
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -1858,24 +1862,24 @@
|
||||
children = (
|
||||
51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
|
||||
51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
|
||||
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */,
|
||||
512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */,
|
||||
512AF9DC236F05230066F8BE /* InteractiveLabel.swift */,
|
||||
51934CC1230F5963006127BE /* InteractiveNavigationController.swift */,
|
||||
51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */,
|
||||
51EAED95231363EF00A9EEE3 /* NonIntrinsicButton.swift */,
|
||||
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */,
|
||||
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */,
|
||||
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */,
|
||||
512363372369155100951F16 /* RoundedProgressView.swift */,
|
||||
51C45250226506F400C03939 /* String-Extensions.swift */,
|
||||
51934CC1230F5963006127BE /* InteractiveNavigationController.swift */,
|
||||
5108F6D723763094001ABC45 /* TickMarkSlider.swift */,
|
||||
51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */,
|
||||
51F85BF622749FA100C787DC /* UIFont-Extensions.swift */,
|
||||
512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */,
|
||||
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */,
|
||||
51FFF0C3235EE8E5002762AA /* VibrantButton.swift */,
|
||||
5186A634235EF3A800C97195 /* VibrantLabel.swift */,
|
||||
5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */,
|
||||
51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */,
|
||||
51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */,
|
||||
);
|
||||
path = "UIKit Extensions";
|
||||
sourceTree = "<group>";
|
||||
@ -1884,7 +1888,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
51C45264226508F600C03939 /* MasterFeedViewController.swift */,
|
||||
51CC9B3D231720B2000E842F /* MasterFeedDataSource.swift */,
|
||||
51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */,
|
||||
51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */,
|
||||
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */,
|
||||
51CE1C0A23622006005548FC /* RefreshProgressView.swift */,
|
||||
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */,
|
||||
51C45260226508F600C03939 /* Cell */,
|
||||
@ -3962,7 +3968,6 @@
|
||||
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
|
||||
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
|
||||
5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */,
|
||||
512E09352268B25900BDCFDD /* UISplitViewController-Extensions.swift in Sources */,
|
||||
51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */,
|
||||
51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */,
|
||||
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
|
||||
@ -3971,6 +3976,7 @@
|
||||
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
|
||||
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */,
|
||||
5141E7562374A2890013FF27 /* ArticleIconSchemeHandler.swift in Sources */,
|
||||
51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */,
|
||||
512AF9DD236F05230066F8BE /* InteractiveLabel.swift in Sources */,
|
||||
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
|
||||
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */,
|
||||
@ -3991,6 +3997,7 @@
|
||||
51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */,
|
||||
51C452AF2265108300C03939 /* ArticleArray.swift in Sources */,
|
||||
51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */,
|
||||
51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */,
|
||||
51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */,
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */,
|
||||
5108F6D42375EEEF001ABC45 /* TimelinePreviewTableViewController.swift in Sources */,
|
||||
@ -4016,6 +4023,7 @@
|
||||
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */,
|
||||
5108F6B72375E612001ABC45 /* CacheCleaner.swift in Sources */,
|
||||
518651DA235621840078E021 /* ImageTransition.swift in Sources */,
|
||||
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
|
||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
|
||||
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
|
||||
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
||||
@ -4027,12 +4035,12 @@
|
||||
51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */,
|
||||
5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */,
|
||||
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */,
|
||||
51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */,
|
||||
FFD43E412340F488009E5CA3 /* UndoAvailableAlertController.swift in Sources */,
|
||||
51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */,
|
||||
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */,
|
||||
51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */,
|
||||
5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */,
|
||||
51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */,
|
||||
51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */,
|
||||
513228FC233037630033D4ED /* Reachability.swift in Sources */,
|
||||
51C45259226508D300C03939 /* AppDefaults.swift in Sources */,
|
||||
|
@ -30,11 +30,11 @@ function error() {
|
||||
document.body.innerHTML = "error";
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
function render(data, scrollY) {
|
||||
document.getElementsByTagName("style")[0].innerHTML = data.style;
|
||||
document.body.innerHTML = data.body;
|
||||
|
||||
window.scrollTo(0, 0);
|
||||
window.scrollTo(0, scrollY);
|
||||
|
||||
wrapFrames()
|
||||
stripStyles()
|
||||
|
@ -89,7 +89,7 @@ struct ArticleStringFormatter {
|
||||
return cachedBody
|
||||
}
|
||||
var s = body.rsparser_stringByDecodingHTMLEntities()
|
||||
s = s.rs_string(byStrippingHTML: 150)
|
||||
s = s.rs_string(byStrippingHTML: 250)
|
||||
s = s.rs_stringByTrimmingWhitespace()
|
||||
s = s.rs_stringWithCollapsedWhitespace()
|
||||
if s == "Comments" { // Hacker News.
|
||||
|
@ -13,6 +13,10 @@ import Account
|
||||
|
||||
final class SmartFeed: PseudoFeed {
|
||||
|
||||
public var defaultReadFilterType: ReadFilterType {
|
||||
return .none
|
||||
}
|
||||
|
||||
var feedID: FeedIdentifier? {
|
||||
delegate.feedID
|
||||
}
|
||||
|
@ -8,13 +8,18 @@
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import Account
|
||||
|
||||
final class SmartFeedsController: DisplayNameProvider {
|
||||
final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable {
|
||||
|
||||
var containerID: ContainerIdentifier? {
|
||||
return ContainerIdentifier.smartFeedController
|
||||
}
|
||||
|
||||
public static let shared = SmartFeedsController()
|
||||
let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title")
|
||||
|
||||
var smartFeeds = [AnyObject]()
|
||||
var smartFeeds = [Feed]()
|
||||
let todayFeed = SmartFeed(delegate: TodayFeedDelegate())
|
||||
let unreadFeed = UnreadFeed()
|
||||
let starredFeed = SmartFeed(delegate: StarredFeedDelegate())
|
||||
@ -23,14 +28,19 @@ final class SmartFeedsController: DisplayNameProvider {
|
||||
self.smartFeeds = [todayFeed, unreadFeed, starredFeed]
|
||||
}
|
||||
|
||||
func find(by identifier: String) -> PseudoFeed? {
|
||||
func find(by identifier: FeedIdentifier) -> PseudoFeed? {
|
||||
switch identifier {
|
||||
case String(describing: TodayFeedDelegate.self):
|
||||
return todayFeed
|
||||
case String(describing: UnreadFeed.self):
|
||||
return unreadFeed
|
||||
case String(describing: StarredFeedDelegate.self):
|
||||
return starredFeed
|
||||
case .smartFeed(let stringIdentifer):
|
||||
switch stringIdentifer {
|
||||
case String(describing: TodayFeedDelegate.self):
|
||||
return todayFeed
|
||||
case String(describing: UnreadFeed.self):
|
||||
return unreadFeed
|
||||
case String(describing: StarredFeedDelegate.self):
|
||||
return starredFeed
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
@ -19,6 +19,10 @@ import Articles
|
||||
|
||||
final class UnreadFeed: PseudoFeed {
|
||||
|
||||
public var defaultReadFilterType: ReadFilterType {
|
||||
return .alwaysRead
|
||||
}
|
||||
|
||||
var feedID: FeedIdentifier? {
|
||||
return FeedIdentifier.smartFeed(String(describing: UnreadFeed.self))
|
||||
}
|
||||
|
@ -19,14 +19,16 @@ typealias FetchRequestOperationResultBlock = (Set<Article>, FetchRequestOperatio
|
||||
final class FetchRequestOperation {
|
||||
|
||||
let id: Int
|
||||
let readFilter: Bool
|
||||
let resultBlock: FetchRequestOperationResultBlock
|
||||
var isCanceled = false
|
||||
var isFinished = false
|
||||
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)
|
||||
self.id = id
|
||||
self.readFilter = readFilter
|
||||
self.representedObjects = representedObjects
|
||||
self.resultBlock = resultBlock
|
||||
}
|
||||
@ -60,25 +62,38 @@ final class FetchRequestOperation {
|
||||
let numberOfFetchers = articleFetchers.count
|
||||
var fetchersReturned = 0
|
||||
var fetchedArticles = Set<Article>()
|
||||
for articleFetcher in articleFetchers {
|
||||
articleFetcher.fetchArticlesAsync { (articles) in
|
||||
precondition(Thread.isMainThread)
|
||||
guard !self.isCanceled else {
|
||||
callCompletionIfNeeded()
|
||||
return
|
||||
}
|
||||
|
||||
assert(!self.isFinished)
|
||||
|
||||
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()
|
||||
fetchedArticles.formUnion(articles)
|
||||
fetchersReturned += 1
|
||||
if fetchersReturned == numberOfFetchers {
|
||||
self.isFinished = true
|
||||
self.resultBlock(fetchedArticles, self)
|
||||
callCompletionIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
for articleFetcher in articleFetchers {
|
||||
if readFilter {
|
||||
articleFetcher.fetchUnreadArticlesAsync { (articles) in
|
||||
process(articles: articles)
|
||||
}
|
||||
} else {
|
||||
articleFetcher.fetchArticlesAsync { (articles) in
|
||||
process(articles: articles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,9 @@ import Account
|
||||
|
||||
final class WebFeedTreeControllerDelegate: TreeControllerDelegate {
|
||||
|
||||
var isReadFiltered = false
|
||||
|
||||
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||
|
||||
if node.isRoot {
|
||||
return childNodesForRootNode(node)
|
||||
}
|
||||
@ -32,29 +33,47 @@ final class WebFeedTreeControllerDelegate: TreeControllerDelegate {
|
||||
private extension WebFeedTreeControllerDelegate {
|
||||
|
||||
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)
|
||||
smartFeedsNode.canHaveChildNodes = true
|
||||
smartFeedsNode.isGroupItem = true
|
||||
|
||||
return [smartFeedsNode] + sortedAccountNodes(rootNode)
|
||||
topLevelNodes.append(contentsOf: sortedAccountNodes(rootNode))
|
||||
|
||||
return topLevelNodes
|
||||
}
|
||||
|
||||
func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] {
|
||||
|
||||
return SmartFeedsController.shared.smartFeeds.map { parentNode.existingOrNewChildNode(with: $0) }
|
||||
return SmartFeedsController.shared.smartFeeds.compactMap { (feed) -> Node? in
|
||||
if isReadFiltered && feed.unreadCount == 0 {
|
||||
return nil
|
||||
}
|
||||
return parentNode.existingOrNewChildNode(with: feed as AnyObject)
|
||||
}
|
||||
}
|
||||
|
||||
func childNodesForContainerNode(_ containerNode: Node) -> [Node]? {
|
||||
|
||||
let container = containerNode.representedObject as! Container
|
||||
|
||||
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 {
|
||||
children.append(contentsOf: Array(folders))
|
||||
for folder in folders {
|
||||
if !(isReadFiltered && folder.unreadCount == 0) {
|
||||
children.append(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updatedChildNodes = [Node]()
|
||||
@ -77,13 +96,14 @@ private extension WebFeedTreeControllerDelegate {
|
||||
}
|
||||
|
||||
func createNode(representedObject: Any, parent: Node) -> Node? {
|
||||
|
||||
if let webFeed = representedObject as? WebFeed {
|
||||
return createNode(webFeed: webFeed, parent: parent)
|
||||
}
|
||||
|
||||
if let folder = representedObject as? Folder {
|
||||
return createNode(folder: folder, parent: parent)
|
||||
}
|
||||
|
||||
if let account = representedObject as? Account {
|
||||
return createNode(account: account, parent: parent)
|
||||
}
|
||||
@ -92,19 +112,16 @@ private extension WebFeedTreeControllerDelegate {
|
||||
}
|
||||
|
||||
func createNode(webFeed: WebFeed, parent: Node) -> Node {
|
||||
|
||||
return parent.createChildNode(webFeed)
|
||||
}
|
||||
|
||||
func createNode(folder: Folder, parent: Node) -> Node {
|
||||
|
||||
let node = parent.createChildNode(folder)
|
||||
node.canHaveChildNodes = true
|
||||
return node
|
||||
}
|
||||
|
||||
func createNode(account: Account, parent: Node) -> Node {
|
||||
|
||||
let node = parent.createChildNode(account)
|
||||
node.canHaveChildNodes = true
|
||||
node.isGroupItem = true
|
||||
@ -112,8 +129,10 @@ private extension WebFeedTreeControllerDelegate {
|
||||
}
|
||||
|
||||
func sortedAccountNodes(_ parent: Node) -> [Node] {
|
||||
|
||||
let nodes = AccountManager.shared.sortedActiveAccounts.map { (account) -> Node in
|
||||
let nodes = AccountManager.shared.sortedActiveAccounts.compactMap { (account) -> Node? in
|
||||
if isReadFiltered && account.unreadCount == 0 {
|
||||
return nil
|
||||
}
|
||||
let accountNode = parent.existingOrNewChildNode(with: account)
|
||||
accountNode.canHaveChildNodes = true
|
||||
accountNode.isGroupItem = true
|
||||
@ -123,7 +142,6 @@ private extension WebFeedTreeControllerDelegate {
|
||||
}
|
||||
|
||||
func nodeInArrayRepresentingObject(_ nodes: [Node], _ representedObject: AnyObject) -> Node? {
|
||||
|
||||
for oneNode in nodes {
|
||||
if oneNode.representedObject === representedObject {
|
||||
return oneNode
|
||||
|
@ -44,7 +44,7 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
}
|
||||
|
||||
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? {
|
||||
|
@ -36,7 +36,7 @@ class LocalAccountViewController: UITableViewController {
|
||||
}
|
||||
|
||||
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? {
|
||||
|
@ -89,6 +89,14 @@ struct AppAssets {
|
||||
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 = {
|
||||
return UIColor(named: "fullScreenBackgroundColor")!
|
||||
}()
|
||||
|
@ -23,6 +23,7 @@ struct AppDefaults {
|
||||
static let timelineNumberOfLines = "timelineNumberOfLines"
|
||||
static let timelineIconSize = "timelineIconSize"
|
||||
static let timelineSortDirection = "timelineSortDirection"
|
||||
static let articleFullscreenEnabled = "articleFullscreenEnabled"
|
||||
static let displayUndoAvailableTip = "displayUndoAvailableTip"
|
||||
static let lastRefresh = "lastRefresh"
|
||||
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 {
|
||||
get {
|
||||
return bool(for: Key.displayUndoAvailableTip)
|
||||
@ -135,6 +145,7 @@ struct AppDefaults {
|
||||
Key.timelineNumberOfLines: 2,
|
||||
Key.timelineIconSize: IconSize.medium.rawValue,
|
||||
Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue,
|
||||
Key.articleFullscreenEnabled: false,
|
||||
Key.displayUndoAvailableTip: true]
|
||||
AppDefaults.shared.register(defaults: defaults)
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ class ArticleViewController: UIViewController {
|
||||
@IBOutlet private weak var webViewContainer: UIView!
|
||||
@IBOutlet private weak var showNavigationView: UIView!
|
||||
@IBOutlet private weak var showToolbarView: UIView!
|
||||
@IBOutlet private weak var showNavigationViewConstraint: NSLayoutConstraint!
|
||||
@IBOutlet private weak var showToolbarViewConstraint: NSLayoutConstraint!
|
||||
|
||||
private var articleExtractorButton: ArticleExtractorButton = {
|
||||
let button = ArticleExtractorButton(type: .system)
|
||||
@ -59,6 +61,8 @@ class ArticleViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
var restoreOffset = 0
|
||||
|
||||
var currentArticle: Article? {
|
||||
switch state {
|
||||
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) {
|
||||
super.viewDidAppear(true)
|
||||
coordinator.isArticleViewControllerPending = false
|
||||
}
|
||||
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
// This will animate if the show/hide bars animation is happening.
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
func updateUI() {
|
||||
|
||||
guard let article = currentArticle else {
|
||||
@ -190,9 +206,11 @@ class ArticleViewController: UIViewController {
|
||||
var render = "error();"
|
||||
if let data = try? encoder.encode(templateData) {
|
||||
let json = String(data: data, encoding: .utf8)!
|
||||
render = "render(\(json));"
|
||||
render = "render(\(json), \(restoreOffset));"
|
||||
}
|
||||
|
||||
restoreOffset = 0
|
||||
|
||||
ArticleViewControllerWebViewProvider.shared.articleIconSchemeHandler.currentArticle = currentArticle
|
||||
webView?.scrollView.setZoomScale(1.0, animated: false)
|
||||
webView?.evaluateJavaScript(render)
|
||||
@ -231,7 +249,10 @@ class ArticleViewController: UIViewController {
|
||||
}
|
||||
|
||||
@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
|
||||
@ -319,6 +340,21 @@ class ArticleViewController: UIViewController {
|
||||
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
|
||||
@ -357,14 +393,6 @@ extension ArticleViewController: WKNavigationDelegate {
|
||||
|
||||
}
|
||||
|
||||
// MARK: InteractiveNavigationControllerTappable
|
||||
|
||||
extension ArticleViewController: InteractiveNavigationControllerTappable {
|
||||
func didTapNavigationBar() {
|
||||
hideBars()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: WKUIDelegate
|
||||
|
||||
extension ArticleViewController: WKUIDelegate {
|
||||
@ -466,7 +494,10 @@ private extension ArticleViewController {
|
||||
|
||||
func showBars() {
|
||||
if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed {
|
||||
AppDefaults.articleFullscreenEnabled = false
|
||||
coordinator.showStatusBar()
|
||||
showNavigationViewConstraint.constant = 0
|
||||
showToolbarViewConstraint.constant = 0
|
||||
navigationController?.setNavigationBarHidden(false, animated: true)
|
||||
navigationController?.setToolbarHidden(false, animated: true)
|
||||
}
|
||||
@ -474,7 +505,10 @@ private extension ArticleViewController {
|
||||
|
||||
func hideBars() {
|
||||
if traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed {
|
||||
AppDefaults.articleFullscreenEnabled = true
|
||||
coordinator.hideStatusBar()
|
||||
showNavigationViewConstraint.constant = 44.0
|
||||
showToolbarViewConstraint.constant = 44.0
|
||||
navigationController?.setNavigationBarHidden(true, animated: true)
|
||||
navigationController?.setToolbarHidden(true, animated: true)
|
||||
}
|
||||
|
@ -1,8 +1,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"/>
|
||||
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@ -70,7 +70,7 @@
|
||||
</toolbarItems>
|
||||
<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="rightBarButtonItem" title="Edit" id="Khk-Hd-iNS"/>
|
||||
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="Khk-Hd-iNS"/>
|
||||
</navigationItem>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
</tableViewController>
|
||||
@ -114,5 +114,6 @@
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="gear" catalog="system" width="64" height="58"/>
|
||||
<image name="line.horizontal.3.decrease.circle" catalog="system" width="64" height="60"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
@ -1,8 +1,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"/>
|
||||
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@ -70,7 +70,7 @@
|
||||
</toolbarItems>
|
||||
<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="rightBarButtonItem" title="Edit" id="Khk-Hd-iNS"/>
|
||||
<barButtonItem key="rightBarButtonItem" image="line.horizontal.3.decrease.circle" catalog="system" id="Khk-Hd-iNS"/>
|
||||
</navigationItem>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
</tableViewController>
|
||||
@ -114,5 +114,6 @@
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="gear" catalog="system" width="64" height="58"/>
|
||||
<image name="line.horizontal.3.decrease.circle" catalog="system" width="64" height="60"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
@ -21,14 +21,14 @@
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
</view>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="100" id="xX2-AK-xJX"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<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"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="100" id="3HX-Dm-bA6"/>
|
||||
@ -37,14 +37,14 @@
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<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="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="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="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="A7j-8T-DqE" firstAttribute="leading" secondItem="VUw-jc-0yf" secondAttribute="leading" id="wny-M6-akA"/>
|
||||
</constraints>
|
||||
@ -121,7 +121,9 @@
|
||||
<outlet property="prevArticleBarButtonItem" destination="v4j-fq-23N" id="Gny-Oh-cQa"/>
|
||||
<outlet property="readBarButtonItem" destination="hy0-LS-MzE" id="BzM-x9-tuj"/>
|
||||
<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="showToolbarViewConstraint" destination="4fZ-pn-fmB" id="ayD-Mq-kft"/>
|
||||
<outlet property="starBarButtonItem" destination="wU4-eH-wC9" id="Z8Q-Lt-dKk"/>
|
||||
<outlet property="webViewContainer" destination="DNb-lt-KzC" id="Fc1-Ae-pWK"/>
|
||||
</connections>
|
||||
@ -167,10 +169,17 @@
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</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"/>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
|
||||
<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="markAllAsReadButton" destination="fTv-eX-72r" id="12S-lN-Sxa"/>
|
||||
</connections>
|
||||
@ -214,9 +223,17 @@
|
||||
<action selector="settings:" destination="7bK-jq-Zjz" id="Y8a-lz-Im7"/>
|
||||
</connections>
|
||||
</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>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="filterButton" destination="ZJu-oJ-c1R" id="jiO-wg-qrG"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@ -289,6 +306,7 @@
|
||||
<image name="chevron.up" catalog="system" width="64" height="36"/>
|
||||
<image name="circle" catalog="system" width="64" height="60"/>
|
||||
<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="square.and.arrow.up" catalog="system" width="56" height="64"/>
|
||||
<image name="square.and.arrow.up.fill" catalog="system" width="56" height="64"/>
|
||||
|
@ -128,7 +128,7 @@ extension AccountInspectorViewController {
|
||||
}
|
||||
|
||||
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? {
|
||||
|
@ -78,7 +78,7 @@ class WebFeedInspectorViewController: UITableViewController {
|
||||
extension WebFeedInspectorViewController {
|
||||
|
||||
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? {
|
||||
|
@ -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
|
||||
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewCellLayout.verticalPadding)
|
||||
@ -117,6 +117,12 @@ struct MasterFeedTableViewCellLayout {
|
||||
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
|
||||
let separatorInset = MasterFeedTableViewCellLayout.disclosureButtonSize.width
|
||||
separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5)
|
||||
|
@ -57,7 +57,7 @@ struct MasterFeedTableViewSectionHeaderLayout {
|
||||
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 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
|
||||
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MasterFeedTableViewSectionHeaderLayout.verticalPadding)
|
||||
@ -74,6 +74,11 @@ struct MasterFeedTableViewSectionHeaderLayout {
|
||||
}
|
||||
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
|
||||
self.height = cellHeight
|
||||
self.unreadCountRect = rUnread
|
||||
|
@ -13,16 +13,6 @@ import Account
|
||||
|
||||
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 {
|
||||
guard let node = itemIdentifier(for: indexPath), !(node.representedObject is PseudoFeed) else {
|
||||
return false
|
||||
@ -30,140 +20,4 @@ class MasterFeedDataSource: UITableViewDiffableDataSource<Node, Node> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
33
iOS/MasterFeed/MasterFeedViewController+Drag.swift
Normal file
33
iOS/MasterFeed/MasterFeedViewController+Drag.swift
Normal 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]
|
||||
}
|
||||
|
||||
}
|
195
iOS/MasterFeed/MasterFeedViewController+Drop.swift
Normal file
195
iOS/MasterFeed/MasterFeedViewController+Drop.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -14,10 +14,11 @@ import RSTree
|
||||
|
||||
class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
@IBOutlet weak var filterButton: UIBarButtonItem!
|
||||
private var refreshProgressView: RefreshProgressView?
|
||||
private var addNewItemButton: UIBarButtonItem!
|
||||
|
||||
private lazy var dataSource = makeDataSource()
|
||||
lazy var dataSource = makeDataSource()
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
weak var coordinator: SceneCoordinator!
|
||||
|
||||
@ -38,11 +39,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
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
|
||||
// that makes a gap between the first section header and the navigation bar
|
||||
var frame = CGRect.zero
|
||||
@ -51,6 +47,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
tableView.dataSource = dataSource
|
||||
tableView.dragDelegate = self
|
||||
tableView.dropDelegate = self
|
||||
tableView.dragInteractionEnabled = true
|
||||
resetEstimatedRowHeight()
|
||||
tableView.separatorStyle = .none
|
||||
|
||||
@ -192,7 +191,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
headerView.tag = section
|
||||
headerView.disclosureExpanded = sectionNode.isExpanded
|
||||
headerView.disclosureExpanded = coordinator.isExpanded(sectionNode)
|
||||
|
||||
if section == tableView.numberOfSections - 1 {
|
||||
headerView.isLastSection = true
|
||||
@ -292,11 +291,22 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return nil
|
||||
}
|
||||
if node.representedObject is WebFeed {
|
||||
return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
||||
return makeFeedContextMenu(node: node, indexPath: indexPath, includeDeleteRename: true)
|
||||
} 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) {
|
||||
becomeFirstResponder()
|
||||
@ -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 destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !destNode.isExpanded) {
|
||||
let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0
|
||||
return IndexPath(row: destIndexPath.row + movementAdjustment, section: destIndexPath.section)
|
||||
if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !coordinator.isExpanded(destNode)) {
|
||||
return proposedDestinationIndexPath
|
||||
}
|
||||
|
||||
// If we are dragging around in the same container, just return the original source
|
||||
@ -351,14 +360,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
} else {
|
||||
|
||||
sortedNodes.remove(at: index)
|
||||
|
||||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
let adjustedIndex = index - movementAdjustment
|
||||
if adjustedIndex >= sortedNodes.count {
|
||||
|
||||
if index >= sortedNodes.count {
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
||||
@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) {
|
||||
coordinator.showAdd(.feed)
|
||||
}
|
||||
@ -384,7 +403,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return
|
||||
}
|
||||
|
||||
if sectionNode.isExpanded {
|
||||
if coordinator.isExpanded(sectionNode) {
|
||||
headerView.disclosureExpanded = false
|
||||
coordinator.collapse(sectionNode)
|
||||
self.applyChanges(animated: true)
|
||||
@ -501,7 +520,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return
|
||||
}
|
||||
|
||||
if !sectionNode.isExpanded {
|
||||
if !coordinator.isExpanded(sectionNode) {
|
||||
coordinator.expand(sectionNode)
|
||||
self.applyChanges(animated: true) {
|
||||
completion?()
|
||||
@ -561,12 +580,22 @@ extension MasterFeedViewController: UIContextMenuInteractionDelegate {
|
||||
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 deactivateAction = self.deactivateAccountAction(account: account)
|
||||
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
|
||||
@ -630,11 +659,13 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
|
||||
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
|
||||
self?.configure(cell, node)
|
||||
return cell
|
||||
})
|
||||
dataSource.defaultRowAnimation = .middle
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func resetEstimatedRowHeight() {
|
||||
@ -656,7 +687,7 @@ private extension MasterFeedViewController {
|
||||
} else {
|
||||
cell.indentationLevel = 1
|
||||
}
|
||||
cell.setDisclosure(isExpanded: node.isExpanded, animated: false)
|
||||
cell.setDisclosure(isExpanded: coordinator.isExpanded(node), animated: false)
|
||||
cell.isDisclosureAvailable = node.canHaveChildNodes
|
||||
|
||||
cell.name = nameFor(node)
|
||||
@ -772,8 +803,8 @@ private extension MasterFeedViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
func makeFeedContextMenu(node: Node, indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: node.uniqueID as NSCopying, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
@ -806,8 +837,8 @@ private extension MasterFeedViewController {
|
||||
|
||||
}
|
||||
|
||||
func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
func makeFolderContextMenu(node: Node, indexPath: IndexPath) -> UIContextMenuConfiguration {
|
||||
return UIContextMenuConfiguration(identifier: node.uniqueID as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in
|
||||
|
||||
guard let self = self else { return nil }
|
||||
|
||||
|
@ -9,13 +9,6 @@
|
||||
import UIKit
|
||||
|
||||
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 {
|
||||
return true
|
||||
|
@ -13,10 +13,11 @@ import Articles
|
||||
|
||||
class MasterTimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
private var titleView: MasterTimelineTitleView?
|
||||
private var numberOfTextLines = 0
|
||||
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 firstUnreadButton: UIBarButtonItem!
|
||||
|
||||
@ -68,12 +69,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
iconSize = AppDefaults.timelineIconSize
|
||||
resetEstimatedRowHeight()
|
||||
|
||||
if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView {
|
||||
navigationItem.titleView = titleView
|
||||
}
|
||||
|
||||
resetUI()
|
||||
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
|
||||
if let restoreIndexPath = coordinator.timelineMiddleIndexPath {
|
||||
tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false)
|
||||
@ -81,13 +83,38 @@ 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) {
|
||||
super.viewDidAppear(true)
|
||||
coordinator.isTimelineViewControllerPending = false
|
||||
|
||||
if navigationController?.navigationBar.alpha == 0 {
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
self.navigationController?.navigationBar.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if coordinator.displayUndoAvailableTip {
|
||||
let alertController = UndoAvailableAlertController.alert { [weak self] _ in
|
||||
@ -129,10 +156,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
|
||||
// MARK: API
|
||||
|
||||
func restoreTimelinePosition() {
|
||||
|
||||
}
|
||||
|
||||
func restoreSelectionIfNecessary(adjustScroll: Bool) {
|
||||
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
|
||||
if adjustScroll {
|
||||
@ -263,7 +286,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
|
||||
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 }
|
||||
|
||||
@ -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) {
|
||||
becomeFirstResponder()
|
||||
let article = dataSource.itemIdentifier(for: indexPath)
|
||||
@ -328,7 +360,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
}
|
||||
|
||||
@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 {
|
||||
return
|
||||
}
|
||||
@ -359,7 +395,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
}
|
||||
|
||||
@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 {
|
||||
queueReloadAvailableCells()
|
||||
}
|
||||
@ -379,7 +417,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
||||
}
|
||||
|
||||
@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() {
|
||||
@ -466,24 +506,34 @@ extension MasterTimelineViewController: UISearchBarDelegate {
|
||||
private extension MasterTimelineViewController {
|
||||
|
||||
func resetUI() {
|
||||
title = coordinator.timelineFeed?.nameForDisplay
|
||||
|
||||
if let titleView = Bundle.main.loadNibNamed("MasterTimelineTitleView", owner: self, options: nil)?[0] as? MasterTimelineTitleView {
|
||||
self.titleView = titleView
|
||||
|
||||
title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline"
|
||||
|
||||
if let titleView = navigationItem.titleView as? MasterTimelineTitleView {
|
||||
titleView.iconView.iconImage = coordinator.timelineIconImage
|
||||
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
|
||||
updateTitleUnreadCount()
|
||||
|
||||
if coordinator.timelineFeed is WebFeed {
|
||||
titleView.heightAnchor.constraint(equalToConstant: 44.0).isActive = true
|
||||
let tap = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:)))
|
||||
titleView.addGestureRecognizer(tap)
|
||||
titleView.addGestureRecognizer(feedTapGestureRecognizer)
|
||||
} else {
|
||||
titleView.removeGestureRecognizer(feedTapGestureRecognizer)
|
||||
}
|
||||
|
||||
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)
|
||||
if dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 {
|
||||
@ -505,7 +555,9 @@ private extension MasterTimelineViewController {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -521,12 +573,12 @@ private extension MasterTimelineViewController {
|
||||
|
||||
func makeDataSource() -> 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
|
||||
self?.configure(cell, article: article)
|
||||
return cell
|
||||
})
|
||||
dataSource.defaultRowAnimation = .left
|
||||
dataSource.defaultRowAnimation = .middle
|
||||
return dataSource
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,7 @@ body .headerTable {
|
||||
border-bottom: 1px solid var(--header-table-border-color);
|
||||
}
|
||||
body .header {
|
||||
font: -apple-system-body;
|
||||
color: var(--header-color);
|
||||
}
|
||||
body .header a:link, body .header a:visited {
|
||||
|
@ -22,9 +22,7 @@ class RootSplitViewController: UISplitViewController {
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
if UIApplication.shared.applicationState != .background {
|
||||
self.coordinator.configureThreePanelMode(for: size)
|
||||
}
|
||||
self.coordinator.configurePanelMode(for: size)
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,11 @@ import Articles
|
||||
import RSCore
|
||||
import RSTree
|
||||
|
||||
enum PanelMode {
|
||||
case unset
|
||||
case three
|
||||
case standard
|
||||
}
|
||||
enum SearchScope: Int {
|
||||
case timeline = 0
|
||||
case global = 1
|
||||
@ -25,6 +30,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
return rootSplitViewController.undoManager
|
||||
}
|
||||
|
||||
private var panelMode: PanelMode = .unset
|
||||
|
||||
private var activityManager = ActivityManager()
|
||||
|
||||
private var isShowingExtractedArticle = false
|
||||
@ -34,10 +41,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
private var masterNavigationController: UINavigationController!
|
||||
private var masterFeedViewController: MasterFeedViewController!
|
||||
private var masterTimelineViewController: MasterTimelineViewController?
|
||||
|
||||
private var subSplitViewController: UISplitViewController? {
|
||||
return rootSplitViewController.children.last as? UISplitViewController
|
||||
}
|
||||
private var subSplitViewController: UISplitViewController?
|
||||
|
||||
private var articleViewController: ArticleViewController? {
|
||||
if let detail = masterNavigationController.viewControllers.last as? ArticleViewController {
|
||||
@ -55,11 +59,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
return nil
|
||||
}
|
||||
|
||||
private var wasRootSplitViewControllerCollapsed = false
|
||||
|
||||
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
||||
private var fetchSerialNumber = 0
|
||||
private let fetchRequestQueue = FetchRequestQueue()
|
||||
|
||||
private var animatingChanges = false
|
||||
private var expandedTable = Set<ContainerIdentifier>()
|
||||
private var shadowTable = [[Node]]()
|
||||
private var lastSearchString = ""
|
||||
private var lastSearchScope: SearchScope? = nil
|
||||
@ -103,9 +110,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
}
|
||||
|
||||
var isThreePanelMode: Bool {
|
||||
return subSplitViewController != nil
|
||||
return panelMode == .three
|
||||
}
|
||||
|
||||
var isUnreadFeedsFiltered: Bool {
|
||||
return treeControllerDelegate.isReadFiltered
|
||||
}
|
||||
|
||||
var articleReadFilterType: ReadFilterType = .none
|
||||
|
||||
var rootNode: Node {
|
||||
return treeController.rootNode
|
||||
}
|
||||
@ -257,8 +270,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
super.init()
|
||||
|
||||
for section in treeController.rootNode.childNodes {
|
||||
section.isExpanded = true
|
||||
for sectionNode in treeController.rootNode.childNodes {
|
||||
markExpanded(sectionNode)
|
||||
shadowTable.append([Node]())
|
||||
}
|
||||
|
||||
@ -297,7 +310,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: true)
|
||||
rootSplitViewController.showDetailViewController(detailNavigationController, sender: self)
|
||||
|
||||
configureThreePanelMode(for: size)
|
||||
configurePanelMode(for: size)
|
||||
|
||||
return rootSplitViewController
|
||||
}
|
||||
@ -325,19 +338,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
handleReadArticle(userInfo)
|
||||
}
|
||||
|
||||
func configureThreePanelMode(for size: CGSize) {
|
||||
guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad && !rootSplitViewController.isCollapsed else {
|
||||
func configurePanelMode(for size: CGSize) {
|
||||
guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad else {
|
||||
return
|
||||
}
|
||||
|
||||
if (size.width / size.height) > 1.2 {
|
||||
if !isThreePanelMode {
|
||||
transitionToThreePanelMode()
|
||||
if panelMode == .unset || panelMode == .standard {
|
||||
panelMode = .three
|
||||
configureThreePanelMode()
|
||||
}
|
||||
} else {
|
||||
if isThreePanelMode {
|
||||
transitionFromThreePanelMode()
|
||||
if panelMode == .unset || panelMode == .three {
|
||||
panelMode = .standard
|
||||
configureStandardPanelMode()
|
||||
}
|
||||
}
|
||||
|
||||
wasRootSplitViewControllerCollapsed = rootSplitViewController.isCollapsed
|
||||
}
|
||||
|
||||
func selectFirstUnreadInAllUnread() {
|
||||
@ -363,7 +381,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
@objc func containerChildrenDidChange(_ note: Notification) {
|
||||
if timelineFetcherContainsAnyPseudoFeed() || timelineFetcherContainsAnyFolder() {
|
||||
fetchAndReplaceArticlesAsync() {}
|
||||
refreshTimeline()
|
||||
}
|
||||
rebuildBackingStores()
|
||||
}
|
||||
@ -377,60 +395,58 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
}
|
||||
|
||||
@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() {
|
||||
fetchAndReplaceArticlesAsync {
|
||||
rebuildAndExpand()
|
||||
fetchAndReplaceArticlesAsync(animated: true) {
|
||||
self.masterTimelineViewController?.reinitializeArticles()
|
||||
self.rebuildBackingStores()
|
||||
}
|
||||
} else {
|
||||
rebuildAndExpand()
|
||||
rebuildBackingStores()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc func userDidAddAccount(_ note: Notification) {
|
||||
|
||||
let rebuildAndExpand = {
|
||||
self.rebuildBackingStores() {
|
||||
// Automatically expand any new accounts
|
||||
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
|
||||
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
||||
node.isExpanded = true
|
||||
}
|
||||
let expandNewAccount = {
|
||||
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
|
||||
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
|
||||
self.markExpanded(node)
|
||||
}
|
||||
}
|
||||
|
||||
if timelineFetcherContainsAnyPseudoFeed() {
|
||||
fetchAndReplaceArticlesAsync {
|
||||
rebuildAndExpand()
|
||||
fetchAndReplaceArticlesAsync(animated: true) {
|
||||
self.masterTimelineViewController?.reinitializeArticles()
|
||||
self.rebuildBackingStores() {
|
||||
expandNewAccount()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rebuildAndExpand()
|
||||
rebuildBackingStores() {
|
||||
expandNewAccount()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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() {
|
||||
fetchAndReplaceArticlesAsync {
|
||||
self.rebuildBackingStores()
|
||||
fetchAndReplaceArticlesAsync(animated: true) {
|
||||
self.masterTimelineViewController?.reinitializeArticles()
|
||||
self.rebuildBackingStores() {
|
||||
cleanupAccount()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rebuildBackingStores()
|
||||
rebuildBackingStores() {
|
||||
cleanupAccount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -473,9 +489,54 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
}
|
||||
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) {
|
||||
node.isExpanded = true
|
||||
markExpanded(node)
|
||||
animatingChanges = true
|
||||
rebuildShadowTable()
|
||||
animatingChanges = false
|
||||
@ -483,10 +544,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
func expandAllSectionsAndFolders() {
|
||||
for sectionNode in treeController.rootNode.childNodes {
|
||||
sectionNode.isExpanded = true
|
||||
markExpanded(sectionNode)
|
||||
for topLevelNode in sectionNode.childNodes {
|
||||
if topLevelNode.representedObject is Folder {
|
||||
topLevelNode.isExpanded = true
|
||||
markExpanded(topLevelNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -496,7 +557,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
}
|
||||
|
||||
func collapse(_ node: Node) {
|
||||
node.isExpanded = false
|
||||
unmarkExpanded(node)
|
||||
animatingChanges = true
|
||||
rebuildShadowTable()
|
||||
animatingChanges = false
|
||||
@ -504,10 +565,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
func collapseAllFolders() {
|
||||
for sectionNode in treeController.rootNode.childNodes {
|
||||
sectionNode.isExpanded = true
|
||||
unmarkExpanded(sectionNode)
|
||||
for topLevelNode in sectionNode.childNodes {
|
||||
if topLevelNode.representedObject is Folder {
|
||||
topLevelNode.isExpanded = true
|
||||
unmarkExpanded(topLevelNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -523,7 +584,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
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 {
|
||||
completion?()
|
||||
return
|
||||
@ -533,19 +594,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
masterFeedViewController.updateFeedSelection(animated: animated)
|
||||
|
||||
emptyTheTimeline()
|
||||
selectArticle(nil)
|
||||
if deselectArticle {
|
||||
selectArticle(nil)
|
||||
}
|
||||
|
||||
if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
|
||||
|
||||
self.activityManager.selecting(feed: feed)
|
||||
self.installTimelineControllerIfNecessary(animated: animated)
|
||||
setTimelineFeed(feed) {
|
||||
setTimelineFeed(feed, animated: false) {
|
||||
completion?()
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
setTimelineFeed(nil) {
|
||||
setTimelineFeed(nil, animated: false) {
|
||||
self.activityManager.invalidateSelecting()
|
||||
if self.rootSplitViewController.isCollapsed && self.navControllerForTimeline().viewControllers.last is MasterTimelineViewController {
|
||||
self.navControllerForTimeline().popViewController(animated: animated)
|
||||
@ -608,9 +671,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
let currentArticleViewController: ArticleViewController
|
||||
if articleViewController == nil {
|
||||
currentArticleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
|
||||
currentArticleViewController.coordinator = self
|
||||
installArticleController(currentArticleViewController, animated: animated)
|
||||
currentArticleViewController = installArticleController(animated: animated)
|
||||
} else {
|
||||
currentArticleViewController = articleViewController!
|
||||
}
|
||||
@ -632,7 +693,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
isSearching = true
|
||||
savedSearchArticles = articles
|
||||
savedSearchArticleIds = Set(articles.map { $0.articleID })
|
||||
setTimelineFeed(nil)
|
||||
setTimelineFeed(nil, animated: true)
|
||||
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 {
|
||||
timelineFeed = feed
|
||||
masterTimelineViewController?.reinitializeArticles()
|
||||
replaceArticles(with: savedSearchArticles!, animate: true)
|
||||
replaceArticles(with: savedSearchArticles!, animated: true)
|
||||
} else {
|
||||
setTimelineFeed(nil)
|
||||
setTimelineFeed(nil, animated: true)
|
||||
}
|
||||
|
||||
lastSearchString = ""
|
||||
@ -658,7 +719,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
guard isSearching else { return }
|
||||
|
||||
if searchString.count < 3 {
|
||||
setTimelineFeed(nil)
|
||||
setTimelineFeed(nil, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
@ -666,9 +727,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
switch searchScope {
|
||||
case .global:
|
||||
setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString)))
|
||||
setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString)), animated: true)
|
||||
case .timeline:
|
||||
setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!)))
|
||||
setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!)), animated: true)
|
||||
}
|
||||
|
||||
lastSearchString = searchString
|
||||
@ -724,9 +785,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
return
|
||||
}
|
||||
|
||||
selectNextUnreadFeedFetcher()
|
||||
if selectNextUnreadArticleInTimeline() {
|
||||
activityManager.selectingNextUnread()
|
||||
selectNextUnreadFeed() {
|
||||
if self.selectNextUnreadArticleInTimeline() {
|
||||
self.activityManager.selectingNextUnread()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -963,15 +1025,36 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
extension SceneCoordinator: UISplitViewControllerDelegate {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
@ -986,7 +1069,6 @@ extension SceneCoordinator: UINavigationControllerDelegate {
|
||||
if UIApplication.shared.applicationState == .background {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// If we are showing the Feeds and only the feeds start clearing stuff
|
||||
if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending {
|
||||
@ -1055,7 +1137,7 @@ private extension SceneCoordinator {
|
||||
}
|
||||
unreadCount = count
|
||||
}
|
||||
|
||||
|
||||
func rebuildBackingStores(_ updateExpandedNodes: (() -> Void)? = nil) {
|
||||
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
||||
treeController.rebuild()
|
||||
@ -1073,10 +1155,10 @@ private extension SceneCoordinator {
|
||||
var result = [Node]()
|
||||
let sectionNode = treeController.rootNode.childAtIndex(i)!
|
||||
|
||||
if sectionNode.isExpanded {
|
||||
if isExpanded(sectionNode) {
|
||||
for node in sectionNode.childNodes {
|
||||
result.append(node)
|
||||
if node.isExpanded {
|
||||
if isExpanded(node) {
|
||||
for child in node.childNodes {
|
||||
result.append(child)
|
||||
}
|
||||
@ -1112,11 +1194,12 @@ private extension SceneCoordinator {
|
||||
return indexPathFor(node)
|
||||
}
|
||||
|
||||
func setTimelineFeed(_ feed: Feed?, completion: (() -> Void)? = nil) {
|
||||
func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
timelineFeed = feed
|
||||
timelineMiddleIndexPath = nil
|
||||
articleReadFilterType = feed?.defaultReadFilterType ?? .none
|
||||
|
||||
fetchAndReplaceArticlesAsync {
|
||||
fetchAndReplaceArticlesAsync(animated: animated) {
|
||||
self.masterTimelineViewController?.reinitializeArticles()
|
||||
completion?()
|
||||
}
|
||||
@ -1234,7 +1317,7 @@ private extension SceneCoordinator {
|
||||
return true
|
||||
}
|
||||
|
||||
if node.isExpanded {
|
||||
if isExpanded(node) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1289,7 +1372,7 @@ private extension SceneCoordinator {
|
||||
|
||||
}
|
||||
|
||||
func selectNextUnreadFeedFetcher() {
|
||||
func selectNextUnreadFeed(completion: @escaping () -> Void) {
|
||||
|
||||
let indexPath: IndexPath = {
|
||||
if currentFeedIndexPath == nil {
|
||||
@ -1312,15 +1395,19 @@ private extension SceneCoordinator {
|
||||
}
|
||||
}()
|
||||
|
||||
if selectNextUnreadFeedFetcher(startingWith: nextIndexPath) {
|
||||
return
|
||||
selectNextUnreadFeed(startingWith: nextIndexPath) { found in
|
||||
if !found {
|
||||
self.selectNextUnreadFeed(startingWith: IndexPath(row: 0, section: 0)) { _ in
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
selectNextUnreadFeedFetcher(startingWith: IndexPath(row: 0, section: 0))
|
||||
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectNextUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool {
|
||||
func selectNextUnreadFeed(startingWith indexPath: IndexPath, completion: @escaping (Bool) -> Void) {
|
||||
|
||||
for i in indexPath.section..<shadowTable.count {
|
||||
|
||||
@ -1337,23 +1424,27 @@ private extension SceneCoordinator {
|
||||
let nextIndexPath = IndexPath(row: j, section: i)
|
||||
guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {
|
||||
assertionFailure()
|
||||
return true
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
if node.isExpanded {
|
||||
if isExpanded(node) {
|
||||
continue
|
||||
}
|
||||
|
||||
if unreadCountProvider.unreadCount > 0 {
|
||||
selectFeed(nextIndexPath, animated: true)
|
||||
return true
|
||||
selectFeed(nextIndexPath, animated: false, deselectArticle: false) {
|
||||
self.currentArticle = nil
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false
|
||||
completion(false)
|
||||
|
||||
}
|
||||
|
||||
@ -1377,25 +1468,25 @@ private extension SceneCoordinator {
|
||||
|
||||
func emptyTheTimeline() {
|
||||
if !articles.isEmpty {
|
||||
replaceArticles(with: Set<Article>(), animate: false)
|
||||
replaceArticles(with: Set<Article>(), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
articles = sortedArticles
|
||||
updateShowNamesAndIcons()
|
||||
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()
|
||||
}
|
||||
|
||||
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.
|
||||
// Example: we have the Today feed selected, and the calendar day just changed.
|
||||
cancelPendingAsyncFetches()
|
||||
@ -1443,7 +1534,7 @@ private extension SceneCoordinator {
|
||||
}
|
||||
|
||||
fetchUnsortedArticlesAsync(for: [timelineFetcher]) { [weak self] (articles) in
|
||||
self?.replaceArticles(with: articles, animate: true)
|
||||
self?.replaceArticles(with: articles, animated: animated)
|
||||
completion()
|
||||
}
|
||||
|
||||
@ -1455,10 +1546,10 @@ private extension SceneCoordinator {
|
||||
precondition(Thread.isMainThread)
|
||||
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)
|
||||
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
|
||||
callback(Set<Article>())
|
||||
return
|
||||
}
|
||||
callback(articles)
|
||||
@ -1507,37 +1598,50 @@ private extension SceneCoordinator {
|
||||
|
||||
func installTimelineControllerIfNecessary(animated: Bool) {
|
||||
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
|
||||
|
||||
isTimelineViewControllerPending = true
|
||||
|
||||
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
|
||||
masterTimelineViewController!.coordinator = self
|
||||
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
|
||||
|
||||
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 {
|
||||
let controller = addNavControllerIfNecessary(articleController, showButton: false)
|
||||
subSplit.showDetailViewController(controller, sender: self)
|
||||
} else if rootSplitViewController.isCollapsed {
|
||||
let controller = addNavControllerIfNecessary(articleController, showButton: false)
|
||||
masterNavigationController.pushViewController(controller, animated: animated)
|
||||
} else if rootSplitViewController.isCollapsed || wasRootSplitViewControllerCollapsed {
|
||||
masterNavigationController.pushViewController(articleController, animated: animated)
|
||||
} else {
|
||||
let controller = addNavControllerIfNecessary(articleController, showButton: true)
|
||||
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 {
|
||||
|
||||
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
|
||||
|
||||
@ -1560,14 +1664,16 @@ private extension SceneCoordinator {
|
||||
|
||||
}
|
||||
|
||||
func configureDoubleSplit() {
|
||||
func installSubSplit() {
|
||||
rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.30
|
||||
|
||||
let subSplit = UISplitViewController.template()
|
||||
subSplit.preferredDisplayMode = .allVisible
|
||||
subSplit.preferredPrimaryColumnWidthFraction = 0.4285
|
||||
subSplitViewController = UISplitViewController()
|
||||
subSplitViewController!.preferredDisplayMode = .allVisible
|
||||
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 {
|
||||
@ -1578,67 +1684,50 @@ private extension SceneCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func transitionToThreePanelMode() -> UIViewController {
|
||||
|
||||
func configureThreePanelMode() {
|
||||
let recycledArticleController = articleViewController
|
||||
defer {
|
||||
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)
|
||||
masterTimelineViewController?.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
|
||||
masterTimelineViewController?.navigationItem.leftItemsSupplementBackButton = true
|
||||
|
||||
// Create the new sub split controller and add the timeline in the primary position
|
||||
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)
|
||||
installArticleController(recycledArticleController, animated: false)
|
||||
|
||||
masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true)
|
||||
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
|
||||
|
||||
if let subSplit = rootSplitViewController.viewControllers.last as? UISplitViewController {
|
||||
|
||||
// Push a new timeline on to the master navigation controller. For some reason recycling the timeline can freak
|
||||
// the system out and throw it into an infinite loop.
|
||||
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)
|
||||
}
|
||||
// Set the is Pending flags early to prevent the navigation controller delegate from thinking that we
|
||||
// swiping around in the user interface
|
||||
isTimelineViewControllerPending = true
|
||||
isArticleViewControllerPending = true
|
||||
|
||||
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
|
||||
@ -1646,14 +1735,14 @@ private extension SceneCoordinator {
|
||||
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
|
||||
guard let userInfo = userInfo,
|
||||
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any],
|
||||
let articleFetcherType = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
|
||||
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch articleFetcherType {
|
||||
switch feedIdentifier {
|
||||
|
||||
case .smartFeed(let identifier):
|
||||
guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return }
|
||||
case .smartFeed:
|
||||
guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return }
|
||||
if let indexPath = indexPathFor(smartFeed) {
|
||||
selectFeed(indexPath, animated: false)
|
||||
}
|
||||
@ -1706,14 +1795,14 @@ private extension SceneCoordinator {
|
||||
|
||||
func restoreFeed(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
|
||||
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : Any],
|
||||
let articleFetcherType = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
|
||||
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch articleFetcherType {
|
||||
switch feedIdentifier {
|
||||
|
||||
case .smartFeed(let identifier):
|
||||
guard let smartFeed = SmartFeedsController.shared.find(by: identifier) else { return false }
|
||||
case .smartFeed:
|
||||
guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false }
|
||||
if smartFeed.fetchArticles().contains(accountID: accountID, articleID: articleID) {
|
||||
if let indexPath = indexPathFor(smartFeed) {
|
||||
selectFeed(indexPath, animated: false) {
|
||||
|
@ -58,7 +58,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
appDelegate.prepareAccountsForForeground()
|
||||
self.coordinator.configureThreePanelMode(for: window!.frame.size)
|
||||
self.coordinator.configurePanelMode(for: window!.frame.size)
|
||||
}
|
||||
|
||||
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
||||
|
@ -19,7 +19,7 @@
|
||||
<tableViewSection headerTitle="Notifications, Badge, Data, & More" id="Bmb-Oi-RZK">
|
||||
<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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -184,7 +184,7 @@
|
||||
<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="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"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
@ -196,17 +196,51 @@
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="About" id="TkH-4v-yhk">
|
||||
<tableViewSection headerTitle="Articles" id="TRr-Ew-IvU">
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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="374" 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"/>
|
||||
<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="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"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<nil key="textColor"/>
|
||||
@ -216,7 +250,7 @@
|
||||
</tableViewCellContentView>
|
||||
</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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -233,7 +267,7 @@
|
||||
</tableViewCellContentView>
|
||||
</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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -250,7 +284,7 @@
|
||||
</tableViewCellContentView>
|
||||
</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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -267,7 +301,7 @@
|
||||
</tableViewCellContentView>
|
||||
</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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -284,7 +318,7 @@
|
||||
</tableViewCellContentView>
|
||||
</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">
|
||||
<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"/>
|
||||
<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"/>
|
||||
@ -300,6 +334,23 @@
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</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>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
@ -317,6 +368,7 @@
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<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"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
|
@ -17,6 +17,7 @@ class SettingsViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var timelineSortOrderSwitch: UISwitch!
|
||||
@IBOutlet weak var groupByFeedSwitch: UISwitch!
|
||||
@IBOutlet weak var showFullscreenArticlesSwitch: UISwitch!
|
||||
|
||||
weak var presentingParentController: UIViewController?
|
||||
|
||||
@ -50,10 +51,16 @@ class SettingsViewController: UITableViewController {
|
||||
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))
|
||||
buildLabel.font = UIFont.systemFont(ofSize: 11.0)
|
||||
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.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
@ -71,24 +78,42 @@ class SettingsViewController: UITableViewController {
|
||||
|
||||
// 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 {
|
||||
switch section {
|
||||
var adjustedSection = section
|
||||
if traitCollection.userInterfaceIdiom != .phone && section > 3 {
|
||||
adjustedSection = adjustedSection + 1
|
||||
}
|
||||
|
||||
switch adjustedSection {
|
||||
case 1:
|
||||
return AccountManager.shared.accounts.count + 1
|
||||
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) {
|
||||
return defaultNumberOfRows - 1
|
||||
}
|
||||
return defaultNumberOfRows
|
||||
default:
|
||||
return super.tableView(tableView, numberOfRowsInSection: section)
|
||||
return super.tableView(tableView, numberOfRowsInSection: adjustedSection)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
switch indexPath.section {
|
||||
switch adjustedSection {
|
||||
case 1:
|
||||
|
||||
let sortedAccounts = AccountManager.shared.sortedAccounts
|
||||
@ -105,8 +130,8 @@ class SettingsViewController: UITableViewController {
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
let adjustedIndexPath = IndexPath(row: indexPath.row, section: adjustedSection)
|
||||
cell = super.tableView(tableView, cellForRowAt: adjustedIndexPath)
|
||||
|
||||
}
|
||||
|
||||
@ -114,7 +139,12 @@ class SettingsViewController: UITableViewController {
|
||||
}
|
||||
|
||||
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:
|
||||
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
@ -156,11 +186,11 @@ class SettingsViewController: UITableViewController {
|
||||
default:
|
||||
break
|
||||
}
|
||||
case 4:
|
||||
case 5:
|
||||
switch indexPath.row {
|
||||
case 0:
|
||||
let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self)
|
||||
self.navigationController?.pushViewController(timeline, animated: true)
|
||||
openURL("https://ranchero.com/netnewswire/help/ios/5.0/en/")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
case 1:
|
||||
openURL("https://ranchero.com/netnewswire/")
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
@ -176,6 +206,9 @@ class SettingsViewController: UITableViewController {
|
||||
case 5:
|
||||
openURL("https://github.com/brentsimmons/NetNewsWire/tree/master/Technotes")
|
||||
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:
|
||||
break
|
||||
}
|
||||
@ -197,19 +230,11 @@ class SettingsViewController: UITableViewController {
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
if indexPath.section == 1 {
|
||||
return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1))
|
||||
} else {
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
}
|
||||
return super.tableView(tableView, heightForRowAt: IndexPath(row: 0, section: 1))
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
|
||||
if indexPath.section == 1 {
|
||||
return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
|
||||
} else {
|
||||
return super.tableView(tableView, indentationLevelForRowAt: indexPath)
|
||||
}
|
||||
return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@objc func contentSizeCategoryDidChange() {
|
||||
|
20
iOS/UIKit Extensions/CroppingPreviewParameters.swift
Normal file
20
iOS/UIKit Extensions/CroppingPreviewParameters.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -10,6 +10,8 @@ import UIKit
|
||||
|
||||
class ImageHeaderView: UITableViewHeaderFooterView {
|
||||
|
||||
static let rowHeight = CGFloat(integerLiteral: 88)
|
||||
|
||||
var imageView = UIImageView()
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
|
@ -13,9 +13,12 @@ class TickMarkSlider: UISlider {
|
||||
private var enableFeedback = false
|
||||
private let feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
|
||||
private var roundedValue: Float?
|
||||
override var value: Float {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -66,6 +69,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?) {
|
||||
value = value.rounded()
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user