Files
NetNewsWire/iOS/MainWindow/Sidebar/SidebarTree.swift

260 lines
5.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// SidebarTree.swift
// NetNewsWire-iOS
//
// Created by Brent Simmons on 2/7/25.
// Copyright © 2025 Ranchero Software. All rights reserved.
//
import Foundation
import Account
typealias SectionID = Int
typealias ItemID = Int
protocol Section: Identifiable {}
protocol Item: Identifiable {}
@MainActor protocol SidebarContainer: Identifiable {
var isExpanded: Bool { get set }
var items: [any Item] { get }
func updateItems()
}
extension SidebarContainer {
func createFeedsDictionary() -> [String: SidebarFeed] {
var d = [String: SidebarFeed]() // feedID: SidebarFeed
for item in items where item is SidebarFeed {
let sidebarFeed = item as! SidebarFeed
d[sidebarFeed.feedID] = sidebarFeed
}
return d
}
func createFoldersDictionary() -> [Int: SidebarFolder] {
var d = [Int: SidebarFolder]()
for item in items where item is SidebarFolder {
let sidebarFolder = item as! SidebarFolder
d[sidebarFolder.folderID] = sidebarFolder
}
return d
}
}
// MARK: - SidebarSmartFeedsFolder
@MainActor final class SidebarSmartFeedsFolder: Section, SidebarContainer {
let id = createID()
var isExpanded = true
let items: [any Item] = [
SidebarSmartFeed(SmartFeedsController.shared.todayFeed),
SidebarSmartFeed(SmartFeedsController.shared.unreadFeed),
SidebarSmartFeed(SmartFeedsController.shared.starredFeed)
]
func updateItems() {
}
}
// MARK: - SidebarSmartFeed
@MainActor final class SidebarSmartFeed: Item {
let id = createID()
let smartFeed: any PseudoFeed
init(_ smartFeed: any PseudoFeed) {
self.smartFeed = smartFeed
}
}
// MARK: - SidebarAccount
@MainActor final class SidebarAccount: Section, SidebarContainer {
let id = createID()
let accountID: String
weak var account: Account?
var isExpanded = true
var items = [any Item]() { // top-level feeds and folders
didSet {
feedsDictionary = createFeedsDictionary()
foldersDictionary = createFoldersDictionary()
}
}
private var feedsDictionary = [String: SidebarFeed]()
private var foldersDictionary = [Int: SidebarFolder]()
init(_ account: Account) {
self.accountID = account.accountID
self.account = account
updateItems()
}
func updateItems() {
guard let account else {
items = [any Item]()
return
}
var sidebarFeeds = [SidebarFeed]()
for feed in account.topLevelFeeds {
if let existingFeed = feedsDictionary[feed.feedID] {
sidebarFeeds.append(existingFeed)
} else {
sidebarFeeds.append(SidebarFeed(feed))
}
}
var sidebarFolders = [SidebarFolder]()
if let folders = account.folders {
for folder in folders {
if let existingFolder = foldersDictionary[folder.folderID] {
sidebarFolders.append(existingFolder)
} else {
sidebarFolders.append(SidebarFolder(folder))
}
}
}
for folder in sidebarFolders {
folder.updateItems()
}
// TODO: sort feeds
items = sidebarFeeds + sidebarFolders
}
}
// MARK: - SidebarFolder
@MainActor final class SidebarFolder: Item, SidebarContainer {
let id = createID()
let folderID: Int
weak var folder: Folder?
var isExpanded = false
var items = [any Item]() { // SidebarFeed
didSet {
feedsDictionary = createFeedsDictionary()
}
}
private var feedsDictionary = [String: SidebarFeed]()
init(_ folder: Folder) {
self.folderID = folder.folderID
self.folder = folder
updateItems()
}
func updateItems() {
guard let folder else {
items = [any Item]()
return
}
var sidebarFeeds = [any Item]()
for feed in folder.topLevelFeeds {
if let existingFeed = feedsDictionary[feed.feedID] {
sidebarFeeds.append(existingFeed)
} else {
sidebarFeeds.append(SidebarFeed(feed))
}
}
// TODO: sort feeds
items = sidebarFeeds
}
}
// MARK: - SidebarFeed
@MainActor final class SidebarFeed: Item {
let id = createID()
let feedID: String
weak var feed: Feed?
init(_ feed: Feed) {
self.feedID = feed.feedID
self.feed = feed
}
}
// MARK: - SidebarTree
@MainActor final class SidebarTree {
var sections = [any Section]()
private lazy var sidebarSmartFeedsFolder = SidebarSmartFeedsFolder()
func rebuildTree() {
rebuildSections()
for section in sections where section is SidebarAccount {
let sidebarAccount = section as! SidebarAccount
sidebarAccount.updateItems()
}
}
}
private extension SidebarTree {
func rebuildSections() {
var updatedSections = [any Section]()
updatedSections.append(sidebarSmartFeedsFolder)
for account in AccountManager.shared.activeAccounts {
if let existingSection = existingSection(for: account) {
updatedSections.append(existingSection)
} else {
updatedSections.append(SidebarAccount(account))
}
}
// TODO: sort accounts
sections = updatedSections
}
func existingSection(for account: Account) -> (any Section)? {
// Linear search through array because its just a few items.
let accountID = account.accountID
for sidebarSection in sections where sidebarSection is SidebarAccount {
let sidebarAccount = sidebarSection as! SidebarAccount
if sidebarAccount.accountID == accountID {
return sidebarAccount
}
}
return nil
}
}
// MARK: - IDs
@MainActor private var autoIncrementingID = 0
@MainActor private func createID() -> Int {
defer { autoIncrementingID += 1 }
return autoIncrementingID
}