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: 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 dragProvider: (MinecraftContentItem) -> NSItemProvider 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, dragProvider: dragProvider) .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 let dragProvider: (MinecraftContentItem) -> NSItemProvider var body: some View { HStack(alignment: .center, spacing: 10) { ItemThumbnailView(iconURL: item.iconURL) .onDrag { dragProvider(item) } 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) } Image(systemName: "square.and.arrow.up") .font(.caption) .foregroundStyle(.tertiary) .help("Drag Out as Minecraft Package") .onDrag { dragProvider(item) } } .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