diff --git a/Modules/Articles/Sources/Articles/Article.swift b/Modules/Articles/Sources/Articles/Article.swift index 23d222afd..8c5848269 100644 --- a/Modules/Articles/Sources/Articles/Article.swift +++ b/Modules/Articles/Sources/Articles/Article.swift @@ -10,8 +10,9 @@ import Foundation public typealias ArticleSetBlock = (Set
) -> Void -public final class Article: Hashable { +public final class Article: Hashable, Identifiable { + public let id: String // Combination of articleID and accountID — unique across all local databases and accounts (not persisted, though) public let articleID: String // Unique database ID (possibly sync service ID) public let accountID: String public let feedID: String // Likely a URL, but not necessarily @@ -44,11 +45,10 @@ public final class Article: Hashable { self.authors = authors self.status = status - if let articleID = articleID { - self.articleID = articleID - } else { - self.articleID = Article.calculatedArticleID(feedID: feedID, uniqueID: uniqueID) - } + let logicalArticleID = articleID ?? Article.calculatedArticleID(feedID: feedID, uniqueID: uniqueID) + self.articleID = logicalArticleID + + self.id = "\(logicalArticleID):\(accountID)" } public static func calculatedArticleID(feedID: String, uniqueID: String) -> String { diff --git a/Modules/RSCore/Sources/RSCore/UIKit/UIColor+RSCore.swift b/Modules/RSCore/Sources/RSCore/UIKit/UIColor+RSCore.swift new file mode 100644 index 000000000..9f1d2bee4 --- /dev/null +++ b/Modules/RSCore/Sources/RSCore/UIKit/UIColor+RSCore.swift @@ -0,0 +1,34 @@ +// +// UIColor+RSCore.swift +// RSCore +// +// Created by Brent Simmons on 2/12/25. +// + +#if os(iOS) + +import Foundation +import UIKit + +extension UIColor { + + public convenience init(hex: String) { + + var s = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + + if (s.hasPrefix("#")) { + s.removeFirst() + } + + var rgb: UInt64 = 0 + Scanner(string: s).scanHexInt64(&rgb) + + let red = CGFloat((rgb >> 16) & 0xFF) / 255.0 + let green = CGFloat((rgb >> 8) & 0xFF) / 255.0 + let blue = CGFloat(rgb & 0xFF) / 255.0 + + self.init(red: red, green: green, blue: blue, alpha: 1.0) + } +} + +#endif diff --git a/Shared/AppColor.swift b/Shared/AppColor.swift index 8c903bff3..da9271c60 100644 --- a/Shared/AppColor.swift +++ b/Shared/AppColor.swift @@ -35,12 +35,15 @@ extension AppColor { extension AppColor { #if os(iOS) + static var barColor = UIColor(hex: "#708090") static var controlBackground = color("controlBackgroundColor") static var fullScreenBackground = color("fullScreenBackgroundColor") static var iconBackground = color("iconBackgroundColor") + static var navigationBarBackground = barColor static var secondaryAccent = color("secondaryAccentColor") static var sectionHeader = color("sectionHeaderColor") static var tickMark = color("tickMarkColor") + static var toolbarBackground = barColor static var vibrantText = color("vibrantTextColor") #endif } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index eabaa174e..182fab7d8 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -78,6 +78,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationC ArticleStatusSyncTimer.shared.update() #endif + configureAppearance() + // Create window and UI mainWindowController = MainWindowController() @@ -256,6 +258,37 @@ private extension AppDelegate { window.overrideUserInterfaceStyle = updatedStyle } } + + // https://developer.apple.com/documentation/technotes/tn3106-customizing-uinavigationbar-appearance + + func configureAppearance() { + let navigationBarAppearance = createNavigationBarAppearance() + let appearance = UINavigationBar.appearance() + appearance.scrollEdgeAppearance = navigationBarAppearance + appearance.compactAppearance = navigationBarAppearance + appearance.standardAppearance = navigationBarAppearance + appearance.compactScrollEdgeAppearance = navigationBarAppearance + } + + func createNavigationBarAppearance() -> UINavigationBarAppearance { + let navigationBarAppearance = UINavigationBarAppearance() + navigationBarAppearance.configureWithOpaqueBackground() + navigationBarAppearance.backgroundColor = AppColor.navigationBarBackground + navigationBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white] + navigationBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white] + + let barButtonItemAppearance = UIBarButtonItemAppearance(style: .plain) + barButtonItemAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.white] + barButtonItemAppearance.disabled.titleTextAttributes = [.foregroundColor: UIColor.lightText] + barButtonItemAppearance.highlighted.titleTextAttributes = [.foregroundColor: UIColor.label] + barButtonItemAppearance.focused.titleTextAttributes = [.foregroundColor: UIColor.white] + + navigationBarAppearance.buttonAppearance = barButtonItemAppearance + navigationBarAppearance.backButtonAppearance = barButtonItemAppearance + navigationBarAppearance.doneButtonAppearance = barButtonItemAppearance + + return navigationBarAppearance + } } // MARK: - BackgroundTaskManagerDelegate diff --git a/iOS/MainWindow/Sidebar/SidebarViewController.swift b/iOS/MainWindow/Sidebar/SidebarViewController.swift index 8974c8a8c..75d05c538 100644 --- a/iOS/MainWindow/Sidebar/SidebarViewController.swift +++ b/iOS/MainWindow/Sidebar/SidebarViewController.swift @@ -61,12 +61,36 @@ final class SidebarViewController: UICollectionViewController { super.viewDidLoad() - collectionView.backgroundColor = .systemRed - title = "Feeds" navigationController?.navigationBar.prefersLargeTitles = true + + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() // Ensures solid background + appearance.backgroundColor = AppColor.navigationBarBackground // Set your desired color + appearance.titleTextAttributes = [.foregroundColor: UIColor.white] // Regular title text + appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white] // Large title text + appearance.shadowColor = .clear + + // Apply the appearance settings + navigationController?.navigationBar.standardAppearance = appearance + navigationController?.navigationBar.scrollEdgeAppearance = appearance + navigationController?.navigationBar.compactAppearance = appearance // Optional + navigationController?.navigationBar.compactScrollEdgeAppearance = appearance + + navigationController?.navigationBar.isTranslucent = false + + if let subviews = navigationController?.navigationBar.subviews { + for subview in subviews { + if subview.frame.height < 2 { + subview.isHidden = true + } + } + } + navigationItem.rightBarButtonItem = filterButton + toolbar.barTintColor = AppColor.toolbarBackground + toolbar.isTranslucent = false toolbar.translatesAutoresizingMaskIntoConstraints = false view.addSubview(toolbar) diff --git a/iOS/MainWindow/Timeline/TimelineCollectionViewController.swift b/iOS/MainWindow/Timeline/TimelineCollectionViewController.swift new file mode 100644 index 000000000..986617f80 --- /dev/null +++ b/iOS/MainWindow/Timeline/TimelineCollectionViewController.swift @@ -0,0 +1,125 @@ +// +// TimelineCollectionViewController.swift +// NetNewsWire-iOS +// +// Created by Brent Simmons on 2/9/25. +// Copyright © 2025 Ranchero Software. All rights reserved. +// + +import Foundation +import UIKit +import Articles + +typealias TimelineSectionID = Int +typealias TimelineArticleID = String + +final class TimelineCell: UICollectionViewCell { + + static let reuseIdentifier = "TimelineCell" + + let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.boldSystemFont(ofSize: 16) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + contentView.addSubview(titleLabel) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - API + + func configure(_ article: Article?, showFeedName: Bool, showIcon: Bool) { + if let article { + titleLabel.text = article.title + } else { + titleLabel.text = "" + } + } +} + +final class TimelineArticlesManager { + + var articles = [Article]() { + didSet { + updateArticleIDsToArticles() + } + } + + subscript(_ articleID: String) -> Article? { + articleIDsToArticles[articleID] + } + + private var articleIDsToArticles = [String: Article]() + + private func updateArticleIDsToArticles() { + var d = [String: Article]() + for article in articles { + d[article.id] = article + } + articleIDsToArticles = d + } +} + +final class TimelineCollectionViewController: UICollectionViewController { + + private let timelineArticlesManager = TimelineArticlesManager() + + typealias DataSource = UICollectionViewDiffableDataSource + private lazy var dataSource = createDataSource() + + init() { + var configuration = UICollectionLayoutListConfiguration(appearance: .plain) + configuration.headerMode = .none + let layout = UICollectionViewCompositionalLayout.list(using: configuration) + + super.init(collectionViewLayout: layout) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Articles" + + collectionView.register(TimelineCell.self, forCellWithReuseIdentifier: TimelineCell.reuseIdentifier) + + timelineArticlesManager.articles = [Article]() + applySnapshot() + } +} + +private extension TimelineCollectionViewController { + + func createDataSource() -> DataSource { + UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, articleID -> UICollectionViewCell? in + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TimelineCell.reuseIdentifier, for: indexPath) as! TimelineCell + let article = self.timelineArticlesManager[articleID] + cell.configure(article, showFeedName: false, showIcon: false) + return cell + } + } + + func applySnapshot() { + + var snapshot = NSDiffableDataSourceSnapshot() + + let oneAndOnlySectionID = 0 + snapshot.appendSections([oneAndOnlySectionID]) + + let articleIDs = timelineArticlesManager.articles.map { $0.id } + snapshot.appendItems(articleIDs, toSection: oneAndOnlySectionID) + + dataSource.apply(snapshot, animatingDifferences: true) + } +}