mirror of
https://github.com/somegeekintn/SimDirs.git
synced 2026-03-25 08:55:54 +00:00
Monitors added/removed devices again
This commit is contained in:
parent
f6d596ad1d
commit
baee85bcae
18 changed files with 428 additions and 335 deletions
|
|
@ -32,8 +32,8 @@
|
|||
C966877529B7641F007BB3F5 /* FilteredNodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876929B7641F007BB3F5 /* FilteredNodeView.swift */; };
|
||||
C966877629B7641F007BB3F5 /* NodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876A29B7641F007BB3F5 /* NodeView.swift */; };
|
||||
C966877729B7641F007BB3F5 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C966876B29B7641F007BB3F5 /* Node.swift */; };
|
||||
C966877829B76533007BB3F5 /* SourceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90BCE4F2861E9D000C2EF35 /* SourceState.swift */; };
|
||||
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9779741284F6DE000706DFB /* ToolbarMenu.swift */; };
|
||||
C98048FC29C5FC2A00DE0DBD /* NodeAB.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */; };
|
||||
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F858283B9F9000D491F4 /* SimDirsApp.swift */; };
|
||||
C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F85A283B9F9000D491F4 /* ContentView.swift */; };
|
||||
C982F85D283B9F9200D491F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C982F85C283B9F9200D491F4 /* Assets.xcassets */; };
|
||||
|
|
@ -61,7 +61,6 @@
|
|||
C90BCE492861DA6700C2EF35 /* RuntimeHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeHeader.swift; sourceTree = "<group>"; };
|
||||
C90BCE4B2861E37900C2EF35 /* AppHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHeader.swift; sourceTree = "<group>"; };
|
||||
C90BCE4D2861E4E400C2EF35 /* AppContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContent.swift; sourceTree = "<group>"; };
|
||||
C90BCE4F2861E9D000C2EF35 /* SourceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceState.swift; sourceTree = "<group>"; };
|
||||
C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceFilter.swift; sourceTree = "<group>"; };
|
||||
C90DCC132896AAAA0072E403 /* ContentHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentHeader.swift; sourceTree = "<group>"; };
|
||||
C90DCC152896B0370072E403 /* AppearancePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePicker.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -82,6 +81,7 @@
|
|||
C966876A29B7641F007BB3F5 /* NodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeView.swift; sourceTree = "<group>"; };
|
||||
C966876B29B7641F007BB3F5 /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
|
||||
C9779741284F6DE000706DFB /* ToolbarMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarMenu.swift; sourceTree = "<group>"; };
|
||||
C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAB.swift; sourceTree = "<group>"; };
|
||||
C982F855283B9F9000D491F4 /* SimDirs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SimDirs.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C982F858283B9F9000D491F4 /* SimDirsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimDirsApp.swift; sourceTree = "<group>"; };
|
||||
C982F85A283B9F9000D491F4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -133,6 +133,7 @@
|
|||
C966875E29B7641F007BB3F5 /* FilteredNode.swift */,
|
||||
C966876629B7641F007BB3F5 /* NodeListBuilder.swift */,
|
||||
C966876B29B7641F007BB3F5 /* Node.swift */,
|
||||
C98048FB29C5FC2A00DE0DBD /* NodeAB.swift */,
|
||||
);
|
||||
path = Node;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -238,7 +239,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
C90BCE512861EDBF00C2EF35 /* SourceFilter.swift */,
|
||||
C90BCE4F2861E9D000C2EF35 /* SourceState.swift */,
|
||||
);
|
||||
path = Presentation;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -342,7 +342,6 @@
|
|||
C966877629B7641F007BB3F5 /* NodeView.swift in Sources */,
|
||||
C966877129B7641F007BB3F5 /* SimRuntime+Node.swift in Sources */,
|
||||
C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */,
|
||||
C966877829B76533007BB3F5 /* SourceState.swift in Sources */,
|
||||
C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */,
|
||||
C9DD54D22860A24B00D46AB3 /* RuntimeContent.swift in Sources */,
|
||||
C9779742284F6DE000706DFB /* ToolbarMenu.swift in Sources */,
|
||||
|
|
@ -360,6 +359,7 @@
|
|||
C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */,
|
||||
C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */,
|
||||
C982F859283B9F9000D491F4 /* SimDirsApp.swift in Sources */,
|
||||
C98048FC29C5FC2A00DE0DBD /* NodeAB.swift in Sources */,
|
||||
C982F871283CE7B800D491F4 /* SimRuntime.swift in Sources */,
|
||||
C95CC0FA28B2414900928FAE /* SystemIconButtonStyle.swift in Sources */,
|
||||
C9EE0CD228478FDB00E9B97A /* PathRow.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -6,26 +6,61 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct ContentView: View {
|
||||
@ObservedObject var state : SourceState
|
||||
@State var filter = SourceFilter.restore()
|
||||
enum Style: Int, CaseIterable, Identifiable {
|
||||
case placeholder
|
||||
case byDevice
|
||||
case byRuntime
|
||||
|
||||
var id : Int { rawValue }
|
||||
var visible : Bool { self != .placeholder }
|
||||
|
||||
var title : String {
|
||||
switch self {
|
||||
case .placeholder: return "Placeholder"
|
||||
case .byDevice: return "By Device"
|
||||
case .byRuntime: return "By Runtime"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State var filter = SourceFilter.restore()
|
||||
@State var viewID = UUID().uuidString
|
||||
@State var style = Style.byDevice
|
||||
let model : SimModel
|
||||
|
||||
init(model: SimModel) {
|
||||
state = SourceState(model: model)
|
||||
self.model = model
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
NavigationView {
|
||||
FilteredNodeView(filter: $filter) { state.items }
|
||||
.id(state.style)
|
||||
.toolbar { ToolbarItem { ToolbarMenu(state: state, filter: $filter) } }
|
||||
FilteredNodeView(filter: $filter) { items }
|
||||
.id(viewID)
|
||||
.toolbar { ToolbarItem { ToolbarMenu(style: $style, filter: $filter) } }
|
||||
.frame(minWidth: 200)
|
||||
|
||||
Image("Icon-256") // Initial View
|
||||
}
|
||||
}
|
||||
.onChange(of: style) { _ in resetView() }
|
||||
.environment(\.deviceUpdates, model.deviceUpdates)
|
||||
}
|
||||
|
||||
@NodeListBuilder
|
||||
var items: [some Node] {
|
||||
switch style {
|
||||
case .placeholder: [] as [LeafNode]
|
||||
case .byDevice: SimProductFamily.presentation.map { $0.linked(from: model) }
|
||||
case .byRuntime: SimPlatform.presentation.map { $0.linked(from: model) }
|
||||
}
|
||||
}
|
||||
|
||||
func resetView() {
|
||||
viewID = UUID().uuidString
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,3 +74,14 @@ struct ContentView_Previews: PreviewProvider {
|
|||
.preferredColorScheme(.light)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DeviceUpdatesKey: EnvironmentKey {
|
||||
static let defaultValue = PassthroughSubject<SimModel.Update, Never>()
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var deviceUpdates: PassthroughSubject<SimModel.Update, Never> {
|
||||
get { self[DeviceUpdatesKey.self] }
|
||||
set { self[DeviceUpdatesKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ class SimDevice: ObservableObject, Decodable {
|
|||
let dataPathSize : Int
|
||||
let logPath : String
|
||||
let deviceTypeIdentifier : String
|
||||
var deviceType : SimDeviceType?
|
||||
var deviceModel : String?
|
||||
var apps = [SimApp]()
|
||||
var dataURL : URL { URL(fileURLWithPath: dataPath) }
|
||||
|
|
@ -315,16 +314,6 @@ extension SimDevice {
|
|||
}
|
||||
|
||||
extension Array where Element == SimDevice {
|
||||
func linkingDeviceType(_ deviceType: SimDeviceType) -> Self {
|
||||
let devices = filter { $0.isDeviceOfType(deviceType) }
|
||||
|
||||
for device in devices {
|
||||
device.deviceType = deviceType
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
func of(deviceType: SimDeviceType) -> Self {
|
||||
filter { $0.isDeviceOfType(deviceType) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ enum SimError: Error {
|
|||
case invalidApp
|
||||
}
|
||||
|
||||
struct SimDevicesUpdates {
|
||||
let runtime : SimRuntime
|
||||
var additions : [SimDevice]
|
||||
var removals : [SimDevice]
|
||||
}
|
||||
|
||||
class SimModel {
|
||||
struct Update {
|
||||
let runtime : SimRuntime
|
||||
var additions : [SimDevice]
|
||||
var removals : [SimDevice]
|
||||
}
|
||||
|
||||
var deviceTypes : [SimDeviceType]
|
||||
var runtimes : [SimRuntime]
|
||||
var monitor : Cancellable?
|
||||
|
|
@ -28,7 +28,7 @@ class SimModel {
|
|||
var devices : [SimDevice] { runtimes.flatMap { $0.devices } }
|
||||
var apps : [SimApp] { devices.flatMap { $0.apps } }
|
||||
|
||||
var deviceUpdates = PassthroughSubject<SimDevicesUpdates, Never>()
|
||||
var deviceUpdates = PassthroughSubject<SimModel.Update, Never>()
|
||||
|
||||
init() {
|
||||
let simctl = SimCtl()
|
||||
|
|
@ -70,25 +70,13 @@ class SimModel {
|
|||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] runtimeDevs in
|
||||
guard let this = self else { return }
|
||||
guard let this = self else { return }
|
||||
|
||||
for (runtimeID, curDevices) in runtimeDevs {
|
||||
guard let runtime = this.runtimes.first(where: { $0.identifier == runtimeID }) else { print("missing runtime: \(runtimeID)"); continue }
|
||||
let curDevIDs = curDevices.map { $0.udid }
|
||||
let lastDevID = runtime.devices.map { $0.udid }
|
||||
let updates = SimDevicesUpdates(
|
||||
runtime: runtime,
|
||||
additions: curDevices.filter { !lastDevID.contains($0.udid) },
|
||||
removals: runtime.devices.filter { !curDevIDs.contains($0.udid) })
|
||||
|
||||
if !updates.removals.isEmpty || !updates.additions.isEmpty {
|
||||
let idsToRemove = updates.removals.map { $0.udid }
|
||||
|
||||
updates.additions.completeSetup(with: this.deviceTypes)
|
||||
runtime.devices.removeAll(where: { idsToRemove.contains($0.udid) })
|
||||
runtime.devices.append(contentsOf: updates.additions)
|
||||
|
||||
this.deviceUpdates.send(updates)
|
||||
|
||||
if let changes = runtime.reconcileDevices(curDevices, forTypes: this.deviceTypes) {
|
||||
this.deviceUpdates.send(changes)
|
||||
}
|
||||
|
||||
for srcDevice in curDevices {
|
||||
|
|
|
|||
|
|
@ -112,6 +112,26 @@ class SimRuntime: ObservableObject, Comparable, Decodable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reconcileDevices(_ curDevices: [SimDevice], forTypes deviceTypes: [SimDeviceType]) -> SimModel.Update? {
|
||||
let curDevIDs = curDevices.map { $0.udid }
|
||||
let ourDevIDs = devices.map { $0.udid }
|
||||
let additions = curDevices.filter { !ourDevIDs.contains($0.udid) }
|
||||
let removals = devices.filter { !curDevIDs.contains($0.udid) }
|
||||
var result : SimModel.Update? = nil
|
||||
|
||||
if !additions.isEmpty || !removals.isEmpty {
|
||||
let idsToRemove = removals.map { $0.udid }
|
||||
|
||||
additions.completeSetup(with: deviceTypes)
|
||||
devices.removeAll(where: { idsToRemove.contains($0.udid) })
|
||||
devices.append(contentsOf: additions)
|
||||
|
||||
result = SimModel.Update(runtime: self, additions: additions, removals: removals)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == SimRuntime {
|
||||
|
|
|
|||
|
|
@ -7,25 +7,38 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
extension SimDevice: Node {
|
||||
var title : String { return name }
|
||||
var headerTitle : String { "Device: \(title)" }
|
||||
|
||||
var header : some View { DeviceHeader(device: self) }
|
||||
var content : some View { DeviceContent(self) }
|
||||
// SimDevice requires a wrapper to simulate Node conformance because its
|
||||
// icon is provided by a SimDeviceType
|
||||
|
||||
// var isEnabled : Bool { isBooted }
|
||||
var iconName : String { deviceType?.productFamily.symbolName ?? "questionmark.circle" }
|
||||
struct SimDeviceNode: Node {
|
||||
let device : SimDevice
|
||||
var iconName : String
|
||||
|
||||
var title : String { device.name }
|
||||
var headerTitle : String { "Device: \(title)" }
|
||||
var header : some View { DeviceHeader(device) }
|
||||
var content : some View { DeviceContent(device) }
|
||||
var items : [SimApp]? {
|
||||
get { apps }
|
||||
set { apps = newValue ?? [] }
|
||||
get { device.apps }
|
||||
set { device.apps = newValue ?? [] }
|
||||
}
|
||||
|
||||
init(_ device: SimDevice, iconName: String) {
|
||||
self.device = device
|
||||
self.iconName = iconName
|
||||
}
|
||||
|
||||
func icon(forHeader: Bool) -> some View {
|
||||
symbolIcon(iconName, color: isAvailable ? .green : .red, forHeader: forHeader)
|
||||
symbolIcon(iconName, color: device.isAvailable ? .green : .red, forHeader: forHeader)
|
||||
}
|
||||
|
||||
func matchedFilterOptions() -> SourceFilter.Options {
|
||||
return !apps.isEmpty ? .withApps : []
|
||||
return !device.apps.isEmpty ? .withApps : []
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == SimDevice {
|
||||
func nodesFor(deviceType: SimDeviceType) -> [SimDeviceNode] {
|
||||
filter({ $0.isDeviceOfType(deviceType) }).map({ SimDeviceNode($0, iconName: deviceType.productFamily.symbolName) })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,29 @@ extension SimDeviceType: Node {
|
|||
func icon(forHeader: Bool) -> some View {
|
||||
symbolIcon(productFamily.symbolName, forHeader: forHeader)
|
||||
}
|
||||
|
||||
func linkedForDeviceStyle(from model: SimModel) -> some Node {
|
||||
NodeLink(self) {
|
||||
model.runtimes.supporting(deviceType: self).map { runtime in
|
||||
runtime.linkedForDeviceStyle(from: model, deviceType: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func linkedForRuntimeStyle(from model: SimModel, runtime: SimRuntime) -> some Node {
|
||||
var node = NodeLink(self, items: runtime.devices.nodesFor(deviceType: self))
|
||||
|
||||
return node.onUpdate { update in
|
||||
guard let runtime = model.runtimes.supporting(deviceType: self).first(where: { $0 == update.runtime }) else { return nil }
|
||||
let ourAdditions = update.additions.filter({ $0.deviceTypeIdentifier == identifier })
|
||||
let ourRemovals = update.removals.filter({ $0.deviceTypeIdentifier == identifier })
|
||||
|
||||
if !ourAdditions.isEmpty || !ourRemovals.isEmpty {
|
||||
return runtime.devices.nodesFor(deviceType: self)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,12 @@ extension SimPlatform: Node {
|
|||
func icon(forHeader: Bool) -> some View {
|
||||
symbolIcon(symbolName, forHeader: forHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func linked(from model: SimModel) -> some Node {
|
||||
NodeLink(self) {
|
||||
model.runtimes.supporting(platform: self).map { runtime in
|
||||
runtime.linkedForRuntimeStyle(from: model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,12 @@ extension SimProductFamily: Node {
|
|||
func icon(forHeader: Bool) -> some View {
|
||||
symbolIcon(symbolName, forHeader: forHeader)
|
||||
}
|
||||
|
||||
func linked(from model: SimModel) -> some Node {
|
||||
NodeLink(self) {
|
||||
model.deviceTypes.supporting(productFamily: self).map { deviceType in
|
||||
deviceType.linkedForDeviceStyle(from: model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,4 +21,30 @@ extension SimRuntime: Node {
|
|||
func matchedFilterOptions() -> SourceFilter.Options {
|
||||
return isAvailable ? .runtimeInstalled : []
|
||||
}
|
||||
|
||||
func linkedForDeviceStyle(from model: SimModel, deviceType: SimDeviceType) -> some Node {
|
||||
var node = NodeLink(self) { devices.nodesFor(deviceType: deviceType) }
|
||||
|
||||
return node.onUpdate { [weak self] update in
|
||||
guard let this = self else { return nil }
|
||||
guard update.runtime == this else { return nil }
|
||||
let ourAdditions = update.additions.filter({ $0.deviceTypeIdentifier == deviceType.identifier })
|
||||
let ourRemovals = update.removals.filter({ $0.deviceTypeIdentifier == deviceType.identifier })
|
||||
|
||||
if !ourAdditions.isEmpty || !ourRemovals.isEmpty {
|
||||
return this.devices.nodesFor(deviceType: deviceType)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func linkedForRuntimeStyle(from model: SimModel) -> some Node {
|
||||
NodeLink(self) {
|
||||
model.deviceTypes.supporting(runtime: self).map { devType in
|
||||
devType.linkedForRuntimeStyle(from: model, runtime: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ import SwiftUI
|
|||
import Combine
|
||||
|
||||
class FilteredNode<T: Node>: Node, ObservableObject {
|
||||
typealias FilteredList = [FilteredNode<T.List.Element>]
|
||||
typealias FilteredList = [FilteredNode<T.Child>]
|
||||
|
||||
@Published var filtered : Bool
|
||||
@Published var isExpanded = false
|
||||
@Published var items : FilteredList?
|
||||
|
||||
var wrappedNode : T
|
||||
var title : String { wrappedNode.title }
|
||||
var headerTitle : String { wrappedNode.headerTitle }
|
||||
var header : some View { wrappedNode.header }
|
||||
var content : some View { wrappedNode.content }
|
||||
var items : FilteredList?
|
||||
var children : FilteredList { items ?? [] }
|
||||
|
||||
init(_ node: T) {
|
||||
|
|
@ -63,9 +63,24 @@ class FilteredNode<T: Node>: Node, ObservableObject {
|
|||
|
||||
return nodeMatch
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func processUpdate(_ update: SimModel.Update) -> Bool {
|
||||
if wrappedNode.processUpdate(update) {
|
||||
items = wrappedNode.items?.asFilteredNodes()
|
||||
}
|
||||
|
||||
if let items {
|
||||
for node in items {
|
||||
node.processUpdate(update)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension NodeList {
|
||||
extension Array where Element: Node {
|
||||
func asFilteredNodes() -> [FilteredNode<Element>] {
|
||||
self.map { FilteredNode($0) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
protocol Node: NodeSource {
|
||||
protocol Node {
|
||||
associatedtype Icon: View
|
||||
associatedtype Header: View
|
||||
associatedtype Content: View
|
||||
associatedtype Child: Node
|
||||
|
||||
var items : [Child]? { get set }
|
||||
|
||||
var title : String { get }
|
||||
var headerTitle : String { get }
|
||||
|
||||
|
|
@ -23,14 +26,13 @@ protocol Node: NodeSource {
|
|||
|
||||
func matchedFilterOptions() -> SourceFilter.Options
|
||||
func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool
|
||||
@discardableResult
|
||||
mutating func processUpdate(_ update: SimModel.Update) -> Bool
|
||||
}
|
||||
|
||||
extension Node {
|
||||
var items : [LeafNode]? {
|
||||
get { nil }
|
||||
set { }
|
||||
}
|
||||
|
||||
var items : [LeafNode]? { get { nil } set { } }
|
||||
|
||||
@ViewBuilder
|
||||
func symbolIcon(_ systemName: String, color: Color? = nil, forHeader: Bool) -> some View {
|
||||
if forHeader {
|
||||
|
|
@ -66,14 +68,11 @@ extension Node {
|
|||
func matchesFilter(_ filter: SourceFilter, inherited options: SourceFilter.Options) -> Bool {
|
||||
filter.options.isSubset(of: options) && matchesTerm(filter.searchTerm)
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates a type that owns a list of Nodes
|
||||
|
||||
protocol NodeSource {
|
||||
associatedtype List: NodeList
|
||||
|
||||
var items : List? { get set }
|
||||
|
||||
@discardableResult
|
||||
mutating func processUpdate(_ update: SimModel.Update) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the requirements of a collection that can serve as a `NodeList`.
|
||||
|
|
@ -82,14 +81,10 @@ protocol NodeList: RandomAccessCollection where Self.Element: Node, Index: Hasha
|
|||
|
||||
extension NodeList {
|
||||
@NodeListBuilder
|
||||
func linkEachTo<Item: Node>(emptyIsNil: Bool = false, @NodeListBuilder items: (Element) -> [Item]) -> some NodeList {
|
||||
func linkEachTo<Item: Node>(emptyIsNil: Bool = false, @NodeListBuilder items: (Element) -> [Item]) -> [some Node] {
|
||||
map { item in
|
||||
item.link(emptyIsNil: emptyIsNil, to: { items(item) })
|
||||
}
|
||||
// Makes compiler unhappy. resultBuilder probably incorrect
|
||||
// for item in self {
|
||||
// item.link(to: { items(item) })
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,17 +119,22 @@ struct RootNode<Item: Node>: Node {
|
|||
self.items = items()
|
||||
}
|
||||
|
||||
func icon(forHeader: Bool) -> some View { symbolIcon("tree", forHeader: forHeader) }
|
||||
func icon(forHeader: Bool) -> some View {
|
||||
symbolIcon("tree", forHeader: forHeader)
|
||||
}
|
||||
}
|
||||
|
||||
struct NodeLink<Base: Node, Item: Node>: Node {
|
||||
var base : Base
|
||||
var items : [Item]?
|
||||
var title : String { base.title }
|
||||
var headerTitle : String { base.headerTitle }
|
||||
var header : Base.Header { base.header }
|
||||
var content : Base.Content { base.content }
|
||||
typealias UpdateHandler = (SimModel.Update) -> [Item]??
|
||||
|
||||
var base : Base
|
||||
var items : [Item]?
|
||||
var title : String { base.title }
|
||||
var headerTitle : String { base.headerTitle }
|
||||
var header : Base.Header { base.header }
|
||||
var content : Base.Content { base.content }
|
||||
var updaterHandler : UpdateHandler? = nil
|
||||
|
||||
init(_ base: Base, emptyIsNil: Bool = false, @NodeListBuilder items: () -> [Item]) {
|
||||
let list = items()
|
||||
|
||||
|
|
@ -142,10 +142,9 @@ struct NodeLink<Base: Node, Item: Node>: Node {
|
|||
self.items = emptyIsNil ? (list.isEmpty ? nil : list) : list
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "Consider using Root { items } instead")
|
||||
init(@NodeListBuilder _ items: () -> [Item]) where Base == RootNode<Item> {
|
||||
self.base = RootNode()
|
||||
self.items = items()
|
||||
init(_ base: Base, emptyIsNil: Bool = false, items: [Item]) {
|
||||
self.base = base
|
||||
self.items = emptyIsNil ? (items.isEmpty ? nil : items) : items
|
||||
}
|
||||
|
||||
func icon(forHeader: Bool) -> some View {
|
||||
|
|
@ -155,4 +154,19 @@ struct NodeLink<Base: Node, Item: Node>: Node {
|
|||
func matchedFilterOptions() -> SourceFilter.Options {
|
||||
return base.matchedFilterOptions()
|
||||
}
|
||||
|
||||
mutating func onUpdate(_ handler: @escaping UpdateHandler) -> Self {
|
||||
updaterHandler = handler
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func processUpdate(_ update: SimModel.Update) -> Bool {
|
||||
guard let newItems = updaterHandler?(update) else { return false }
|
||||
|
||||
self.items = newItems
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
119
SimDirs/Node/NodeAB.swift
Normal file
119
SimDirs/Node/NodeAB.swift
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// NodeAB.swift
|
||||
// SimDirs
|
||||
//
|
||||
// Created by Casey Fleser on 3/18/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum NodeAB<A: Node, B: Node>: Node, CustomStringConvertible {
|
||||
case a(A)
|
||||
case b(B)
|
||||
|
||||
var title : String {
|
||||
switch self {
|
||||
case .a(let node): return node.title
|
||||
case .b(let node): return node.title
|
||||
}
|
||||
}
|
||||
|
||||
var headerTitle : String {
|
||||
switch self {
|
||||
case .a(let node): return node.headerTitle
|
||||
case .b(let node): return node.headerTitle
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var header : some View {
|
||||
switch self {
|
||||
case .a(let node): node.header
|
||||
case .b(let node): node.header
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var content : some View {
|
||||
switch self {
|
||||
case .a(let node): node.content
|
||||
case .b(let node): node.content
|
||||
}
|
||||
}
|
||||
|
||||
var items : [NodeAB<A.Child, B.Child>]? {
|
||||
get {
|
||||
switch self {
|
||||
case .a(let node): return node.items?.map { .a($0) }
|
||||
case .b(let node): return node.items?.map { .b($0) }
|
||||
}
|
||||
}
|
||||
set {
|
||||
switch self {
|
||||
case .a(var node):
|
||||
let items : [A.Child]? = newValue?.compactMap({ ab in
|
||||
guard case .a(let a) = ab else { return nil }
|
||||
|
||||
return a
|
||||
})
|
||||
|
||||
node.items = items
|
||||
self = .a(node)
|
||||
|
||||
case .b(var node):
|
||||
let items : [B.Child]? = newValue?.compactMap({ ab in
|
||||
guard case .b(let b) = ab else { return nil }
|
||||
|
||||
return b
|
||||
})
|
||||
|
||||
node.items = items
|
||||
self = .b(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var description : String {
|
||||
let valueDesc : String
|
||||
|
||||
switch self {
|
||||
case .a(let node): valueDesc = ".a: \(String(describing: node))"
|
||||
case .b(let node): valueDesc = ".b: \(String(describing: node))"
|
||||
}
|
||||
|
||||
return "NodeAB<A-\(A.self), B-\(B.self)>: \(valueDesc)"
|
||||
}
|
||||
|
||||
func icon(forHeader: Bool) -> some View {
|
||||
switch self {
|
||||
case .a(let node): node.icon(forHeader: forHeader)
|
||||
case .b(let node): node.icon(forHeader: forHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func matchedFilterOptions() -> SourceFilter.Options {
|
||||
switch self {
|
||||
case .a(let node): return node.matchedFilterOptions()
|
||||
case .b(let node): return node.matchedFilterOptions()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func processUpdate(_ update: SimModel.Update) -> Bool {
|
||||
switch self {
|
||||
case .a(var node):
|
||||
let result = node.processUpdate(update)
|
||||
|
||||
self = .a(node)
|
||||
|
||||
return result
|
||||
|
||||
case .b(var node):
|
||||
let result = node.processUpdate(update)
|
||||
|
||||
self = .b(node)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,123 +9,65 @@ import SwiftUI
|
|||
|
||||
@resultBuilder struct NodeListBuilder {
|
||||
typealias P = Node
|
||||
typealias OneOf = NodeAB
|
||||
|
||||
enum OneOf<A: P, B: P>: P, CustomStringConvertible {
|
||||
typealias List = [NodeListBuilder.OneOf<A.List.Element, B.List.Element>]
|
||||
|
||||
case a(A)
|
||||
case b(B)
|
||||
|
||||
var title : String {
|
||||
switch self {
|
||||
case .a(let node): return node.title
|
||||
case .b(let node): return node.title
|
||||
}
|
||||
}
|
||||
|
||||
var headerTitle : String {
|
||||
switch self {
|
||||
case .a(let node): return node.headerTitle
|
||||
case .b(let node): return node.headerTitle
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var header : some View {
|
||||
switch self {
|
||||
case .a(let node): node.header
|
||||
case .b(let node): node.header
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var content : some View {
|
||||
switch self {
|
||||
case .a(let node): node.content
|
||||
case .b(let node): node.content
|
||||
}
|
||||
}
|
||||
|
||||
var items : List? {
|
||||
get {
|
||||
switch self {
|
||||
case .a(let node): return node.items?.map { .a($0) }
|
||||
case .b(let node): return node.items?.map { .b($0) }
|
||||
}
|
||||
}
|
||||
set { }
|
||||
}
|
||||
|
||||
var description : String {
|
||||
let valueDesc : String
|
||||
|
||||
switch self {
|
||||
case .a(let node): valueDesc = ".a: \(String(describing: node))"
|
||||
case .b(let node): valueDesc = ".b: \(String(describing: node))"
|
||||
}
|
||||
|
||||
return "OneOf<A - \(A.self), B - \(B.self)>: \(valueDesc)"
|
||||
}
|
||||
|
||||
func icon(forHeader: Bool) -> some View {
|
||||
switch self {
|
||||
case .a(let node): node.icon(forHeader: forHeader)
|
||||
case .b(let node): node.icon(forHeader: forHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func matchedFilterOptions() -> SourceFilter.Options {
|
||||
switch self {
|
||||
case .a(let node): return node.matchedFilterOptions()
|
||||
case .b(let node): return node.matchedFilterOptions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func buildBlock<C: P>(_ c: [C]) -> [C] {
|
||||
static func buildPartialBlock<C: P>(first c: [C]) -> [C] {
|
||||
c
|
||||
}
|
||||
|
||||
static func buildBlock<C0: P, C1: P> (
|
||||
_ c0: [C0], _ c1: [C1]) -> [OneOf<C0, C1>]
|
||||
{
|
||||
[buildEither(first: c0), buildEither(second: c1)].flatMap { $0 }
|
||||
|
||||
// matching types
|
||||
static func buildPartialBlock<C: P>(accumulated c0: [C], next c1: [C]) -> [C] {
|
||||
c0 + c1
|
||||
}
|
||||
|
||||
static func buildBlock<C0: P, C1: P, C2: P> (
|
||||
_ c0: [C0], _ c1: [C1], _ c2: [C2]) -> [OneOf<OneOf<C0, C1>, C2>]
|
||||
{
|
||||
[buildEither(first: buildBlock(c0, c1)), buildEither(second: c2)].flatMap { $0 }
|
||||
// matches A of OneOf<A, B>
|
||||
static func buildPartialBlock<A: P, B: P>(accumulated ab: [OneOf<A, B>], next a: [A]) -> [OneOf<A, B>] {
|
||||
ab + a.map { .a($0) }
|
||||
}
|
||||
|
||||
// matches B of OneOf<A, B>
|
||||
static func buildPartialBlock<A: P, B: P>(accumulated ab: [OneOf<A, B>], next b: [B]) -> [OneOf<A, B>] {
|
||||
ab + b.map { .b($0) }
|
||||
}
|
||||
|
||||
// matches A of OneOf<OneOf<A, B>, C>
|
||||
static func buildPartialBlock<A: P, B: P, C: P>(accumulated abc: [OneOf<OneOf<A, B>, C>], next a: [A]) -> [OneOf<OneOf<A, B>, C>] {
|
||||
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: a).map { .a($0) }
|
||||
}
|
||||
|
||||
// matches B of OneOf<OneOf<A, B>, C>
|
||||
static func buildPartialBlock<A: P, B: P, C: P>(accumulated abc: [OneOf<OneOf<A, B>, C>], next b: [B]) -> [OneOf<OneOf<A, B>, C>] {
|
||||
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: b).map { .a($0) }
|
||||
}
|
||||
|
||||
// matches C of OneOf<OneOf<A, B>, C>
|
||||
static func buildPartialBlock<A: P, B: P, C: P>(accumulated abc: [OneOf<OneOf<A, B>, C>], next c: [C]) -> [OneOf<OneOf<A, B>, C>] {
|
||||
abc + c.map { .b($0) }
|
||||
}
|
||||
|
||||
// matches A of OneOf<OneOf<A, B>, OneOf<C, D>>
|
||||
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next a: [A]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
|
||||
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: a).map { .a($0) }
|
||||
}
|
||||
|
||||
// matches B of OneOf<OneOf<A, B>, OneOf<C, D>>
|
||||
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next b: [B]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
|
||||
buildPartialBlock(accumulated: [] as [OneOf<A, B>], next: b).map { .a($0) }
|
||||
}
|
||||
|
||||
static func buildBlock<C0: P, C1: P, C2: P, C3: P> (
|
||||
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3]) -> [OneOf<OneOf<C0, C1>, OneOf<C2, C3>>]
|
||||
{
|
||||
[buildEither(first: buildBlock(c0, c1)), buildEither(second: buildBlock(c2, c3))].flatMap { $0 }
|
||||
// matches C of OneOf<OneOf<A, B>, OneOf<C, D>>
|
||||
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next c: [C]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
|
||||
buildPartialBlock(accumulated: [] as [OneOf<C, D>], next: c).map { .b($0) }
|
||||
}
|
||||
|
||||
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P> (
|
||||
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, C4>]
|
||||
{
|
||||
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: c4)].flatMap { $0 }
|
||||
// matches D of OneOf<OneOf<A, B>, OneOf<C, D>>
|
||||
static func buildPartialBlock<A: P, B: P, C: P, D: P>(accumulated abcd: [OneOf<OneOf<A, B>, OneOf<C, D>>], next d: [D]) -> [OneOf<OneOf<A, B>, OneOf<C, D>>] {
|
||||
buildPartialBlock(accumulated: [] as [OneOf<C, D>], next: d).map { .b($0) }
|
||||
}
|
||||
|
||||
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P> (
|
||||
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<C4, C5>>]
|
||||
{
|
||||
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5))].flatMap { $0 }
|
||||
}
|
||||
|
||||
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P, C6: P> (
|
||||
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5], _ c6: [C6]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<OneOf<C4, C5>, C6>>]
|
||||
{
|
||||
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5, c6))].flatMap { $0 }
|
||||
}
|
||||
|
||||
static func buildBlock<C0: P, C1: P, C2: P, C3: P, C4: P, C5: P, C6: P, C7: P> (
|
||||
_ c0: [C0], _ c1: [C1], _ c2: [C2], _ c3: [C3], _ c4: [C4], _ c5: [C5], _ c6: [C6], _ c7: [C7]) -> [OneOf<OneOf<OneOf<C0, C1>, OneOf<C2, C3>>, OneOf<OneOf<C4, C5>, OneOf<C6, C7>>>]
|
||||
{
|
||||
[buildEither(first: buildBlock(c0, c1, c2, c3)), buildEither(second: buildBlock(c4, c5, c6, c7))].flatMap { $0 }
|
||||
// non-matching types
|
||||
static func buildPartialBlock<C0: P, C1: P>(accumulated c0: [C0], next c1: [C1]) -> [OneOf<C0, C1>] {
|
||||
c0.map({ OneOf<C0, C1>.a($0) }) + c1.map({ OneOf<C0, C1>.b($0) })
|
||||
}
|
||||
|
||||
// static func buildBlock<C: Node>(_ c: [C]...) -> [C] {
|
||||
|
|
@ -159,15 +101,11 @@ import SwiftUI
|
|||
}
|
||||
|
||||
static func buildExpression<N: Node>(_ node: N) -> [N] {
|
||||
[node]
|
||||
return [node]
|
||||
}
|
||||
|
||||
static func buildExpression<NL: NodeList>(_ nodeList: NL) -> [NL.Element] {
|
||||
Array(nodeList)
|
||||
}
|
||||
|
||||
static func buildExpression<NS: NodeSource>(_ nodeSource: NS) -> [NS.List.Element] {
|
||||
nodeSource.items.map({ buildExpression($0) }) ?? []
|
||||
return Array(nodeList)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
struct FilteredNodeView<T: Node>: View {
|
||||
@Environment(\.deviceUpdates) var deviceUpdates
|
||||
@StateObject var node : FilteredNode<T>
|
||||
@Binding var filter : SourceFilter
|
||||
|
||||
|
|
@ -25,6 +27,10 @@ struct FilteredNodeView<T: Node>: View {
|
|||
.searchable(text: $filter.searchTerm, placement: .sidebar)
|
||||
.onAppear { node.applyFilter(filter) }
|
||||
.onChange(of: filter) { node.applyFilter($0) }
|
||||
.onReceive(deviceUpdates) { update in
|
||||
node.processUpdate(update)
|
||||
node.applyFilter(filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +38,7 @@ extension FilteredNodeView {
|
|||
struct Root: View {
|
||||
@ObservedObject var node : FilteredNode<T>
|
||||
|
||||
var visibleItems : FilteredNode<T>.List { node.items.map { $0.filter { !$0.filtered} } ?? [] }
|
||||
var visibleItems : [FilteredNode<T.Child>] { node.items.map { $0.filter { !$0.filtered} } ?? [] }
|
||||
|
||||
var body: some View {
|
||||
let items = visibleItems
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
//
|
||||
// SourceState.swift
|
||||
// SimDirs
|
||||
//
|
||||
// Created by Casey Fleser on 6/21/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
class SourceState: ObservableObject {
|
||||
enum Style: Int, CaseIterable, Identifiable {
|
||||
case placeholder
|
||||
case byDevice
|
||||
case byRuntime
|
||||
|
||||
var id : Int { rawValue }
|
||||
var title : String {
|
||||
switch self {
|
||||
case .placeholder: return "Placeholder"
|
||||
case .byDevice: return "By Device"
|
||||
case .byRuntime: return "By Runtime"
|
||||
}
|
||||
}
|
||||
var visible : Bool {
|
||||
switch self {
|
||||
case .placeholder: return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var style = Style.placeholder // { didSet { rebuildBase() } }
|
||||
@Published var selection : UUID?
|
||||
|
||||
var model : SimModel
|
||||
var deviceUpdates : Cancellable?
|
||||
|
||||
init(model: SimModel) {
|
||||
self.style = .byDevice
|
||||
self.model = model
|
||||
|
||||
deviceUpdates = model.deviceUpdates.sink(receiveValue: applyDeviceUpdates)
|
||||
}
|
||||
|
||||
#warning("TODO: still need to apply updates")
|
||||
func applyDeviceUpdates(_ updates: SimDevicesUpdates) {
|
||||
#if false
|
||||
switch base {
|
||||
case .placeholder:
|
||||
break
|
||||
|
||||
case let .device(_, item):
|
||||
for prodFamily in item.children {
|
||||
for devType in prodFamily.children {
|
||||
for runtime in devType.children {
|
||||
guard updates.runtime.identifier == runtime.data.identifier else { continue }
|
||||
let devTypeDevices = updates.additions.filter { $0.isDeviceOfType(devType.data) }
|
||||
|
||||
runtime.children = runtime.children.filter { device in !updates.removals.contains { $0.udid == device.data.udid } }
|
||||
runtime.children.append(contentsOf: devTypeDevices.map { device in
|
||||
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
|
||||
|
||||
return Device(data: device, children: device.apps.map { app in App(data: app, children: []) }, customImgDesc: imageDesc)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case let .runtime(_, item):
|
||||
for platform in item.children {
|
||||
for runtime in platform.children {
|
||||
guard updates.runtime.identifier == runtime.data.identifier else { continue }
|
||||
|
||||
for devType in runtime.children {
|
||||
let devTypeDevices = updates.additions.filter { $0.isDeviceOfType(devType.data) }
|
||||
|
||||
devType.children = devType.children.filter { device in !updates.removals.contains { $0.udid == device.data.udid } }
|
||||
devType.children.append(contentsOf: devTypeDevices.map { device in
|
||||
let imageDesc = devType.imageDesc.withColor(device.isAvailable ? .green : .red)
|
||||
|
||||
return Device(data: device, children: device.apps.map { app in App(data: app, children: []) }, customImgDesc: imageDesc)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyFilter()
|
||||
#endif
|
||||
}
|
||||
|
||||
@NodeListBuilder
|
||||
var items: some NodeList {
|
||||
switch style {
|
||||
case .placeholder: [] as [LeafNode]
|
||||
case .byDevice: deviceStyleItems
|
||||
case .byRuntime: runtimeStyleItems
|
||||
}
|
||||
}
|
||||
|
||||
@NodeListBuilder
|
||||
var deviceStyleItems: some NodeList {
|
||||
SimProductFamily.presentation.linkEachTo { family in
|
||||
model.deviceTypes.supporting(productFamily: family).linkEachTo(emptyIsNil: true) { devType in
|
||||
model.runtimes.supporting(deviceType: devType).linkEachTo(emptyIsNil: true) { runtime in
|
||||
runtime.devices.linkingDeviceType(devType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NodeListBuilder
|
||||
var runtimeStyleItems: some NodeList {
|
||||
SimPlatform.presentation.linkEachTo(emptyIsNil: true) { platform in
|
||||
model.runtimes.supporting(platform: platform).linkEachTo(emptyIsNil: true) { runtime in
|
||||
model.deviceTypes.supporting(runtime: runtime).linkEachTo(emptyIsNil: true) { devType in
|
||||
runtime.devices.linkingDeviceType(devType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -10,6 +10,10 @@ import SwiftUI
|
|||
struct DeviceHeader: View {
|
||||
@ObservedObject var device : SimDevice
|
||||
|
||||
init(_ device: SimDevice) {
|
||||
self.device = device
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 3.0) {
|
||||
HStack(spacing: 8.0) {
|
||||
|
|
@ -36,8 +40,8 @@ struct DeviceHeader_Previews: PreviewProvider {
|
|||
|
||||
static var previews: some View {
|
||||
if !devices.isEmpty {
|
||||
DeviceHeader(device: devices[0])
|
||||
DeviceHeader(device: devices.randomElement() ?? devices[1])
|
||||
DeviceHeader(devices[0])
|
||||
DeviceHeader(devices.randomElement() ?? devices[1])
|
||||
}
|
||||
else {
|
||||
Text("No devices")
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ToolbarMenu: View {
|
||||
@ObservedObject var state : SourceState
|
||||
@Binding var style : ContentView.Style
|
||||
@Binding var filter : SourceFilter
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
Picker("Style", selection: $state.style) {
|
||||
ForEach(SourceState.Style.allCases) { style in
|
||||
Picker("Style", selection: $style) {
|
||||
ForEach(ContentView.Style.allCases) { style in
|
||||
if style.visible {
|
||||
Text(style.title).tag(style)
|
||||
}
|
||||
|
|
@ -30,10 +30,10 @@ struct ToolbarMenu: View {
|
|||
}
|
||||
|
||||
struct ToolbarMenu_Previews: PreviewProvider {
|
||||
static var state = SourceState(model: SimModel())
|
||||
@State static var style = ContentView.Style.byDevice
|
||||
@State static var filter = SourceFilter.restore()
|
||||
|
||||
static var previews: some View {
|
||||
ToolbarMenu(state: state, filter: $filter)
|
||||
ToolbarMenu(style: $style, filter: $filter)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue