Files
Termind/Frameworks/TermindApplication/FeatureModels/LibraryExpansionState.swift
T

114 lines
4.0 KiB
Swift

import Foundation
import TermindDomain
/// Device-local presentation state for the Hosts and Snippets libraries.
///
/// The Vault still carries the legacy `isExpanded` fields for wire and schema
/// compatibility, but those values are imported only once. Every later change
/// belongs to this installation and never enters a Vault transaction.
struct LibraryExpansionState {
private struct Snapshot: Codable, Equatable {
var collapsedHostGroupIDs: Set<HostGroup.ID> = []
var collapsedSnippetPackageIDs: Set<SnippetPackage.ID> = []
var hostsUngroupedExpanded = true
var snippetsUngroupedExpanded = true
}
static let storageKey = "library.expansionState.v1"
static let migrationKey = "library.expansionState.migrated.v1"
private let defaults: UserDefaults?
private var snapshot: Snapshot
init(
defaults: UserDefaults? = nil,
legacyGroups: [HostGroup] = [],
legacySnippetPackages: [SnippetPackage] = []
) {
self.defaults = defaults
if let defaults, defaults.bool(forKey: Self.migrationKey) {
if let data = defaults.data(forKey: Self.storageKey),
let decoded = try? JSONDecoder().decode(Snapshot.self, from: data)
{
snapshot = decoded
} else {
snapshot = Snapshot()
persist()
}
return
}
snapshot = Snapshot(
collapsedHostGroupIDs: Set(legacyGroups.filter { !$0.isExpanded }.map(\.id)),
collapsedSnippetPackageIDs: Set(
legacySnippetPackages.filter { !$0.isExpanded }.map(\.id)
)
)
persist()
defaults?.set(true, forKey: Self.migrationKey)
}
func isHostGroupExpanded(_ id: HostGroup.ID) -> Bool {
!snapshot.collapsedHostGroupIDs.contains(id)
}
func isSnippetPackageExpanded(_ id: SnippetPackage.ID) -> Bool {
!snapshot.collapsedSnippetPackageIDs.contains(id)
}
var isHostsUngroupedExpanded: Bool { snapshot.hostsUngroupedExpanded }
var isSnippetsUngroupedExpanded: Bool { snapshot.snippetsUngroupedExpanded }
mutating func setHostGroupExpanded(_ expanded: Bool, for id: HostGroup.ID) {
guard Self.set(expanded, for: id, in: &snapshot.collapsedHostGroupIDs) else { return }
persist()
}
mutating func setSnippetPackageExpanded(_ expanded: Bool, for id: SnippetPackage.ID) {
guard Self.set(expanded, for: id, in: &snapshot.collapsedSnippetPackageIDs) else { return }
persist()
}
mutating func setSnippetPackagesExpanded(_ ids: Set<SnippetPackage.ID>) {
let previous = snapshot.collapsedSnippetPackageIDs
snapshot.collapsedSnippetPackageIDs.subtract(ids)
guard snapshot.collapsedSnippetPackageIDs != previous else { return }
persist()
}
mutating func setHostsUngroupedExpanded(_ expanded: Bool) {
guard snapshot.hostsUngroupedExpanded != expanded else { return }
snapshot.hostsUngroupedExpanded = expanded
persist()
}
mutating func setSnippetsUngroupedExpanded(_ expanded: Bool) {
guard snapshot.snippetsUngroupedExpanded != expanded else { return }
snapshot.snippetsUngroupedExpanded = expanded
persist()
}
mutating func reconcile(
hostGroupIDs: Set<HostGroup.ID>,
snippetPackageIDs: Set<SnippetPackage.ID>
) {
let previous = snapshot
snapshot.collapsedHostGroupIDs.formIntersection(hostGroupIDs)
snapshot.collapsedSnippetPackageIDs.formIntersection(snippetPackageIDs)
guard snapshot != previous else { return }
persist()
}
private static func set<ID: Hashable>(
_ expanded: Bool, for id: ID, in collapsed: inout Set<ID>
) -> Bool {
expanded ? collapsed.remove(id) != nil : collapsed.insert(id).inserted
}
private func persist() {
guard let defaults, let data = try? JSONEncoder().encode(snapshot) else { return }
defaults.set(data, forKey: Self.storageKey)
}
}