Merge branch 'master' of https://github.com/brentsimmons/NetNewsWire into GroupArticlesByFeed
# Conflicts: # Mac/MainWindow/Timeline/TimelineViewController.swift
This commit is contained in:
commit
8a6e3c9f37
@ -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[
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -53,7 +53,7 @@ public extension Article {
|
||||
}
|
||||
|
||||
var feed: Feed? {
|
||||
return account?.existingFeed(with: feedID)
|
||||
return account?.existingFeed(withFeedID: feedID)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 don’t 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 don’t store it
|
||||
// as part of feed metadata.
|
||||
public var faviconURL: String? {
|
||||
get {
|
||||
return metadata.faviconURL
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"/>
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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> {
|
||||
|
@ -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 = (
|
||||
|
@ -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? {
|
||||
|
@ -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>
|
||||
|
@ -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]()
|
@ -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("Can’t add this feed because you’ve 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("Can’t 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("Can’t 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 {
|
||||
|
@ -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")!
|
||||
}()
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)!
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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?) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
26
iOS/ShareExtension/Base.lproj/MainInterface.storyboard
Normal file
26
iOS/ShareExtension/Base.lproj/MainInterface.storyboard
Normal 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>
|
41
iOS/ShareExtension/Info.plist
Normal file
41
iOS/ShareExtension/Info.plist
Normal 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>
|
@ -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>
|
12
iOS/ShareExtension/SafariExt.js
Normal file
12
iOS/ShareExtension/SafariExt.js
Normal 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;
|
50
iOS/ShareExtension/ShareFolderPickerController.swift
Normal file
50
iOS/ShareExtension/ShareFolderPickerController.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
172
iOS/ShareExtension/ShareViewController.swift
Normal file
172
iOS/ShareExtension/ShareViewController.swift
Normal 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
|
7
xcconfig/NetNewsWire_iOSshareextension_target.xcconfig
Normal file
7
xcconfig/NetNewsWire_iOSshareextension_target.xcconfig
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user