Merge branch 'main' into ios-ui-settings

# Conflicts:
#	iOS/AppDefaults.swift
This commit is contained in:
Stuart Breckenridge
2022-12-04 21:10:17 +08:00
46 changed files with 654 additions and 327 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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)")
}
}

View File

@@ -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)")
}

View File

@@ -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?

View File

@@ -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(()))

View File

@@ -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)
}
}
}
}
}

View File

@@ -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:

View File

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

View File

@@ -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 }
}
}

View File

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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -204,7 +204,15 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(copyArticleURL(_:)) {
return canCopyArticleURL()
let canCopyArticleURL = canCopyArticleURL()
if let item = item as? NSMenuItem {
let format = NSLocalizedString("Copy Article URL", comment: "Copy Article URL");
item.title = String.localizedStringWithFormat(format, selectedArticles?.count ?? 0)
}
return canCopyArticleURL
}
if item.action == #selector(copyExternalURL(_:)) {
@@ -321,21 +329,21 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
}
@IBAction func copyArticleURL(_ sender: Any?) {
if let link = oneSelectedArticle?.preferredURL?.absoluteString {
URLPasteboardWriter.write(urlString: link, to: .general)
if let currentLinks {
URLPasteboardWriter.write(urlStrings: currentLinks, alertingIn: window)
}
}
@IBAction func copyExternalURL(_ sender: Any?) {
if let link = oneSelectedArticle?.externalLink {
URLPasteboardWriter.write(urlString: link, to: .general)
if let links = selectedArticles?.compactMap({ $0.externalLink }) {
URLPasteboardWriter.write(urlStrings: links, to: .general)
}
}
@IBAction func openArticleInBrowser(_ sender: Any?) {
if let link = currentLink {
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
}
guard let selectedArticles else { return }
let urlStrings = selectedArticles.compactMap { $0.preferredLink }
Browser.open(urlStrings, fromWindow: window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
}
@IBAction func openInBrowser(_ sender: Any?) {
@@ -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]?) {
// Dont 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
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -0,0 +1,36 @@
//
// URLPasteboardWriter+NetNewsWire.swift
// NetNewsWire
//
// Created by Nate Weaver on 2022-10-10.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import RSCore
extension URLPasteboardWriter {
/// Copy URL strings, alerting the user the first time the array of URL strings contains `nil`.
/// - Parameters:
/// - urlStrings: The URL strings to copy.
/// - pasteboard: The pastebaord to copy to.
/// - window: The window to use as a sheet parent for the alert. If `nil`, will run the alert modally.
static func write(urlStrings: [String?], to pasteboard: NSPasteboard = .general, alertingIn window: NSWindow?) {
URLPasteboardWriter.write(urlStrings: urlStrings.compactMap { $0 }, to: pasteboard)
if urlStrings.contains(nil), !AppDefaults.shared.hasSeenNotAllArticlesHaveURLsAlert {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Some articles dont have links, so they weren't copied.", comment: "\"Some articles have no links\" copy alert message text")
alert.informativeText = NSLocalizedString("You won't see this message again.", comment: "You won't see this message again")
if let window {
alert.beginSheetModal(for: window)
} else {
alert.runModal() // this should never happen
}
AppDefaults.shared.hasSeenNotAllArticlesHaveURLsAlert = true
}
}
}

View File

@@ -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

View File

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

View File

@@ -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 */,

View File

@@ -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"
}
},
{

View File

@@ -219,6 +219,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
video {
width: 100% !important;
}
iframe {
max-width: 100%;
margin: 0 auto;

View File

@@ -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 {
}
}
}

View File

@@ -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()
}
}

View File

@@ -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? {

View File

@@ -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"/>

View File

@@ -277,6 +277,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
video {
width: 100% !important;
}
iframe {
max-width: 100%;
margin: 0 auto;

View File

@@ -224,6 +224,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
video {
width: 100% !important;
}
iframe {
max-width: 100%;
margin: 0 auto;

View File

@@ -273,6 +273,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
video {
width: 100% !important;
}
iframe {
max-width: 100%;
margin: 0 auto;

View File

@@ -246,6 +246,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
video {
width: 100% !important;
}
iframe {
max-width: 100%;
margin: 0 auto;

View File

@@ -241,6 +241,10 @@ img, figure, video, div, object {
margin: 0 auto;
}
video {
width: 100% !important;
}
iframe {
max-width: 100%;
margin: 0 auto;

View File

@@ -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

View File

@@ -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 arent 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

View File

@@ -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()
}
}

View File

@@ -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()
}
})
}
}
}

View File

@@ -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
})

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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;