From f6ab43833cbefea95fcd395c46beee142cec3a01 Mon Sep 17 00:00:00 2001 From: John Burwell Date: Fri, 29 May 2026 16:33:13 -0500 Subject: [PATCH] Add drag-out export from item list --- .../UI/List/ItemListColumnViews.swift | 4 ++ .../UI/Preview/PreviewFixtures.swift | 1 + .../UI/Root/ContentView.swift | 47 ++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/World Manager for Minecraft/UI/List/ItemListColumnViews.swift b/World Manager for Minecraft/UI/List/ItemListColumnViews.swift index 4e13c28..45d283b 100644 --- a/World Manager for Minecraft/UI/List/ItemListColumnViews.swift +++ b/World Manager for Minecraft/UI/List/ItemListColumnViews.swift @@ -37,6 +37,7 @@ struct ItemListColumnView: View { let searchPrompt: String let chooseFolderAction: () -> Void let dropAction: ([NSItemProvider]) -> Bool + let dragProvider: (MinecraftContentItem) -> NSItemProvider let itemContextMenu: (MinecraftContentItem) -> MenuContent var body: some View { @@ -51,6 +52,9 @@ struct ItemListColumnView: View { List(items, selection: $selectedItemID) { item in ContentRowView(item: item) .tag(item.id) + .onDrag { + dragProvider(item) + } .contextMenu { itemContextMenu(item) } diff --git a/World Manager for Minecraft/UI/Preview/PreviewFixtures.swift b/World Manager for Minecraft/UI/Preview/PreviewFixtures.swift index 23be141..421f19f 100644 --- a/World Manager for Minecraft/UI/Preview/PreviewFixtures.swift +++ b/World Manager for Minecraft/UI/Preview/PreviewFixtures.swift @@ -321,6 +321,7 @@ struct ItemListColumnPreviewContainer: View { searchPrompt: "Search Worlds", chooseFolderAction: {}, dropAction: { _ in false }, + dragProvider: { _ in NSItemProvider() }, itemContextMenu: { item in Button("Reveal \(item.displayName)") {} } diff --git a/World Manager for Minecraft/UI/Root/ContentView.swift b/World Manager for Minecraft/UI/Root/ContentView.swift index 15eaf46..4bf7e80 100644 --- a/World Manager for Minecraft/UI/Root/ContentView.swift +++ b/World Manager for Minecraft/UI/Root/ContentView.swift @@ -74,6 +74,7 @@ struct ContentView: View { searchPrompt: searchPrompt, chooseFolderAction: pickFolder, dropAction: handleDroppedProviders(_:), + dragProvider: dragProvider(for:), itemContextMenu: itemContextMenu(for:) ) .navigationSplitViewColumnWidth(min: 340, ideal: 400, max: 460) @@ -653,7 +654,13 @@ struct ContentView: View { return false } - return source.availability == .available + return source.availability == .available && source.capabilities.canExportPortablePackages + } + + private func sourceForItem(_ item: MinecraftContentItem) -> MinecraftSource? { + library.visibleSources.first(where: { source in + source.items.contains(where: { $0.id == item.id }) + }) } private func saveItem(_ item: MinecraftContentItem) { @@ -791,6 +798,44 @@ struct ContentView: View { private func archiveType(for item: MinecraftContentItem) -> UTType { UTType(filenameExtension: item.contentType.archiveExtension) ?? .data } + + private func dragProvider(for item: MinecraftContentItem) -> NSItemProvider { + let provider = NSItemProvider() + let contentType = archiveType(for: item) + provider.suggestedName = itemActionService.suggestedArchiveFilename(for: item) + + provider.registerFileRepresentation( + forTypeIdentifier: contentType.identifier, + fileOptions: [], + visibility: .all + ) { completion in + guard let source = sourceForItem(item), areFileActionsEnabled(for: item) else { + completion(nil, false, SourceAccessError.accessFailed(reason: "This item is not currently available for export.")) + return nil + } + + let task = Task { + do { + let representation = try await library.externalRepresentation( + for: item, + in: source, + preferredKind: .portablePackage + ) + completion(representation.url, representation.isTemporary, nil) + } catch { + completion(nil, false, error) + } + } + + let progress = Progress(totalUnitCount: 1) + progress.cancellationHandler = { + task.cancel() + } + return progress + } + + return provider + } } struct ContentView_Previews: PreviewProvider {