Implement detail next and prev article buttons.
This commit is contained in:
parent
73500e0244
commit
e81defb934
|
@ -14,6 +14,8 @@ import RSWeb
|
|||
|
||||
enum DetailState: Equatable {
|
||||
case noSelection
|
||||
@IBOutlet weak var nextUnreadButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var nextUnreadButtonItem: UIBarButtonItem!
|
||||
case multipleSelection
|
||||
case article(Article)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; };
|
||||
5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; };
|
||||
5126EE97226CB48A00C22AFC /* NavigationModelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* NavigationModelController.swift */; };
|
||||
5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* NavigationStateController.swift */; };
|
||||
5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; };
|
||||
5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; };
|
||||
512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; };
|
||||
|
@ -603,7 +603,7 @@
|
|||
51121AA12265430A00BC0EC1 /* NetNewsWire_iOS_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOS_target.xcconfig; sourceTree = "<group>"; };
|
||||
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = "<group>"; };
|
||||
51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = "<group>"; };
|
||||
5126EE96226CB48A00C22AFC /* NavigationModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModelController.swift; sourceTree = "<group>"; };
|
||||
5126EE96226CB48A00C22AFC /* NavigationStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStateController.swift; sourceTree = "<group>"; };
|
||||
5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailKeyboardDelegate.swift; sourceTree = "<group>"; };
|
||||
5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = "<group>"; };
|
||||
512E08F722688F7C00BDCFDD /* MasterTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTableViewSectionHeader.swift; sourceTree = "<group>"; };
|
||||
|
@ -1514,7 +1514,7 @@
|
|||
840D617E2029031C009BC708 /* AppDelegate.swift */,
|
||||
51C45254226507D200C03939 /* AppAssets.swift */,
|
||||
51C45255226507D200C03939 /* AppDefaults.swift */,
|
||||
5126EE96226CB48A00C22AFC /* NavigationModelController.swift */,
|
||||
5126EE96226CB48A00C22AFC /* NavigationStateController.swift */,
|
||||
51C4525D226508F600C03939 /* Master */,
|
||||
51C4526D2265091600C03939 /* Timeline */,
|
||||
51C4527D2265092C00C03939 /* Detail */,
|
||||
|
@ -2149,7 +2149,7 @@
|
|||
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
|
||||
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */,
|
||||
51C4526B226508F600C03939 /* MasterViewController.swift in Sources */,
|
||||
5126EE97226CB48A00C22AFC /* NavigationModelController.swift in Sources */,
|
||||
5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */,
|
||||
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
||||
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
|
||||
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
|
||||
|
|
|
@ -128,7 +128,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
|
|||
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
|
||||
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
|
||||
guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
|
||||
if topAsDetailController.article == nil {
|
||||
if topAsDetailController.navState?.currentArticle == nil {
|
||||
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -57,11 +57,23 @@
|
|||
<viewLayoutGuide key="safeArea" id="VUw-jc-0yf"/>
|
||||
</view>
|
||||
<toolbarItems>
|
||||
<barButtonItem enabled="NO" image="nextUnreadImage" id="2w5-e9-C2V"/>
|
||||
<barButtonItem enabled="NO" image="nextUnreadImage" id="2w5-e9-C2V">
|
||||
<connections>
|
||||
<action selector="nextUnread:" destination="JEX-9P-axG" id="USD-hC-C6z"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="vAq-iW-Yyo"/>
|
||||
<barButtonItem image="prevArticleImage" style="plain" id="v4j-fq-23N"/>
|
||||
<barButtonItem image="prevArticleImage" style="plain" id="v4j-fq-23N">
|
||||
<connections>
|
||||
<action selector="prevArticle:" destination="JEX-9P-axG" id="cMZ-tk-I4W"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="G92-nw-kXs"/>
|
||||
<barButtonItem image="nextArticleImage" id="2qz-M5-Yhk"/>
|
||||
<barButtonItem image="nextArticleImage" id="2qz-M5-Yhk">
|
||||
<connections>
|
||||
<action selector="nextArticle:" destination="JEX-9P-axG" id="P77-KM-j8D"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="Myj-ux-Zc5"/>
|
||||
<barButtonItem image="circleOpenImage" id="hy0-LS-MzE">
|
||||
<connections>
|
||||
|
@ -92,6 +104,9 @@
|
|||
<connections>
|
||||
<outlet property="actionBarButtonItem" destination="9Ut-5B-JKP" id="9bO-kz-cTz"/>
|
||||
<outlet property="browserBarButtonItem" destination="DMh-3X-ebd" id="PkT-Tn-8kG"/>
|
||||
<outlet property="nextArticleBarButtonItem" destination="2qz-M5-Yhk" id="IQd-jx-qEr"/>
|
||||
<outlet property="nextUnreadBarButtonItem" destination="2w5-e9-C2V" id="xJr-5y-p1N"/>
|
||||
<outlet property="prevArticleBarButtonItem" destination="v4j-fq-23N" id="Gny-Oh-cQa"/>
|
||||
<outlet property="readBarButtonItem" destination="hy0-LS-MzE" id="BzM-x9-tuj"/>
|
||||
<outlet property="starBarButtonItem" destination="wU4-eH-wC9" id="Z8Q-Lt-dKk"/>
|
||||
<outlet property="webView" destination="t8d-md-Yhc" id="Iqg-bg-wds"/>
|
||||
|
|
|
@ -14,18 +14,16 @@ import SafariServices
|
|||
|
||||
class DetailViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var nextUnreadBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var prevArticleBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var nextArticleBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var readBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var starBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var actionBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var browserBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var webView: WKWebView!
|
||||
|
||||
var article: Article? {
|
||||
didSet {
|
||||
reloadUI()
|
||||
reloadHTML()
|
||||
}
|
||||
}
|
||||
weak var navState: NavigationStateController?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
@ -34,11 +32,15 @@ class DetailViewController: UIViewController {
|
|||
reloadUI()
|
||||
reloadHTML()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleSelectionChange(_:)), name: .ArticleSelectionChange, object: navState)
|
||||
}
|
||||
|
||||
func reloadUI() {
|
||||
|
||||
guard let article = article else {
|
||||
guard let article = navState?.currentArticle else {
|
||||
nextUnreadBarButtonItem.isEnabled = false
|
||||
prevArticleBarButtonItem.isEnabled = false
|
||||
nextArticleBarButtonItem.isEnabled = false
|
||||
readBarButtonItem.isEnabled = false
|
||||
starBarButtonItem.isEnabled = false
|
||||
browserBarButtonItem.isEnabled = false
|
||||
|
@ -46,6 +48,10 @@ class DetailViewController: UIViewController {
|
|||
return
|
||||
}
|
||||
|
||||
nextArticleBarButtonItem.isEnabled = false
|
||||
prevArticleBarButtonItem.isEnabled = navState?.isPrevArticleAvailable ?? false
|
||||
nextArticleBarButtonItem.isEnabled = navState?.isNextArticleAvailable ?? false
|
||||
|
||||
readBarButtonItem.isEnabled = true
|
||||
starBarButtonItem.isEnabled = true
|
||||
browserBarButtonItem.isEnabled = true
|
||||
|
@ -60,47 +66,67 @@ class DetailViewController: UIViewController {
|
|||
}
|
||||
|
||||
func reloadHTML() {
|
||||
guard let article = article, let webView = webView else {
|
||||
|
||||
guard let article = navState?.currentArticle, let webView = webView else {
|
||||
return
|
||||
}
|
||||
let style = ArticleStylesManager.shared.currentStyle
|
||||
let html = ArticleRenderer.articleHTML(article: article, style: style)
|
||||
webView.loadHTMLString(html, baseURL: nil)
|
||||
|
||||
}
|
||||
|
||||
@objc func statusesDidChange(_ note: Notification) {
|
||||
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
|
||||
return
|
||||
}
|
||||
if articles.count == 1 && articles.first?.articleID == article?.articleID {
|
||||
if articles.count == 1 && articles.first?.articleID == navState?.currentArticle?.articleID {
|
||||
reloadUI()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func articleSelectionChange(_ note: Notification) {
|
||||
reloadUI()
|
||||
reloadHTML()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func nextUnread(_ sender: Any) {
|
||||
}
|
||||
|
||||
@IBAction func prevArticle(_ sender: Any) {
|
||||
navState?.currentArticleIndexPath = navState?.prevArticleIndexPath
|
||||
}
|
||||
|
||||
@IBAction func nextArticle(_ sender: Any) {
|
||||
navState?.currentArticleIndexPath = navState?.nextArticleIndexPath
|
||||
}
|
||||
|
||||
@IBAction func toggleRead(_ sender: Any) {
|
||||
if let article = article {
|
||||
if let article = navState?.currentArticle {
|
||||
markArticles(Set([article]), statusKey: .read, flag: !article.status.read)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleStar(_ sender: Any) {
|
||||
if let article = article {
|
||||
if let article = navState?.currentArticle {
|
||||
markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func openBrowser(_ sender: Any) {
|
||||
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
guard let preferredLink = navState?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
return
|
||||
}
|
||||
UIApplication.shared.open(url, options: [:])
|
||||
}
|
||||
|
||||
@IBAction func showActivityDialog(_ sender: Any) {
|
||||
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
guard let preferredLink = navState?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
return
|
||||
}
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: article?.title)
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: navState?.currentArticle?.title)
|
||||
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
|
||||
present(activityViewController, animated: true)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
let nmc = NavigationModelController()
|
||||
let navState = NavigationStateController()
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
@ -77,8 +77,8 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
}
|
||||
|
||||
if let account = representedObject as? Account {
|
||||
if let node = nmc.rootNode.childNodeRepresentingObject(account) {
|
||||
let sectionIndex = nmc.rootNode.indexOfChild(node)!
|
||||
if let node = navState.rootNode.childNodeRepresentingObject(account) {
|
||||
let sectionIndex = navState.rootNode.indexOfChild(node)!
|
||||
let headerView = tableView.headerView(forSection: sectionIndex) as! MasterTableViewSectionHeader
|
||||
headerView.unreadCount = account.unreadCount
|
||||
}
|
||||
|
@ -108,27 +108,27 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
@objc func userDidAddFeed(_ notification: Notification) {
|
||||
|
||||
guard let feed = notification.userInfo?[UserInfoKey.feed],
|
||||
let node = nmc.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else {
|
||||
let node = navState.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let indexPath = nmc.indexPathFor(node) {
|
||||
if let indexPath = navState.indexPathFor(node) {
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
// It wasn't already visable, so expand its folder and try again
|
||||
guard let parent = node.parent, let indexPath = nmc.indexPathFor(parent) else {
|
||||
guard let parent = node.parent, let indexPath = navState.indexPathFor(parent) else {
|
||||
return
|
||||
}
|
||||
|
||||
nmc.expand(indexPath) { [weak self] indexPaths in
|
||||
navState.expand(indexPath) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
}
|
||||
|
||||
if let indexPath = nmc.indexPathFor(node) {
|
||||
if let indexPath = navState.indexPathFor(node) {
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
|
||||
}
|
||||
|
||||
|
@ -137,11 +137,11 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
// MARK: Table View
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return nmc.shadowTable.count
|
||||
return navState.shadowTable.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return nmc.shadowTable[section].count
|
||||
return navState.shadowTable[section].count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
|
@ -150,14 +150,14 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
|
||||
guard let nameProvider = nmc.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
guard let nameProvider = navState.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! MasterTableViewSectionHeader
|
||||
headerView.name = nameProvider.nameForDisplay
|
||||
|
||||
guard let sectionNode = nmc.rootNode.childAtIndex(section) else {
|
||||
guard let sectionNode = navState.rootNode.childAtIndex(section) else {
|
||||
return headerView
|
||||
}
|
||||
|
||||
|
@ -168,7 +168,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
}
|
||||
|
||||
headerView.tag = section
|
||||
headerView.disclosureExpanded = nmc.expandedNodes.contains(sectionNode)
|
||||
headerView.disclosureExpanded = navState.expandedNodes.contains(sectionNode)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:)))
|
||||
headerView.addGestureRecognizer(tap)
|
||||
|
@ -189,7 +189,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTableViewCell
|
||||
|
||||
guard let node = nmc.nodeFor(indexPath) else {
|
||||
guard let node = navState.nodeFor(indexPath) else {
|
||||
return cell
|
||||
}
|
||||
|
||||
|
@ -199,7 +199,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
guard let node = nmc.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
|
||||
guard let node = navState.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -231,7 +231,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
guard let node = nmc.nodeFor(indexPath) else {
|
||||
guard let node = navState.nodeFor(indexPath) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
@ -239,21 +239,21 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
|
||||
|
||||
if let fetcher = node.representedObject as? ArticleFetcher {
|
||||
nmc.timelineFetcher = fetcher
|
||||
navState.timelineFetcher = fetcher
|
||||
}
|
||||
|
||||
if let nameProvider = node.representedObject as? DisplayNameProvider {
|
||||
timeline.title = nameProvider.nameForDisplay
|
||||
}
|
||||
|
||||
timeline.nmc = nmc
|
||||
timeline.navState = navState
|
||||
|
||||
self.navigationController?.pushViewController(timeline, animated: true)
|
||||
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
|
||||
guard let node = nmc.nodeFor(indexPath) else {
|
||||
guard let node = navState.nodeFor(indexPath) else {
|
||||
return false
|
||||
}
|
||||
return node.representedObject is Feed
|
||||
|
@ -269,13 +269,13 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
return proposedDestinationIndexPath
|
||||
}()
|
||||
|
||||
guard let draggedNode = nmc.nodeFor(sourceIndexPath), let destNode = nmc.nodeFor(destIndexPath), let parentNode = destNode.parent else {
|
||||
guard let draggedNode = navState.nodeFor(sourceIndexPath), let destNode = navState.nodeFor(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 || !nmc.expandedNodes.contains(destNode)) {
|
||||
if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !navState.expandedNodes.contains(destNode)) {
|
||||
let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0
|
||||
return IndexPath(row: destIndexPath.row + movementAdjustment, section: destIndexPath.section)
|
||||
}
|
||||
|
@ -296,7 +296,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
if parentNode.representedObject is Account {
|
||||
return IndexPath(row: 0, section: destIndexPath.section)
|
||||
} else {
|
||||
return nmc.indexPathFor(parentNode)!
|
||||
return navState.indexPathFor(parentNode)!
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -306,10 +306,10 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
let adjustedIndex = index - movementAdjustment
|
||||
if adjustedIndex >= sortedNodes.count {
|
||||
let lastSortedIndexPath = nmc.indexPathFor(sortedNodes[sortedNodes.count - 1])!
|
||||
let lastSortedIndexPath = navState.indexPathFor(sortedNodes[sortedNodes.count - 1])!
|
||||
return IndexPath(row: lastSortedIndexPath.row + 1, section: lastSortedIndexPath.section)
|
||||
} else {
|
||||
return nmc.indexPathFor(sortedNodes[adjustedIndex])!
|
||||
return navState.indexPathFor(sortedNodes[adjustedIndex])!
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -318,18 +318,18 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
||||
|
||||
guard let sourceNode = nmc.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else {
|
||||
guard let sourceNode = navState.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else {
|
||||
return
|
||||
}
|
||||
|
||||
// Based on the drop we have to determine a node to start looking for a parent container.
|
||||
let destNode: Node = {
|
||||
if destinationIndexPath.row == 0 {
|
||||
return nmc.rootNode.childAtIndex(destinationIndexPath.section)!
|
||||
return navState.rootNode.childAtIndex(destinationIndexPath.section)!
|
||||
} else {
|
||||
let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0
|
||||
let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section)
|
||||
return nmc.nodeFor(adjustedDestIndexPath)!
|
||||
return navState.nodeFor(adjustedDestIndexPath)!
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -452,22 +452,22 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
@objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) {
|
||||
|
||||
guard let sectionIndex = sender.view?.tag,
|
||||
let sectionNode = nmc.rootNode.childAtIndex(sectionIndex),
|
||||
let sectionNode = navState.rootNode.childAtIndex(sectionIndex),
|
||||
let headerView = sender.view as? MasterTableViewSectionHeader
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if nmc.expandedNodes.contains(sectionNode) {
|
||||
if navState.expandedNodes.contains(sectionNode) {
|
||||
headerView.disclosureExpanded = false
|
||||
nmc.collapse(section: sectionIndex) { [weak self] indexPaths in
|
||||
navState.collapse(section: sectionIndex) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.deleteRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
}
|
||||
} else {
|
||||
headerView.disclosureExpanded = true
|
||||
nmc.expand(section: sectionIndex) { [weak self] indexPaths in
|
||||
navState.expand(section: sectionIndex) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
|
@ -486,7 +486,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
} else {
|
||||
cell.indentationLevel = 0
|
||||
}
|
||||
cell.disclosureExpanded = nmc.expandedNodes.contains(node)
|
||||
cell.disclosureExpanded = navState.expandedNodes.contains(node)
|
||||
cell.allowDisclosureSelection = node.canHaveChildNodes
|
||||
|
||||
cell.name = nameFor(node)
|
||||
|
@ -528,25 +528,25 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
func delete(indexPath: IndexPath) {
|
||||
|
||||
guard let undoManager = undoManager,
|
||||
let deleteNode = nmc.nodeFor(indexPath),
|
||||
let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: nmc.treeController, undoManager: undoManager)
|
||||
let deleteNode = navState.nodeFor(indexPath),
|
||||
let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: navState.treeController, undoManager: undoManager)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
nmc.animatingChanges = true
|
||||
navState.animatingChanges = true
|
||||
|
||||
runCommand(deleteCommand)
|
||||
nmc.rebuildShadowTable()
|
||||
navState.rebuildShadowTable()
|
||||
tableView.deleteRows(at: [indexPath], with: .automatic)
|
||||
|
||||
nmc.animatingChanges = false
|
||||
navState.animatingChanges = false
|
||||
|
||||
}
|
||||
|
||||
func rename(indexPath: IndexPath) {
|
||||
|
||||
let name = (nmc.nodeFor(indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? ""
|
||||
let name = (navState.nodeFor(indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? ""
|
||||
let formatString = NSLocalizedString("Rename “%@”", comment: "Feed finder")
|
||||
let title = NSString.localizedStringWithFormat(formatString as NSString, name) as String
|
||||
|
||||
|
@ -558,7 +558,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
|
||||
let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in
|
||||
|
||||
guard let node = self?.nmc.nodeFor(indexPath),
|
||||
guard let node = self?.navState.nodeFor(indexPath),
|
||||
let name = alertController.textFields?[0].text,
|
||||
!name.isEmpty else {
|
||||
return
|
||||
|
@ -645,7 +645,7 @@ private extension MasterViewController {
|
|||
|
||||
func applyToAvailableCells(_ callback: (MasterTableViewCell, Node) -> Void) {
|
||||
tableView.visibleCells.forEach { cell in
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = nmc.nodeFor(indexPath) else {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = navState.nodeFor(indexPath) else {
|
||||
return
|
||||
}
|
||||
callback(cell as! MasterTableViewCell, node)
|
||||
|
@ -669,7 +669,7 @@ private extension MasterViewController {
|
|||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
return
|
||||
}
|
||||
nmc.expand(indexPath) { [weak self] indexPaths in
|
||||
navState.expand(indexPath) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
|
@ -680,7 +680,7 @@ private extension MasterViewController {
|
|||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
return
|
||||
}
|
||||
nmc.collapse(indexPath) { [weak self] indexPaths in
|
||||
navState.collapse(indexPath) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.deleteRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
|
|
|
@ -18,25 +18,20 @@ public extension Notification.Name {
|
|||
static let ArticlesReinitialized = Notification.Name(rawValue: "ArticlesReinitialized")
|
||||
static let ArticleDataDidChange = Notification.Name(rawValue: "ArticleDataDidChange")
|
||||
static let ArticlesDidChange = Notification.Name(rawValue: "ArticlesDidChange")
|
||||
static let ArticleSelectionChange = Notification.Name(rawValue: "ArticleSelectionChange")
|
||||
}
|
||||
|
||||
class NavigationModelController {
|
||||
class NavigationStateController {
|
||||
|
||||
static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
||||
|
||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||
|
||||
// Eventually I want these to be private too -Maurice
|
||||
var animatingChanges = false
|
||||
var expandedNodes = [Node]()
|
||||
var shadowTable = [[Node]]()
|
||||
|
||||
let treeControllerDelegate = FeedTreeControllerDelegate()
|
||||
lazy var treeController: TreeController = {
|
||||
return TreeController(delegate: treeControllerDelegate)
|
||||
}()
|
||||
|
||||
var rootNode: Node {
|
||||
return treeController.rootNode
|
||||
}
|
||||
|
||||
private var sortDirection = AppDefaults.timelineSortDirection {
|
||||
didSet {
|
||||
if sortDirection != oldValue {
|
||||
|
@ -45,6 +40,15 @@ class NavigationModelController {
|
|||
}
|
||||
}
|
||||
|
||||
private let treeControllerDelegate = FeedTreeControllerDelegate()
|
||||
lazy var treeController: TreeController = {
|
||||
return TreeController(delegate: treeControllerDelegate)
|
||||
}()
|
||||
|
||||
var rootNode: Node {
|
||||
return treeController.rootNode
|
||||
}
|
||||
|
||||
var showFeedNames = false {
|
||||
didSet {
|
||||
NotificationCenter.default.post(name: .ShowFeedNamesDidChange, object: self, userInfo: nil)
|
||||
|
@ -54,6 +58,7 @@ class NavigationModelController {
|
|||
|
||||
var timelineFetcher: ArticleFetcher? {
|
||||
didSet {
|
||||
currentArticleIndexPath = nil
|
||||
if timelineFetcher is Feed {
|
||||
showFeedNames = false
|
||||
} else {
|
||||
|
@ -64,6 +69,49 @@ class NavigationModelController {
|
|||
}
|
||||
}
|
||||
|
||||
var isPrevArticleAvailable: Bool {
|
||||
guard let indexPath = currentArticleIndexPath else {
|
||||
return false
|
||||
}
|
||||
return indexPath.row > 0
|
||||
}
|
||||
|
||||
var isNextArticleAvailable: Bool {
|
||||
guard let indexPath = currentArticleIndexPath else {
|
||||
return false
|
||||
}
|
||||
return indexPath.row + 1 < articles.count
|
||||
}
|
||||
|
||||
var prevArticleIndexPath: IndexPath? {
|
||||
guard let indexPath = currentArticleIndexPath else {
|
||||
return nil
|
||||
}
|
||||
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
|
||||
}
|
||||
|
||||
var nextArticleIndexPath: IndexPath? {
|
||||
guard let indexPath = currentArticleIndexPath else {
|
||||
return nil
|
||||
}
|
||||
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
||||
}
|
||||
|
||||
var currentArticle: Article? {
|
||||
if let indexPath = currentArticleIndexPath {
|
||||
return articles[indexPath.row]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentArticleIndexPath: IndexPath? {
|
||||
didSet {
|
||||
if currentArticleIndexPath != oldValue {
|
||||
NotificationCenter.default.post(name: .ArticleSelectionChange, object: self, userInfo: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var articles = ArticleArray() {
|
||||
didSet {
|
||||
if articles == oldValue {
|
||||
|
@ -80,8 +128,6 @@ class NavigationModelController {
|
|||
}
|
||||
}
|
||||
|
||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||
|
||||
init() {
|
||||
|
||||
for section in treeController.rootNode.childNodes {
|
||||
|
@ -165,6 +211,9 @@ class NavigationModelController {
|
|||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
|
@ -303,7 +352,7 @@ class NavigationModelController {
|
|||
|
||||
}
|
||||
|
||||
private extension NavigationModelController {
|
||||
private extension NavigationStateController {
|
||||
|
||||
// MARK: Fetching Articles
|
||||
|
||||
|
@ -358,7 +407,7 @@ private extension NavigationModelController {
|
|||
}
|
||||
|
||||
func queueFetchAndMergeArticles() {
|
||||
NavigationModelController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
||||
NavigationStateController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
||||
}
|
||||
|
||||
@objc func fetchAndMergeArticles() {
|
|
@ -17,10 +17,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
private var rowHeightWithoutFeedName: CGFloat = 0.0
|
||||
|
||||
private var currentRowHeight: CGFloat {
|
||||
return nmc.showFeedNames ? rowHeightWithFeedName : rowHeightWithoutFeedName
|
||||
return navState?.showFeedNames ?? false ? rowHeightWithFeedName : rowHeightWithoutFeedName
|
||||
}
|
||||
|
||||
var nmc: NavigationModelController!
|
||||
weak var navState: NavigationStateController?
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
var detailViewController: DetailViewController? {
|
||||
|
@ -47,10 +47,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: nmc)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(showFeedNamesDidChange(_:)), name: .ShowFeedNamesDidChange, object: nmc)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: nmc)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesDidChange(_:)), name: .ArticlesDidChange, object: nmc)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(showFeedNamesDidChange(_:)), name: .ShowFeedNamesDidChange, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesDidChange(_:)), name: .ArticlesDidChange, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleSelectionChange(_:)), name: .ArticleSelectionChange, object: navState)
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
|
||||
|
@ -69,14 +70,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if segue.identifier == "showDetail" {
|
||||
if let indexPath = tableView.indexPathForSelectedRow {
|
||||
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
|
||||
let article = nmc.articles[indexPath.row]
|
||||
controller.article = article
|
||||
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
|
||||
controller.navigationItem.leftItemsSupplementBackButton = true
|
||||
splitViewController?.toggleMasterView()
|
||||
}
|
||||
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
|
||||
controller.navState = navState
|
||||
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
|
||||
controller.navigationItem.leftItemsSupplementBackButton = true
|
||||
splitViewController?.toggleMasterView()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,7 +93,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
let markTitle = NSLocalizedString("Mark All Read", comment: "Mark All Read")
|
||||
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in
|
||||
|
||||
guard let articles = self?.nmc.articles,
|
||||
guard let articles = self?.navState?.articles,
|
||||
let undoManager = self?.undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else {
|
||||
return
|
||||
|
@ -116,12 +114,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return nmc.articles.count
|
||||
return navState?.articles.count ?? 0
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
let article = nmc.articles[indexPath.row]
|
||||
guard let article = navState?.articles[indexPath.row] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set up the star action
|
||||
let starTitle = article.status.starred ?
|
||||
|
@ -165,7 +165,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell
|
||||
let article = nmc.articles[indexPath.row]
|
||||
|
||||
guard let article = navState?.articles[indexPath.row] else {
|
||||
return cell
|
||||
}
|
||||
|
||||
configureTimelineCell(cell, article: article)
|
||||
|
||||
|
@ -173,10 +176,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let article = nmc.articles[indexPath.row]
|
||||
if !article.status.read {
|
||||
markArticles(Set([article]), statusKey: .read, flag: true)
|
||||
}
|
||||
navState?.currentArticleIndexPath = indexPath
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
@ -206,7 +206,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
|
||||
guard let article = nmc.articles.articleAtRow(indexPath.row) else {
|
||||
guard let article = navState?.articles.articleAtRow(indexPath.row) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -222,14 +222,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
|
||||
guard nmc.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
guard navState?.showAvatars ?? false, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
|
||||
guard let article = nmc.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
|
||||
guard let article = navState?.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -245,13 +245,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
@objc func imageDidBecomeAvailable(_ note: Notification) {
|
||||
if nmc.showAvatars {
|
||||
if navState?.showAvatars ?? false {
|
||||
queueReloadVisableCells()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func articlesReinitialized(_ note: Notification) {
|
||||
if nmc.articles.count > 0 {
|
||||
if navState?.articles.count ?? 0 > 0 {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
|
@ -268,6 +268,17 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
tableView.reloadData()
|
||||
}
|
||||
|
||||
@objc func articleSelectionChange(_ note: Notification) {
|
||||
|
||||
if let indexPath = navState?.currentArticleIndexPath, let article = navState?.articles[indexPath.row] {
|
||||
if !article.status.read {
|
||||
markArticles(Set([article]), statusKey: .read, flag: true)
|
||||
}
|
||||
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Reloading
|
||||
|
||||
@objc func reloadAllVisibleCells() {
|
||||
|
@ -290,8 +301,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
if articleIDs.isEmpty {
|
||||
return
|
||||
}
|
||||
let indexes = nmc.indexesForArticleIDs(articleIDs)
|
||||
reloadVisibleCells(for: indexes)
|
||||
if let indexes = navState?.indexesForArticleIDs(articleIDs) {
|
||||
reloadVisibleCells(for: indexes)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadVisibleCells(for indexes: IndexSet) {
|
||||
|
@ -344,13 +356,15 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
let featuredImage = featuredImageFor(article)
|
||||
|
||||
cell.cellData = MasterTimelineCellData(article: article, showFeedName: nmc.showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: nmc.showAvatars, featuredImage: featuredImage)
|
||||
let showFeedNames = navState?.showFeedNames ?? false
|
||||
let showAvatars = navState?.showAvatars ?? false
|
||||
cell.cellData = MasterTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: featuredImage)
|
||||
|
||||
}
|
||||
|
||||
func avatarFor(_ article: Article) -> UIImage? {
|
||||
|
||||
if !nmc.showAvatars {
|
||||
if !(navState?.showAvatars ?? false) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue