mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
260 lines
5.2 KiB
Swift
260 lines
5.2 KiB
Swift
//
|
||
// 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 it’s 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
|
||
}
|