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:
@@ -58,6 +58,67 @@ enum ArchiveConverter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Packs a whole selection into one ZIP, the way Finder's *Compress* does.
|
||||
/// Inputs that live in the same folder keep their relative names; a
|
||||
/// selection spanning folders is staged through symlinks so the archive
|
||||
/// still has flat, readable entries instead of absolute paths.
|
||||
static func createArchive(
|
||||
from inputs: [URL],
|
||||
at destination: URL,
|
||||
cancellation: Cancellation? = nil,
|
||||
progress: ((Double) -> Void)? = nil
|
||||
) throws {
|
||||
guard !inputs.isEmpty else {
|
||||
throw ConversionError.unsupported("There is nothing to archive.")
|
||||
}
|
||||
try cancellation?.check()
|
||||
progress?(0.1)
|
||||
|
||||
let folders = Set(inputs.map { $0.deletingLastPathComponent().path })
|
||||
if folders.count == 1, let folder = inputs.first?.deletingLastPathComponent() {
|
||||
try Shell.run(
|
||||
Toolchain.zip,
|
||||
["-r", "-X", destination.path] + inputs.map { $0.lastPathComponent },
|
||||
currentDirectory: folder,
|
||||
cancellation: cancellation
|
||||
)
|
||||
progress?(1)
|
||||
return
|
||||
}
|
||||
|
||||
let staging = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("konverter-zip-\(UUID().uuidString)", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: staging) }
|
||||
|
||||
var names: [String] = []
|
||||
for input in inputs {
|
||||
var name = input.lastPathComponent
|
||||
var index = 2
|
||||
while names.contains(name) {
|
||||
let ext = input.pathExtension
|
||||
let base = input.deletingPathExtension().lastPathComponent
|
||||
name = ext.isEmpty ? "\(base) \(index)" : "\(base) \(index).\(ext)"
|
||||
index += 1
|
||||
}
|
||||
names.append(name)
|
||||
try FileManager.default.createSymbolicLink(
|
||||
at: staging.appendingPathComponent(name),
|
||||
withDestinationURL: input
|
||||
)
|
||||
}
|
||||
|
||||
// `zip` follows symlinks unless asked for `-y`, so the archive holds
|
||||
// the real files rather than links to them.
|
||||
try Shell.run(
|
||||
Toolchain.zip,
|
||||
["-r", "-X", destination.path] + names,
|
||||
currentDirectory: staging,
|
||||
cancellation: cancellation
|
||||
)
|
||||
progress?(1)
|
||||
}
|
||||
|
||||
/// Expands an archive into `directory`, returning the folder that callers
|
||||
/// should reveal.
|
||||
@discardableResult
|
||||
@@ -117,10 +178,10 @@ enum ArchiveConverter {
|
||||
case "tar", "tgz":
|
||||
return try Shell.run(Toolchain.tar, ["-tf", source.path]).stdout
|
||||
case "gz":
|
||||
let size = (try? FileManager.default.attributesOfItem(atPath: source.path)[.size] as? Int) ?? 0
|
||||
let size = (try? FileManager.default.attributesOfItem(atPath: source.path))?[.size] as? Int ?? 0
|
||||
return """
|
||||
GZIP stream
|
||||
Compressed size: \(ByteCountFormatter.string(fromByteCount: Int64(size ?? 0), countStyle: .file))
|
||||
Compressed size: \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file))
|
||||
Inner name: \(source.deletingPathExtension().lastPathComponent)
|
||||
"""
|
||||
default:
|
||||
|
||||
@@ -207,7 +207,37 @@ enum ToolRunner {
|
||||
for: first.deletingLastPathComponent()
|
||||
.appendingPathComponent(first.deletingPathExtension().lastPathComponent + " merged.pdf")
|
||||
)
|
||||
try PDFConverter.merge(inputs, at: destination, cancellation: cancellation)
|
||||
// A mixed selection is merged by turning each non-PDF into a PDF
|
||||
// first, so "3 photos + 2 PDFs" is one drop instead of two jobs.
|
||||
var staged: [URL] = []
|
||||
let staging = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("konverter-merge-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { if !staged.isEmpty { try? FileManager.default.removeItem(at: staging) } }
|
||||
|
||||
var sources: [URL] = []
|
||||
for (index, input) in inputs.enumerated() {
|
||||
try cancellation?.check()
|
||||
if FileKind.category(of: input) == .pdf {
|
||||
sources.append(input)
|
||||
continue
|
||||
}
|
||||
if staged.isEmpty {
|
||||
try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true)
|
||||
}
|
||||
let name = String(format: "%03d", index) + "-"
|
||||
+ input.deletingPathExtension().lastPathComponent + ".pdf"
|
||||
let temp = staging.appendingPathComponent(name)
|
||||
try Converter.convert(
|
||||
source: input,
|
||||
to: .pdf,
|
||||
at: temp,
|
||||
cancellation: cancellation,
|
||||
progress: { fraction in progress?(fraction * 0.5) }
|
||||
)
|
||||
staged.append(temp)
|
||||
sources.append(temp)
|
||||
}
|
||||
try PDFConverter.merge(sources, at: destination, cancellation: cancellation)
|
||||
progress?(1)
|
||||
return ToolOutcome(outputs: [destination])
|
||||
|
||||
@@ -377,6 +407,21 @@ enum ToolRunner {
|
||||
results.append(MediaInfo.describe(source))
|
||||
}
|
||||
return ToolOutcome(message: results.joined(separator: "\n\n"), messageTitle: "Info")
|
||||
|
||||
case .createArchive:
|
||||
let folder = inputs[0].deletingLastPathComponent()
|
||||
let name = params.string("name", default: "Archive")
|
||||
let base = name.hasSuffix(".zip") ? String(name.dropLast(4)) : name
|
||||
let destination = OutputNaming.availableURL(
|
||||
for: folder.appendingPathComponent(base).appendingPathExtension("zip")
|
||||
)
|
||||
try ArchiveConverter.createArchive(
|
||||
from: inputs,
|
||||
at: destination,
|
||||
cancellation: cancellation,
|
||||
progress: progress
|
||||
)
|
||||
return ToolOutcome(outputs: [destination])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user