134 lines
4.0 KiB
Swift
134 lines
4.0 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 title: String
|
|
let subtitle: String
|
|
let items: [MinecraftContentItem]
|
|
let searchPrompt: String
|
|
let chooseFolderAction: () -> Void
|
|
let dropAction: ([NSItemProvider]) -> Bool
|
|
let refreshAction: () -> Void
|
|
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)
|
|
}
|
|
}
|
|
.searchable(text: $searchText, prompt: searchPrompt)
|
|
.navigationTitle(isEmpty ? "Library" : title)
|
|
.navigationSubtitle(isEmpty ? "" : subtitle)
|
|
.toolbar {
|
|
if !isEmpty {
|
|
ToolbarItemGroup {
|
|
Button(action: refreshAction) {
|
|
Image(systemName: "arrow.clockwise")
|
|
}
|
|
.help("Rescan Source")
|
|
|
|
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 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.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)"
|
|
}
|
|
}
|
|
|
|
struct ItemListColumnViews_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
ItemListColumnPreviewContainer()
|
|
}
|
|
}
|