mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'main' into bsc-662-catch-up
This commit is contained in:
55
Mac/MainWindow/About/AboutNetNewsWireView.swift
Normal file
55
Mac/MainWindow/About/AboutNetNewsWireView.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// AboutNetNewsWireView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 03/10/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 12, *)
|
||||
struct AboutNetNewsWireView: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack(spacing: 8) {
|
||||
Spacer()
|
||||
|
||||
Image("About")
|
||||
.resizable()
|
||||
.frame(width: 75, height: 75)
|
||||
|
||||
Text("NetNewsWire")
|
||||
.font(.headline)
|
||||
|
||||
Text("\(Bundle.main.versionNumber) (\(Bundle.main.buildNumber))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.callout)
|
||||
|
||||
Text("By Brent Simmons and the NetNewsWire team.")
|
||||
.font(.subheadline)
|
||||
|
||||
Text("[netnewswire.com](https://netnewswire.com)")
|
||||
.font(.callout)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(verbatim: "Copyright © Brent Simmons 2002 - \(Calendar.current.component(.year, from: .now))")
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(width: 400, height: 400)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 12, *)
|
||||
struct AboutNetNewsWireView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AboutNetNewsWireView()
|
||||
}
|
||||
}
|
||||
131
Mac/MainWindow/About/AboutWindowController.swift
Normal file
131
Mac/MainWindow/About/AboutWindowController.swift
Normal file
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// AboutWindowController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 03/10/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import RSCore
|
||||
|
||||
extension NSToolbarItem.Identifier {
|
||||
static let aboutGroup = NSToolbarItem.Identifier("about.toolbar.group")
|
||||
}
|
||||
|
||||
extension NSUserInterfaceItemIdentifier {
|
||||
static let aboutNetNewsWire = NSUserInterfaceItemIdentifier("about.netnewswire")
|
||||
}
|
||||
|
||||
// MARK: - AboutWindowController
|
||||
|
||||
@available(macOS 12, *)
|
||||
class AboutWindowController: NSWindowController, NSToolbarDelegate {
|
||||
|
||||
var hostingController: AboutHostingController
|
||||
|
||||
override init(window: NSWindow?) {
|
||||
self.hostingController = AboutHostingController(rootView: AnyView(AboutNetNewsWireView()))
|
||||
super.init(window: window)
|
||||
let window = NSWindow(contentViewController: hostingController)
|
||||
window.identifier = .aboutNetNewsWire
|
||||
window.standardWindowButton(.zoomButton)?.isEnabled = false
|
||||
window.titleVisibility = .hidden
|
||||
self.window = window
|
||||
self.hostingController.configureToolbar()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
super.windowDidLoad()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AboutHostingController
|
||||
|
||||
@available(macOS 12, *)
|
||||
class AboutHostingController: NSHostingController<AnyView>, NSToolbarDelegate {
|
||||
|
||||
private lazy var segmentedControl: NSSegmentedControl = {
|
||||
let control = NSSegmentedControl(labels: ["About", "Credits"],
|
||||
trackingMode: .selectOne,
|
||||
target: self,
|
||||
action: #selector(segmentedControlSelectionChanged(_:)))
|
||||
control.segmentCount = 2
|
||||
control.setSelected(true, forSegment: 0)
|
||||
return control
|
||||
}()
|
||||
|
||||
override init(rootView: AnyView) {
|
||||
super.init(rootView: rootView)
|
||||
}
|
||||
|
||||
@MainActor required dynamic init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func configureToolbar() {
|
||||
let toolbar = NSToolbar(identifier: NSToolbar.Identifier("netnewswire.about.toolbar"))
|
||||
toolbar.delegate = self
|
||||
toolbar.autosavesConfiguration = false
|
||||
toolbar.allowsUserCustomization = false
|
||||
view.window?.toolbar = toolbar
|
||||
view.window?.toolbarStyle = .unified
|
||||
toolbar.insertItem(withItemIdentifier: .flexibleSpace, at: 0)
|
||||
toolbar.insertItem(withItemIdentifier: .flexibleSpace, at: 2)
|
||||
}
|
||||
|
||||
// MARK: NSToolbarDelegate
|
||||
|
||||
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
|
||||
switch itemIdentifier {
|
||||
|
||||
case .aboutGroup:
|
||||
let toolbarItem = NSToolbarItem(itemIdentifier: .aboutGroup)
|
||||
toolbarItem.view = segmentedControl
|
||||
toolbarItem.autovalidates = true
|
||||
return toolbarItem
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
return [.aboutGroup]
|
||||
}
|
||||
|
||||
func toolbarWillAddItem(_ notification: Notification) {
|
||||
//
|
||||
}
|
||||
|
||||
func toolbarDidRemoveItem(_ notification: Notification) {
|
||||
//
|
||||
}
|
||||
|
||||
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
return [.aboutGroup]
|
||||
}
|
||||
|
||||
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
return []
|
||||
}
|
||||
|
||||
// MARK: - Target/Action
|
||||
@objc
|
||||
func segmentedControlSelectionChanged(_ sender: NSSegmentedControl) {
|
||||
if sender.selectedSegment == 0 {
|
||||
rootView = AnyView(AboutNetNewsWireView())
|
||||
} else {
|
||||
rootView = AnyView(CreditsNetNewsWireView())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
80
Mac/MainWindow/About/CreditsNetNewsWireView.swift
Normal file
80
Mac/MainWindow/About/CreditsNetNewsWireView.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// CreditsNetNewsWireView.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Stuart Breckenridge on 03/10/2022.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@available(macOS 12, *)
|
||||
struct CreditsNetNewsWireView: View, LoadableAboutData {
|
||||
var body: some View {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
Spacer()
|
||||
.frame(height: 12)
|
||||
Section("Primary Contributors") {
|
||||
GroupBox {
|
||||
ForEach(0..<about.PrimaryContributors.count, id: \.self) { i in
|
||||
contributorView(about.PrimaryContributors[i])
|
||||
.padding(.vertical, 2)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Section("Additional Contributors") {
|
||||
GroupBox {
|
||||
ForEach(0..<about.AdditionalContributors.count, id: \.self) { i in
|
||||
contributorView(about.AdditionalContributors[i])
|
||||
.padding(.vertical, 2)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 12))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Section("Thanks") {
|
||||
GroupBox {
|
||||
Text(about.ThanksMarkdown)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout)
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
}
|
||||
Spacer()
|
||||
.frame(height: 12)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(width: 400, height: 400)
|
||||
}
|
||||
|
||||
func contributorView(_ appCredit: AboutData.Contributor) -> some View {
|
||||
HStack {
|
||||
Text(appCredit.name)
|
||||
Spacer()
|
||||
if let role = appCredit.role {
|
||||
Text(role)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.onTapGesture {
|
||||
guard let url = appCredit.url else { return }
|
||||
if let _ = URL(string: url) {
|
||||
Browser.open(url, inBackground: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 12, *)
|
||||
struct CreditsNetNewsWireView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CreditsNetNewsWireView()
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,10 @@ final class DetailContainerView: NSView, NSTextFinderBarContainer {
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(contentView, positioned: .below, relativeTo: detailStatusBarView)
|
||||
|
||||
// Constrain the content view to fill the available space on all sides except the top, which we'll constrain to the find bar
|
||||
var constraints = constraintsToMakeSubViewFullSize(contentView).filter { $0.firstAttribute != .top }
|
||||
// Constrain the content view to fill the available space on all sides
|
||||
var constraints = constraintsToMakeSubViewFullSize(contentView)
|
||||
|
||||
constraints.append(findBarContainerView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor))
|
||||
constraints.append(findBarContainerView.bottomAnchor.constraint(equalTo: contentView.topAnchor))
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
contentViewConstraints = constraints
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ private extension NSUserInterfaceItemIdentifier {
|
||||
|
||||
private extension DetailWebView {
|
||||
|
||||
static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload, .DetailMenuItemIdentifierOpenLink]
|
||||
static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload]
|
||||
static let menuItemIdentifierMatchStrings = ["newwindow", "download"]
|
||||
|
||||
func shouldHideMenuItem(_ menuItem: NSMenuItem) -> Bool {
|
||||
|
||||
@@ -204,7 +204,15 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||
|
||||
if item.action == #selector(copyArticleURL(_:)) {
|
||||
return canCopyArticleURL()
|
||||
let canCopyArticleURL = canCopyArticleURL()
|
||||
|
||||
if let item = item as? NSMenuItem {
|
||||
let format = NSLocalizedString("Copy Article URL", comment: "Copy Article URL");
|
||||
|
||||
item.title = String.localizedStringWithFormat(format, selectedArticles?.count ?? 0)
|
||||
}
|
||||
|
||||
return canCopyArticleURL
|
||||
}
|
||||
|
||||
if item.action == #selector(copyExternalURL(_:)) {
|
||||
@@ -321,21 +329,21 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
}
|
||||
|
||||
@IBAction func copyArticleURL(_ sender: Any?) {
|
||||
if let link = oneSelectedArticle?.preferredURL?.absoluteString {
|
||||
URLPasteboardWriter.write(urlString: link, to: .general)
|
||||
if let currentLinks {
|
||||
URLPasteboardWriter.write(urlStrings: currentLinks, alertingIn: window)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func copyExternalURL(_ sender: Any?) {
|
||||
if let link = oneSelectedArticle?.externalLink {
|
||||
URLPasteboardWriter.write(urlString: link, to: .general)
|
||||
if let links = selectedArticles?.compactMap({ $0.externalLink }) {
|
||||
URLPasteboardWriter.write(urlStrings: links, to: .general)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func openArticleInBrowser(_ sender: Any?) {
|
||||
if let link = currentLink {
|
||||
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
guard let selectedArticles else { return }
|
||||
let urlStrings = selectedArticles.compactMap { $0.preferredLink }
|
||||
Browser.open(urlStrings, fromWindow: window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
|
||||
@IBAction func openInBrowser(_ sender: Any?) {
|
||||
@@ -529,16 +537,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
assertionFailure("Expected toolbarShowShareMenu to be called only by the Share item in the toolbar.")
|
||||
return
|
||||
}
|
||||
guard let view = shareToolbarItem.view else {
|
||||
// TODO: handle menu form representation
|
||||
return
|
||||
}
|
||||
|
||||
let sortedArticles = selectedArticles.sortedByDate(.orderedAscending)
|
||||
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
|
||||
let sharingServicePicker = NSSharingServicePicker(items: items)
|
||||
sharingServicePicker.delegate = sharingServicePickerDelegate
|
||||
sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
|
||||
|
||||
if let view = shareToolbarItem.view, view.window != nil {
|
||||
sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
|
||||
} else if let view = window?.contentView {
|
||||
sharingServicePicker.show(relativeTo: CGRect(x: view.frame.width / 2.0, y: view.frame.height - 4, width: 1, height: 1), of: view, preferredEdge: .minY)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func moveFocusToSearchField(_ sender: Any?) {
|
||||
@@ -628,6 +637,10 @@ extension MainWindowController: NSWindowDelegate {
|
||||
|
||||
extension MainWindowController: SidebarDelegate {
|
||||
|
||||
var directlyMarkedAsUnreadArticles: Set<Article>? {
|
||||
return timelineContainerViewController?.currentTimelineViewController?.directlyMarkedAsUnreadArticles
|
||||
}
|
||||
|
||||
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?) {
|
||||
// Don’t update the timeline if it already has those objects.
|
||||
let representedObjectsAreTheSame = timelineContainerViewController?.regularTimelineViewControllerHasRepresentedObjects(selectedObjects) ?? false
|
||||
@@ -666,6 +679,9 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
|
||||
articleExtractor = nil
|
||||
isShowingExtractedArticle = false
|
||||
makeToolbarValidate()
|
||||
if #available(macOS 13.0, *) { } else {
|
||||
updateShareToolbarItemMenu()
|
||||
}
|
||||
|
||||
let detailState: DetailState
|
||||
if let articles = articles {
|
||||
@@ -894,11 +910,23 @@ extension MainWindowController: NSToolbarDelegate {
|
||||
button.action = #selector(toggleArticleExtractor(_:))
|
||||
button.rightClickAction = #selector(showArticleExtractorMenu(_:))
|
||||
toolbarItem.view = button
|
||||
toolbarItem.menuFormRepresentation = NSMenuItem(title: description, action: #selector(toggleArticleExtractor(_:)), keyEquivalent: "")
|
||||
return toolbarItem
|
||||
|
||||
case .share:
|
||||
let title = NSLocalizedString("Share", comment: "Share")
|
||||
return buildToolbarButton(.share, title, AppAssets.shareImage, "toolbarShowShareMenu:")
|
||||
let image = AppAssets.shareImage
|
||||
if #available(macOS 13.0, *) {
|
||||
// `item.view` is required for properly positioning the sharing picker.
|
||||
return buildToolbarButton(.share, title, image, "toolbarShowShareMenu:", usesCustomButtonView: true)
|
||||
} else {
|
||||
let item = NSMenuToolbarItem(itemIdentifier: .share)
|
||||
item.image = image
|
||||
item.toolTip = title
|
||||
item.label = title
|
||||
item.showsIndicator = false
|
||||
return item
|
||||
}
|
||||
|
||||
case .openInBrowser:
|
||||
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
||||
@@ -1043,7 +1071,11 @@ private extension MainWindowController {
|
||||
}
|
||||
|
||||
var currentLink: String? {
|
||||
return oneSelectedArticle?.preferredLink
|
||||
return selectedArticles?.first { $0.preferredLink != nil }?.preferredLink
|
||||
}
|
||||
|
||||
var currentLinks: [String?]? {
|
||||
return selectedArticles?.map { $0.preferredLink }
|
||||
}
|
||||
|
||||
// MARK: - State Restoration
|
||||
@@ -1081,7 +1113,11 @@ private extension MainWindowController {
|
||||
// MARK: - Command Validation
|
||||
|
||||
func canCopyArticleURL() -> Bool {
|
||||
return currentLink != nil
|
||||
if let currentLinks, currentLinks.count != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func canCopyExternalURL() -> Bool {
|
||||
@@ -1130,16 +1166,13 @@ private extension MainWindowController {
|
||||
|
||||
if let toolbarItem = item as? NSToolbarItem {
|
||||
toolbarItem.toolTip = commandName
|
||||
toolbarItem.image = markingRead ? AppAssets.readClosedImage : AppAssets.readOpenImage
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1220,16 +1253,13 @@ private extension MainWindowController {
|
||||
|
||||
if let toolbarItem = item as? NSToolbarItem {
|
||||
toolbarItem.toolTip = commandName
|
||||
toolbarItem.image = starring ? AppAssets.starOpenImage : AppAssets.starClosedImage
|
||||
}
|
||||
|
||||
if let menuItem = item as? NSMenuItem {
|
||||
menuItem.title = commandName
|
||||
}
|
||||
|
||||
if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
||||
button.image = starring ? AppAssets.starOpenImage : AppAssets.starClosedImage
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1252,24 +1282,24 @@ private extension MainWindowController {
|
||||
|
||||
guard let isReadFiltered = timelineContainerViewController?.isReadFiltered else {
|
||||
(item as? NSMenuItem)?.title = hideCommand
|
||||
if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
||||
if let toolbarItem = item as? NSToolbarItem {
|
||||
toolbarItem.toolTip = hideCommand
|
||||
button.image = AppAssets.filterInactive
|
||||
toolbarItem.image = AppAssets.filterInactive
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if isReadFiltered {
|
||||
(item as? NSMenuItem)?.title = showCommand
|
||||
if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
||||
if let toolbarItem = item as? NSToolbarItem {
|
||||
toolbarItem.toolTip = showCommand
|
||||
button.image = AppAssets.filterActive
|
||||
toolbarItem.image = AppAssets.filterActive
|
||||
}
|
||||
} else {
|
||||
(item as? NSMenuItem)?.title = hideCommand
|
||||
if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
||||
if let toolbarItem = item as? NSToolbarItem {
|
||||
toolbarItem.toolTip = hideCommand
|
||||
button.image = AppAssets.filterInactive
|
||||
toolbarItem.image = AppAssets.filterInactive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1386,19 +1416,26 @@ private extension MainWindowController {
|
||||
}
|
||||
}
|
||||
|
||||
func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String) -> NSToolbarItem {
|
||||
func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String, usesCustomButtonView: Bool = false) -> 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
|
||||
|
||||
if usesCustomButtonView {
|
||||
let button = NSButton()
|
||||
button.bezelStyle = .texturedRounded
|
||||
button.image = image
|
||||
button.imageScaling = .scaleProportionallyDown
|
||||
button.action = Selector((selector))
|
||||
toolbarItem.view = button
|
||||
toolbarItem.menuFormRepresentation = NSMenuItem(title: title, action: Selector((selector)), keyEquivalent: "")
|
||||
} else {
|
||||
toolbarItem.image = image
|
||||
toolbarItem.isBordered = true
|
||||
toolbarItem.action = Selector((selector))
|
||||
}
|
||||
return toolbarItem
|
||||
}
|
||||
|
||||
@@ -1434,7 +1471,7 @@ private extension MainWindowController {
|
||||
let defaultThemeItem = NSMenuItem()
|
||||
defaultThemeItem.title = ArticleTheme.defaultTheme.name
|
||||
defaultThemeItem.action = #selector(selectArticleTheme(_:))
|
||||
defaultThemeItem.state = defaultThemeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off
|
||||
defaultThemeItem.state = defaultThemeItem.title == ArticleThemesManager.shared.currentTheme.name ? .on : .off
|
||||
articleThemeMenu.addItem(defaultThemeItem)
|
||||
|
||||
articleThemeMenu.addItem(NSMenuItem.separator())
|
||||
@@ -1443,7 +1480,7 @@ private extension MainWindowController {
|
||||
let themeItem = NSMenuItem()
|
||||
themeItem.title = themeName
|
||||
themeItem.action = #selector(selectArticleTheme(_:))
|
||||
themeItem.state = themeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off
|
||||
themeItem.state = themeItem.title == ArticleThemesManager.shared.currentTheme.name ? .on : .off
|
||||
articleThemeMenu.addItem(themeItem)
|
||||
}
|
||||
|
||||
@@ -1451,5 +1488,17 @@ private extension MainWindowController {
|
||||
articleThemePopUpButton?.menu = articleThemeMenu
|
||||
}
|
||||
|
||||
func updateShareToolbarItemMenu() {
|
||||
guard let shareToolbarItem = shareToolbarItem as? NSMenuToolbarItem else {
|
||||
return
|
||||
}
|
||||
if let shareMenu = shareMenu {
|
||||
shareToolbarItem.isEnabled = true
|
||||
shareToolbarItem.menu = shareMenu
|
||||
} else {
|
||||
shareToolbarItem.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ final class SidebarStatusBarView: NSView {
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
progressIndicator.isHidden = true
|
||||
progressIndicator.usesThreadedAnimation = true
|
||||
progressLabel.isHidden = true
|
||||
|
||||
let progressLabelFontSize = progressLabel.font?.pointSize ?? 13.0
|
||||
@@ -43,20 +43,18 @@ final class SidebarStatusBarView: NSView {
|
||||
}
|
||||
|
||||
@objc func updateUI() {
|
||||
|
||||
guard let progress = progress else {
|
||||
stopProgressIfNeeded()
|
||||
return
|
||||
}
|
||||
|
||||
updateProgressIndicator(progress)
|
||||
updateProgressLabel(progress)
|
||||
progressLabel.stringValue = progress.label
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc dynamic func progressDidChange(_ notification: Notification) {
|
||||
|
||||
progress = AccountManager.shared.combinedRefreshProgress
|
||||
}
|
||||
}
|
||||
@@ -68,10 +66,10 @@ private extension SidebarStatusBarView {
|
||||
static let animationDuration = 0.2
|
||||
|
||||
func stopProgressIfNeeded() {
|
||||
|
||||
if !isAnimatingProgress {
|
||||
return
|
||||
}
|
||||
|
||||
isAnimatingProgress = false
|
||||
self.progressIndicator.stopAnimation(self)
|
||||
progressIndicator.isHidden = true
|
||||
@@ -88,10 +86,10 @@ private extension SidebarStatusBarView {
|
||||
}
|
||||
|
||||
func startProgressIfNeeded() {
|
||||
|
||||
if isAnimatingProgress {
|
||||
return
|
||||
}
|
||||
|
||||
isAnimatingProgress = true
|
||||
progressIndicator.isHidden = false
|
||||
progressLabel.isHidden = false
|
||||
@@ -108,12 +106,16 @@ private extension SidebarStatusBarView {
|
||||
}
|
||||
|
||||
func updateProgressIndicator(_ progress: CombinedRefreshProgress) {
|
||||
|
||||
if progress.isComplete {
|
||||
stopProgressIfNeeded()
|
||||
return
|
||||
}
|
||||
|
||||
if progressIndicator.isIndeterminate != progress.isIndeterminate {
|
||||
stopProgressIfNeeded()
|
||||
progressIndicator.isIndeterminate = progress.isIndeterminate
|
||||
}
|
||||
|
||||
startProgressIfNeeded()
|
||||
|
||||
let maxValue = Double(progress.numberOfTasks)
|
||||
@@ -127,16 +129,4 @@ private extension SidebarStatusBarView {
|
||||
}
|
||||
}
|
||||
|
||||
func updateProgressLabel(_ progress: CombinedRefreshProgress) {
|
||||
|
||||
if progress.isComplete {
|
||||
progressLabel.stringValue = ""
|
||||
return
|
||||
}
|
||||
|
||||
let formatString = NSLocalizedString("%@ of %@", comment: "Status bar progress")
|
||||
let s = NSString(format: formatString as NSString, NSNumber(value: progress.numberCompleted), NSNumber(value: progress.numberOfTasks))
|
||||
|
||||
progressLabel.stringValue = s as String
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,16 @@ extension SidebarViewController {
|
||||
return
|
||||
}
|
||||
|
||||
let articles = unreadArticles(for: objects)
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else {
|
||||
var markableArticles = unreadArticles(for: objects)
|
||||
if let directlyMarkedAsUnreadArticles = delegate?.directlyMarkedAsUnreadArticles {
|
||||
markableArticles = markableArticles.subtracting(directlyMarkedAsUnreadArticles)
|
||||
}
|
||||
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
|
||||
@@ -17,6 +17,7 @@ extension Notification.Name {
|
||||
}
|
||||
|
||||
protocol SidebarDelegate: AnyObject {
|
||||
var directlyMarkedAsUnreadArticles: Set<Article>? { get }
|
||||
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?)
|
||||
func unreadCount(for: AnyObject) -> Int
|
||||
func sidebarInvalidatedRestorationState(_: SidebarViewController)
|
||||
@@ -256,7 +257,11 @@ protocol SidebarDelegate: AnyObject {
|
||||
return
|
||||
}
|
||||
if AppDefaults.shared.feedDoubleClickMarkAsRead, let articles = try? singleSelectedWebFeed?.fetchUnreadArticles() {
|
||||
if let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) {
|
||||
if let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: Array(articles),
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager) {
|
||||
runCommand(markReadCommand)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ extension TimelineViewController {
|
||||
|
||||
@objc func markArticlesReadFromContextualMenu(_ sender: Any?) {
|
||||
guard let articles = articles(from: sender) else { return }
|
||||
markArticles(articles, read: true)
|
||||
markArticles(articles, read: true, directlyMarked: true)
|
||||
}
|
||||
|
||||
@objc func markArticlesUnreadFromContextualMenu(_ sender: Any?) {
|
||||
guard let articles = articles(from: sender) else { return }
|
||||
markArticles(articles, read: false)
|
||||
markArticles(articles, read: false, directlyMarked: true)
|
||||
}
|
||||
|
||||
@objc func markAboveArticlesReadFromContextualMenu(_ sender: Any?) {
|
||||
@@ -59,14 +59,14 @@ extension TimelineViewController {
|
||||
|
||||
@objc func markArticlesStarredFromContextualMenu(_ sender: Any?) {
|
||||
guard let articles = articles(from: sender) else { return }
|
||||
markArticles(articles, starred: true)
|
||||
markArticles(articles, starred: true, directlyMarked: true)
|
||||
}
|
||||
|
||||
@objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) {
|
||||
guard let articles = articles(from: sender) else {
|
||||
return
|
||||
}
|
||||
markArticles(articles, starred: false)
|
||||
markArticles(articles, starred: false, directlyMarked: true)
|
||||
}
|
||||
|
||||
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
|
||||
@@ -81,7 +81,11 @@ extension TimelineViewController {
|
||||
return
|
||||
}
|
||||
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: feedArticles, markingRead: true, undoManager: undoManager) else {
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: feedArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,18 +93,19 @@ extension TimelineViewController {
|
||||
}
|
||||
|
||||
@objc func openInBrowserFromContextualMenu(_ sender: Any?) {
|
||||
|
||||
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
||||
guard let menuItem = sender as? NSMenuItem, let urlStrings = menuItem.representedObject as? [String] else {
|
||||
return
|
||||
}
|
||||
Browser.open(urlString, inBackground: false)
|
||||
|
||||
Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
|
||||
@objc func copyURLFromContextualMenu(_ sender: Any?) {
|
||||
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
||||
guard let menuItem = sender as? NSMenuItem, let urlStrings = menuItem.representedObject as? [String?] else {
|
||||
return
|
||||
}
|
||||
URLPasteboardWriter.write(urlString: urlString, to: .general)
|
||||
|
||||
URLPasteboardWriter.write(urlStrings: urlStrings, alertingIn: self.view.window)
|
||||
}
|
||||
|
||||
@objc func performShareServiceFromContextualMenu(_ sender: Any?) {
|
||||
@@ -114,16 +119,21 @@ extension TimelineViewController {
|
||||
|
||||
private extension TimelineViewController {
|
||||
|
||||
func markArticles(_ articles: [Article], read: Bool) {
|
||||
markArticles(articles, statusKey: .read, flag: read)
|
||||
func markArticles(_ articles: [Article], read: Bool, directlyMarked: Bool) {
|
||||
markArticles(articles, statusKey: .read, flag: read, directlyMarked: directlyMarked)
|
||||
}
|
||||
|
||||
func markArticles(_ articles: [Article], starred: Bool) {
|
||||
markArticles(articles, statusKey: .starred, flag: starred)
|
||||
func markArticles(_ articles: [Article], starred: Bool, directlyMarked: Bool) {
|
||||
markArticles(articles, statusKey: .starred, flag: starred, directlyMarked: directlyMarked)
|
||||
}
|
||||
|
||||
func markArticles(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) {
|
||||
guard let undoManager = undoManager, let markStatusCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) else {
|
||||
func markArticles(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool) {
|
||||
guard let undoManager = undoManager,
|
||||
let markStatusCommand = MarkStatusCommand(initialArticles: articles,
|
||||
statusKey: statusKey,
|
||||
flag: flag,
|
||||
directlyMarked: directlyMarked,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -176,14 +186,19 @@ private extension TimelineViewController {
|
||||
menu.addItem(markAllMenuItem)
|
||||
}
|
||||
}
|
||||
|
||||
if articles.count == 1, let link = articles.first!.preferredLink {
|
||||
|
||||
let links = articles.map { $0.preferredLink }
|
||||
let compactLinks = links.compactMap { $0 }
|
||||
|
||||
if compactLinks.count > 0 {
|
||||
menu.addSeparatorIfNeeded()
|
||||
menu.addItem(openInBrowserMenuItem(link))
|
||||
menu.addItem(openInBrowserMenuItem(compactLinks))
|
||||
menu.addItem(openInBrowserReversedMenuItem(compactLinks))
|
||||
|
||||
menu.addSeparatorIfNeeded()
|
||||
menu.addItem(copyArticleURLMenuItem(link))
|
||||
|
||||
if let externalLink = articles.first?.externalLink, externalLink != link {
|
||||
menu.addItem(copyArticleURLsMenuItem(links))
|
||||
|
||||
if let externalLink = articles.first?.externalLink, externalLink != links.first {
|
||||
menu.addItem(copyExternalURLMenuItem(externalLink))
|
||||
}
|
||||
}
|
||||
@@ -274,13 +289,21 @@ private extension TimelineViewController {
|
||||
return menuItem(menuText, #selector(markAllInFeedAsRead(_:)), articles)
|
||||
}
|
||||
|
||||
func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem {
|
||||
func openInBrowserMenuItem(_ urlStrings: [String]) -> NSMenuItem {
|
||||
return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlStrings)
|
||||
}
|
||||
|
||||
return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString)
|
||||
func openInBrowserReversedMenuItem(_ urlStrings: [String]) -> NSMenuItem {
|
||||
let item = menuItem(Browser.titleForOpenInBrowserInverted, #selector(openInBrowserFromContextualMenu(_:)), urlStrings)
|
||||
item.keyEquivalentModifierMask = .shift
|
||||
item.isAlternate = true
|
||||
return item;
|
||||
}
|
||||
|
||||
func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem {
|
||||
return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
|
||||
func copyArticleURLsMenuItem(_ urlStrings: [String?]) -> NSMenuItem {
|
||||
let format = NSLocalizedString("Copy Article URL", comment: "Command")
|
||||
let title = String.localizedStringWithFormat(format, urlStrings.count)
|
||||
return menuItem(title, #selector(copyURLFromContextualMenu(_:)), urlStrings)
|
||||
}
|
||||
|
||||
func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem {
|
||||
|
||||
@@ -65,7 +65,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
|
||||
unreadCount = 0
|
||||
|
||||
selectionDidChange(nil)
|
||||
if showsSearchResults {
|
||||
fetchAndReplaceArticlesAsync()
|
||||
} else {
|
||||
@@ -75,6 +74,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
updateUnreadCount()
|
||||
}
|
||||
selectionDidChange(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,10 +123,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
showFeedNames = .feed
|
||||
}
|
||||
|
||||
directlyMarkedAsUnreadArticles = Set<Article>()
|
||||
articleRowMap = [String: [Int]]()
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
var directlyMarkedAsUnreadArticles = Set<Article>()
|
||||
|
||||
var unreadCount: Int = 0 {
|
||||
didSet {
|
||||
@@ -219,6 +222,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidDirectMarking(_:)), name: .MarkStatusCommandDidDirectMarking, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidUndoDirectMarking(_:)), name: .MarkStatusCommandDidUndoDirectMarking, object: nil)
|
||||
didRegisterForNotifications = true
|
||||
}
|
||||
}
|
||||
@@ -230,7 +235,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
// MARK: - API
|
||||
|
||||
func markAllAsRead(completion: (() -> Void)? = nil) {
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager, completion: completion) else {
|
||||
let markableArticles = Set(articles).subtracting(directlyMarkedAsUnreadArticles)
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager,
|
||||
completion: completion) else {
|
||||
return
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
@@ -315,9 +326,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
// MARK: - Actions
|
||||
|
||||
@objc func openArticleInBrowser(_ sender: Any?) {
|
||||
if let link = oneSelectedArticle?.preferredLink {
|
||||
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
let urlStrings = selectedArticles.compactMap { $0.preferredLink }
|
||||
Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
|
||||
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
|
||||
@@ -337,14 +347,22 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
@IBAction func markSelectedArticlesAsRead(_ sender: Any?) {
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: true, undoManager: undoManager) else {
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: selectedArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: true,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
}
|
||||
|
||||
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
|
||||
guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else {
|
||||
guard let undoManager = undoManager,
|
||||
let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles,
|
||||
markingRead: false,
|
||||
directlyMarked: true,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markUnreadCommand)
|
||||
@@ -412,7 +430,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
return
|
||||
}
|
||||
|
||||
guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: markingRead, undoManager: undoManager) else {
|
||||
guard let undoManager = undoManager,
|
||||
let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles,
|
||||
markingRead: markingRead,
|
||||
directlyMarked: true,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -435,7 +457,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
return
|
||||
}
|
||||
|
||||
guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingStarred: starring, undoManager: undoManager) else {
|
||||
guard let undoManager = undoManager,
|
||||
let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles,
|
||||
markingStarred: starring,
|
||||
directlyMarked: true,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markStarredCommand)
|
||||
@@ -502,7 +528,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
return
|
||||
}
|
||||
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else {
|
||||
let markableArticles = Set(articlesToMark).subtracting(directlyMarkedAsUnreadArticles)
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
@@ -510,9 +541,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
func markAboveArticlesRead(_ selectedArticles: [Article]) {
|
||||
guard let first = selectedArticles.first else { return }
|
||||
|
||||
let articlesToMark = articles.articlesAbove(article: first)
|
||||
guard !articlesToMark.isEmpty else { return }
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else {
|
||||
|
||||
let markableArticles = Set(articlesToMark).subtracting(directlyMarkedAsUnreadArticles)
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
@@ -520,9 +558,16 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
func markBelowArticlesRead(_ selectedArticles: [Article]) {
|
||||
guard let last = selectedArticles.last else { return }
|
||||
|
||||
let articlesToMark = articles.articlesBelow(article: last)
|
||||
guard !articlesToMark.isEmpty else { return }
|
||||
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articlesToMark, markingRead: true, undoManager: undoManager) else {
|
||||
|
||||
let markableArticles = Set(articlesToMark).subtracting(directlyMarkedAsUnreadArticles)
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
|
||||
markingRead: true,
|
||||
directlyMarked: false,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
runCommand(markReadCommand)
|
||||
@@ -666,6 +711,28 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
self.groupByFeed = AppDefaults.shared.timelineGroupByFeed
|
||||
}
|
||||
|
||||
@objc func markStatusCommandDidDirectMarking(_ note: Notification) {
|
||||
guard let userInfo = note.userInfo,
|
||||
let articles = userInfo[Account.UserInfoKey.articles] as? Set<Article>,
|
||||
let statusKey = userInfo[Account.UserInfoKey.statusKey] as? ArticleStatus.Key,
|
||||
let flag = userInfo[Account.UserInfoKey.statusFlag] as? Bool else { return }
|
||||
|
||||
if statusKey == .read && flag == false {
|
||||
directlyMarkedAsUnreadArticles.formUnion(articles)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func markStatusCommandDidUndoDirectMarking(_ note: Notification) {
|
||||
guard let userInfo = note.userInfo,
|
||||
let articles = userInfo[Account.UserInfoKey.articles] as? Set<Article>,
|
||||
let statusKey = userInfo[Account.UserInfoKey.statusKey] as? ArticleStatus.Key,
|
||||
let flag = userInfo[Account.UserInfoKey.statusFlag] as? Bool else { return }
|
||||
|
||||
if statusKey == .read && flag == false {
|
||||
directlyMarkedAsUnreadArticles.subtract(articles)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reloading Data
|
||||
|
||||
private func cellForRowView(_ rowView: NSView) -> NSView? {
|
||||
@@ -779,8 +846,7 @@ extension TimelineViewController: NSUserInterfaceValidations {
|
||||
item.title = Browser.titleForOpenInBrowserInverted
|
||||
}
|
||||
|
||||
let currentLink = oneSelectedArticle?.preferredLink
|
||||
return currentLink != nil
|
||||
return selectedArticles.first { $0.preferredLink != nil } != nil
|
||||
}
|
||||
|
||||
if item.action == #selector(copy(_:)) {
|
||||
@@ -901,14 +967,22 @@ extension TimelineViewController: NSTableViewDelegate {
|
||||
}
|
||||
|
||||
private func toggleArticleRead(_ article: Article) {
|
||||
guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: [article], markingRead: !article.status.read, undoManager: undoManager) else {
|
||||
guard let undoManager = undoManager,
|
||||
let markUnreadCommand = MarkStatusCommand(initialArticles: [article],
|
||||
markingRead: !article.status.read,
|
||||
directlyMarked: true,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
self.runCommand(markUnreadCommand)
|
||||
}
|
||||
|
||||
|
||||
private func toggleArticleStarred(_ article: Article) {
|
||||
guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: [article], markingStarred: !article.status.starred, undoManager: undoManager) else {
|
||||
guard let undoManager = undoManager,
|
||||
let markUnreadCommand = MarkStatusCommand(initialArticles: [article],
|
||||
markingStarred: !article.status.starred,
|
||||
directlyMarked: true,
|
||||
undoManager: undoManager) else {
|
||||
return
|
||||
}
|
||||
self.runCommand(markUnreadCommand)
|
||||
|
||||
36
Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift
Normal file
36
Mac/MainWindow/URLPasteboardWriter+NetNewsWire.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// URLPasteboardWriter+NetNewsWire.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Nate Weaver on 2022-10-10.
|
||||
// Copyright © 2022 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import RSCore
|
||||
|
||||
extension URLPasteboardWriter {
|
||||
|
||||
/// Copy URL strings, alerting the user the first time the array of URL strings contains `nil`.
|
||||
/// - Parameters:
|
||||
/// - urlStrings: The URL strings to copy.
|
||||
/// - pasteboard: The pastebaord to copy to.
|
||||
/// - window: The window to use as a sheet parent for the alert. If `nil`, will run the alert modally.
|
||||
static func write(urlStrings: [String?], to pasteboard: NSPasteboard = .general, alertingIn window: NSWindow?) {
|
||||
URLPasteboardWriter.write(urlStrings: urlStrings.compactMap { $0 }, to: pasteboard)
|
||||
|
||||
if urlStrings.contains(nil), !AppDefaults.shared.hasSeenNotAllArticlesHaveURLsAlert {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Some articles don’t have links, so they weren't copied.", comment: "\"Some articles have no links\" copy alert message text")
|
||||
alert.informativeText = NSLocalizedString("You won't see this message again.", comment: "You won't see this message again")
|
||||
|
||||
if let window {
|
||||
alert.beginSheetModal(for: window)
|
||||
} else {
|
||||
alert.runModal() // this should never happen
|
||||
}
|
||||
|
||||
AppDefaults.shared.hasSeenNotAllArticlesHaveURLsAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user