Merge remote-tracking branch 'brentsimmons/master'

This commit is contained in:
Olof Hellman 2018-02-11 01:20:38 -08:00
commit f563c2f78e
24 changed files with 544 additions and 62 deletions

View File

@ -103,6 +103,7 @@
84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; };
84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; };
84A37CBB201ECE590087C5AF /* RenameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84A37CB9201ECE590087C5AF /* RenameSheet.xib */; };
84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */; };
84B06FAE1ED37DBD00F0B54B /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; };
84B06FAF1ED37DBD00F0B54B /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
84B06FB21ED37DBD00F0B54B /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06F9D1ED37DA000F0B54B /* RSDatabase.framework */; };
@ -135,6 +136,8 @@
84DAEE321F870B390058304B /* DockBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE311F870B390058304B /* DockBadge.swift */; };
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; };
84E850861FCB60CE0072EA88 /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; };
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */; };
84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0EA202F693600562D8F /* DetailWebView.swift */; };
84E95CF71FABB3C800552D99 /* FeedList.plist in Resources */ = {isa = PBXBuildFile; fileRef = 84E95CF61FABB3C800552D99 /* FeedList.plist */; };
84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */; };
84EB381F1FBA8B9F000D2111 /* KeyboardShortcuts.html in Resources */ = {isa = PBXBuildFile; fileRef = 84EB38101FBA8B9F000D2111 /* KeyboardShortcuts.html */; };
@ -619,6 +622,7 @@
84A37CBA201ECE590087C5AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Evergreen/Base.lproj/RenameSheet.xib; sourceTree = SOURCE_ROOT; };
84A6B6931FB8D43C006754AC /* DinosaursWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DinosaursWindow.xib; sourceTree = "<group>"; };
84A6B6951FB8DBD2006754AC /* DinosaursWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DinosaursWindowController.swift; sourceTree = "<group>"; };
84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContextualMenuDelegate.swift; sourceTree = "<group>"; };
84B06F961ED37DA000F0B54B /* RSDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSDatabase.xcodeproj; path = Frameworks/RSDatabase/RSDatabase.xcodeproj; sourceTree = "<group>"; };
84B06FA21ED37DAC00F0B54B /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = Frameworks/RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
84B06FB61ED37E8B00F0B54B /* RSWeb.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSWeb.xcodeproj; path = Frameworks/RSWeb/RSWeb.xcodeproj; sourceTree = "<group>"; };
@ -644,6 +648,8 @@
84DAEE311F870B390058304B /* DockBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DockBadge.swift; path = Evergreen/DockBadge.swift; sourceTree = "<group>"; };
84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDefaults.swift; path = Evergreen/AppDefaults.swift; sourceTree = "<group>"; };
84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorAvatarDownloader.swift; sourceTree = "<group>"; };
84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineViewController+ContextualMenus.swift"; sourceTree = "<group>"; };
84E8E0EA202F693600562D8F /* DetailWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailWebView.swift; sourceTree = "<group>"; };
84E95CF61FABB3C800552D99 /* FeedList.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = FeedList.plist; sourceTree = "<group>"; };
84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlePasteboardWriter.swift; sourceTree = "<group>"; };
84EB38101FBA8B9F000D2111 /* KeyboardShortcuts.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = KeyboardShortcuts.html; sourceTree = "<group>"; };
@ -962,12 +968,14 @@
isa = PBXGroup;
children = (
849A976B1ED9EBC8007D329B /* TimelineViewController.swift */,
84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */,
84F204DF1FAACBB30076E152 /* ArticleArray.swift */,
849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */,
849A976A1ED9EBC8007D329B /* TimelineTableView.swift */,
844B5B6C1FEA282400C7C76A /* Keyboard */,
84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */,
8414AD241FCF5A1E00955102 /* TimelineHeaderView.swift */,
84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */,
849A976F1ED9EC04007D329B /* Cell */,
);
path = Timeline;
@ -990,6 +998,7 @@
isa = PBXGroup;
children = (
849A977E1ED9EC42007D329B /* DetailViewController.swift */,
84E8E0EA202F693600562D8F /* DetailWebView.swift */,
849A977D1ED9EC42007D329B /* ArticleRenderer.swift */,
84D52E941FE588BB00D14F5B /* DetailStatusBarView.swift */,
849A979A1ED9EFEB007D329B /* styleSheet.css */,
@ -1906,6 +1915,7 @@
84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */,
D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */,
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */,
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */,
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */,
@ -1914,6 +1924,7 @@
848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */,
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */,
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */,
84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */,
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
@ -1933,6 +1944,7 @@
D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */,
842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */,
849A97781ED9EC04007D329B /* TimelineCellLayout.swift in Sources */,
84E8E0EB202F693600562D8F /* DetailWebView.swift in Sources */,
84CC08061FF5D2E000C0C0ED /* FeedListSplitViewController.swift in Sources */,
849A976C1ED9EBC8007D329B /* TimelineTableRowView.swift in Sources */,
849A977B1ED9EC04007D329B /* UnreadIndicatorView.swift in Sources */,

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14087.3" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14092" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14087.3"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14092"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -21,9 +21,9 @@
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="YMs-P5-Xhn"/>
<toolbarItem implicitItemIdentifier="DD0FA79F-72C1-488B-B113-0D2DE89AA468" label="Search" paletteLabel="Search" toolTip="Search Articles" id="1Ql-WJ-KYi">
<size key="minSize" width="96" height="22"/>
<size key="maxSize" width="256" height="28"/>
<size key="maxSize" width="320" height="28"/>
<searchField key="view" wantsLayer="YES" verticalHuggingPriority="750" id="Fcs-4u-xuP">
<rect key="frame" x="0.0" y="14" width="256" height="22"/>
<rect key="frame" x="0.0" y="14" width="320" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<searchFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" borderStyle="bezel" usesSingleLineMode="YES" bezelStyle="round" id="syc-TO-rPc">
<font key="font" metaFont="system"/>
@ -167,25 +167,6 @@
<action selector="refreshAll:" target="Oky-zY-oP4" id="KRz-Df-3zA"/>
</connections>
</toolbarItem>
<toolbarItem implicitItemIdentifier="BEE79EC5-174E-40DB-AA76-0F1141C97804" label="Action" paletteLabel="Action" toolTip="Action" image="action" id="eyg-zr-m4a">
<size key="minSize" width="38" height="25"/>
<size key="maxSize" width="38" height="28"/>
<popUpButton key="view" verticalHuggingPriority="750" id="3z3-Db-0Kb">
<rect key="frame" x="2" y="14" width="38" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="action" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" pullsDown="YES" selectedItem="NHu-v8-gM6" id="lev-Yv-VWb">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="jk1-Ql-ozP">
<items>
<menuItem state="on" image="action" hidden="YES" id="NHu-v8-gM6"/>
<menuItem title="Item 2" id="1rz-c0-zPY"/>
<menuItem title="Item 3" id="9ZT-Uj-OTH"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="Skp-5r-70Q"/>
@ -193,13 +174,13 @@
<toolbarItem reference="QD0-SQ-OIM"/>
<toolbarItem reference="YMs-P5-Xhn"/>
<toolbarItem reference="lst-vn-0Iw"/>
<toolbarItem reference="Gxg-WQ-ufC"/>
<toolbarItem reference="1Ql-WJ-KYi"/>
<toolbarItem reference="YMs-P5-Xhn"/>
<toolbarItem reference="p7Y-Vm-ILH"/>
<toolbarItem reference="Gxg-WQ-ufC"/>
<toolbarItem reference="N7D-g2-EPD"/>
<toolbarItem reference="tid-SB-me3"/>
<toolbarItem reference="nv0-Ju-lP7"/>
<toolbarItem reference="YMs-P5-Xhn"/>
<toolbarItem reference="1Ql-WJ-KYi"/>
</defaultToolbarItems>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="hrC-d9-Cge"/>
@ -592,6 +573,7 @@
<outlet property="dataSource" destination="36G-bQ-b96" id="OpB-zC-ItJ"/>
<outlet property="delegate" destination="36G-bQ-b96" id="s1m-42-GQ4"/>
<outlet property="keyboardDelegate" destination="ZOV-xh-WJE" id="HiG-Bz-vD0"/>
<outlet property="menu" destination="gb5-z4-YPr" id="pey-0u-ogu"/>
</connections>
</tableView>
</subviews>
@ -623,15 +605,37 @@
</userDefinedRuntimeAttributes>
</view>
<connections>
<outlet property="contextualMenuDelegate" destination="iD1-KK-gFc" id="b0j-aW-e4B"/>
<outlet property="tableView" destination="DRs-j8-R9a" id="2AG-SP-7n2"/>
</connections>
</viewController>
<customObject id="Ebq-4s-EwK" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<menu id="gb5-z4-YPr">
<items>
<menuItem title="Item 1" id="Ikx-w7-cua">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Item 2" id="QX3-hL-Dqh">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Item 3" id="IsQ-j7-Njb">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
<connections>
<outlet property="delegate" destination="iD1-KK-gFc" id="q4j-wE-tnf"/>
</connections>
</menu>
<customObject id="ZOV-xh-WJE" customClass="TimelineKeyboardDelegate" customModule="Evergreen" customModuleProvider="target">
<connections>
<outlet property="timelineViewController" destination="36G-bQ-b96" id="rED-2Z-kh6"/>
</connections>
</customObject>
<customObject id="iD1-KK-gFc" customClass="TimelineContextualMenuDelegate" customModule="Evergreen" customModuleProvider="target">
<connections>
<outlet property="timelineViewController" destination="36G-bQ-b96" id="oE9-uV-TNi"/>
</connections>
</customObject>
</objects>
<point key="canvasLocation" x="62" y="394"/>
</scene>
@ -705,12 +709,12 @@
<image name="NSMobileMe" width="32" height="32"/>
<image name="NSRefreshTemplate" width="11" height="15"/>
<image name="NSShareTemplate" width="11" height="16"/>
<image name="action" width="19" height="19"/>
<image name="markAllRead" width="22" height="19"/>
<image name="markRead" width="19" height="19"/>
<image name="newFolder" width="19" height="19"/>
<image name="nextUnread" width="24" height="19"/>
<image name="openInBrowser" width="19" height="19"/>
<image name="star" width="19" height="19"/>
<image name="action" width="9.5" height="9.5"/>
<image name="markAllRead" width="11" height="9.5"/>
<image name="markRead" width="9.5" height="9.5"/>
<image name="newFolder" width="9.5" height="9.5"/>
<image name="nextUnread" width="12" height="9.5"/>
<image name="openInBrowser" width="9.5" height="9.5"/>
<image name="star" width="9.5" height="9.5"/>
</resources>
</document>

View File

@ -16,7 +16,7 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe
@IBOutlet var containerView: DetailContainerView!
var webview: WKWebView!
var webview: DetailWebView!
var noSelectionView: NoSelectionView!
var article: Article? {
@ -54,7 +54,7 @@ final class DetailViewController: NSViewController, WKNavigationDelegate, WKUIDe
userContentController.add(self, name: MessageName.mouseDidExit)
configuration.userContentController = userContentController
webview = WKWebView(frame: self.view.bounds, configuration: configuration)
webview = DetailWebView(frame: self.view.bounds, configuration: configuration)
webview.uiDelegate = self
webview.navigationDelegate = self
webview.translatesAutoresizingMaskIntoConstraints = false

View File

@ -0,0 +1,66 @@
//
// DetailWebView.swift
// Evergreen
//
// Created by Brent Simmons on 2/10/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
import WebKit
// Theres no API for affecting a WKWebViews contextual menu.
// (WebView had API for this.)
//
// This a minor hack. It hides unwanted menu items.
// The menu item identifiers are not documented anywhere;
// they could change, and this code would need updating.
final class DetailWebView: WKWebView {
// MARK: NSView
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if shouldHideMenuItem(menuItem) {
menuItem.isHidden = true
}
}
super.willOpenMenu(menu, with: event)
}
}
private extension NSUserInterfaceItemIdentifier {
static let DetailMenuItemIdentifierReload = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierReload")
static let DetailMenuItemIdentifierOpenLink = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierOpenLink")
}
private extension DetailWebView {
static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload, .DetailMenuItemIdentifierOpenLink]
static let menuItemIdentifierMatchStrings = ["newwindow", "download"]
func shouldHideMenuItem(_ menuItem: NSMenuItem) -> Bool {
guard let identifier = menuItem.identifier else {
return false
}
if DetailWebView.menuItemIdentifiersToHide.contains(identifier) {
return true
}
let lowerIdentifier = identifier.rawValue.lowercased()
for matchString in DetailWebView.menuItemIdentifierMatchStrings {
if lowerIdentifier.contains(matchString) {
return true
}
}
return false
}
}

