This commit is contained in:
Brent Simmons 2019-09-06 09:03:45 -07:00
commit 80ef4e18f0
21 changed files with 524 additions and 156 deletions

View File

@ -63,6 +63,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
private var crashReportWindowController: CrashReportWindowController? // For testing only
private let log = Log()
private let appNewsURLString = "https://nnw.ranchero.com/feed.json"
private let appMovementMonitor = RSAppMovementMonitor()
override init() {
NSWindow.allowsAutomaticWindowTabbing = false

View File

@ -124,7 +124,7 @@ pre {
}
img, figure, video, iframe {
max-width: 100%;
height: auto;
height: auto !important;
margin: 0 auto;
}

View File

@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
5110AB7822B7BD6200A94F76 /* AddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5110AB7722B7BD6200A94F76 /* AddView.swift */; };
51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; };
511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; };
@ -34,6 +33,7 @@
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */; };
5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; };
514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; };
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7D1E23219F3C00BAC947 /* AddControllerType.swift */; };
51543685228F6753005E1CDF /* DetailAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51543684228F6753005E1CDF /* DetailAccountViewController.swift */; };
515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515436872291D75D005E1CDF /* AddLocalAccountViewController.swift */; };
5154368A2291FED9005E1CDF /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */; };
@ -686,7 +686,6 @@
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = "<group>"; };
510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeedbinAccountView.swift; sourceTree = "<group>"; };
510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountLabelView.swift; sourceTree = "<group>"; };
5110AB7722B7BD6200A94F76 /* AddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddView.swift; sourceTree = "<group>"; };
51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSapp_target.xcconfig; sourceTree = "<group>"; };
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = "<group>"; };
51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = "<group>"; };
@ -707,6 +706,7 @@
5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsFeedbinWindowController.swift; sourceTree = "<group>"; };
5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsFeedbin.xib; sourceTree = "<group>"; };
514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = "<group>"; };
514B7D1E23219F3C00BAC947 /* AddControllerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddControllerType.swift; sourceTree = "<group>"; };
51543684228F6753005E1CDF /* DetailAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAccountViewController.swift; sourceTree = "<group>"; };
515436872291D75D005E1CDF /* AddLocalAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLocalAccountViewController.swift; sourceTree = "<group>"; };
515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = "<group>"; };
@ -1028,18 +1028,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5110AB6E22B7BD3C00A94F76 /* UIKit */ = {
isa = PBXGroup;
children = (
51C452822265093600C03939 /* Add.storyboard */,
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */,
51C452842265093600C03939 /* AddFeedViewController.swift */,
51C452812265093600C03939 /* AddFeedFolderPickerData.swift */,
51C4528B2265095F00C03939 /* AddFolderViewController.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
511D43CE231FA51100FB1562 /* Resources */ = {
isa = PBXGroup;
children = (
@ -1217,8 +1205,12 @@
51C452802265093600C03939 /* Add */ = {
isa = PBXGroup;
children = (
5110AB6E22B7BD3C00A94F76 /* UIKit */,
5110AB7722B7BD6200A94F76 /* AddView.swift */,
51C452822265093600C03939 /* Add.storyboard */,
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */,
514B7D1E23219F3C00BAC947 /* AddControllerType.swift */,
51C452842265093600C03939 /* AddFeedViewController.swift */,
51C452812265093600C03939 /* AddFeedFolderPickerData.swift */,
51C4528B2265095F00C03939 /* AddFolderViewController.swift */,
);
path = Add;
sourceTree = "<group>";
@ -2434,7 +2426,6 @@
files = (
840D617F2029031C009BC708 /* AppDelegate.swift in Sources */,
512E08E72268801200BDCFDD /* FeedTreeControllerDelegate.swift in Sources */,
5110AB7822B7BD6200A94F76 /* AddView.swift in Sources */,
51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */,
51EF0F79227716380050506E /* ColorHash.swift in Sources */,
5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */,
@ -2477,6 +2468,7 @@
51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */,
51C45292226509C800C03939 /* TodayFeedDelegate.swift in Sources */,
51C452A222650A1900C03939 /* RSHTMLMetadata+Extension.swift in Sources */,
514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */,
51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */,
5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */,
51EF0F7C2277919E0050506E /* TimelineNumberOfLinesViewController.swift in Sources */,

View File

@ -19,8 +19,6 @@
<string>goToPreviousUnread:</string>
</dict>
<dict>
<key>title</key>
<string>Go to Previous Unread</string>
<key>key</key>
<string>[uparrow]</string>
<key>shiftModifier</key>
@ -29,16 +27,12 @@
<string>goToPreviousUnread:</string>
</dict>
<dict>
<key>title</key>
<string>Next Unread</string>
<key>key</key>
<string>+</string>
<key>action</key>
<string>nextUnread:</string>
</dict>
<dict>
<key>title</key>
<string>Next Unread</string>
<key>key</key>
<string>+</string>
<key>shiftModifier</key>

View File

@ -10,7 +10,7 @@
</dict>
<dict>
<key>title</key>
<string>Collapse Selected Rows</string>
<string>Collapse Selected Row</string>
<key>key</key>
<string>,</string>
<key>action</key>
@ -18,7 +18,7 @@
</dict>
<dict>
<key>title</key>
<string>Expand Selected Rows</string>
<string>Expand Selected Row</string>
<key>key</key>
<string>.</string>
<key>action</key>

View File

@ -33,6 +33,7 @@ class AddContainerViewController: UIViewController {
private var currentViewController: AddContainerViewControllerChild?
var initialControllerType: AddControllerType?
var initialFeed: String?
var initialFeedName: String?
@ -40,20 +41,26 @@ class AddContainerViewController: UIViewController {
super.viewDidLoad()
activityIndicatorView.isHidden = true
switchToFeed()
typeSelectorSegmentedControl.selectedSegmentIndex = initialControllerType?.rawValue ?? 0
switch initialControllerType {
case .feed:
switchToFeed()
case .folder:
switchToFolder()
default:
assertionFailure()
}
}
@IBAction func typeSelectorChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
switchToFeed()
default:
switchToFolder()
}
}
@IBAction func cancel(_ sender: Any) {

View File

@ -0,0 +1,14 @@
//
// AddControllerType.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 9/5/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
enum AddControllerType: Int {
case feed = 0
case folder = 1
}

View File

@ -44,6 +44,7 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
urlTextField.autocapitalizationType = .none
urlTextField.text = initialFeed
urlTextField.delegate = self
urlTextField.becomeFirstResponder()
if initialFeed != nil {
delegate?.readyToAdd(state: true)

View File

@ -31,6 +31,8 @@ class AddFolderViewController: UITableViewController, AddContainerViewController
accounts = AccountManager.shared.sortedActiveAccounts
nameTextField.delegate = self
nameTextField.becomeFirstResponder()
accountLabel.text = (accounts[0] as DisplayNameProvider).nameForDisplay
if shouldDisplayPicker {

View File

@ -1,23 +0,0 @@
//
// AddView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 6/17/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import SwiftUI
struct AddView : View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
}
}
#if DEBUG
struct AddView_Previews : PreviewProvider {
static var previews: some View {
AddView()
}
}
#endif

View File

@ -26,15 +26,11 @@ class DetailViewController: UIViewController {
weak var coordinator: SceneCoordinator!
lazy var keyboardManager = KeyboardManager(type: .detail, coordinator: coordinator)
private let keyboardManager = KeyboardManager(type: .detail)
override var keyCommands: [UIKeyCommand]? {
return keyboardManager.keyCommands
}
override var canBecomeFirstResponder: Bool {
return true
}
deinit {
webView.removeFromSuperview()
DetailViewControllerWebViewProvider.shared.enqueueWebView(webView)
@ -155,7 +151,7 @@ class DetailViewController: UIViewController {
}
@IBAction func toggleStar(_ sender: Any) {
coordinator.toggleStarForCurrentArticle()
coordinator.toggleStarredForCurrentArticle()
}
@IBAction func openBrowser(_ sender: Any) {
@ -188,7 +184,28 @@ class DetailViewController: UIViewController {
webView.becomeFirstResponder()
}
func finalScrollPosition() -> CGFloat {
return webView.scrollView.contentSize.height - webView.scrollView.bounds.size.height + webView.scrollView.contentInset.bottom
}
func canScrollDown() -> Bool {
return webView.scrollView.contentOffset.y < finalScrollPosition()
}
func scrollPageDown() {
let scrollToY: CGFloat = {
let fullScroll = webView.scrollView.contentOffset.y + webView.scrollView.bounds.size.height
let final = finalScrollPosition()
return fullScroll < final ? fullScroll : final
}()
let convertedPoint = self.view.convert(CGPoint(x: 0, y: 0), to: webView.scrollView)
let scrollToPoint = CGPoint(x: convertedPoint.x, y: scrollToY)
webView.scrollView.setContentOffset(scrollToPoint, animated: true)
}
}
//print("\(candidateY) : \(webView.scrollView.contentSize.height)")
class ArticleActivityItemSource: NSObject, UIActivityItemSource {

View File

@ -17,36 +17,27 @@ enum KeyboardType: String {
class KeyboardManager {
private let coordinator: SceneCoordinator
private(set) var keyCommands: [UIKeyCommand]?
init(type: KeyboardType, coordinator: SceneCoordinator) {
self.coordinator = coordinator
load(type: type)
init(type: KeyboardType) {
let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")!
let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]]
keyCommands = globalEntries.compactMap { createKeyCommand(keyEntry: $0) }
keyCommands!.append(contentsOf: globalAuxilaryKeyCommands())
let specificFile = Bundle.main.path(forResource: type.rawValue, ofType: "plist")!
let specificEntries = NSArray(contentsOfFile: specificFile)! as! [[String: Any]]
keyCommands!.append(contentsOf: specificEntries.compactMap { createKeyCommand(keyEntry: $0) } )
if type == .sidebar {
keyCommands!.append(contentsOf: sidebarAuxilaryKeyCommands())
}
}
}
private extension KeyboardManager {
func load(type: KeyboardType) {
let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")!
let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]]
var globalCommands = globalEntries.compactMap { createKeyCommand(keyEntry: $0) }
let specificFile = Bundle.main.path(forResource: type.rawValue, ofType: "plist")!
let specificEntries = NSArray(contentsOfFile: specificFile)! as! [[String: Any]]
let specificCommands = specificEntries.compactMap { createKeyCommand(keyEntry: $0) }
globalCommands.append(contentsOf: specificCommands)
if type == .sidebar {
globalCommands.append(contentsOf: sidebarAuxilaryKeyCommands())
}
keyCommands = globalCommands
}
func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? {
guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil }
let modifiers = createKeyModifierFlags(keyEntry: keyEntry)
@ -69,7 +60,7 @@ private extension KeyboardManager {
switch(key) {
case "[space]":
return " "
return "\u{0020}"
case "[uparrow]":
return UIKeyCommand.inputUpArrow
case "[downarrow]":
@ -116,6 +107,48 @@ private extension KeyboardManager {
return flags
}
func globalAuxilaryKeyCommands() -> [UIKeyCommand] {
var keys = [UIKeyCommand]()
let addNewFeedTitle = NSLocalizedString("New Feed", comment: "New Feed")
keys.append(createKeyCommand(title: addNewFeedTitle, action: "addNewFeed:", input: "n", modifiers: [.command]))
let addNewFolderTitle = NSLocalizedString("New Folder", comment: "New Folder")
keys.append(createKeyCommand(title: addNewFolderTitle, action: "addNewFolder:", input: "n", modifiers: [.command, .shift]))
let refreshTitle = NSLocalizedString("Refresh", comment: "Refresh")
keys.append(createKeyCommand(title: refreshTitle, action: "refresh:", input: "r", modifiers: [.command]))
let nextUnreadTitle = NSLocalizedString("Next Unread", comment: "Next Unread")
keys.append(createKeyCommand(title: nextUnreadTitle, action: "nextUnread:", input: "/", modifiers: [.command]))
let goToTodayTitle = NSLocalizedString("Go To Today", comment: "Go To Today")
keys.append(createKeyCommand(title: goToTodayTitle, action: "goToToday:", input: "1", modifiers: [.command]))
let goToAllUnreadTitle = NSLocalizedString("Go To All Unread", comment: "Go To All Unread")
keys.append(createKeyCommand(title: goToAllUnreadTitle, action: "goToAllUnread:", input: "2", modifiers: [.command]))
let goToStarredTitle = NSLocalizedString("Go To Starred", comment: "Go To Starred")
keys.append(createKeyCommand(title: goToStarredTitle, action: "goToStarred:", input: "3", modifiers: [.command]))
let toggleReadTitle = NSLocalizedString("Toggle Read Status", comment: "Toggle Read Status")
keys.append(createKeyCommand(title: toggleReadTitle, action: "toggleRead:", input: "U", modifiers: [.command, .shift]))
let markAllAsReadTitle = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
keys.append(createKeyCommand(title: markAllAsReadTitle, action: "markAllAsRead:", input: "k", modifiers: [.command]))
let markOlderAsReadTitle = NSLocalizedString("Mark Older as Read", comment: "Mark Older as Read")
keys.append(createKeyCommand(title: markOlderAsReadTitle, action: "markOlderArticlesAsRead:", input: "k", modifiers: [.command, .shift]))
let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status")
keys.append(createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift]))
let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser")
keys.append(createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command]))
return keys
}
func sidebarAuxilaryKeyCommands() -> [UIKeyCommand] {
var keys = [UIKeyCommand]()

View File

@ -22,7 +22,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
var undoableCommands = [UndoableCommand]()
weak var coordinator: SceneCoordinator!
lazy var keyboardManager = KeyboardManager(type: .sidebar, coordinator: coordinator)
private let keyboardManager = KeyboardManager(type: .sidebar)
override var keyCommands: [UIKeyCommand]? {
return keyboardManager.keyCommands
}
@ -56,6 +56,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
updateUI()
applyChanges(animate: false)
becomeFirstResponder()
}
@ -259,7 +260,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
becomeFirstResponder()
coordinator.selectFeed(indexPath)
coordinator.selectFeed(indexPath, automated: false)
}
override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath {
@ -347,7 +348,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
@IBAction func add(_ sender: UIBarButtonItem) {
coordinator.showAdd()
coordinator.showAdd(.feed)
}
@objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) {
@ -361,11 +362,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
if coordinator.isExpanded(sectionNode) {
headerView.disclosureExpanded = false
coordinator.collapse(section: sectionIndex)
coordinator.collapseSection(sectionIndex)
self.applyChanges(animate: true)
} else {
headerView.disclosureExpanded = true
coordinator.expand(section: sectionIndex)
coordinator.expandSection(sectionIndex)
self.applyChanges(animate: true)
}
@ -398,6 +399,36 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
coordinator.showBrowserForCurrentFeed()
}
@objc override func delete(_ sender: Any?) {
if let indexPath = coordinator.currentFeedIndexPath {
delete(indexPath: indexPath)
}
}
@objc func expandSelectedRows(_ sender: Any?) {
if let indexPath = coordinator.currentFeedIndexPath {
coordinator.expandFolder(indexPath)
self.applyChanges(animate: true)
}
}
@objc func collapseSelectedRows(_ sender: Any?) {
if let indexPath = coordinator.currentFeedIndexPath {
coordinator.collapseFolder(indexPath)
self.applyChanges(animate: true)
}
}
@objc func expandAll(_ sender: Any?) {
coordinator.expandAllSectionsAndFolders()
self.applyChanges(animate: true)
}
@objc func collapseAllExceptForGroupItems(_ sender: Any?) {
coordinator.collapseAllFolders()
self.applyChanges(animate: true)
}
// MARK: API
func updateFeedSelection() {
@ -421,24 +452,42 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
}
func ensureSectionIsExpanded(_ sectionIndex: Int, completion: (() -> Void)? = nil) {
guard let sectionNode = coordinator.rootNode.childAtIndex(sectionIndex) else {
return
}
if !coordinator.isExpanded(sectionNode) {
coordinator.expandSection(sectionIndex)
self.applyChanges(animate: true) {
completion?()
}
} else {
completion?()
}
}
func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) {
guard let node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else {
return
completion?()
return
}
if let indexPath = coordinator.indexPathFor(node) {
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
coordinator.selectFeed(indexPath)
completion?()
return
}
// It wasn't already visable, so expand its folder and try again
guard let parent = node.parent, let indexPath = coordinator.indexPathFor(parent) else {
completion?()
return
}
coordinator.expand(indexPath)
coordinator.expandFolder(indexPath)
reloadNode(parent)
self.applyChanges(animate: true) { [weak self] in
@ -481,12 +530,10 @@ private extension MasterFeedViewController {
}
func reloadNode(_ node: Node) {
let savedNode = selectedNode()
var snapshot = dataSource.snapshot()
snapshot.reloadItems([node])
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
self?.selectRow(node: savedNode)
self?.restoreSelectionIfNecessary()
}
}
@ -504,21 +551,6 @@ private extension MasterFeedViewController {
completion?()
}
}
func selectedNode() -> Node? {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
return coordinator.nodeFor(selectedIndexPath)
} else {
return nil
}
}
func selectRow(node: Node?) {
if let nodeToSelect = node, let selectingIndexPath = coordinator.indexPathFor(nodeToSelect) {
tableView.selectRow(at: selectingIndexPath, animated: false, scrollPosition: .none)
}
}
func makeDataSource() -> UITableViewDiffableDataSource<Int, Node> {
return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in
@ -613,7 +645,7 @@ private extension MasterFeedViewController {
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
coordinator.expand(indexPath)
coordinator.expandFolder(indexPath)
self.applyChanges(animate: true)
}
@ -621,7 +653,7 @@ private extension MasterFeedViewController {
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
coordinator.collapse(indexPath)
coordinator.collapseFolder(indexPath)
self.applyChanges(animate: true)
}

View File

@ -24,7 +24,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
weak var coordinator: SceneCoordinator!
var undoableCommands = [UndoableCommand]()
lazy var keyboardManager = KeyboardManager(type: .timeline, coordinator: coordinator)
private let keyboardManager = KeyboardManager(type: .timeline)
override var keyCommands: [UIKeyCommand]? {
return keyboardManager.keyCommands
}
@ -156,6 +156,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
func updateArticleSelection(animate: Bool) {
guard !coordinator.articles.isEmpty else { return }
if let indexPath = coordinator.currentArticleIndexPath {
if tableView.indexPathForSelectedRow != indexPath {
tableView.selectRow(at: indexPath, animated: animate, scrollPosition: .middle)
@ -163,12 +165,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
} else {
tableView.selectRow(at: nil, animated: animate, scrollPosition: .none)
}
updateUI()
}
func showSearchAll() {
navigationItem.searchController?.isActive = true
navigationItem.searchController?.searchBar.selectedScopeButtonIndex = 1
navigationItem.searchController?.searchBar.becomeFirstResponder()
}
func focus() {

View File

@ -122,7 +122,7 @@ pre {
}
img, figure, video, iframe {
max-width: 100%;
height: auto;
height: auto !important;
margin: 0 auto;
}

View File

@ -7,6 +7,7 @@
//
import UIKit
import Account
class RootSplitViewController: UISplitViewController {
@ -14,8 +15,82 @@ class RootSplitViewController: UISplitViewController {
// MARK: Keyboard Shortcuts
@objc func scrollOrGoToNextUnread(_ sender: Any?) {
coordinator.scrollOrGoToNextUnread()
}
@objc func goToPreviousUnread(_ sender: Any?) {
coordinator.selectPrevUnread()
}
@objc func nextUnread(_ sender: Any?) {
coordinator.selectNextUnread()
}
@objc func markRead(_ sender: Any?) {
coordinator.markAsReadForCurrentArticle()
}
@objc func markUnreadAndGoToNextUnread(_ sender: Any?) {
coordinator.markAsUnreadForCurrentArticle()
coordinator.selectNextUnread()
}
@objc func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
coordinator.markAllAsReadInTimeline()
coordinator.selectNextUnread()
}
@objc func markOlderArticlesAsRead(_ sender: Any?) {
coordinator.markAsReadOlderArticlesInTimeline()
}
@objc func markUnread(_ sender: Any?) {
coordinator.markAsUnreadForCurrentArticle()
}
@objc func goToPreviousSubscription(_ sender: Any?) {
coordinator.selectPrevFeed()
}
@objc func goToNextSubscription(_ sender: Any?) {
coordinator.selectNextFeed()
}
@objc func openInBrowser(_ sender: Any?) {
coordinator.showBrowserForCurrentArticle()
}
@objc func addNewFeed(_ sender: Any?) {
coordinator.showAdd(.feed)
}
@objc func addNewFolder(_ sender: Any?) {
coordinator.showAdd(.folder)
}
@objc func refresh(_ sender: Any?) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self))
}
@objc func goToToday(_ sender: Any?) {
coordinator.selectTodayFeed()
}
@objc func goToAllUnread(_ sender: Any?) {
coordinator.selectAllUnreadFeed()
}
@objc func goToStarred(_ sender: Any?) {
coordinator.selectStarredFeed()
}
@objc func toggleRead(_ sender: Any?) {
coordinator.toggleReadForCurrentArticle()
}
@objc func toggleStarred(_ sender: Any?) {
coordinator.toggleStarredForCurrentArticle()
}
}

View File

@ -108,7 +108,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
var timelineFetcher: ArticleFetcher? {
didSet {
selectArticle(nil)
if timelineFetcher is Feed {
showFeedNames = false
} else {
@ -300,6 +299,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func handle(_ activity: NSUserActivity) {
selectFeed(nil)
guard let activityType = ActivityType(rawValue: activity.activityType) else { return }
switch activityType {
case .selectToday:
@ -326,7 +327,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
func showSearch() {
selectFeed(nil)
masterTimelineViewController?.showSearchAll()
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self
navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.masterTimelineViewController!.showSearchAll()
}
}
// MARK: Notifications
@ -436,10 +444,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return 0
}
func expand(section: Int) {
guard let expandNode = treeController.rootNode.childAtIndex(section) else {
func expandSection(_ section: Int) {
guard let expandNode = treeController.rootNode.childAtIndex(section), !expandedNodes.contains(expandNode) else {
return
}
expandedNodes.append(expandNode)
animatingChanges = true
@ -463,8 +472,23 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
animatingChanges = false
}
func expand(_ indexPath: IndexPath) {
func expandAllSectionsAndFolders() {
for (sectionIndex, sectionNode) in treeController.rootNode.childNodes.enumerated() {
expandSection(sectionIndex)
for topLevelNode in sectionNode.childNodes {
if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) {
expandFolder(indexPath)
}
}
}
}
func expandFolder(_ indexPath: IndexPath) {
let expandNode = shadowTable[indexPath.section][indexPath.row]
guard !expandedNodes.contains(expandNode) else { return }
expandedNodes.append(expandNode)
animatingChanges = true
@ -479,13 +503,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
animatingChanges = false
}
func collapse(section: Int) {
animatingChanges = true
guard let collapseNode = treeController.rootNode.childAtIndex(section) else {
func collapseSection(_ section: Int) {
guard let collapseNode = treeController.rootNode.childAtIndex(section), expandedNodes.contains(collapseNode) else {
return
}
animatingChanges = true
if let removeNode = expandedNodes.firstIndex(of: collapseNode) {
expandedNodes.remove(at: removeNode)
}
@ -495,10 +519,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
animatingChanges = false
}
func collapse(_ indexPath: IndexPath) {
func collapseAllFolders() {
for sectionNode in treeController.rootNode.childNodes {
for topLevelNode in sectionNode.childNodes {
if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) {
collapseFolder(indexPath)
}
}
}
}
func collapseFolder(_ indexPath: IndexPath) {
animatingChanges = true
let collapseNode = shadowTable[indexPath.section][indexPath.row]
guard expandedNodes.contains(collapseNode) else { return }
if let removeNode = expandedNodes.firstIndex(of: collapseNode) {
expandedNodes.remove(at: removeNode)
}
@ -540,24 +575,28 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return indexes
}
func selectFeed(_ indexPath: IndexPath?) {
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self
navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: true)
}
func selectFeed(_ indexPath: IndexPath?, automated: Bool = true) {
selectArticle(nil)
currentFeedIndexPath = indexPath
if let ip = indexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher {
timelineFetcher = fetcher
updateSelectingActivity(with: node)
if navControllerForTimeline().viewControllers.filter({ $0 is MasterTimelineViewController }).count < 1 {
masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
masterTimelineViewController!.coordinator = self
navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: !automated)
}
} else {
timelineFetcher = nil
if rootSplitViewController.isCollapsed && navControllerForTimeline().viewControllers.last is MasterTimelineViewController {
navControllerForTimeline().popViewController(animated: !automated)
}
}
masterFeedViewController.updateFeedSelection()
selectArticle(nil)
}
func selectPrevFeed() {
@ -571,6 +610,24 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
selectFeed(indexPath)
}
}
func selectTodayFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) {
self.selectFeed(IndexPath(row: 0, section: 0))
}
}
func selectAllUnreadFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) {
self.selectFeed(IndexPath(row: 1, section: 0))
}
}
func selectStarredFeed() {
masterFeedViewController?.ensureSectionIsExpanded(0) {
self.selectFeed(IndexPath(row: 2, section: 0))
}
}
func selectArticle(_ indexPath: IndexPath?, automated: Bool = true) {
currentArticleIndexPath = indexPath
@ -581,18 +638,22 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
if indexPath == nil {
if !rootSplitViewController.isCollapsed {
if rootSplitViewController.isCollapsed {
if masterNavigationController.children.last is DetailViewController {
masterNavigationController.popViewController(animated: !automated)
}
} else {
let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
installDetailController(systemMessageViewController)
installDetailController(systemMessageViewController, automated: automated)
}
masterTimelineViewController?.updateArticleSelection(animate: true)
masterTimelineViewController?.updateArticleSelection(animate: !automated)
return
}
if detailViewController == nil {
let detailViewController = UIStoryboard.main.instantiateController(ofType: DetailViewController.self)
detailViewController.coordinator = self
installDetailController(detailViewController)
installDetailController(detailViewController, automated: automated)
}
// Automatically hide the overlay
@ -604,7 +665,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
if automated {
masterTimelineViewController?.updateArticleSelection(animate: true)
masterTimelineViewController?.updateArticleSelection(animate: false)
}
detailViewController?.updateArticleSelection()
@ -672,6 +733,22 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
func selectPrevUnread() {
// This should never happen, but I don't want to risk throwing us
// into an infinate loop searching for an unread that isn't there.
if appDelegate.unreadCount < 1 {
return
}
if selectPrevUnreadArticleInTimeline() {
return
}
selectPrevUnreadFeedFetcher()
selectPrevUnreadArticleInTimeline()
}
func selectNextUnread() {
// This should never happen, but I don't want to risk throwing us
@ -692,6 +769,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func scrollOrGoToNextUnread() {
if detailViewController?.canScrollDown() ?? false {
detailViewController?.scrollPageDown()
} else {
selectNextUnread()
}
}
func markAllAsRead(_ articles: [Article]) {
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else {
return
@ -713,6 +798,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
masterNavigationController.popViewController(animated: true)
}
func markAsReadOlderArticlesInTimeline() {
if let indexPath = currentArticleIndexPath {
markAsReadOlderArticlesInTimeline(indexPath)
}
}
func markAsReadOlderArticlesInTimeline(_ indexPath: IndexPath) {
let article = articles[indexPath.row]
let articlesToMark = articles.filter { $0.logicalDatePublished < article.logicalDatePublished }
@ -722,6 +812,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
markAllAsRead(articlesToMark)
}
func markAsReadForCurrentArticle() {
if let article = currentArticle {
markArticles(Set([article]), statusKey: .read, flag: true)
}
}
func markAsUnreadForCurrentArticle() {
if let article = currentArticle {
markArticles(Set([article]), statusKey: .read, flag: false)
}
}
func toggleReadForCurrentArticle() {
if let article = currentArticle {
markArticles(Set([article]), statusKey: .read, flag: !article.status.read)
@ -737,7 +839,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
runCommand(markReadCommand)
}
func toggleStarForCurrentArticle() {
func toggleStarredForCurrentArticle() {
if let article = currentArticle {
markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred)
}
@ -753,7 +855,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func discloseFeed(_ feed: Feed, completion: (() -> Void)? = nil) {
masterNavigationController.popViewController(animated: true)
masterFeedViewController.discloseFeed(feed) {
completion?()
}
@ -770,8 +871,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
// self.present(settings, animated: true)
}
func showAdd() {
let addViewController = UIStoryboard.add.instantiateInitialViewController()!
func showAdd(_ type: AddControllerType) {
selectFeed(nil)
let addViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController
let containerController = addViewController.topViewController as! AddContainerViewController
containerController.initialControllerType = type
addViewController.modalPresentationStyle = .formSheet
addViewController.preferredContentSize = AddContainerViewController.preferredContentSizeForFormSheetDisplay
masterFeedViewController.present(addViewController, animated: true)
@ -819,10 +924,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func navigateToTimeline() {
masterTimelineViewController?.focus()
if currentArticleIndexPath == nil {
selectArticle(IndexPath(row: 0, section: 0))
}
masterTimelineViewController?.focus()
}
func navigateToDetail() {
@ -984,11 +1089,113 @@ private extension SceneCoordinator {
self.showAvatars = false
}
// MARK: Select Next Unread
// MARK: Select Prev Unread
@discardableResult
func selectPrevUnreadArticleInTimeline() -> Bool {
let startingRow: Int = {
if let indexPath = currentArticleIndexPath {
return indexPath.row - 1
} else {
return articles.count - 1
}
}()
return selectPrevArticleInTimeline(startingRow: startingRow)
}
func selectPrevArticleInTimeline(startingRow: Int) -> Bool {
guard startingRow >= 0 else {
return false
}
for i in (0...startingRow).reversed() {
let article = articles[i]
if !article.status.read {
selectArticle(IndexPath(row: i, section: 0))
return true
}
}
return false
}
func selectPrevUnreadFeedFetcher() {
let indexPath: IndexPath = {
if currentFeedIndexPath == nil {
return IndexPath(row: 0, section: 0)
} else {
return currentFeedIndexPath!
}
}()
// Increment or wrap around the IndexPath
let nextIndexPath: IndexPath = {
if indexPath.row - 1 < 0 {
if indexPath.section - 1 < 0 {
return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
} else {
return IndexPath(row: shadowTable[indexPath.section - 1].count - 1, section: indexPath.section - 1)
}
} else {
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
}
}()
if selectPrevUnreadFeedFetcher(startingWith: nextIndexPath) {
return
}
let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
selectPrevUnreadFeedFetcher(startingWith: maxIndexPath)
}
@discardableResult
func selectPrevUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool {
for i in (0...indexPath.section).reversed() {
let startingRow: Int = {
if indexPath.section == i {
return indexPath.row
} else {
return shadowTable[i].count - 1
}
}()
for j in (0...startingRow).reversed() {
let prevIndexPath = IndexPath(row: j, section: i)
guard let node = nodeFor(prevIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {
assertionFailure()
return true
}
if expandedNodes.contains(node) {
continue
}
if unreadCountProvider.unreadCount > 0 {
selectFeed(prevIndexPath)
return true
}
}
}
return false
}
// MARK: Select Next Unread
@discardableResult
func selectFirstUnreadArticleInTimeline() -> Bool {
return selectArticleInTimeline(startingRow: 0)
return selectNextArticleInTimeline(startingRow: 0)
}
@discardableResult
@ -1001,10 +1208,10 @@ private extension SceneCoordinator {
}
}()
return selectArticleInTimeline(startingRow: startingRow)
return selectNextArticleInTimeline(startingRow: startingRow)
}
func selectArticleInTimeline(startingRow: Int) -> Bool {
func selectNextArticleInTimeline(startingRow: Int) -> Bool {
guard startingRow < articles.count else {
return false
@ -1024,10 +1231,13 @@ private extension SceneCoordinator {
func selectNextUnreadFeedFetcher() {
guard let indexPath = currentFeedIndexPath else {
assertionFailure()
return
}
let indexPath: IndexPath = {
if currentFeedIndexPath == nil {
return IndexPath(row: -1, section: 0)
} else {
return currentFeedIndexPath!
}
}()
// Increment or wrap around the IndexPath
let nextIndexPath: IndexPath = {
@ -1054,7 +1264,15 @@ private extension SceneCoordinator {
for i in indexPath.section..<shadowTable.count {
for j in indexPath.row..<shadowTable[indexPath.section].count {
let startingRow: Int = {
if indexPath.section == i {
return indexPath.row
} else {
return 0
}
}()
for j in startingRow..<shadowTable[indexPath.section].count {
let nextIndexPath = IndexPath(row: j, section: i)
guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {
@ -1262,7 +1480,7 @@ private extension SceneCoordinator {
// during the display mode change callback (in the split view controller delegate). To fool the
// system, we leave the same controller, the shim, in place and change its child controllers instead.
func installDetailController(_ detailController: UIViewController) {
func installDetailController(_ detailController: UIViewController, automated: Bool) {
let showButton = rootSplitViewController.displayMode != .allVisible
let controller = addNavControllerIfNecessary(detailController, showButton: showButton)
@ -1270,7 +1488,7 @@ private extension SceneCoordinator {
let targetSplit = ensureDoubleSplit().children.first as! UISplitViewController
targetSplit.showDetailViewController(controller, sender: self)
} else if rootSplitViewController.isCollapsed {
masterNavigationController.pushViewController(controller, animated: true)
masterNavigationController.pushViewController(controller, animated: !automated)
} else {
if let shimController = rootSplitViewController.viewControllers.last {
shimController.replaceChildAndPinView(controller)

View File

@ -17,21 +17,22 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// UIWindowScene delegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
window = UIWindow(windowScene: scene as! UIWindowScene)
window!.tintColor = AppAssets.netNewsWireBlueColor
window!.rootViewController = coordinator.start()
window!.makeKeyAndVisible()
if let shortcutItem = connectionOptions.shortcutItem {
window!.makeKeyAndVisible()
handleShortcutItem(shortcutItem)
return
}
if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.coordinator.handle(userActivity)
}
self.coordinator.handle(userActivity)
}
window!.makeKeyAndVisible()
}
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
@ -66,7 +67,7 @@ private extension SceneDelegate {
case "com.ranchero.NetNewsWire.ShowSearch":
coordinator.showSearch()
case "com.ranchero.NetNewsWire.ShowAdd":
coordinator.showAdd()
coordinator.showAdd(.feed)
default:
break
}

@ -1 +1 @@
Subproject commit b8656655f68f207bf9d14e9fda2c928c1bcbe0cf
Subproject commit 50cf102acd0592ec3bff2446f19386b6593e1ff8