Render sidebar sections with outline groups
This commit is contained in:
parent
0e52db80df
commit
da53ee4e9b
@ -29,6 +29,21 @@ struct SidebarFilter: Identifiable, Hashable {
|
|||||||
let selection: SidebarSelection
|
let selection: SidebarSelection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct SidebarNode: Identifiable, Hashable {
|
||||||
|
let id: SidebarSelection
|
||||||
|
let row: SidebarNodeRow
|
||||||
|
let children: [SidebarNode]?
|
||||||
|
|
||||||
|
var selection: SidebarSelection { id }
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SidebarNodeRow: Hashable {
|
||||||
|
case source(MinecraftSource)
|
||||||
|
case filter(SidebarFilter)
|
||||||
|
case connectedDevice(ConnectedDeviceSidebarEntry)
|
||||||
|
case sourceCandidate(SourceCandidate)
|
||||||
|
}
|
||||||
|
|
||||||
struct SourcesSidebarView: View {
|
struct SourcesSidebarView: View {
|
||||||
let sources: [MinecraftSource]
|
let sources: [MinecraftSource]
|
||||||
let connectedDevices: [ConnectedDeviceSidebarEntry]
|
let connectedDevices: [ConnectedDeviceSidebarEntry]
|
||||||
@ -46,42 +61,25 @@ struct SourcesSidebarView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(selection: $selection) {
|
List(selection: $selection) {
|
||||||
if !sources.isEmpty {
|
if !libraryNodes.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
ForEach(sources) { source in
|
OutlineGroup(libraryNodes, children: \.children, content: sidebarNodeRow)
|
||||||
sourceSectionRows(for: source)
|
|
||||||
}
|
|
||||||
} header: {
|
} header: {
|
||||||
SidebarSourcesSectionHeaderView(title: "Libraries")
|
SidebarSourcesSectionHeaderView(title: "Libraries")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !connectedDevices.isEmpty {
|
if !deviceNodes.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
ForEach(connectedDevices) { entry in
|
OutlineGroup(deviceNodes, children: \.children, content: sidebarNodeRow)
|
||||||
connectedDeviceSectionRows(for: entry)
|
|
||||||
}
|
|
||||||
} header: {
|
} header: {
|
||||||
SidebarSourcesSectionHeaderView(title: "Available Devices")
|
SidebarSourcesSectionHeaderView(title: "Available Devices")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sourceCandidates.isEmpty {
|
if !candidateNodes.isEmpty {
|
||||||
Section {
|
Section {
|
||||||
ForEach(sourceCandidates) { candidate in
|
OutlineGroup(candidateNodes, children: \.children, content: sidebarNodeRow)
|
||||||
SourceCandidateRow(
|
|
||||||
candidate: candidate,
|
|
||||||
onSelect: {
|
|
||||||
selection = .sourceCandidate(candidateID: candidate.id)
|
|
||||||
},
|
|
||||||
addAction: {
|
|
||||||
addCandidateSourceAction(candidate)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.tag(SidebarSelection.sourceCandidate(candidateID: candidate.id) as SidebarSelection?)
|
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
|
||||||
}
|
|
||||||
} header: {
|
} header: {
|
||||||
SidebarSourcesSectionHeaderView(title: "Found Sources")
|
SidebarSourcesSectionHeaderView(title: "Found Sources")
|
||||||
}
|
}
|
||||||
@ -118,18 +116,50 @@ struct SourcesSidebarView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
private var libraryNodes: [SidebarNode] {
|
||||||
private func sourceSectionRows(for source: MinecraftSource) -> some View {
|
sources.map { source in
|
||||||
let sourceFilters = filters(source)
|
let childNodes = filters(source).map { filter in
|
||||||
|
SidebarNode(
|
||||||
SourceHeaderRow(
|
id: filter.selection,
|
||||||
source: source,
|
row: .filter(filter),
|
||||||
isSelected: selection == .source(sourceID: source.id),
|
children: nil
|
||||||
onSelect: {
|
|
||||||
selection = .source(sourceID: source.id)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.tag(SidebarSelection.source(sourceID: source.id) as SidebarSelection?)
|
}
|
||||||
|
|
||||||
|
return SidebarNode(
|
||||||
|
id: .source(sourceID: source.id),
|
||||||
|
row: .source(source),
|
||||||
|
children: childNodes.isEmpty ? nil : childNodes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var deviceNodes: [SidebarNode] {
|
||||||
|
connectedDevices.map { entry in
|
||||||
|
SidebarNode(
|
||||||
|
id: .connectedDevice(deviceID: entry.id),
|
||||||
|
row: .connectedDevice(entry),
|
||||||
|
children: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var candidateNodes: [SidebarNode] {
|
||||||
|
sourceCandidates.map { candidate in
|
||||||
|
SidebarNode(
|
||||||
|
id: .sourceCandidate(candidateID: candidate.id),
|
||||||
|
row: .sourceCandidate(candidate),
|
||||||
|
children: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func sidebarNodeRow(_ node: SidebarNode) -> some View {
|
||||||
|
switch node.row {
|
||||||
|
case .source(let source):
|
||||||
|
SourceHeaderRow(source: source)
|
||||||
|
.tag(node.selection as SidebarSelection?)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
@ -143,33 +173,35 @@ struct SourcesSidebarView: View {
|
|||||||
removeSourceAction(source)
|
removeSourceAction(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case .filter(let filter):
|
||||||
ForEach(sourceFilters) { filter in
|
SidebarFilterRow(filter: filter)
|
||||||
SidebarFilterRow(filter: filter, isIndented: true)
|
.tag(node.selection as SidebarSelection?)
|
||||||
.tag(filter.selection as SidebarSelection?)
|
case .connectedDevice(let entry):
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func connectedDeviceSectionRows(for entry: ConnectedDeviceSidebarEntry) -> some View {
|
|
||||||
ConnectedDeviceRow(
|
ConnectedDeviceRow(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
onSelect: {
|
|
||||||
selection = .connectedDevice(deviceID: entry.id)
|
|
||||||
},
|
|
||||||
addAction: entry.hasMinecraftContainer ? {
|
addAction: entry.hasMinecraftContainer ? {
|
||||||
addConnectedDeviceAction(entry)
|
addConnectedDeviceAction(entry)
|
||||||
} : nil
|
} : nil
|
||||||
)
|
)
|
||||||
.tag(SidebarSelection.connectedDevice(deviceID: entry.id) as SidebarSelection?)
|
.tag(node.selection as SidebarSelection?)
|
||||||
.listRowSeparator(.hidden)
|
.listRowSeparator(.hidden)
|
||||||
.listRowInsets(EdgeInsets(top: 6, leading: 8, bottom: 0, trailing: 8))
|
.listRowInsets(EdgeInsets(top: 6, leading: 8, bottom: 0, trailing: 8))
|
||||||
|
case .sourceCandidate(let candidate):
|
||||||
|
SourceCandidateRow(
|
||||||
|
candidate: candidate,
|
||||||
|
addAction: {
|
||||||
|
addCandidateSourceAction(candidate)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.tag(node.selection as SidebarSelection?)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowInsets(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct SourceCandidateRow: View {
|
private struct SourceCandidateRow: View {
|
||||||
let candidate: SourceCandidate
|
let candidate: SourceCandidate
|
||||||
let onSelect: () -> Void
|
|
||||||
let addAction: () -> Void
|
let addAction: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -196,8 +228,6 @@ private struct SourceCandidateRow: View {
|
|||||||
.appMiniProminentButton()
|
.appMiniProminentButton()
|
||||||
.help("Add Source")
|
.help("Add Source")
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture(perform: onSelect)
|
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +248,6 @@ private struct SourceCandidateRow: View {
|
|||||||
|
|
||||||
private struct SidebarFilterRow: View {
|
private struct SidebarFilterRow: View {
|
||||||
let filter: SidebarFilter
|
let filter: SidebarFilter
|
||||||
let isIndented: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
@ -233,7 +262,6 @@ private struct SidebarFilterRow: View {
|
|||||||
Text(filter.count, format: .number)
|
Text(filter.count, format: .number)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
.padding(.leading, isIndented ? 16 : 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,8 +275,6 @@ private struct SidebarSourcesSectionHeaderView: View {
|
|||||||
|
|
||||||
private struct SourceHeaderRow: View {
|
private struct SourceHeaderRow: View {
|
||||||
let source: MinecraftSource
|
let source: MinecraftSource
|
||||||
let isSelected: Bool
|
|
||||||
let onSelect: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
@ -277,14 +303,6 @@ private struct SourceHeaderRow: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 5)
|
||||||
.background {
|
|
||||||
if isSelected {
|
|
||||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
||||||
.fill(Color.accentColor.opacity(0.18))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture(perform: onSelect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var connection: DeviceConnection? {
|
private var connection: DeviceConnection? {
|
||||||
@ -408,7 +426,6 @@ private struct CircularScanProgressView: View {
|
|||||||
|
|
||||||
private struct ConnectedDeviceRow: View {
|
private struct ConnectedDeviceRow: View {
|
||||||
let entry: ConnectedDeviceSidebarEntry
|
let entry: ConnectedDeviceSidebarEntry
|
||||||
let onSelect: () -> Void
|
|
||||||
let addAction: (() -> Void)?
|
let addAction: (() -> Void)?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -438,8 +455,6 @@ private struct ConnectedDeviceRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.opacity(addAction == nil ? 0.68 : 1)
|
.opacity(addAction == nil ? 0.68 : 1)
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture(perform: onSelect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var iconName: String {
|
private var iconName: String {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user