225 lines
6.4 KiB
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()
|
|
}
|
|
}
|