Add drag-out export from item list

This commit is contained in:
John Burwell 2026-05-29 16:33:13 -05:00
parent d1c7d1de73
commit f6ab43833c
3 changed files with 51 additions and 1 deletions

View File

@ -37,6 +37,7 @@ struct ItemListColumnView<MenuContent: View>: View {
let searchPrompt: String let searchPrompt: String
let chooseFolderAction: () -> Void let chooseFolderAction: () -> Void
let dropAction: ([NSItemProvider]) -> Bool let dropAction: ([NSItemProvider]) -> Bool
let dragProvider: (MinecraftContentItem) -> NSItemProvider
let itemContextMenu: (MinecraftContentItem) -> MenuContent let itemContextMenu: (MinecraftContentItem) -> MenuContent
var body: some View { var body: some View {
@ -51,6 +52,9 @@ struct ItemListColumnView<MenuContent: View>: View {
List(items, selection: $selectedItemID) { item in List(items, selection: $selectedItemID) { item in
ContentRowView(item: item) ContentRowView(item: item)
.tag(item.id) .tag(item.id)
.onDrag {
dragProvider(item)
}
.contextMenu { .contextMenu {
itemContextMenu(item) itemContextMenu(item)
} }

View File

@ -321,6 +321,7 @@ struct ItemListColumnPreviewContainer: View {
searchPrompt: "Search Worlds", searchPrompt: "Search Worlds",
chooseFolderAction: {}, chooseFolderAction: {},
dropAction: { _ in false }, dropAction: { _ in false },
dragProvider: { _ in NSItemProvider() },
itemContextMenu: { item in itemContextMenu: { item in
Button("Reveal \(item.displayName)") {} Button("Reveal \(item.displayName)") {}
} }

View File

@ -74,6 +74,7 @@ struct ContentView: View {
searchPrompt: searchPrompt, searchPrompt: searchPrompt,
chooseFolderAction: pickFolder, chooseFolderAction: pickFolder,
dropAction: handleDroppedProviders(_:), dropAction: handleDroppedProviders(_:),
dragProvider: dragProvider(for:),
itemContextMenu: itemContextMenu(for:) itemContextMenu: itemContextMenu(for:)
) )
.navigationSplitViewColumnWidth(min: 340, ideal: 400, max: 460) .navigationSplitViewColumnWidth(min: 340, ideal: 400, max: 460)
@ -653,7 +654,13 @@ struct ContentView: View {
return false 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) { private func saveItem(_ item: MinecraftContentItem) {
@ -791,6 +798,44 @@ struct ContentView: View {
private func archiveType(for item: MinecraftContentItem) -> UTType { private func archiveType(for item: MinecraftContentItem) -> UTType {
UTType(filenameExtension: item.contentType.archiveExtension) ?? .data 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 { struct ContentView_Previews: PreviewProvider {