Give mixed selections something to do

A Drag of different kinds showed an empty wheel, because the catalog only
answers for a single category. Filee's answer is a mixed profile, so add
Zip (one archive, flat names) and Merge (non-PDFs are converted to PDF
first); a multi-file drag of one kind gets Zip too.
This commit is contained in:
2026-10-20 01:30:42 +08:00
parent 2d031e58a4
commit a01d5def89
8 changed files with 295 additions and 41 deletions
+40 -20
View File
@@ -59,7 +59,12 @@ enum WheelBuilder {
private static func conversionItems(urls: [URL]) -> [WheelItem] {
let categories = urls.map { FileKind.category(of: $0) }
let formats = ConversionCatalog.outputs(for: categories)
guard let category = categories.first, !formats.isEmpty else { return [] }
// A mixed drag has no single output format, but it can still become one
// file, so the convert wheel offers the combine tools instead of
// vanishing (this is Filee's mixed profile).
guard let category = categories.first, !formats.isEmpty else {
return combineItems(urls: urls)
}
return formats.compactMap { format in
guard !ConversionCatalog.isRedundant(format, urls: urls) else { return nil }
@@ -77,26 +82,41 @@ enum WheelBuilder {
private static func toolItems(urls: [URL]) -> [WheelItem] {
let categories = Set(urls.map { FileKind.category(of: $0) })
guard categories.count == 1, let category = categories.first else { return [] }
return ToolID.tools(for: category).compactMap { tool in
guard urls.count >= tool.minimumFileCount else { return nil }
var availability = Availability.ok
if tool.producesArtifacts, urls.count > 1 {
availability = .ok
}
if requiresFFmpeg(tool), !Toolchain.hasFFmpeg {
availability = .unavailable("Needs ffmpeg — brew install ffmpeg")
}
return WheelItem(
id: "tool-\(tool.rawValue)",
payload: .tool(tool),
title: tool.title,
subtitle: availability.isAvailable ? nil : availability.reason,
symbol: tool.symbol,
availability: availability
)
if categories.count == 1, let category = categories.first,
category != .folder, category != .unknown {
var tools = ToolID.tools(for: category)
// Any multi-file selection can be packed into one archive.
if urls.count >= 2 { tools.append(.createArchive) }
return tools.compactMap { item(for: $0, urls: urls) }
}
return combineItems(urls: urls)
}
/// The slices a selection of mixed kinds can still act on.
private static func combineItems(urls: [URL]) -> [WheelItem] {
let subtitles: [ToolID: String] = [
.mergePDF: "One PDF",
.createArchive: "One ZIP"
]
return ToolID.combineTools(for: urls).compactMap { tool in
item(for: tool, urls: urls, subtitle: subtitles[tool])
}
}
private static func item(for tool: ToolID, urls: [URL], subtitle: String? = nil) -> WheelItem? {
guard urls.count >= tool.minimumFileCount else { return nil }
var availability = Availability.ok
if requiresFFmpeg(tool), !Toolchain.hasFFmpeg {
availability = .unavailable("Needs ffmpeg — brew install ffmpeg")
}
return WheelItem(
id: "tool-\(tool.rawValue)",
payload: .tool(tool),
title: tool.title,
subtitle: availability.isAvailable ? subtitle : availability.reason,
symbol: tool.symbol,
availability: availability
)
}
private static func requiresFFmpeg(_ tool: ToolID) -> Bool {