View File

@ -7,6 +7,7 @@
//
import AppKit
import RSCore
@objc final class SidebarContextualMenuDelegate: NSObject, NSMenuDelegate {
@ -24,11 +25,7 @@ import AppKit
return
}
let items = contextualMenu.items
contextualMenu.removeAllItems()
for menuItem in items {
menu.addItem(menuItem)
}
menu.takeItems(from: contextualMenu)
}
}

View File

@ -78,13 +78,38 @@ extension Array where Element == Article {
func canMarkAllAsRead() -> Bool {
return anyArticleIsUnread()
}
func anyArticlePassesTest(_ test: ((Article) -> Bool)) -> Bool {
for article in self {
if !article.status.read {
if test(article) {
return true
}
}
return false
}
func anyArticleIsRead() -> Bool {
return anyArticlePassesTest { $0.status.read }
}
func anyArticleIsUnread() -> Bool {
return anyArticlePassesTest { !$0.status.read }
}
func anyArticleIsStarred() -> Bool {
return anyArticlePassesTest { $0.status.starred }
}
func anyArticleIsUnstarred() -> Bool {
return anyArticlePassesTest { !$0.status.starred }
}
}
private extension Array where Element == Article {

View File

@ -0,0 +1,32 @@
//
// TimelineContextualMenuDelegate.swift
// Evergreen
//
// Created by Brent Simmons on 2/8/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
import RSCore
@objc final class TimelineContextualMenuDelegate: NSObject, NSMenuDelegate {
@IBOutlet weak var timelineViewController: TimelineViewController?
public func menuNeedsUpdate(_ menu: NSMenu) {
guard let timelineViewController = timelineViewController else {
return
}
menu.removeAllItems()
guard let contextualMenu = timelineViewController.contextualMenuForClickedRows() else {
return
}
menu.takeItems(from: contextualMenu)
}
}

View File

@ -0,0 +1,162 @@
//
// TimelineViewController+ContextualMenus.swift
// Evergreen
//
// Created by Brent Simmons on 2/9/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
import Data
import Account
extension TimelineViewController {
func contextualMenuForClickedRows() -> NSMenu? {
let row = tableView.clickedRow
guard row != -1, let article = articles.articleAtRow(row) else {
return nil
}
if selectedArticles.contains(article) {
// If the clickedRow is part of the selected rows, then do a contextual menu for all the selected rows.
return menu(for: selectedArticles)
}
return menu(for: [article])
}
}
// MARK: Contextual Menu Actions
extension TimelineViewController {
@objc func markArticlesReadFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else {
return
}
markArticles(articles, read: true)
}
@objc func markArticlesUnreadFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else {
return
}
markArticles(articles, read: false)
}
@objc func markArticlesStarredFromContextualMenu(_ sender: Any?) {
}
@objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) {
}
@objc func openInBrowserFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
return
}
Browser.open(urlString, inBackground: false)
}
}
private extension TimelineViewController {
func markArticles(_ articles: [Article], read: Bool) {
guard let articlesToMark = read ? unreadArticles(from: articles) : readArticles(from: articles) else {
return
}
guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: Array(articlesToMark), markingRead: read, undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
}
func unreadArticles(from articles: [Article]) -> [Article]? {
let filteredArticles = articles.filter { !$0.status.read }
return filteredArticles.isEmpty ? nil : filteredArticles
}
func readArticles(from articles: [Article]) -> [Article]? {
let filteredArticles = articles.filter { $0.status.read }
return filteredArticles.isEmpty ? nil : filteredArticles
}
func articles(from sender: Any?) -> [Article]? {
return (sender as? NSMenuItem)?.representedObject as? [Article]
}
func menu(for articles: [Article]) -> NSMenu? {
let menu = NSMenu(title: "")
if articles.anyArticleIsUnread() {
menu.addItem(markReadMenuItem(articles))
}
if articles.anyArticleIsRead() {
menu.addItem(markUnreadMenuItem(articles))
}
if menu.items.count > 0 {
menu.addItem(NSMenuItem.separator())
}
// if articles.anyArticleIsUnstarred() {
// menu.addItem(markStarredMenuItem(articles))
// }
// if articles.anyArticleIsStarred() {
// menu.addItem(markUnstarredMenuItem(articles))
// }
if menu.items.count > 0 && !menu.items.last!.isSeparatorItem {
menu.addItem(NSMenuItem.separator())
}
if articles.count == 1, let link = articles.first!.preferredLink {
menu.addItem(openInBrowserMenuItem(link))
}
return menu
}
func markReadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("Mark as Read", comment: "Command"), #selector(markArticlesReadFromContextualMenu(_:)), articles)
}
func markUnreadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("Mark as Unread", comment: "Command"), #selector(markArticlesUnreadFromContextualMenu(_:)), articles)
}
func markStarredMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("Mark as Starred", comment: "Command"), #selector(markArticlesStarredFromContextualMenu(_:)), articles)
}
func markUnstarredMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("Mark as Unstarred", comment: "Command"), #selector(markArticlesUnstarredFromContextualMenu(_:)), articles)
}
func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem {
return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString)
}
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
item.representedObject = representedObject
item.target = self
return item
}
}

