Get Mac and iOS builds building.

This commit is contained in:
Brent Simmons 2025-01-03 22:58:25 -08:00
parent 83e3324a4a
commit 59af6041ca
39 changed files with 293 additions and 431 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -168,15 +168,6 @@ public final class Feed: SidebarItem, Renamable, Hashable {
}
}
public var sinceToken: String? {
get {
return metadata.sinceToken
}
set {
metadata.sinceToken = newValue
}
}
public var externalID: String? {
get {
return metadata.externalID

View File

@ -216,7 +216,7 @@ private extension LocalAccountRefresher {
if let url = urlCache[urlString] {
return url
}
if let url = URL(unicodeString: urlString) {
if let url = URL(string: urlString) {
urlCache[urlString] = url
return url
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -779,7 +779,8 @@ internal extension AppDelegate {
guard let window = mainWindowController?.window else { return }
do {
let theme = try ArticleTheme(path: filename, isAppTheme: false)
let themeURL = URL(filePath: filename)
let theme = try ArticleTheme(url: themeURL, isAppTheme: false)
let alert = NSAlert()
alert.alertStyle = .informational

View File

@ -75,8 +75,8 @@ protocol SidebarDelegate: AnyObject {
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .FeedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .feedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
DistributedNotificationCenter.default().addObserver(self, selector: #selector(appleSideBarDefaultIconSizeChanged(_:)), name: .appleSideBarDefaultIconSizeChanged, object: nil)
@ -153,7 +153,7 @@ protocol SidebarDelegate: AnyObject {
return
}
if let timelineViewController = representedObject as? MainTimelineViewController {
if let timelineViewController = representedObject as? TimelineViewController {
configureUnreadCountForCellsForRepresentedObjects(timelineViewController.representedObjects)
} else {
configureUnreadCountForCellsForRepresentedObjects([representedObject as AnyObject])

View File

@ -1,79 +0,0 @@
//
// TimelineCellData.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import AppKit
import Articles
struct MainTimelineCellData {
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
let title: String
let attributedTitle: NSAttributedString
let text: String
let dateString: String
let feedName: String
let byline: String
let showFeedName: TimelineShowFeedName
let iconImage: IconImage? // feed icon, user avatar, or favicon
let showIcon: Bool // Make space even when icon is nil
let featuredImage: NSImage? // image from within the article
let read: Bool
let starred: Bool
init(article: Article, showFeedName: TimelineShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
self.title = ArticleStringFormatter.truncatedTitle(article)
self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article)
let truncatedSummary = ArticleStringFormatter.truncatedSummary(article)
if self.title.isEmpty && truncatedSummary.isEmpty {
self.text = Self.noText
} else {
self.text = truncatedSummary
}
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
if let feedName = feedName {
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
} else {
self.feedName = ""
}
if let byline = byline {
self.byline = byline
} else {
self.byline = ""
}
self.showFeedName = showFeedName
self.showIcon = showIcon
self.iconImage = iconImage
self.featuredImage = featuredImage
self.read = article.status.read
self.starred = article.status.starred
}
init() { //Empty
self.title = ""
self.text = ""
self.dateString = ""
self.feedName = ""
self.byline = ""
self.showFeedName = .none
self.showIcon = false
self.iconImage = nil
self.featuredImage = nil
self.read = true
self.starred = false
self.attributedTitle = NSAttributedString()
}
}

View File

@ -1,45 +0,0 @@
//
// UnreadIndicatorView.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/16/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import AppKit
class MainUnreadIndicatorView: 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()
}
MainUnreadIndicatorView.bezierPath.fill()
}
}

View File

@ -0,0 +1,79 @@
//
// TimelineCellData.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import AppKit
import Articles
struct TimelineCellData {
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
let title: String
let attributedTitle: NSAttributedString
let text: String
let dateString: String
let feedName: String
let byline: String
let showFeedName: TimelineShowFeedName
let iconImage: IconImage? // feed icon, user avatar, or favicon
let showIcon: Bool // Make space even when icon is nil
let featuredImage: NSImage? // image from within the article
let read: Bool
let starred: Bool
init(article: Article, showFeedName: TimelineShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, featuredImage: NSImage?) {
self.title = ArticleStringFormatter.truncatedTitle(article)
self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article)
let truncatedSummary = ArticleStringFormatter.truncatedSummary(article)
if self.title.isEmpty && truncatedSummary.isEmpty {
self.text = Self.noText
} else {
self.text = truncatedSummary
}
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
if let feedName = feedName {
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
} else {
self.feedName = ""
}
if let byline = byline {
self.byline = byline
} else {
self.byline = ""
}
self.showFeedName = showFeedName
self.showIcon = showIcon
self.iconImage = iconImage
self.featuredImage = featuredImage
self.read = article.status.read
self.starred = article.status.starred
}
init() { //Empty
self.title = ""
self.text = ""
self.dateString = ""
self.feedName = ""
self.byline = ""
self.showFeedName = .none
self.showIcon = false
self.iconImage = nil
self.featuredImage = nil
self.read = true
self.starred = false
self.attributedTitle = NSAttributedString()
}
}

View File

@ -48,7 +48,7 @@ struct TimelineCellLayout {
}
}
init(width: CGFloat, height: CGFloat, cellData: MainTimelineCellData, appearance: TimelineCellAppearance, hasIcon: Bool) {
init(width: CGFloat, height: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance, hasIcon: Bool) {
// If height == 0.0, then height is calculated.
@ -82,7 +82,7 @@ struct TimelineCellLayout {
self.init(width: width, height: height, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, numberOfLinesForTitle: numberOfLinesForTitle, summaryRect: summaryRect, textRect: textRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, iconImageRect: iconImageRect, separatorRect: separatorRect, paddingBottom: paddingBottom)
}
static func height(for width: CGFloat, cellData: MainTimelineCellData, appearance: TimelineCellAppearance) -> 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)
}
}

View File

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

View File

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

View File

@ -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<KeyboardShortcut>
override init() {

View File

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

View File

@ -5,7 +5,7 @@
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="MainTimelineViewController" customModule="NetNewsWire" customModuleProvider="target">
<customObject id="-2" userLabel="File's Owner" customClass="TimelineViewController" customModule="NetNewsWire" customModuleProvider="target">
<connections>
<outlet property="tableView" destination="opA-RM-DKR" id="Hnf-mE-gcq"/>
<outlet property="view" destination="dbt-sN-FU2" id="96u-gC-hW0"/>

View File

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

View File

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

View File

@ -920,7 +920,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1610;
LastUpgradeCheck = 1620;
ORGANIZATIONNAME = "Ranchero Software";
TargetAttributes = {
176813F22564BB2C00D98635 = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.8">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -0,0 +1,67 @@
//
// AppConfig.swift
// NetNewsWire
//
// Created by Brent Simmons on 6/26/24.
// Copyright © 2024 Ranchero Software. All rights reserved.
//
import Foundation
public final class AppConfig {
public static let appName: String = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String)
public static let cacheFolder: URL = {
let folderURL: URL
if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) {
folderURL = userCacheFolder
} else {
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
let tempFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier)
folderURL = URL(fileURLWithPath: tempFolder, isDirectory: true)
createFolderIfNecessary(folderURL)
}
return folderURL
}()
/// Returns URL to subfolder in cache folder (creating the folder if it doesnt exist)
public static func cacheSubfolder(named name: String) -> 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 doesnt 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)
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1610"
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -12,7 +12,7 @@ import Articles
import Account
import RSCore
import RSWeb
import RSParser
import Parser
import UniformTypeIdentifiers
extension Notification.Name {
@ -305,18 +305,18 @@ private extension FaviconDownloader {
}
}
private extension RSHTMLMetadata {
private extension HTMLMetadata {
func usableFaviconURLs() -> [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) {

View File

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

View File

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

View File

@ -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<String>()
private var cache = [Feed: IconImage]()
private var waitingForFeedURLs = [String: Feed]()
private var feedURLToIconURLCache = [String: String]()
private var feedURLToIconURLCachePath: URL

View File

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

View File

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

View File

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

View File

@ -112,9 +112,9 @@
<!--Timeline-->
<scene sceneID="fag-XH-avP">
<objects>
<tableViewController storyboardIdentifier="MainTimelineViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" clearsSelectionOnViewWillAppear="NO" id="Kyk-vK-QRX" customClass="MainTimelineViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableViewController storyboardIdentifier="MainTimelineViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" clearsSelectionOnViewWillAppear="NO" id="Kyk-vK-QRX" customClass="TimelineViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="mtv-Ik-FoJ">
<rect key="frame" x="0.0" y="0.0" width="414" height="721"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<prototypes>
@ -431,7 +431,7 @@
<color red="0.031372549019607843" green="0.41568627450980394" blue="0.93333333333333335" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="separatorColor">
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>

View File

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

View File

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

View File

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