world-manager/World Manager for Minecraft/SidebarColumnViews.swift

225 lines
6.4 KiB
Swift

import SwiftUI
enum SidebarSelection: Hashable {
case allContent(sourceID: URL)
case contentType(sourceID: URL, contentType: MinecraftContentType)
var sourceID: URL {
switch self {
case .allContent(let sourceID), .contentType(let sourceID, _):
return sourceID
}
}
}
struct SidebarFilter: Identifiable, Hashable {
var id: SidebarSelection { selection }
let title: String
let iconName: String
let count: Int
let selection: SidebarSelection
}
struct SourcesSidebarView: View {
let sources: [MinecraftSource]
@Binding var selection: SidebarSelection?
let footerState: SidebarFooterState
let addSourceAction: () -> Void
let addDeviceSourceAction: () -> Void
let rescanSourceAction: (MinecraftSource) -> Void
let removeSourceAction: (MinecraftSource) -> Void
let revealFooterURLAction: (URL) -> Void
let filters: (MinecraftSource) -> [SidebarFilter]
var body: some View {
List(selection: $selection) {
Section {
ForEach(sources) { source in
SourceHeaderRow(title: source.displayName)
.listRowSeparator(.hidden)
.padding(.top, 6)
.contextMenu {
Button("Rescan \"\(source.displayName)\"") {
rescanSourceAction(source)
}
Divider()
Button("Remove \"\(source.displayName)\"", role: .destructive) {
removeSourceAction(source)
}
}
ForEach(filters(source)) { filter in
SidebarFilterRow(filter: filter, isIndented: true)
.tag(filter.selection as SidebarSelection?)
}
}
} header: {
SidebarSourcesSectionHeaderView()
}
}
.listStyle(.sidebar)
.overlay(alignment: .bottom) {
if footerState.style != .idle {
SidebarFooterView(
state: footerState,
revealAction: revealFooterURLAction
)
.padding(.horizontal, 10)
.padding(.bottom, 10)
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.toolbar {
ToolbarItem {
Button(action: addSourceAction) {
Image(systemName: "folder.badge.plus")
}
.help("Add Source Folder")
}
ToolbarItem {
Button(action: addDeviceSourceAction) {
Image(systemName: "iphone.gen3")
}
.help("Add Connected Device Source")
}
}
.animation(.easeInOut(duration: 0.2), value: footerState.style)
}
}
private struct SidebarFilterRow: View {
let filter: SidebarFilter
let isIndented: Bool
var body: some View {
HStack(spacing: 10) {
Image(systemName: filter.iconName)
.frame(width: 16)
.foregroundStyle(.secondary)
Text(filter.title)
Spacer()
Text(filter.count, format: .number)
.foregroundStyle(.secondary)
}
.padding(.leading, isIndented ? 16 : 0)
}
}
private struct SidebarSourcesSectionHeaderView: View {
var body: some View {
Text("Libraries")
.font(.headline)
.foregroundStyle(.secondary)
.textCase(nil)
}
}
private struct SourceHeaderRow: View {
let title: String
var body: some View {
Text(title)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.secondary)
}
}
private struct SidebarFooterView: View {
let state: SidebarFooterState
let revealAction: (URL) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
if state.style == .inProgress {
ProgressView()
.controlSize(.small)
.tint(.appAccent)
}
Text(state.title)
.font(.footnote.weight(.semibold))
.foregroundStyle(primaryColor)
.lineLimit(3)
}
if let subtitle = state.subtitle {
Text(subtitle)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(2)
}
if let detail = state.detail {
Text(detail)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
if let revealURL = state.revealURL {
Button("Reveal in Finder") {
revealAction(revealURL)
}
.buttonStyle(.link)
.font(.footnote)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
.padding(.vertical, 12)
.background(cardBackground, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
.overlay {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.strokeBorder(cardStroke)
}
}
private var primaryColor: Color {
switch state.style {
case .idle:
return .primary
case .inProgress:
return .appAccent
case .failure:
return .red
case .success:
return .appAccent
}
}
private var cardBackground: AnyShapeStyle {
switch state.style {
case .inProgress:
return AnyShapeStyle(Color.appAccent.opacity(0.08))
default:
return AnyShapeStyle(.regularMaterial)
}
}
private var cardStroke: Color {
switch state.style {
case .inProgress:
return Color.appAccent.opacity(0.18)
case .failure:
return .red.opacity(0.18)
case .success:
return Color.appAccent.opacity(0.16)
case .idle:
return .white.opacity(0.08)
}
}
}
struct SidebarColumnViews_Previews: PreviewProvider {
static var previews: some View {
SidebarColumnPreviewContainer()
}
}