View File

@ -15,7 +15,8 @@ import Account
class TimelineViewController: NSViewController, UndoableCommandRunner {
@IBOutlet var tableView: TimelineTableView!
@IBOutlet var contextualMenuDelegate: TimelineContextualMenuDelegate?
var selectedArticles: [Article] {
get {
return Array(articles.articlesForIndexes(tableView.selectedRowIndexes))
@ -28,6 +29,16 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
}
}
var articles = ArticleArray() {
didSet {
if articles != oldValue {
clearUndoableCommands()
updateShowAvatars()
tableView.reloadData()
}
}
}
var undoableCommands = [UndoableCommand]()
private var cellAppearance: TimelineCellAppearance!
private var cellAppearanceWithAvatar: TimelineCellAppearance!
@ -60,16 +71,6 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
}
}
}
private var articles = ArticleArray() {
didSet {
if articles != oldValue {
clearUndoableCommands()
updateShowAvatars()
tableView.reloadData()
}
}
}
private var fontSize: FontSize = AppDefaults.shared.timelineFontSize {
didSet {
if fontSize != oldValue {
@ -678,11 +679,8 @@ private extension TimelineViewController {
for object in representedObjects {
if let feed = object as? Feed {
fetchedArticles.formUnion(feed.fetchArticles())
}
else if let folder = object as? Folder {
fetchedArticles.formUnion(folder.fetchArticles())
if let articleFetcher = object as? ArticleFetcher {
fetchedArticles.formUnion(articleFetcher.fetchArticles())
}
}

View File

@ -11,7 +11,7 @@ import RSCore
import Data
import Account
protocol SmartFeedDelegate: DisplayNameProvider {
protocol SmartFeedDelegate: DisplayNameProvider, ArticleFetcher {
func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void)
}
@ -51,6 +51,19 @@ final class SmartFeed: PseudoFeed {
}
}
extension SmartFeed: ArticleFetcher {
func fetchArticles() -> Set<Article> {
return delegate.fetchArticles()
}
func fetchUnreadArticles() -> Set<Article> {
return delegate.fetchUnreadArticles()
}
}
private extension SmartFeed {
// MARK: - Unread Counts

View File

@ -19,4 +19,18 @@ struct StarredFeedDelegate: SmartFeedDelegate {
account.fetchUnreadCountForStarredArticles(callback)
}
// MARK: ArticleFetcher
func fetchArticles() -> Set<Article> {
// TODO
return Set<Article>()
}
func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
}
}

View File

@ -18,5 +18,21 @@ struct TodayFeedDelegate: SmartFeedDelegate {
account.fetchUnreadCountForToday(callback)
}
// MARK: ArticleFetcher
func fetchArticles() -> Set<Article> {
var articles = Set<Article>()
for account in AccountManager.shared.accounts {
articles.formUnion(account.fetchTodayArticles())
}
return articles
}
func fetchUnreadArticles() -> Set<Article> {
return fetchArticles().unreadArticles()
}
}

View File

@ -7,6 +7,8 @@
//
import Foundation
import Account
import Data
// This just shows the global unread count, which appDelegate already has. Easy.
@ -34,3 +36,20 @@ final class UnreadFeed: PseudoFeed {
unreadCount = appDelegate.unreadCount
}
}
extension UnreadFeed: ArticleFetcher {
func fetchArticles() -> Set<Article> {
return fetchUnreadArticles()
}
func fetchUnreadArticles() -> Set<Article> {
var articles = Set<Article>()
for account in AccountManager.shared.accounts {
articles.formUnion(account.fetchUnreadArticles())
}
return articles
}
}

View File

@ -341,14 +341,29 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return articles
}
public func fetchUnreadArticles() -> Set<Article> {
return fetchUnreadArticles(forContainer: self)
}
public func fetchArticles(folder: Folder) -> Set<Article> {
let feeds = folder.flattenedFeeds()
return fetchUnreadArticles(forContainer: folder)
}
public func fetchUnreadArticles(forContainer container: Container) -> Set<Article> {
let feeds = container.flattenedFeeds()
let articles = database.fetchUnreadArticles(for: feeds)
feeds.forEach { validateUnreadCount($0, articles) }
return articles
}
public func fetchTodayArticles() -> Set<Article> {
return database.fetchTodayArticles(for: flattenedFeeds())
}
private func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
// articles must contain all the unread articles for the feed.

