From 59af6041cab134210c0381bf7e4720db75bf919b Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 3 Jan 2025 22:58:25 -0800 Subject: [PATCH] Get Mac and iOS builds building. --- .../xcshareddata/xcschemes/Account.xcscheme | 2 +- Account/Sources/Account/Feed.swift | 9 -- .../LocalAccount/LocalAccountRefresher.swift | 2 +- .../xcshareddata/xcschemes/Articles.xcscheme | 2 +- Mac/AppDelegate.swift | 3 +- .../Sidebar/SidebarViewController.swift | 6 +- .../Timeline/Cell/MainTimelineCellData.swift | 79 ------------- .../Cell/MainUnreadIndicatorView.swift | 45 -------- .../Timeline/Cell/TimelineCellData.swift | 79 +++++++++++++ .../Timeline/Cell/TimelineCellLayout.swift | 20 ++-- .../Timeline/Cell/TimelineTableCellView.swift | 4 +- .../Timeline/Cell/UnreadIndicatorView.swift | 45 ++++++++ .../Keyboard/TimelineKeyboardDelegate.swift | 2 +- .../TimelineContainerViewController.swift | 18 +-- Mac/MainWindow/Timeline/TimelineTableView.xib | 2 +- ...melineViewController+ContextualMenus.swift | 6 +- ...ler.swift => TimelineViewController.swift} | 38 +++---- NetNewsWire.xcodeproj/project.pbxproj | 2 +- .../xcschemes/NetNewsWire MAS.xcscheme | 2 +- .../NetNewsWire iOS Share Extension.xcscheme | 2 +- .../xcschemes/NetNewsWire-iOS.xcscheme | 2 +- .../xcschemes/NetNewsWire.xcscheme | 2 +- .../xcshareddata/xcschemes/Parser.xcscheme | 2 +- .../xcschemes/ParserTests.xcscheme | 2 +- .../xcshareddata/xcschemes/RSCore.xcscheme | 2 +- .../xcschemes/RSCoreTests.xcscheme | 2 +- RSCore/Sources/RSCore/AppConfig.swift | 67 +++++++++++ .../xcschemes/RSDatabase.xcscheme | 2 +- Shared/Favicons/FaviconDownloader.swift | 8 +- Shared/Favicons/FaviconURLFinder.swift | 67 ----------- .../HTMLMetadata/HTMLMetadataDownloader.swift | 43 ------- Shared/Images/FeedIconDownloader.swift | 5 +- .../Widget Views/SmartFeedSummaryWidget.swift | 107 ------------------ Widget/Widget Views/WidgetLayout.swift | 19 ++++ iOS/Article/WebViewController.swift | 2 +- iOS/Base.lproj/Main.storyboard | 6 +- .../FeedInspectorViewController.swift | 2 +- ...ler.swift => TimelineViewController.swift} | 12 +- iOS/SceneCoordinator.swift | 4 +- 39 files changed, 293 insertions(+), 431 deletions(-) delete mode 100644 Mac/MainWindow/Timeline/Cell/MainTimelineCellData.swift delete mode 100644 Mac/MainWindow/Timeline/Cell/MainUnreadIndicatorView.swift rename Mac/MainWindow/Timeline/{MainTimelineViewController.swift => TimelineViewController.swift} (95%) create mode 100644 RSCore/Sources/RSCore/AppConfig.swift delete mode 100644 Shared/Favicons/FaviconURLFinder.swift delete mode 100644 Shared/HTMLMetadata/HTMLMetadataDownloader.swift delete mode 100644 Widget/Widget Views/SmartFeedSummaryWidget.swift create mode 100644 Widget/Widget Views/WidgetLayout.swift rename iOS/MainTimeline/{MainTimelineViewController.swift => TimelineViewController.swift} (98%) diff --git a/Account/.swiftpm/xcode/xcshareddata/xcschemes/Account.xcscheme b/Account/.swiftpm/xcode/xcshareddata/xcschemes/Account.xcscheme index ae99f51f5..52200823d 100644 --- a/Account/.swiftpm/xcode/xcshareddata/xcschemes/Account.xcscheme +++ b/Account/.swiftpm/xcode/xcshareddata/xcschemes/Account.xcscheme @@ -1,6 +1,6 @@ CGFloat { + static func height(for width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> CGFloat { let layout = TimelineCellLayout(width: width, height: 0.0, cellData: cellData, appearance: appearance, hasIcon: true) return layout.height @@ -93,7 +93,7 @@ struct TimelineCellLayout { private extension TimelineCellLayout { - static func rectForTextBox(_ appearance: TimelineCellAppearance, _ cellData: MainTimelineCellData, _ showIcon: Bool, _ width: CGFloat) -> NSRect { + static func rectForTextBox(_ appearance: TimelineCellAppearance, _ cellData: TimelineCellData, _ showIcon: Bool, _ width: CGFloat) -> NSRect { // Returned height is a placeholder. Not needed when this is calculated. @@ -106,7 +106,7 @@ private extension TimelineCellLayout { return textBoxRect } - static func rectForTitle(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: MainTimelineCellData) -> (NSRect, Int) { + static func rectForTitle(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> (NSRect, Int) { var r = textBoxRect @@ -124,7 +124,7 @@ private extension TimelineCellLayout { return (r, sizeInfo.numberOfLinesUsed) } - static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: MainTimelineCellData) -> NSRect { + static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { if titleNumberOfLines >= appearance.titleNumberOfLines || cellData.text.isEmpty { return NSRect.zero } @@ -142,7 +142,7 @@ private extension TimelineCellLayout { } - static func rectForText(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: MainTimelineCellData) -> NSRect { + static func rectForText(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { var r = textBoxRect if cellData.text.isEmpty { @@ -158,7 +158,7 @@ private extension TimelineCellLayout { return r } - static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: MainTimelineCellData) -> NSRect { + static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { let textFieldSize = SingleLineTextFieldSizer.size(for: cellData.dateString, font: appearance.dateFont) var r = NSZeroRect @@ -171,7 +171,7 @@ private extension TimelineCellLayout { return r } - static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: MainTimelineCellData) -> NSRect { + static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { if cellData.showFeedName == .none { return NSZeroRect } @@ -208,7 +208,7 @@ private extension TimelineCellLayout { return r } - static func rectForIcon(_ cellData: MainTimelineCellData, _ appearance: TimelineCellAppearance, _ showIcon: Bool, _ textBoxRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect { + static func rectForIcon(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ showIcon: Bool, _ textBoxRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect { var r = NSRect.zero if !showIcon { @@ -221,7 +221,7 @@ private extension TimelineCellLayout { return r } - static func rectForSeparator(_ cellData: MainTimelineCellData, _ appearance: TimelineCellAppearance, _ alignmentRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect { + static func rectForSeparator(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ alignmentRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect { return NSRect(x: alignmentRect.minX, y: height - 1, width: width - alignmentRect.minX, height: 1) } } diff --git a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift index 2e0930670..24a88abc0 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -14,7 +14,7 @@ class TimelineTableCellView: NSTableCellView { private let titleView = TimelineTableCellView.multiLineTextField() private let summaryView = TimelineTableCellView.multiLineTextField() private let textView = TimelineTableCellView.multiLineTextField() - private let unreadIndicatorView = MainUnreadIndicatorView(frame: NSZeroRect) + private let unreadIndicatorView = UnreadIndicatorView(frame: NSZeroRect) private let dateView = TimelineTableCellView.singleLineTextField() private let feedNameView = TimelineTableCellView.singleLineTextField() @@ -36,7 +36,7 @@ class TimelineTableCellView: NSTableCellView { } } - var cellData: MainTimelineCellData! { + var cellData: TimelineCellData! { didSet { updateSubviews() } diff --git a/Mac/MainWindow/Timeline/Cell/UnreadIndicatorView.swift b/Mac/MainWindow/Timeline/Cell/UnreadIndicatorView.swift index e69de29bb..d3b4d7c00 100644 --- a/Mac/MainWindow/Timeline/Cell/UnreadIndicatorView.swift +++ b/Mac/MainWindow/Timeline/Cell/UnreadIndicatorView.swift @@ -0,0 +1,45 @@ +// +// UnreadIndicatorView.swift +// NetNewsWire +// +// Created by Brent Simmons on 2/16/16. +// Copyright © 2016 Ranchero Software, LLC. All rights reserved. +// + +import AppKit + +class UnreadIndicatorView: NSView { + + static let unreadCircleDimension: CGFloat = 8.0 + + var isEmphasized = false { + didSet { + if isEmphasized != oldValue { + needsDisplay = true + } + } + } + + var isSelected = false { + didSet { + if isSelected != oldValue { + needsDisplay = true + } + } + } + + static let bezierPath: NSBezierPath = { + let r = NSRect(x: 0.0, y: 0.0, width: unreadCircleDimension, height: unreadCircleDimension) + return NSBezierPath(ovalIn: r) + }() + + override func draw(_ dirtyRect: NSRect) { + if isSelected && isEmphasized { + NSColor.white.setFill() + } else { + NSColor.controlAccentColor.setFill() + } + UnreadIndicatorView.bezierPath.fill() + } + +} diff --git a/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift b/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift index 94cd5a236..fb228538b 100644 --- a/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift +++ b/Mac/MainWindow/Timeline/Keyboard/TimelineKeyboardDelegate.swift @@ -13,7 +13,7 @@ import RSCore @objc final class TimelineKeyboardDelegate: NSObject, KeyboardDelegate { - @IBOutlet weak var timelineViewController: MainTimelineViewController? + @IBOutlet weak var timelineViewController: TimelineViewController? let shortcuts: Set override init() { diff --git a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift index af61c9a9b..b589aa939 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerViewController.swift @@ -27,7 +27,7 @@ final class TimelineContainerViewController: NSViewController { @IBOutlet weak var readFilteredButton: NSButton! @IBOutlet var containerView: TimelineContainerView! - var currentTimelineViewController: MainTimelineViewController? { + var currentTimelineViewController: TimelineViewController? { didSet { let view = currentTimelineViewController?.view if containerView.contentView === view { @@ -51,10 +51,10 @@ final class TimelineContainerViewController: NSViewController { } lazy var regularTimelineViewController = { - return MainTimelineViewController(delegate: self) + return TimelineViewController(delegate: self) }() - private lazy var searchTimelineViewController: MainTimelineViewController = { - let viewController = MainTimelineViewController(delegate: self) + private lazy var searchTimelineViewController: TimelineViewController = { + let viewController = TimelineViewController(delegate: self) viewController.showsSearchResults = true return viewController }() @@ -137,15 +137,15 @@ final class TimelineContainerViewController: NSViewController { extension TimelineContainerViewController: TimelineDelegate { - func timelineSelectionDidChange(_ timelineViewController: MainTimelineViewController, selectedArticles: [Article]?) { + func timelineSelectionDidChange(_ timelineViewController: TimelineViewController, selectedArticles: [Article]?) { delegate?.timelineSelectionDidChange(self, articles: selectedArticles, mode: mode(for: timelineViewController)) } - func timelineRequestedFeedSelection(_: MainTimelineViewController, feed: Feed) { + func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) { delegate?.timelineRequestedFeedSelection(self, feed: feed) } - func timelineInvalidatedRestorationState(_: MainTimelineViewController) { + func timelineInvalidatedRestorationState(_: TimelineViewController) { delegate?.timelineInvalidatedRestorationState(self) } @@ -158,7 +158,7 @@ private extension TimelineContainerViewController { attributes: [NSAttributedString.Key.font: NSFont.controlContentFont(ofSize: NSFont.systemFontSize)]) } - func timelineViewController(for mode: TimelineSourceMode) -> MainTimelineViewController { + func timelineViewController(for mode: TimelineSourceMode) -> TimelineViewController { switch mode { case .regular: return regularTimelineViewController @@ -167,7 +167,7 @@ private extension TimelineContainerViewController { } } - func mode(for timelineViewController: MainTimelineViewController) -> TimelineSourceMode { + func mode(for timelineViewController: TimelineViewController) -> TimelineSourceMode { if timelineViewController === regularTimelineViewController { return .regular } diff --git a/Mac/MainWindow/Timeline/TimelineTableView.xib b/Mac/MainWindow/Timeline/TimelineTableView.xib index 438d06f84..94c4b9c88 100644 --- a/Mac/MainWindow/Timeline/TimelineTableView.xib +++ b/Mac/MainWindow/Timeline/TimelineTableView.xib @@ -5,7 +5,7 @@ - + diff --git a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index 11916d131..0a05787e3 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -11,7 +11,7 @@ import RSCore import Articles import Account -extension MainTimelineViewController { +extension TimelineViewController { func contextualMenuForClickedRows() -> NSMenu? { @@ -30,7 +30,7 @@ extension MainTimelineViewController { // MARK: Contextual Menu Actions -extension MainTimelineViewController { +extension TimelineViewController { @objc func markArticlesReadFromContextualMenu(_ sender: Any?) { guard let articles = articles(from: sender) else { return } @@ -107,7 +107,7 @@ extension MainTimelineViewController { } -private extension MainTimelineViewController { +private extension TimelineViewController { func markArticles(_ articles: [Article], read: Bool) { markArticles(articles, statusKey: .read, flag: read) diff --git a/Mac/MainWindow/Timeline/MainTimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift similarity index 95% rename from Mac/MainWindow/Timeline/MainTimelineViewController.swift rename to Mac/MainWindow/Timeline/TimelineViewController.swift index 05b3abd7f..f9648c13e 100644 --- a/Mac/MainWindow/Timeline/MainTimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -13,9 +13,9 @@ import Account import os.log protocol TimelineDelegate: AnyObject { - func timelineSelectionDidChange(_: MainTimelineViewController, selectedArticles: [Article]?) - func timelineRequestedFeedSelection(_: MainTimelineViewController, feed: Feed) - func timelineInvalidatedRestorationState(_: MainTimelineViewController) + func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?) + func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) + func timelineInvalidatedRestorationState(_: TimelineViewController) } enum TimelineShowFeedName { @@ -24,7 +24,7 @@ enum TimelineShowFeedName { case feed } -final class MainTimelineViewController: NSViewController, UndoableCommandRunner, UnreadCountProvider { +final class TimelineViewController: NSViewController, UndoableCommandRunner, UnreadCountProvider { @IBOutlet var tableView: TimelineTableView! @@ -211,7 +211,7 @@ final class MainTimelineViewController: NSViewController, UndoableCommandRunner, if !didRegisterForNotifications { NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) @@ -723,7 +723,7 @@ final class MainTimelineViewController: NSViewController, UndoableCommandRunner, let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date()) let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: Constants.prototypeText, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) - let prototypeCellData = MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, featuredImage: nil) + let prototypeCellData = TimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, featuredImage: nil) let height = TimelineCellLayout.height(for: 100, cellData: prototypeCellData, appearance: cellAppearance) return height } @@ -759,7 +759,7 @@ final class MainTimelineViewController: NSViewController, UndoableCommandRunner, // MARK: - NSMenuDelegate -extension MainTimelineViewController: NSMenuDelegate { +extension TimelineViewController: NSMenuDelegate { public func menuNeedsUpdate(_ menu: NSMenu) { menu.removeAllItems() @@ -772,7 +772,7 @@ extension MainTimelineViewController: NSMenuDelegate { // MARK: - NSUserInterfaceValidations -extension MainTimelineViewController: NSUserInterfaceValidations { +extension TimelineViewController: NSUserInterfaceValidations { func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { if item.action == #selector(openArticleInBrowser(_:)) { @@ -794,7 +794,7 @@ extension MainTimelineViewController: NSUserInterfaceValidations { // MARK: - NSTableViewDataSource -extension MainTimelineViewController: NSTableViewDataSource { +extension TimelineViewController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { return articles.count } @@ -813,15 +813,15 @@ extension MainTimelineViewController: NSTableViewDataSource { // MARK: - NSTableViewDelegate -extension MainTimelineViewController: NSTableViewDelegate { +extension TimelineViewController: NSTableViewDelegate { private static let rowViewIdentifier = NSUserInterfaceItemIdentifier(rawValue: "timelineRow") func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { - if let rowView: TimelineTableRowView = tableView.makeView(withIdentifier: MainTimelineViewController.rowViewIdentifier, owner: nil) as? TimelineTableRowView { + if let rowView: TimelineTableRowView = tableView.makeView(withIdentifier: TimelineViewController.rowViewIdentifier, owner: nil) as? TimelineTableRowView { return rowView } let rowView = TimelineTableRowView() - rowView.identifier = MainTimelineViewController.rowViewIdentifier + rowView.identifier = TimelineViewController.rowViewIdentifier return rowView } @@ -839,13 +839,13 @@ extension MainTimelineViewController: NSTableViewDelegate { } } - if let cell = tableView.makeView(withIdentifier: MainTimelineViewController.timelineCellIdentifier, owner: nil) as? TimelineTableCellView { + if let cell = tableView.makeView(withIdentifier: TimelineViewController.timelineCellIdentifier, owner: nil) as? TimelineTableCellView { configure(cell) return cell } let cell = TimelineTableCellView() - cell.identifier = MainTimelineViewController.timelineCellIdentifier + cell.identifier = TimelineViewController.timelineCellIdentifier configure(cell) return cell } @@ -876,7 +876,7 @@ extension MainTimelineViewController: NSTableViewDelegate { private func configureTimelineCell(_ cell: TimelineTableCellView, article: Article) { cell.objectValue = article let iconImage = article.iconImage() - cell.cellData = MainTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcons, featuredImage: nil) + cell.cellData = TimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcons, featuredImage: nil) } private func iconFor(_ article: Article) -> IconImage? { @@ -887,12 +887,12 @@ extension MainTimelineViewController: NSTableViewDelegate { } private func avatarForAuthor(_ author: Author) -> IconImage? { - return appDelegate.authorAvatarDownloader.image(for: author) + return AuthorAvatarDownloader.shared.image(for: author) } private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) { cell.objectValue = nil - cell.cellData = MainTimelineCellData() + cell.cellData = TimelineCellData() } private func toggleArticleRead(_ article: Article) { @@ -943,7 +943,7 @@ extension MainTimelineViewController: NSTableViewDelegate { // MARK: - Private -private extension MainTimelineViewController { +private extension TimelineViewController { func fetchAndReplacePreservingSelection() { if let article = oneSelectedArticle, let account = article.account { @@ -1187,7 +1187,7 @@ private extension MainTimelineViewController { } func queueFetchAndMergeArticles() { - MainTimelineViewController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) + TimelineViewController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) } func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool { diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 8ac3a42a6..4e922b5d9 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -920,7 +920,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1610; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { 176813F22564BB2C00D98635 = { diff --git a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire MAS.xcscheme b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire MAS.xcscheme index fceb91056..0a6d44163 100644 --- a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire MAS.xcscheme +++ b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/NetNewsWire MAS.xcscheme @@ -1,6 +1,6 @@ URL { + ensureSubfolder(named: name, folderURL: cacheFolder) + } + + public static let dataFolder: URL = { + +#if os(macOS) + var dataFolder = try! FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + dataFolder = dataFolder.appendingPathComponent(appName) +#elseif os(iOS) + var dataFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! +#endif + + createFolderIfNecessary(dataFolder) + return dataFolder + }() + + /// Returns URL to subfolder in data folder (creating the folder if it doesn’t exist) + public static func dataSubfolder(named name: String) -> URL { + ensureSubfolder(named: name, folderURL: dataFolder) + } + + public static func ensureSubfolder(named name: String, folderURL: URL) -> URL { + + let folder = folderURL.appendingPathComponent(name, isDirectory: true) + createFolderIfNecessary(folder) + return folder + } +} + +private extension AppConfig { + + static func createFolderIfNecessary(_ folderURL: URL) { + try! FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) + } +} diff --git a/RSDatabase/.swiftpm/xcode/xcshareddata/xcschemes/RSDatabase.xcscheme b/RSDatabase/.swiftpm/xcode/xcshareddata/xcschemes/RSDatabase.xcscheme index fa933c531..072820b75 100644 --- a/RSDatabase/.swiftpm/xcode/xcshareddata/xcschemes/RSDatabase.xcscheme +++ b/RSDatabase/.swiftpm/xcode/xcshareddata/xcschemes/RSDatabase.xcscheme @@ -1,6 +1,6 @@ [String]? { - favicons.compactMap { favicon in + favicons?.compactMap { favicon in shouldAllowFavicon(favicon) ? favicon.urlString : nil } } static let ignoredTypes = [UTType.svg] - private func shouldAllowFavicon(_ favicon: RSHTMLMetadataFavicon) -> Bool { + private func shouldAllowFavicon(_ favicon: HTMLMetadataFavicon) -> Bool { // Check mime type. if let mimeType = favicon.type, let utType = UTType(mimeType: mimeType) { diff --git a/Shared/Favicons/FaviconURLFinder.swift b/Shared/Favicons/FaviconURLFinder.swift deleted file mode 100644 index a5beaafd1..000000000 --- a/Shared/Favicons/FaviconURLFinder.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// FaviconURLFinder.swift -// NetNewsWire -// -// Created by Brent Simmons on 11/20/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation -import CoreServices -import Parser -import UniformTypeIdentifiers - -// The favicon URLs may be specified in the head section of the home page. - -struct FaviconURLFinder { - - /// Finds favicon URLs in a web page. - /// - Parameters: - /// - homePageURL: The page to search. - /// - completion: A closure called when the links have been found. - /// - urls: An array of favicon URLs as strings. - static func findFaviconURLs(with homePageURL: String, _ completion: @escaping ([String]?) -> Void) { - - guard let _ = URL(string: homePageURL) else { - completion(nil) - return - } - - // If the favicon has an explicit type, check that for an ignored type; otherwise, check the file extension. - HTMLMetadataDownloader.downloadMetadata(for: homePageURL) { (htmlMetadata) in - - guard let favicons = htmlMetadata?.favicons else { - completion(nil) - return - } - - let faviconURLs = favicons.compactMap { - shouldAllowFavicon($0) ? $0.urlString : nil - } - - completion(faviconURLs) - } - } - - private static let ignoredTypes = [UTType.svg] - - private static func shouldAllowFavicon(_ favicon: HTMLMetadataFavicon) -> Bool { - - // Check mime type. - if let mimeType = favicon.type, let utType = UTType(mimeType: mimeType) { - if Self.ignoredTypes.contains(utType) { - return false - } - } - - // Check file extension. - if let urlString = favicon.urlString, let url = URL(string: urlString), let utType = UTType(filenameExtension: url.pathExtension) { - if Self.ignoredTypes.contains(utType) { - return false - } - } - - return true - } -} - diff --git a/Shared/HTMLMetadata/HTMLMetadataDownloader.swift b/Shared/HTMLMetadata/HTMLMetadataDownloader.swift deleted file mode 100644 index b3f5ec2b7..000000000 --- a/Shared/HTMLMetadata/HTMLMetadataDownloader.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// HTMLMetadataDownloader.swift -// NetNewsWire -// -// Created by Brent Simmons on 11/26/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation -import RSWeb -import Parser - -struct HTMLMetadataDownloader { - - static let serialDispatchQueue = DispatchQueue(label: "HTMLMetadataDownloader") - - static func downloadMetadata(for url: String, _ completion: @escaping (HTMLMetadata?) -> Void) { - guard let actualURL = URL(string: url) else { - completion(nil) - return - } - - downloadUsingCache(actualURL) { (data, response, error) in - if let data = data, !data.isEmpty, let response = response, response.statusIsOK, error == nil { - let urlToUse = response.url ?? actualURL - let parserData = ParserData(url: urlToUse.absoluteString, data: data) - parseMetadata(with: parserData, completion) - return - } - - completion(nil) - } - } - - private static func parseMetadata(with parserData: ParserData, _ completion: @escaping (HTMLMetadata?) -> Void) { - serialDispatchQueue.async { - let htmlMetadata = HTMLMetadataParser.metadata(with: parserData) - DispatchQueue.main.async { - completion(htmlMetadata) - } - } - } -} diff --git a/Shared/Images/FeedIconDownloader.swift b/Shared/Images/FeedIconDownloader.swift index 80aa65f7b..bbbb9ecdb 100644 --- a/Shared/Images/FeedIconDownloader.swift +++ b/Shared/Images/FeedIconDownloader.swift @@ -24,8 +24,9 @@ public final class FeedIconDownloader { private let imageDownloader = ImageDownloader.shared private static let saveQueue = CoalescingQueue(name: "Cache Save Queue", interval: 1.0) - - private let imageDownloader: ImageDownloader + private var homePagesWithNoIconURL = Set() + private var cache = [Feed: IconImage]() + private var waitingForFeedURLs = [String: Feed]() private var feedURLToIconURLCache = [String: String]() private var feedURLToIconURLCachePath: URL diff --git a/Widget/Widget Views/SmartFeedSummaryWidget.swift b/Widget/Widget Views/SmartFeedSummaryWidget.swift deleted file mode 100644 index efa4fb756..000000000 --- a/Widget/Widget Views/SmartFeedSummaryWidget.swift +++ /dev/null @@ -1,107 +0,0 @@ -// -// SmartFeedSummaryWidget.swift -// NetNewsWire Widget Extension -// -// Created by Stuart Breckenridge on 18/11/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import WidgetKit -import SwiftUI - - -struct SmartFeedSummaryWidgetView: View { - - @Environment(\.widgetFamily) var family: WidgetFamily - - var entry: Provider.Entry - - var body: some View { - smallWidget - .widgetURL(WidgetDeepLink.icon.url) - } - - @ViewBuilder - var smallWidget: some View { - VStack(alignment: .leading) { - Spacer() - Link(destination: WidgetDeepLink.today.url, label: { - HStack { - todayImage - VStack(alignment: .leading, spacing: nil, content: { - Text(formattedCount(entry.widgetData.currentTodayCount)).font(Font.system(.caption, design: .rounded)).bold() - Text(L10n.today).bold().font(.caption).textCase(.uppercase) - }).foregroundColor(.white) - Spacer() - } - }) - - Link(destination: WidgetDeepLink.unread.url, label: { - HStack { - unreadImage - VStack(alignment: .leading, spacing: nil, content: { - Text(formattedCount(entry.widgetData.currentUnreadCount)).font(Font.system(.caption, design: .rounded)).bold() - Text(L10n.unread).bold().font(.caption).textCase(.uppercase) - }).foregroundColor(.white) - Spacer() - } - }) - - Link(destination: WidgetDeepLink.starred.url, label: { - HStack { - starredImage - VStack(alignment: .leading, spacing: nil, content: { - Text(formattedCount(entry.widgetData.currentStarredCount)).font(Font.system(.caption, design: .rounded)).bold() - Text(L10n.starred).bold().font(.caption).textCase(.uppercase) - }).foregroundColor(.white) - Spacer() - } - }) - Spacer() - }.padding() - } - - func formattedCount(_ count: Int) -> String { - let formatter = NumberFormatter() - formatter.locale = Locale.current - formatter.numberStyle = .decimal - return formatter.string(from: NSNumber(value: count))! - } - - var unreadImage: some View { - Image(systemName: "largecircle.fill.circle") - .resizable() - .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(.white) - } - - var nnwImage: some View { - Image("CornerIcon") - .resizable() - .frame(width: 20, height: 20, alignment: .center) - .cornerRadius(4) - } - - var starredImage: some View { - Image(systemName: "star.fill") - .resizable() - .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(.white) - } - - var todayImage: some View { - Image(systemName: "sun.max.fill") - .resizable() - .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(.white) - } - -} - - -struct SmartFeedSummaryWidgetView_Previews: PreviewProvider { - - static var previews: some View { - SmartFeedSummaryWidgetView(entry: Provider.Entry.init(date: Date(), widgetData: WidgetDataDecoder.sampleData())) - } -} diff --git a/Widget/Widget Views/WidgetLayout.swift b/Widget/Widget Views/WidgetLayout.swift new file mode 100644 index 000000000..38e5b4d23 --- /dev/null +++ b/Widget/Widget Views/WidgetLayout.swift @@ -0,0 +1,19 @@ +// +// WidgetLayout.swift +// NetNewsWire iOS Widget Extension +// +// Created by Brent Simmons on 12/26/24. +// Copyright © 2024 Ranchero Software. All rights reserved. +// + +import Foundation + +struct WidgetLayout { + + static let titleImageSize = CGFloat(20) + static let titleImagePaddingRight = CGFloat(6) + static let leftSideWidth = titleImageSize + titleImagePaddingRight + static let feedIconSize = CGFloat(24) + static let articleItemViewPaddingTop = CGFloat(8) + static let articleItemViewPaddingBottom = CGFloat(4) +} diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 91aa5a6e8..f4740489c 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -68,7 +68,7 @@ class WebViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil) diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index ad2c65b9e..f6b204db6 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -112,9 +112,9 @@ - + - + @@ -431,7 +431,7 @@ - + diff --git a/iOS/Inspector/FeedInspectorViewController.swift b/iOS/Inspector/FeedInspectorViewController.swift index 8ea6c33ca..0c1080bed 100644 --- a/iOS/Inspector/FeedInspectorViewController.swift +++ b/iOS/Inspector/FeedInspectorViewController.swift @@ -48,7 +48,7 @@ class FeedInspectorViewController: UITableViewController { homePageLabel.text = feed.homePageURL feedURLLabel.text = feed.url - NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil) diff --git a/iOS/MainTimeline/MainTimelineViewController.swift b/iOS/MainTimeline/TimelineViewController.swift similarity index 98% rename from iOS/MainTimeline/MainTimelineViewController.swift rename to iOS/MainTimeline/TimelineViewController.swift index a606ee5aa..0c3cfe041 100644 --- a/iOS/MainTimeline/MainTimelineViewController.swift +++ b/iOS/MainTimeline/TimelineViewController.swift @@ -11,7 +11,7 @@ import RSCore import Account import Articles -class MainTimelineViewController: UITableViewController, UndoableCommandRunner { +class TimelineViewController: UITableViewController, UndoableCommandRunner { private var numberOfTextLines = 0 private var iconSize = IconSize.medium @@ -552,7 +552,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner { let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date()) let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: Constants.prototypeText, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) - let prototypeCellData = MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, featuredImage: nil, numberOfLines: numberOfTextLines, iconSize: iconSize) + let prototypeCellData = MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, numberOfLines: numberOfTextLines, iconSize: iconSize) if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory { let layout = MainTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData) @@ -568,7 +568,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner { // MARK: Searching -extension MainTimelineViewController: UISearchControllerDelegate { +extension TimelineViewController: UISearchControllerDelegate { func willPresentSearchController(_ searchController: UISearchController) { coordinator.beginSearching() @@ -582,7 +582,7 @@ extension MainTimelineViewController: UISearchControllerDelegate { } -extension MainTimelineViewController: UISearchResultsUpdating { +extension TimelineViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { let searchScope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex)! @@ -591,7 +591,7 @@ extension MainTimelineViewController: UISearchResultsUpdating { } -extension MainTimelineViewController: UISearchBarDelegate { +extension TimelineViewController: UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { let searchScope = SearchScope(rawValue: selectedScope)! coordinator.searchArticles(searchBar.text!, searchScope) @@ -600,7 +600,7 @@ extension MainTimelineViewController: UISearchBarDelegate { // MARK: Private -private extension MainTimelineViewController { +private extension TimelineViewController { func configureToolbar() { guard !(splitViewController?.isCollapsed ?? true) else { diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index fe31e511c..a0b4e3b65 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -53,7 +53,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { private var rootSplitViewController: RootSplitViewController! private var mainFeedViewController: MainFeedViewController! - private var mainTimelineViewController: MainTimelineViewController? + private var mainTimelineViewController: TimelineViewController? private var articleViewController: ArticleViewController? private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) @@ -284,7 +284,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.mainFeedViewController.coordinator = self self.mainFeedViewController?.navigationController?.delegate = self - self.mainTimelineViewController = rootSplitViewController.viewController(for: .supplementary) as? MainTimelineViewController + self.mainTimelineViewController = rootSplitViewController.viewController(for: .supplementary) as? TimelineViewController self.mainTimelineViewController?.coordinator = self self.mainTimelineViewController?.navigationController?.delegate = self