mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Fix lint issues.
This commit is contained in:
@@ -28,7 +28,7 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
private var addFeedWindowController: AddFeedWindowController?
|
||||
private var foundFeedURLString: String?
|
||||
private var titleFromFeed: String?
|
||||
|
||||
|
||||
init(hostWindow: NSWindow) {
|
||||
self.hostWindow = hostWindow
|
||||
}
|
||||
@@ -62,11 +62,11 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
}
|
||||
|
||||
account.createFeed(url: url.absoluteString, name: title, container: container, validateFeed: true) { result in
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.endShowingProgress()
|
||||
}
|
||||
|
||||
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
@@ -82,9 +82,9 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
beginShowingProgress()
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ private extension AddFeedController {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
struct AccountAndFolderSpecifier {
|
||||
let account: Account
|
||||
let folder: Folder?
|
||||
@@ -156,13 +156,12 @@ private extension AddFeedController {
|
||||
// MARK: Progress
|
||||
|
||||
func beginShowingProgress() {
|
||||
runIndeterminateProgressWithMessage(NSLocalizedString("Finding feed…", comment:"Feed finder"))
|
||||
runIndeterminateProgressWithMessage(NSLocalizedString("Finding feed…", comment: "Feed finder"))
|
||||
}
|
||||
|
||||
|
||||
func endShowingProgress() {
|
||||
stopIndeterminateProgress()
|
||||
hostWindow.makeKeyAndOrderFront(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ protocol AddFeedWindowControllerDelegate: AnyObject {
|
||||
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
|
||||
}
|
||||
|
||||
//protocol AddFeedWindowController {
|
||||
// protocol AddFeedWindowController {
|
||||
//
|
||||
// var window: NSWindow? { get }
|
||||
// func runSheetOnWindow(_ hostWindow: NSWindow)
|
||||
//}
|
||||
// }
|
||||
|
||||
@@ -12,7 +12,7 @@ import RSTree
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
final class AddFeedWindowController : NSWindowController {
|
||||
final class AddFeedWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet var urlTextField: NSTextField!
|
||||
@IBOutlet var nameTextField: NSTextField!
|
||||
@@ -34,7 +34,7 @@ final class AddFeedWindowController : NSWindowController {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
var hostWindow: NSWindow!
|
||||
|
||||
convenience init(urlString: String?, name: String?, account: Account?, folder: Folder?, folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
|
||||
@@ -46,9 +46,9 @@ final class AddFeedWindowController : NSWindowController {
|
||||
self.delegate = delegate
|
||||
self.folderTreeController = folderTreeController
|
||||
}
|
||||
|
||||
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow) {
|
||||
hostWindow.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in
|
||||
hostWindow.beginSheet(window!) { (_: NSApplication.ModalResponse) in
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ final class AddFeedWindowController : NSWindowController {
|
||||
}
|
||||
|
||||
folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode)
|
||||
|
||||
|
||||
if let account = initialAccount {
|
||||
FolderTreeMenu.select(account: account, folder: initialFolder, in: folderPopupButton)
|
||||
} else if let container = AddFeedDefaultContainer.defaultContainer {
|
||||
@@ -73,41 +73,41 @@ final class AddFeedWindowController : NSWindowController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: Any?) {
|
||||
cancelSheet()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func addFeed(_ sender: Any?) {
|
||||
let urlString = urlTextField.stringValue
|
||||
let normalizedURLString = urlString.normalizedURL
|
||||
|
||||
if normalizedURLString.isEmpty {
|
||||
cancelSheet()
|
||||
return;
|
||||
return
|
||||
}
|
||||
guard let url = URL(string: normalizedURLString) else {
|
||||
cancelSheet()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let container = selectedContainer() else { return }
|
||||
AddFeedDefaultContainer.saveDefaultContainer(container)
|
||||
|
||||
delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: container)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@IBAction func localShowFeedList(_ sender: Any?) {
|
||||
NSApplication.shared.sendAction(NSSelectorFromString("showFeedList:"), to: nil, from: sender)
|
||||
hostWindow.endSheet(window!, returnCode: NSApplication.ModalResponse.continue)
|
||||
}
|
||||
|
||||
|
||||
// MARK: NSTextFieldDelegate
|
||||
|
||||
@objc func controlTextDidEndEditing(_ obj: Notification) {
|
||||
@@ -117,7 +117,7 @@ final class AddFeedWindowController : NSWindowController {
|
||||
@objc func controlTextDidChange(_ obj: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
|
||||
private func updateUI() {
|
||||
addButton.isEnabled = urlTextField.stringValue.mayBeURL && selectedContainer() != nil
|
||||
}
|
||||
|
||||
@@ -14,34 +14,34 @@ import Account
|
||||
class FolderTreeMenu {
|
||||
|
||||
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {
|
||||
|
||||
|
||||
let menu = NSMenu(title: "Folders")
|
||||
menu.autoenablesItems = false
|
||||
|
||||
|
||||
for childNode in rootNode.childNodes {
|
||||
|
||||
|
||||
guard let account = childNode.representedObject as? Account else {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if restrictToSpecialAccounts && !(account.type == .onMyMac || account.type == .cloudKit) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
let menuItem = NSMenuItem(title: account.nameForDisplay, action: nil, keyEquivalent: "")
|
||||
menuItem.representedObject = childNode.representedObject
|
||||
|
||||
|
||||
if account.behaviors.contains(.disallowFeedInRootFolder) {
|
||||
menuItem.isEnabled = false
|
||||
}
|
||||
|
||||
|
||||
menu.addItem(menuItem)
|
||||
|
||||
let childNodes = childNode.childNodes
|
||||
addFolderItemsToMenuWithNodes(menu: menu, nodes: childNodes, indentationLevel: 1)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
@@ -61,21 +61,21 @@ class FolderTreeMenu {
|
||||
}
|
||||
|
||||
private static func addFolderItemsToMenuWithNodes(menu: NSMenu, nodes: [Node], indentationLevel: Int) {
|
||||
|
||||
|
||||
for oneNode in nodes {
|
||||
|
||||
if let nameProvider = oneNode.representedObject as? DisplayNameProvider {
|
||||
|
||||
|
||||
let menuItem = NSMenuItem(title: nameProvider.nameForDisplay, action: nil, keyEquivalent: "")
|
||||
menuItem.indentationLevel = indentationLevel
|
||||
menuItem.representedObject = oneNode.representedObject
|
||||
menu.addItem(menuItem)
|
||||
|
||||
|
||||
if oneNode.numberOfChildNodes > 0 {
|
||||
addFolderItemsToMenuWithNodes(menu: menu, nodes: oneNode.childNodes, indentationLevel: indentationLevel + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import AppKit
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
class AddFolderWindowController : NSWindowController {
|
||||
|
||||
class AddFolderWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet var folderNameTextField: NSTextField!
|
||||
@IBOutlet var accountPopupButton: NSPopUpButton!
|
||||
@IBOutlet var addFolderButton: NSButton!
|
||||
@@ -22,11 +22,11 @@ class AddFolderWindowController : NSWindowController {
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
|
||||
|
||||
func runSheetOnWindow(_ w: NSWindow) {
|
||||
hostWindow = w
|
||||
hostWindow!.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in
|
||||
|
||||
hostWindow!.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) in
|
||||
|
||||
if returnCode == NSApplication.ModalResponse.OK {
|
||||
self.addFolderIfNeeded()
|
||||
}
|
||||
@@ -34,37 +34,37 @@ class AddFolderWindowController : NSWindowController {
|
||||
}
|
||||
|
||||
// MARK: - NSViewController
|
||||
|
||||
|
||||
override func windowDidLoad() {
|
||||
let preferredAccountID = AppDefaults.shared.addFolderAccountID
|
||||
accountPopupButton.removeAllItems()
|
||||
|
||||
|
||||
let menu = NSMenu()
|
||||
accountPopupButton.menu = menu
|
||||
|
||||
|
||||
let accounts = AccountManager.shared
|
||||
.sortedActiveAccounts
|
||||
.filter { !$0.behaviors.contains(.disallowFolderManagement) }
|
||||
|
||||
|
||||
for oneAccount in accounts {
|
||||
|
||||
|
||||
let oneMenuItem = NSMenuItem()
|
||||
oneMenuItem.title = oneAccount.nameForDisplay
|
||||
oneMenuItem.representedObject = oneAccount
|
||||
menu.addItem(oneMenuItem)
|
||||
|
||||
|
||||
if oneAccount.accountID == preferredAccountID {
|
||||
accountPopupButton.select(oneMenuItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: Any?) {
|
||||
hostWindow!.endSheet(window!, returnCode: .cancel)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func addFolder(_ sender: Any?) {
|
||||
hostWindow!.endSheet(window!, returnCode: .OK)
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@ enum ArticleExtractorButtonState {
|
||||
}
|
||||
|
||||
class ArticleExtractorButton: NSButton {
|
||||
|
||||
|
||||
private var animatedLayer: CALayer?
|
||||
|
||||
|
||||
var buttonState: ArticleExtractorButtonState = .off {
|
||||
didSet {
|
||||
if buttonState != oldValue {
|
||||
@@ -39,7 +39,7 @@ class ArticleExtractorButton: NSButton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func accessibilityLabel() -> String? {
|
||||
switch buttonState {
|
||||
case .error:
|
||||
@@ -57,12 +57,12 @@ class ArticleExtractorButton: NSButton {
|
||||
super.init(frame: frameRect)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
private func commonInit() {
|
||||
wantsLayer = true
|
||||
bezelStyle = .texturedRounded
|
||||
@@ -70,7 +70,7 @@ class ArticleExtractorButton: NSButton {
|
||||
imageScaling = .scaleProportionallyDown
|
||||
widthAnchor.constraint(equalTo: heightAnchor).isActive = true
|
||||
}
|
||||
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
guard case .animated = buttonState else {
|
||||
@@ -79,31 +79,31 @@ class ArticleExtractorButton: NSButton {
|
||||
stripAnimatedSublayer()
|
||||
addAnimatedSublayer(to: layer!)
|
||||
}
|
||||
|
||||
|
||||
private func stripAnimatedSublayer() {
|
||||
animatedLayer?.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
|
||||
private func addAnimatedSublayer(to hostedLayer: CALayer) {
|
||||
let image1 = AppAssets.articleExtractorOff.tinted(with: NSColor.controlTextColor)
|
||||
let image2 = AppAssets.articleExtractorOn.tinted(with: NSColor.controlTextColor)
|
||||
let images = [image1, image2, image1]
|
||||
|
||||
|
||||
animatedLayer = CALayer()
|
||||
let imageSize = AppAssets.articleExtractorOff.size
|
||||
animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)
|
||||
animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
|
||||
|
||||
hostedLayer.addSublayer(animatedLayer!)
|
||||
|
||||
|
||||
let animation = CAKeyframeAnimation(keyPath: "contents")
|
||||
animation.calculationMode = CAAnimationCalculationMode.linear
|
||||
animation.keyTimes = [0, 0.5, 1]
|
||||
animation.duration = 2
|
||||
animation.values = images
|
||||
animation.repeatCount = HUGE
|
||||
|
||||
|
||||
animatedLayer!.add(animation, forKey: "contents")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ final class DetailContainerView: NSView {
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
NSColor.controlBackgroundColor.set()
|
||||
let r = NSIntersectionRect(dirtyRect, bounds)
|
||||
let r = dirtyRect.intersection(bounds)
|
||||
r.fill()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ final class DetailStatusBarView: NSView {
|
||||
if let link = linkForDisplay {
|
||||
urlLabel.stringValue = link
|
||||
self.isHidden = false
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
urlLabel.stringValue = ""
|
||||
self.isHidden = true
|
||||
}
|
||||
@@ -38,7 +37,7 @@ final class DetailStatusBarView: NSView {
|
||||
override var isOpaque: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
override var isFlipped: Bool {
|
||||
return true
|
||||
}
|
||||
@@ -68,11 +67,8 @@ private extension DetailStatusBarView {
|
||||
func updateLinkForDisplay() {
|
||||
if let mouseoverLink = mouseoverLink, !mouseoverLink.isEmpty {
|
||||
linkForDisplay = mouseoverLink.strippingHTTPOrHTTPSScheme
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
linkForDisplay = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ final class DetailViewController: NSViewController, WKUIDelegate {
|
||||
func stopMediaPlayback() {
|
||||
currentWebViewController.stopMediaPlayback()
|
||||
}
|
||||
|
||||
|
||||
func canScrollDown(_ callback: @escaping (Bool) -> Void) {
|
||||
currentWebViewController.canScrollDown(callback)
|
||||
}
|
||||
@@ -98,22 +98,22 @@ final class DetailViewController: NSViewController, WKUIDelegate {
|
||||
override func scrollPageUp(_ sender: Any?) {
|
||||
currentWebViewController.scrollPageUp(sender)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
|
||||
func focus() {
|
||||
guard let window = currentWebViewController.webView.window else {
|
||||
return
|
||||
}
|
||||
window.makeFirstResponderUnlessDescendantIsFirstResponder(currentWebViewController.webView)
|
||||
}
|
||||
|
||||
|
||||
// MARK: State Restoration
|
||||
|
||||
func saveState(to state: inout [AnyHashable : Any]) {
|
||||
|
||||
func saveState(to state: inout [AnyHashable: Any]) {
|
||||
currentWebViewController.saveState(to: &state)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - DetailWebViewControllerDelegate
|
||||
@@ -158,7 +158,7 @@ private extension DetailViewController {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func userDefaultsDidChange(_ : Notification) {
|
||||
@objc func userDefaultsDidChange(_: Notification) {
|
||||
if AppDefaults.shared.isArticleContentJavascriptEnabled != isArticleContentJavascriptEnabled {
|
||||
isArticleContentJavascriptEnabled = AppDefaults.shared.isArticleContentJavascriptEnabled
|
||||
createNewWebViewsAndRestoreState()
|
||||
|
||||
@@ -13,13 +13,13 @@ import RSCore
|
||||
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) {
|
||||
if keyboardDelegate?.keydown(event, in: self) ?? false {
|
||||
return
|
||||
@@ -55,16 +55,16 @@ final class DetailWebView: WKWebView {
|
||||
evaluateJavaScript("document.body.style.overflow = 'visible';", completionHandler: nil)
|
||||
bigSurOffsetFix()
|
||||
}
|
||||
|
||||
|
||||
override func setFrameSize(_ newSize: NSSize) {
|
||||
super.setFrameSize(newSize)
|
||||
if (!inLiveResize) {
|
||||
if !inLiveResize {
|
||||
bigSurOffsetFix()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var inBigSurOffsetFix = false
|
||||
|
||||
|
||||
private func bigSurOffsetFix() {
|
||||
/*
|
||||
On macOS 11, when a user exits full screen
|
||||
@@ -77,17 +77,17 @@ final class DetailWebView: WKWebView {
|
||||
guard var frame = window?.frame else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard !inBigSurOffsetFix else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
inBigSurOffsetFix = true
|
||||
|
||||
|
||||
defer {
|
||||
inBigSurOffsetFix = false
|
||||
}
|
||||
|
||||
|
||||
frame.size = NSSize(width: window!.frame.width, height: window!.frame.height - 1)
|
||||
window!.setFrame(frame, display: false)
|
||||
frame.size = NSSize(width: window!.frame.width, height: window!.frame.height + 1)
|
||||
@@ -128,4 +128,3 @@ private extension DetailWebView {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ final class DetailWebViewController: NSViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var article: Article? {
|
||||
switch state {
|
||||
case .article(let article, _):
|
||||
@@ -45,9 +45,9 @@ final class DetailWebViewController: NSViewController {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var articleTextSize = AppDefaults.shared.articleTextSize
|
||||
|
||||
|
||||
private var webInspectorEnabled: Bool {
|
||||
get {
|
||||
return webView.configuration.preferences._developerExtrasEnabled
|
||||
@@ -64,7 +64,7 @@ final class DetailWebViewController: NSViewController {
|
||||
|
||||
private var isShowingExtractedArticle: Bool {
|
||||
switch state {
|
||||
case .extracted(_, _, _):
|
||||
case .extracted:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -96,7 +96,7 @@ final class DetailWebViewController: NSViewController {
|
||||
// See bug #901.
|
||||
webView.isHidden = true
|
||||
waitingForFirstReload = true
|
||||
|
||||
|
||||
webInspectorEnabled = AppDefaults.shared.webInspectorEnabled
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webInspectorEnabledDidChange(_:)), name: .WebInspectorEnabledDidChange, object: nil)
|
||||
|
||||
@@ -110,7 +110,7 @@ final class DetailWebViewController: NSViewController {
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
@@ -122,24 +122,24 @@ final class DetailWebViewController: NSViewController {
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
|
||||
|
||||
@objc func userDefaultsDidChange(_ note: Notification) {
|
||||
if articleTextSize != AppDefaults.shared.articleTextSize {
|
||||
articleTextSize = AppDefaults.shared.articleTextSize
|
||||
reloadHTMLMaintainingScrollPosition()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func currentArticleThemeDidChangeNotification(_ note: Notification) {
|
||||
reloadHTMLMaintainingScrollPosition()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Media Functions
|
||||
|
||||
|
||||
func stopMediaPlayback() {
|
||||
webView.evaluateJavaScript("stopMediaPlayback();")
|
||||
}
|
||||
|
||||
|
||||
// MARK: Scrolling
|
||||
|
||||
func canScrollDown(_ completion: @escaping (Bool) -> Void) {
|
||||
@@ -163,12 +163,12 @@ final class DetailWebViewController: NSViewController {
|
||||
}
|
||||
|
||||
// MARK: State Restoration
|
||||
|
||||
func saveState(to state: inout [AnyHashable : Any]) {
|
||||
|
||||
func saveState(to state: inout [AnyHashable: Any]) {
|
||||
state[UserInfoKey.isShowingExtractedArticle] = isShowingExtractedArticle
|
||||
state[UserInfoKey.articleWindowScrollY] = windowScrollY
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ArticleIconSchemeHandlerDelegate
|
||||
@@ -228,7 +228,7 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
||||
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
|
||||
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
// See note in viewDidLoad()
|
||||
if waitingForFirstReload {
|
||||
@@ -250,7 +250,7 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
||||
}
|
||||
|
||||
// WKUIDelegate
|
||||
|
||||
|
||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
||||
// This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One
|
||||
// example where this is used is in YouTube's embedded video player when a user clicks on the video's title
|
||||
@@ -270,18 +270,18 @@ private extension DetailWebViewController {
|
||||
|
||||
func reloadArticleImage() {
|
||||
guard let article = article else { return }
|
||||
|
||||
|
||||
var components = URLComponents()
|
||||
components.scheme = ArticleRenderer.imageIconScheme
|
||||
components.path = article.articleID
|
||||
|
||||
|
||||
if let imageSrc = components.string {
|
||||
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func reloadHTMLMaintainingScrollPosition() {
|
||||
fetchScrollInfo() { scrollInfo in
|
||||
fetchScrollInfo { scrollInfo in
|
||||
self.windowScrollY = scrollInfo?.offsetY
|
||||
self.reloadHTML()
|
||||
}
|
||||
@@ -289,7 +289,7 @@ private extension DetailWebViewController {
|
||||
|
||||
func reloadHTML() {
|
||||
delegate?.mouseDidExit(self)
|
||||
|
||||
|
||||
let theme = ArticleThemesManager.shared.currentTheme
|
||||
let rendering: ArticleRenderer.Rendering
|
||||
|
||||
@@ -305,14 +305,14 @@ private extension DetailWebViewController {
|
||||
case .extracted(let article, let extractedArticle, _):
|
||||
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme)
|
||||
}
|
||||
|
||||
|
||||
let substitutions = [
|
||||
"title": rendering.title,
|
||||
"baseURL": rendering.baseURL,
|
||||
"style": rendering.style,
|
||||
"body": rendering.html
|
||||
]
|
||||
|
||||
|
||||
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
|
||||
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
|
||||
}
|
||||
@@ -320,7 +320,7 @@ private extension DetailWebViewController {
|
||||
func fetchScrollInfo(_ completion: @escaping (ScrollInfo?) -> Void) {
|
||||
let javascriptString = "var x = {contentHeight: document.body.scrollHeight, offsetY: window.pageYOffset}; x"
|
||||
|
||||
webView.evaluateJavaScript(javascriptString) { (info, error) in
|
||||
webView.evaluateJavaScript(javascriptString) { (info, _) in
|
||||
guard let info = info as? [String: Any] else {
|
||||
completion(nil)
|
||||
return
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
|
||||
final class IconView: NSView {
|
||||
|
||||
var iconImage: IconImage? = nil {
|
||||
var iconImage: IconImage? {
|
||||
didSet {
|
||||
if iconImage !== oldValue {
|
||||
imageView.image = iconImage?.image
|
||||
@@ -36,7 +36,7 @@ final class IconView: NSView {
|
||||
}
|
||||
|
||||
private var isDiscernable = true
|
||||
|
||||
|
||||
override var isFlipped: Bool {
|
||||
return true
|
||||
}
|
||||
@@ -76,7 +76,7 @@ final class IconView: NSView {
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
resizeSubviews(withOldSize: NSZeroSize)
|
||||
resizeSubviews(withOldSize: NSSize.zero)
|
||||
}
|
||||
|
||||
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
||||
@@ -89,7 +89,7 @@ final class IconView: NSView {
|
||||
|
||||
let color = NSApplication.shared.effectiveAppearance.isDarkMode ? IconView.darkBackgroundColor : IconView.lightBackgroundColor
|
||||
color.set()
|
||||
let r = NSIntersectionRect(dirtyRect, bounds)
|
||||
let r = dirtyRect.intersection(bounds)
|
||||
r.fill()
|
||||
}
|
||||
}
|
||||
@@ -104,9 +104,9 @@ private extension IconView {
|
||||
|
||||
func rectForImageView() -> NSRect {
|
||||
guard !(iconImage?.isSymbol ?? false) else {
|
||||
return NSMakeRect(0.0, 0.0, bounds.size.width, bounds.size.height)
|
||||
return NSRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height)
|
||||
}
|
||||
|
||||
|
||||
guard let image = iconImage?.image else {
|
||||
return NSRect.zero
|
||||
}
|
||||
@@ -116,22 +116,21 @@ private extension IconView {
|
||||
if imageSize.height == imageSize.width {
|
||||
if imageSize.height >= viewSize.height * 0.75 {
|
||||
// Close enough to viewSize to scale up the image.
|
||||
return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height)
|
||||
return NSRect(x: 0.0, y: 0.0, width: viewSize.width, height: viewSize.height)
|
||||
}
|
||||
let offset = floor((viewSize.height - imageSize.height) / 2.0)
|
||||
return NSMakeRect(offset, offset, imageSize.width, imageSize.height)
|
||||
}
|
||||
else if imageSize.height > imageSize.width {
|
||||
return NSRect(x: offset, y: offset, width: imageSize.width, height: imageSize.height)
|
||||
} else if imageSize.height > imageSize.width {
|
||||
let factor = viewSize.height / imageSize.height
|
||||
let width = imageSize.width * factor
|
||||
let originX = floor((viewSize.width - width) / 2.0)
|
||||
return NSMakeRect(originX, 0.0, width, viewSize.height)
|
||||
return NSRect(x: originX, y: 0.0, width: width, height: viewSize.height)
|
||||
}
|
||||
|
||||
// Wider than tall: imageSize.width > imageSize.height
|
||||
let factor = viewSize.width / imageSize.width
|
||||
let height = imageSize.height * factor
|
||||
let originY = floor((viewSize.height - height) / 2.0)
|
||||
return NSMakeRect(0.0, originY, viewSize.width, height)
|
||||
return NSRect(x: 0.0, y: originY, width: viewSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,3 @@ final class MainWindowKeyboardHandler: KeyboardDelegate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
class LegacyArticleExtractorButton: NSButton {
|
||||
|
||||
|
||||
var isError = false {
|
||||
didSet {
|
||||
if isError != oldValue {
|
||||
@@ -17,7 +17,7 @@ class LegacyArticleExtractorButton: NSButton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isInProgress = false {
|
||||
didSet {
|
||||
if isInProgress != oldValue {
|
||||
@@ -25,12 +25,12 @@ class LegacyArticleExtractorButton: NSButton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
wantsLayer = true
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
wantsLayer = true
|
||||
@@ -38,7 +38,7 @@ class LegacyArticleExtractorButton: NSButton {
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
super.draw(dirtyRect)
|
||||
|
||||
|
||||
guard let hostedLayer = self.layer else {
|
||||
return
|
||||
}
|
||||
@@ -52,7 +52,7 @@ class LegacyArticleExtractorButton: NSButton {
|
||||
}
|
||||
|
||||
let opacity: Float = isEnabled ? 1.0 : 0.5
|
||||
|
||||
|
||||
switch true {
|
||||
case isError:
|
||||
addImageSublayer(to: hostedLayer, image: AppAssets.legacyArticleExtractorError, opacity: opacity)
|
||||
@@ -70,42 +70,42 @@ class LegacyArticleExtractorButton: NSButton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func makeLayerForImage(_ image: NSImage) -> CALayer {
|
||||
let imageLayer = CALayer()
|
||||
imageLayer.bounds = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
|
||||
imageLayer.position = CGPoint(x: bounds.midX, y: floor(bounds.midY))
|
||||
return imageLayer
|
||||
}
|
||||
|
||||
|
||||
private func addImageSublayer(to hostedLayer: CALayer, image: NSImage, opacity: Float = 1.0) {
|
||||
let imageLayer = makeLayerForImage(image)
|
||||
imageLayer.contents = image
|
||||
imageLayer.opacity = opacity
|
||||
hostedLayer.addSublayer(imageLayer)
|
||||
}
|
||||
|
||||
|
||||
private func addAnimatedSublayer(to hostedLayer: CALayer) {
|
||||
let imageProgress1 = AppAssets.legacyArticleExtractorProgress1
|
||||
let imageProgress2 = AppAssets.legacyArticleExtractorProgress2
|
||||
let imageProgress3 = AppAssets.legacyArticleExtractorProgress3
|
||||
let imageProgress4 = AppAssets.legacyArticleExtractorProgress4
|
||||
let images = [imageProgress1, imageProgress2, imageProgress3, imageProgress4, imageProgress3, imageProgress2, imageProgress1]
|
||||
|
||||
|
||||
let imageLayer = CALayer()
|
||||
imageLayer.bounds = CGRect(x: 0, y: 0, width: imageProgress1?.size.width ?? 0, height: imageProgress1?.size.height ?? 0)
|
||||
imageLayer.position = CGPoint(x: bounds.midX, y: floor(bounds.midY))
|
||||
|
||||
|
||||
hostedLayer.addSublayer(imageLayer)
|
||||
|
||||
|
||||
let animation = CAKeyframeAnimation(keyPath: "contents")
|
||||
animation.calculationMode = CAAnimationCalculationMode.linear
|
||||
animation.keyTimes = [0, 0.16, 0.32, 0.50, 0.66, 0.82, 1]
|
||||
animation.duration = 2
|
||||
animation.values = images as [Any]
|
||||
animation.repeatCount = HUGE
|
||||
|
||||
|
||||
imageLayer.add(animation, forKey: "contents")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,14 +16,14 @@ enum TimelineSourceMode {
|
||||
case regular, search
|
||||
}
|
||||
|
||||
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
class MainWindowController: NSWindowController, NSUserInterfaceValidations {
|
||||
|
||||
@IBOutlet weak var articleThemePopUpButton: NSPopUpButton?
|
||||
|
||||
|
||||
private var activityManager = ActivityManager()
|
||||
|
||||
private var isShowingExtractedArticle = false
|
||||
private var articleExtractor: ArticleExtractor? = nil
|
||||
private var articleExtractor: ArticleExtractor?
|
||||
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
|
||||
|
||||
private let windowAutosaveName = NSWindow.FrameAutosaveName("MainWindow")
|
||||
@@ -36,7 +36,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
}
|
||||
return selectedObjects.first
|
||||
}
|
||||
|
||||
|
||||
private var shareToolbarItem: NSToolbarItem? {
|
||||
return window?.toolbar?.existingItem(withIdentifier: .share)
|
||||
}
|
||||
@@ -45,17 +45,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
private var sidebarViewController: SidebarViewController?
|
||||
private var timelineContainerViewController: TimelineContainerViewController?
|
||||
private var detailViewController: DetailViewController?
|
||||
private var currentSearchField: NSSearchField? = nil
|
||||
private var currentSearchField: NSSearchField?
|
||||
private let articleThemeMenuToolbarItem = NSMenuToolbarItem(itemIdentifier: .articleThemeMenu)
|
||||
private var searchString: String? = nil
|
||||
private var lastSentSearchString: String? = nil
|
||||
private var searchString: String?
|
||||
private var lastSentSearchString: String?
|
||||
private var timelineSourceMode: TimelineSourceMode = .regular {
|
||||
didSet {
|
||||
timelineContainerViewController?.showTimeline(for: timelineSourceMode)
|
||||
detailViewController?.showDetail(for: timelineSourceMode)
|
||||
}
|
||||
}
|
||||
private var searchSmartFeed: SmartFeed? = nil
|
||||
private var searchSmartFeed: SmartFeed?
|
||||
private var restoreArticleWindowScrollY: CGFloat?
|
||||
|
||||
// MARK: - NSWindowController
|
||||
@@ -97,10 +97,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil)
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateWindowTitle()
|
||||
}
|
||||
@@ -115,14 +115,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
|
||||
func handle(_ response: UNNotificationResponse) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any] else { return }
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
}
|
||||
|
||||
func handle(_ activity: NSUserActivity) {
|
||||
guard let userInfo = activity.userInfo else { return }
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any] else { return }
|
||||
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
||||
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
||||
}
|
||||
@@ -131,14 +131,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
AppDefaults.shared.windowState = savableState()
|
||||
window?.saveFrame(usingName: windowAutosaveName)
|
||||
}
|
||||
|
||||
|
||||
func restoreStateFromUserDefaults() {
|
||||
if let state = AppDefaults.shared.windowState {
|
||||
restoreState(from: state)
|
||||
window?.setFrameUsingName(windowAutosaveName, force: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc func refreshProgressDidChange(_ note: Notification) {
|
||||
@@ -148,7 +148,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
updateWindowTitleIfNecessary(note.object)
|
||||
}
|
||||
|
||||
|
||||
@objc func displayNameDidChange(_ note: Notification) {
|
||||
updateWindowTitleIfNecessary(note.object)
|
||||
}
|
||||
@@ -162,21 +162,21 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
}
|
||||
|
||||
private func updateWindowTitleIfNecessary(_ noteObject: Any?) {
|
||||
|
||||
|
||||
if let folder = currentFeedOrFolder as? Folder, let noteObject = noteObject as? Folder {
|
||||
if folder == noteObject {
|
||||
updateWindowTitle()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let feed = currentFeedOrFolder as? Feed, let noteObject = noteObject as? Feed {
|
||||
if feed == noteObject {
|
||||
updateWindowTitle()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we don't recognize the changed object, we will test it for identity instead
|
||||
// of equality. This works well for us if the window title is displaying a
|
||||
// PsuedoFeed object.
|
||||
@@ -185,28 +185,28 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
updateWindowTitle()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Toolbar
|
||||
|
||||
|
||||
@objc func makeToolbarValidate() {
|
||||
|
||||
|
||||
window?.toolbar?.validateVisibleItems()
|
||||
}
|
||||
|
||||
// MARK: - NSUserInterfaceValidations
|
||||
|
||||
|
||||
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||
|
||||
|
||||
if item.action == #selector(copyArticleURL(_:)) {
|
||||
return canCopyArticleURL()
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(copyExternalURL(_:)) {
|
||||
return canCopyExternalURL()
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(openArticleInBrowser(_:)) {
|
||||
if let item = item as? NSMenuItem, item.keyEquivalentModifierMask.contains(.shift) {
|
||||
item.title = Browser.titleForOpenInBrowserInverted
|
||||
@@ -214,11 +214,11 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
|
||||
return currentLink != nil
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(nextUnread(_:)) {
|
||||
return canGoToNextUnread(wrappingToTop: true)
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(markAllAsRead(_:)) {
|
||||
return canMarkAllAsRead()
|
||||
}
|
||||
@@ -242,7 +242,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
if item.action == #selector(toggleArticleExtractor(_:)) {
|
||||
return validateToggleArticleExtractor(item)
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(toolbarShowShareMenu(_:)) {
|
||||
return canShowShareMenu()
|
||||
}
|
||||
@@ -283,7 +283,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
return
|
||||
}
|
||||
detailViewController.canScrollUp { (canScroll) in
|
||||
if (canScroll) {
|
||||
if canScroll {
|
||||
NSCursor.setHiddenUntilMouseMoves(true)
|
||||
detailViewController.scrollPageUp(sender)
|
||||
}
|
||||
@@ -306,7 +306,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
@IBAction func openArticleInBrowser(_ sender: Any?) {
|
||||
if let link = currentLink {
|
||||
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func openInBrowser(_ sender: Any?) {
|
||||
@@ -340,8 +340,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
// TODO: handle search mode
|
||||
if timelineViewController.canGoToNextUnread(wrappingToTop: false) {
|
||||
goToNextUnreadInTimeline(wrappingToTop: false)
|
||||
}
|
||||
else if sidebarViewController.canGoToNextUnread(wrappingToTop: true) {
|
||||
} else if sidebarViewController.canGoToNextUnread(wrappingToTop: true) {
|
||||
sidebarViewController.goToNextUnread(wrappingToTop: true)
|
||||
|
||||
// If we ended up on the same timelineViewController, we may need to wrap
|
||||
@@ -373,7 +372,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
}
|
||||
|
||||
@IBAction func toggleArticleExtractor(_ sender: Any?) {
|
||||
|
||||
|
||||
guard let currentLink = currentLink, let article = oneSelectedArticle else {
|
||||
return
|
||||
}
|
||||
@@ -381,12 +380,12 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
defer {
|
||||
makeToolbarValidate()
|
||||
}
|
||||
|
||||
|
||||
if articleExtractor?.state == .failedToParse {
|
||||
startArticleExtractorForCurrentLink()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard articleExtractor?.state != .processing else {
|
||||
articleExtractor?.cancel()
|
||||
articleExtractor = nil
|
||||
@@ -394,13 +393,13 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard !isShowingExtractedArticle else {
|
||||
isShowingExtractedArticle = false
|
||||
detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article {
|
||||
if currentLink == articleExtractor.articleLink {
|
||||
isShowingExtractedArticle = true
|
||||
@@ -410,11 +409,11 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
} else {
|
||||
startArticleExtractorForCurrentLink()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
|
||||
currentTimelineViewController?.markAllAsRead() {
|
||||
currentTimelineViewController?.markAllAsRead {
|
||||
self.nextUnread(sender)
|
||||
}
|
||||
}
|
||||
@@ -432,7 +431,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
@IBAction func markOlderArticlesAsRead(_ sender: Any?) {
|
||||
currentTimelineViewController?.markOlderArticlesRead()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func markAboveArticlesAsRead(_ sender: Any?) {
|
||||
currentTimelineViewController?.markAboveArticlesRead()
|
||||
}
|
||||
@@ -452,7 +451,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
@IBAction func navigateToDetail(_ sender: Any?) {
|
||||
detailViewController?.focus()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func goToPreviousSubscription(_ sender: Any?) {
|
||||
sidebarViewController?.outlineView.selectPreviousRow(sender)
|
||||
}
|
||||
@@ -504,31 +503,31 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
@IBAction func cleanUp(_ sender: Any?) {
|
||||
timelineContainerViewController?.cleanUp()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggleReadFeedsFilter(_ sender: Any?) {
|
||||
sidebarViewController?.toggleReadFilter()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggleReadArticlesFilter(_ sender: Any?) {
|
||||
timelineContainerViewController?.toggleReadFilter()
|
||||
}
|
||||
|
||||
|
||||
@objc func selectArticleTheme(_ menuItem: NSMenuItem) {
|
||||
ArticleThemesManager.shared.currentThemeName = menuItem.title
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: NSWindowDelegate
|
||||
|
||||
extension MainWindowController: NSWindowDelegate {
|
||||
|
||||
|
||||
func window(_ window: NSWindow, willEncodeRestorableState coder: NSCoder) {
|
||||
coder.encode(savableState(), forKey: UserInfoKey.windowState)
|
||||
}
|
||||
|
||||
func window(_ window: NSWindow, didDecodeRestorableState coder: NSCoder) {
|
||||
guard let state = try? coder.decodeTopLevelObject(forKey: UserInfoKey.windowState) as? [AnyHashable : Any] else { return }
|
||||
guard let state = try? coder.decodeTopLevelObject(forKey: UserInfoKey.windowState) as? [AnyHashable: Any] else { return }
|
||||
restoreState(from: state)
|
||||
}
|
||||
|
||||
@@ -536,7 +535,7 @@ extension MainWindowController: NSWindowDelegate {
|
||||
detailViewController?.stopMediaPlayback()
|
||||
appDelegate.removeMainWindow(self)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - SidebarDelegate
|
||||
@@ -563,11 +562,11 @@ extension MainWindowController: SidebarDelegate {
|
||||
}
|
||||
return timelineViewController.unreadCount
|
||||
}
|
||||
|
||||
|
||||
func sidebarInvalidatedRestorationState(_: SidebarViewController) {
|
||||
invalidateRestorableState()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - TimelineContainerViewControllerDelegate
|
||||
@@ -576,12 +575,12 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
|
||||
|
||||
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
|
||||
activityManager.invalidateReading()
|
||||
|
||||
|
||||
articleExtractor?.cancel()
|
||||
articleExtractor = nil
|
||||
isShowingExtractedArticle = false
|
||||
makeToolbarValidate()
|
||||
|
||||
|
||||
let detailState: DetailState
|
||||
if let articles = articles {
|
||||
if articles.count == 1 {
|
||||
@@ -606,11 +605,11 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
|
||||
func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed) {
|
||||
sidebarViewController?.selectFeed(feed)
|
||||
}
|
||||
|
||||
|
||||
func timelineInvalidatedRestorationState(_: TimelineContainerViewController) {
|
||||
invalidateRestorableState()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSSearchFieldDelegate
|
||||
@@ -683,11 +682,11 @@ extension MainWindowController: NSSearchFieldDelegate {
|
||||
// MARK: - ArticleExtractorDelegate
|
||||
|
||||
extension MainWindowController: ArticleExtractorDelegate {
|
||||
|
||||
|
||||
func articleExtractionDidFail(with: Error) {
|
||||
makeToolbarValidate()
|
||||
}
|
||||
|
||||
|
||||
func articleExtractionDidComplete(extractedArticle: ExtractedArticle) {
|
||||
if let article = oneSelectedArticle, articleExtractor?.state != .cancelled {
|
||||
isShowingExtractedArticle = true
|
||||
@@ -697,7 +696,7 @@ extension MainWindowController: ArticleExtractorDelegate {
|
||||
makeToolbarValidate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Scripting Access
|
||||
@@ -710,7 +709,7 @@ extension MainWindowController: ArticleExtractorDelegate {
|
||||
but for now, we'll keep the stratification of visibility
|
||||
*/
|
||||
|
||||
extension MainWindowController : ScriptingMainWindowController {
|
||||
extension MainWindowController: ScriptingMainWindowController {
|
||||
|
||||
internal var scriptingCurrentArticle: Article? {
|
||||
return self.oneSelectedArticle
|
||||
@@ -827,7 +826,7 @@ extension MainWindowController: NSToolbarDelegate {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
[
|
||||
NSToolbarItem.Identifier.toggleSidebar,
|
||||
@@ -929,7 +928,7 @@ private extension MainWindowController {
|
||||
var detailSplitViewItem: NSSplitViewItem? {
|
||||
return splitViewController?.splitViewItems[2]
|
||||
}
|
||||
|
||||
|
||||
var selectedArticles: [Article]? {
|
||||
return currentTimelineViewController?.selectedArticles
|
||||
}
|
||||
@@ -946,9 +945,9 @@ private extension MainWindowController {
|
||||
}
|
||||
|
||||
// MARK: - State Restoration
|
||||
|
||||
func savableState() -> [AnyHashable : Any] {
|
||||
var state = [AnyHashable : Any]()
|
||||
|
||||
func savableState() -> [AnyHashable: Any] {
|
||||
var state = [AnyHashable: Any]()
|
||||
state[UserInfoKey.windowFullScreenState] = window?.styleMask.contains(.fullScreen) ?? false
|
||||
saveSplitViewState(to: &state)
|
||||
sidebarViewController?.saveState(to: &state)
|
||||
@@ -957,56 +956,56 @@ private extension MainWindowController {
|
||||
return state
|
||||
}
|
||||
|
||||
func restoreState(from state: [AnyHashable : Any]) {
|
||||
func restoreState(from state: [AnyHashable: Any]) {
|
||||
if let fullScreen = state[UserInfoKey.windowFullScreenState] as? Bool, fullScreen {
|
||||
window?.toggleFullScreen(self)
|
||||
}
|
||||
restoreSplitViewState(from: state)
|
||||
|
||||
|
||||
sidebarViewController?.restoreState(from: state)
|
||||
|
||||
|
||||
let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat
|
||||
restoreArticleWindowScrollY = articleWindowScrollY
|
||||
timelineContainerViewController?.restoreState(from: state)
|
||||
|
||||
|
||||
let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false
|
||||
if isShowingExtractedArticle {
|
||||
restoreArticleWindowScrollY = articleWindowScrollY
|
||||
startArticleExtractorForCurrentLink()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Command Validation
|
||||
|
||||
|
||||
func canCopyArticleURL() -> Bool {
|
||||
return currentLink != nil
|
||||
}
|
||||
|
||||
|
||||
func canCopyExternalURL() -> Bool {
|
||||
return oneSelectedArticle?.externalLink != nil && oneSelectedArticle?.externalLink != currentLink
|
||||
}
|
||||
|
||||
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
|
||||
|
||||
|
||||
guard let timelineViewController = currentTimelineViewController, let sidebarViewController = sidebarViewController else {
|
||||
return false
|
||||
}
|
||||
// TODO: handle search mode
|
||||
return timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) || sidebarViewController.canGoToNextUnread(wrappingToTop: wrapping)
|
||||
}
|
||||
|
||||
|
||||
func canMarkAllAsRead() -> Bool {
|
||||
|
||||
|
||||
return currentTimelineViewController?.canMarkAllAsRead() ?? false
|
||||
}
|
||||
|
||||
|
||||
func validateToggleRead(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||
|
||||
let validationStatus = currentTimelineViewController?.markReadCommandStatus() ?? .canDoNothing
|
||||
let markingRead: Bool
|
||||
let result: Bool
|
||||
|
||||
|
||||
switch validationStatus {
|
||||
case .canMark:
|
||||
markingRead = true
|
||||
@@ -1018,21 +1017,21 @@ private extension MainWindowController {
|
||||
markingRead = true
|
||||
result = false
|
||||
}
|
||||
|
||||
|
||||
let commandName = markingRead ? NSLocalizedString("Mark as Read", comment: "Command") : NSLocalizedString("Mark as Unread", comment: "Command")
|
||||
|
||||
|
||||
if let toolbarItem = item as? NSToolbarItem {
|
||||
toolbarItem.toolTip = commandName
|
||||
}
|
||||
|
||||
|
||||
if let menuItem = item as? NSMenuItem {
|
||||
menuItem.title = commandName
|
||||
}
|
||||
|
||||
|
||||
if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
||||
button.image = markingRead ? AppAssets.readClosedImage : AppAssets.readOpenImage
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1076,7 +1075,7 @@ private extension MainWindowController {
|
||||
func canMarkBelowArticlesAsRead() -> Bool {
|
||||
return currentTimelineViewController?.canMarkBelowArticlesAsRead() ?? false
|
||||
}
|
||||
|
||||
|
||||
func canShowShareMenu() -> Bool {
|
||||
|
||||
guard let selectedArticles = selectedArticles else {
|
||||
@@ -1119,7 +1118,7 @@ private extension MainWindowController {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
func validateCleanUp(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||
return timelineContainerViewController?.isCleanUpAvailable ?? false
|
||||
}
|
||||
@@ -1159,7 +1158,7 @@ private extension MainWindowController {
|
||||
button.image = AppAssets.filterInactive
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1192,7 +1191,7 @@ private extension MainWindowController {
|
||||
window?.subtitle = ""
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func setSubtitle(_ count: Int) {
|
||||
let localizedLabel = NSLocalizedString("%d unread", comment: "Unread")
|
||||
let formattedLabel = NSString.localizedStringWithFormat(localizedLabel as NSString, count)
|
||||
@@ -1235,16 +1234,16 @@ private extension MainWindowController {
|
||||
}
|
||||
}
|
||||
|
||||
func saveSplitViewState(to state: inout [AnyHashable : Any]) {
|
||||
func saveSplitViewState(to state: inout [AnyHashable: Any]) {
|
||||
guard let splitView = splitViewController?.splitView else {
|
||||
return
|
||||
}
|
||||
|
||||
let widths = splitView.arrangedSubviews.map{ Int(floor($0.frame.width)) }
|
||||
let widths = splitView.arrangedSubviews.map { Int(floor($0.frame.width)) }
|
||||
state[MainWindowController.mainWindowWidthsStateKey] = widths
|
||||
}
|
||||
|
||||
func restoreSplitViewState(from state: [AnyHashable : Any]) {
|
||||
func restoreSplitViewState(from state: [AnyHashable: Any]) {
|
||||
guard let splitView = splitViewController?.splitView,
|
||||
let widths = state[MainWindowController.mainWindowWidthsStateKey] as? [Int],
|
||||
widths.count == 3,
|
||||
@@ -1269,13 +1268,13 @@ private extension MainWindowController {
|
||||
func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String) -> NSToolbarItem {
|
||||
let toolbarItem = RSToolbarItem(itemIdentifier: itemIdentifier)
|
||||
toolbarItem.autovalidates = true
|
||||
|
||||
|
||||
let button = NSButton()
|
||||
button.bezelStyle = .texturedRounded
|
||||
button.image = image
|
||||
button.imageScaling = .scaleProportionallyDown
|
||||
button.action = Selector((selector))
|
||||
|
||||
|
||||
toolbarItem.view = button
|
||||
toolbarItem.toolTip = title
|
||||
toolbarItem.label = title
|
||||
@@ -1284,23 +1283,23 @@ private extension MainWindowController {
|
||||
|
||||
func buildNewSidebarItemMenu() -> NSMenu {
|
||||
let menu = NSMenu()
|
||||
|
||||
|
||||
let newFeedItem = NSMenuItem()
|
||||
newFeedItem.title = NSLocalizedString("New Feed…", comment: "New Feed")
|
||||
newFeedItem.action = Selector(("showAddFeedWindow:"))
|
||||
menu.addItem(newFeedItem)
|
||||
|
||||
|
||||
let newFolderFeedItem = NSMenuItem()
|
||||
newFolderFeedItem.title = NSLocalizedString("New Folder…", comment: "New Folder")
|
||||
newFolderFeedItem.action = Selector(("showAddFolderWindow:"))
|
||||
menu.addItem(newFolderFeedItem)
|
||||
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
func updateArticleThemeMenu() {
|
||||
let articleThemeMenu = NSMenu()
|
||||
|
||||
|
||||
let defaultThemeItem = NSMenuItem()
|
||||
defaultThemeItem.title = ArticleTheme.defaultTheme.name
|
||||
defaultThemeItem.action = #selector(selectArticleTheme(_:))
|
||||
@@ -1322,4 +1321,3 @@ private extension MainWindowController {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -141,4 +141,3 @@ extension NNW3Feed: OPMLRepresentable {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ private extension NNW3ImportController {
|
||||
panel.accessoryView = accessoryViewController.view
|
||||
panel.isAccessoryViewDisclosed = true
|
||||
panel.title = NSLocalizedString("Choose a Subscriptions.plist file:", comment: "NNW3 Import")
|
||||
|
||||
|
||||
panel.beginSheetModal(for: window) { modalResult in
|
||||
guard modalResult == .OK, let subscriptionsPlistURL = panel.url else {
|
||||
return
|
||||
|
||||
@@ -22,7 +22,7 @@ final class NNW3OpenPanelAccessoryViewController: NSViewController {
|
||||
}
|
||||
|
||||
// MARK: - NSViewController
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("NNW3OpenPanelAccessoryViewController.init(coder) not implemented by design.")
|
||||
}
|
||||
|
||||
@@ -14,24 +14,24 @@ class ExportOPMLWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var accountPopUpButton: NSPopUpButton!
|
||||
private weak var hostWindow: NSWindow?
|
||||
|
||||
|
||||
convenience init() {
|
||||
self.init(windowNibName: NSNib.Name("ExportOPMLSheet"))
|
||||
}
|
||||
|
||||
|
||||
override func windowDidLoad() {
|
||||
accountPopUpButton.removeAllItems()
|
||||
|
||||
let menu = NSMenu()
|
||||
accountPopUpButton.menu = menu
|
||||
|
||||
|
||||
for oneAccount in AccountManager.shared.sortedAccounts {
|
||||
|
||||
|
||||
let oneMenuItem = NSMenuItem()
|
||||
oneMenuItem.title = oneAccount.nameForDisplay
|
||||
oneMenuItem.representedObject = oneAccount
|
||||
menu.addItem(oneMenuItem)
|
||||
|
||||
|
||||
if oneAccount.accountID == AppDefaults.shared.exportOPMLAccountID {
|
||||
accountPopUpButton.select(oneMenuItem)
|
||||
}
|
||||
@@ -40,26 +40,26 @@ class ExportOPMLWindowController: NSWindowController {
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow) {
|
||||
|
||||
|
||||
self.hostWindow = hostWindow
|
||||
|
||||
|
||||
if AccountManager.shared.accounts.count == 1 {
|
||||
let account = AccountManager.shared.accounts.first!
|
||||
exportOPML(account: account)
|
||||
} else {
|
||||
hostWindow.beginSheet(window!)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func exportOPML(_ sender: Any) {
|
||||
|
||||
guard let menuItem = accountPopUpButton.selectedItem else {
|
||||
@@ -70,11 +70,11 @@ class ExportOPMLWindowController: NSWindowController {
|
||||
AppDefaults.shared.exportOPMLAccountID = account.accountID
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
exportOPML(account: account)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
func exportOPML(account: Account) {
|
||||
|
||||
|
||||
let panel = NSSavePanel()
|
||||
panel.allowedContentTypes = [UTType.opml]
|
||||
panel.allowsOtherFileTypes = false
|
||||
@@ -83,10 +83,10 @@ class ExportOPMLWindowController: NSWindowController {
|
||||
panel.nameFieldLabel = NSLocalizedString("Export to:", comment: "Export OPML")
|
||||
panel.message = NSLocalizedString("Choose a location for the exported OPML file.", comment: "Export OPML")
|
||||
panel.isExtensionHidden = false
|
||||
|
||||
|
||||
let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces)
|
||||
panel.nameFieldStringValue = "Subscriptions-\(accountName).opml"
|
||||
|
||||
|
||||
panel.beginSheetModal(for: hostWindow!) { result in
|
||||
if result == NSApplication.ModalResponse.OK, let url = panel.url {
|
||||
DispatchQueue.main.async {
|
||||
@@ -94,14 +94,13 @@ class ExportOPMLWindowController: NSWindowController {
|
||||
let opmlString = OPMLExporter.OPMLString(with: account, title: filename)
|
||||
do {
|
||||
try opmlString.write(to: url, atomically: true, encoding: String.Encoding.utf8)
|
||||
}
|
||||
catch let error as NSError {
|
||||
} catch let error as NSError {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -14,71 +14,71 @@ class ImportOPMLWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var accountPopUpButton: NSPopUpButton!
|
||||
private weak var hostWindow: NSWindow?
|
||||
|
||||
|
||||
convenience init() {
|
||||
self.init(windowNibName: NSNib.Name("ImportOPMLSheet"))
|
||||
}
|
||||
|
||||
|
||||
override func windowDidLoad() {
|
||||
accountPopUpButton.removeAllItems()
|
||||
|
||||
|
||||
let menu = NSMenu()
|
||||
accountPopUpButton.menu = menu
|
||||
|
||||
for oneAccount in AccountManager.shared.sortedActiveAccounts {
|
||||
|
||||
|
||||
if oneAccount.behaviors.contains(.disallowOPMLImports) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
let oneMenuItem = NSMenuItem()
|
||||
oneMenuItem.title = oneAccount.nameForDisplay
|
||||
oneMenuItem.representedObject = oneAccount
|
||||
menu.addItem(oneMenuItem)
|
||||
|
||||
|
||||
if oneAccount.accountID == AppDefaults.shared.importOPMLAccountID {
|
||||
accountPopUpButton.select(oneMenuItem)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: API
|
||||
|
||||
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow) {
|
||||
|
||||
|
||||
self.hostWindow = hostWindow
|
||||
|
||||
|
||||
if AccountManager.shared.activeAccounts.count == 1 {
|
||||
let account = AccountManager.shared.activeAccounts.first!
|
||||
importOPML(account: account)
|
||||
} else {
|
||||
hostWindow.beginSheet(window!)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func importOPML(_ sender: Any) {
|
||||
|
||||
guard let menuItem = accountPopUpButton.selectedItem else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let account = menuItem.representedObject as! Account
|
||||
AppDefaults.shared.importOPMLAccountID = account.accountID
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
importOPML(account: account)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
func importOPML(account: Account) {
|
||||
|
||||
|
||||
let panel = NSOpenPanel()
|
||||
panel.canDownloadUbiquitousContents = true
|
||||
panel.canResolveUbiquitousConflicts = true
|
||||
@@ -88,7 +88,7 @@ class ImportOPMLWindowController: NSWindowController {
|
||||
panel.resolvesAliases = true
|
||||
panel.allowedContentTypes = [UTType.opml, UTType.xml]
|
||||
panel.allowsOtherFileTypes = false
|
||||
|
||||
|
||||
panel.beginSheetModal(for: hostWindow!) { modalResult in
|
||||
if modalResult == NSApplication.ModalResponse.OK, let url = panel.url {
|
||||
account.importOPML(url) { result in
|
||||
@@ -101,8 +101,7 @@ class ImportOPMLWindowController: NSWindowController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import AppKit
|
||||
@objc final class SharingServiceDelegate: NSObject, NSSharingServiceDelegate {
|
||||
|
||||
weak var window: NSWindow?
|
||||
|
||||
|
||||
init(_ window: NSWindow?) {
|
||||
self.window = window
|
||||
}
|
||||
@@ -24,9 +24,9 @@ import AppKit
|
||||
}
|
||||
.joined(separator: ", ")
|
||||
}
|
||||
|
||||
|
||||
func sharingService(_ sharingService: NSSharingService, sourceWindowForShareItems items: [Any], sharingContentScope: UnsafeMutablePointer<NSSharingService.SharingContentScope>) -> NSWindow? {
|
||||
return window
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ import AppKit
|
||||
import RSCore
|
||||
|
||||
@objc final class SharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate {
|
||||
|
||||
|
||||
private let sharingServiceDelegate: SharingServiceDelegate
|
||||
|
||||
|
||||
init(_ window: NSWindow?) {
|
||||
sharingServiceDelegate = SharingServiceDelegate(window)
|
||||
}
|
||||
|
||||
|
||||
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
|
||||
let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" }
|
||||
return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items)
|
||||
@@ -34,7 +34,7 @@ import RSCore
|
||||
guard let object = items.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
guard sendToCommand.canSendObject(object, selectedText: nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import RSCore
|
||||
import Account
|
||||
import RSTree
|
||||
|
||||
class SidebarCell : NSTableCellView {
|
||||
class SidebarCell: NSTableCellView {
|
||||
|
||||
var iconImage: IconImage? {
|
||||
didSet {
|
||||
@@ -73,14 +73,14 @@ class SidebarCell : NSTableCellView {
|
||||
}()
|
||||
|
||||
private let faviconImageView = IconView()
|
||||
private let unreadCountView = UnreadCountView(frame: NSZeroRect)
|
||||
private let unreadCountView = UnreadCountView(frame: NSRect.zero)
|
||||
|
||||
override var backgroundStyle: NSView.BackgroundStyle {
|
||||
didSet {
|
||||
updateFaviconImage()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override var isFlipped: Bool {
|
||||
return true
|
||||
}
|
||||
@@ -89,8 +89,8 @@ class SidebarCell : NSTableCellView {
|
||||
super.init(frame: frameRect)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
@@ -99,7 +99,7 @@ class SidebarCell : NSTableCellView {
|
||||
if let cellAppearance = cellAppearance {
|
||||
titleView.font = cellAppearance.textFieldFont
|
||||
}
|
||||
resizeSubviews(withOldSize: NSZeroSize)
|
||||
resizeSubviews(withOldSize: NSSize.zero)
|
||||
}
|
||||
|
||||
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
||||
@@ -138,10 +138,10 @@ private extension SidebarCell {
|
||||
titleView.setFrame(ifNotEqualTo: layout.titleRect)
|
||||
unreadCountView.setFrame(ifNotEqualTo: layout.unreadCountRect)
|
||||
}
|
||||
|
||||
|
||||
func updateFaviconImage() {
|
||||
var updatedIconImage = iconImage
|
||||
|
||||
|
||||
if let iconImage = iconImage, iconImage.isSymbol {
|
||||
if backgroundStyle != .normal {
|
||||
let image = iconImage.image.tinted(with: .white)
|
||||
@@ -163,6 +163,5 @@ private extension SidebarCell {
|
||||
faviconImageView.iconImage = nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@ struct SidebarCellAppearance: Equatable {
|
||||
imageSize = CGSize(width: 19, height: 19)
|
||||
textFieldFontSize = 13
|
||||
}
|
||||
|
||||
|
||||
self.textFieldFont = NSFont.systemFont(ofSize: textFieldFontSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ struct SidebarCellLayout {
|
||||
let faviconRect: CGRect
|
||||
let titleRect: CGRect
|
||||
let unreadCountRect: CGRect
|
||||
|
||||
|
||||
init(appearance: SidebarCellAppearance, cellSize: NSSize, shouldShowImage: Bool, textField: NSTextField, unreadCountView: UnreadCountView) {
|
||||
|
||||
let bounds = NSRect(x: 0.0, y: 0.0, width: floor(cellSize.width), height: floor(cellSize.height))
|
||||
@@ -32,7 +32,7 @@ struct SidebarCellLayout {
|
||||
|
||||
var rTextField = NSRect(x: 0.0, y: 0.0, width: textFieldSize.width, height: textFieldSize.height)
|
||||
if shouldShowImage {
|
||||
rTextField.origin.x = NSMaxX(rFavicon) + appearance.imageMarginRight
|
||||
rTextField.origin.x = rFavicon.maxX + appearance.imageMarginRight
|
||||
}
|
||||
rTextField = rTextField.centeredVertically(in: bounds)
|
||||
|
||||
@@ -42,17 +42,17 @@ struct SidebarCellLayout {
|
||||
var rUnread = NSRect.zero
|
||||
if !unreadCountIsHidden {
|
||||
rUnread.size = unreadCountSize
|
||||
rUnread.origin.x = NSMaxX(bounds) - unreadCountSize.width
|
||||
rUnread.origin.x = bounds.maxX - unreadCountSize.width
|
||||
rUnread = rUnread.centeredVertically(in: bounds)
|
||||
let textFieldMaxX = NSMinX(rUnread) - appearance.unreadCountMarginLeft
|
||||
if NSMaxX(rTextField) > textFieldMaxX {
|
||||
rTextField.size.width = textFieldMaxX - NSMinX(rTextField)
|
||||
let textFieldMaxX = rUnread.minX - appearance.unreadCountMarginLeft
|
||||
if rTextField.maxX > textFieldMaxX {
|
||||
rTextField.size.width = textFieldMaxX - rTextField.minX
|
||||
}
|
||||
}
|
||||
self.unreadCountRect = rUnread
|
||||
|
||||
if NSMaxX(rTextField) > NSMaxX(bounds) {
|
||||
rTextField.size.width = NSMaxX(bounds) - NSMinX(rTextField)
|
||||
if rTextField.maxX > bounds.maxX {
|
||||
rTextField.size.width = bounds.maxX - rTextField.minX
|
||||
}
|
||||
self.titleRect = rTextField
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import RSCore
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
func keydown(_ event: NSEvent, in view: NSView) -> Bool {
|
||||
|
||||
if MainWindowKeyboardHandler.shared.keydown(event, in: view) {
|
||||
@@ -39,4 +39,3 @@ import RSCore
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,11 +60,11 @@ struct PasteboardFeed: Hashable {
|
||||
let feedID = dictionary[Key.feedID]
|
||||
let editedName = dictionary[Key.editedName]
|
||||
|
||||
var accountType: AccountType? = nil
|
||||
var accountType: AccountType?
|
||||
if let accountTypeString = dictionary[Key.accountType], let accountTypeInt = Int(accountTypeString) {
|
||||
accountType = AccountType(rawValue: accountTypeInt)
|
||||
}
|
||||
|
||||
|
||||
self.init(url: url, feedID: feedID, homePageURL: homePageURL, name: name, editedName: editedName, accountID: accountID, accountType: accountType)
|
||||
}
|
||||
|
||||
@@ -72,8 +72,7 @@ struct PasteboardFeed: Hashable {
|
||||
var pasteboardType: NSPasteboard.PasteboardType?
|
||||
if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIInternalType) {
|
||||
pasteboardType = FeedPasteboardWriter.feedUTIInternalType
|
||||
}
|
||||
else if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIType) {
|
||||
} else if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIType) {
|
||||
pasteboardType = FeedPasteboardWriter.feedUTIType
|
||||
}
|
||||
if let foundType = pasteboardType {
|
||||
@@ -87,8 +86,7 @@ struct PasteboardFeed: Hashable {
|
||||
// Check for URL or a string that may be a URL.
|
||||
if pasteboardItem.types.contains(.URL) {
|
||||
pasteboardType = .URL
|
||||
}
|
||||
else if pasteboardItem.types.contains(.string) {
|
||||
} else if pasteboardItem.types.contains(.string) {
|
||||
pasteboardType = .string
|
||||
}
|
||||
if let foundType = pasteboardType {
|
||||
@@ -161,7 +159,6 @@ extension Feed: @retroactive PasteboardWriterOwner {
|
||||
static let feedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.feed"
|
||||
static let feedUTIInternalType = NSPasteboard.PasteboardType(rawValue: feedUTIInternal)
|
||||
|
||||
|
||||
init(feed: Feed) {
|
||||
self.feed = feed
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import RSCore
|
||||
typealias PasteboardFolderDictionary = [String: String]
|
||||
|
||||
struct PasteboardFolder: Hashable {
|
||||
|
||||
|
||||
private struct Key {
|
||||
static let name = "name"
|
||||
// Internal
|
||||
@@ -21,30 +21,29 @@ struct PasteboardFolder: Hashable {
|
||||
static let accountID = "accountID"
|
||||
}
|
||||
|
||||
|
||||
let name: String
|
||||
let folderID: String?
|
||||
let accountID: String?
|
||||
|
||||
|
||||
init(name: String, folderID: String?, accountID: String?) {
|
||||
self.name = name
|
||||
self.folderID = folderID
|
||||
self.accountID = accountID
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Reading
|
||||
|
||||
|
||||
init?(dictionary: PasteboardFolderDictionary) {
|
||||
guard let name = dictionary[Key.name] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let folderID = dictionary[Key.folderID]
|
||||
let accountID = dictionary[Key.accountID]
|
||||
|
||||
|
||||
self.init(name: name, folderID: folderID, accountID: accountID)
|
||||
}
|
||||
|
||||
|
||||
init?(pasteboardItem: NSPasteboardItem) {
|
||||
var pasteboardType: NSPasteboard.PasteboardType?
|
||||
if pasteboardItem.types.contains(FolderPasteboardWriter.folderUTIInternalType) {
|
||||
@@ -57,10 +56,10 @@ struct PasteboardFolder: Hashable {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
static func pasteboardFolders(with pasteboard: NSPasteboard) -> Set<PasteboardFolder>? {
|
||||
guard let items = pasteboard.pasteboardItems else {
|
||||
return nil
|
||||
@@ -68,9 +67,9 @@ struct PasteboardFolder: Hashable {
|
||||
let folders = items.compactMap { PasteboardFolder(pasteboardItem: $0) }
|
||||
return folders.isEmpty ? nil : Set(folders)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Writing
|
||||
|
||||
|
||||
func internalDictionary() -> PasteboardFolderDictionary {
|
||||
var d = PasteboardFeedDictionary()
|
||||
d[PasteboardFolder.Key.name] = name
|
||||
@@ -130,7 +129,7 @@ private extension FolderPasteboardWriter {
|
||||
var pasteboardFolder: PasteboardFolder {
|
||||
return PasteboardFolder(name: folder.name ?? "", folderID: String(folder.folderID), accountID: folder.account?.accountID)
|
||||
}
|
||||
|
||||
|
||||
var internalDictionary: PasteboardFeedDictionary {
|
||||
return pasteboardFolder.internalDictionary()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ final class RenameWindowController: NSWindowController {
|
||||
@IBOutlet var renamePrompt: NSTextField!
|
||||
@IBOutlet var newTitleTextField: NSTextField!
|
||||
@IBOutlet var renameButton: NSButton!
|
||||
|
||||
|
||||
private var originalTitle: String?
|
||||
private var representedObject: Any?
|
||||
private var delegate: RenameWindowControllerDelegate?
|
||||
|
||||
@@ -11,12 +11,12 @@ import RSTree
|
||||
import Account
|
||||
|
||||
enum SidebarDeleteItemsAlert {
|
||||
|
||||
|
||||
/// Builds a delete confirmation dialog for the supplied nodes
|
||||
static func build(_ nodes: [Node]) -> NSAlert {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
|
||||
|
||||
if nodes.count == 1 {
|
||||
if let folder = nodes.first?.representedObject as? Folder {
|
||||
alert.messageText = NSLocalizedString("Delete Folder", comment: "Delete Folder")
|
||||
@@ -32,11 +32,11 @@ enum SidebarDeleteItemsAlert {
|
||||
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the %d selected items?", comment: "Items delete text")
|
||||
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, nodes.count) as String
|
||||
}
|
||||
|
||||
|
||||
alert.addButton(withTitle: NSLocalizedString("Delete", comment: "Delete Account"))
|
||||
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Delete Account"))
|
||||
|
||||
|
||||
return alert
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import Account
|
||||
|
||||
let treeController: TreeController
|
||||
static let dragOperationNone = NSDragOperation(rawValue: 0)
|
||||
private var draggedNodes: Set<Node>? = nil
|
||||
private var draggedNodes: Set<Node>?
|
||||
|
||||
init(treeController: TreeController) {
|
||||
self.treeController = treeController
|
||||
@@ -56,7 +56,7 @@ import Account
|
||||
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
let parentNode = nodeForItem(item)
|
||||
@@ -68,7 +68,7 @@ import Account
|
||||
return validateLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let draggedFeeds = draggedFeeds {
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
@@ -88,11 +88,11 @@ import Account
|
||||
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return false
|
||||
}
|
||||
let parentNode = nodeForItem(item)
|
||||
@@ -100,7 +100,7 @@ import Account
|
||||
if let draggedFolders = draggedFolders {
|
||||
return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
||||
}
|
||||
|
||||
|
||||
if let draggedFeeds = draggedFeeds {
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
@@ -116,7 +116,7 @@ import Account
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -159,8 +159,7 @@ private extension SidebarOutlineDataSource {
|
||||
for feed in draggedFeeds {
|
||||
if feed.isLocalFeed {
|
||||
hasLocalFeed = true
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
hasNonLocalFeed = true
|
||||
}
|
||||
if hasLocalFeed && hasNonLocalFeed {
|
||||
@@ -228,7 +227,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
return localDragOperation(parentNode: parentNode)
|
||||
}
|
||||
|
||||
|
||||
func localDragOperation(parentNode: Node) -> NSDragOperation {
|
||||
guard let firstDraggedNode = draggedNodes?.first else { return .move }
|
||||
if sameAccount(firstDraggedNode, parentNode) {
|
||||
@@ -275,7 +274,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
guard let dropAccount = parentNode.representedObject as? Account, dropAccount.accountID != draggedFolder.accountID else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
@@ -289,7 +288,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
return localDragOperation(parentNode: parentNode)
|
||||
}
|
||||
|
||||
|
||||
func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
guard let dropAccount = parentNode.representedObject as? Account else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
@@ -307,12 +306,12 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
return localDragOperation(parentNode: parentNode)
|
||||
}
|
||||
|
||||
|
||||
func copyFeedInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
destination.account?.addFeed(feed, to: destination) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -348,7 +347,7 @@ private extension SidebarOutlineDataSource {
|
||||
let destinationContainer = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
||||
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
@@ -386,7 +385,7 @@ private extension SidebarOutlineDataSource {
|
||||
copyFeedBetweenAccounts(node: node, to: parentNode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -463,13 +462,13 @@ private extension SidebarOutlineDataSource {
|
||||
guard let draggedNodes = draggedNodes else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
for node in draggedNodes {
|
||||
if !sameAccount(node, parentNode) {
|
||||
copyFolderBetweenAccounts(node: node, to: parentNode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -486,7 +485,7 @@ private extension SidebarOutlineDataSource {
|
||||
let folder = parentNode.representedObject as? Folder
|
||||
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder)
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -505,7 +504,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func sameAccount(_ node: Node, _ parentNode: Node) -> Bool {
|
||||
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
|
||||
if accountID == parentAccountID {
|
||||
@@ -514,7 +513,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func nodeAccount(_ node: Node) -> Account? {
|
||||
if let account = node.representedObject as? Account {
|
||||
return account
|
||||
@@ -527,11 +526,11 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func nodeAccountID(_ node: Node) -> String? {
|
||||
return nodeAccount(node)?.accountID
|
||||
}
|
||||
|
||||
|
||||
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
for node in parentNode.childNodes {
|
||||
if nodeRepresentsAnyDraggedFeed(node, draggedFeeds) {
|
||||
@@ -544,7 +543,7 @@ private extension SidebarOutlineDataSource {
|
||||
func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeed: PasteboardFeed) -> Bool {
|
||||
return violatesAccountSpecificBehavior(dropTargetNode, Set([draggedFeed]))
|
||||
}
|
||||
|
||||
|
||||
func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
if violatesDisallowFeedInRootFolder(dropTargetNode) {
|
||||
return true
|
||||
@@ -553,23 +552,23 @@ private extension SidebarOutlineDataSource {
|
||||
if violatesDisallowFeedCopyInRootFolder(dropTargetNode, draggedFeeds) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
if violatesDisallowFeedInMultipleFolders(dropTargetNode, draggedFeeds) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func violatesDisallowFeedInRootFolder(_ dropTargetNode: Node) -> Bool {
|
||||
guard let parentAccount = nodeAccount(dropTargetNode), parentAccount.behaviors.contains(.disallowFeedInRootFolder) else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
if dropTargetNode.representedObject is Account {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -577,17 +576,17 @@ private extension SidebarOutlineDataSource {
|
||||
guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
for draggedFeed in draggedFeeds {
|
||||
if dropTargetAccount.accountID != draggedFeed.accountID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if dropTargetNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -595,7 +594,7 @@ private extension SidebarOutlineDataSource {
|
||||
guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedInMultipleFolders) else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
for draggedFeed in draggedFeeds {
|
||||
if dropTargetAccount.accountID == draggedFeed.accountID {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
@@ -607,7 +606,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -627,7 +626,7 @@ private extension SidebarOutlineDataSource {
|
||||
let draggedFolderNode = Node(representedObject: draggedFolderWrapper, parent: nil)
|
||||
draggedFolderNode.canHaveChildNodes = true
|
||||
let nodes = parentNode.childNodes + [draggedFolderNode]
|
||||
|
||||
|
||||
// Revisit if the tree controller can ever be sorted in some other way.
|
||||
let sortedNodes = nodes.sortedAlphabeticallyWithFoldersAtEnd()
|
||||
let index = sortedNodes.firstIndex(of: draggedFolderNode)!
|
||||
@@ -648,12 +647,12 @@ final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
||||
}
|
||||
|
||||
final class PasteboardFolderObjectWrapper: DisplayNameProvider {
|
||||
|
||||
|
||||
var nameForDisplay: String {
|
||||
return pasteboardFolder.name
|
||||
}
|
||||
let pasteboardFolder: PasteboardFolder
|
||||
|
||||
|
||||
init(pasteboardFolder: PasteboardFolder) {
|
||||
self.pasteboardFolder = pasteboardFolder
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import RSCore
|
||||
import RSTree
|
||||
|
||||
class SidebarOutlineView : NSOutlineView {
|
||||
class SidebarOutlineView: NSOutlineView {
|
||||
|
||||
@IBOutlet var keyboardDelegate: KeyboardDelegate!
|
||||
|
||||
@@ -40,15 +40,15 @@ class SidebarOutlineView : NSOutlineView {
|
||||
// MARK: NSView
|
||||
|
||||
override func viewWillStartLiveResize() {
|
||||
|
||||
|
||||
if let scrollView = self.enclosingScrollView {
|
||||
scrollView.hasVerticalScroller = false
|
||||
}
|
||||
super.viewWillStartLiveResize()
|
||||
}
|
||||
|
||||
|
||||
override func viewDidEndLiveResize() {
|
||||
|
||||
|
||||
if let scrollView = self.enclosingScrollView {
|
||||
scrollView.hasVerticalScroller = true
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ final class SidebarStatusBarView: NSView {
|
||||
|
||||
let progressLabelFontSize = progressLabel.font?.pointSize ?? 13.0
|
||||
progressLabel.font = NSFont.monospacedDigitSystemFont(ofSize: progressLabelFontSize, weight: NSFont.Weight.regular)
|
||||
progressLabel.stringValue = ""
|
||||
|
||||
progressLabel.stringValue = ""
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ extension SidebarViewController {
|
||||
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let articles = unreadArticles(for: objects)
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else {
|
||||
return
|
||||
@@ -80,7 +80,7 @@ extension SidebarViewController {
|
||||
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [AnyObject] else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let nodes = objects.compactMap { treeController.nodeInTreeRepresentingObject($0) }
|
||||
|
||||
let alert = SidebarDeleteItemsAlert.build(nodes)
|
||||
@@ -103,7 +103,7 @@ extension SidebarViewController {
|
||||
}
|
||||
window.beginSheet(renameSheet)
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleNotificationsFromContextMenu(_ sender: Any?) {
|
||||
guard let item = sender as? NSMenuItem,
|
||||
let feed = item.representedObject as? Feed else {
|
||||
@@ -119,7 +119,7 @@ extension SidebarViewController {
|
||||
NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu))
|
||||
}
|
||||
} else {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
if feed.isNotifyAboutNewArticles == nil { feed.isNotifyAboutNewArticles = false }
|
||||
@@ -134,7 +134,7 @@ extension SidebarViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) {
|
||||
guard let item = sender as? NSMenuItem,
|
||||
let feed = item.representedObject as? Feed else {
|
||||
@@ -144,7 +144,7 @@ extension SidebarViewController {
|
||||
feed.isArticleExtractorAlwaysOn?.toggle()
|
||||
NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu))
|
||||
}
|
||||
|
||||
|
||||
func showNotificationsNotEnabledAlert() {
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
@@ -163,7 +163,7 @@ extension SidebarViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension SidebarViewController: RenameWindowControllerDelegate {
|
||||
@@ -229,9 +229,9 @@ private extension SidebarViewController {
|
||||
menu.addItem(item)
|
||||
}
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
let notificationText = feed.notificationDisplayName.capitalized
|
||||
|
||||
|
||||
let notificationMenuItem = menuItem(notificationText, #selector(toggleNotificationsFromContextMenu(_:)), feed)
|
||||
if feed.isNotifyAboutNewArticles == nil || feed.isNotifyAboutNewArticles! == false {
|
||||
notificationMenuItem.state = .off
|
||||
@@ -251,7 +251,7 @@ private extension SidebarViewController {
|
||||
menu.addItem(articleExtractorMenuItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
|
||||
menu.addItem(renameMenuItem(feed))
|
||||
menu.addItem(deleteMenuItem([feed]))
|
||||
|
||||
@@ -362,4 +362,3 @@ private extension SidebarViewController {
|
||||
return articles
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ protocol SidebarDelegate: AnyObject {
|
||||
}
|
||||
|
||||
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
|
||||
|
||||
|
||||
@IBOutlet weak var outlineView: NSOutlineView!
|
||||
|
||||
|
||||
weak var delegate: SidebarDelegate?
|
||||
|
||||
private let rebuildTreeAndRestoreSelectionQueue = CoalescingQueue(name: "Rebuild Tree Queue", interval: 1.0)
|
||||
@@ -36,7 +36,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
lazy var dataSource: SidebarOutlineDataSource = {
|
||||
return SidebarOutlineDataSource(treeController: treeController)
|
||||
}()
|
||||
|
||||
|
||||
var isReadFiltered: Bool {
|
||||
get {
|
||||
return treeControllerDelegate.isReadFiltered
|
||||
@@ -89,19 +89,19 @@ protocol SidebarDelegate: AnyObject {
|
||||
}
|
||||
}
|
||||
expandNodes()
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: State Restoration
|
||||
|
||||
func saveState(to state: inout [AnyHashable : Any]) {
|
||||
|
||||
func saveState(to state: inout [AnyHashable: Any]) {
|
||||
state[UserInfoKey.readFeedsFilterState] = isReadFiltered
|
||||
state[UserInfoKey.containerExpandedWindowState] = expandedTable.map { $0.userInfo }
|
||||
state[UserInfoKey.selectedFeedsState] = selectedFeeds.compactMap { $0.sidebarItemID?.userInfo }
|
||||
}
|
||||
|
||||
func restoreState(from state: [AnyHashable : Any]) {
|
||||
|
||||
|
||||
func restoreState(from state: [AnyHashable: Any]) {
|
||||
|
||||
if let containerExpandedWindowState = state[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: AnyHashable]] {
|
||||
let containerIdentifiers = containerExpandedWindowState.compactMap( { ContainerIdentifier(userInfo: $0) })
|
||||
expandedTable = Set(containerIdentifiers)
|
||||
@@ -117,7 +117,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
}
|
||||
|
||||
rebuildTreeAndReloadDataIfNeeded()
|
||||
|
||||
|
||||
var selectIndexes = IndexSet()
|
||||
|
||||
func selectFeedsVisitor(node: Node) {
|
||||
@@ -131,12 +131,12 @@ protocol SidebarDelegate: AnyObject {
|
||||
treeController.visitNodes(selectFeedsVisitor(node:))
|
||||
outlineView.selectRowIndexes(selectIndexes, byExtendingSelection: false)
|
||||
focus()
|
||||
|
||||
|
||||
if let readFeedsFilterState = state[UserInfoKey.readFeedsFilterState] as? Bool {
|
||||
isReadFiltered = readFeedsFilterState
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc func unreadCountDidInitialize(_ notification: Notification) {
|
||||
@@ -152,7 +152,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
guard let representedObject = note.object else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let timelineViewController = representedObject as? TimelineViewController {
|
||||
configureUnreadCountForCellsForRepresentedObjects(timelineViewController.representedObjects)
|
||||
} else {
|
||||
@@ -175,7 +175,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
@objc func accountsDidChange(_ notification: Notification) {
|
||||
rebuildTreeAndRestoreSelection()
|
||||
}
|
||||
|
||||
|
||||
@objc func accountStateDidChange(_ notification: Notification) {
|
||||
rebuildTreeAndRestoreSelection()
|
||||
}
|
||||
@@ -183,7 +183,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
@objc func batchUpdateDidPerform(_ notification: Notification) {
|
||||
rebuildTreeAndRestoreSelection()
|
||||
}
|
||||
|
||||
|
||||
@objc func userDidAddFeed(_ notification: Notification) {
|
||||
guard let feed = notification.userInfo?[UserInfoKey.feed] else {
|
||||
return
|
||||
@@ -199,7 +199,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else { return }
|
||||
configureCellsForRepresentedObject(feed)
|
||||
}
|
||||
|
||||
|
||||
@objc func feedSettingDidChange(_ note: Notification) {
|
||||
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
|
||||
return
|
||||
@@ -228,18 +228,18 @@ protocol SidebarDelegate: AnyObject {
|
||||
self.restoreSelection(to: savedSelection, sendNotificationIfChanged: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func delete(_ sender: AnyObject?) {
|
||||
let availableSelectedNodes = selectedNodes.filter { !($0.representedObject is PseudoFeed) }
|
||||
|
||||
|
||||
if availableSelectedNodes.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let alert = SidebarDeleteItemsAlert.build(availableSelectedNodes)
|
||||
|
||||
|
||||
alert.beginSheetModal(for: view.window!) { [weak self] result in
|
||||
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
||||
guard let self = self else { return }
|
||||
@@ -252,7 +252,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func doubleClickedSidebar(_ sender: Any?) {
|
||||
guard outlineView.clickedRow == outlineView.selectedRow else {
|
||||
return
|
||||
@@ -300,20 +300,20 @@ protocol SidebarDelegate: AnyObject {
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
|
||||
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
|
||||
if let _ = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func goToNextUnread(wrappingToTop wrapping: Bool = false) {
|
||||
guard let row = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) else {
|
||||
assertionFailure("goToNextUnread called before checking if there is a next unread.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
NSCursor.setHiddenUntilMouseMoves(true)
|
||||
outlineView.selectRowIndexes(IndexSet([row]), byExtendingSelection: false)
|
||||
outlineView.scrollTo(row: row)
|
||||
@@ -339,13 +339,13 @@ protocol SidebarDelegate: AnyObject {
|
||||
// If the clickedRow is part of the selected rows, then do a contextual menu for all the selected rows.
|
||||
return contextualMenuForSelectedObjects()
|
||||
}
|
||||
|
||||
|
||||
let object = node.representedObject
|
||||
return menu(for: [object])
|
||||
}
|
||||
|
||||
// MARK: - NSMenuDelegate
|
||||
|
||||
|
||||
public func menuNeedsUpdate(_ menu: NSMenu) {
|
||||
menu.removeAllItems()
|
||||
guard let contextualMenu = contextualMenuForClickedRows() else {
|
||||
@@ -354,9 +354,8 @@ protocol SidebarDelegate: AnyObject {
|
||||
menu.takeItems(from: contextualMenu)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - NSOutlineViewDelegate
|
||||
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
|
||||
let node = item as! Node
|
||||
|
||||
@@ -398,7 +397,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
func outlineViewSelectionDidChange(_ notification: Notification) {
|
||||
selectionDidChange(selectedObjects.isEmpty ? nil : selectedObjects)
|
||||
}
|
||||
|
||||
|
||||
func outlineViewItemDidExpand(_ notification: Notification) {
|
||||
guard let node = notification.userInfo?["NSObject"] as? Node,
|
||||
let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else {
|
||||
@@ -409,7 +408,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
delegate?.sidebarInvalidatedRestorationState(self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func outlineViewItemDidCollapse(_ notification: Notification) {
|
||||
guard let node = notification.userInfo?["NSObject"] as? Node,
|
||||
let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else {
|
||||
@@ -420,43 +419,43 @@ protocol SidebarDelegate: AnyObject {
|
||||
delegate?.sidebarInvalidatedRestorationState(self)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Node Manipulation
|
||||
|
||||
|
||||
// MARK: - Node Manipulation
|
||||
|
||||
func deleteNodes(_ nodes: [Node]) {
|
||||
let nodesToDelete = treeController.normalizedSelectedNodes(nodes)
|
||||
|
||||
|
||||
guard let undoManager = undoManager, let deleteCommand = DeleteCommand(nodesToDelete: nodesToDelete, treeController: treeController, undoManager: undoManager, errorHandler: ErrorHandler.present) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
animatingChanges = true
|
||||
outlineView.beginUpdates()
|
||||
|
||||
|
||||
let indexSetsGroupedByParent = Node.indexSetsGroupedByParent(nodesToDelete)
|
||||
for (parent, indexSet) in indexSetsGroupedByParent {
|
||||
outlineView.removeItems(at: indexSet, inParent: parent.isRoot ? nil : parent, withAnimation: [.slideDown])
|
||||
}
|
||||
|
||||
|
||||
outlineView.endUpdates()
|
||||
|
||||
|
||||
runCommand(deleteCommand)
|
||||
animatingChanges = false
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
|
||||
|
||||
func selectFeed(_ feed: SidebarItem) {
|
||||
if isReadFiltered, let feedID = feed.sidebarItemID {
|
||||
self.treeControllerDelegate.addFilterException(feedID)
|
||||
|
||||
|
||||
if let feed = feed as? Feed, let account = feed.account {
|
||||
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(feed) })
|
||||
if let parentFolderFeedID = parentFolder?.sidebarItemID {
|
||||
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
addTreeControllerToFilterExceptions()
|
||||
rebuildTreeAndRestoreSelection()
|
||||
}
|
||||
@@ -464,7 +463,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||
}
|
||||
|
||||
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
|
||||
func deepLinkRevealAndSelect(for userInfo: [AnyHashable: Any]) {
|
||||
guard let accountNode = findAccountNode(userInfo),
|
||||
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
|
||||
let feed = feedNode.representedObject as? SidebarItem else {
|
||||
@@ -482,7 +481,7 @@ protocol SidebarDelegate: AnyObject {
|
||||
delegate?.sidebarInvalidatedRestorationState(self)
|
||||
rebuildTreeAndRestoreSelection()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSUserInterfaceValidations
|
||||
@@ -497,21 +496,21 @@ extension SidebarViewController: NSUserInterfaceValidations {
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Private
|
||||
// MARK: - Private
|
||||
|
||||
private extension SidebarViewController {
|
||||
|
||||
|
||||
var accountNodes: [Account] {
|
||||
return treeController.rootNode.childNodes.compactMap { $0.representedObject as? Account }
|
||||
}
|
||||
|
||||
|
||||
var selectedNodes: [Node] {
|
||||
if let nodes = outlineView.selectedItems as? [Node] {
|
||||
return nodes
|
||||
}
|
||||
return [Node]()
|
||||
}
|
||||
|
||||
|
||||
var selectedFeeds: [SidebarItem] {
|
||||
selectedNodes.compactMap { $0.representedObject as? SidebarItem }
|
||||
}
|
||||
@@ -529,13 +528,13 @@ private extension SidebarViewController {
|
||||
}
|
||||
return node.representedObject as? Feed
|
||||
}
|
||||
|
||||
|
||||
func addAllSelectedToFilterExceptions() {
|
||||
for feed in selectedFeeds {
|
||||
addToFilterExceptionsIfNecessary(feed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func addToFilterExceptionsIfNecessary(_ feed: SidebarItem?) {
|
||||
if isReadFiltered, let feedID = feed?.sidebarItemID {
|
||||
if feed is PseudoFeed {
|
||||
@@ -552,29 +551,28 @@ private extension SidebarViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func addParentFolderToFilterExceptions(_ feed: SidebarItem) {
|
||||
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
||||
let folder = node.parent?.representedObject as? Folder,
|
||||
let folderFeedID = folder.sidebarItemID else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
treeControllerDelegate.addFilterException(folderFeedID)
|
||||
}
|
||||
|
||||
|
||||
func queueRebuildTreeAndRestoreSelection() {
|
||||
rebuildTreeAndRestoreSelectionQueue.add(self, #selector(rebuildTreeAndRestoreSelection))
|
||||
}
|
||||
|
||||
|
||||
@objc func rebuildTreeAndRestoreSelection() {
|
||||
let savedAccounts = accountNodes
|
||||
let savedSelection = selectedNodes
|
||||
|
||||
|
||||
rebuildTreeAndReloadDataIfNeeded()
|
||||
restoreSelection(to: savedSelection, sendNotificationIfChanged: true)
|
||||
|
||||
|
||||
// Automatically expand any new or newly active accounts
|
||||
for account in AccountManager.shared.activeAccounts {
|
||||
if !savedAccounts.contains(account) {
|
||||
@@ -583,7 +581,7 @@ private extension SidebarViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func rebuildTreeAndReloadDataIfNeeded() {
|
||||
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
||||
addAllSelectedToFilterExceptions()
|
||||
@@ -593,11 +591,11 @@ private extension SidebarViewController {
|
||||
expandNodes()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func expandNodes() {
|
||||
treeController.visitNodes(expandNodesVisitor(node:))
|
||||
}
|
||||
|
||||
|
||||
func expandNodesVisitor(node: Node) {
|
||||
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID {
|
||||
if expandedTable.contains(containerID) {
|
||||
@@ -607,7 +605,7 @@ private extension SidebarViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func addTreeControllerToFilterExceptions() {
|
||||
treeController.visitNodes(addTreeControllerToFilterExceptionsVisitor(node:))
|
||||
}
|
||||
@@ -654,13 +652,13 @@ private extension SidebarViewController {
|
||||
if row < 0 || row >= outlineView.numberOfRows {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let node = outlineView.item(atRow: row) as? Node {
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func rowHasAtLeastOneUnreadArticle(_ row: Int) -> Bool {
|
||||
if let oneNode = nodeForRow(row) {
|
||||
if let unreadCountProvider = oneNode.representedObject as? UnreadCountProvider {
|
||||
@@ -716,15 +714,15 @@ private extension SidebarViewController {
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? {
|
||||
func findAccountNode(_ userInfo: [AnyHashable: Any]?) -> Node? {
|
||||
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
|
||||
return node
|
||||
}
|
||||
@@ -739,8 +737,8 @@ private extension SidebarViewController {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? {
|
||||
|
||||
func findFeedNode(_ userInfo: [AnyHashable: Any]?, beginningAt startingNode: Node) -> Node? {
|
||||
guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else {
|
||||
return nil
|
||||
}
|
||||
@@ -749,7 +747,7 @@ private extension SidebarViewController {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func configure(_ cell: SidebarCell, _ node: Node) {
|
||||
cell.cellAppearance = SidebarCellAppearance(rowSizeStyle: outlineView.effectiveRowSizeStyle)
|
||||
cell.name = nameFor(node)
|
||||
@@ -819,7 +817,7 @@ private extension SidebarViewController {
|
||||
}
|
||||
|
||||
func applyToAvailableCells(_ completion: (SidebarCell, Node) -> Void) {
|
||||
outlineView.enumerateAvailableRowViews { (rowView: NSTableRowView, row: Int) -> Void in
|
||||
outlineView.enumerateAvailableRowViews { (rowView: NSTableRowView, row: Int) in
|
||||
guard let cell = cellForRowView(rowView), let node = nodeForRow(row) else {
|
||||
return
|
||||
}
|
||||
@@ -852,7 +850,7 @@ private extension SidebarViewController {
|
||||
func revealAndSelectRepresentedObject(_ representedObject: AnyObject) -> Bool {
|
||||
return outlineView.revealAndSelectRepresentedObject(representedObject, treeController)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension Node {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
class UnreadCountView : NSView {
|
||||
class UnreadCountView: NSView {
|
||||
|
||||
struct Appearance {
|
||||
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
|
||||
@@ -31,11 +31,11 @@ class UnreadCountView : NSView {
|
||||
}
|
||||
|
||||
private var intrinsicContentSizeIsValid = false
|
||||
private var _intrinsicContentSize = NSZeroSize
|
||||
|
||||
private var _intrinsicContentSize = NSSize.zero
|
||||
|
||||
override var intrinsicContentSize: NSSize {
|
||||
if !intrinsicContentSizeIsValid {
|
||||
var size = NSZeroSize
|
||||
var size = NSSize.zero
|
||||
if unreadCount > 0 {
|
||||
size = textSize()
|
||||
size.width += (Appearance.padding.left + Appearance.padding.right)
|
||||
@@ -46,11 +46,11 @@ class UnreadCountView : NSView {
|
||||
}
|
||||
return _intrinsicContentSize
|
||||
}
|
||||
|
||||
|
||||
override var isFlipped: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override func invalidateIntrinsicContentSize() {
|
||||
intrinsicContentSizeIsValid = false
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class UnreadCountView : NSView {
|
||||
|
||||
private func textSize() -> NSSize {
|
||||
if unreadCount < 1 {
|
||||
return NSZeroSize
|
||||
return NSSize.zero
|
||||
}
|
||||
|
||||
if let cachedSize = UnreadCountView.textSizeCache[unreadCount] {
|
||||
@@ -76,9 +76,9 @@ class UnreadCountView : NSView {
|
||||
|
||||
private func textRect() -> NSRect {
|
||||
let size = textSize()
|
||||
var r = NSZeroRect
|
||||
var r = NSRect.zero
|
||||
r.size = size
|
||||
r.origin.x = (NSMaxX(bounds) - Appearance.padding.right) - r.size.width
|
||||
r.origin.x = (bounds.maxX - Appearance.padding.right) - r.size.width
|
||||
r.origin.y = Appearance.padding.top
|
||||
return r
|
||||
}
|
||||
@@ -93,4 +93,3 @@ class UnreadCountView : NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,11 +78,9 @@ private extension ArticlePasteboardWriter {
|
||||
}
|
||||
if let text = article.contentText {
|
||||
s += "\(text)\n\n"
|
||||
}
|
||||
else if let summary = article.summary {
|
||||
} else if let summary = article.summary {
|
||||
s += "\(summary)\n\n"
|
||||
}
|
||||
else if let html = article.contentHTML {
|
||||
} else if let html = article.contentHTML {
|
||||
let convertedHTML = html.convertingToPlainText()
|
||||
s += "\(convertedHTML)\n\n"
|
||||
}
|
||||
@@ -184,7 +182,6 @@ private extension ArticlePasteboardWriter {
|
||||
guard let authors = article.authors, !authors.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return authors.map{ authorDictionary($0) }
|
||||
return authors.map { authorDictionary($0) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ final class MultilineTextFieldSizer {
|
||||
|
||||
private let numberOfLines: Int
|
||||
private let font: NSFont
|
||||
private let textField:NSTextField
|
||||
private let textField: NSTextField
|
||||
private let singleLineHeightEstimate: Int
|
||||
private let doubleLineHeightEstimate: Int
|
||||
private var cache = [String: WidthHeightCache]() // Each string has a cache.
|
||||
@@ -56,7 +56,7 @@ final class MultilineTextFieldSizer {
|
||||
guard attributedString.length > 0 else {
|
||||
return TextFieldSizeInfo(size: NSSize.zero, numberOfLinesUsed: 0)
|
||||
}
|
||||
|
||||
|
||||
// Assumes the same font family/size for the whole string
|
||||
let font = attributedString.attribute(.font, at: 0, effectiveRange: nil) as! NSFont
|
||||
|
||||
@@ -227,8 +227,7 @@ private extension MultilineTextFieldSizer {
|
||||
|
||||
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
|
||||
smallNeighbor = (oneWidth, oneHeight)
|
||||
}
|
||||
else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
|
||||
} else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
|
||||
largeNeighbor = (oneWidth, oneHeight)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ final class SingleLineTextFieldSizer {
|
||||
var calculatedSize = textField.fittingSize
|
||||
calculatedSize.height = ceil(calculatedSize.height)
|
||||
calculatedSize.width = ceil(calculatedSize.width)
|
||||
|
||||
|
||||
cache[text] = calculatedSize
|
||||
return calculatedSize
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ struct TimelineCellAppearance: Equatable {
|
||||
let showIcon: Bool
|
||||
|
||||
let cellPadding: NSEdgeInsets
|
||||
|
||||
|
||||
let feedNameFont: NSFont
|
||||
|
||||
let dateFont: NSFont
|
||||
@@ -22,7 +22,7 @@ struct TimelineCellAppearance: Equatable {
|
||||
let titleFont: NSFont
|
||||
let titleBottomMargin: CGFloat = 1.0
|
||||
let titleNumberOfLines = 3
|
||||
|
||||
|
||||
let textFont: NSFont
|
||||
|
||||
let textOnlyFont: NSFont
|
||||
@@ -55,7 +55,7 @@ struct TimelineCellAppearance: Equatable {
|
||||
self.textOnlyFont = NSFont.systemFont(ofSize: largeItemFontSize)
|
||||
|
||||
self.showIcon = showIcon
|
||||
|
||||
|
||||
cellPadding = NSEdgeInsets(top: 8.0, left: 4.0, bottom: 10.0, right: 4.0)
|
||||
|
||||
let margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight
|
||||
|
||||
@@ -10,9 +10,9 @@ import AppKit
|
||||
import Articles
|
||||
|
||||
struct TimelineCellData {
|
||||
|
||||
|
||||
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
|
||||
|
||||
|
||||
let title: String
|
||||
let attributedTitle: NSAttributedString
|
||||
let text: String
|
||||
@@ -37,7 +37,7 @@ struct TimelineCellData {
|
||||
} else {
|
||||
self.text = truncatedSummary
|
||||
}
|
||||
|
||||
|
||||
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
|
||||
|
||||
if let feedName = feedName {
|
||||
@@ -45,7 +45,7 @@ struct TimelineCellData {
|
||||
} else {
|
||||
self.feedName = ""
|
||||
}
|
||||
|
||||
|
||||
if let byline = byline {
|
||||
self.byline = byline
|
||||
} else {
|
||||
@@ -57,12 +57,12 @@ struct TimelineCellData {
|
||||
self.showIcon = showIcon
|
||||
self.iconImage = iconImage
|
||||
self.featuredImage = featuredImage
|
||||
|
||||
|
||||
self.read = article.status.read
|
||||
self.starred = article.status.starred
|
||||
}
|
||||
|
||||
init() { //Empty
|
||||
init() { // Empty
|
||||
self.title = ""
|
||||
self.text = ""
|
||||
self.dateString = ""
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import RSCore
|
||||
|
||||
struct TimelineCellLayout {
|
||||
|
||||
|
||||
let width: CGFloat
|
||||
let height: CGFloat
|
||||
let feedNameRect: NSRect
|
||||
@@ -24,9 +24,9 @@ struct TimelineCellLayout {
|
||||
let iconImageRect: NSRect
|
||||
let separatorRect: NSRect
|
||||
let paddingBottom: CGFloat
|
||||
|
||||
|
||||
init(width: CGFloat, height: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, numberOfLinesForTitle: Int, summaryRect: NSRect, textRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, iconImageRect: NSRect, separatorRect: NSRect, paddingBottom: CGFloat) {
|
||||
|
||||
|
||||
self.width = width
|
||||
self.feedNameRect = feedNameRect
|
||||
self.dateRect = dateRect
|
||||
@@ -42,8 +42,7 @@ struct TimelineCellLayout {
|
||||
|
||||
if height > 0.1 {
|
||||
self.height = height
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, iconImageRect].maxY() + paddingBottom
|
||||
}
|
||||
}
|
||||
@@ -62,8 +61,7 @@ struct TimelineCellLayout {
|
||||
var lastTextRect = titleRect
|
||||
if numberOfLinesForTitle == 0 {
|
||||
lastTextRect = textRect
|
||||
}
|
||||
else if numberOfLinesForTitle < appearance.titleNumberOfLines {
|
||||
} else if numberOfLinesForTitle < appearance.titleNumberOfLines {
|
||||
if summaryRect.height > 0.1 {
|
||||
lastTextRect = summaryRect
|
||||
}
|
||||
@@ -114,7 +112,7 @@ private extension TimelineCellLayout {
|
||||
r.size.height = 0
|
||||
return (r, 0)
|
||||
}
|
||||
|
||||
|
||||
let attributedTitle = cellData.attributedTitle.adding(font: appearance.titleFont)
|
||||
let sizeInfo = MultilineTextFieldSizer.size(for: attributedTitle, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
|
||||
r.size.height = sizeInfo.size.height
|
||||
@@ -124,15 +122,15 @@ private extension TimelineCellLayout {
|
||||
return (r, sizeInfo.numberOfLinesUsed)
|
||||
}
|
||||
|
||||
static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
if titleNumberOfLines >= appearance.titleNumberOfLines || cellData.text.isEmpty {
|
||||
return NSRect.zero
|
||||
}
|
||||
|
||||
var r = textBoxRect
|
||||
r.origin.y = NSMaxY(titleRect)
|
||||
r.origin.y = titleRect.maxY
|
||||
let summaryNumberOfLines = appearance.titleNumberOfLines - titleNumberOfLines
|
||||
|
||||
|
||||
let sizeInfo = MultilineTextFieldSizer.size(for: cellData.text, font: appearance.textOnlyFont, numberOfLines: summaryNumberOfLines, width: Int(textBoxRect.width))
|
||||
r.size.height = sizeInfo.size.height
|
||||
if sizeInfo.numberOfLinesUsed < 1 {
|
||||
@@ -160,10 +158,10 @@ private extension TimelineCellLayout {
|
||||
|
||||
static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
let textFieldSize = SingleLineTextFieldSizer.size(for: cellData.dateString, font: appearance.dateFont)
|
||||
|
||||
var r = NSZeroRect
|
||||
|
||||
var r = NSRect.zero
|
||||
r.size = textFieldSize
|
||||
r.origin.y = NSMaxY(rectAbove) + appearance.titleBottomMargin
|
||||
r.origin.y = rectAbove.maxY + appearance.titleBottomMargin
|
||||
r.size.width = textFieldSize.width
|
||||
|
||||
r.origin.x = textBoxRect.maxX - textFieldSize.width
|
||||
@@ -173,22 +171,22 @@ private extension TimelineCellLayout {
|
||||
|
||||
static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
if cellData.showFeedName == .none {
|
||||
return NSZeroRect
|
||||
return NSRect.zero
|
||||
}
|
||||
|
||||
let textFieldSize = SingleLineTextFieldSizer.size(for: cellData.feedName, font: appearance.feedNameFont)
|
||||
var r = NSZeroRect
|
||||
var r = NSRect.zero
|
||||
r.size = textFieldSize
|
||||
r.origin.y = dateRect.minY
|
||||
r.origin.x = textBoxRect.origin.x
|
||||
r.size.width = (textBoxRect.maxX - (dateRect.size.width + appearance.dateMarginLeft)) - textBoxRect.origin.x
|
||||
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleRect: NSRect) -> NSRect {
|
||||
|
||||
var r = NSZeroRect
|
||||
var r = NSRect.zero
|
||||
r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension)
|
||||
r.origin.x = appearance.cellPadding.left
|
||||
r.origin.y = titleRect.minY + 6
|
||||
@@ -220,7 +218,7 @@ private extension TimelineCellLayout {
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
static func rectForSeparator(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ alignmentRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect {
|
||||
return NSRect(x: alignmentRect.minX, y: height - 1, width: width - alignmentRect.minX, height: 1)
|
||||
}
|
||||
@@ -237,4 +235,3 @@ private extension Array where Element == NSRect {
|
||||
return y
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class TimelineTableCellView: NSTableCellView {
|
||||
private let titleView = TimelineTableCellView.multiLineTextField()
|
||||
private let summaryView = TimelineTableCellView.multiLineTextField()
|
||||
private let textView = TimelineTableCellView.multiLineTextField()
|
||||
private let unreadIndicatorView = UnreadIndicatorView(frame: NSZeroRect)
|
||||
private let unreadIndicatorView = UnreadIndicatorView(frame: NSRect.zero)
|
||||
private let dateView = TimelineTableCellView.singleLineTextField()
|
||||
private let feedNameView = TimelineTableCellView.singleLineTextField()
|
||||
|
||||
@@ -35,13 +35,13 @@ class TimelineTableCellView: NSTableCellView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var cellData: TimelineCellData! {
|
||||
didSet {
|
||||
updateSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isEmphasized: Bool = false {
|
||||
didSet {
|
||||
unreadIndicatorView.isEmphasized = isEmphasized
|
||||
@@ -55,7 +55,7 @@ class TimelineTableCellView: NSTableCellView {
|
||||
updateStarView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override var isFlipped: Bool {
|
||||
return true
|
||||
}
|
||||
@@ -64,8 +64,8 @@ class TimelineTableCellView: NSTableCellView {
|
||||
super.init(frame: frameRect)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
@@ -73,29 +73,29 @@ class TimelineTableCellView: NSTableCellView {
|
||||
convenience init() {
|
||||
self.init(frame: NSRect.zero)
|
||||
}
|
||||
|
||||
|
||||
override func setFrameSize(_ newSize: NSSize) {
|
||||
|
||||
|
||||
if newSize == self.frame.size {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
super.setFrameSize(newSize)
|
||||
needsLayout = true
|
||||
}
|
||||
|
||||
override func viewDidMoveToSuperview() {
|
||||
|
||||
|
||||
updateSubviews()
|
||||
}
|
||||
|
||||
|
||||
override func layout() {
|
||||
|
||||
resizeSubviews(withOldSize: NSZeroSize)
|
||||
resizeSubviews(withOldSize: NSSize.zero)
|
||||
}
|
||||
|
||||
|
||||
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
||||
|
||||
|
||||
let layoutRects = updatedLayoutRects()
|
||||
|
||||
setFrame(for: titleView, rect: layoutRects.titleRect)
|
||||
@@ -144,13 +144,12 @@ private extension TimelineTableCellView {
|
||||
imageView.imageScaling = scaling
|
||||
return imageView
|
||||
}
|
||||
|
||||
|
||||
func setFrame(for textField: NSTextField, rect: NSRect) {
|
||||
|
||||
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
|
||||
hideView(textField)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
showView(textField)
|
||||
textField.setFrame(ifNotEqualTo: rect)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import AppKit
|
||||
class UnreadIndicatorView: NSView {
|
||||
|
||||
static let unreadCircleDimension: CGFloat = 8.0
|
||||
|
||||
|
||||
var isEmphasized = false {
|
||||
didSet {
|
||||
if isEmphasized != oldValue {
|
||||
@@ -19,7 +19,7 @@ class UnreadIndicatorView: NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isSelected = false {
|
||||
didSet {
|
||||
if isSelected != oldValue {
|
||||
@@ -41,5 +41,5 @@ class UnreadIndicatorView: NSView {
|
||||
}
|
||||
UnreadIndicatorView.bezierPath.fill()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -34,4 +34,3 @@ final class TimelineContainerView: NSView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ final class TimelineContainerViewController: NSViewController {
|
||||
@IBOutlet weak var newestToOldestMenuItem: NSMenuItem!
|
||||
@IBOutlet weak var oldestToNewestMenuItem: NSMenuItem!
|
||||
@IBOutlet weak var groupByFeedMenuItem: NSMenuItem!
|
||||
|
||||
|
||||
@IBOutlet weak var readFilteredButton: NSButton!
|
||||
@IBOutlet var containerView: TimelineContainerView!
|
||||
|
||||
@@ -49,7 +49,7 @@ final class TimelineContainerViewController: NSViewController {
|
||||
guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return false }
|
||||
return regularTimelineViewController.isCleanUpAvailable
|
||||
}
|
||||
|
||||
|
||||
lazy var regularTimelineViewController = {
|
||||
return TimelineViewController(delegate: self)
|
||||
}()
|
||||
@@ -63,21 +63,21 @@ final class TimelineContainerViewController: NSViewController {
|
||||
super.viewDidLoad()
|
||||
setRepresentedObjects(nil, mode: .regular)
|
||||
showTimeline(for: .regular)
|
||||
|
||||
|
||||
makeMenuItemTitleLarger(newestToOldestMenuItem)
|
||||
makeMenuItemTitleLarger(oldestToNewestMenuItem)
|
||||
makeMenuItemTitleLarger(groupByFeedMenuItem)
|
||||
updateViewOptionsPopUpButton()
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
|
||||
@objc func userDefaultsDidChange(_ note: Notification) {
|
||||
updateViewOptionsPopUpButton()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - API
|
||||
|
||||
func setRepresentedObjects(_ objects: [AnyObject]?, mode: TimelineSourceMode) {
|
||||
@@ -107,29 +107,29 @@ final class TimelineContainerViewController: NSViewController {
|
||||
return false
|
||||
}
|
||||
for object in representedObjects {
|
||||
guard let _ = currentObjects.firstIndex(where: { $0 === object } ) else {
|
||||
guard let _ = currentObjects.firstIndex(where: { $0 === object }) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func cleanUp() {
|
||||
regularTimelineViewController.cleanUp()
|
||||
}
|
||||
|
||||
|
||||
func toggleReadFilter() {
|
||||
regularTimelineViewController.toggleReadFilter()
|
||||
updateReadFilterButton()
|
||||
}
|
||||
|
||||
|
||||
// MARK: State Restoration
|
||||
|
||||
func saveState(to state: inout [AnyHashable : Any]) {
|
||||
|
||||
func saveState(to state: inout [AnyHashable: Any]) {
|
||||
regularTimelineViewController.saveState(to: &state)
|
||||
}
|
||||
|
||||
func restoreState(from state: [AnyHashable : Any]) {
|
||||
|
||||
func restoreState(from state: [AnyHashable: Any]) {
|
||||
regularTimelineViewController.restoreState(from: state)
|
||||
updateReadFilterButton()
|
||||
}
|
||||
@@ -144,11 +144,11 @@ extension TimelineContainerViewController: TimelineDelegate {
|
||||
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) {
|
||||
delegate?.timelineRequestedFeedSelection(self, feed: feed)
|
||||
}
|
||||
|
||||
|
||||
func timelineInvalidatedRestorationState(_: TimelineViewController) {
|
||||
delegate?.timelineInvalidatedRestorationState(self)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension TimelineContainerViewController {
|
||||
@@ -157,7 +157,7 @@ private extension TimelineContainerViewController {
|
||||
menuItem.attributedTitle = NSAttributedString(string: menuItem.title,
|
||||
attributes: [NSAttributedString.Key.font: NSFont.controlContentFont(ofSize: NSFont.systemFontSize)])
|
||||
}
|
||||
|
||||
|
||||
func timelineViewController(for mode: TimelineSourceMode) -> TimelineViewController {
|
||||
switch mode {
|
||||
case .regular:
|
||||
@@ -170,14 +170,13 @@ private extension TimelineContainerViewController {
|
||||
func mode(for timelineViewController: TimelineViewController) -> TimelineSourceMode {
|
||||
if timelineViewController === regularTimelineViewController {
|
||||
return .regular
|
||||
}
|
||||
else if timelineViewController === searchTimelineViewController {
|
||||
} else if timelineViewController === searchTimelineViewController {
|
||||
return .search
|
||||
}
|
||||
assertionFailure("Expected timelineViewController to match either regular or search timelineViewController, but it doesn’t.")
|
||||
return .regular // Should never get here.
|
||||
}
|
||||
|
||||
|
||||
func updateViewOptionsPopUpButton() {
|
||||
if AppDefaults.shared.timelineSortDirection == .orderedAscending {
|
||||
newestToOldestMenuItem.state = .off
|
||||
@@ -188,32 +187,32 @@ private extension TimelineContainerViewController {
|
||||
oldestToNewestMenuItem.state = .off
|
||||
viewOptionsPopUpButton.setTitle(newestToOldestMenuItem.title)
|
||||
}
|
||||
|
||||
|
||||
if AppDefaults.shared.timelineGroupByFeed == true {
|
||||
groupByFeedMenuItem.state = .on
|
||||
} else {
|
||||
groupByFeedMenuItem.state = .off
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateReadFilterButton() {
|
||||
guard currentTimelineViewController == regularTimelineViewController else {
|
||||
readFilteredButton.isHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let isReadFiltered = regularTimelineViewController.isReadFiltered else {
|
||||
readFilteredButton.isHidden = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
readFilteredButton.isHidden = false
|
||||
|
||||
|
||||
if isReadFiltered {
|
||||
readFilteredButton.image = AppAssets.filterActive
|
||||
} else {
|
||||
readFilteredButton.image = AppAssets.filterInactive
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
class TimelineTableRowView : NSTableRowView {
|
||||
class TimelineTableRowView: NSTableRowView {
|
||||
|
||||
private var separator: NSView?
|
||||
|
||||
|
||||
override var isOpaque: Bool {
|
||||
return true
|
||||
}
|
||||
@@ -21,14 +21,14 @@ class TimelineTableRowView : NSTableRowView {
|
||||
cellView?.isEmphasized = isEmphasized
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override var isSelected: Bool {
|
||||
didSet {
|
||||
cellView?.isSelected = isSelected
|
||||
separator?.isHidden = isSelected
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init() {
|
||||
super.init(frame: NSRect.zero)
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class TimelineTableRowView : NSTableRowView {
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
|
||||
private var cellView: TimelineTableCellView? {
|
||||
for oneSubview in subviews {
|
||||
if let foundView = oneSubview as? TimelineTableCellView {
|
||||
@@ -51,7 +51,7 @@ class TimelineTableRowView : NSTableRowView {
|
||||
addSeparatorView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func addSeparatorView() {
|
||||
guard let cellView = cellView, separator == nil else { return }
|
||||
separator = NSView()
|
||||
|
||||
@@ -10,15 +10,15 @@ import AppKit
|
||||
import RSCore
|
||||
|
||||
class TimelineTableView: NSTableView {
|
||||
|
||||
|
||||
weak var keyboardDelegate: KeyboardDelegate?
|
||||
|
||||
|
||||
override func accessibilityLabel() -> String? {
|
||||
return NSLocalizedString("Timeline", comment: "Timeline")
|
||||
}
|
||||
|
||||
|
||||
// MARK: - NSResponder
|
||||
|
||||
|
||||
override func keyDown(with event: NSEvent) {
|
||||
if keyboardDelegate?.keydown(event, in: self) ?? false {
|
||||
return
|
||||
@@ -31,14 +31,14 @@ class TimelineTableView: NSTableView {
|
||||
override var isOpaque: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override func viewWillStartLiveResize() {
|
||||
if let scrollView = self.enclosingScrollView {
|
||||
scrollView.hasVerticalScroller = false
|
||||
}
|
||||
super.viewWillStartLiveResize()
|
||||
}
|
||||
|
||||
|
||||
override func viewDidEndLiveResize() {
|
||||
if let scrollView = self.enclosingScrollView {
|
||||
scrollView.hasVerticalScroller = true
|
||||
|
||||
@@ -70,19 +70,19 @@ extension TimelineViewController {
|
||||
}
|
||||
delegate?.timelineRequestedFeedSelection(self, feed: feed)
|
||||
}
|
||||
|
||||
|
||||
@objc func markAllInFeedAsRead(_ sender: Any?) {
|
||||
guard let menuItem = sender as? NSMenuItem, let feedArticles = menuItem.representedObject as? ArticleArray else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: feedArticles, markingRead: true, undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
runCommand(markReadCommand)
|
||||
}
|
||||
|
||||
|
||||
@objc func openInBrowserFromContextualMenu(_ sender: Any?) {
|
||||
|
||||
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
||||
@@ -90,7 +90,7 @@ extension TimelineViewController {
|
||||
}
|
||||
Browser.open(urlString, inBackground: false)
|
||||
}
|
||||
|
||||
|
||||
@objc func copyURLFromContextualMenu(_ sender: Any?) {
|
||||
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
||||
return
|
||||
@@ -106,7 +106,6 @@ extension TimelineViewController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension TimelineViewController {
|
||||
|
||||
func markArticles(_ articles: [Article], read: Bool) {
|
||||
@@ -162,7 +161,7 @@ private extension TimelineViewController {
|
||||
}
|
||||
|
||||
menu.addSeparatorIfNeeded()
|
||||
|
||||
|
||||
if articles.count == 1, let feed = articles.first!.feed {
|
||||
if !(representedObjects?.contains(where: { $0 as? Feed == feed }) ?? false) {
|
||||
menu.addItem(selectFeedInSidebarMenuItem(feed))
|
||||
@@ -171,13 +170,13 @@ private extension TimelineViewController {
|
||||
menu.addItem(markAllMenuItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if articles.count == 1, let link = articles.first!.preferredLink {
|
||||
menu.addSeparatorIfNeeded()
|
||||
menu.addItem(openInBrowserMenuItem(link))
|
||||
menu.addSeparatorIfNeeded()
|
||||
menu.addItem(copyArticleURLMenuItem(link))
|
||||
|
||||
|
||||
if let externalLink = articles.first?.externalLink, externalLink != link {
|
||||
menu.addItem(copyExternalURLMenuItem(externalLink))
|
||||
}
|
||||
@@ -241,11 +240,11 @@ private extension TimelineViewController {
|
||||
}
|
||||
|
||||
func markAboveReadMenuItem(_ articles: [Article]) -> NSMenuItem {
|
||||
return menuItem(NSLocalizedString("Mark Above as Read", comment: "Command"), #selector(markAboveArticlesReadFromContextualMenu(_:)), articles)
|
||||
return menuItem(NSLocalizedString("Mark Above as Read", comment: "Command"), #selector(markAboveArticlesReadFromContextualMenu(_:)), articles)
|
||||
}
|
||||
|
||||
|
||||
func markBelowReadMenuItem(_ articles: [Article]) -> NSMenuItem {
|
||||
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
|
||||
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
|
||||
}
|
||||
|
||||
func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
|
||||
@@ -265,24 +264,23 @@ private extension TimelineViewController {
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let menuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
|
||||
|
||||
return menuItem(menuText, #selector(markAllInFeedAsRead(_:)), articles)
|
||||
}
|
||||
|
||||
|
||||
func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem {
|
||||
|
||||
return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString)
|
||||
}
|
||||
|
||||
|
||||
func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem {
|
||||
return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
|
||||
}
|
||||
|
||||
|
||||
func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem {
|
||||
return menuItem(NSLocalizedString("Copy External URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
|
||||
}
|
||||
|
||||
|
||||
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {
|
||||
|
||||
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
||||
|
||||
@@ -12,7 +12,7 @@ import Articles
|
||||
import Account
|
||||
import os.log
|
||||
|
||||
protocol TimelineDelegate: AnyObject {
|
||||
protocol TimelineDelegate: AnyObject {
|
||||
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
|
||||
func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||
@@ -42,10 +42,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
return timelineFeed.defaultReadFilterType == .read
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isCleanUpAvailable: Bool {
|
||||
let isEligibleForCleanUp: Bool?
|
||||
|
||||
|
||||
if representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem, timelineFeed.defaultReadFilterType == .alwaysRead {
|
||||
isEligibleForCleanUp = true
|
||||
} else {
|
||||
@@ -53,14 +53,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
guard isEligibleForCleanUp ?? false else { return false }
|
||||
|
||||
|
||||
let readSelectedCount = selectedArticles.filter({ $0.status.read }).count
|
||||
let readArticleCount = articles.count - unreadCount
|
||||
let availableToCleanCount = readArticleCount - readSelectedCount
|
||||
|
||||
|
||||
return availableToCleanCount > 0
|
||||
}
|
||||
|
||||
|
||||
var representedObjects: [AnyObject]? {
|
||||
didSet {
|
||||
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
|
||||
@@ -196,7 +196,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
self.init(nibName: "TimelineTableView", bundle: nil)
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
cellAppearance = TimelineCellAppearance(showIcon: false, fontSize: fontSize)
|
||||
cellAppearanceWithIcon = TimelineCellAppearance(showIcon: true, fontSize: fontSize)
|
||||
@@ -208,7 +208,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
tableView.setDraggingSourceOperationMask(.copy, forLocal: false)
|
||||
tableView.keyboardDelegate = keyboardDelegate
|
||||
tableView.style = .inset
|
||||
|
||||
|
||||
if !didRegisterForNotifications {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
|
||||
@@ -223,13 +223,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
didRegisterForNotifications = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear() {
|
||||
sharingServiceDelegate = SharingServiceDelegate(self.view.window)
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
|
||||
|
||||
func markAllAsRead(completion: (() -> Void)? = nil) {
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager, completion: completion) else {
|
||||
return
|
||||
@@ -254,7 +254,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
return representedObjects.first! === object
|
||||
}
|
||||
|
||||
|
||||
func cleanUp() {
|
||||
fetchAndReplacePreservingSelection()
|
||||
}
|
||||
@@ -265,19 +265,19 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
delegate?.timelineInvalidatedRestorationState(self)
|
||||
fetchAndReplacePreservingSelection()
|
||||
}
|
||||
|
||||
|
||||
// MARK: State Restoration
|
||||
|
||||
func saveState(to state: inout [AnyHashable : Any]) {
|
||||
|
||||
func saveState(to state: inout [AnyHashable: Any]) {
|
||||
state[UserInfoKey.readArticlesFilterStateKeys] = readFilterEnabledTable.keys.compactMap { $0.userInfo }
|
||||
state[UserInfoKey.readArticlesFilterStateValues] = readFilterEnabledTable.values.compactMap( { $0 })
|
||||
|
||||
|
||||
if selectedArticles.count == 1 {
|
||||
state[UserInfoKey.articlePath] = selectedArticles.first!.pathUserInfo
|
||||
}
|
||||
}
|
||||
|
||||
func restoreState(from state: [AnyHashable : Any]) {
|
||||
|
||||
func restoreState(from state: [AnyHashable: Any]) {
|
||||
guard let readArticlesFilterStateKeys = state[UserInfoKey.readArticlesFilterStateKeys] as? [[AnyHashable: AnyHashable]],
|
||||
let readArticlesFilterStateValues = state[UserInfoKey.readArticlesFilterStateValues] as? [Bool] else {
|
||||
return
|
||||
@@ -288,15 +288,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
readFilterEnabledTable[feedIdentifier] = readArticlesFilterStateValues[i]
|
||||
}
|
||||
}
|
||||
|
||||
if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
|
||||
if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable: Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let account = AccountManager.shared.existingAccount(with: accountID),
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String {
|
||||
|
||||
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
|
||||
fetchAndReplaceArticlesSync()
|
||||
|
||||
|
||||
if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) {
|
||||
tableView.selectRow(selectedIndex)
|
||||
DispatchQueue.main.async {
|
||||
@@ -306,13 +306,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
fetchAndReplaceArticlesSync()
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc func openArticleInBrowser(_ sender: Any?) {
|
||||
@@ -320,7 +320,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
|
||||
guard !selectedArticles.isEmpty else {
|
||||
return
|
||||
@@ -331,8 +331,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
if markAsRead {
|
||||
markSelectedArticlesAsRead(sender)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
markSelectedArticlesAsUnread(sender)
|
||||
}
|
||||
}
|
||||
@@ -343,7 +342,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
|
||||
guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else {
|
||||
return
|
||||
@@ -359,36 +358,36 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
guard let lastSelectedRow = tableView.selectedRowIndexes.last else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let nextRowIndex = lastSelectedRow - 1
|
||||
if nextRowIndex <= 0 {
|
||||
tableView.scrollTo(row: 0, extraHeight: 0)
|
||||
}
|
||||
|
||||
|
||||
tableView.selectRow(nextRowIndex)
|
||||
|
||||
|
||||
let followingRowIndex = nextRowIndex - 1
|
||||
if followingRowIndex < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
tableView.scrollToRowIfNotVisible(followingRowIndex)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@IBAction func selectNextDown(_ sender: Any?) {
|
||||
guard let firstSelectedRow = tableView.selectedRowIndexes.first else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let tableMaxIndex = tableView.numberOfRows - 1
|
||||
let nextRowIndex = firstSelectedRow + 1
|
||||
if nextRowIndex >= tableMaxIndex {
|
||||
tableView.scrollTo(row: tableMaxIndex, extraHeight: 0)
|
||||
}
|
||||
|
||||
|
||||
tableView.selectRow(nextRowIndex)
|
||||
|
||||
|
||||
let followingRowIndex = nextRowIndex + 1
|
||||
if followingRowIndex > tableMaxIndex {
|
||||
return
|
||||
@@ -397,11 +396,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
tableView.scrollToRowIfNotVisible(followingRowIndex)
|
||||
|
||||
}
|
||||
|
||||
|
||||
func toggleReadStatusForSelectedArticles() {
|
||||
// If any one of the selected articles is unread, then mark them as read.
|
||||
// If all articles are read, then mark them as unread them.
|
||||
|
||||
|
||||
let commandStatus = markReadCommandStatus()
|
||||
let markingRead: Bool
|
||||
switch commandStatus {
|
||||
@@ -412,14 +411,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
case .canDoNothing:
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: markingRead, undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
runCommand(markStarredCommand)
|
||||
}
|
||||
|
||||
|
||||
func toggleStarredStatusForSelectedArticles() {
|
||||
|
||||
// If any one of the selected articles is not starred, then star them.
|
||||
@@ -448,11 +447,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
func markReadCommandStatus() -> MarkCommandValidationStatus {
|
||||
let articles = selectedArticles
|
||||
|
||||
|
||||
if articles.anyArticleIsUnread() {
|
||||
return .canMark
|
||||
}
|
||||
|
||||
|
||||
if articles.anyArticleIsReadAndCanMarkUnread() {
|
||||
return .canUnmark
|
||||
}
|
||||
@@ -485,12 +484,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
func markOlderArticlesRead(_ selectedArticles: [Article]) {
|
||||
// Mark articles older than the selectedArticles(s) as read.
|
||||
|
||||
var cutoffDate: Date? = nil
|
||||
var cutoffDate: Date?
|
||||
for article in selectedArticles {
|
||||
if cutoffDate == nil {
|
||||
cutoffDate = article.logicalDatePublished
|
||||
}
|
||||
else if cutoffDate! > article.logicalDatePublished {
|
||||
} else if cutoffDate! > article.logicalDatePublished {
|
||||
cutoffDate = article.logicalDatePublished
|
||||
}
|
||||
}
|
||||
@@ -530,8 +528,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
func goToDeepLink(for userInfo: [AnyHashable : Any]) {
|
||||
|
||||
func goToDeepLink(for userInfo: [AnyHashable: Any]) {
|
||||
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
|
||||
|
||||
if isReadFiltered ?? false {
|
||||
@@ -543,12 +541,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return }
|
||||
|
||||
|
||||
NSCursor.setHiddenUntilMouseMoves(true)
|
||||
tableView.selectRow(ix)
|
||||
tableView.scrollTo(row: ix)
|
||||
}
|
||||
|
||||
|
||||
func goToNextUnread(wrappingToTop wrapping: Bool = false) {
|
||||
guard let ix = indexOfNextUnreadArticle() else {
|
||||
return
|
||||
@@ -557,14 +555,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
tableView.selectRow(ix)
|
||||
tableView.scrollTo(row: ix)
|
||||
}
|
||||
|
||||
|
||||
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
|
||||
guard let _ = indexOfNextUnreadArticle(wrappingToTop: wrapping) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func indexOfNextUnreadArticle(wrappingToTop wrapping: Bool = false) -> Int? {
|
||||
return articles.rowOfNextUnreadArticle(tableView.selectedRow, wrappingToTop: wrapping)
|
||||
}
|
||||
@@ -573,7 +571,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
guard let window = tableView.window else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
window.makeFirstResponderUnlessDescendantIsFirstResponder(tableView)
|
||||
if !hasAtLeastOneSelectedArticle && articles.count > 0 {
|
||||
tableView.selectRowAndScrollToVisible(0)
|
||||
@@ -648,7 +646,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
fetchAndReplaceArticlesAsync()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func accountsDidChange(_ note: Notification) {
|
||||
if representedObjectsContainsAnyPseudoFeed() {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
@@ -666,7 +664,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
self.sortDirection = AppDefaults.shared.timelineSortDirection
|
||||
self.groupByFeed = AppDefaults.shared.timelineGroupByFeed
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Reloading Data
|
||||
|
||||
private func cellForRowView(_ rowView: NSView) -> NSView? {
|
||||
@@ -682,7 +680,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
reloadVisibleCells(for: indexes)
|
||||
}
|
||||
|
||||
|
||||
private func reloadVisibleCells(for articles: [Article]) {
|
||||
reloadVisibleCells(for: Set(articles.articleIDs()))
|
||||
}
|
||||
@@ -690,7 +688,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
private func reloadVisibleCells(for articles: Set<Article>) {
|
||||
reloadVisibleCells(for: articles.articleIDs())
|
||||
}
|
||||
|
||||
|
||||
private func reloadVisibleCells(for articleIDs: Set<String>) {
|
||||
if articleIDs.isEmpty {
|
||||
return
|
||||
@@ -714,7 +712,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
tableView.reloadData(forRowIndexes: indexes, columnIndexes: NSIndexSet(index: 0) as IndexSet)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Cell Configuring
|
||||
|
||||
private func calculateRowHeight() -> CGFloat {
|
||||
@@ -833,8 +831,7 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
cell.cellAppearance = showIcons ? cellAppearanceWithIcon : cellAppearance
|
||||
if let article = articles.articleAtRow(row) {
|
||||
configureTimelineCell(cell, article: article)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
makeTimelineCellEmpty(cell)
|
||||
}
|
||||
}
|
||||
@@ -917,16 +914,16 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
|
||||
switch edge {
|
||||
case .leading:
|
||||
let action = NSTableViewRowAction(style: .regular, title: article.status.read ? "Unread" : "Read") { (action, row) in
|
||||
self.toggleArticleRead(article);
|
||||
let action = NSTableViewRowAction(style: .regular, title: article.status.read ? "Unread" : "Read") { (_, _) in
|
||||
self.toggleArticleRead(article)
|
||||
tableView.rowActionsVisible = false
|
||||
}
|
||||
action.image = article.status.read ? AppAssets.swipeMarkUnreadImage : AppAssets.swipeMarkReadImage
|
||||
return [action]
|
||||
|
||||
case .trailing:
|
||||
let action = NSTableViewRowAction(style: .regular, title: article.status.starred ? "Unstar" : "Star") { (action, row) in
|
||||
self.toggleArticleStarred(article);
|
||||
let action = NSTableViewRowAction(style: .regular, title: article.status.starred ? "Unstar" : "Star") { (_, _) in
|
||||
self.toggleArticleStarred(article)
|
||||
tableView.rowActionsVisible = false
|
||||
}
|
||||
action.backgroundColor = AppAssets.starColor
|
||||
@@ -944,7 +941,7 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
// MARK: - Private
|
||||
|
||||
private extension TimelineViewController {
|
||||
|
||||
|
||||
func fetchAndReplacePreservingSelection() {
|
||||
if let article = oneSelectedArticle, let account = article.account {
|
||||
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID)
|
||||
@@ -953,7 +950,7 @@ private extension TimelineViewController {
|
||||
fetchAndReplaceArticlesSync()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func reloadAvailableCells() {
|
||||
if let indexesToReload = tableView.indexesOfAvailableRows() {
|
||||
reloadCells(for: indexesToReload)
|
||||
@@ -988,7 +985,7 @@ private extension TimelineViewController {
|
||||
self.showIcons = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
for article in articles {
|
||||
if let authors = article.authors {
|
||||
for author in authors {
|
||||
@@ -1015,7 +1012,7 @@ private extension TimelineViewController {
|
||||
replaceArticles(with: unsortedArticles)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func selectedArticleIDs() -> [String] {
|
||||
return selectedArticles.articleIDs()
|
||||
}
|
||||
@@ -1090,7 +1087,7 @@ private extension TimelineViewController {
|
||||
}
|
||||
|
||||
// MARK: - Fetching Articles
|
||||
|
||||
|
||||
func fetchAndReplaceArticlesSync() {
|
||||
// To be called when the user has made a change of selection in the sidebar.
|
||||
// It blocks the main thread, so that there’s no async delay,
|
||||
@@ -1101,12 +1098,12 @@ private extension TimelineViewController {
|
||||
emptyTheTimeline()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if exceptionArticleFetcher != nil {
|
||||
representedObjects.append(exceptionArticleFetcher as AnyObject)
|
||||
exceptionArticleFetcher = nil
|
||||
}
|
||||
|
||||
|
||||
let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects)
|
||||
replaceArticles(with: fetchedArticles)
|
||||
}
|
||||
@@ -1119,12 +1116,12 @@ private extension TimelineViewController {
|
||||
emptyTheTimeline()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if exceptionArticleFetcher != nil {
|
||||
representedObjects.append(exceptionArticleFetcher as AnyObject)
|
||||
exceptionArticleFetcher = nil
|
||||
}
|
||||
|
||||
|
||||
fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (articles) in
|
||||
self?.replaceArticles(with: articles)
|
||||
}
|
||||
@@ -1141,7 +1138,7 @@ private extension TimelineViewController {
|
||||
|
||||
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {
|
||||
cancelPendingAsyncFetches()
|
||||
let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher }
|
||||
let fetchers = representedObjects.compactMap { $0 as? ArticleFetcher }
|
||||
if fetchers.isEmpty {
|
||||
return Set<Article>()
|
||||
}
|
||||
@@ -1236,8 +1233,7 @@ private extension TimelineViewController {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let folder = representedObject as? Folder {
|
||||
} else if let folder = representedObject as? Folder {
|
||||
for oneFeed in feeds {
|
||||
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user