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

194 lines
5.7 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
enum ItemSortMode: String, CaseIterable, Identifiable {
case name
case modifiedDate
case size
var id: String { rawValue }
var title: String {
switch self {
case .name:
return "Name"
case .modifiedDate:
return "Modified Date"
case .size:
return "Size"
}
}
}
struct ItemListColumnView<MenuContent: View>: View {
let isEmpty: Bool
@Binding var isDropTargeted: Bool
@Binding var selectedItemID: MinecraftContentItem.ID?
@Binding var searchText: String
@Binding var sortMode: ItemSortMode
let showsHeader: Bool
let sourceName: String
let showsSourceName: Bool
let title: String
let subtitle: String
let showsSubtitle: Bool
let isRefreshing: Bool
let items: [MinecraftContentItem]
let searchPrompt: String
let chooseFolderAction: () -> Void
let dropAction: ([NSItemProvider]) -> Bool
let itemContextMenu: (MinecraftContentItem) -> MenuContent
var body: some View {
Group {
if isEmpty {
EmptySourcesView(
isDropTargeted: isDropTargeted,
chooseFolder: chooseFolderAction
)
.onDrop(of: [UTType.fileURL.identifier], isTargeted: $isDropTargeted, perform: dropAction)
} else {
List(items, selection: $selectedItemID) { item in
ContentRowView(item: item)
.tag(item.id)
.contextMenu {
itemContextMenu(item)
}
}
.listStyle(.inset)
}
}
.safeAreaInset(edge: .top, spacing: 0) {
if !isEmpty && showsHeader {
ItemListHeaderView(
sourceName: sourceName,
showsSourceName: showsSourceName,
title: title,
subtitle: subtitle,
showsSubtitle: showsSubtitle,
isRefreshing: isRefreshing
)
}
}
.searchable(text: $searchText, prompt: searchPrompt)
.navigationTitle(isEmpty ? "Library" : title)
.navigationSubtitle(isEmpty ? "" : subtitle)
.toolbar {
if !isEmpty {
ToolbarItemGroup {
Menu {
Picker("Sort By", selection: $sortMode) {
ForEach(ItemSortMode.allCases) { mode in
Text(mode.title).tag(mode)
}
}
} label: {
Image(systemName: "ellipsis.circle")
}
.help("List Options")
}
}
}
}
}
private struct ItemListHeaderView: View {
let sourceName: String
let showsSourceName: Bool
let title: String
let subtitle: String
let showsSubtitle: Bool
let isRefreshing: Bool
var body: some View {
VStack(alignment: .leading, spacing: 8) {
if showsSourceName {
Text(sourceName)
.font(.caption.weight(.semibold))
.foregroundStyle(.secondary)
.textCase(.uppercase)
}
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(title)
.font(.title2.weight(.semibold))
.lineLimit(2)
if isRefreshing {
ProgressView()
.controlSize(.small)
}
}
if showsSubtitle {
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.top, 10)
.padding(.bottom, 12)
.background(.regularMaterial)
.overlay(alignment: .bottom) {
Divider()
}
}
}
private struct ContentRowView: View {
let item: MinecraftContentItem
var body: some View {
HStack(alignment: .center, spacing: 10) {
ItemThumbnailView(iconURL: item.iconURL)
VStack(alignment: .leading, spacing: 4) {
Text(item.displayName)
.lineLimit(1)
Text(metadataLine)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
if !item.metadataLoaded || !item.sizeLoaded {
ProgressView()
.controlSize(.small)
}
}
.padding(.vertical, 2)
.contentShape(Rectangle())
}
private var metadataLine: String {
let sizeText: String
if let sizeBytes = item.sizeBytes {
sizeText = ByteCountFormatter.string(fromByteCount: sizeBytes, countStyle: .file)
} else if item.sizeLoaded {
sizeText = "Size unavailable"
} else if item.metadataLoaded {
sizeText = "Calculating size..."
} else {
sizeText = "Loading metadata..."
}
let dateText = item.displayDate.map {
$0.formatted(date: .abbreviated, time: .omitted)
} ?? "Date unavailable"
return "\(item.contentType.rawValue)\(sizeText)\(item.displayDateLabel) \(dateText)"
}
}
#if DEBUG
struct ItemListColumnViews_Previews: PreviewProvider {
static var previews: some View {
ItemListColumnPreviewContainer()
}
}
#endif