Merge pull request #3701 from Wevah/multiple-article-urls-copy

Support copying and opening multiple article URLs
This commit is contained in:
Maurice Parker
2022-11-12 17:40:25 -06:00
committed by GitHub
8 changed files with 181 additions and 30 deletions

View File

@@ -42,6 +42,7 @@ final class AppDefaults {
static let exportOPMLAccountID = "exportOPMLAccountID"
static let defaultBrowserID = "defaultBrowserID"
static let currentThemeName = "currentThemeName"
static let hasSeenNotAllArticlesHaveURLsAlert = "hasSeenNotAllArticlesHaveURLsAlert"
// Hidden prefs
static let showDebugMenu = "ShowDebugMenu"
@@ -220,6 +221,15 @@ final class AppDefaults {
AppDefaults.setString(for: Key.currentThemeName, newValue)
}
}
var hasSeenNotAllArticlesHaveURLsAlert: Bool {
get {
return UserDefaults.standard.bool(forKey: Key.hasSeenNotAllArticlesHaveURLsAlert)
}
set {
UserDefaults.standard.set(newValue, forKey: Key.hasSeenNotAllArticlesHaveURLsAlert)
}
}
var showTitleOnMainWindow: Bool {
return AppDefaults.bool(for: Key.showTitleOnMainWindow)

View File

@@ -73,3 +73,48 @@ extension Browser {
NSLocalizedString("Open in Browser in Background", comment: "Open in Browser in Background menu item title")
}
}
extension Browser {
/// Open multiple pages in the default browser, warning if over a certain number of URLs are passed.
/// - Parameters:
/// - urlStrings: The URL strings to open.
/// - window: The window on which to display the "over limit" alert sheet. If `nil`, will be displayed as a
/// modal dialog.
/// - invertPreference: Whether to invert the user's "Open web pages in background in browser" preference.
static func open(_ urlStrings: [String], fromWindow window: NSWindow?, invertPreference: Bool = false) {
if urlStrings.count > 500 {
return
}
func doOpenURLs() {
for urlString in urlStrings {
Browser.open(urlString, invertPreference: invertPreference)
}
}
if urlStrings.count > 20 {
let alert = NSAlert()
let messageFormat = NSLocalizedString("Are you sure you want to open %ld articles in your browser?", comment: "Open in Browser confirmation alert message format")
alert.messageText = String.localizedStringWithFormat(messageFormat, urlStrings.count)
let confirmButtonTitleFormat = NSLocalizedString("Open %ld Articles", comment: "Open URLs in Browser confirm button format")
alert.addButton(withTitle: String.localizedStringWithFormat(confirmButtonTitleFormat, urlStrings.count))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel button"))
if let window {
alert.beginSheetModal(for: window) { response in
if response == .alertFirstButtonReturn {
doOpenURLs()
}
}
} else {
if alert.runModal() == .alertFirstButtonReturn {
doOpenURLs()
}
}
} else {
doOpenURLs()
}
}
}

View File

@@ -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?) {
@@ -1060,7 +1068,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
@@ -1098,7 +1110,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 {

View File

@@ -89,18 +89,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?) {
@@ -176,14 +177,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 +280,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 {

View File

@@ -315,9 +315,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?) {
@@ -779,8 +778,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(_:)) {

View 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 dont 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
}
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Copy Article URL</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@copy_article_url@</string>
<key>copy_article_url</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>ld</string>
<key>other</key>
<string>Copy Article URLs</string>
<key>one</key>
<string>Copy Article URL</string>
</dict>
</dict>
</dict>
</plist>

View File

@@ -818,6 +818,7 @@
84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE1213660A100CF2DE4 /* testGenericScript.applescript */; };
84F9EAF5213660A100CF2DE4 /* establishMainWindowStartingState.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */; };
84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; };
B20180AB28E3B76F0059686A /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = B20180AA28E3B76F0059686A /* Localizable.stringsdict */; };
B24E9ADC245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
B24E9ADD245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; };
@@ -827,6 +828,8 @@
B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; };
B2C12C6628F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */; };
B2C12C6728F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */; };
B528F81E23333C7E00E735DD /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = B528F81D23333C7E00E735DD /* page.html */; };
BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; };
BDCB516824282C8A00102A80 /* AccountsNewsBlur.xib in Resources */ = {isa = PBXBuildFile; fileRef = BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */; };
@@ -1565,11 +1568,13 @@
84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = "<group>"; };
84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = "<group>"; };
B20180AA28E3B76F0059686A /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = "<group>"; };
B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = "<group>"; };
B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = "<group>"; };
B27EEBDF244D15F2000932E6 /* stylesheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = stylesheet.css; sourceTree = "<group>"; };
B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = "<group>"; };
B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLPasteboardWriter+NetNewsWire.swift"; sourceTree = "<group>"; };
B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = "<group>"; };
BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsNewsBlur.xib; sourceTree = "<group>"; };
C5A6ED5123C9AF4300AB6BE2 /* TitleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleActivityItemSource.swift; sourceTree = "<group>"; };
@@ -2268,6 +2273,7 @@
5117715424E1EA0F00A2A836 /* ArticleExtractorButton.swift */,
51FA73B62332D5F70090D516 /* LegacyArticleExtractorButton.swift */,
847CD6C9232F4CBF00FAC46D /* IconView.swift */,
B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */,
844B5B6B1FEA224B00C7C76A /* Keyboard */,
849A975F1ED9EB95007D329B /* Sidebar */,
849A97681ED9EBC8007D329B /* Timeline */,
@@ -2673,6 +2679,7 @@
children = (
849C64671ED37A5D003D8FC0 /* Assets.xcassets */,
84C9FC8922629E8F00D921D6 /* Credits.rtf */,
B20180AA28E3B76F0059686A /* Localizable.stringsdict */,
84C9FC8A22629E8F00D921D6 /* NetNewsWire.sdef */,
84C9FC9022629ECB00D921D6 /* NetNewsWire.entitlements */,
51F805D32428499E0022C792 /* NetNewsWire-dev.entitlements */,
@@ -3520,6 +3527,7 @@
BDCB516724282C8A00102A80 /* AccountsNewsBlur.xib in Resources */,
514A89A2244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */,
5103A9982421643300410853 /* blank.html in Resources */,
B20180AB28E3B76F0059686A /* Localizable.stringsdict in Resources */,
515A516E243E7F950089E588 /* ExtensionPointDetail.xib in Resources */,
84BAE64921CEDAF20046DB56 /* CrashReporterWindow.xib in Resources */,
51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */,
@@ -3869,6 +3877,7 @@
65ED3FD0235DEF6C0081F399 /* Author+Scriptability.swift in Sources */,
65ED3FD1235DEF6C0081F399 /* PseudoFeed.swift in Sources */,
65ED3FD3235DEF6C0081F399 /* NSScriptCommand+NetNewsWire.swift in Sources */,
B2C12C6728F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */,
65ED3FD4235DEF6C0081F399 /* Article+Scriptability.swift in Sources */,
515A5172243E802B0089E588 /* ExtensionPointDetailViewController.swift in Sources */,
65ED3FD5235DEF6C0081F399 /* SmartFeed.swift in Sources */,
@@ -4219,6 +4228,7 @@
848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */,
515A5171243E802B0089E588 /* ExtensionPointDetailViewController.swift in Sources */,
847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */,
B2C12C6628F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift in Sources */,
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */,
51EF0F7A22771B890050506E /* ColorHash.swift in Sources */,
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */,