// // ContentView.swift // World Manager for Minecraft // // Created by John Burwell on 2026-05-25. // import AppKit import SwiftUI import UniformTypeIdentifiers struct ContentView: View { @StateObject private var library = SourceLibrary() @State private var selectedItemID: MinecraftContentItem.ID? @State private var selectedSidebarSelection: SidebarSelection? @State private var searchText = "" @State private var isDropTargeted = false @State private var isPerformingItemAction = false @State private var sortMode: ItemSortMode = .name private let directoryPreviewLimit = 12 var body: some View { NavigationSplitView { SourcesSidebarView( sources: library.sources, selection: $selectedSidebarSelection, footerState: library.sidebarFooterState, addSourceAction: pickFolder, rescanSourceAction: { source in library.rescanSource(withID: source.id) }, removeSourceAction: { source in removeSource(source.id) }, revealFooterURLAction: revealURLInFinder(_:), filters: sidebarFilters(for:) ) .navigationSplitViewColumnWidth(min: 280, ideal: 320, max: 380) } content: { ItemListColumnView( isEmpty: library.sources.isEmpty, isDropTargeted: $isDropTargeted, selectedItemID: $selectedItemID, searchText: $searchText, sortMode: $sortMode, title: collectionHeaderTitle, subtitle: collectionHeaderSubtitle, items: displayedItems, searchPrompt: searchPrompt, chooseFolderAction: pickFolder, dropAction: handleDroppedProviders(_:), refreshAction: rescanCurrentSource, itemContextMenu: itemContextMenu(for:) ) .navigationSplitViewColumnWidth(min: 340, ideal: 400, max: 460) } detail: { ItemDetailColumnView( item: currentSelectedItem, behaviorPacks: currentSelectedItem.map { logicalPackReferences(for: $0, type: .behaviorPack) } ?? [], resourcePacks: currentSelectedItem.map { logicalPackReferences(for: $0, type: .resourcePack) } ?? [], worldsUsingPack: currentSelectedItem.map(worldsUsingPack(for:)) ?? [], backingPackInstances: currentSelectedItem.map(backingPackInstances(for:)) ?? [], isSuspiciousPack: currentSelectedItem.map(isSuspiciousPack(_:)) ?? false, contents: currentSelectedItem.map(directoryPreviewEntries(for:)) ?? [], directoryPreviewLimit: directoryPreviewLimit, isEmpty: library.sources.isEmpty, isPerformingItemAction: isPerformingItemAction, exportTitle: currentSelectedItem.map(primaryActionTitle(for:)), exportAction: { guard let item = currentSelectedItem else { return } saveItem(item) }, revealAction: { guard let item = currentSelectedItem else { return } revealInFinder(item) }, shareAction: { anchorView in guard let item = currentSelectedItem else { return } shareItem(item, from: anchorView) } ) .frame(minWidth: 450) } .onChange(of: displayedItems.map(\.id)) { _, filteredIDs in guard let selectedItemID, !filteredIDs.contains(selectedItemID) else { return } self.selectedItemID = nil } .onChange(of: library.sources.map(\.id)) { _, sourceIDs in syncSelection(with: sourceIDs) } } private var scopedItems: [MinecraftContentItem] { guard let selectedSidebarSelection else { return [] } switch selectedSidebarSelection { case .allContent(let sourceID): return library.source(withID: sourceID)?.items ?? [] case .contentType(let sourceID, let contentType): return library.source(withID: sourceID)?.items.filter { $0.contentType == contentType } ?? [] } } private var filteredItems: [MinecraftContentItem] { let trimmedSearchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedSearchText.isEmpty else { return scopedItems } return scopedItems.filter { item in item.searchText.localizedCaseInsensitiveContains(trimmedSearchText) } } private var displayedItems: [MinecraftContentItem] { filteredItems.sorted(by: sortComparator) } private var currentSource: MinecraftSource? { guard let sourceID = selectedSidebarSelection?.sourceID else { return library.sources.first } return library.source(withID: sourceID) } private var currentSelectedItem: MinecraftContentItem? { guard let selectedItemID else { return nil } return library.sources .flatMap(\.items) .first(where: { $0.id == selectedItemID }) } private var sortComparator: (MinecraftContentItem, MinecraftContentItem) -> Bool { switch sortMode { case .name: return { lhs, rhs in lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending } case .modifiedDate: return { lhs, rhs in switch (lhs.displayDate, rhs.displayDate) { case let (lhsDate?, rhsDate?): if lhsDate != rhsDate { return lhsDate > rhsDate } case (.some, nil): return true case (nil, .some): return false case (nil, nil): break } return lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending } case .size: return { lhs, rhs in switch (lhs.sizeBytes, rhs.sizeBytes) { case let (lhsSize?, rhsSize?): if lhsSize != rhsSize { return lhsSize > rhsSize } case (.some, nil): return true case (nil, .some): return false case (nil, nil): break } return lhs.displayName.localizedStandardCompare(rhs.displayName) == .orderedAscending } } } private var collectionHeaderTitle: String { if isSearching { return "Searching “\(searchScopeTitle)”" } guard let selectedSidebarSelection else { return "Library" } switch selectedSidebarSelection { case .allContent: return "All Items" case .contentType(_, let contentType): return sidebarTitle(for: contentType) } } private var collectionHeaderSubtitle: String { let totalCount = scopedItems.count let filteredCount = filteredItems.count let noun = collectionCountNoun if !isSearching { return "\(totalCount.formatted(.number)) \(noun)" } return "\(filteredCount.formatted(.number)) of \(totalCount.formatted(.number)) \(noun)" } private var searchScopeTitle: String { switch selectedSidebarSelection { case .some(.allContent): return "All" case .some(.contentType(_, let contentType)): return sidebarTitle(for: contentType) case .none: return "Library" } } private var isSearching: Bool { !searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } private var collectionCountNoun: String { guard let selectedSidebarSelection else { return "items" } switch selectedSidebarSelection { case .allContent: return scopedItems.count == 1 ? "item" : "items" case .contentType(_, let contentType): switch contentType { case .world: return scopedItems.count == 1 ? "world" : "worlds" case .behaviorPack, .resourcePack, .skinPack, .worldTemplate: return scopedItems.count == 1 ? "pack" : "packs" } } } private var searchPrompt: String { switch selectedSidebarSelection { case .some(.allContent): return "Search All Items" case .some(.contentType(_, let contentType)): return "Search \(sidebarTitle(for: contentType))" case .none: return "Search Library" } } private func sidebarFilters(for source: MinecraftSource) -> [SidebarFilter] { var filters = [ SidebarFilter( title: "All Items", iconName: "square.grid.2x2", count: source.items.count, selection: .allContent(sourceID: source.id) ) ] filters.append( contentsOf: MinecraftContentType.allCases.compactMap { contentType in let count = source.items.filter { $0.contentType == contentType }.count guard count > 0 else { return nil } return SidebarFilter( title: sidebarTitle(for: contentType), iconName: sidebarIcon(for: contentType), count: count, selection: .contentType(sourceID: source.id, contentType: contentType) ) } ) return filters } private func sidebarTitle(for contentType: MinecraftContentType) -> String { switch contentType { case .world: return "Worlds" case .behaviorPack: return "Behavior Packs" case .resourcePack: return "Resource Packs" case .skinPack: return "Skin Packs" case .worldTemplate: return "World Templates" } } private func sidebarIcon(for contentType: MinecraftContentType) -> String { switch contentType { case .world: return "globe.europe.africa" case .behaviorPack: return "shippingbox" case .resourcePack: return "paintpalette" case .skinPack: return "person.crop.square" case .worldTemplate: return "doc.on.doc" } } @ViewBuilder private func itemContextMenu(for item: MinecraftContentItem) -> some View { Button("Share...") { shareItem(item, from: nil) } Button(exportMenuTitle(for: item)) { saveItem(item) } Divider() Button("Reveal in Finder") { revealInFinder(item) } } private func exportMenuTitle(for item: MinecraftContentItem) -> String { switch item.contentType { case .world: return "Create Minecraft World File..." case .behaviorPack, .resourcePack, .skinPack: return "Create Minecraft Pack File..." case .worldTemplate: return "Create Minecraft Template File..." } } private func primaryActionTitle(for item: MinecraftContentItem) -> String { switch item.contentType { case .world: return "Create Minecraft World File..." case .behaviorPack, .resourcePack, .skinPack: return "Create Minecraft Pack File..." case .worldTemplate: return "Create Minecraft Template File..." } } private func primaryActionSubtitle(for item: MinecraftContentItem) -> String { switch item.contentType { case .world: return "Creates a .mcworld file that can be opened on another device to import this world into Minecraft." case .behaviorPack, .resourcePack, .skinPack: return "Creates a .mcpack file that can be shared or opened on another device." case .worldTemplate: return "Creates a .mctemplate file that can be opened on another device." } } private func logicalPackReferences(for item: MinecraftContentItem, type: MinecraftContentType) -> [ContentPackReference] { guard item.contentType == .world, let source = currentSource else { return [] } return source.resolvedPackReferences(for: item.id, type: type) } private func worldsUsingPack(for item: MinecraftContentItem) -> [MinecraftContentItem] { guard (item.contentType == .behaviorPack || item.contentType == .resourcePack), let source = currentSource, let logicalPack = source.logicalPack(forRepresentativeItemID: item.id) else { return [] } return source.worldsUsingPack(logicalPack.id).sorted(by: sortComparator) } private func backingPackInstances(for item: MinecraftContentItem) -> [MinecraftContentItem] { guard (item.contentType == .behaviorPack || item.contentType == .resourcePack), let source = currentSource, let logicalPack = source.logicalPack(forRepresentativeItemID: item.id) else { return [] } return source .packInstances(for: logicalPack.id) .compactMap { source.rawItem(withID: $0.itemID) } .sorted(by: WorldScanner.sortItems) } private func isSuspiciousPack(_ item: MinecraftContentItem) -> Bool { guard (item.contentType == .behaviorPack || item.contentType == .resourcePack), let source = currentSource, let logicalPack = source.logicalPack(forRepresentativeItemID: item.id) else { return false } return logicalPack.isSuspicious } private func directoryPreviewEntries(for item: MinecraftContentItem) -> [DirectoryPreviewEntry] { let fileManager = FileManager.default guard let urls = try? fileManager.contentsOfDirectory( at: item.folderURL, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles] ) else { return [] } return urls .map { url in let isDirectory = (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) == true return DirectoryPreviewEntry(name: url.lastPathComponent, isDirectory: isDirectory) } .sorted { lhs, rhs in if lhs.isDirectory != rhs.isDirectory { return lhs.isDirectory && !rhs.isDirectory } return lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending } .prefix(directoryPreviewLimit) .map { $0 } } private func pickFolder() { let panel = NSOpenPanel() panel.allowsMultipleSelection = true panel.canChooseDirectories = true panel.canChooseFiles = false panel.title = "Add Minecraft Source Folders" guard panel.runModal() == .OK else { return } for url in panel.urls { let sourceID = library.addSource(at: url) selectSourceIfNeeded(sourceID) } } private func handleDroppedProviders(_ providers: [NSItemProvider]) -> Bool { let fileURLType = UTType.fileURL.identifier let supportedProviders = providers.filter { $0.hasItemConformingToTypeIdentifier(fileURLType) } guard !supportedProviders.isEmpty else { return false } for provider in supportedProviders { provider.loadDataRepresentation(forTypeIdentifier: fileURLType) { data, _ in guard let data, let url = NSURL(absoluteURLWithDataRepresentation: data, relativeTo: nil) as URL? else { return } Task { @MainActor in let sourceID = library.addSource(at: url) selectSourceIfNeeded(sourceID) } } } return true } private func selectSourceIfNeeded(_ sourceID: URL) { guard selectedSidebarSelection == nil else { return } selectedSidebarSelection = .allContent(sourceID: sourceID) } private func removeSource(_ sourceID: URL) { let fallbackSourceID = library.sources.first(where: { $0.id != sourceID })?.id library.removeSource(withID: sourceID) if selectedSidebarSelection?.sourceID == sourceID { selectedSidebarSelection = fallbackSourceID.map { .allContent(sourceID: $0) } } if let selectedItemID, currentSelectedItem?.id != selectedItemID { self.selectedItemID = nil } } private func syncSelection(with sourceIDs: [URL]) { if let selectedSidebarSelection, !sourceIDs.contains(selectedSidebarSelection.sourceID) { self.selectedSidebarSelection = sourceIDs.first.map { .allContent(sourceID: $0) } } else if self.selectedSidebarSelection == nil, let firstSourceID = sourceIDs.first { self.selectedSidebarSelection = .allContent(sourceID: firstSourceID) } if let selectedItemID { let itemStillExists = library.sources .flatMap(\.items) .contains(where: { $0.id == selectedItemID }) if !itemStillExists { self.selectedItemID = nil } } } private func saveItem(_ item: MinecraftContentItem) { guard !isPerformingItemAction else { return } let panel = NSSavePanel() panel.canCreateDirectories = true panel.isExtensionHidden = false panel.title = primaryActionTitle(for: item) panel.message = primaryActionSubtitle(for: item) panel.nameFieldStringValue = ContentPackageExporter.suggestedBaseFilename(for: item) panel.allowedContentTypes = [archiveType(for: item)] guard panel.runModal() == .OK, let destinationURL = panel.url else { return } isPerformingItemAction = true library.setItemActionInProgress("Creating \(item.contentType.archiveExtension) file...") Task { do { try await Task.detached(priority: .userInitiated) { try ContentPackageExporter.exportItem(item, to: destinationURL) }.value let finalURL = ContentPackageExporter.finalArchiveURL(for: item, destinationURL: destinationURL) await MainActor.run { isPerformingItemAction = false library.setItemActionSuccess( title: "Created \(finalURL.lastPathComponent)", subtitle: "Ready to move to another device", revealURL: finalURL ) } } catch { await MainActor.run { isPerformingItemAction = false library.setItemActionFailure(error.localizedDescription) } } } } private func shareItem(_ item: MinecraftContentItem, from anchorView: NSView?) { guard !isPerformingItemAction else { return } isPerformingItemAction = true library.setItemActionInProgress("Preparing \(item.contentType.archiveExtension) file...") Task { do { let shareURL = try await Task.detached(priority: .userInitiated) { try ContentPackageExporter.prepareShareFile(for: item) }.value await MainActor.run { isPerformingItemAction = false let presentationView = anchorView ?? NSApp.keyWindow?.contentView guard let presentationView else { library.setItemActionFailure("Could not present the share menu.") return } library.setItemActionSuccess( title: "Share ready", subtitle: shareURL.lastPathComponent, revealURL: shareURL ) let picker = NSSharingServicePicker(items: [shareURL]) let targetRect = anchorView?.bounds ?? presentationView.bounds.insetBy( dx: presentationView.bounds.width / 2, dy: presentationView.bounds.height / 2 ) picker.show(relativeTo: targetRect, of: presentationView, preferredEdge: .minY) } } catch { await MainActor.run { isPerformingItemAction = false library.setItemActionFailure(error.localizedDescription) } } } } private func revealInFinder(_ item: MinecraftContentItem) { NSWorkspace.shared.activateFileViewerSelecting([item.folderURL]) } private func rescanCurrentSource() { guard let sourceID = selectedSidebarSelection?.sourceID else { return } library.rescanSource(withID: sourceID) } private func revealURLInFinder(_ url: URL) { NSWorkspace.shared.activateFileViewerSelecting([url]) } private func archiveType(for item: MinecraftContentItem) -> UTType { UTType(filenameExtension: item.contentType.archiveExtension) ?? .data } } private 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 } } } private 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" } } } private struct SidebarFilter: Identifiable, Hashable { var id: SidebarSelection { selection } let title: String let iconName: String let count: Int let selection: SidebarSelection } private struct SourcesSidebarView: View { let sources: [MinecraftSource] @Binding var selection: SidebarSelection? let footerState: SidebarFooterState let addSourceAction: () -> 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") } } .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: 6) { HStack(spacing: 8) { if state.style == .inProgress { ProgressView() .controlSize(.small) } Text(state.title) .font(.footnote.weight(.semibold)) .foregroundStyle(primaryColor) .lineLimit(2) } if let subtitle = state.subtitle { Text(subtitle) .font(.footnote) .foregroundStyle(.secondary) .lineLimit(3) } 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, 10) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) } private var primaryColor: Color { switch state.style { case .idle, .inProgress: return .primary case .failure: return .red case .success: return .appAccent } } } private struct ItemListColumnView: 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 ItemDetailColumnView: View { let item: MinecraftContentItem? let behaviorPacks: [ContentPackReference] let resourcePacks: [ContentPackReference] let worldsUsingPack: [MinecraftContentItem] let backingPackInstances: [MinecraftContentItem] let isSuspiciousPack: Bool let contents: [DirectoryPreviewEntry] let directoryPreviewLimit: Int let isEmpty: Bool let isPerformingItemAction: Bool let exportTitle: String? let exportAction: () -> Void let revealAction: () -> Void let shareAction: (NSView?) -> Void var body: some View { Group { if isEmpty { Text("Add a source folder to start scanning your Minecraft library") .foregroundStyle(.secondary) } else if let item { ItemDetailView( item: item, behaviorPacks: behaviorPacks, resourcePacks: resourcePacks, worldsUsingPack: worldsUsingPack, backingPackInstances: backingPackInstances, isSuspiciousPack: isSuspiciousPack, contents: contents, directoryPreviewLimit: directoryPreviewLimit ) } else { Text("Select a world or pack to see details") .foregroundStyle(.secondary) } } .toolbar { if item != nil { ToolbarItemGroup { Button(action: exportAction) { Image(systemName: "arrow.down.circle") } .disabled(isPerformingItemAction) .help(exportTitle ?? "Export") Button(action: revealAction) { Image(systemName: "folder") } .disabled(isPerformingItemAction) .help("Reveal in Finder") SharingPickerButton( title: nil, systemImage: "square.and.arrow.up", isEnabled: !isPerformingItemAction ) { anchorView in shareAction(anchorView) } .help("Share") } } } } } 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)" } } private struct ItemDetailView: View { let item: MinecraftContentItem let behaviorPacks: [ContentPackReference] let resourcePacks: [ContentPackReference] let worldsUsingPack: [MinecraftContentItem] let backingPackInstances: [MinecraftContentItem] let isSuspiciousPack: Bool let contents: [DirectoryPreviewEntry] let directoryPreviewLimit: Int @State private var isTechnicalDetailsExpanded = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 18) { LargeItemThumbnailView(iconURL: item.iconURL, contentType: item.contentType) .frame(maxWidth: .infinity) VStack(alignment: .leading, spacing: 6) { Text(item.displayName) .font(.largeTitle.weight(.semibold)) Text(item.contentType.rawValue) .font(.title3) .foregroundStyle(.secondary) } } detailCard { VStack(alignment: .leading, spacing: 14) { Text("Details") .font(.headline) if isSuspiciousPack { Label("Manifest UUID is missing or unreadable for this pack.", systemImage: "exclamationmark.triangle") .font(.subheadline) .foregroundStyle(.orange) } detailValueRow(title: "Size", value: sizeText) detailValueRow(title: item.displayDateLabel, value: displayDateText) if item.contentType == .world { detailValueRow( title: "Last Played", value: item.lastPlayedDate?.formatted(date: .abbreviated, time: .omitted) ?? "Not available" ) } } } if item.contentType == .world, !behaviorPacks.isEmpty || !resourcePacks.isEmpty { detailCard { VStack(alignment: .leading, spacing: 14) { Text("Packs Used") .font(.headline) if !behaviorPacks.isEmpty { packSection(title: "Behavior Packs", packs: behaviorPacks) } if !resourcePacks.isEmpty { packSection(title: "Resource Packs", packs: resourcePacks) } } } } if (item.contentType == .behaviorPack || item.contentType == .resourcePack), !worldsUsingPack.isEmpty { detailCard { VStack(alignment: .leading, spacing: 14) { Text("Used By Worlds") .font(.headline) ForEach(worldsUsingPack) { world in HStack(alignment: .top, spacing: 12) { PackReferenceIconView(iconURL: world.iconURL) VStack(alignment: .leading, spacing: 2) { Text(world.displayName) Text(worldUsageSecondaryText(for: world)) .font(.caption) .foregroundStyle(.secondary) } } } } } } if (item.contentType == .behaviorPack || item.contentType == .resourcePack), !backingPackInstances.isEmpty { detailCard { VStack(alignment: .leading, spacing: 14) { Text("Pack Instances") .font(.headline) ForEach(backingPackInstances) { instance in HStack(alignment: .top, spacing: 12) { PackReferenceIconView(iconURL: instance.iconURL) VStack(alignment: .leading, spacing: 2) { Text(instance.folderName) Text(packInstanceSecondaryText(for: instance)) .font(.caption) .foregroundStyle(.secondary) } } } } } } detailCard { DisclosureGroup(isExpanded: $isTechnicalDetailsExpanded) { VStack(alignment: .leading, spacing: 18) { detailRow(title: "Folder ID", value: item.folderID) detailRow(title: "Folder Path", value: item.folderURL.path) detailRow(title: "Collection Root", value: item.collectionRootURL.path) VStack(alignment: .leading, spacing: 8) { Text("Contents") .font(.caption) .foregroundStyle(.secondary) if contents.isEmpty { Text("No visible files or folders") .foregroundStyle(.secondary) } else { ForEach(contents) { entry in HStack(spacing: 10) { Image(systemName: entry.isDirectory ? "folder" : "doc") .foregroundStyle(.secondary) Text(entry.name) .lineLimit(1) Spacer() } } if contents.count == directoryPreviewLimit { Text("Showing the first \(directoryPreviewLimit) items") .font(.caption) .foregroundStyle(.secondary) } } } } .padding(.top, 8) } label: { HStack { Text("Technical Details") Spacer() } .contentShape(Rectangle()) .onTapGesture { isTechnicalDetailsExpanded.toggle() } } } } .padding(28) .frame(maxWidth: 450, alignment: .leading) } } @ViewBuilder private func detailCard(@ViewBuilder content: () -> Content) -> some View { content() .frame(maxWidth: .infinity, alignment: .leading) .padding(18) .background(.quaternary.opacity(0.45), in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } @ViewBuilder private func packSection(title: String, packs: [ContentPackReference]) -> some View { VStack(alignment: .leading, spacing: 8) { Text(title) .font(.subheadline.weight(.semibold)) ForEach(packs) { pack in HStack(alignment: .top, spacing: 12) { PackReferenceIconView(iconURL: pack.iconURL) VStack(alignment: .leading, spacing: 2) { Text(pack.name) if let secondary = packSecondaryText(pack), !secondary.isEmpty { Text(secondary) .font(.caption) .foregroundStyle(.secondary) } } } } } } @ViewBuilder private func detailRow(title: String, value: String) -> some View { VStack(alignment: .leading, spacing: 4) { Text(title) .font(.caption) .foregroundStyle(.secondary) Text(value) .textSelection(.enabled) } } @ViewBuilder private func detailValueRow(title: String, value: String) -> some View { HStack(alignment: .firstTextBaseline, spacing: 16) { Text(title) .foregroundStyle(.secondary) Spacer() Text(value) .fontWeight(.medium) .multilineTextAlignment(.trailing) } } private var sizeText: String { if let sizeBytes = item.sizeBytes { return ByteCountFormatter.string(fromByteCount: sizeBytes, countStyle: .file) } return item.metadataLoaded ? "Calculating..." : "Loading..." } private var displayDateText: String { item.displayDate.map { $0.formatted(date: .abbreviated, time: .omitted) } ?? "Unknown" } private func packSecondaryText(_ pack: ContentPackReference) -> String? { let components = [pack.version.map { "v\($0)" }, pack.uuid] .compactMap { $0 } return components.isEmpty ? nil : components.joined(separator: " • ") } private func worldUsageSecondaryText(for world: MinecraftContentItem) -> String { let dateText = world.displayDate?.formatted(date: .abbreviated, time: .omitted) ?? "Date unavailable" return "\(world.displayDateLabel) \(dateText)" } private func packInstanceSecondaryText(for instance: MinecraftContentItem) -> String { if instance.folderURL.pathComponents.contains(MinecraftContentType.world.collectionFolderName) { return "Embedded in world copy" } return "Top-level pack folder" } } private struct DirectoryPreviewEntry: Identifiable { let id = UUID() let name: String let isDirectory: Bool } private struct SharingPickerButton: NSViewRepresentable { let title: String? let systemImage: String let isEnabled: Bool let action: (NSView) -> Void func makeCoordinator() -> Coordinator { Coordinator(action: action) } func makeNSView(context: Context) -> NSButton { let button = NSButton() button.target = context.coordinator button.action = #selector(Coordinator.didPressButton(_:)) button.bezelStyle = .texturedRounded update(button) return button } func updateNSView(_ nsView: NSButton, context: Context) { context.coordinator.action = action update(nsView) } private func update(_ button: NSButton) { button.image = NSImage( systemSymbolName: systemImage, accessibilityDescription: title ?? "Share" ) button.imagePosition = title == nil ? .imageOnly : .imageLeading button.title = title ?? "" button.isEnabled = isEnabled } final class Coordinator: NSObject { var action: (NSView) -> Void init(action: @escaping (NSView) -> Void) { self.action = action } @objc func didPressButton(_ sender: NSButton) { action(sender) } } } private struct PackReferenceIconView: View { let iconURL: URL? var body: some View { if let image = loadImage(from: iconURL) { Image(nsImage: image) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 34, height: 34) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { RoundedRectangle(cornerRadius: 8) .fill(.quaternary) .frame(width: 34, height: 34) .overlay( Image(systemName: "shippingbox") .font(.caption) .foregroundStyle(.secondary) ) } } } private struct EmptySourcesView: View { let isDropTargeted: Bool let chooseFolder: () -> Void var body: some View { VStack(spacing: 24) { ZStack { RoundedRectangle(cornerRadius: 24) .strokeBorder(style: StrokeStyle(lineWidth: 2, dash: [10, 10])) .foregroundStyle(isDropTargeted ? Color.appAccent : Color.secondary.opacity(0.25)) .frame(width: 220, height: 160) Image(systemName: "folder.badge.plus") .font(.system(size: 56, weight: .regular)) .foregroundStyle(isDropTargeted ? Color.appAccent : Color.secondary) } VStack(spacing: 8) { Text("Add a Minecraft Source") .font(.title2) Text("Choose a copied Minecraft folder or drop one here to start scanning worlds, packs, and templates.") .foregroundStyle(.secondary) .multilineTextAlignment(.center) .frame(maxWidth: 420) } Button("Choose Minecraft Folder...") { chooseFolder() } .controlSize(.large) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(40) } } private struct ItemThumbnailView: View { let iconURL: URL? var body: some View { if let image = loadImage(from: iconURL) { Image(nsImage: image) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 40, height: 40) .clipShape(RoundedRectangle(cornerRadius: 7)) } else { RoundedRectangle(cornerRadius: 7) .fill(.quaternary) .frame(width: 40, height: 40) .overlay( Image(systemName: "shippingbox") .foregroundStyle(.secondary) ) } } } private struct LargeItemThumbnailView: View { let iconURL: URL? let contentType: MinecraftContentType var body: some View { if let image = loadImage(from: iconURL) { Image(nsImage: image) .resizable() .aspectRatio(image.size, contentMode: .fit) .frame(maxWidth: 420, maxHeight: 340) .clipShape(RoundedRectangle(cornerRadius: 28)) } else { RoundedRectangle(cornerRadius: 28) .fill(.quaternary) .frame(maxWidth: 420, minHeight: 260, maxHeight: 340) .overlay( Image(systemName: fallbackIconName) .font(.system(size: 56)) .foregroundStyle(.secondary) ) } } private var fallbackIconName: String { switch contentType { case .world: return "globe.europe.africa" case .behaviorPack: return "shippingbox" case .resourcePack: return "paintpalette" case .skinPack: return "person.crop.square" case .worldTemplate: return "doc.on.doc" } } } private func loadImage(from url: URL?) -> NSImage? { guard let url else { return nil } return NSImage(contentsOf: url) } private extension Color { static let appAccent = Color("AccentColor") } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }