Merge branch 'master' of https://github.com/brentsimmons/NetNewsWire into GroupArticlesByFeed

# Conflicts:
#	Mac/MainWindow/Timeline/TimelineViewController.swift
This commit is contained in:
Phil Viso 2019-09-13 08:23:31 -05:00
commit 8a6e3c9f37
59 changed files with 1190 additions and 753 deletions

View File

@ -6,6 +6,16 @@
<description>Most recent NetNewsWire changes with links to updates.</description>
<language>en</language>
<item>
<title>NetNewsWire 5.0.1b1</title>
<description><![CDATA[
<p>Timeline: reload the timeline when show-feed-names is toggled. This fixes a bug where switching between a folder and a feed with the exact same list of articles to appear in the timeline would result in display glitches.</p>
]]></description>
<pubDate>Tue, 10 Sep 2019 20:50:00 -0700</pubDate>
<enclosure url="https://github.com/brentsimmons/NetNewsWire/releases/download/mac-5.0.1b1/NetNewsWire5.0.1b1.zip" sparkle:version="2613" sparkle:shortVersionString="5.0.1b1" length="5094966" type="application/zip" />
<sparkle:minimumSystemVersion>10.14.4</sparkle:minimumSystemVersion>
</item>
<item>
<title>NetNewsWire 5.0.1d2</title>
<description><![CDATA[

View File

@ -129,7 +129,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public var folders: Set<Folder>? = Set<Folder>()
private var feedDictionaryNeedsUpdate = true
private var _idToFeedDictionary = [String: Feed]()
var idToFeedDictionary: [String: Feed] {
private var idToFeedDictionary: [String: Feed] {
if feedDictionaryNeedsUpdate {
rebuildFeedDictionaries()
}
@ -816,7 +816,7 @@ extension Account: FeedMetadataDelegate {
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
feedMetadataDirty = true
guard let feed = existingFeed(with: feedMetadata.feedID) else {
guard let feed = existingFeed(withFeedID: feedMetadata.feedID) else {
return
}
feed.postFeedSettingDidChangeNotification(key)
@ -1213,7 +1213,7 @@ private extension Account {
extension Account {
public func existingFeed(with feedID: String) -> Feed? {
public func existingFeed(withFeedID feedID: String) -> Feed? {
return idToFeedDictionary[feedID]
}
}

View File

@ -14,10 +14,11 @@ import Articles
public final class AccountManager: UnreadCountProvider {
public static let shared = AccountManager()
public static var shared: AccountManager!
public let defaultAccount: Account
private let accountsFolder = RSDataSubfolder(nil, "Accounts")!
private let accountsFolder: String
private var accountsDictionary = [String: Account]()
private let defaultAccountFolderName = "OnMyMac"
@ -71,7 +72,9 @@ public final class AccountManager: UnreadCountProvider {
return CombinedRefreshProgress(downloadProgressArray: downloadProgressArray)
}
public init() {
public init(accountsFolder: String) {
self.accountsFolder = accountsFolder
// The local "On My Mac" account must always exist, even if it's empty.
let localAccountFolder = (accountsFolder as NSString).appendingPathComponent("OnMyMac")
do {

View File

@ -36,7 +36,7 @@ public protocol Container: class {
func has(_ feed: Feed) -> Bool
func hasFeed(with feedID: String) -> Bool
func hasFeed(withURL url: String) -> Bool
func existingFeed(with feedID: String) -> Feed?
func existingFeed(withFeedID: String) -> Feed?
func existingFeed(withURL url: String) -> Feed?
func existingFolder(with name: String) -> Folder?
func existingFolder(withID: Int) -> Folder?
@ -88,7 +88,7 @@ public extension Container {
}
func hasFeed(with feedID: String) -> Bool {
return existingFeed(with: feedID) != nil
return existingFeed(withFeedID: feedID) != nil
}
func hasFeed(withURL url: String) -> Bool {
@ -99,7 +99,7 @@ public extension Container {
return flattenedFeeds().contains(feed)
}
func existingFeed(with feedID: String) -> Feed? {
func existingFeed(withFeedID feedID: String) -> Feed? {
for feed in flattenedFeeds() {
if feed.feedID == feedID {
return feed

View File

@ -53,7 +53,7 @@ public extension Article {
}
var feed: Feed? {
return account?.existingFeed(with: feedID)
return account?.existingFeed(withFeedID: feedID)
}
}

View File

@ -39,6 +39,10 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha
}
}
// Note: this is available only if the icon URL was available in the feed.
// The icon URL is a JSON-Feed-only feature.
// Otherwise we find an icon URL via other means, but we dont store it
// as part of feed metadata.
public var iconURL: String? {
get {
return metadata.iconURL
@ -48,6 +52,10 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha
}
}
// Note: this is available only if the favicon URL was available in the feed.
// The favicon URL is a JSON-Feed-only feature.
// Otherwise we find a favicon URL via other means, but we dont store it
// as part of feed metadata.
public var faviconURL: String? {
get {
return metadata.faviconURL

View File

@ -715,7 +715,7 @@ private extension FeedbinAccountDelegate {
let subFeedId = String(subscription.feedID)
if let feed = account.idToFeedDictionary[subFeedId] {
if let feed = account.existingFeed(withFeedID: subFeedId) {
feed.name = subscription.name
// If the name has been changed on the server remove the locally edited name
feed.editedName = nil
@ -778,7 +778,7 @@ private extension FeedbinAccountDelegate {
for tagging in groupedTaggings {
let taggingFeedID = String(tagging.feedID)
if !folderFeedIds.contains(taggingFeedID) {
guard let feed = account.idToFeedDictionary[taggingFeedID] else {
guard let feed = account.existingFeed(withFeedID: taggingFeedID) else {
continue
}
saveFolderRelationship(for: feed, withFolderName: folderName, id: String(tagging.taggingID))
@ -1072,7 +1072,7 @@ private extension FeedbinAccountDelegate {
group.enter()
if let feed = account.idToFeedDictionary[feedID] {
if let feed = account.existingFeed(withFeedID: feedID) {
DispatchQueue.main.async {
account.update(feed, parsedItems: Set(mapItems), defaultRead: true) {
group.leave()

View File

@ -549,7 +549,7 @@ private extension ReaderAPIAccountDelegate {
subscriptions.forEach { subscription in
let subFeedId = String(subscription.feedID)
if let feed = account.idToFeedDictionary[subFeedId] {
if let feed = account.existingFeed(withFeedID: subFeedId) {
feed.name = subscription.name
feed.homePageURL = subscription.homePageURL
} else {
@ -619,8 +619,7 @@ private extension ReaderAPIAccountDelegate {
for subscription in groupedTaggings {
let taggingFeedID = String(subscription.feedID)
if !folderFeedIds.contains(taggingFeedID) {
let idDictionary = account.idToFeedDictionary
guard let feed = idDictionary[taggingFeedID] else {
guard let feed = account.existingFeed(withFeedID: taggingFeedID) else {
continue
}
saveFolderRelationship(for: feed, withFolderName: folderName, id: String(subscription.feedID))
@ -880,7 +879,7 @@ private extension ReaderAPIAccountDelegate {
group.enter()
if let feed = account.idToFeedDictionary[feedID] {
if let feed = account.existingFeed(withFeedID: feedID) {
DispatchQueue.main.async {
account.update(feed, parsedItems: Set(mapItems), defaultRead: true) {
group.leave()

View File

@ -70,6 +70,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
NSWindow.allowsAutomaticWindowTabbing = false
super.init()
AccountManager.shared = AccountManager(accountsFolder: RSDataSubfolder(nil, "Accounts")!)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14865.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14865.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -268,14 +269,14 @@
<subviews>
<outlineView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="firstColumnOnly" selectionHighlightStyle="sourceList" columnReordering="NO" columnResizing="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="26" viewBased="YES" floatsGroupRows="NO" indentationPerLevel="23" outlineTableColumn="ih9-mJ-EA7" id="cnV-kg-Dn2" customClass="SidebarOutlineView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="167" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="0.0"/>
<color key="backgroundColor" name="_sourceListBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="164" minWidth="16" maxWidth="1000" id="ih9-mJ-EA7">
<tableColumn width="164" minWidth="23" maxWidth="1000" id="ih9-mJ-EA7">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="smallSystem"/>
<font key="font" metaFont="label" size="11"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
@ -311,6 +312,7 @@
</prototypeCellViews>
</tableColumn>
</tableColumns>
<accessibility description="Feeds"/>
<connections>
<outlet property="delegate" destination="XML-A3-pDn" id="fPE-cv-p5c"/>
<outlet property="keyboardDelegate" destination="h5K-zR-cUa" id="BlT-aW-sea"/>
@ -345,7 +347,7 @@
</constraints>
</progressIndicator>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="iyL-pW-cT6">
<rect key="frame" x="62" y="6" width="86" height="17"/>
<rect key="frame" x="62" y="6" width="86" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Label" id="dVE-XG-mlU">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@ -444,7 +446,7 @@
<rect key="frame" x="6" y="2" width="12" height="20"/>
<subviews>
<textField horizontalHuggingPriority="850" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="Dim-ed-Dcz" userLabel="URL Label">
<rect key="frame" x="4" y="2" width="4" height="17"/>
<rect key="frame" x="4" y="2" width="4" height="16"/>
<textFieldCell key="cell" lineBreakMode="truncatingMiddle" selectable="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="left" usesSingleLineMode="YES" id="znU-Fh-L7H">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>

View File

@ -14,6 +14,10 @@ final class DetailWebView: WKWebView {
weak var keyboardDelegate: KeyboardDelegate?
override func accessibilityLabel() -> String? {
return NSLocalizedString("Article", comment: "Article")
}
// MARK: - NSResponder
override func keyDown(with event: NSEvent) {

View File

@ -13,6 +13,10 @@ class TimelineTableView: NSTableView {
weak var keyboardDelegate: KeyboardDelegate?
override func accessibilityLabel() -> String? {
return NSLocalizedString("Timeline", comment: "Timeline")
}
// MARK: - NSResponder
override func keyDown(with event: NSEvent) {

View File

@ -48,6 +48,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
if articles.count > 0 {
tableView.scrollRowToVisible(0)
}
updateUnreadCount()
}
}
}
@ -106,6 +107,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
if showFeedNames != oldValue {
updateShowAvatars()
updateTableViewRowHeight()
reloadVisibleCells()
}
}
}
@ -992,10 +994,7 @@ private extension TimelineViewController {
}
func replaceArticles(with unsortedArticles: Set<Article>) {
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
if articles != sortedArticles {
articles = sortedArticles
}
articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed)
}
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {

View File

@ -23,6 +23,27 @@
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 */; };
513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513C5CE8232571C2003D4054 /* ShareViewController.swift */; };
513C5CEC232571C2003D4054 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 513C5CEA232571C2003D4054 /* MainInterface.storyboard */; };
513C5CF0232571C2003D4054 /* NetNewsWire iOS Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
513C5CFD2325749A003D4054 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; };
513C5CFE2325749A003D4054 /* Account.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407166A2262A60D00344432 /* Account.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D00232574AF003D4054 /* Articles.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; };
513C5D01232574AF003D4054 /* Articles.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 840716732262A60F00344432 /* Articles.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D02232574B4003D4054 /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; };
513C5D03232574B4003D4054 /* ArticlesDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D04232574B9003D4054 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8520DD8CF200CA8CF5 /* RSCore.framework */; };
513C5D05232574B9003D4054 /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8520DD8CF200CA8CF5 /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D06232574C0003D4054 /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FC420DD8E0C00CA8CF5 /* RSDatabase.framework */; };
513C5D07232574C0003D4054 /* RSDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FC420DD8E0C00CA8CF5 /* RSDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D08232574C6003D4054 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8C20DD8CF800CA8CF5 /* RSParser.framework */; };
513C5D09232574C6003D4054 /* RSParser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F8C20DD8CF800CA8CF5 /* RSParser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D0A232574D2003D4054 /* RSWeb.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FA320DD8D0500CA8CF5 /* RSWeb.framework */; };
513C5D0B232574D2003D4054 /* RSWeb.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37FA320DD8D0500CA8CF5 /* RSWeb.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D0C232574DA003D4054 /* RSTree.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; };
513C5D0D232574DA003D4054 /* RSTree.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84C37F9520DD8CFE00CA8CF5 /* RSTree.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
513C5D0E232574E4003D4054 /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
513C5D0F232574E4003D4054 /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5144EA2F2279FAB600D19003 /* AccountsDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */; };
5144EA362279FC3D00D19003 /* AccountsAddLocal.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */; };
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA372279FC6200D19003 /* AccountsAddLocalWindowController.swift */; };
@ -42,6 +63,11 @@
51554C30228B71A10055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; };
51554C31228B71A10055115A /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
515ADE4022E11FAE006B2460 /* SystemMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515ADE3F22E11FAE006B2460 /* SystemMessageViewController.swift */; };
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; };
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; };
515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; };
5170743A232AABFC00A461A3 /* FlattenedAccountFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */; };
5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; };
5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; };
5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */; };
@ -102,7 +128,7 @@
51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452742265091600C03939 /* MasterUnreadIndicatorView.swift */; };
51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452752265091600C03939 /* MasterTimelineDefaultCellLayout.swift */; };
51C4527F2265092C00C03939 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4527E2265092C00C03939 /* DetailViewController.swift */; };
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* AddFeedFolderPickerData.swift */; };
51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */; };
51C452862265093600C03939 /* Add.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C452822265093600C03939 /* Add.storyboard */; };
51C452882265093600C03939 /* AddFeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452842265093600C03939 /* AddFeedViewController.swift */; };
51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4528B2265095F00C03939 /* AddFolderViewController.swift */; };
@ -346,6 +372,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
513C5CEE232571C2003D4054 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 849C64581ED37A5D003D8FC0 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 513C5CE5232571C2003D4054;
remoteInfo = "NetNewsWire iOS Share Extension";
};
51554C00228B6EB50055115A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 51554BFC228B6EB50055115A /* SyncDatabase.xcodeproj */;
@ -622,6 +655,36 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
513C5CF1232571C2003D4054 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
513C5CF0232571C2003D4054 /* NetNewsWire iOS Share Extension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
513C5CFF2325749A003D4054 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
513C5D0B232574D2003D4054 /* RSWeb.framework in Embed Frameworks */,
513C5D0D232574DA003D4054 /* RSTree.framework in Embed Frameworks */,
513C5CFE2325749A003D4054 /* Account.framework in Embed Frameworks */,
513C5D01232574AF003D4054 /* Articles.framework in Embed Frameworks */,
513C5D09232574C6003D4054 /* RSParser.framework in Embed Frameworks */,
513C5D07232574C0003D4054 /* RSDatabase.framework in Embed Frameworks */,
513C5D0F232574E4003D4054 /* SyncDatabase.framework in Embed Frameworks */,
513C5D05232574B9003D4054 /* RSCore.framework in Embed Frameworks */,
513C5D03232574B4003D4054 /* ArticlesDatabase.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
51C451DF2264C7F200C03939 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -698,6 +761,10 @@
5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; 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>"; };
513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5144EA2E2279FAB600D19003 /* AccountsDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsDetailViewController.swift; sourceTree = "<group>"; };
5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountsAddLocal.xib; sourceTree = "<group>"; };
5144EA372279FC6200D19003 /* AccountsAddLocalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsAddLocalWindowController.swift; sourceTree = "<group>"; };
@ -714,6 +781,10 @@
515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = "<group>"; };
51554BFC228B6EB50055115A /* SyncDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SyncDatabase.xcodeproj; path = Frameworks/SyncDatabase/SyncDatabase.xcodeproj; sourceTree = SOURCE_ROOT; };
515ADE3F22E11FAE006B2460 /* SystemMessageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMessageViewController.swift; sourceTree = "<group>"; };
515D4FCB2325815A00EE1167 /* SafariExt.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = SafariExt.js; sourceTree = "<group>"; };
515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = "<group>"; };
515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = "<group>"; };
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = "<group>"; };
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = "<group>"; };
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = "<group>"; };
5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationProgressView.swift; sourceTree = "<group>"; };
@ -747,7 +818,7 @@
51C452742265091600C03939 /* MasterUnreadIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterUnreadIndicatorView.swift; sourceTree = "<group>"; };
51C452752265091600C03939 /* MasterTimelineDefaultCellLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTimelineDefaultCellLayout.swift; sourceTree = "<group>"; };
51C4527E2265092C00C03939 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = "<group>"; };
51C452812265093600C03939 /* AddFeedFolderPickerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFeedFolderPickerData.swift; sourceTree = "<group>"; };
51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlattenedAccountFolderPickerData.swift; sourceTree = "<group>"; };
51C452822265093600C03939 /* Add.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Add.storyboard; sourceTree = "<group>"; };
51C452842265093600C03939 /* AddFeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFeedViewController.swift; sourceTree = "<group>"; };
51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = "<group>"; };
@ -983,6 +1054,22 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
513C5CE3232571C2003D4054 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
513C5D0A232574D2003D4054 /* RSWeb.framework in Frameworks */,
513C5D0C232574DA003D4054 /* RSTree.framework in Frameworks */,
513C5CFD2325749A003D4054 /* Account.framework in Frameworks */,
513C5D00232574AF003D4054 /* Articles.framework in Frameworks */,
513C5D08232574C6003D4054 /* RSParser.framework in Frameworks */,
513C5D06232574C0003D4054 /* RSDatabase.framework in Frameworks */,
513C5D0E232574E4003D4054 /* SyncDatabase.framework in Frameworks */,
513C5D04232574B9003D4054 /* RSCore.framework in Frameworks */,
513C5D02232574B4003D4054 /* ArticlesDatabase.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6581C73020CED60000F4AD34 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -1056,12 +1143,26 @@
512E08DD22687FA000BDCFDD /* Tree */ = {
isa = PBXGroup;
children = (
51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */,
849A97611ED9EB96007D329B /* FeedTreeControllerDelegate.swift */,
849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */,
);
path = Tree;
sourceTree = "<group>";
};
513C5CE7232571C2003D4054 /* ShareExtension */ = {
isa = PBXGroup;
children = (
513C5CE8232571C2003D4054 /* ShareViewController.swift */,
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */,
513C5CEA232571C2003D4054 /* MainInterface.storyboard */,
513C5CED232571C2003D4054 /* Info.plist */,
515D4FCB2325815A00EE1167 /* SafariExt.js */,
515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */,
);
path = ShareExtension;
sourceTree = "<group>";
};
5144EA39227A377700D19003 /* OPML */ = {
isa = PBXGroup;
children = (
@ -1160,7 +1261,7 @@
path = "Model Extensions";
sourceTree = "<group>";
};
51C45245226506C800C03939 /* Extensions */ = {
51C45245226506C800C03939 /* UIKit Extensions */ = {
isa = PBXGroup;
children = (
51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
@ -1175,7 +1276,7 @@
512E092B2268B25500BDCFDD /* UISplitViewController-Extensions.swift */,
51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */,
);
path = Extensions;
path = "UIKit Extensions";
sourceTree = "<group>";
};
51C4525D226508F600C03939 /* MasterFeed */ = {
@ -1240,7 +1341,6 @@
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */,
514B7D1E23219F3C00BAC947 /* AddControllerType.swift */,
51C452842265093600C03939 /* AddFeedViewController.swift */,
51C452812265093600C03939 /* AddFeedFolderPickerData.swift */,
51C4528B2265095F00C03939 /* AddFolderViewController.swift */,
);
path = Add;
@ -1606,6 +1706,7 @@
849C64711ED37A5D003D8FC0 /* NetNewsWireTests.xctest */,
840D617C2029031C009BC708 /* NetNewsWire.app */,
6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */,
513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -1820,10 +1921,11 @@
51C452802265093600C03939 /* Add */,
5183CCEB227117C70010922C /* Settings */,
5183CCDB226F1EEB0010922C /* Progress */,
51C45245226506C800C03939 /* Extensions */,
519D740423243C68008BB345 /* Model Extensions */,
51C45245226506C800C03939 /* UIKit Extensions */,
5F3237FF231DF9D000706F6B /* Views */,
5194B5E222B693EC00144881 /* Wrappers */,
513C5CE7232571C2003D4054 /* ShareExtension */,
84C9FC9A2262A1A900D921D6 /* Resources */,
);
path = iOS;
@ -1945,6 +2047,7 @@
D5907CDF2002F0F9005947E5 /* NetNewsWireTests_target.xcconfig */,
D519E74722EE553300923F27 /* NetNewsWire_safariextension_target.xcconfig */,
51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */,
515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */,
6543108B2322D90900658221 /* common */,
);
path = xcconfig;
@ -1971,6 +2074,24 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
513C5CE5232571C2003D4054 /* NetNewsWire iOS Share Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 513C5CFC232571C2003D4054 /* Build configuration list for PBXNativeTarget "NetNewsWire iOS Share Extension" */;
buildPhases = (
513C5CE2232571C2003D4054 /* Sources */,
513C5CE3232571C2003D4054 /* Frameworks */,
513C5CE4232571C2003D4054 /* Resources */,
513C5CFF2325749A003D4054 /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = "NetNewsWire iOS Share Extension";
productName = "NetNewsWire iOS Share Extension";
productReference = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
6581C73220CED60000F4AD34 /* Subscribe to Feed */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6581C75620CED60100F4AD34 /* Build configuration list for PBXNativeTarget "Subscribe to Feed" */;
@ -1996,6 +2117,8 @@
840D61792029031C009BC708 /* Frameworks */,
840D617A2029031C009BC708 /* Resources */,
51C451DF2264C7F200C03939 /* Embed Frameworks */,
513C5CF1232571C2003D4054 /* Embed App Extensions */,
515D50802326D02600EE1167 /* Run Script: Verify No Build Settings */,
);
buildRules = (
);
@ -2009,6 +2132,7 @@
51C451F72264C83900C03939 /* PBXTargetDependency */,
51C451FB2264C83E00C03939 /* PBXTargetDependency */,
51554C33228B71A10055115A /* PBXTargetDependency */,
513C5CEF232571C2003D4054 /* PBXTargetDependency */,
);
name = "NetNewsWire-iOS";
productName = "NetNewsWire-iOS";
@ -2071,10 +2195,15 @@
849C64581ED37A5D003D8FC0 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0930;
LastSwiftUpdateCheck = 1100;
LastUpgradeCheck = 0930;
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
513C5CE5232571C2003D4054 = {
CreatedOnToolsVersion = 11.0;
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
};
6581C73220CED60000F4AD34 = {
DevelopmentTeam = SHJK2V3AJG;
ProvisioningStyle = Automatic;
@ -2163,6 +2292,7 @@
849C64701ED37A5D003D8FC0 /* NetNewsWireTests */,
840D617B2029031C009BC708 /* NetNewsWire-iOS */,
6581C73220CED60000F4AD34 /* Subscribe to Feed */,
513C5CE5232571C2003D4054 /* NetNewsWire iOS Share Extension */,
);
};
/* End PBXProject section */
@ -2311,6 +2441,15 @@
/* End PBXReferenceProxy section */
/* Begin PBXResourcesBuildPhase section */
513C5CE4232571C2003D4054 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */,
513C5CEC232571C2003D4054 /* MainInterface.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6581C73120CED60000F4AD34 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -2390,6 +2529,24 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
515D50802326D02600EE1167 /* Run Script: Verify No Build Settings */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Run Script: Verify No Build Settings";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n";
};
8423E3E3220158E700C3795B /* Run Script: codesign release builds */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -2443,6 +2600,18 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
513C5CE2232571C2003D4054 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
515D4FC123257A3200EE1167 /* FolderTreeControllerDelegate.swift in Sources */,
515D4FCA23257CB500EE1167 /* Node-Extensions.swift in Sources */,
513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */,
5170743A232AABFC00A461A3 /* FlattenedAccountFolderPickerData.swift in Sources */,
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6581C72F20CED60000F4AD34 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -2484,7 +2653,7 @@
FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */,
51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */,
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */,
51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */,
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */,
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
@ -2722,6 +2891,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
513C5CEF232571C2003D4054 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 513C5CE5232571C2003D4054 /* NetNewsWire iOS Share Extension */;
targetProxy = 513C5CEE232571C2003D4054 /* PBXContainerItemProxy */;
};
51554C27228B71910055115A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = SyncDatabase;
@ -2828,6 +3002,14 @@
name = LaunchScreenPad.storyboard;
sourceTree = "<group>";
};
513C5CEA232571C2003D4054 /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
513C5CEB232571C2003D4054 /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */ = {
isa = PBXVariantGroup;
children = (
@ -2903,6 +3085,20 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
513C5CF2232571C2003D4054 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */;
buildSettings = {
};
name = Debug;
};
513C5CF3232571C2003D4054 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */;
buildSettings = {
};
name = Release;
};
6581C74720CED60100F4AD34 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D519E74722EE553300923F27 /* NetNewsWire_safariextension_target.xcconfig */;
@ -2976,6 +3172,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
513C5CFC232571C2003D4054 /* Build configuration list for PBXNativeTarget "NetNewsWire iOS Share Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
513C5CF2232571C2003D4054 /* Debug */,
513C5CF3232571C2003D4054 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
6581C75620CED60100F4AD34 /* Build configuration list for PBXNativeTarget "Subscribe to Feed" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -43,7 +43,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
extension Article {
var feed: Feed? {
return account?.existingFeed(with: feedID)
return account?.existingFeed(withFeedID: feedID)
}
var preferredLink: String? {

View File

@ -82,5 +82,25 @@
<key>action</key>
<string>delete:</string>
</dict>
<dict>
<key>title</key>
<string>Expand Selected Row</string>
<key>key</key>
<string>[rightarrow]</string>
<key>action</key>
<string>expandSelectedRows:</string>
<key>commandModifier</key>
<true/>
</dict>
<dict>
<key>title</key>
<string>Collapse Selected Row</string>
<key>key</key>
<string>[leftarrow]</string>
<key>action</key>
<string>collapseSelectedRows:</string>
<key>commandModifier</key>
<true/>
</dict>
</array>
</plist>

View File

@ -1,5 +1,5 @@
//
// AddFeedFolderPickerData.swift
// FlattenedAccountFolderPickerData.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/16/19.
@ -12,7 +12,7 @@ import Account
import RSCore
import RSTree
struct AddFeedFolderPickerData {
struct FlattenedAccountFolderPickerData {
var containerNames = [String]()
var containers = [Container]()

View File

@ -19,7 +19,7 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
@IBOutlet private weak var folderPickerView: UIPickerView!
@IBOutlet private weak var folderLabel: UILabel!
private lazy var pickerData: AddFeedFolderPickerData = AddFeedFolderPickerData()
private lazy var pickerData: FlattenedAccountFolderPickerData = FlattenedAccountFolderPickerData()
private var shouldDisplayPicker: Bool {
return pickerData.containerNames.count > 1
}
@ -93,7 +93,7 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
}
if account!.hasFeed(withURL: url.absoluteString) {
showAlreadySubscribedError()
presentError(AccountError.createErrorAlreadySubscribed)
return
}
@ -112,17 +112,8 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
self.delegate?.processingDidEnd()
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
case .failure(let error):
switch error {
case AccountError.createErrorAlreadySubscribed:
self.showAlreadySubscribedError()
self.delegate?.processingDidCancel()
case AccountError.createErrorNotFound:
self.showNoFeedsErrorMessage()
self.delegate?.processingDidCancel()
default:
self.presentError(error)
self.delegate?.processingDidCancel()
}
self.presentError(error)
self.delegate?.processingDidCancel()
}
}
@ -165,29 +156,6 @@ extension AddFeedViewController: UIPickerViewDataSource, UIPickerViewDelegate {
}
private extension AddFeedViewController {
private func showAlreadySubscribedError() {
let title = NSLocalizedString("Already subscribed", comment: "Feed finder")
let message = NSLocalizedString("Cant add this feed because youve already subscribed to it.", comment: "Feed finder")
presentError(title: title, message: message)
}
private func showNoFeedsErrorMessage() {
let title = NSLocalizedString("Feed not found", comment: "Feed finder")
let message = NSLocalizedString("Cant add a feed because no feed was found.", comment: "Feed finder")
presentError(title: title, message: message)
}
private func showInitialDownloadError(_ error: Error) {
let title = NSLocalizedString("Download Error", comment: "Feed finder")
let formatString = NSLocalizedString("Cant add this feed because of a download error: “%@”", comment: "Feed finder")
let message = NSString.localizedStringWithFormat(formatString as NSString, error.localizedDescription)
presentError(title: title, message: message as String)
}
}
extension AddFeedViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {

View File

@ -104,10 +104,6 @@ struct AppAssets {
return UIImage(systemName: "square.and.arrow.up")!
}()
static var smartFeedColor: UIColor = {
return UIColor(named: "smartFeedColor")!
}()
static var smartFeedImage: UIImage = {
return UIImage(systemName: "gear")!
}()
@ -128,6 +124,10 @@ struct AppAssets {
return UIColor(named: "tableViewCellHighlightedTextColor")!
}()
static var tableViewCellIconColor: UIColor = {
return UIColor(named: "tableViewCellIconColor")!
}()
static var tableViewCellSelectionColor: UIColor = {
return UIColor(named: "tableViewCellSelectionColor")!
}()

View File

@ -52,6 +52,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
super.init()
appDelegate = self
AccountManager.shared = AccountManager(accountsFolder: RSDataSubfolder(nil, "Accounts")!)
registerBackgroundTasks()
@ -59,9 +61,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
// Reinitialize the shared state as early as possible
_ = AccountManager.shared
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

View File

@ -17,45 +17,52 @@ enum KeyboardType: String {
class KeyboardManager {
private(set) var keyCommands: [UIKeyCommand]?
private(set) var keyCommands: [UIKeyCommand]
init(type: KeyboardType) {
keyCommands = KeyboardManager.globalAuxilaryKeyCommands()
switch type {
case .sidebar:
keyCommands.append(contentsOf: KeyboardManager.hardcodeFeedKeyCommands())
case .timeline, .detail:
keyCommands.append(contentsOf: KeyboardManager.hardcodeArticleKeyCommands())
default:
break
}
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 globalCommands = globalEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) }
keyCommands.append(contentsOf: globalCommands)
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())
}
keyCommands.append(contentsOf: specificEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) } )
}
static func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand {
let selector = NSSelectorFromString(action)
return UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on)
}
}
private extension KeyboardManager {
func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? {
static func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? {
guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil }
let modifiers = createKeyModifierFlags(keyEntry: keyEntry)
let action = keyEntry["action"] as! String
if let title = keyEntry["title"] as? String {
return createKeyCommand(title: title, action: action, input: input, modifiers: modifiers)
return KeyboardManager.createKeyCommand(title: title, action: action, input: input, modifiers: modifiers)
} else {
return UIKeyCommand(input: input, modifierFlags: modifiers, action: NSSelectorFromString(action))
}
}
func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand {
let selector = NSSelectorFromString(action)
return UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on)
}
func createKeyCommandInput(keyEntry: [String: Any]) -> String? {
static func createKeyCommandInput(keyEntry: [String: Any]) -> String? {
guard let key = keyEntry["key"] as? String else { return nil }
switch(key) {
@ -85,7 +92,7 @@ private extension KeyboardManager {
}
func createKeyModifierFlags(keyEntry: [String: Any]) -> UIKeyModifierFlags {
static func createKeyModifierFlags(keyEntry: [String: Any]) -> UIKeyModifierFlags {
var flags = UIKeyModifierFlags()
if keyEntry["shiftModifier"] as? Bool ?? false {
@ -107,59 +114,65 @@ private extension KeyboardManager {
return flags
}
func globalAuxilaryKeyCommands() -> [UIKeyCommand] {
static 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]))
keys.append(KeyboardManager.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]))
keys.append(KeyboardManager.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]))
keys.append(KeyboardManager.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]))
keys.append(KeyboardManager.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]))
keys.append(KeyboardManager.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]))
keys.append(KeyboardManager.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]))
keys.append(KeyboardManager.createKeyCommand(title: goToStarredTitle, action: "goToStarred:", input: "3", modifiers: [.command]))
let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search")
keys.append(createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .shift]))
keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .shift]))
let markAllAsReadTitle = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
keys.append(KeyboardManager.createKeyCommand(title: markAllAsReadTitle, action: "markAllAsRead:", input: "k", modifiers: [.command]))
return keys
}
func sidebarAuxilaryKeyCommands() -> [UIKeyCommand] {
static func hardcodeFeedKeyCommands() -> [UIKeyCommand] {
var keys = [UIKeyCommand]()
let nextUpTitle = NSLocalizedString("Select Next Up", comment: "Select Next Up")
keys.append(createKeyCommand(title: nextUpTitle, action: "selectNextUp:", input: UIKeyCommand.inputUpArrow, modifiers: []))
keys.append(KeyboardManager.createKeyCommand(title: nextUpTitle, action: "selectNextUp:", input: UIKeyCommand.inputUpArrow, modifiers: []))
let nextDownTitle = NSLocalizedString("Select Next Down", comment: "Select Next Down")
keys.append(createKeyCommand(title: nextDownTitle, action: "selectNextDown:", input: UIKeyCommand.inputDownArrow, modifiers: []))
keys.append(KeyboardManager.createKeyCommand(title: nextDownTitle, action: "selectNextDown:", input: UIKeyCommand.inputDownArrow, modifiers: []))
return keys
}
static func hardcodeArticleKeyCommands() -> [UIKeyCommand] {
var keys = [UIKeyCommand]()
let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser")
keys.append(KeyboardManager.createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command]))
let toggleReadTitle = NSLocalizedString("Toggle Read Status", comment: "Toggle Read Status")
keys.append(KeyboardManager.createKeyCommand(title: toggleReadTitle, action: "toggleRead:", input: "u", modifiers: [.command, .shift]))
let markOlderAsReadTitle = NSLocalizedString("Mark Older as Read", comment: "Mark Older as Read")
keys.append(KeyboardManager.createKeyCommand(title: markOlderAsReadTitle, action: "markOlderArticlesAsRead:", input: "k", modifiers: [.command, .shift]))
let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status")
keys.append(KeyboardManager.createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift]))
return keys
}

View File

@ -113,14 +113,14 @@ class MasterFeedTableViewCell : NNWTableViewCell {
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
super.setHighlighted(highlighted, animated: animated)
let tintColor = isHighlighted || isSelected ? AppAssets.tableViewCellHighlightedTextColor : AppAssets.netNewsWireBlueColor
let tintColor = isHighlighted || isSelected ? AppAssets.tableViewCellHighlightedTextColor : AppAssets.tableViewCellIconColor
faviconImageView.tintColor = tintColor
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
let tintColor = isHighlighted || isSelected ? AppAssets.tableViewCellHighlightedTextColor : AppAssets.netNewsWireBlueColor
let tintColor = isHighlighted || isSelected ? AppAssets.tableViewCellHighlightedTextColor : AppAssets.tableViewCellIconColor
faviconImageView.tintColor = tintColor
}

View File

@ -11,26 +11,26 @@ import RSCore
import RSTree
import Account
class MasterFeedDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable {
class MasterFeedDataSource: UITableViewDiffableDataSource<Node, Node> {
private var coordinator: SceneCoordinator!
private var errorHandler: ((Error) -> ())!
init(coordinator: SceneCoordinator, errorHandler: @escaping (Error) -> (), tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider) {
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
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
guard let node = coordinator.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
guard let node = itemIdentifier(for: indexPath), !(node.representedObject is PseudoFeed) else {
return false
}
return true
}
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
guard let node = coordinator.nodeFor(indexPath) else {
guard let node = itemIdentifier(for: indexPath) else {
return false
}
return node.representedObject is Feed
@ -38,7 +38,7 @@ class MasterFeedDataSource<SectionIdentifierType, ItemIdentifierType>: UITableVi
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
guard let sourceNode = coordinator.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else {
guard let sourceNode = itemIdentifier(for: sourceIndexPath), let feed = sourceNode.representedObject as? Feed else {
return
}
@ -49,7 +49,7 @@ class MasterFeedDataSource<SectionIdentifierType, ItemIdentifierType>: UITableVi
} else {
let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0
let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section)
return coordinator.nodeFor(adjustedDestIndexPath)!
return itemIdentifier(for: adjustedDestIndexPath)!
}
}()

View File

@ -55,7 +55,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
updateUI()
applyChanges(animate: false)
becomeFirstResponder()
}
@ -63,6 +62,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
override func viewWillAppear(_ animated: Bool) {
navigationController?.title = NSLocalizedString("Feeds", comment: "Feeds")
clearsSelectionOnViewWillAppear = coordinator.isRootSplitCollapsed
applyChanges(animate: false)
super.viewWillAppear(animated)
}
@ -97,7 +97,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject)
}
if let node = node, let indexPath = coordinator.indexPathFor(node), let unreadCountProvider = node.representedObject as? UnreadCountProvider {
if let node = node, let indexPath = dataSource.indexPath(for: node), let unreadCountProvider = node.representedObject as? UnreadCountProvider {
if let cell = tableView.cellForRow(at: indexPath) as? MasterFeedTableViewCell {
cell.unreadCount = unreadCountProvider.unreadCount
}
@ -168,7 +168,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
headerView.tag = section
headerView.disclosureExpanded = coordinator.isExpanded(sectionNode)
headerView.disclosureExpanded = sectionNode.isExpanded
let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:)))
headerView.addGestureRecognizer(tap)
@ -206,7 +206,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
renameAction.backgroundColor = UIColor.systemOrange
actions.append(renameAction)
if let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed {
if let feed = dataSource.itemIdentifier(for: indexPath)?.representedObject as? Feed {
let moreTitle = NSLocalizedString("More", comment: "More")
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completionHandler) in
@ -250,7 +250,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let node = coordinator.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
guard let node = dataSource.itemIdentifier(for: indexPath), !(node.representedObject is PseudoFeed) else {
return nil
}
if node.representedObject is Feed {
@ -275,13 +275,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return coordinator.cappedIndexPath(proposedDestinationIndexPath)
}()
guard let draggedNode = coordinator.nodeFor(sourceIndexPath), let destNode = coordinator.nodeFor(destIndexPath), let parentNode = destNode.parent else {
guard let draggedNode = dataSource.itemIdentifier(for: sourceIndexPath), let destNode = dataSource.itemIdentifier(for: destIndexPath), let parentNode = destNode.parent else {
assertionFailure("This should never happen")
return sourceIndexPath
}
// 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 || !coordinator.isExpanded(destNode)) {
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)
}
@ -302,7 +302,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
if parentNode.representedObject is Account {
return IndexPath(row: 0, section: destIndexPath.section)
} else {
return coordinator.indexPathFor(parentNode)!
return dataSource.indexPath(for: parentNode)!
}
} else {
@ -312,10 +312,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
let adjustedIndex = index - movementAdjustment
if adjustedIndex >= sortedNodes.count {
let lastSortedIndexPath = coordinator.indexPathFor(sortedNodes[sortedNodes.count - 1])!
let lastSortedIndexPath = dataSource.indexPath(for: sortedNodes[sortedNodes.count - 1])!
return IndexPath(row: lastSortedIndexPath.row + 1, section: lastSortedIndexPath.section)
} else {
return coordinator.indexPathFor(sortedNodes[adjustedIndex])!
return dataSource.indexPath(for: sortedNodes[adjustedIndex])!
}
}
@ -362,13 +362,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return
}
if coordinator.isExpanded(sectionNode) {
if sectionNode.isExpanded {
headerView.disclosureExpanded = false
coordinator.collapseSection(sectionIndex)
coordinator.collapse(sectionNode)
self.applyChanges(animate: true)
} else {
headerView.disclosureExpanded = true
coordinator.expandSection(sectionIndex)
coordinator.expand(sectionNode)
self.applyChanges(animate: true)
}
@ -408,38 +408,58 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
@objc func expandSelectedRows(_ sender: Any?) {
if let indexPath = coordinator.currentFeedIndexPath {
coordinator.expandFolder(indexPath)
self.applyChanges(animate: true)
if let indexPath = coordinator.currentFeedIndexPath, let node = dataSource.itemIdentifier(for: indexPath) {
coordinator.expand(node)
self.applyChanges(animate: true) {
self.reloadAllVisibleCells()
}
}
}
@objc func collapseSelectedRows(_ sender: Any?) {
if let indexPath = coordinator.currentFeedIndexPath {
coordinator.collapseFolder(indexPath)
self.applyChanges(animate: true)
if let indexPath = coordinator.currentFeedIndexPath, let node = dataSource.itemIdentifier(for: indexPath) {
coordinator.collapse(node)
self.applyChanges(animate: true) {
self.reloadAllVisibleCells()
}
}
}
@objc func expandAll(_ sender: Any?) {
coordinator.expandAllSectionsAndFolders()
self.applyChanges(animate: true)
self.applyChanges(animate: true) {
self.reloadAllVisibleCells()
}
}
@objc func collapseAllExceptForGroupItems(_ sender: Any?) {
coordinator.collapseAllFolders()
self.applyChanges(animate: true)
self.applyChanges(animate: true) {
self.reloadAllVisibleCells()
}
}
// MARK: API
func updateFeedSelection() {
if let indexPath = coordinator.currentFeedIndexPath {
if tableView.indexPathForSelectedRow != indexPath {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true, deselect: coordinator.isRootSplitCollapsed)
func restoreSelectionIfNecessary(adjustScroll: Bool) {
if let indexPath = coordinator.masterFeedIndexPathForCurrentTimeline() {
if adjustScroll {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed)
} else {
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
}
}
}
func updateFeedSelection() {
if dataSource.snapshot().numberOfItems > 0 {
if let indexPath = coordinator.currentFeedIndexPath {
if tableView.indexPathForSelectedRow != indexPath {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true, deselect: coordinator.isRootSplitCollapsed)
}
} else {
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
}
} else {
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
}
}
@ -459,8 +479,8 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return
}
if !coordinator.isExpanded(sectionNode) {
coordinator.expandSection(sectionIndex)
if !sectionNode.isExpanded {
coordinator.expand(sectionNode)
self.applyChanges(animate: true) {
completion?()
}
@ -476,25 +496,23 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return
}
if let indexPath = coordinator.indexPathFor(node) {
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
if let indexPath = dataSource.indexPath(for: node) {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, 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 {
guard let parent = node.parent else {
completion?()
return
}
coordinator.expandFolder(indexPath)
reloadNode(parent)
coordinator.expand(parent)
self.applyChanges(animate: true) { [weak self] in
if let indexPath = self?.coordinator.indexPathFor(node) {
self?.tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
self.applyChanges(animate: true, adjustScroll: true) { [weak self] in
if let indexPath = self?.dataSource.indexPath(for: node) {
self?.coordinator.selectFeed(indexPath)
completion?()
}
@ -535,26 +553,27 @@ private extension MasterFeedViewController {
var snapshot = dataSource.snapshot()
snapshot.reloadItems([node])
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
self?.restoreSelectionIfNecessary()
self?.restoreSelectionIfNecessary(adjustScroll: false)
}
}
func applyChanges(animate: Bool, completion: (() -> Void)? = nil) {
var snapshot = NSDiffableDataSourceSnapshot<Int, Node>()
let sections = coordinator.allSections
snapshot.appendSections(sections)
func applyChanges(animate: Bool, adjustScroll: Bool = false, completion: (() -> Void)? = nil) {
var snapshot = NSDiffableDataSourceSnapshot<Node, Node>()
let sectionNodes = coordinator.rootNode.childNodes
snapshot.appendSections(sectionNodes)
for section in sections {
snapshot.appendItems(coordinator.nodesFor(section: section), toSection: section)
for (index, sectionNode) in sectionNodes.enumerated() {
let shadowTableNodes = coordinator.shadowNodesFor(section: index)
snapshot.appendItems(shadowTableNodes, toSection: sectionNode)
}
dataSource.apply(snapshot, animatingDifferences: animate) { [weak self] in
self?.restoreSelectionIfNecessary()
self?.restoreSelectionIfNecessary(adjustScroll: adjustScroll)
completion?()
}
}
func makeDataSource() -> UITableViewDiffableDataSource<Int, Node> {
func makeDataSource() -> UITableViewDiffableDataSource<Node, Node> {
return MasterFeedDataSource(coordinator: coordinator, errorHandler: ErrorHandler.present(self), tableView: tableView, cellProvider: { [weak self] tableView, indexPath, node in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
self?.configure(cell, node)
@ -570,7 +589,7 @@ private extension MasterFeedViewController {
} else {
cell.indentationLevel = 0
}
cell.disclosureExpanded = coordinator.isExpanded(node)
cell.disclosureExpanded = node.isExpanded
cell.allowDisclosureSelection = node.canHaveChildNodes
cell.name = nameFor(node)
@ -612,7 +631,7 @@ private extension MasterFeedViewController {
func applyToAvailableCells(_ callback: (MasterFeedTableViewCell, Node) -> Void) {
tableView.visibleCells.forEach { cell in
guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else {
guard let indexPath = tableView.indexPath(for: cell), let node = dataSource.itemIdentifier(for: indexPath) else {
return
}
callback(cell as! MasterFeedTableViewCell, node)
@ -620,7 +639,7 @@ private extension MasterFeedViewController {
}
private func reloadAllVisibleCells() {
let visibleNodes = tableView.indexPathsForVisibleRows!.compactMap { return coordinator.nodeFor($0) }
let visibleNodes = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
reloadCells(visibleNodes)
}
@ -628,7 +647,7 @@ private extension MasterFeedViewController {
var snapshot = dataSource.snapshot()
snapshot.reloadItems(nodes)
dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
self?.restoreSelectionIfNecessary()
self?.restoreSelectionIfNecessary(adjustScroll: false)
}
}
@ -646,27 +665,21 @@ private extension MasterFeedViewController {
}
func expand(_ cell: MasterFeedTableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else {
guard let indexPath = tableView.indexPath(for: cell), let node = dataSource.itemIdentifier(for: indexPath) else {
return
}
coordinator.expandFolder(indexPath)
coordinator.expand(node)
self.applyChanges(animate: true)
}
func collapse(_ cell: MasterFeedTableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else {
guard let indexPath = tableView.indexPath(for: cell), let node = dataSource.itemIdentifier(for: indexPath) else {
return
}
coordinator.collapseFolder(indexPath)
coordinator.collapse(node)
self.applyChanges(animate: true)
}
func restoreSelectionIfNecessary() {
if let indexPath = coordinator.masterFeedIndexPathForCurrentTimeline(), indexPath != tableView.indexPathForSelectedRow {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed)
}
}
func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in
@ -737,7 +750,7 @@ private extension MasterFeedViewController {
}
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
guard let node = coordinator.nodeFor(indexPath),
guard let node = dataSource.itemIdentifier(for: indexPath),
let feed = node.representedObject as? Feed,
let url = URL(string: feed.url) else {
return nil
@ -751,7 +764,7 @@ private extension MasterFeedViewController {
}
func copyFeedPageAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let node = coordinator.nodeFor(indexPath),
guard let node = dataSource.itemIdentifier(for: indexPath),
let feed = node.representedObject as? Feed,
let url = URL(string: feed.url) else {
return nil
@ -766,7 +779,7 @@ private extension MasterFeedViewController {
}
func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
guard let node = coordinator.nodeFor(indexPath),
guard let node = dataSource.itemIdentifier(for: indexPath),
let feed = node.representedObject as? Feed,
let homePageURL = feed.homePageURL,
let url = URL(string: homePageURL) else {
@ -781,7 +794,7 @@ private extension MasterFeedViewController {
}
func copyHomePageAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let node = coordinator.nodeFor(indexPath),
guard let node = dataSource.itemIdentifier(for: indexPath),
let feed = node.representedObject as? Feed,
let homePageURL = feed.homePageURL,
let url = URL(string: homePageURL) else {
@ -814,7 +827,7 @@ private extension MasterFeedViewController {
func rename(indexPath: IndexPath) {
let name = (coordinator.nodeFor(indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? ""
let name = (dataSource.itemIdentifier(for: indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? ""
let formatString = NSLocalizedString("Rename “%@”", comment: "Feed finder")
let title = NSString.localizedStringWithFormat(formatString as NSString, name) as String
@ -826,7 +839,7 @@ private extension MasterFeedViewController {
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in
guard let node = self?.coordinator.nodeFor(indexPath),
guard let node = self?.dataSource.itemIdentifier(for: indexPath),
let name = alertController.textFields?[0].text,
!name.isEmpty else {
return
@ -868,7 +881,7 @@ private extension MasterFeedViewController {
func delete(indexPath: IndexPath) {
guard let undoManager = undoManager,
let deleteNode = coordinator.nodeFor(indexPath),
let deleteNode = dataSource.itemIdentifier(for: indexPath),
let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], undoManager: undoManager, errorHandler: ErrorHandler.present(self))
else {
return

View File

@ -68,13 +68,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
numberOfTextLines = AppDefaults.timelineNumberOfLines
resetEstimatedRowHeight()
applyChanges(animate: false)
resetUI()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = coordinator.isRootSplitCollapsed
applyChanges(animate: false)
super.viewWillAppear(animated)
}
@ -145,18 +145,22 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// MARK: API
func restoreSelectionIfNecessary() {
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed)
}
}
func reinitializeArticles() {
resetUI()
}
func reloadArticles(animate: Bool) {
applyChanges(animate: animate) { [weak self] in
self?.updateArticleSelection(animate: animate)
}
applyChanges(animate: animate)
}
func updateArticleSelection(animate: Bool) {
if let indexPath = coordinator.currentArticleIndexPath {
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
if tableView.indexPathForSelectedRow != indexPath {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true, deselect: coordinator.isRootSplitCollapsed)
}
@ -181,7 +185,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let article = coordinator.articles[indexPath.row]
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
// Set up the read action
let readTitle = article.status.read ?
@ -189,7 +193,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
NSLocalizedString("Read", comment: "Read")
let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completionHandler) in
self?.coordinator.toggleRead(for: indexPath)
self?.coordinator.toggleRead(article)
completionHandler(true)
}
@ -202,7 +206,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
NSLocalizedString("Star", comment: "Star")
let starAction = UIContextualAction(style: .normal, title: starTitle) { [weak self] (action, view, completionHandler) in
self?.coordinator.toggleStar(for: indexPath)
self?.coordinator.toggleStar(article)
completionHandler(true)
}
@ -221,21 +225,21 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1)
}
alert.addAction(self.markOlderAsReadAlertAction(indexPath: indexPath, completionHandler: completionHandler))
alert.addAction(self.markOlderAsReadAlertAction(article, completionHandler: completionHandler))
if let action = self.discloseFeedAlertAction(indexPath: indexPath, completionHandler: completionHandler) {
if let action = self.discloseFeedAlertAction(article, completionHandler: completionHandler) {
alert.addAction(action)
}
if let action = self.markAllInFeedAsReadAlertAction(indexPath: indexPath, completionHandler: completionHandler) {
if let action = self.markAllInFeedAsReadAlertAction(article, completionHandler: completionHandler) {
alert.addAction(action)
}
if let action = self.openInBrowserAlertAction(indexPath: indexPath, completionHandler: completionHandler) {
if let action = self.openInBrowserAlertAction(article, completionHandler: completionHandler) {
alert.addAction(action)
}
if let action = self.shareAlertAction(indexPath: indexPath, completionHandler: completionHandler) {
if let action = self.shareAlertAction(article, indexPath: indexPath, completionHandler: completionHandler) {
alert.addAction(action)
}
@ -260,28 +264,30 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { [weak self] suggestedActions in
guard let self = self else { return nil }
var actions = [UIAction]()
actions.append(self.toggleArticleReadStatusAction(indexPath: indexPath))
actions.append(self.toggleArticleStarStatusAction(indexPath: indexPath))
actions.append(self.markOlderAsReadAction(indexPath: indexPath))
actions.append(self.toggleArticleReadStatusAction(article))
actions.append(self.toggleArticleStarStatusAction(article))
actions.append(self.markOlderAsReadAction(article))
if let action = self.discloseFeedAction(indexPath: indexPath) {
if let action = self.discloseFeedAction(article) {
actions.append(action)
}
if let action = self.markAllInFeedAsReadAction(indexPath: indexPath) {
if let action = self.markAllInFeedAsReadAction(article) {
actions.append(action)
}
if let action = self.openInBrowserAction(indexPath: indexPath) {
if let action = self.openInBrowserAction(article) {
actions.append(action)
}
if let action = self.shareAction(indexPath: indexPath) {
if let action = self.shareAction(article, indexPath: indexPath) {
actions.append(action)
}
@ -293,7 +299,8 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
becomeFirstResponder()
coordinator.selectArticle(indexPath, automated: false)
let article = dataSource.itemIdentifier(for: indexPath)
coordinator.selectArticle(article, automated: false)
}
// MARK: Notifications
@ -307,12 +314,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
return
}
let visibleArticles = tableView.indexPathsForVisibleRows!.map { return coordinator.articles[$0.row] }
let visibleArticles = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
let visibleUpdatedArticles = visibleArticles.filter { updatedArticles.contains($0) }
for article in visibleUpdatedArticles {
if let articleIndex = coordinator.indexForArticleID(article.articleID) {
if let cell = tableView.cellForRow(at: IndexPath(row: articleIndex, section: 0)) as? MasterTimelineTableViewCell {
if let indexPath = dataSource.indexPath(for: article) {
if let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell {
configure(cell, article: article)
}
}
@ -324,7 +331,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
return
}
tableView.indexPathsForVisibleRows?.forEach { indexPath in
guard let article = coordinator.articles.articleAtRow(indexPath.row) else {
guard let article = dataSource.itemIdentifier(for: indexPath) else {
return
}
if article.feed == feed, let cell = tableView.cellForRow(at: indexPath) as? MasterTimelineTableViewCell, let image = avatarFor(article) {
@ -338,7 +345,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
return
}
tableView.indexPathsForVisibleRows?.forEach { indexPath in
guard let article = coordinator.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
guard let article = dataSource.itemIdentifier(for: indexPath), let authors = article.authors, !authors.isEmpty else {
return
}
for author in authors {
@ -378,7 +385,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
@objc private func reloadAllVisibleCells() {
let visibleArticles = tableView.indexPathsForVisibleRows!.map { return coordinator.articles[$0.row] }
let visibleArticles = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) }
reloadCells(visibleArticles)
}
@ -465,7 +472,7 @@ private extension MasterTimelineViewController {
navigationController?.title = coordinator.timelineName
tableView.selectRow(at: nil, animated: false, scrollPosition: .top)
if coordinator.articles.count > 0 {
if dataSource.snapshot().itemIdentifiers(inSection: 0).count > 0 {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
}
@ -527,16 +534,8 @@ private extension MasterTimelineViewController {
}
return nil
}
func restoreSelectionIfNecessary() {
if let articleID = coordinator.currentArticle?.articleID, let index = coordinator.indexForArticleID(articleID) {
let indexPath = IndexPath(row: index, section: 0)
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false, deselect: coordinator.isRootSplitCollapsed)
}
}
func toggleArticleReadStatusAction(indexPath: IndexPath) -> UIAction {
let article = coordinator.articles[indexPath.row]
func toggleArticleReadStatusAction(_ article: Article) -> UIAction {
let title = article.status.read ?
NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
@ -544,14 +543,13 @@ private extension MasterTimelineViewController {
let image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
let action = UIAction(title: title, image: image) { [weak self] action in
self?.coordinator.toggleRead(for: indexPath)
self?.coordinator.toggleRead(article)
}
return action
}
func toggleArticleStarStatusAction(indexPath: IndexPath) -> UIAction {
let article = coordinator.articles[indexPath.row]
func toggleArticleStarStatusAction(_ article: Article) -> UIAction {
let title = article.status.starred ?
NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") :
@ -559,34 +557,33 @@ private extension MasterTimelineViewController {
let image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
let action = UIAction(title: title, image: image) { [weak self] action in
self?.coordinator.toggleStar(for: indexPath)
self?.coordinator.toggleStar(article)
}
return action
}
func markOlderAsReadAction(indexPath: IndexPath) -> UIAction {
func markOlderAsReadAction(_ article: Article) -> UIAction {
let title = NSLocalizedString("Mark Older as Read", comment: "Mark Older as Read")
let image = coordinator.sortDirection == .orderedDescending ? AppAssets.markOlderAsReadDownImage : AppAssets.markOlderAsReadUpImage
let action = UIAction(title: title, image: image) { [weak self] action in
self?.coordinator.markAsReadOlderArticlesInTimeline(indexPath)
self?.coordinator.markAsReadOlderArticlesInTimeline(article)
}
return action
}
func markOlderAsReadAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction {
func markOlderAsReadAlertAction(_ article: Article, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction {
let title = NSLocalizedString("Mark Older as Read", comment: "Mark Older as Read")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.markAsReadOlderArticlesInTimeline(indexPath)
self?.coordinator.markAsReadOlderArticlesInTimeline(article)
completionHandler(true)
}
return action
}
func discloseFeedAction(indexPath: IndexPath) -> UIAction? {
guard let feed = coordinator.articles[indexPath.row].feed else {
return nil
}
func discloseFeedAction(_ article: Article) -> UIAction? {
guard let feed = article.feed else { return nil }
let title = NSLocalizedString("Select Feed", comment: "Select Feed")
let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in
self?.coordinator.discloseFeed(feed)
@ -594,10 +591,9 @@ private extension MasterTimelineViewController {
return action
}
func discloseFeedAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let feed = coordinator.articles[indexPath.row].feed else {
return nil
}
func discloseFeedAlertAction(_ article: Article, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let feed = article.feed else { return nil }
let title = NSLocalizedString("Select Feed", comment: "Select Feed")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.discloseFeed(feed)
@ -606,11 +602,9 @@ private extension MasterTimelineViewController {
return action
}
func markAllInFeedAsReadAction(indexPath: IndexPath) -> UIAction? {
guard let feed = coordinator.articles[indexPath.row].feed else {
return nil
}
func markAllInFeedAsReadAction(_ article: Article) -> UIAction? {
guard let feed = article.feed else { return nil }
let articles = Array(feed.fetchArticles())
guard articles.canMarkAllAsRead() else {
return nil
@ -625,11 +619,9 @@ private extension MasterTimelineViewController {
return action
}
func markAllInFeedAsReadAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let feed = coordinator.articles[indexPath.row].feed else {
return nil
}
func markAllInFeedAsReadAlertAction(_ article: Article, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let feed = article.feed else { return nil }
let articles = Array(feed.fetchArticles())
guard articles.canMarkAllAsRead() else {
return nil
@ -645,24 +637,24 @@ private extension MasterTimelineViewController {
return action
}
func openInBrowserAction(indexPath: IndexPath) -> UIAction? {
guard let preferredLink = coordinator.articles[indexPath.row].preferredLink, let _ = URL(string: preferredLink) else {
func openInBrowserAction(_ article: Article) -> UIAction? {
guard let preferredLink = article.preferredLink, let _ = URL(string: preferredLink) else {
return nil
}
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in
self?.coordinator.showBrowserForArticle(indexPath)
self?.coordinator.showBrowserForArticle(article)
}
return action
}
func openInBrowserAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let preferredLink = coordinator.articles[indexPath.row].preferredLink, let _ = URL(string: preferredLink) else {
func openInBrowserAlertAction(_ article: Article, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let preferredLink = article.preferredLink, let _ = URL(string: preferredLink) else {
return nil
}
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.showBrowserForArticle(indexPath)
self?.coordinator.showBrowserForArticle(article)
completionHandler(true)
}
return action
@ -680,8 +672,7 @@ private extension MasterTimelineViewController {
present(activityViewController, animated: true)
}
func shareAction(indexPath: IndexPath) -> UIAction? {
let article = coordinator.articles[indexPath.row]
func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
return nil
}
@ -693,8 +684,7 @@ private extension MasterTimelineViewController {
return action
}
func shareAlertAction(indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
let article = coordinator.articles[indexPath.row]
func shareAlertAction(_ article: Article, indexPath: IndexPath, completionHandler: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
return nil
}

View File

@ -24,7 +24,7 @@ public final class NavigationProgressView: UIView {
internal let bar = UIView()
@objc public dynamic var progressTintColor: UIColor? = AppAssets.netNewsWireBlueColor {
@objc public dynamic var progressTintColor: UIColor? = AppAssets.barTintColor {
didSet {
bar.backgroundColor = progressTintColor
}

View File

@ -15,6 +15,24 @@
"green" : "0x6A"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0x44",
"alpha" : "1.000",
"blue" : "0xE2",
"green" : "0x90"
}
}
}
]
}

View File

@ -1,20 +0,0 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "85",
"alpha" : "1.000",
"blue" : "85",
"green" : "85"
}
}
}
]
}

View File

@ -0,0 +1,38 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0x44",
"alpha" : "1.000",
"blue" : "0xE2",
"green" : "0x90"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0x8C",
"alpha" : "1.000",
"blue" : "0xFA",
"green" : "0xBF"
}
}
}
]
}

View File

@ -27,10 +27,10 @@
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.267",
"red" : "0x44",
"alpha" : "1.000",
"blue" : "0.886",
"green" : "0.565"
"blue" : "0xE1",
"green" : "0x90"
}
}
}

View File

@ -9,10 +9,28 @@
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "111",
"red" : "0x44",
"alpha" : "1.000",
"blue" : "250",
"green" : "175"
"blue" : "0xE2",
"green" : "0x90"
}
}
},
{
"idiom" : "universal",
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.549",
"alpha" : "1.000",
"blue" : "0.980",
"green" : "0.749"
}
}
}

View File

@ -45,7 +45,7 @@ a:hover {
@media(prefers-color-scheme: dark) {
:root {
--link-color: #4490e2;
--link-color: #8CBFFA;
--header-table-border-color: rgba(255, 255, 255, 0.1);
--header-color: #d2d2d2;
--header-link-color: #4490e2;

View File

@ -13,6 +13,14 @@ class RootSplitViewController: UISplitViewController {
var coordinator: SceneCoordinator!
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { [weak self] context in
if UIApplication.shared.applicationState != .background {
self?.coordinator.configureThreePanelMode(for: size)
}
})
}
// MARK: Keyboard Shortcuts
@objc func scrollOrGoToNextUnread(_ sender: Any?) {

View File

@ -32,16 +32,20 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private var masterFeedViewController: MasterFeedViewController!
private var masterTimelineViewController: MasterTimelineViewController?
private var subSplitViewController: UISplitViewController? {
return rootSplitViewController.children.last as? UISplitViewController
}
private var detailViewController: DetailViewController? {
if let detail = masterNavigationController.viewControllers.last as? DetailViewController {
return detail
}
if let subSplit = rootSplitViewController.viewControllers.last?.children.first as? UISplitViewController {
if let subSplit = subSplitViewController {
if let navController = subSplit.viewControllers.last as? UINavigationController {
return navController.topViewController as? DetailViewController
}
} else {
if let navController = rootSplitViewController.viewControllers.last?.children.first as? UINavigationController {
if let navController = rootSplitViewController.viewControllers.last as? UINavigationController {
return navController.topViewController as? DetailViewController
}
}
@ -51,10 +55,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
private var articleRowMap = [String: Int]() // articleID: rowIndex
private var animatingChanges = false
private var expandedNodes = [Node]()
private var shadowTable = [[Node]]()
private var lastSearchString = ""
private var lastSearchScope: SearchScope? = nil
@ -90,23 +92,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
var isThreePanelMode: Bool {
return rootSplitViewController.traitCollection.userInterfaceIdiom == .pad &&
!rootSplitViewController.isCollapsed &&
rootSplitViewController.displayMode == .allVisible
return subSplitViewController != nil
}
var rootNode: Node {
return treeController.rootNode
}
var allSections: [Int] {
var sections = [Int]()
for (index, _) in shadowTable.enumerated() {
sections.append(index)
}
return sections
}
private(set) var currentFeedIndexPath: IndexPath?
var timelineName: String? {
@ -193,31 +185,31 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
var isPrevArticleAvailable: Bool {
guard let indexPath = currentArticleIndexPath else {
guard let articleRow = currentArticleRow else {
return false
}
return indexPath.row > 0
return articleRow > 0
}
var isNextArticleAvailable: Bool {
guard let indexPath = currentArticleIndexPath else {
guard let articleRow = currentArticleRow else {
return false
}
return indexPath.row + 1 < articles.count
return articleRow + 1 < articles.count
}
var prevArticleIndexPath: IndexPath? {
guard isPrevArticleAvailable, let indexPath = currentArticleIndexPath else {
var prevArticle: Article? {
guard isPrevArticleAvailable, let articleRow = currentArticleRow else {
return nil
}
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
return articles[articleRow - 1]
}
var nextArticleIndexPath: IndexPath? {
guard isNextArticleAvailable, let indexPath = currentArticleIndexPath else {
var nextArticle: Article? {
guard isNextArticleAvailable, let articleRow = currentArticleRow else {
return nil
}
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
return articles[articleRow + 1]
}
var firstUnreadArticleIndexPath: IndexPath? {
@ -229,17 +221,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return nil
}
var currentArticle: Article? {
if let indexPath = currentArticleIndexPath, indexPath.row < articles.count {
return articles[indexPath.row]
}
return nil
}
private(set) var currentArticleIndexPath: IndexPath?
var currentArticle: Article?
private(set) var articles = ArticleArray()
private var currentArticleRow: Int? {
guard let article = currentArticle else { return nil }
return articles.firstIndex(of: article)
}
var isTimelineUnreadAvailable: Bool {
if let unreadProvider = timelineFetcher as? UnreadCountProvider {
return unreadProvider.unreadCount > 0
@ -263,7 +252,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
super.init()
for section in treeController.rootNode.childNodes {
expandedNodes.append(section)
section.isExpanded = true
shadowTable.append([Node]())
}
@ -284,10 +273,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
let _ = DetailViewControllerWebViewProvider.shared
}
func start() -> UIViewController {
func start(for size: CGSize) -> UIViewController {
rootSplitViewController = RootSplitViewController()
rootSplitViewController.coordinator = self
rootSplitViewController.preferredDisplayMode = .automatic
rootSplitViewController.preferredDisplayMode = .allVisible
rootSplitViewController.viewControllers = [ThemedNavigationController.template()]
rootSplitViewController.delegate = self
@ -298,12 +287,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
masterFeedViewController.coordinator = self
masterNavigationController.pushViewController(masterFeedViewController, animated: false)
let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
let detailNavController = addNavControllerIfNecessary(systemMessageViewController, showButton: true)
let shimController = UIViewController()
shimController.replaceChildAndPinView(detailNavController)
rootSplitViewController.showDetailViewController(shimController, sender: self)
let noSelectionController = fullyWrappedSystemMesssageController(showButton: true)
rootSplitViewController.showDetailViewController(noSelectionController, sender: self)
configureThreePanelMode(for: size)
return rootSplitViewController
}
@ -329,6 +317,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
func configureThreePanelMode(for size: CGSize) {
guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad && !rootSplitViewController.isCollapsed else {
return
}
if size.width > size.height {
if !isThreePanelMode {
transitionToThreePanelMode()
}
} else {
if isThreePanelMode {
transitionFromThreePanelMode()
}
}
}
func selectFirstUnreadInAllUnread() {
selectFeed(IndexPath(row: 1, section: 0))
selectFirstUnreadArticleInTimeline()
@ -373,17 +376,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return
}
// If we are deactivating an account, clean up the expandedNodes table
if !account.isActive, let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
if let nodeIndex = self.expandedNodes.firstIndex(of: node) {
self.expandedNodes.remove(at: nodeIndex)
}
}
rebuildBackingStores() {
// If we are activating an account, then automatically expand it
if account.isActive, let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
self.expandedNodes.append(node)
node.isExpanded = true
}
}
}
@ -397,7 +393,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
// Automatically expand any new accounts
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
let node = self.treeController.rootNode.childNodeRepresentingObject(account) {
self.expandedNodes.append(node)
node.isExpanded = true
}
}
}
@ -406,15 +402,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
if timelineFetcherContainsAnyPseudoFeed() {
fetchAndReplaceArticlesSync()
}
rebuildBackingStores() {
// Clean up the expandedNodes table for any deleted accounts
if let account = note.userInfo?[Account.UserInfoKey.account] as? Account,
let node = self.treeController.rootNode.childNodeRepresentingObject(account),
let nodeIndex = self.expandedNodes.firstIndex(of: node) {
self.expandedNodes.remove(at: nodeIndex)
}
}
rebuildBackingStores()
}
@objc func userDefaultsDidChange(_ note: Notification) {
@ -435,22 +423,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
// MARK: API
func rowsInSection(_ section: Int) -> Int {
return shadowTable[section].count
}
func isExpanded(_ node: Node) -> Bool {
return expandedNodes.contains(node)
}
func nodeFor(_ indexPath: IndexPath) -> Node? {
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
return nil
}
return shadowTable[indexPath.section][indexPath.row]
}
func nodesFor(section: Int) -> [Node] {
func shadowNodesFor(section: Int) -> [Node] {
return shadowTable[section]
}
@ -461,22 +434,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return indexPath
}
func indexPathFor(_ node: Node) -> IndexPath? {
for i in 0..<shadowTable.count {
if let row = shadowTable[i].firstIndex(of: node) {
return IndexPath(row: row, section: i)
}
}
return nil
}
func indexPathFor(_ object: AnyObject) -> IndexPath? {
guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else {
return nil
}
return indexPathFor(node)
}
func unreadCountFor(_ node: Node) -> Int {
// The coordinator supplies the unread count for the currently selected feed node
if let indexPath = currentFeedIndexPath, let selectedNode = nodeFor(indexPath), selectedNode == node {
@ -488,137 +445,55 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
return 0
}
func expandSection(_ section: Int) {
guard let expandNode = treeController.rootNode.childAtIndex(section), !expandedNodes.contains(expandNode) else {
return
}
expandedNodes.append(expandNode)
func expand(_ node: Node) {
node.isExpanded = true
animatingChanges = true
var i = 0
func addNode(_ node: Node) {
shadowTable[section].insert(node, at: i)
i = i + 1
}
for child in expandNode.childNodes {
addNode(child)
if expandedNodes.contains(child) {
for gChild in child.childNodes {
addNode(gChild)
}
}
}
rebuildShadowTable()
animatingChanges = false
}
func expandAllSectionsAndFolders() {
for (sectionIndex, sectionNode) in treeController.rootNode.childNodes.enumerated() {
expandSection(sectionIndex)
for sectionNode in treeController.rootNode.childNodes {
sectionNode.isExpanded = true
for topLevelNode in sectionNode.childNodes {
if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) {
expandFolder(indexPath)
if topLevelNode.representedObject is Folder {
topLevelNode.isExpanded = true
}
}
}
}
func expandFolder(_ indexPath: IndexPath) {
let expandNode = shadowTable[indexPath.section][indexPath.row]
guard !expandedNodes.contains(expandNode) else { return }
expandedNodes.append(expandNode)
animatingChanges = true
for i in 0..<expandNode.childNodes.count {
if let child = expandNode.childAtIndex(i) {
let nextIndex = indexPath.row + i + 1
shadowTable[indexPath.section].insert(child, at: nextIndex)
}
}
rebuildShadowTable()
animatingChanges = false
}
func collapseSection(_ section: Int) {
guard let collapseNode = treeController.rootNode.childAtIndex(section), expandedNodes.contains(collapseNode) else {
return
}
func collapse(_ node: Node) {
node.isExpanded = false
animatingChanges = true
if let removeNode = expandedNodes.firstIndex(of: collapseNode) {
expandedNodes.remove(at: removeNode)
}
shadowTable[section] = [Node]()
rebuildShadowTable()
animatingChanges = false
}
func collapseAllFolders() {
for sectionNode in treeController.rootNode.childNodes {
sectionNode.isExpanded = true
for topLevelNode in sectionNode.childNodes {
if topLevelNode.representedObject is Folder, let indexPath = indexPathFor(topLevelNode) {
collapseFolder(indexPath)
if topLevelNode.representedObject is Folder {
topLevelNode.isExpanded = true
}
}
}
}
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)
}
for child in collapseNode.childNodes {
if let index = shadowTable[indexPath.section].firstIndex(of: child) {
shadowTable[indexPath.section].remove(at: index)
}
}
rebuildShadowTable()
animatingChanges = false
}
func masterFeedIndexPathForCurrentTimeline() -> IndexPath? {
guard let node = treeController.rootNode.descendantNode(where: { return $0.representedObject === timelineFetcher as AnyObject }) else {
guard let node = treeController.rootNode.descendantNodeRepresentingObject(timelineFetcher as AnyObject) else {
return nil
}
return indexPathFor(node)
}
func indexForArticleID(_ articleID: String?) -> Int? {
guard let articleID = articleID else { return nil }
updateArticleRowMapIfNeeded()
return articleRowMap[articleID]
}
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
var indexes = IndexSet()
articleIDs.forEach { (articleID) in
guard let oneIndex = indexForArticleID(articleID) else {
return
}
if oneIndex != NSNotFound {
indexes.insert(oneIndex)
}
}
return indexes
}
func selectFeed(_ indexPath: IndexPath?, automated: Bool = true) {
selectArticle(nil)
currentFeedIndexPath = indexPath
@ -631,7 +506,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
installTimelineControllerIfNecessary(animated: !automated)
} else {
timelineFetcher = nil
activityManager.invalidateSelecting()
if rootSplitViewController.isCollapsed && navControllerForTimeline().viewControllers.last is MasterTimelineViewController {
navControllerForTimeline().popViewController(animated: !automated)
}
@ -669,11 +544,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
func selectArticle(_ indexPath: IndexPath?, automated: Bool = true) {
currentArticleIndexPath = indexPath
func selectArticle(_ article: Article?, automated: Bool = true) {
currentArticle = article
activityManager.reading(currentArticle)
if indexPath == nil {
if article == nil {
if rootSplitViewController.isCollapsed {
if masterNavigationController.children.last is DetailViewController {
masterNavigationController.popViewController(animated: !automated)
@ -692,14 +567,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
installDetailController(detailViewController, automated: automated)
}
// Automatically hide the overlay
if rootSplitViewController.displayMode == .primaryOverlay {
UIView.animate(withDuration: 0.3) {
self.rootSplitViewController.preferredDisplayMode = .primaryHidden
}
rootSplitViewController.preferredDisplayMode = .automatic
}
if automated {
masterTimelineViewController?.updateArticleSelection(animate: false)
}
@ -728,6 +595,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
} else {
timelineFetcher = nil
}
selectArticle(nil)
}
func searchArticles(_ searchString: String, _ searchScope: SearchScope) {
@ -755,14 +624,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func selectPrevArticle() {
if let indexPath = prevArticleIndexPath {
selectArticle(indexPath)
if let article = prevArticle {
selectArticle(article)
}
}
func selectNextArticle() {
if let indexPath = nextArticleIndexPath {
selectArticle(indexPath)
if let article = nextArticle {
selectArticle(article)
}
}
@ -838,12 +707,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func markAsReadOlderArticlesInTimeline() {
if let indexPath = currentArticleIndexPath {
markAsReadOlderArticlesInTimeline(indexPath)
if let article = currentArticle {
markAsReadOlderArticlesInTimeline(article)
}
}
func markAsReadOlderArticlesInTimeline(_ indexPath: IndexPath) {
let article = articles[indexPath.row]
func markAsReadOlderArticlesInTimeline(_ article: Article) {
let articlesToMark = articles.filter { $0.logicalDatePublished < article.logicalDatePublished }
if articlesToMark.isEmpty {
return
@ -869,8 +738,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
func toggleRead(for indexPath: IndexPath) {
let article = articles[indexPath.row]
func toggleRead(_ article: Article) {
guard let undoManager = undoManager,
let markReadCommand = MarkStatusCommand(initialArticles: [article], markingRead: !article.status.read, undoManager: undoManager) else {
return
@ -884,8 +752,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
func toggleStar(for indexPath: IndexPath) {
let article = articles[indexPath.row]
func toggleStar(_ article: Article) {
guard let undoManager = undoManager,
let markReadCommand = MarkStatusCommand(initialArticles: [article], markingStarred: !article.status.starred, undoManager: undoManager) else {
return
@ -944,8 +811,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
func showBrowserForArticle(_ indexPath: IndexPath) {
guard let preferredLink = articles[indexPath.row].preferredLink, let url = URL(string: preferredLink) else {
func showBrowserForArticle(_ article: Article) {
guard let preferredLink = article.preferredLink, let url = URL(string: preferredLink) else {
return
}
UIApplication.shared.open(url, options: [:])
@ -964,8 +831,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
func navigateToTimeline() {
if currentArticleIndexPath == nil {
selectArticle(IndexPath(row: 0, section: 0))
if currentArticle == nil && articles.count > 0 {
selectArticle(articles[0])
}
masterTimelineViewController?.focus()
}
@ -979,23 +846,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
// MARK: UISplitViewControllerDelegate
extension SceneCoordinator: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
guard splitViewController.traitCollection.userInterfaceIdiom == .pad && !splitViewController.isCollapsed else {
return
}
if splitViewController.displayMode != .allVisible && displayMode == .allVisible {
transitionToThreePanelMode()
}
if splitViewController.displayMode == .allVisible && displayMode != .allVisible {
transitionFromThreePanelMode()
}
}
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
// Check to see if the system is currently configured for three panel mode
if let subSplit = secondaryViewController.children.first as? UISplitViewController {
if let subSplit = secondaryViewController as? UISplitViewController {
// Take the timeline controller out of the subsplit and throw it on the master navigation stack
if let masterTimelineNav = subSplit.viewControllers.first as? UINavigationController, let masterTimeline = masterTimelineNav.topViewController {
@ -1015,7 +870,7 @@ extension SceneCoordinator: UISplitViewControllerDelegate {
}
// Take the detail view (ignoring system message controllers) and put it on the master navigation stack
if let detailNav = secondaryViewController.children.first as? UINavigationController, let detail = detailNav.topViewController as? DetailViewController {
if let detailNav = secondaryViewController as? UINavigationController, let detail = detailNav.topViewController as? DetailViewController {
// I have no idea why, I have to wire up the left bar button item for this, but not when I am transitioning from three panel mode
detail.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
detail.navigationItem.leftItemsSupplementBackButton = true
@ -1030,19 +885,16 @@ extension SceneCoordinator: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
// If we are in three panel mode, return back the new shim controller that contains a new sub split controller
if isThreePanelMode {
return transitionToThreePanelMode()
}
if let detail = masterNavigationController.viewControllers.last as? DetailViewController {
// If we have a detail controller on the stack, remove it, wrap it in a shim, and return it.
// If we have a detail controller on the stack, remove it and return it.
masterNavigationController.viewControllers.removeLast()
let detailNav = addNavControllerIfNecessary(detail, showButton: true)
let shimController = UIViewController()
shimController.addChildAndPinView(detailNav)
return shimController
return detailNav
} else {
@ -1057,11 +909,22 @@ extension SceneCoordinator: UISplitViewControllerDelegate {
// MARK: UINavigationControllerDelegate
extension SceneCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
if rootSplitViewController.isCollapsed && viewController === masterFeedViewController {
// If we are showing the Feeds and only the feeds start clearing stuff
if viewController === masterFeedViewController && !isThreePanelMode {
activityManager.invalidateCurrentActivities()
selectFeed(nil)
}
// If we are using a phone and navigate away from the detail, clear up the article resources (including activity)
if viewController === masterTimelineViewController && !isThreePanelMode && rootSplitViewController.isCollapsed {
selectArticle(nil)
}
}
}
// MARK: Private
@ -1095,10 +958,10 @@ private extension SceneCoordinator {
var result = [Node]()
let sectionNode = treeController.rootNode.childAtIndex(i)!
if expandedNodes.contains(sectionNode) {
if sectionNode.isExpanded {
for node in sectionNode.childNodes {
result.append(node)
if expandedNodes.contains(node) {
if node.isExpanded {
for child in node.childNodes {
result.append(child)
}
@ -1111,6 +974,29 @@ private extension SceneCoordinator {
}
}
func nodeFor(_ indexPath: IndexPath) -> Node? {
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
return nil
}
return shadowTable[indexPath.section][indexPath.row]
}
func indexPathFor(_ node: Node) -> IndexPath? {
for i in 0..<shadowTable.count {
if let row = shadowTable[i].firstIndex(of: node) {
return IndexPath(row: row, section: i)
}
}
return nil
}
func indexPathFor(_ object: AnyObject) -> IndexPath? {
guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else {
return nil
}
return indexPathFor(node)
}
func updateShowAvatars() {
if showFeedNames {
@ -1137,8 +1023,8 @@ private extension SceneCoordinator {
@discardableResult
func selectPrevUnreadArticleInTimeline() -> Bool {
let startingRow: Int = {
if let indexPath = currentArticleIndexPath {
return indexPath.row - 1
if let articleRow = currentArticleRow {
return articleRow
} else {
return articles.count - 1
}
@ -1156,7 +1042,7 @@ private extension SceneCoordinator {
for i in (0...startingRow).reversed() {
let article = articles[i]
if !article.status.read {
selectArticle(IndexPath(row: i, section: 0))
selectArticle(article)
return true
}
}
@ -1217,7 +1103,7 @@ private extension SceneCoordinator {
return true
}
if expandedNodes.contains(node) {
if node.isExpanded {
continue
}
@ -1244,8 +1130,8 @@ private extension SceneCoordinator {
@discardableResult
func selectNextUnreadArticleInTimeline() -> Bool {
let startingRow: Int = {
if let indexPath = currentArticleIndexPath {
return indexPath.row + 1
if let articleRow = currentArticleRow {
return articleRow + 1
} else {
return 0
}
@ -1263,7 +1149,7 @@ private extension SceneCoordinator {
for i in startingRow..<articles.count {
let article = articles[i]
if !article.status.read {
selectArticle(IndexPath(row: i, section: 0))
selectArticle(article)
return true
}
}
@ -1323,7 +1209,7 @@ private extension SceneCoordinator {
return true
}
if expandedNodes.contains(node) {
if node.isExpanded {
continue
}
@ -1361,34 +1247,11 @@ private extension SceneCoordinator {
if articles != sortedArticles {
let article = currentArticle
articles = sortedArticles
updateShowAvatars()
articleRowMap = [String: Int]()
updateUnreadCount()
masterTimelineViewController?.reloadArticles(animate: animate)
if let articleID = article?.articleID, let index = indexForArticleID(articleID) {
currentArticleIndexPath = IndexPath(row: index, section: 0)
}
}
}
func updateArticleRowMap() {
var rowMap = [String: Int]()
var index = 0
articles.forEach { (article) in
rowMap[article.articleID] = index
index += 1
}
articleRowMap = rowMap
}
func updateArticleRowMapIfNeeded() {
if articleRowMap.isEmpty {
updateArticleRowMap()
}
}
@ -1528,64 +1391,58 @@ private extension SceneCoordinator {
}
}
// Note about the Shim Controller
// In the root split view controller's secondary (or detail) position we use a view controller that
// only acts as a shim (or wrapper) for the actually desired contents of the second position. This
// is because we normally can't change the root split view controllers second position contents
// 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, automated: Bool) {
let showButton = rootSplitViewController.displayMode != .allVisible
let controller = addNavControllerIfNecessary(detailController, showButton: showButton)
if isThreePanelMode {
let targetSplit = ensureDoubleSplit().children.first as! UISplitViewController
targetSplit.showDetailViewController(controller, sender: self)
if let subSplit = subSplitViewController {
let controller = addNavControllerIfNecessary(detailController, showButton: false)
subSplit.showDetailViewController(controller, sender: self)
} else if rootSplitViewController.isCollapsed {
let controller = addNavControllerIfNecessary(detailController, showButton: false)
masterNavigationController.pushViewController(controller, animated: !automated)
} else {
if let shimController = rootSplitViewController.viewControllers.last {
shimController.replaceChildAndPinView(controller)
}
}
let controller = addNavControllerIfNecessary(detailController, showButton: true)
rootSplitViewController.showDetailViewController(controller, sender: self)
}
}
func addNavControllerIfNecessary(_ controller: UIViewController, showButton: Bool) -> UIViewController {
if rootSplitViewController.isCollapsed {
return controller
} else {
let navController = ThemedNavigationController.template(rootViewController: controller)
navController.isToolbarHidden = false
if showButton {
controller.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
} else {
controller.navigationItem.leftBarButtonItem = nil
controller.navigationItem.leftItemsSupplementBackButton = false
}
return navController
}
}
func ensureDoubleSplit() -> UIViewController {
if let shimController = rootSplitViewController.viewControllers.last, shimController.children.first is UISplitViewController {
return shimController
}
}
func configureDoubleSplit() {
rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.30
let subSplit = UISplitViewController.template()
subSplit.preferredDisplayMode = .allVisible
subSplit.preferredPrimaryColumnWidthFraction = 0.4285
let shimController = UIViewController()
shimController.addChildAndPinView(subSplit)
rootSplitViewController.showDetailViewController(shimController, sender: self)
return shimController
rootSplitViewController.showDetailViewController(subSplit, sender: self)
}
func navControllerForTimeline() -> UINavigationController {
if isThreePanelMode {
let subSplit = ensureDoubleSplit().children.first as! UISplitViewController
if let subSplit = subSplitViewController {
return subSplit.viewControllers.first as! UINavigationController
} else {
return masterNavigationController
@ -1595,64 +1452,64 @@ private extension SceneCoordinator {
func fullyWrappedSystemMesssageController(showButton: Bool) -> UIViewController {
let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
let navController = addNavControllerIfNecessary(systemMessageViewController, showButton: showButton)
let shimController = UIViewController()
shimController.addChildAndPinView(navController)
return shimController
return navController
}
@discardableResult
func transitionToThreePanelMode() -> UIViewController {
defer {
masterNavigationController.viewControllers = [masterFeedViewController]
}
let controller: UIViewController = {
if let result = detailViewController {
return result
} else {
return UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
}
}()
configureDoubleSplit()
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)
masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true)
masterTimelineViewController!.restoreSelectionIfNecessary()
// We made sure this was there above when we called configureDoubleSplit
return subSplitViewController!
if currentFeedIndexPath == nil && currentArticleIndexPath == nil {
let wrappedSystemMessageController = fullyWrappedSystemMesssageController(showButton: false)
rootSplitViewController.showDetailViewController(wrappedSystemMessageController, sender: self)
return wrappedSystemMessageController
} else {
let controller: UIViewController = {
if let result = detailViewController {
return result
} else {
return UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self)
}
}()
// Create the new sub split controller (wrapped in the shim of course) and add the timeline in the primary position
let shimController = ensureDoubleSplit()
let subSplit = shimController.children.first as! UISplitViewController
let masterTimelineNavController = subSplit.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)
subSplit.showDetailViewController(navController, sender: self)
return shimController
}
}
func transitionFromThreePanelMode() {
rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension
if let shimController = rootSplitViewController.viewControllers.last, let subSplit = shimController.children.first as? UISplitViewController {
if let subSplit = rootSplitViewController.viewControllers.last as? UISplitViewController {
// Push the timeline on to the master navigation controller. This should always be true if we have installed
// the sub split controller because we only install the sub split controller if a timeline needs to be displayed.
if let masterTimelineNav = subSplit.viewControllers.first as? UINavigationController, let masterTimeline = masterTimelineNav.topViewController {
masterNavigationController.pushViewController(masterTimeline, animated: false)
// 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, by replacing the contents of the shim controller in the second position.
// secondary (detail) position.
if let detailNav = subSplit.viewControllers.last as? UINavigationController, let topController = detailNav.topViewController {
let newNav = addNavControllerIfNecessary(topController, showButton: true)
shimController.replaceChildAndPinView(newNav)
rootSplitViewController.showDetailViewController(newNav, sender: self)
}
}
@ -1722,12 +1579,8 @@ private extension SceneCoordinator {
discloseFeed(feedNode.representedObject as! Feed) {
guard let articleID = activity.userInfo?[ActivityID.articleID.rawValue] as? String else { return }
for (index, article) in self.articles.enumerated() {
if article.articleID == articleID {
self.selectArticle(IndexPath(row: index, section: 0))
break
}
if let article = self.articles.first(where: { $0.articleID == articleID }) {
self.selectArticle(article)
}
}

View File

@ -20,7 +20,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
window = UIWindow(windowScene: scene as! UIWindowScene)
window!.tintColor = AppAssets.netNewsWireBlueColor
window!.rootViewController = coordinator.start()
window!.rootViewController = coordinator.start(for: window!.frame.size)
if let shortcutItem = connectionOptions.shortcutItem {
window!.makeKeyAndVisible()

View File

@ -11,34 +11,16 @@ import Account
struct SettingsAddAccountView : View {
@Environment(\.presentationMode) var presentation
@State private var selectedAccountType: AccountType = nil
var body: some View {
Form {
Button(action: {
self.selectedAccountType = AccountType.onMyMac
}) {
NavigationLink(destination: SettingsLocalAccountView(name: "")) {
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName)
}
Button(action: {
self.selectedAccountType = AccountType.feedbin
}) {
NavigationLink(destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())) {
SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin")
}
}
.sheet(item: $selectedAccountType) { accountType in
if accountType == .onMyMac {
SettingsLocalAccountView(name: "", onDismiss: { self.presentation.wrappedValue.dismiss() })
}
if accountType == .feedbin {
SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel(), onDismiss: { self.presentation.wrappedValue.dismiss() })
}
}
.navigationBarTitle(Text("Add Account"), displayMode: .inline)
}
}

View File

@ -71,7 +71,7 @@ struct SettingsDetailAccountView : View {
var settingsFeedbinAccountView: SettingsFeedbinAccountView {
let feedbinViewModel = SettingsFeedbinAccountView.ViewModel(account: viewModel.account)
return SettingsFeedbinAccountView(viewModel: feedbinViewModel, onDismiss: {})
return SettingsFeedbinAccountView(viewModel: feedbinViewModel)
}
class ViewModel: ObservableObject {

View File

@ -17,49 +17,43 @@ struct SettingsFeedbinAccountView : View {
@State var busy: Bool = false
@State var error: String = ""
// This is a hack around the fact that onDismiss isn't being called by the sheet modifier.
var onDismiss: () -> Void
var body: some View {
NavigationView {
Form {
Section(header:
HStack {
Spacer()
SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin").padding()
Spacer()
}
) {
TextField("Email", text: $viewModel.email).textContentType(.emailAddress)
SecureField("Password", text: $viewModel.password)
Form {
Section(header:
HStack {
Spacer()
SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin")
.padding()
.layoutPriority(1.0)
Spacer()
}
Section(footer:
HStack {
Spacer()
Text(verbatim: error).foregroundColor(.red)
Spacer()
}
) {
HStack {
Spacer()
Button(action: { self.addAccount() }) {
if viewModel.isUpdate {
Text("Update Account")
} else {
Text("Add Account")
}
) {
TextField("Email", text: $viewModel.email).textContentType(.emailAddress)
SecureField("Password", text: $viewModel.password)
}
Section(footer:
HStack {
Spacer()
Text(verbatim: error).foregroundColor(.red)
Spacer()
}
) {
HStack {
Spacer()
Button(action: { self.addAccount() }) {
if viewModel.isUpdate {
Text("Update Account")
} else {
Text("Add Account")
}
.disabled(!viewModel.isValid)
Spacer()
}
.disabled(!viewModel.isValid)
Spacer()
}
}
.disabled(busy)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading:
Button(action: { self.dismiss() }) { Text("Cancel") }
)
}
// .disabled(busy) // Maybe someday we can do this, but right now it crashes on the iPad
.navigationBarTitle(Text(""), displayMode: .inline)
}
private func addAccount() {
@ -71,14 +65,14 @@ struct SettingsFeedbinAccountView : View {
let credentials = Credentials.basic(username: emailAddress, password: viewModel.password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.busy = false
switch result {
case .success(let authenticated):
if (authenticated != nil) {
var newAccount = false
let workAccount: Account
if self.viewModel.account == nil {
@ -87,39 +81,38 @@ struct SettingsFeedbinAccountView : View {
} else {
workAccount = self.viewModel.account!
}
do {
do {
try workAccount.removeCredentials()
} catch {}
try workAccount.storeCredentials(credentials)
if newAccount {
workAccount.refreshAll() { result in }
}
self.dismiss()
} catch {
self.error = "Keychain error while storing credentials."
}
} else {
self.error = "Invalid email/password combination."
}
case .failure:
self.error = "Network error. Try again later."
}
}
}
private func dismiss() {
presentation.wrappedValue.dismiss()
onDismiss()
}
class ViewModel: ObservableObject {
@ -164,7 +157,7 @@ struct SettingsFeedbinAccountView : View {
#if DEBUG
struct SettingsFeedbinAccountView_Previews : PreviewProvider {
static var previews: some View {
SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel(), onDismiss: {})
SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())
}
}
#endif

View File

@ -13,36 +13,32 @@ struct SettingsLocalAccountView : View {
@Environment(\.presentationMode) var presentation
@State var name: String
// This is a hack around the fact that onDismiss isn't being called by the sheet modifier.
var onDismiss: () -> Void
var body: some View {
NavigationView {
Form {
Section(header:
HStack {
Spacer()
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName).padding()
Spacer()
}
) {
HStack {
TextField("Name", text: $name)
}
Form {
Section(header:
HStack {
Spacer()
SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: Account.defaultLocalAccountName)
.padding()
.layoutPriority(1.0)
Spacer()
}
Section {
HStack {
Spacer()
Button(action: { self.addAccount() }) {
Text("Add Account")
}
Spacer()
}
) {
HStack {
TextField("Name", text: $name)
}
}
Section {
HStack {
Spacer()
Button(action: { self.addAccount() }) {
Text("Add Account")
}
Spacer()
}
}
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(leading: Button(action: { self.dismiss() }) { Text("Cancel") } )
}
.navigationBarTitle(Text(""), displayMode: .inline)
}
private func addAccount() {
@ -53,7 +49,6 @@ struct SettingsLocalAccountView : View {
private func dismiss() {
presentation.wrappedValue.dismiss()
onDismiss()
}
}
@ -61,7 +56,7 @@ struct SettingsLocalAccountView : View {
#if DEBUG
struct SettingsLocalAccountView_Previews : PreviewProvider {
static var previews: some View {
SettingsLocalAccountView(name: "", onDismiss: {})
SettingsLocalAccountView(name: "")
}
}
#endif

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModule="NetNewsWire_iOS_Share_Extension" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="139" y="139"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<string>1</string>
</dict>
<key>NSExtensionJavaScriptPreprocessingFile</key>
<string>SafariExt</string>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@ -0,0 +1,12 @@
var SafariExtPreprocessorClass = function() {};
SafariExtPreprocessorClass.prototype = {
run: function(arguments) {
arguments.completionFunction({ "url": document.URL });
}
};
// The JavaScript file must contain a global object named "ExtensionPreprocessingJS".
var ExtensionPreprocessingJS = new SafariExtPreprocessorClass;

View File

@ -0,0 +1,50 @@
//
// ShareFolderPickerController.swift
// NetNewsWire iOS Share Extension
//
// Created by Maurice Parker on 9/12/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
protocol ShareFolderPickerControllerDelegate: class {
func shareFolderPickerDidSelect(_ container: Container)
}
class ShareFolderPickerController: UITableViewController {
var pickerData: FlattenedAccountFolderPickerData?
var selectedContainer: Container?
weak var delegate: ShareFolderPickerControllerDelegate?
override func viewDidLoad() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pickerData?.containerNames.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = pickerData?.containerNames[indexPath.row] ?? ""
if pickerData?.containers[indexPath.row] === selectedContainer {
cell.accessoryType = .checkmark
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let pickerData = pickerData else { return }
delegate?.shareFolderPickerDidSelect(pickerData.containers[indexPath.row])
navigationController?.popViewController(animated: true)
}
}

View File

@ -0,0 +1,172 @@
//
// ShareViewController.swift
// NetNewsWire iOS Share Extension
//
// Created by Maurice Parker on 9/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import MobileCoreServices
import Social
import Account
import Articles
import RSCore
import RSTree
class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate {
private var pickerData: FlattenedAccountFolderPickerData?
private var url: URL?
private var container: Container?
override func viewDidLoad() {
AccountManager.shared = AccountManager(accountsFolder: RSDataSubfolder(nil, "Accounts")!)
pickerData = FlattenedAccountFolderPickerData()
if pickerData?.containers.count ?? 0 > 0 {
container = pickerData?.containers[0]
}
title = "NetNewsWire"
placeholder = "Feed Name (Optional)"
if let button = navigationController?.navigationBar.topItem?.rightBarButtonItem {
button.title = "Add Feed"
button.isEnabled = true
}
// Hack the bottom table rows to be smaller since the controller itself doesn't have enough sense to size itself correctly
if let nav = self.children.first as? UINavigationController, let tableView = nav.children.first?.view.subviews.first as? UITableView {
tableView.rowHeight = 38
}
var provider: NSItemProvider? = nil
// Try to get any HTML that is maybe passed in
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
for itemProvider in item.attachments! {
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypePropertyList as String) {
provider = itemProvider
}
}
}
if provider != nil {
provider!.loadItem(forTypeIdentifier: kUTTypePropertyList as String, options: nil, completionHandler: { [weak self] (pList, error) in
if error != nil {
return
}
guard let dataGraph = pList as? NSDictionary else {
return
}
guard let results = dataGraph["NSExtensionJavaScriptPreprocessingResultsKey"] as? NSDictionary else {
return
}
if let url = URL(string: results["url"] as! String) {
self?.url = url
}
})
return
}
// Try to get the URL if it is passed in
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
for itemProvider in item.attachments! {
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
provider = itemProvider
}
}
}
if provider != nil {
provider!.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { [weak self] (urlCoded, error) in
if error != nil {
return
}
guard let url = urlCoded as? URL else {
return
}
self?.url = url
})
}
}
override func isContentValid() -> Bool {
return url != nil && container != nil
}
override func didSelectPost() {
var account: Account?
if let containerAccount = container as? Account {
account = containerAccount
} else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
account = containerAccount
}
if let urlString = url?.absoluteString, account!.hasFeed(withURL: urlString) {
presentError(AccountError.createErrorAlreadySubscribed)
return
}
let feedName = contentText.isEmpty ? nil : contentText
account!.createFeed(url: url!.absoluteString, name: feedName, container: container!) { result in
switch result {
case .success:
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
case .failure(let error):
self.presentError(error) {
self.extensionContext!.cancelRequest(withError: error)
}
}
}
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
}
func shareFolderPickerDidSelect(_ container: Container) {
self.container = container
}
override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
guard let urlItem = SLComposeSheetConfigurationItem() else { return nil }
urlItem.title = "URL"
urlItem.value = url?.absoluteString ?? ""
guard let folderItem = SLComposeSheetConfigurationItem() else { return nil }
folderItem.title = "Folder"
if let nameProvider = container as? DisplayNameProvider {
folderItem.value = nameProvider.nameForDisplay
}
folderItem.tapHandler = {
let folderPickerController = ShareFolderPickerController()
folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder")
folderPickerController.delegate = self
folderPickerController.pickerData = self.pickerData
folderPickerController.selectedContainer = self.container
self.pushConfigurationViewController(folderPickerController)
}
return [folderItem, urlItem]
}
}

@ -1 +1 @@
Subproject commit 7af10d021f35df5596fa898ba55f5173fcb0e26b
Subproject commit d640a2310b96a0a3d4d34c49c08c7bce195d0762

@ -1 +1 @@
Subproject commit db208e17bdf4f5e7e643c580acbc339191693537
Subproject commit d333739a776236aae32b3868415729499021cec3

View File

@ -0,0 +1,7 @@
#include "./NetNewsWire_iOSapp_target.xcconfig"
CODE_SIGN_ENTITLEMENTS = iOS/ShareExtension/NetNewsWire_iOS_ShareExtension.entitlements
INFOPLIST_FILE = iOS/ShareExtension/Info.plist
PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.NetNewsWire-Evergreen.iOS.Share-Extension
PRODUCT_NAME = $(TARGET_NAME)