mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Make progress on iOS SidebarViewController.
This commit is contained in:
@@ -193,9 +193,11 @@ public final class Feed: SidebarItem, Renamable, Hashable {
|
||||
if let s = name, !s.isEmpty {
|
||||
return s
|
||||
}
|
||||
return NSLocalizedString("Untitled", comment: "Feed name")
|
||||
return Self.untitledName
|
||||
}
|
||||
|
||||
public static let untitledName = NSLocalizedString("Untitled", comment: "Feed name")
|
||||
|
||||
// MARK: - Renamable
|
||||
|
||||
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
@@ -42,7 +42,7 @@ public final class Folder: SidebarItem, Renamable, Container, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
static let untitledName = NSLocalizedString("Untitled ƒ", comment: "Folder name")
|
||||
public static let untitledName = NSLocalizedString("Untitled ƒ", comment: "Folder name")
|
||||
public let folderID: Int // not saved: per-run only
|
||||
public var externalID: String?
|
||||
static var incrementingID = 0
|
||||
|
||||
@@ -47,6 +47,7 @@ struct AppImage {
|
||||
static var faviconTemplate = appImage("faviconTemplateImage")
|
||||
static var filterActive = systemImage("line.horizontal.3.decrease.circle.fill")
|
||||
static var filterInactive = systemImage("line.horizontal.3.decrease.circle")
|
||||
static var folder = systemImage("folder")
|
||||
static var markAllAsRead = appImage("markAllAsRead")
|
||||
static let nnwFeedIcon = RSImage(named: "nnwFeedIcon")!
|
||||
static var share = systemImage("square.and.arrow.up")
|
||||
@@ -122,7 +123,7 @@ extension AppImage {
|
||||
return IconImage(coloredImage, isSymbol: true, isBackgroundSuppressed: true, preferredColor: preferredColor.cgColor)
|
||||
}()
|
||||
|
||||
static var folder: IconImage = {
|
||||
static var folderIconImage: IconImage = {
|
||||
let image = systemImage("folder")
|
||||
let preferredColor = AppColor.accent
|
||||
let coloredImage = image.tinted(with: preferredColor)
|
||||
@@ -174,10 +175,7 @@ extension AppImage {
|
||||
static var todayFeed = IconImage(today, isSymbol: true, isBackgroundSuppressed: true, preferredColor: UIColor.systemOrange.cgColor)
|
||||
static var unreadFeed = IconImage(allUnread, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppColor.secondaryAccent.cgColor)
|
||||
|
||||
static var folder: IconImage = {
|
||||
let image = systemImage("folder.fill")
|
||||
return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppColor.secondaryAccent.cgColor)
|
||||
}()
|
||||
static var folderIconImage = IconImage(AppImage.folder, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppColor.secondaryAccent.cgColor)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,6 @@ extension Feed: SmallIconProvider {
|
||||
|
||||
extension Folder: SmallIconProvider {
|
||||
var smallIcon: IconImage? {
|
||||
AppImage.folder
|
||||
AppImage.folderIconImage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import Articles
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider, PasteboardWriterOwner {
|
||||
protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider, PasteboardWriterOwner, DisplayNameProvider {
|
||||
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import Articles
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider {
|
||||
protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider, DisplayNameProvider {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,41 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Account
|
||||
|
||||
typealias SectionID = Int
|
||||
typealias ItemID = Int
|
||||
|
||||
protocol Section: Identifiable {}
|
||||
protocol Item: Identifiable {}
|
||||
// MARK: - Protocols
|
||||
|
||||
@MainActor protocol SidebarContainer: Identifiable {
|
||||
/// A top-level collapsible item.
|
||||
///
|
||||
/// The Smart Feeds group and active `Account`s are `Section`s.
|
||||
@MainActor protocol Section: SidebarContainer, Identifiable where ID == SectionID {
|
||||
var title: String { get }
|
||||
|
||||
/// All `Item`s in the `Section`, even if contained by a `SidebarFolder`
|
||||
/// in that section.
|
||||
func flattenedItems() -> [any Item]
|
||||
}
|
||||
|
||||
/// `Item`s are contained by `Sections`. They never appear at the top level.
|
||||
/// They are hidden when a `Section` is collapsed.
|
||||
///
|
||||
/// Items are smart feeds, folders, and feeds.
|
||||
@MainActor protocol Item: Identifiable where ID == ItemID {
|
||||
var title: String { get }
|
||||
var image: UIImage? { get }
|
||||
}
|
||||
|
||||
/// A `SidebarContainer` contains `Item`s and is collapsible.
|
||||
///
|
||||
/// All `Section`s are `SidebarContainer`s.
|
||||
///
|
||||
/// `SidebarFolder` is also a `SidebarContainer`,
|
||||
/// though it’s not a `Section`, because it contains `Item`s and is collapsible.
|
||||
@MainActor protocol SidebarContainer: Identifiable where ID == SectionID {
|
||||
var isExpanded: Bool { get set }
|
||||
var items: [any Item] { get }
|
||||
|
||||
@@ -25,6 +50,13 @@ protocol Item: Identifiable {}
|
||||
|
||||
extension SidebarContainer {
|
||||
|
||||
// These dictionaries make it fast to look up, given a Feed or Folder,
|
||||
// the existing SidebarFeed and SidebarFolder.
|
||||
// This way we can preserve identity across rebuilds of the tree.
|
||||
// (Meaning: after rebuilding the tree, a given Feed in a given Account
|
||||
// or Folder should have the same SidebarFeed object with the same id.
|
||||
// Same with Folders and SidebarFolder.)
|
||||
|
||||
func createFeedsDictionary() -> [String: SidebarFeed] {
|
||||
var d = [String: SidebarFeed]() // feedID: SidebarFeed
|
||||
for item in items where item is SidebarFeed {
|
||||
@@ -49,8 +81,13 @@ extension SidebarContainer {
|
||||
@MainActor final class SidebarSmartFeedsFolder: Section, SidebarContainer {
|
||||
|
||||
let id = createID()
|
||||
var isExpanded = true
|
||||
|
||||
var title: String {
|
||||
SmartFeedsController.shared.nameForDisplay
|
||||
}
|
||||
|
||||
var isExpanded = true
|
||||
|
||||
let items: [any Item] = [
|
||||
SidebarSmartFeed(SmartFeedsController.shared.todayFeed),
|
||||
SidebarSmartFeed(SmartFeedsController.shared.unreadFeed),
|
||||
@@ -59,8 +96,11 @@ extension SidebarContainer {
|
||||
|
||||
func updateItems() {
|
||||
}
|
||||
}
|
||||
|
||||
func flattenedItems() -> [any Item] {
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SidebarSmartFeed
|
||||
|
||||
@@ -69,6 +109,14 @@ extension SidebarContainer {
|
||||
let id = createID()
|
||||
let smartFeed: any PseudoFeed
|
||||
|
||||
var title: String {
|
||||
smartFeed.nameForDisplay
|
||||
}
|
||||
|
||||
var image: UIImage? {
|
||||
smartFeed.smallIcon?.image
|
||||
}
|
||||
|
||||
init(_ smartFeed: any PseudoFeed) {
|
||||
self.smartFeed = smartFeed
|
||||
}
|
||||
@@ -79,6 +127,10 @@ extension SidebarContainer {
|
||||
@MainActor final class SidebarAccount: Section, SidebarContainer {
|
||||
|
||||
let id = createID()
|
||||
var title: String {
|
||||
account?.nameForDisplay ?? "Untitled Account"
|
||||
}
|
||||
|
||||
let accountID: String
|
||||
weak var account: Account?
|
||||
var isExpanded = true
|
||||
@@ -133,6 +185,20 @@ extension SidebarContainer {
|
||||
// TODO: sort feeds
|
||||
items = sidebarFeeds + sidebarFolders
|
||||
}
|
||||
|
||||
func flattenedItems() -> [any Item] {
|
||||
|
||||
var temp = items
|
||||
|
||||
for item in items {
|
||||
temp.append(item)
|
||||
if let container = item as? any SidebarContainer {
|
||||
temp.append(contentsOf: container.items)
|
||||
}
|
||||
}
|
||||
|
||||
return temp
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SidebarFolder
|
||||
@@ -140,6 +206,15 @@ extension SidebarContainer {
|
||||
@MainActor final class SidebarFolder: Item, SidebarContainer {
|
||||
|
||||
let id = createID()
|
||||
|
||||
var title: String {
|
||||
folder?.nameForDisplay ?? Folder.untitledName
|
||||
}
|
||||
|
||||
var image: UIImage? {
|
||||
AppImage.folder
|
||||
}
|
||||
|
||||
let folderID: Int
|
||||
weak var folder: Folder?
|
||||
var isExpanded = false
|
||||
@@ -186,6 +261,18 @@ extension SidebarContainer {
|
||||
let feedID: String
|
||||
weak var feed: Feed?
|
||||
|
||||
var title: String {
|
||||
feed?.nameForDisplay ?? Feed.untitledName
|
||||
}
|
||||
|
||||
var image: UIImage? {
|
||||
if let feed {
|
||||
return IconImageCache.shared.imageForFeed(feed)?.image
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
init(_ feed: Feed) {
|
||||
self.feedID = feed.feedID
|
||||
self.feed = feed
|
||||
@@ -197,17 +284,28 @@ extension SidebarContainer {
|
||||
@MainActor final class SidebarTree {
|
||||
|
||||
var sections = [any Section]()
|
||||
private var idsToItems = [ItemID: any Item]()
|
||||
private var idsToSections = [SectionID: any Section]()
|
||||
|
||||
private lazy var sidebarSmartFeedsFolder = SidebarSmartFeedsFolder()
|
||||
|
||||
func rebuildTree() {
|
||||
func section(with id: SectionID) -> (any Section)? {
|
||||
idsToSections[id]
|
||||
}
|
||||
|
||||
func item(with id: ItemID) -> (any Item)? {
|
||||
idsToItems[id]
|
||||
}
|
||||
|
||||
func rebuild() {
|
||||
|
||||
// In my testing, with two accounts and hundreds of feeds,
|
||||
// this function takes 2-3 milliseconds.
|
||||
|
||||
rebuildSections()
|
||||
|
||||
for section in sections where section is SidebarAccount {
|
||||
let sidebarAccount = section as! SidebarAccount
|
||||
sidebarAccount.updateItems()
|
||||
}
|
||||
updateAllItems()
|
||||
rebuildIDsToItems()
|
||||
rebuildIDsToSections()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +314,6 @@ private extension SidebarTree {
|
||||
func rebuildSections() {
|
||||
|
||||
var updatedSections = [any Section]()
|
||||
|
||||
updatedSections.append(sidebarSmartFeedsFolder)
|
||||
|
||||
for account in AccountManager.shared.activeAccounts {
|
||||
@@ -232,6 +329,38 @@ private extension SidebarTree {
|
||||
sections = updatedSections
|
||||
}
|
||||
|
||||
func updateAllItems() {
|
||||
|
||||
for section in sections where section is SidebarAccount {
|
||||
let sidebarAccount = section as! SidebarAccount
|
||||
sidebarAccount.updateItems()
|
||||
}
|
||||
}
|
||||
|
||||
func rebuildIDsToItems() {
|
||||
|
||||
var d = [ItemID: any Item]()
|
||||
|
||||
for section in sections {
|
||||
for item in section.flattenedItems() {
|
||||
d[item.id] = item
|
||||
}
|
||||
}
|
||||
|
||||
idsToItems = d
|
||||
}
|
||||
|
||||
func rebuildIDsToSections() {
|
||||
|
||||
var d = [SectionID: any Section]()
|
||||
|
||||
for section in sections {
|
||||
d[section.id] = section
|
||||
}
|
||||
|
||||
idsToSections = d
|
||||
}
|
||||
|
||||
func existingSection(for account: Account) -> (any Section)? {
|
||||
|
||||
// Linear search through array because it’s just a few items.
|
||||
|
||||
@@ -1,213 +1,231 @@
|
||||
////
|
||||
//// SidebarViewController.swift
|
||||
//// NetNewsWire-iOS
|
||||
////
|
||||
//// Created by Brent Simmons on 2/2/25.
|
||||
//// Copyright © 2025 Ranchero Software. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import UIKit
|
||||
//import Account
|
||||
// SidebarViewController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
//final class SidebarViewController: UICollectionViewController {
|
||||
// Created by Brent Simmons on 2/2/25.
|
||||
// Copyright © 2025 Ranchero Software. All rights reserved.
|
||||
//
|
||||
// typealias DataSource = UICollectionViewDiffableDataSource<SectionID, SidebarItemID>
|
||||
// private lazy var dataSource = createDataSource()
|
||||
//
|
||||
// private lazy var filterButton = UIBarButtonItem(
|
||||
// image: AppImage.filterInactive,
|
||||
// style: .plain,
|
||||
// target: self,
|
||||
// action: #selector(toggleFilter(_:))
|
||||
// )
|
||||
//
|
||||
// private lazy var settingsButton = UIBarButtonItem(
|
||||
// image: AppImage.settings,
|
||||
// style: .plain,
|
||||
// target: self,
|
||||
// action: #selector(showSettings(_:))
|
||||
// )
|
||||
//
|
||||
// private lazy var addNewItemButton = UIBarButtonItem(systemItem: .add)
|
||||
//
|
||||
// private lazy var refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
||||
// private lazy var refreshProgressView: RefreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as! RefreshProgressView
|
||||
//
|
||||
// private lazy var flexibleSpaceBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
//
|
||||
// private lazy var toolbar = UIToolbar()
|
||||
//
|
||||
// init() {
|
||||
// var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
|
||||
// configuration.headerMode = .supplementary
|
||||
// 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()
|
||||
//
|
||||
// collectionView.contentInset = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0)
|
||||
//
|
||||
// title = "Feeds"
|
||||
// navigationController?.navigationBar.prefersLargeTitles = true
|
||||
// navigationItem.rightBarButtonItem = filterButton
|
||||
//
|
||||
// toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.addSubview(toolbar)
|
||||
//
|
||||
// NSLayoutConstraint.activate([
|
||||
// toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
// toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
// toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||
// toolbar.heightAnchor.constraint(equalToConstant: 44)
|
||||
// ])
|
||||
//
|
||||
// let toolbarItems = [
|
||||
// settingsButton,
|
||||
// flexibleSpaceBarButtonItem,
|
||||
// refreshProgressItemButton,
|
||||
// flexibleSpaceBarButtonItem,
|
||||
// addNewItemButton
|
||||
// ]
|
||||
// toolbar.setItems(toolbarItems, animated: false)
|
||||
//
|
||||
// collectionView.register(SidebarHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SidebarHeaderView.reuseIdentifier)
|
||||
//
|
||||
// applySnapshot()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - Actions
|
||||
//
|
||||
//// TODO: Implement actions
|
||||
//
|
||||
//extension SidebarViewController {
|
||||
//
|
||||
// @objc func toggleFilter(_ sender: Any) {
|
||||
// }
|
||||
//
|
||||
// @objc func showSettings(_ sender: Any?) {
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//// MARK: - SidebarHeaderViewDelegate
|
||||
//
|
||||
//extension SidebarViewController: SidebarHeaderViewDelegate {
|
||||
//
|
||||
// func sidebarHeaderViewUserDidToggleExpanded(_ sidebarHeaderView: SidebarHeaderView) {
|
||||
// var section = sections[sidebarHeaderView.sectionIndex]
|
||||
// section.isExpanded.toggle()
|
||||
// applySnapshot()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//private extension SidebarViewController {
|
||||
//
|
||||
// func createDataSource() -> DataSource {
|
||||
// let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SidebarItem> { (cell, _, item) in
|
||||
// var content = UIListContentConfiguration.cell()
|
||||
// content.text = item.title
|
||||
// content.image = item.icon
|
||||
// cell.contentConfiguration = content
|
||||
// }
|
||||
//
|
||||
// dataSource = UICollectionViewDiffableDataSource<Section, SidebarItem>(collectionView: collectionView) { (collectionView, indexPath, item) in
|
||||
// return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
|
||||
// }
|
||||
//
|
||||
// let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { header, _, indexPath in
|
||||
//
|
||||
// let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
|
||||
//
|
||||
// var content = UIListContentConfiguration.sidebarHeader()
|
||||
// content.text = sectionTitle(for: section)
|
||||
// header.contentConfiguration = content
|
||||
//
|
||||
// // Add a disclosure indicator to show collapsible behavior
|
||||
// let button = UIButton(type: .system)
|
||||
// button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
|
||||
// button.tintColor = .secondaryLabel
|
||||
// button.addTarget(self, action: #selector(self.toggleSection(_:)), for: .touchUpInside)
|
||||
// button.tag = indexPath.section
|
||||
// header.accessories = [.customView(configuration: .init(customView: button, placement: .trailing(displayed: .always)))]
|
||||
//
|
||||
// // Rotate chevron based on section state
|
||||
// button.transform = sectionStates[section] == true ? .identity : CGAffineTransform(rotationAngle: -.pi / 2)
|
||||
// }
|
||||
//
|
||||
// dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
|
||||
// let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SidebarHeaderView.reuseIdentifier, for: indexPath) as! SidebarHeaderView
|
||||
// let section = self.sections[indexPath.section]
|
||||
// header.configure(title: section.title, sectionIndex: indexPath.section, delegate: self)
|
||||
// return header
|
||||
// }
|
||||
//
|
||||
// return dataSource
|
||||
// }
|
||||
//
|
||||
// func applySnapshot() {
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<Section, SidebarItem>()
|
||||
//
|
||||
// for section in sections {
|
||||
// snapshot.appendSections([section])
|
||||
// if section.isExpanded {
|
||||
// snapshot.appendItems(section.items, toSection: section)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// dataSource.apply(snapshot, animatingDifferences: true)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//protocol SidebarHeaderViewDelegate: AnyObject {
|
||||
//
|
||||
// func sidebarHeaderViewUserDidToggleExpanded(_: SidebarHeaderView)
|
||||
//}
|
||||
//
|
||||
//final class SidebarHeaderView: UICollectionReusableView {
|
||||
//
|
||||
// static let reuseIdentifier = "SidebarHeaderView"
|
||||
//
|
||||
// var sectionIndex: Int = 0
|
||||
// var delegate: SidebarHeaderViewDelegate?
|
||||
//
|
||||
// override init(frame: CGRect) {
|
||||
// super.init(frame: frame)
|
||||
// backgroundColor = .darkGray
|
||||
//
|
||||
// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleExpanded(_:)))
|
||||
// addGestureRecognizer(tapGesture)
|
||||
// }
|
||||
//
|
||||
// required init?(coder: NSCoder) {
|
||||
// fatalError("init(coder:) has not been implemented")
|
||||
// }
|
||||
//
|
||||
// func configure(title: String, sectionIndex: Int, delegate: SidebarHeaderViewDelegate) {
|
||||
// self.sectionIndex = sectionIndex
|
||||
// self.delegate = delegate
|
||||
//
|
||||
// let label = UILabel(frame: bounds)
|
||||
// label.text = title
|
||||
// label.textColor = .white
|
||||
// label.textAlignment = .center
|
||||
// addSubview(label)
|
||||
// }
|
||||
//
|
||||
// @objc func toggleExpanded(_ sender: Any?) {
|
||||
// delegate?.sidebarHeaderViewUserDidToggleExpanded(self)
|
||||
//// guard let controller else {
|
||||
//// return
|
||||
//// }
|
||||
//// controller.sections[sectionIndex].state.toggle()
|
||||
//// controller.applySnapshot()
|
||||
// }
|
||||
//}
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Account
|
||||
|
||||
final class SidebarViewController: UICollectionViewController {
|
||||
|
||||
private let tree = SidebarTree()
|
||||
|
||||
typealias DataSource = UICollectionViewDiffableDataSource<SectionID, ItemID>
|
||||
private lazy var dataSource = createDataSource()
|
||||
|
||||
private lazy var filterButton = UIBarButtonItem(
|
||||
image: AppImage.filterInactive,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(toggleFilter(_:))
|
||||
)
|
||||
|
||||
private lazy var settingsButton = UIBarButtonItem(
|
||||
image: AppImage.settings,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(showSettings(_:))
|
||||
)
|
||||
|
||||
private lazy var addNewItemButton = UIBarButtonItem(systemItem: .add)
|
||||
|
||||
private lazy var refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
|
||||
private lazy var refreshProgressView: RefreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as! RefreshProgressView
|
||||
|
||||
private lazy var flexibleSpaceBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
|
||||
private lazy var toolbar = UIToolbar()
|
||||
|
||||
init() {
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
|
||||
configuration.headerMode = .supplementary
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
|
||||
super.init(collectionViewLayout: layout)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
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(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
collectionView.backgroundColor = .systemRed
|
||||
|
||||
title = "Feeds"
|
||||
navigationController?.navigationBar.prefersLargeTitles = true
|
||||
navigationItem.rightBarButtonItem = filterButton
|
||||
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
view.addSubview(toolbar)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
||||
])
|
||||
|
||||
let toolbarItems = [
|
||||
settingsButton,
|
||||
flexibleSpaceBarButtonItem,
|
||||
refreshProgressItemButton,
|
||||
flexibleSpaceBarButtonItem,
|
||||
addNewItemButton
|
||||
]
|
||||
toolbar.setItems(toolbarItems, animated: false)
|
||||
|
||||
tree.rebuild()
|
||||
applySnapshot()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// Make collection view aware of toolbar — so that bottom isn’t trapped under the toolbar.
|
||||
let toolbarHeight = toolbar.frame.height
|
||||
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: toolbarHeight, right: 0)
|
||||
collectionView.verticalScrollIndicatorInsets.bottom = toolbarHeight
|
||||
}
|
||||
|
||||
func updateVisibleRows() {
|
||||
|
||||
var itemIDsToReload = [ItemID]()
|
||||
|
||||
for indexPath in collectionView.indexPathsForVisibleItems {
|
||||
if let itemID = dataSource.itemIdentifier(for: indexPath) {
|
||||
itemIDsToReload.append(itemID)
|
||||
}
|
||||
}
|
||||
|
||||
if !itemIDsToReload.isEmpty {
|
||||
var snapshot = dataSource.snapshot()
|
||||
snapshot.reloadItems(itemIDsToReload)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc func faviconDidBecomeAvailable(_ note: Notification) {
|
||||
updateVisibleRows()
|
||||
}
|
||||
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
updateVisibleRows()
|
||||
}
|
||||
|
||||
@objc func feedSettingDidChange(_ note: Notification) {
|
||||
updateVisibleRows()
|
||||
}
|
||||
|
||||
@objc func displayNameDidChange(_ note: Notification) {
|
||||
updateVisibleRows()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
// TODO: Implement actions
|
||||
|
||||
extension SidebarViewController {
|
||||
|
||||
@objc func toggleFilter(_ sender: Any) {
|
||||
}
|
||||
|
||||
@objc func showSettings(_ sender: Any?) {
|
||||
}
|
||||
}
|
||||
|
||||
private extension SidebarViewController {
|
||||
|
||||
static let imageSize = CGSize(width: 24, height: 24)
|
||||
|
||||
func createDataSource() -> DataSource {
|
||||
|
||||
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ItemID> { cell, _, itemID in
|
||||
|
||||
guard let item = self.tree.item(with: itemID) else {
|
||||
preconditionFailure("Expected item \(itemID) to exist in sidebar tree.")
|
||||
}
|
||||
|
||||
var config = UIListContentConfiguration.cell()
|
||||
config.text = item.title
|
||||
|
||||
// TODO: different configuration for SF Symbols?
|
||||
//config.imageProperties.preferredSymbolConfiguration = UIImage.SymbolConfiguration(pointSize: 18, weight: .regular)
|
||||
|
||||
config.image = item.image
|
||||
config.imageProperties.cornerRadius = 6
|
||||
config.imageProperties.reservedLayoutSize = Self.imageSize
|
||||
config.imageProperties.maximumSize = Self.imageSize
|
||||
// config.imageProperties.tintColor = .secondaryLabel
|
||||
|
||||
cell.contentConfiguration = config
|
||||
}
|
||||
|
||||
let dataSource = DataSource(collectionView: collectionView) { collectionView, indexPath, itemID in
|
||||
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemID)
|
||||
}
|
||||
|
||||
let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { header, _, indexPath in
|
||||
|
||||
let sectionID = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
|
||||
guard let section = self.tree.section(with: sectionID) else {
|
||||
preconditionFailure("Expected sectionID and section for indexPath \(indexPath) to exist in sidebar tree.")
|
||||
}
|
||||
|
||||
var content = UIListContentConfiguration.header()
|
||||
content.text = section.title
|
||||
header.contentConfiguration = content
|
||||
|
||||
// Add a disclosure indicator to show collapsible behavior
|
||||
let button = UIButton(type: .system)
|
||||
button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
|
||||
button.tintColor = .secondaryLabel
|
||||
// button.addTarget(self, action: #selector(self.toggleSection(_:)), for: .touchUpInside)
|
||||
button.tag = indexPath.section
|
||||
header.accessories = [.customView(configuration: .init(customView: button, placement: .trailing(displayed: .always)))]
|
||||
|
||||
// Rotate chevron based on section state
|
||||
button.transform = section.isExpanded ? .identity : CGAffineTransform(rotationAngle: -.pi / 2)
|
||||
}
|
||||
|
||||
dataSource.supplementaryViewProvider = { collectionView, _, indexPath in
|
||||
collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func applySnapshot() {
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<SectionID, ItemID>()
|
||||
|
||||
for section in tree.sections {
|
||||
let sectionID = section.id
|
||||
snapshot.appendSections([sectionID])
|
||||
|
||||
if section.isExpanded {
|
||||
let itemIDs = section.items.map { $0.id }
|
||||
snapshot.appendItems(itemIDs, toSection: sectionID)
|
||||
|
||||
// TODO: handle folders
|
||||
}
|
||||
}
|
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ final class ShareFolderPickerController: UITableViewController {
|
||||
if let account = container as? ExtensionAccount {
|
||||
cell.icon.image = AppImage.account(account.type)
|
||||
} else {
|
||||
cell.icon.image = AppImage.folder.image
|
||||
cell.icon.image = AppImage.folder
|
||||
}
|
||||
|
||||
cell.label?.text = container?.name ?? ""
|
||||
|
||||
Reference in New Issue
Block a user