Merge branch 'master' into feature/feed-wrangler

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

View File

@ -47,7 +47,7 @@ public enum FetchType {
case starred
case 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())

View File

@ -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 */,

View File

@ -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)
}
}

View File

@ -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 }

View File

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

View File

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

View File

@ -12,6 +12,18 @@ import RSCore
public final class Folder: Feed, Renamable, Container, Hashable {
public 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.")

View File

@ -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.")

View File

@ -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)
}

View File

@ -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> {

View File

@ -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>

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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 {

View File

@ -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 its been superseded by a newer fetch, or the timeline was emptied, etc., it wont 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

View File

@ -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 */,

View File

@ -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()

View File

@ -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.

View File

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

View File

@ -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
}

View File

@ -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))
}

View File

@ -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)
}
}
}
}
}

View File

@ -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

View File

@ -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? {

View File

@ -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? {

View File

@ -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")!
}()

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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>

View File

@ -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>

View File

@ -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"/>

View File

@ -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? {

View File

@ -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? {

View File

@ -98,7 +98,7 @@ struct MasterFeedTableViewCellLayout {
}
}
let rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height
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)

View File

@ -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

View File

@ -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)
}
}
}
}
}

View File

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

View File

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

View File

@ -14,10 +14,11 @@ import RSTree
class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
@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 }

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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? {

View File

@ -19,7 +19,7 @@
<tableViewSection headerTitle="Notifications, Badge, Data, &amp; 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>

View File

@ -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() {

View File

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

View File

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

View File

@ -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()

View File

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

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