mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'main' into ios-ui-settings
# Conflicts: # iOS/AppDefaults.swift
This commit is contained in:
@@ -517,10 +517,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
addOPMLItems(OPMLNormalizer.normalize(items))
|
||||
}
|
||||
|
||||
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag, completion: completion)
|
||||
}
|
||||
|
||||
func existingContainer(withExternalID externalID: String) -> Container? {
|
||||
guard self.externalID != externalID else {
|
||||
return self
|
||||
@@ -639,6 +635,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
||||
}
|
||||
|
||||
public func mark(articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag, completion: completion)
|
||||
}
|
||||
|
||||
func clearWebFeedMetadata(_ feed: WebFeed) {
|
||||
webFeedMetadata[feed.url] = nil
|
||||
}
|
||||
@@ -832,40 +832,46 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Mark articleIDs statuses based on statusKey and flag.
|
||||
/// Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
|
||||
/// Returns a set of new article statuses.
|
||||
func markAndFetchNew(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock? = nil) {
|
||||
func mark(articleIDs: Set<String>, statusKey: ArticleStatus.Key, flag: Bool, completion: DatabaseCompletionBlock? = nil) {
|
||||
guard !articleIDs.isEmpty else {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag, completion: completion)
|
||||
database.mark(articleIDs: articleIDs, statusKey: statusKey, flag: flag) { databaseError in
|
||||
if let databaseError = databaseError {
|
||||
completion?(databaseError)
|
||||
} else {
|
||||
self.noteStatusesForArticleIDsDidChange(articleIDs: articleIDs, statusKey: statusKey, flag: flag)
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark articleIDs as read. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
|
||||
/// Returns a set of new article statuses.
|
||||
func markAsRead(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
|
||||
markAndFetchNew(articleIDs: articleIDs, statusKey: .read, flag: true, completion: completion)
|
||||
mark(articleIDs: articleIDs, statusKey: .read, flag: true, completion: completion)
|
||||
}
|
||||
|
||||
/// Mark articleIDs as unread. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
|
||||
/// Returns a set of new article statuses.
|
||||
func markAsUnread(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
|
||||
markAndFetchNew(articleIDs: articleIDs, statusKey: .read, flag: false, completion: completion)
|
||||
mark(articleIDs: articleIDs, statusKey: .read, flag: false, completion: completion)
|
||||
}
|
||||
|
||||
/// Mark articleIDs as starred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
|
||||
/// Returns a set of new article statuses.
|
||||
func markAsStarred(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
|
||||
markAndFetchNew(articleIDs: articleIDs, statusKey: .starred, flag: true, completion: completion)
|
||||
mark(articleIDs: articleIDs, statusKey: .starred, flag: true, completion: completion)
|
||||
}
|
||||
|
||||
/// Mark articleIDs as unstarred. Will create statuses in the database and in memory as needed. Sends a .StatusesDidChange notification.
|
||||
/// Returns a set of new article statuses.
|
||||
func markAsUnstarred(_ articleIDs: Set<String>, completion: DatabaseCompletionBlock? = nil) {
|
||||
markAndFetchNew(articleIDs: articleIDs, statusKey: .starred, flag: false, completion: completion)
|
||||
mark(articleIDs: articleIDs, statusKey: .starred, flag: false, completion: completion)
|
||||
}
|
||||
|
||||
// Delete the articles associated with the given set of articleIDs
|
||||
|
||||
@@ -70,6 +70,9 @@ public final class AccountManager: UnreadCountProvider {
|
||||
if lastArticleFetchEndTime == nil || lastArticleFetchEndTime! < accountLastArticleFetchEndTime {
|
||||
lastArticleFetchEndTime = accountLastArticleFetchEndTime
|
||||
}
|
||||
} else {
|
||||
lastArticleFetchEndTime = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
return lastArticleFetchEndTime
|
||||
@@ -265,34 +268,6 @@ public final class AccountManager: UnreadCountProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshAll(completion: (() -> Void)? = nil) {
|
||||
guard let reachability = try? Reachability(hostname: "apple.com"), reachability.connection != .unavailable else { return }
|
||||
|
||||
var syncErrors = [AccountSyncError]()
|
||||
let group = DispatchGroup()
|
||||
|
||||
activeAccounts.forEach { account in
|
||||
group.enter()
|
||||
account.refreshAll() { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
syncErrors.append(AccountSyncError(account: account, error: error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
if syncErrors.count > 0 {
|
||||
NotificationCenter.default.post(Notification(name: .AccountsDidFailToSyncWithErrors, object: self, userInfo: [Account.UserInfoKey.syncErrors: syncErrors]))
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func sendArticleStatusAll(completion: (() -> Void)? = nil) {
|
||||
let group = DispatchGroup()
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// AccountSyncError.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Stuart Breckenridge on 24/7/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
|
||||
public extension Notification.Name {
|
||||
static let AccountsDidFailToSyncWithErrors = Notification.Name("AccountsDidFailToSyncWithErrors")
|
||||
}
|
||||
|
||||
public struct AccountSyncError: Logging {
|
||||
|
||||
public let account: Account
|
||||
public let error: Error
|
||||
|
||||
init(account: Account, error: Error) {
|
||||
self.account = account
|
||||
self.error = error
|
||||
AccountSyncError.logger.error("Account Sync Error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -804,7 +804,7 @@ private extension CloudKitAccountDelegate {
|
||||
self.sendArticleStatus(for: account, showProgress: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.articlesZone.fetchChangesInZone() { _ in }
|
||||
self.refreshArticleStatus(for: account) { _ in }
|
||||
case .failure(let error):
|
||||
self.logger.error("CloudKit Feed send articles error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ final class CloudKitAccountZone: CloudKitZone {
|
||||
|
||||
var zoneID: CKRecordZone.ID
|
||||
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||
|
||||
weak var container: CKContainer?
|
||||
weak var database: CKDatabase?
|
||||
var delegate: CloudKitZoneDelegate?
|
||||
|
||||
@@ -19,8 +19,6 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
|
||||
var zoneID: CKRecordZone.ID
|
||||
|
||||
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||
|
||||
weak var container: CKContainer?
|
||||
weak var database: CKDatabase?
|
||||
var delegate: CloudKitZoneDelegate? = nil
|
||||
@@ -64,28 +62,6 @@ final class CloudKitArticlesZone: CloudKitZone {
|
||||
migrateChangeToken()
|
||||
}
|
||||
|
||||
func refreshArticles(completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
fetchChangesInZone() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
if case CloudKitZoneError.userDeletedZone = error {
|
||||
self.createZoneRecord() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticles(completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveNewArticles(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
guard !articles.isEmpty else {
|
||||
completion(.success(()))
|
||||
|
||||
@@ -37,14 +37,16 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
|
||||
self.database.selectPendingStarredStatusArticleIDs() { result in
|
||||
switch result {
|
||||
case .success(let pendingStarredStatusArticleIDs):
|
||||
|
||||
self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) {
|
||||
self.update(records: updated,
|
||||
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
|
||||
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
|
||||
completion: completion)
|
||||
self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) { error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
} else {
|
||||
self.update(records: updated,
|
||||
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
|
||||
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
|
||||
completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
self.logger.error("Error occurred getting pending starred records: \(error.localizedDescription, privacy: .public)")
|
||||
completion(.failure(CloudKitZoneError.unknown))
|
||||
@@ -63,19 +65,27 @@ class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate, Logging {
|
||||
|
||||
private extension CloudKitArticlesZoneDelegate {
|
||||
|
||||
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>, completion: @escaping () -> Void) {
|
||||
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Error?) -> Void) {
|
||||
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })
|
||||
let receivedArticleIDs = Set(receivedRecordIDs.map({ stripPrefix($0.externalID) }))
|
||||
let deletableArticleIDs = receivedArticleIDs.subtracting(pendingStarredStatusArticleIDs)
|
||||
|
||||
guard !deletableArticleIDs.isEmpty else {
|
||||
completion()
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { _ in
|
||||
self.account?.delete(articleIDs: deletableArticleIDs) { _ in
|
||||
completion()
|
||||
database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { databaseError in
|
||||
if let databaseError = databaseError {
|
||||
completion(databaseError)
|
||||
} else {
|
||||
self.account?.delete(articleIDs: deletableArticleIDs) { databaseError in
|
||||
if let databaseError = databaseError {
|
||||
completion(databaseError)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class CloudKitReceiveStatusOperation: MainThreadOperation, Logging {
|
||||
|
||||
logger.debug("Refreshing article statuses...")
|
||||
|
||||
articlesZone.refreshArticles() { result in
|
||||
articlesZone.fetchChangesInZone() { result in
|
||||
self.logger.debug("Done refreshing article statuses.")
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -241,7 +241,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
refreshTimer = AccountRefreshTimer()
|
||||
syncTimer = ArticleStatusSyncTimer()
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .badge]) { (granted, error) in }
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in }
|
||||
|
||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||
if settings.authorizationStatus == .authorized {
|
||||
@@ -1050,41 +1050,31 @@ extension AppDelegate: NSWindowRestoration {
|
||||
private extension AppDelegate {
|
||||
|
||||
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
||||
markArticle(userInfo: userInfo, statusKey: .read)
|
||||
}
|
||||
|
||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
||||
markArticle(userInfo: userInfo, statusKey: .starred)
|
||||
}
|
||||
|
||||
func markArticle(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
guard let account = AccountManager.shared.existingAccount(with: accountID) else {
|
||||
logger.debug("No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
|
||||
guard let articles = try? account.fetchArticles(.articleIDs([articleID])), !articles.isEmpty else {
|
||||
logger.debug("No article found from search using: \(articleID, privacy: .public)")
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
|
||||
|
||||
account.mark(articles: articles, statusKey: statusKey, flag: true) { _ in }
|
||||
}
|
||||
|
||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
logger.debug("No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
logger.debug("No article found from search using: \(articleID, privacy: .public)")
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .starred, flag: true) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ struct CreditsNetNewsWireView: View, LoadableAboutData {
|
||||
.frame(height: 12)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.frame(width: 400, height: 400)
|
||||
}
|
||||
|
||||
func contributorView(_ appCredit: AboutData.Contributor) -> some View {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,6 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="3300" height="2200">
|
||||
<!--glyph: "uni10096D.medium", point size: 100.0, font version: "18.0d12e2", template writer version: "101"-->
|
||||
<style>.monochrome-0 {fill:#000000}
|
||||
|
||||
.SFSymbolsPreview000000 {fill:#000000;opacity:1.0}
|
||||
</style>
|
||||
<g id="Notes">
|
||||
<rect height="2200" id="artboard" style="fill:white;opacity:1" width="3300" x="0" y="0"/>
|
||||
<line style="fill:none;stroke:black;opacity:1;stroke-width:0.5;" x1="263" x2="3036" y1="292" y2="292"/>
|
||||
@@ -52,8 +48,8 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;font-weight:bold;" transform="matrix(1 0 0 1 1289 1953)">Exporting</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1971)">Symbols should be outlined when exporting to ensure the</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 1289 1989)">design is preserved when submitting to Xcode.</text>
|
||||
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.4.0</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 14 or greater</text>
|
||||
<text id="template-version" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1933)">Template v.3.0</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1951)">Requires Xcode 13 or greater</text>
|
||||
<text id="descriptive-name" style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1969)">Generated from puzzlepiece.extension</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;text-anchor:end;" transform="matrix(1 0 0 1 3036 1987)">Typeset at 100 points</text>
|
||||
<text style="stroke:none;fill:black;font-family:sans-serif;font-size:13;" transform="matrix(1 0 0 1 263 726)">Small</text>
|
||||
@@ -85,13 +81,13 @@ PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
</g>
|
||||
<g id="Symbols">
|
||||
<g id="Black-S" transform="matrix(1 0 0 1 2875.37 696)">
|
||||
<path class="monochrome-0 SFSymbolsPreview000000" d="M9.76562-9.17969C9.76562-0.878906 15.3809 4.6875 23.8281 4.6875L71.9727 4.6875C80.4199 4.6875 86.0352-0.878906 86.0352-9.17969L86.0352-18.5547C86.0352-19.2383 86.5234-19.4336 87.3047-19.0918C88.7207-18.457 90.5762-18.2129 92.7734-18.2129C101.074-18.2129 109.229-24.4629 109.229-35.3027C109.229-46.1426 101.074-52.3926 92.7734-52.3926C90.5762-52.3926 88.7207-52.1484 87.3047-51.5137C86.5234-51.1719 86.0352-51.3672 86.0352-52.0508L86.0352-61.4258C86.0352-69.7266 80.4199-75.293 71.9727-75.293L23.8281-75.293C15.3809-75.293 9.76562-69.7266 9.76562-61.4258L9.76562-44.8242C9.76562-40.1367 13.0371-36.6699 17.4316-36.6699C20.1172-36.6699 21.7285-37.5977 23.1934-38.5254C24.707-39.502 26.0254-40.4785 28.0762-40.4785C31.1523-40.4785 33.2031-38.4277 33.2031-35.3027C33.2031-32.1777 31.1523-30.127 28.0762-30.127C26.0254-30.127 24.707-31.1035 23.1934-32.0801C21.7285-33.0078 20.1172-33.9355 17.4316-33.9355C13.0371-33.9355 9.76562-30.4688 9.76562-25.7812ZM21.9727-9.57031L21.9727-18.5547C21.9727-19.2383 22.4609-19.4336 23.2422-19.0918C24.6582-18.457 26.5137-18.2129 28.7109-18.2129C37.0117-18.2129 45.166-24.4629 45.166-35.3027C45.166-46.1426 37.0117-52.3926 28.7109-52.3926C26.5137-52.3926 24.6582-52.1484 23.2422-51.5137C22.4609-51.1719 21.9727-51.3672 21.9727-52.0508L21.9727-61.0352C21.9727-62.2559 22.7539-63.0859 23.9258-63.0859L71.875-63.0859C73.0469-63.0859 73.8281-62.2559 73.8281-61.0352L73.8281-44.8242C73.8281-40.1367 77.0996-36.6699 81.4941-36.6699C84.1797-36.6699 85.791-37.5977 87.2559-38.5254C88.7695-39.502 90.0879-40.4785 92.1387-40.4785C95.2148-40.4785 97.2656-38.4277 97.2656-35.3027C97.2656-32.1777 95.2148-30.127 92.1387-30.127C90.0879-30.127 88.7695-31.1035 87.2559-32.0801C85.791-33.0078 84.1797-33.9355 81.4941-33.9355C77.0996-33.9355 73.8281-30.4688 73.8281-25.7812L73.8281-9.57031C73.8281-8.34961 73.0469-7.51953 71.875-7.51953L23.9258-7.51953C22.7539-7.51953 21.9727-8.34961 21.9727-9.57031Z"/>
|
||||
<path d="M9.76562-9.17969C9.76562-0.878906 15.3809 4.6875 23.8281 4.6875L71.9727 4.6875C80.4199 4.6875 86.0352-0.878906 86.0352-9.17969L86.0352-18.5547C86.0352-19.2383 86.5234-19.4336 87.3047-19.0918C88.7207-18.457 90.5762-18.2129 92.7734-18.2129C101.074-18.2129 109.229-24.4629 109.229-35.3027C109.229-46.1426 101.074-52.3926 92.7734-52.3926C90.5762-52.3926 88.7207-52.1484 87.3047-51.5137C86.5234-51.1719 86.0352-51.3672 86.0352-52.0508L86.0352-61.4258C86.0352-69.7266 80.4199-75.293 71.9727-75.293L23.8281-75.293C15.3809-75.293 9.76562-69.7266 9.76562-61.4258L9.76562-44.8242C9.76562-40.1367 13.0371-36.6699 17.4316-36.6699C20.1172-36.6699 21.7285-37.5977 23.1934-38.5254C24.707-39.502 26.0254-40.4785 28.0762-40.4785C31.1523-40.4785 33.2031-38.4277 33.2031-35.3027C33.2031-32.1777 31.1523-30.127 28.0762-30.127C26.0254-30.127 24.707-31.1035 23.1934-32.0801C21.7285-33.0078 20.1172-33.9355 17.4316-33.9355C13.0371-33.9355 9.76562-30.4688 9.76562-25.7812ZM21.9727-9.57031L21.9727-18.5547C21.9727-19.2383 22.4609-19.4336 23.2422-19.0918C24.6582-18.457 26.5137-18.2129 28.7109-18.2129C37.0117-18.2129 45.166-24.4629 45.166-35.3027C45.166-46.1426 37.0117-52.3926 28.7109-52.3926C26.5137-52.3926 24.6582-52.1484 23.2422-51.5137C22.4609-51.1719 21.9727-51.3672 21.9727-52.0508L21.9727-61.0352C21.9727-62.2559 22.7539-63.0859 23.9258-63.0859L71.875-63.0859C73.0469-63.0859 73.8281-62.2559 73.8281-61.0352L73.8281-44.8242C73.8281-40.1367 77.0996-36.6699 81.4941-36.6699C84.1797-36.6699 85.791-37.5977 87.2559-38.5254C88.7695-39.502 90.0879-40.4785 92.1387-40.4785C95.2148-40.4785 97.2656-38.4277 97.2656-35.3027C97.2656-32.1777 95.2148-30.127 92.1387-30.127C90.0879-30.127 88.7695-31.1035 87.2559-32.0801C85.791-33.0078 84.1797-33.9355 81.4941-33.9355C77.0996-33.9355 73.8281-30.4688 73.8281-25.7812L73.8281-9.57031C73.8281-8.34961 73.0469-7.51953 71.875-7.51953L23.9258-7.51953C22.7539-7.51953 21.9727-8.34961 21.9727-9.57031Z"/>
|
||||
</g>
|
||||
<g id="Regular-S" transform="matrix(1 0 0 1 1394.38 696)">
|
||||
<path class="monochrome-0 SFSymbolsPreview000000" d="M9.76562-10.0586C9.76562-1.95312 13.916 2.09961 22.168 2.09961L68.5059 2.09961C76.709 2.09961 80.8594-1.95312 80.8594-10.0586L80.8594-22.9492C80.8594-23.4375 81.2012-23.6816 81.7871-23.2422C84.2773-21.5332 87.207-20.5078 89.9414-20.5078C97.2168-20.5078 104.102-26.5137 104.102-35.2539C104.102-44.043 97.2168-50 89.9414-50C87.207-50 84.2773-49.0234 81.7871-47.2656C81.2012-46.875 80.8594-47.0703 80.8594-47.6074L80.8594-60.5469C80.8594-68.6523 76.709-72.7051 68.5059-72.7051L22.168-72.7051C13.916-72.7051 9.76562-68.6523 9.76562-60.5469L9.76562-43.7988C9.76562-40.3809 12.0605-38.2324 14.8926-38.2324C16.4551-38.2324 18.1152-38.916 19.6777-40.3809C21.4355-41.9922 23.5352-43.0176 25.5371-43.0176C29.5898-43.0176 33.2031-39.9414 33.2031-35.2539C33.2031-30.5664 29.5898-27.4902 25.5371-27.4902C23.5352-27.4902 21.4355-28.5156 19.6777-30.127C18.1152-31.5918 16.4551-32.2754 14.8926-32.2754C12.0605-32.2754 9.76562-30.127 9.76562-26.709ZM16.7969-10.4492L16.7969-22.168C16.7969-24.1211 17.7246-23.2422 18.4082-22.8516C20.7031-21.3867 23.3398-20.5078 25.8789-20.5078C33.1543-20.5078 39.9902-26.5137 39.9902-35.2539C39.9902-43.9941 33.1543-50 25.8789-50C23.3398-50 20.7031-49.1211 18.4082-47.6562C17.7246-47.2656 16.7969-46.3867 16.7969-48.3398L16.7969-60.1562C16.7969-63.7695 18.7988-65.6738 22.2656-65.6738L68.4082-65.6738C71.8262-65.6738 73.8281-63.7695 73.8281-60.1562L73.8281-43.8477C73.8281-40.4297 76.123-38.2324 78.9551-38.2324C80.5176-38.2324 82.1777-38.916 83.7402-40.3809C85.498-42.041 87.5977-43.0176 89.5996-43.0176C93.6523-43.0176 97.2656-39.9414 97.2656-35.2539C97.2656-30.6152 93.6523-27.4902 89.5996-27.4902C87.5977-27.4902 85.498-28.5156 83.7402-30.127C82.1777-31.5918 80.5176-32.2754 78.9551-32.2754C76.123-32.2754 73.8281-30.127 73.8281-26.709L73.8281-10.4492C73.8281-6.83594 71.8262-4.93164 68.4082-4.93164L22.2656-4.93164C18.7988-4.93164 16.7969-6.83594 16.7969-10.4492Z"/>
|
||||
<path d="M9.76562-10.0586C9.76562-1.95312 13.916 2.09961 22.168 2.09961L68.5059 2.09961C76.709 2.09961 80.8594-1.95312 80.8594-10.0586L80.8594-22.9492C80.8594-23.4375 81.2012-23.6816 81.7871-23.2422C84.2773-21.5332 87.207-20.5078 89.9414-20.5078C97.2168-20.5078 104.102-26.5137 104.102-35.2539C104.102-44.043 97.2168-50 89.9414-50C87.207-50 84.2773-49.0234 81.7871-47.2656C81.2012-46.875 80.8594-47.0703 80.8594-47.6074L80.8594-60.5469C80.8594-68.6523 76.709-72.7051 68.5059-72.7051L22.168-72.7051C13.916-72.7051 9.76562-68.6523 9.76562-60.5469L9.76562-43.7988C9.76562-40.3809 12.0605-38.2324 14.8926-38.2324C16.4551-38.2324 18.1152-38.916 19.6777-40.3809C21.4355-41.9922 23.5352-43.0176 25.5371-43.0176C29.5898-43.0176 33.2031-39.9414 33.2031-35.2539C33.2031-30.5664 29.5898-27.4902 25.5371-27.4902C23.5352-27.4902 21.4355-28.5156 19.6777-30.127C18.1152-31.5918 16.4551-32.2754 14.8926-32.2754C12.0605-32.2754 9.76562-30.127 9.76562-26.709ZM16.7969-10.4492L16.7969-22.168C16.7969-24.1211 17.7246-23.2422 18.4082-22.8516C20.7031-21.3867 23.3398-20.5078 25.8789-20.5078C33.1543-20.5078 39.9902-26.5137 39.9902-35.2539C39.9902-43.9941 33.1543-50 25.8789-50C23.3398-50 20.7031-49.1211 18.4082-47.6562C17.7246-47.2656 16.7969-46.3867 16.7969-48.3398L16.7969-60.1562C16.7969-63.7695 18.7988-65.6738 22.2656-65.6738L68.4082-65.6738C71.8262-65.6738 73.8281-63.7695 73.8281-60.1562L73.8281-43.8477C73.8281-40.4297 76.123-38.2324 78.9551-38.2324C80.5176-38.2324 82.1777-38.916 83.7402-40.3809C85.498-42.041 87.5977-43.0176 89.5996-43.0176C93.6523-43.0176 97.2656-39.9414 97.2656-35.2539C97.2656-30.6152 93.6523-27.4902 89.5996-27.4902C87.5977-27.4902 85.498-28.5156 83.7402-30.127C82.1777-31.5918 80.5176-32.2754 78.9551-32.2754C76.123-32.2754 73.8281-30.127 73.8281-26.709L73.8281-10.4492C73.8281-6.83594 71.8262-4.93164 68.4082-4.93164L22.2656-4.93164C18.7988-4.93164 16.7969-6.83594 16.7969-10.4492Z"/>
|
||||
</g>
|
||||
<g id="Ultralight-S" transform="matrix(1 0 0 1 506.104 696)">
|
||||
<path class="monochrome-0 SFSymbolsPreview000000" d="M9.76562-8.74171C9.76562-3.6787 13.1895-0.30711 18.399-0.30711L68.3242-0.30711C73.5303-0.30711 76.9541-3.6787 76.9541-8.74171L76.9541-25.1743C76.9541-26.2075 77.7954-26.9057 78.8809-26.103C81.0532-24.5302 83.4834-22.2788 88.5337-22.2788C95.582-22.2788 100.378-27.1948 100.378-34.6636C100.378-42.0903 95.582-47.0484 88.5337-47.0484C83.4834-47.0484 81.0532-44.7549 78.8809-43.1787C77.7954-42.4248 76.9541-43.0743 76.9541-44.1109L76.9541-61.8638C76.9541-66.9268 73.5303-70.2984 68.3242-70.2984L18.399-70.2984C13.1895-70.2984 9.76562-66.9268 9.76562-61.8638L9.76562-43.9805C9.76562-41.9248 10.9707-40.9116 12.8491-40.9116C13.7305-40.9116 14.5279-41.2773 15.5909-42.1518C17.939-44.081 20.084-45.4243 23.5391-45.4243C29.3174-45.4243 33.2031-41.3945 33.2031-35.2539C33.2031-29.1133 29.3174-25.0835 23.5391-25.0835C20.084-25.0835 17.939-26.4268 15.5909-28.356C14.5279-29.2305 13.7305-29.5962 12.8491-29.5962C10.9707-29.5962 9.76562-28.583 9.76562-26.5273ZM11.9834-8.81447L11.9834-26.2094C11.9834-27.3452 12.8658-27.4653 13.9126-26.7114C16.1167-25.1557 18.5264-22.8691 23.563-22.8691C30.6113-22.8691 35.4038-27.8306 35.4038-35.2539C35.4038-42.6773 30.6113-47.6387 23.563-47.6387C18.5264-47.6387 16.1167-45.3521 13.9126-43.7964C12.8658-43.0425 11.9834-43.1626 11.9834-44.2984L11.9834-61.791C11.9834-65.5859 14.5757-68.0805 18.4966-68.0805L68.2266-68.0805C72.144-68.0805 74.7363-65.5859 74.7363-61.791L74.7363-43.8477C74.7363-41.792 75.9868-40.3213 77.8198-40.3213C78.7012-40.3213 79.544-40.687 80.5615-41.5615C82.9551-43.4941 85.1001-44.834 88.5098-44.834C94.2881-44.834 98.1738-40.7588 98.1738-34.6636C98.1738-28.5264 94.2881-24.4932 88.5098-24.4932C85.1001-24.4932 82.9551-25.8364 80.5615-27.7656C79.544-28.6402 78.7012-28.9605 77.8198-28.9605C75.9868-28.9605 74.7363-27.5386 74.7363-25.4375L74.7363-8.81447C74.7363-5.01955 72.144-2.52492 68.2266-2.52492L18.4966-2.52492C14.5757-2.52492 11.9834-5.01955 11.9834-8.81447Z"/>
|
||||
<path d="M9.76562-8.74171C9.76562-3.6787 13.1895-0.30711 18.399-0.30711L68.3242-0.30711C73.5303-0.30711 76.9541-3.6787 76.9541-8.74171L76.9541-25.1743C76.9541-26.2075 77.7954-26.9057 78.8809-26.103C81.0532-24.5302 83.4834-22.2788 88.5337-22.2788C95.582-22.2788 100.378-27.1948 100.378-34.6636C100.378-42.0903 95.582-47.0484 88.5337-47.0484C83.4834-47.0484 81.0532-44.7549 78.8809-43.1787C77.7954-42.4248 76.9541-43.0743 76.9541-44.1109L76.9541-61.8638C76.9541-66.9268 73.5303-70.2984 68.3242-70.2984L18.399-70.2984C13.1895-70.2984 9.76562-66.9268 9.76562-61.8638L9.76562-43.9805C9.76562-41.9248 10.9707-40.9116 12.8491-40.9116C13.7305-40.9116 14.5279-41.2773 15.5909-42.1518C17.939-44.081 20.084-45.4243 23.5391-45.4243C29.3174-45.4243 33.2031-41.3945 33.2031-35.2539C33.2031-29.1133 29.3174-25.0835 23.5391-25.0835C20.084-25.0835 17.939-26.4268 15.5909-28.356C14.5279-29.2305 13.7305-29.5962 12.8491-29.5962C10.9707-29.5962 9.76562-28.583 9.76562-26.5273ZM11.9834-8.81447L11.9834-26.2094C11.9834-27.3452 12.8658-27.4653 13.9126-26.7114C16.1167-25.1557 18.5264-22.8691 23.563-22.8691C30.6113-22.8691 35.4038-27.8306 35.4038-35.2539C35.4038-42.6773 30.6113-47.6387 23.563-47.6387C18.5264-47.6387 16.1167-45.3521 13.9126-43.7964C12.8658-43.0425 11.9834-43.1626 11.9834-44.2984L11.9834-61.791C11.9834-65.5859 14.5757-68.0805 18.4966-68.0805L68.2266-68.0805C72.144-68.0805 74.7363-65.5859 74.7363-61.791L74.7363-43.8477C74.7363-41.792 75.9868-40.3213 77.8198-40.3213C78.7012-40.3213 79.544-40.687 80.5615-41.5615C82.9551-43.4941 85.1001-44.834 88.5098-44.834C94.2881-44.834 98.1738-40.7588 98.1738-34.6636C98.1738-28.5264 94.2881-24.4932 88.5098-24.4932C85.1001-24.4932 82.9551-25.8364 80.5615-27.7656C79.544-28.6402 78.7012-28.9605 77.8198-28.9605C75.9868-28.9605 74.7363-27.5386 74.7363-25.4375L74.7363-8.81447C74.7363-5.01955 72.144-2.52492 68.2266-2.52492L18.4966-2.52492C14.5757-2.52492 11.9834-5.01955 11.9834-8.81447Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
22
Mac/Resources/Localizable.stringsdict
Normal file
22
Mac/Resources/Localizable.stringsdict
Normal 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>
|
||||
@@ -192,6 +192,7 @@
|
||||
513F32882593EF8F0003048F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; };
|
||||
513F32892593EF8F0003048F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||
5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */; };
|
||||
514217062921C9DD00963F14 /* Bundle-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF42273625800C787DC /* Bundle-Extensions.swift */; };
|
||||
5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142192923522B5500E07E2C /* ImageViewController.swift */; };
|
||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514219362352510100E07E2C /* ImageScrollView.swift */; };
|
||||
5142194B2353C1CF00E07E2C /* main_mac.js in Resources */ = {isa = PBXBuildFile; fileRef = 5142194A2353C1CF00E07E2C /* main_mac.js */; };
|
||||
@@ -818,6 +819,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 +829,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 */; };
|
||||
@@ -1578,11 +1582,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>"; };
|
||||
@@ -2074,7 +2080,6 @@
|
||||
children = (
|
||||
51F9F3FA23DFB25700A314FD /* Animations.swift */,
|
||||
51F85BFA2275D85000C787DC /* Array-Extensions.swift */,
|
||||
51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
|
||||
51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */,
|
||||
512AF9C1236ED52C0066F8BE /* ImageHeaderView.swift */,
|
||||
512AF9DC236F05230066F8BE /* InteractiveLabel.swift */,
|
||||
@@ -2293,6 +2298,7 @@
|
||||
5117715424E1EA0F00A2A836 /* ArticleExtractorButton.swift */,
|
||||
51FA73B62332D5F70090D516 /* LegacyArticleExtractorButton.swift */,
|
||||
847CD6C9232F4CBF00FAC46D /* IconView.swift */,
|
||||
B2C12C6528F4C46800373730 /* URLPasteboardWriter+NetNewsWire.swift */,
|
||||
844B5B6B1FEA224B00C7C76A /* Keyboard */,
|
||||
849A975F1ED9EB95007D329B /* Sidebar */,
|
||||
849A97681ED9EBC8007D329B /* Timeline */,
|
||||
@@ -2423,6 +2429,7 @@
|
||||
51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */,
|
||||
849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */,
|
||||
849A97581ED9EB0D007D329B /* ArticleUtilities.swift */,
|
||||
51F85BF42273625800C787DC /* Bundle-Extensions.swift */,
|
||||
5108F6B52375E612001ABC45 /* CacheCleaner.swift */,
|
||||
516AE9DE2372269A007DEEAA /* IconImage.swift */,
|
||||
849A97971ED9EFAA007D329B /* Node-Extensions.swift */,
|
||||
@@ -2698,6 +2705,7 @@
|
||||
children = (
|
||||
849C64671ED37A5D003D8FC0 /* Assets.xcassets */,
|
||||
84C9FC8922629E8F00D921D6 /* Credits.rtf */,
|
||||
B20180AA28E3B76F0059686A /* Localizable.stringsdict */,
|
||||
84C9FC8A22629E8F00D921D6 /* NetNewsWire.sdef */,
|
||||
84C9FC9022629ECB00D921D6 /* NetNewsWire.entitlements */,
|
||||
51F805D32428499E0022C792 /* NetNewsWire-dev.entitlements */,
|
||||
@@ -3603,6 +3611,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 */,
|
||||
@@ -3953,6 +3962,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 */,
|
||||
@@ -4008,6 +4018,7 @@
|
||||
65ED3FFA235DEF6C0081F399 /* WebFeedInspectorViewController.swift in Sources */,
|
||||
65ED3FFB235DEF6C0081F399 /* AccountsReaderAPIWindowController.swift in Sources */,
|
||||
65ED3FFC235DEF6C0081F399 /* AccountsAddLocalWindowController.swift in Sources */,
|
||||
514217062921C9DD00963F14 /* Bundle-Extensions.swift in Sources */,
|
||||
65ED3FFD235DEF6C0081F399 /* PasteboardFolder.swift in Sources */,
|
||||
51386A8F25673277005F3762 /* AccountCell.swift in Sources */,
|
||||
65ED3FFE235DEF6C0081F399 /* AccountsFeedbinWindowController.swift in Sources */,
|
||||
@@ -4314,6 +4325,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 */,
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
"repositoryURL": "https://github.com/Ranchero-Software/RSCore.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b0d9ac8811cc35f8cce7b099f552bc947bfcddf5",
|
||||
"version": "1.1.0"
|
||||
"revision": "fd64fb77de2c4b6a87a971d353e7eea75100f694",
|
||||
"version": "1.1.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -219,6 +219,10 @@ img, figure, video, div, object {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -8,9 +8,20 @@
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import Account
|
||||
import Articles
|
||||
|
||||
// Mark articles read/unread, starred/unstarred, deleted/undeleted.
|
||||
//
|
||||
// Directly marked articles are ones that were statused by selecting with a cursor or were selected by group.
|
||||
// Indirectly marked articles didn't have any focus and were picked up using a Mark All command like Mark All as Read.
|
||||
//
|
||||
// See discussion for details: https://github.com/Ranchero-Software/NetNewsWire/issues/3734
|
||||
|
||||
public extension Notification.Name {
|
||||
static let MarkStatusCommandDidDirectMarking = Notification.Name("MarkStatusCommandDid√DirectMarking")
|
||||
static let MarkStatusCommandDidUndoDirectMarking = Notification.Name("MarkStatusCommandDidUndoDirectMarking")
|
||||
}
|
||||
|
||||
final class MarkStatusCommand: UndoableCommand {
|
||||
|
||||
@@ -19,10 +30,11 @@ final class MarkStatusCommand: UndoableCommand {
|
||||
let articles: Set<Article>
|
||||
let undoManager: UndoManager
|
||||
let flag: Bool
|
||||
let directlyMarked: Bool
|
||||
let statusKey: ArticleStatus.Key
|
||||
var completion: (() -> Void)? = nil
|
||||
|
||||
init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
init?(initialArticles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
|
||||
// Filter out articles that already have the desired status or can't be marked.
|
||||
let articlesToMark = MarkStatusCommand.filteredArticles(initialArticles, statusKey, flag)
|
||||
@@ -30,8 +42,9 @@ final class MarkStatusCommand: UndoableCommand {
|
||||
completion?()
|
||||
return nil
|
||||
}
|
||||
self.articles = Set(articlesToMark)
|
||||
self.articles = articlesToMark
|
||||
|
||||
self.directlyMarked = directlyMarked
|
||||
self.flag = flag
|
||||
self.statusKey = statusKey
|
||||
self.undoManager = undoManager
|
||||
@@ -42,21 +55,39 @@ final class MarkStatusCommand: UndoableCommand {
|
||||
self.redoActionName = actionName
|
||||
}
|
||||
|
||||
convenience init?(initialArticles: [Article], markingRead: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: initialArticles, statusKey: .read, flag: markingRead, undoManager: undoManager, completion: completion)
|
||||
convenience init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: Set(initialArticles), statusKey: .read, flag: flag, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
|
||||
}
|
||||
|
||||
convenience init?(initialArticles: [Article], markingStarred: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, undoManager: undoManager, completion: completion)
|
||||
convenience init?(initialArticles: Set<Article>, markingRead: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: initialArticles, statusKey: .read, flag: markingRead, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
|
||||
}
|
||||
|
||||
convenience init?(initialArticles: [Article], markingRead: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: initialArticles, statusKey: .read, flag: markingRead, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
|
||||
}
|
||||
|
||||
convenience init?(initialArticles: Set<Article>, markingStarred: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
|
||||
}
|
||||
|
||||
convenience init?(initialArticles: [Article], markingStarred: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: initialArticles, statusKey: .starred, flag: markingStarred, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
|
||||
}
|
||||
|
||||
func perform() {
|
||||
mark(statusKey, flag)
|
||||
if directlyMarked {
|
||||
markStatusCommandDidDirectMarking()
|
||||
}
|
||||
registerUndo()
|
||||
}
|
||||
|
||||
func undo() {
|
||||
mark(statusKey, !flag)
|
||||
if directlyMarked {
|
||||
markStatusCommandDidUndoDirectMarking()
|
||||
}
|
||||
registerRedo()
|
||||
}
|
||||
}
|
||||
@@ -67,6 +98,18 @@ private extension MarkStatusCommand {
|
||||
markArticles(articles, statusKey: statusKey, flag: flag, completion: completion)
|
||||
completion = nil
|
||||
}
|
||||
|
||||
func markStatusCommandDidDirectMarking() {
|
||||
NotificationCenter.default.post(name: .MarkStatusCommandDidDirectMarking, object: self, userInfo: [Account.UserInfoKey.articles: articles,
|
||||
Account.UserInfoKey.statusKey: statusKey,
|
||||
Account.UserInfoKey.statusFlag: flag])
|
||||
}
|
||||
|
||||
func markStatusCommandDidUndoDirectMarking() {
|
||||
NotificationCenter.default.post(name: .MarkStatusCommandDidUndoDirectMarking, object: self, userInfo: [Account.UserInfoKey.articles: articles,
|
||||
Account.UserInfoKey.statusKey: statusKey,
|
||||
Account.UserInfoKey.statusFlag: flag])
|
||||
}
|
||||
|
||||
static private let markReadActionName = NSLocalizedString("Mark Read", comment: "command")
|
||||
static private let markUnreadActionName = NSLocalizedString("Mark Unread", comment: "command")
|
||||
@@ -83,7 +126,7 @@ private extension MarkStatusCommand {
|
||||
}
|
||||
}
|
||||
|
||||
static func filteredArticles(_ articles: [Article], _ statusKey: ArticleStatus.Key, _ flag: Bool) -> [Article] {
|
||||
static func filteredArticles(_ articles: Set<Article>, _ statusKey: ArticleStatus.Key, _ flag: Bool) -> Set<Article> {
|
||||
|
||||
return articles.filter{ article in
|
||||
guard article.status.boolStatus(forKey: statusKey) != flag else { return false }
|
||||
@@ -93,4 +136,5 @@ private extension MarkStatusCommand {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import Account
|
||||
// These handle multiple accounts.
|
||||
|
||||
func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
|
||||
|
||||
let d: [String: Set<Article>] = accountAndArticlesDictionary(articles)
|
||||
|
||||
let group = DispatchGroup()
|
||||
@@ -24,7 +23,7 @@ func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag:
|
||||
continue
|
||||
}
|
||||
group.enter()
|
||||
account.markArticles(accountArticles, statusKey: statusKey, flag: flag) { _ in
|
||||
account.mark(articles: accountArticles, statusKey: statusKey, flag: flag) { _ in
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,17 @@ extension URL {
|
||||
|
||||
/// URL pointing to current app version release notes.
|
||||
static var releaseNotes: URL {
|
||||
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
|
||||
var gitHub = "https://github.com/Ranchero-Software/NetNewsWire/releases/tag/"
|
||||
|
||||
#if os(macOS)
|
||||
gitHub += "mac-\(String(describing: appVersion))"
|
||||
return URL(string: gitHub)!
|
||||
gitHub += "mac-"
|
||||
#else
|
||||
gitHub += "ios-\(String(describing: appVersion))"
|
||||
return URL(string: gitHub)!
|
||||
gitHub += "ios-"
|
||||
#endif
|
||||
|
||||
gitHub += "\(Bundle.main.versionNumber)-\(Bundle.main.buildNumber)"
|
||||
|
||||
return URL(string: gitHub)!
|
||||
}
|
||||
|
||||
func valueFor(_ parameter: String) -> String? {
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
</head>
|
||||
<body>
|
||||
<outline text="Colossal" title="Colossal" type="rss" version="RSS" htmlUrl="https://www.thisiscolossal.com/" xmlUrl="https://www.thisiscolossal.com/feed/"/>
|
||||
<outline text="Accidentally in Code" title="Accidentally in Code" type="rss" version="RSS" htmlUrl="https://cate.blog/" xmlUrl="https://cate.blog/feed/"/>
|
||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
|
||||
<outline text="Maurice Parker" title="Maurice Parker" type="rss" version="RSS" htmlUrl="https://vincode.io/" xmlUrl="https://vincode.io/feed.xml"/>
|
||||
<outline text="Maurice Parker" title="Maurice Parker" type="rss" version="RSS" htmlUrl="https://vincode.io/" xmlUrl="https://vincode.io/feed.xml"/>
|
||||
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>
|
||||
<outline text="Manton Reece" title="Manton Reece" type="rss" version="RSS" htmlUrl="https://manton.org/" xmlUrl="https://www.manton.org/feed/json"/>
|
||||
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
|
||||
<outline text="Julia Evans" title="Julia Evans" type="rss" version="RSS" htmlUrl="https://jvns.ca/" xmlUrl="https://jvns.ca/atom.xml"/>
|
||||
<outline text="Jason Kottke" title="Jason Kottke" type="rss" version="RSS" htmlUrl="https://kottke.org/" xmlUrl="http://feeds.kottke.org/json"/>
|
||||
<outline text="Six Colors" title="Six Colors" type="rss" version="RSS" htmlUrl="https://sixcolors.com/" xmlUrl="https://feedpress.me/sixcolors?type=xml"/>
|
||||
|
||||
@@ -277,6 +277,10 @@ img, figure, video, div, object {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -224,6 +224,10 @@ img, figure, video, div, object {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -273,6 +273,10 @@ img, figure, video, div, object {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -246,6 +246,10 @@ img, figure, video, div, object {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -241,6 +241,10 @@ img, figure, video, div, object {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Mac Release Notes
|
||||
|
||||
### 6.1.1b1 build 6107 3 Nov 2022
|
||||
|
||||
Fixed a bug that could prevent users from accessing BazQux if an article was missing a field
|
||||
Fixed an issue that could prevent Feedly users from syncing if they tried to mark too many articles as read at the same time
|
||||
|
||||
### 6.1 build 6106 6 April 2022
|
||||
|
||||
Small cosmetic change — better alignment for items in General Preferences pane
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
# iOS Release Notes
|
||||
|
||||
### 6.1 Release build 6110 - 9 Nov 2022
|
||||
|
||||
Changes since 6.0.1…
|
||||
|
||||
Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences.
|
||||
Fixed a bug that could prevent BazQux syncing when an article may not contain all the info we expect
|
||||
Fixed a bug that could prevent Feedly syncing when marking a large number of articles as read
|
||||
Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled
|
||||
Added links to iCloud Syncing Limitations & Solutions on iCloud Account Management UI
|
||||
Copy URLs using repaired, rather than raw, feed links
|
||||
Fixed bug showing quote tweets that only included an image
|
||||
Video autoplay is now disallowed
|
||||
Article view now supports RTL layout
|
||||
Fixed a few crashing bugs
|
||||
Fixed a layout bug that could happen on returning to the Feeds list
|
||||
Fixed a bug where go-to-feed might not properly expand disclosure triangles
|
||||
Prevented the Delete option from showing in the Edit menu on the Article View
|
||||
Fixed Widget article icon lookup bug
|
||||
|
||||
|
||||
### 6.1 TestFlight build 6109 - 31 Oct 2022
|
||||
|
||||
Enhanced Widget integration to make counts more accurate
|
||||
Enhanced Widget integration to make make it more efficient and save on battery life
|
||||
|
||||
### 6.1 TestFlight build 6108 - 28 Oct 2022
|
||||
|
||||
Fixed a bug that could prevent BazQux syncing when an article may not contain all the info we expect
|
||||
Fixed a bug that could prevent Feedly syncing when marking a large number of articles as read
|
||||
Prevent Widget integration from running while in the background to remove some crashes
|
||||
@@ -43,6 +69,14 @@ Fixed a bug where go-to-feed might not properly expand disclosure triangles
|
||||
* Video autoplay is now disallowed.
|
||||
* Article view now supports RTL layout.
|
||||
|
||||
### 6.0.2 Release - 15 Oct 2021
|
||||
|
||||
Makes a particular crash on startup, that happens only on iPad, far less likely.
|
||||
|
||||
### 6.0.2 TestFlight build 610 - 25 Sep 2021
|
||||
|
||||
Fixed bug with state restoration on launch (bug introduced in previous TestFlight build)
|
||||
|
||||
### 6.0.1 TestFlight build 608 - 28 Aug 2021
|
||||
|
||||
* Fixed our top crashing bug — it could happen when updating a table view
|
||||
|
||||
@@ -55,7 +55,6 @@ final class AppDefaults: ObservableObject {
|
||||
static let articleFullscreenEnabled = "articleFullscreenEnabled"
|
||||
static let hasUsedFullScreenPreviously = "hasUsedFullScreenPreviously"
|
||||
static let confirmMarkAllAsRead = "confirmMarkAllAsRead"
|
||||
static let lastRefresh = "lastRefresh"
|
||||
static let addWebFeedAccountID = "addWebFeedAccountID"
|
||||
static let addWebFeedFolderName = "addWebFeedFolderName"
|
||||
static let addFolderAccountID = "addFolderAccountID"
|
||||
@@ -223,17 +222,6 @@ final class AppDefaults: ObservableObject {
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.confirmMarkAllAsRead, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
var lastRefresh: Date? {
|
||||
get {
|
||||
return AppDefaults.date(for: Key.lastRefresh)
|
||||
}
|
||||
set {
|
||||
AppDefaults.setDate(for: Key.lastRefresh, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import UIKit
|
||||
import RSCore
|
||||
import RSWeb
|
||||
import Account
|
||||
import Articles
|
||||
import BackgroundTasks
|
||||
import Secrets
|
||||
import WidgetKit
|
||||
@@ -73,7 +74,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
FeedProviderManager.shared.delegate = ExtensionPointManager.shared
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
@@ -150,10 +150,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
}
|
||||
|
||||
@objc func accountRefreshDidFinish(_ note: Notification) {
|
||||
AppDefaults.shared.lastRefresh = Date()
|
||||
}
|
||||
|
||||
// MARK: - API
|
||||
|
||||
func manualRefresh(errorHandler: @escaping (Error) -> ()) {
|
||||
@@ -183,7 +179,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
extensionFeedAddRequestFile.resume()
|
||||
syncTimer?.update()
|
||||
|
||||
if let lastRefresh = AppDefaults.shared.lastRefresh {
|
||||
if let lastRefresh = AccountManager.shared.lastArticleFetchEndTime {
|
||||
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||
} else {
|
||||
@@ -432,55 +428,40 @@ private extension AppDelegate {
|
||||
private extension AppDelegate {
|
||||
|
||||
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
resumeDatabaseProcessingIfNecessary()
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
logger.debug("No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
logger.debug("No account found from search using \(articleID, privacy: .public)")
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
|
||||
self.prepareAccountsForBackground()
|
||||
account!.syncArticleStatus(completion: { [weak self] _ in
|
||||
if !AccountManager.shared.isSuspended {
|
||||
self?.prepareAccountsForBackground()
|
||||
self?.suspendApplication()
|
||||
}
|
||||
})
|
||||
markArticle(userInfo: userInfo, statusKey: .read)
|
||||
}
|
||||
|
||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
||||
markArticle(userInfo: userInfo, statusKey: .starred)
|
||||
}
|
||||
|
||||
func markArticle(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
resumeDatabaseProcessingIfNecessary()
|
||||
let account = AccountManager.shared.existingAccount(with: accountID)
|
||||
guard account != nil else {
|
||||
|
||||
guard let account = AccountManager.shared.existingAccount(with: accountID) else {
|
||||
logger.debug("No account found from notification.")
|
||||
return
|
||||
}
|
||||
let article = try? account!.fetchArticles(.articleIDs([articleID]))
|
||||
guard article != nil else {
|
||||
|
||||
guard let articles = try? account.fetchArticles(.articleIDs([articleID])), !articles.isEmpty else {
|
||||
logger.debug("No article found from search using \(articleID, privacy: .public)")
|
||||
return
|
||||
}
|
||||
account!.markArticles(article!, statusKey: .starred, flag: true) { _ in }
|
||||
account!.syncArticleStatus(completion: { [weak self] _ in
|
||||
if !AccountManager.shared.isSuspended {
|
||||
self?.prepareAccountsForBackground()
|
||||
self?.suspendApplication()
|
||||
}
|
||||
})
|
||||
|
||||
account.mark(articles: articles, statusKey: statusKey, flag: true) { [weak self] _ in
|
||||
account.syncArticleStatus(completion: { [weak self] _ in
|
||||
if !AccountManager.shared.isSuspended {
|
||||
self?.prepareAccountsForBackground()
|
||||
self?.suspendApplication()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import WebKit
|
||||
import Account
|
||||
import Articles
|
||||
import SafariServices
|
||||
import RSCore
|
||||
|
||||
class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
class ArticleViewController: UIViewController, MainControllerIdentifiable, Logging {
|
||||
|
||||
typealias State = (extractedArticle: ExtractedArticle?,
|
||||
isShowingExtractedArticle: Bool,
|
||||
@@ -259,7 +260,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: ArticleThemesManager.shared.currentThemeName == themeName ? .on : .off,
|
||||
state: ArticleThemesManager.shared.currentTheme.name == themeName ? .on : .off,
|
||||
handler: { action in
|
||||
ArticleThemesManager.shared.currentThemeName = themeName
|
||||
})
|
||||
@@ -271,7 +272,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable {
|
||||
identifier: nil,
|
||||
discoverabilityTitle: nil,
|
||||
attributes: [],
|
||||
state: ArticleThemesManager.shared.currentThemeName == AppDefaults.defaultThemeName ? .on : .off,
|
||||
state: ArticleThemesManager.shared.currentTheme.name == AppDefaults.defaultThemeName ? .on : .off,
|
||||
handler: { _ in
|
||||
ArticleThemesManager.shared.currentThemeName = AppDefaults.defaultThemeName
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ class AccountInspectorViewController: UITableViewController {
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
@IBOutlet weak var activeSwitch: UISwitch!
|
||||
@IBOutlet weak var deleteAccountButton: VibrantButton!
|
||||
@IBOutlet weak var limitationsAndSolutionsButton: UIButton!
|
||||
@IBOutlet weak var limitationsAndSolutionsView: UIView!
|
||||
|
||||
var isModal = false
|
||||
weak var account: Account?
|
||||
@@ -59,7 +59,7 @@ class AccountInspectorViewController: UITableViewController {
|
||||
}
|
||||
|
||||
if account.type != .cloudKit {
|
||||
limitationsAndSolutionsButton.isHidden = true
|
||||
limitationsAndSolutionsView.isHidden = true
|
||||
}
|
||||
|
||||
if isModal {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@@ -174,7 +174,7 @@
|
||||
<connections>
|
||||
<outlet property="activeSwitch" destination="6YV-K0-yPS" id="d9M-GP-aTR"/>
|
||||
<outlet property="deleteAccountButton" destination="obv-a5-Pl6" id="idW-gm-BIJ"/>
|
||||
<outlet property="limitationsAndSolutionsButton" destination="dgD-uX-vcx" id="5ti-AM-xms"/>
|
||||
<outlet property="limitationsAndSolutionsView" destination="3V2-Cm-ezj" id="Na9-t7-crH"/>
|
||||
<outlet property="nameTextField" destination="LUW-uv-piz" id="e2P-Hq-guh"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
|
||||
@@ -1059,8 +1059,7 @@ private extension MasterFeedViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Command")
|
||||
let cancel = {
|
||||
completion(true)
|
||||
}
|
||||
@@ -1140,8 +1139,7 @@ private extension MasterFeedViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Command")
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
||||
if let articles = try? feed.fetchUnreadArticles() {
|
||||
@@ -1158,8 +1156,7 @@ private extension MasterFeedViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Command")
|
||||
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
|
||||
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
|
||||
@@ -53,14 +53,16 @@ struct RefreshProgressView: View {
|
||||
.offset(x: -Self.width * 0.6, y: 0)
|
||||
.offset(x: Self.width * 1.2 * self.offset, y: 0)
|
||||
.animation(.default.repeatForever().speed(0.265), value: self.offset)
|
||||
.onAppear{
|
||||
.onAppear {
|
||||
withAnimation {
|
||||
self.offset = 1
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
self.offset = 0
|
||||
}
|
||||
)
|
||||
.clipShape(Capsule())
|
||||
.animation(.default, value: refreshProgressModel.isRefreshing)
|
||||
.frame(width: Self.width, height: Self.height)
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
}
|
||||
}
|
||||
|
||||
private var directlyMarkedAsUnreadArticles = Set<Article>()
|
||||
|
||||
var prefersStatusBarHidden = false
|
||||
|
||||
private let treeControllerDelegate = WebFeedTreeControllerDelegate()
|
||||
@@ -331,6 +333,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidDirectMarking(_:)), name: .MarkStatusCommandDidDirectMarking, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidUndoDirectMarking(_:)), name: .MarkStatusCommandDidUndoDirectMarking, object: nil)
|
||||
}
|
||||
|
||||
func restoreWindowState(_ activity: NSUserActivity?) {
|
||||
@@ -547,6 +551,28 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
}
|
||||
}
|
||||
|
||||
@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: API
|
||||
|
||||
func suspend() {
|
||||
@@ -611,7 +637,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
}
|
||||
|
||||
func nodeFor(_ indexPath: IndexPath) -> Node? {
|
||||
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].feedNodes.count else {
|
||||
guard indexPath.section > -1 &&
|
||||
indexPath.row > -1 &&
|
||||
indexPath.section < shadowTable.count &&
|
||||
indexPath.row < shadowTable[indexPath.section].feedNodes.count else {
|
||||
return nil
|
||||
}
|
||||
return shadowTable[indexPath.section].feedNodes[indexPath.row].node
|
||||
@@ -863,8 +892,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
if let oldTimelineFeed = preSearchTimelineFeed {
|
||||
emptyTheTimeline()
|
||||
timelineFeed = oldTimelineFeed
|
||||
masterTimelineViewController?.reinitializeArticles(resetScroll: true)
|
||||
replaceArticles(with: savedSearchArticles!, animated: true)
|
||||
masterTimelineViewController?.reinitializeArticles(resetScroll: true)
|
||||
} else {
|
||||
setTimelineFeed(nil, animated: true)
|
||||
}
|
||||
@@ -997,7 +1026,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
}
|
||||
|
||||
func markAllAsRead(_ articles: [Article], completion: (() -> Void)? = nil) {
|
||||
markArticlesWithUndo(articles, statusKey: .read, flag: true, completion: completion)
|
||||
var markableArticles = Set(articles)
|
||||
markableArticles.subtract(directlyMarkedAsUnreadArticles)
|
||||
markArticlesWithUndo(markableArticles, statusKey: .read, flag: true, directlyMarked: false, completion: completion)
|
||||
}
|
||||
|
||||
func markAllAsReadInTimeline(completion: (() -> Void)? = nil) {
|
||||
@@ -1045,13 +1076,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
|
||||
func markAsReadForCurrentArticle() {
|
||||
if let article = currentArticle {
|
||||
markArticlesWithUndo([article], statusKey: .read, flag: true)
|
||||
markArticlesWithUndo([article], statusKey: .read, flag: true, directlyMarked: true)
|
||||
}
|
||||
}
|
||||
|
||||
func markAsUnreadForCurrentArticle() {
|
||||
if let article = currentArticle {
|
||||
markArticlesWithUndo([article], statusKey: .read, flag: false)
|
||||
markArticlesWithUndo([article], statusKey: .read, flag: false, directlyMarked: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1063,7 +1094,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
|
||||
func toggleRead(_ article: Article) {
|
||||
guard !article.status.read || article.isAvailableToMarkUnread else { return }
|
||||
markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read)
|
||||
markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read, directlyMarked: true)
|
||||
}
|
||||
|
||||
func toggleStarredForCurrentArticle() {
|
||||
@@ -1073,7 +1104,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging {
|
||||
}
|
||||
|
||||
func toggleStar(_ article: Article) {
|
||||
markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred)
|
||||
markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred, directlyMarked: true)
|
||||
}
|
||||
|
||||
func timelineFeedIsEqualTo(_ feed: WebFeed) -> Bool {
|
||||
@@ -1413,9 +1444,18 @@ private extension SceneCoordinator {
|
||||
navController.toolbar.tintColor = AppAssets.primaryAccentColor
|
||||
}
|
||||
|
||||
func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, completion: (() -> Void)? = nil) {
|
||||
func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, completion: (() -> Void)? = nil) {
|
||||
markArticlesWithUndo(Set(articles), statusKey: statusKey, flag: flag, directlyMarked: directlyMarked, completion: completion)
|
||||
}
|
||||
|
||||
func markArticlesWithUndo(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, completion: (() -> Void)? = nil) {
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager, completion: completion) else {
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: articles,
|
||||
statusKey: statusKey,
|
||||
flag: flag,
|
||||
directlyMarked: directlyMarked,
|
||||
undoManager: undoManager,
|
||||
completion: completion) else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
@@ -1933,6 +1973,7 @@ private extension SceneCoordinator {
|
||||
|
||||
func emptyTheTimeline() {
|
||||
if !articles.isEmpty {
|
||||
directlyMarkedAsUnreadArticles = Set<Article>()
|
||||
replaceArticles(with: Set<Article>(), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
||||
MARKETING_VERSION = 6.1
|
||||
CURRENT_PROJECT_VERSION = 6108
|
||||
CURRENT_PROJECT_VERSION = 6110
|
||||
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// High Level Settings common to both the Mac application and any extensions we bundle with it
|
||||
MARKETING_VERSION = 6.1
|
||||
CURRENT_PROJECT_VERSION = 6106
|
||||
MARKETING_VERSION = 6.1.1
|
||||
CURRENT_PROJECT_VERSION = 6107
|
||||
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
|
||||
Reference in New Issue
Block a user