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:
@@ -20,7 +20,7 @@ enum ToolID: String, CaseIterable, Identifiable, Codable {
|
||||
// Subtitles
|
||||
case shiftSubtitle
|
||||
// Archives
|
||||
case extractArchive, listArchive
|
||||
case extractArchive, listArchive, createArchive
|
||||
// Media metadata
|
||||
case mediaInfo
|
||||
|
||||
@@ -65,6 +65,7 @@ enum ToolID: String, CaseIterable, Identifiable, Codable {
|
||||
case .shiftSubtitle: return "Shift Time"
|
||||
case .extractArchive: return "Extract"
|
||||
case .listArchive: return "List Contents"
|
||||
case .createArchive: return "Zip"
|
||||
case .mediaInfo: return "Info"
|
||||
}
|
||||
}
|
||||
@@ -100,6 +101,7 @@ enum ToolID: String, CaseIterable, Identifiable, Codable {
|
||||
case .shiftSubtitle: return "clock.arrow.circlepath"
|
||||
case .extractArchive: return "archivebox"
|
||||
case .listArchive: return "list.bullet.rectangle"
|
||||
case .createArchive: return "doc.zipper"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +124,7 @@ enum ToolID: String, CaseIterable, Identifiable, Codable {
|
||||
return .spreadsheet
|
||||
case .shiftSubtitle:
|
||||
return .subtitle
|
||||
case .extractArchive, .listArchive:
|
||||
case .extractArchive, .listArchive, .createArchive:
|
||||
return .archive
|
||||
case .mediaInfo:
|
||||
return .video
|
||||
@@ -131,7 +133,10 @@ enum ToolID: String, CaseIterable, Identifiable, Codable {
|
||||
|
||||
/// Tools that only make sense for a multi-file selection.
|
||||
var requiresMultipleFiles: Bool {
|
||||
self == .mergePDF || self == .imagesToPDF || self == .collageImages
|
||||
switch self {
|
||||
case .mergePDF, .imagesToPDF, .collageImages, .createArchive: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var minimumFileCount: Int { requiresMultipleFiles ? 2 : 1 }
|
||||
@@ -151,6 +156,26 @@ enum ToolID: String, CaseIterable, Identifiable, Codable {
|
||||
static func tools(for category: FileCategory) -> [ToolID] {
|
||||
allCases.filter { $0.category == category }
|
||||
}
|
||||
|
||||
/// Tools that accept a drag of mixed kinds. When a selection cannot become
|
||||
/// one format, there is still something useful to do with it: Filee calls
|
||||
/// this a mixed profile, and it is why its donut is never empty.
|
||||
static func combineTools(for urls: [URL]) -> [ToolID] {
|
||||
guard urls.count >= 2 else { return [] }
|
||||
var tools: [ToolID] = []
|
||||
if urls.allSatisfy(canBecomePDF) { tools.append(.mergePDF) }
|
||||
tools.append(.createArchive)
|
||||
return tools
|
||||
}
|
||||
|
||||
/// Kinds that `Converter` knows how to turn into a PDF, which is what
|
||||
/// ``mergePDF`` needs from anything that is not a PDF already.
|
||||
static func canBecomePDF(_ url: URL) -> Bool {
|
||||
switch FileKind.category(of: url) {
|
||||
case .image, .pdf, .document, .slides, .spreadsheet, .ebook: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One editable field in a tool's options form.
|
||||
@@ -256,6 +281,8 @@ extension ToolID {
|
||||
]
|
||||
case .shiftSubtitle:
|
||||
return [ToolField("offset", "Shift by", .decimal(min: -600, max: 600), default: "1", unit: "s")]
|
||||
case .createArchive:
|
||||
return [ToolField("name", "Archive name", .text, default: "Archive", help: "Saved beside the first file")]
|
||||
case .listArchive, .extractArchive, .wordCount, .mediaInfo, .readQR, .stripMetadata,
|
||||
.grayscaleImage, .normalizeAudio, .extractPDFText, .documentToPDF, .sheetsToCSV,
|
||||
.muteVideo, .mergePDF, .imagesToPDF:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user