View File

@ -75,6 +75,12 @@ public extension Set where Element == Article {
return Set<String>(map { $0.articleID })
}
public func unreadArticles() -> Set<Article> {
let articles = self.filter { !$0.status.read }
return Set(articles)
}
}
public extension Array where Element == Article {

View File

@ -72,6 +72,11 @@ final class ArticlesTable: DatabaseTable {
return fetchUnreadArticles(feeds.feedIDs())
}
public func fetchTodayArticles(for feeds: Set<Feed>) -> Set<Article> {
return fetchTodayArticles(feeds.feedIDs())
}
// MARK: Updating
func update(_ feed: Feed, _ parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesWithFeedCompletionBlock) {
@ -328,8 +333,14 @@ private extension ArticlesTable {
// * Must not be deleted.
// * Must be either 1) starred or 2) dateArrived must be newer than cutoff date.
let sql = withLimits ? "select * from articles natural join statuses where \(whereClause) and userDeleted=0 and (starred=1 or dateArrived>?);" : "select * from articles natural join statuses where \(whereClause);"
return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database)
if withLimits {
let sql = "select * from articles natural join statuses where \(whereClause) and userDeleted=0 and (starred=1 or dateArrived>?);"
return articlesWithSQL(sql, parameters + [articleCutoffDate as AnyObject], database)
}
else {
let sql = "select * from articles natural join statuses where \(whereClause);"
return articlesWithSQL(sql, parameters, database)
}
}
func fetchUnreadCount(_ feedID: String, _ database: FMDatabase) -> Int {
@ -369,6 +380,31 @@ private extension ArticlesTable {
return articles
}
func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
if feedIDs.isEmpty {
return Set<Article>()
}
var articles = Set<Article>()
queue.fetchSync { (database) in
// select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?)
//
// datePublished may be nil, so we fall back to dateArrived.
let startOfToday = NSCalendar.startOfToday()
let parameters = feedIDs.map { $0 as AnyObject } + [startOfToday as AnyObject, startOfToday as AnyObject]
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))!
let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0"
// let whereClause = "feedID in \(placeholders) and datePublished > ? and userDeleted = 0"
articles = self.fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false)
}
return articles
}
func articlesWithSQL(_ sql: String, _ parameters: [AnyObject], _ database: FMDatabase) -> Set<Article> {
guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else {

View File

@ -57,6 +57,11 @@ public final class Database {
return articlesTable.fetchUnreadArticles(for: feeds)
}
public func fetchTodayArticles(for feeds: Set<Feed>) -> Set<Article> {
return articlesTable.fetchTodayArticles(for: feeds)
}
// MARK: - Unread Counts
public func fetchUnreadCounts(for feeds: Set<Feed>, _ completion: @escaping UnreadCountCompletionBlock) {

View File

@ -23,7 +23,7 @@ extension Article {
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments)
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, attachments: attachments, status: status)
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished ?? parsedItem.dateModified, dateModified: parsedItem.dateModified, authors: authors, attachments: attachments, status: status)
}
private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath<Article,String?>, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) {

View File

@ -158,6 +158,7 @@
84CFF56E1AC3D20A00CEA6C8 /* NSImage+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */; };
84D5BA1E201E87E2009092BD /* URLPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */; };
84E34DA61F9FA1070077082F /* UndoableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E34DA51F9FA1070077082F /* UndoableCommand.swift */; };
84E8E0D9202EC39800562D8F /* NSMenu+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */; };
84F20F831F16BA6200D8E682 /* PropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F821F16BA6200D8E682 /* PropertyList.swift */; };
84FE9FC31C00453900081CE9 /* NSStoryboard+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
84FE9FC41C00453900081CE9 /* NSStoryboard+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */; };
@ -276,6 +277,7 @@
84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSImage+RSCore.m"; sourceTree = "<group>"; };
84D5BA1D201E87E2009092BD /* URLPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = URLPasteboardWriter.swift; path = AppKit/URLPasteboardWriter.swift; sourceTree = "<group>"; };
84E34DA51F9FA1070077082F /* UndoableCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UndoableCommand.swift; path = RSCore/UndoableCommand.swift; sourceTree = "<group>"; };
84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSMenu+Extensions.swift"; path = "AppKit/NSMenu+Extensions.swift"; sourceTree = "<group>"; };
84F20F821F16BA6200D8E682 /* PropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyList.swift; sourceTree = "<group>"; };
84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSStoryboard+RSCore.h"; sourceTree = "<group>"; };
84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSStoryboard+RSCore.m"; sourceTree = "<group>"; };
@ -454,6 +456,7 @@
8415CB891BF84D24007B1E98 /* NSEvent+RSCore.m */,
84CFF56B1AC3D20A00CEA6C8 /* NSImage+RSCore.h */,
84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */,
84E8E0D8202EC39800562D8F /* NSMenu+Extensions.swift */,
842635581D7FA24800196285 /* NSOutlineView+Extensions.swift */,
849B08951BF7BCE30090CEE4 /* NSPasteboard+RSCore.h */,
849B08961BF7BCE30090CEE4 /* NSPasteboard+RSCore.m */,
@ -661,6 +664,7 @@
};
84CFF4F31AC3C69700CEA6C8 = {
CreatedOnToolsVersion = 6.2;
DevelopmentTeam = 9C84TZ7Q6Z;
LastSwiftMigration = 0800;
};
84CFF4FE1AC3C69700CEA6C8 = {
@ -784,6 +788,7 @@
84C687321FBAA3DF00345C9E /* LogWindowController.swift in Sources */,
84C687381FBC028900345C9E /* LogItem.swift in Sources */,
8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */,
84E8E0D9202EC39800562D8F /* NSMenu+Extensions.swift in Sources */,
84D5BA1E201E87E2009092BD /* URLPasteboardWriter.swift in Sources */,
849B08981BF7BCE30090CEE4 /* NSPasteboard+RSCore.m in Sources */,
842635571D7FA1C800196285 /* NSTableView+Extensions.swift in Sources */,

View File

@ -0,0 +1,23 @@
//
// NSMenu+Extensions.swift
// RSCore
//
// Created by Brent Simmons on 2/9/18.
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
//
import AppKit
public extension NSMenu {
public func takeItems(from menu: NSMenu) {
// The passed-in menu gets all its items removed.
let items = menu.items
menu.removeAllItems()
for menuItem in items {
addItem(menuItem)
}
}
}

View File

@ -27,5 +27,6 @@
- (NSRect)rs_rectCentered:(NSRect)originalRect;
- (NSTableView *)rs_enclosingTableView;
@end

View File

@ -65,4 +65,18 @@
}
- (NSTableView *)rs_enclosingTableView {
NSView *nomad = self.superview;
while (nomad != nil) {
if ([nomad isKindOfClass:[NSTableView class]]) {
return (NSTableView *)nomad;
}
nomad = nomad.superview;
}
return nil;
}
@end

View File

@ -6,6 +6,7 @@
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
@import RSCore;
#import "RSMultiLineView.h"
#import "RSMultiLineRenderer.h"
#import "RSMultiLineRendererMeasurements.h"
@ -137,6 +138,16 @@ static NSAttributedString *emptyAttributedString = nil;
}
- (NSMenu *)menuForEvent:(NSEvent *)event {
NSTableView *tableView = [self rs_enclosingTableView];
if (tableView) {
return [tableView menuForEvent:event];
}
return nil;
}
- (void)drawRect:(NSRect)r {
if (self.selected) {

View File

@ -124,6 +124,14 @@ static NSAttributedString *emptyAttributedString = nil;
return self.intrinsicSize;
}
- (NSMenu *)menuForEvent:(NSEvent *)event {
NSTableView *tableView = [self rs_enclosingTableView];
if (tableView) {
return [tableView menuForEvent:event];
}
return nil;
}
- (void)drawRect:(NSRect)r {