|
@@ -44,36 +44,6 @@ enum ScanFileService {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- static func addPage(from window: NSWindow?, completion: @escaping (NSImage?) -> Void) {
|
|
|
|
|
- let hostWindow = window ?? NSApp.keyWindow
|
|
|
|
|
- showSourcePicker(from: hostWindow) { source in
|
|
|
|
|
- guard let source else {
|
|
|
|
|
- completion(nil)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- switch source {
|
|
|
|
|
- case .scanner:
|
|
|
|
|
- startScannerScan(from: hostWindow) { result in
|
|
|
|
|
- switch result {
|
|
|
|
|
- case .success(let image):
|
|
|
|
|
- completion(image)
|
|
|
|
|
- case .failure:
|
|
|
|
|
- completion(nil)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- case .importImage:
|
|
|
|
|
- importImage(from: hostWindow) { result in
|
|
|
|
|
- switch result {
|
|
|
|
|
- case .success(let image):
|
|
|
|
|
- completion(image)
|
|
|
|
|
- case .failure:
|
|
|
|
|
- completion(nil)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private enum ScanSource {
|
|
private enum ScanSource {
|
|
|
case scanner
|
|
case scanner
|
|
|
case importImage
|
|
case importImage
|
|
@@ -334,57 +304,6 @@ enum ScanExporter {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- static func save(pages: [NSImage], from window: NSWindow?) {
|
|
|
|
|
- guard !pages.isEmpty else { return }
|
|
|
|
|
-
|
|
|
|
|
- let format = AppSettings.scanFormat
|
|
|
|
|
- let panel = NSSavePanel()
|
|
|
|
|
- panel.title = "Save Scan"
|
|
|
|
|
- panel.message = "Choose where to save your scanned document."
|
|
|
|
|
- panel.prompt = "Save"
|
|
|
|
|
- panel.canCreateDirectories = true
|
|
|
|
|
-
|
|
|
|
|
- switch format {
|
|
|
|
|
- case .pdf:
|
|
|
|
|
- panel.allowedContentTypes = [.pdf]
|
|
|
|
|
- panel.nameFieldStringValue = "Scan.pdf"
|
|
|
|
|
- case .jpeg:
|
|
|
|
|
- panel.allowedContentTypes = [.jpeg]
|
|
|
|
|
- panel.nameFieldStringValue = pages.count > 1 ? "Scan Page 1.jpg" : "Scan.jpg"
|
|
|
|
|
- case .png:
|
|
|
|
|
- panel.allowedContentTypes = [.png]
|
|
|
|
|
- panel.nameFieldStringValue = pages.count > 1 ? "Scan Page 1.png" : "Scan.png"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let hostWindow = window ?? NSApp.keyWindow
|
|
|
|
|
- let response: NSApplication.ModalResponse
|
|
|
|
|
- if let hostWindow {
|
|
|
|
|
- response = panel.runModal()
|
|
|
|
|
- } else {
|
|
|
|
|
- response = panel.runModal()
|
|
|
|
|
- }
|
|
|
|
|
- guard response == .OK, let url = panel.url else { return }
|
|
|
|
|
-
|
|
|
|
|
- let scaledPages = pages.map { scaleForResolution($0) }
|
|
|
|
|
- let saved: Bool
|
|
|
|
|
- switch format {
|
|
|
|
|
- case .pdf:
|
|
|
|
|
- saved = writePDF(pages: scaledPages, to: url)
|
|
|
|
|
- case .jpeg:
|
|
|
|
|
- saved = pages.count == 1
|
|
|
|
|
- ? writeJPEG(scaledPages[0], to: url)
|
|
|
|
|
- : writeMultipleImages(scaledPages, baseURL: url, format: .jpeg)
|
|
|
|
|
- case .png:
|
|
|
|
|
- saved = pages.count == 1
|
|
|
|
|
- ? writePNG(scaledPages[0], to: url)
|
|
|
|
|
- : writeMultipleImages(scaledPages, baseURL: url, format: .png)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if !saved {
|
|
|
|
|
- showSaveFailed(from: hostWindow)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private static func scaleForResolution(_ image: NSImage) -> NSImage {
|
|
private static func scaleForResolution(_ image: NSImage) -> NSImage {
|
|
|
let targetDPI = AppSettings.scanResolution.dpi
|
|
let targetDPI = AppSettings.scanResolution.dpi
|
|
|
let scale = targetDPI / 300.0
|
|
let scale = targetDPI / 300.0
|
|
@@ -407,37 +326,6 @@ enum ScanExporter {
|
|
|
return document.write(to: url)
|
|
return document.write(to: url)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private static func writeJPEG(_ image: NSImage, to url: URL) -> Bool {
|
|
|
|
|
- guard let tiff = image.tiffRepresentation,
|
|
|
|
|
- let bitmap = NSBitmapImageRep(data: tiff),
|
|
|
|
|
- let data = bitmap.representation(using: .jpeg, properties: [.compressionFactor: 0.9]) else {
|
|
|
|
|
- return false
|
|
|
|
|
- }
|
|
|
|
|
- return (try? data.write(to: url)) != nil
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private static func writePNG(_ image: NSImage, to url: URL) -> Bool {
|
|
|
|
|
- guard let tiff = image.tiffRepresentation,
|
|
|
|
|
- let bitmap = NSBitmapImageRep(data: tiff),
|
|
|
|
|
- let data = bitmap.representation(using: .png, properties: [:]) else {
|
|
|
|
|
- return false
|
|
|
|
|
- }
|
|
|
|
|
- return (try? data.write(to: url)) != nil
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private static func writeMultipleImages(_ pages: [NSImage], baseURL: URL, format: ScanFormat) -> Bool {
|
|
|
|
|
- let directory = baseURL.deletingLastPathComponent()
|
|
|
|
|
- let baseName = baseURL.deletingPathExtension().lastPathComponent
|
|
|
|
|
- let ext = format == .jpeg ? "jpg" : "png"
|
|
|
|
|
-
|
|
|
|
|
- for (index, page) in pages.enumerated() {
|
|
|
|
|
- let pageURL = directory.appendingPathComponent("\(baseName) \(index + 1).\(ext)")
|
|
|
|
|
- let ok = format == .jpeg ? writeJPEG(page, to: pageURL) : writePNG(page, to: pageURL)
|
|
|
|
|
- if !ok { return false }
|
|
|
|
|
- }
|
|
|
|
|
- return true
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private static func showSaveFailed(from window: NSWindow?) {
|
|
private static func showSaveFailed(from window: NSWindow?) {
|
|
|
let alert = NSAlert()
|
|
let alert = NSAlert()
|
|
|
alert.messageText = "Save Failed"
|
|
alert.messageText = "Save Failed"
|
|
@@ -475,8 +363,6 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
|
|
|
private let counterLabel = NSTextField(labelWithString: "")
|
|
private let counterLabel = NSTextField(labelWithString: "")
|
|
|
private let previewContainer = NSView()
|
|
private let previewContainer = NSView()
|
|
|
private let imageView = ScanPreviewImageView()
|
|
private let imageView = ScanPreviewImageView()
|
|
|
- private let addPageButton = ScanFileSecondaryButton(title: "Add Page", symbolName: "plus")
|
|
|
|
|
- private let saveButton = ScanFileSecondaryButton(title: "Save", symbolName: "square.and.arrow.down")
|
|
|
|
|
private let printButton = ScanFilePrintButton()
|
|
private let printButton = ScanFilePrintButton()
|
|
|
|
|
|
|
|
init(pages: [NSImage]) {
|
|
init(pages: [NSImage]) {
|
|
@@ -512,8 +398,6 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
|
|
|
previewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
previewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
|
previewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
previewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
backButton.refreshAppearance()
|
|
backButton.refreshAppearance()
|
|
|
- addPageButton.refreshAppearance()
|
|
|
|
|
- saveButton.refreshAppearance()
|
|
|
|
|
printButton.refreshAppearance()
|
|
printButton.refreshAppearance()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -578,13 +462,9 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
|
|
|
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
|
|
|
|
|
backButton.translatesAutoresizingMaskIntoConstraints = false
|
|
backButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- addPageButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- saveButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
printButton.translatesAutoresizingMaskIntoConstraints = false
|
|
printButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
backButton.onClick = { [weak self] in self?.dismiss() }
|
|
backButton.onClick = { [weak self] in self?.dismiss() }
|
|
|
- addPageButton.onClick = { [weak self] in self?.addPage() }
|
|
|
|
|
- saveButton.onClick = { [weak self] in self?.saveScan() }
|
|
|
|
|
printButton.onClick = { [weak self] in self?.printScan() }
|
|
printButton.onClick = { [weak self] in self?.printScan() }
|
|
|
|
|
|
|
|
previewContainer.addSubview(imageView)
|
|
previewContainer.addSubview(imageView)
|
|
@@ -593,14 +473,6 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
|
|
|
addSubview(counterLabel)
|
|
addSubview(counterLabel)
|
|
|
addSubview(previewContainer)
|
|
addSubview(previewContainer)
|
|
|
|
|
|
|
|
- let actionRow = NSStackView(views: [addPageButton, saveButton])
|
|
|
|
|
- actionRow.orientation = .horizontal
|
|
|
|
|
- actionRow.spacing = 12
|
|
|
|
|
- actionRow.distribution = .fillEqually
|
|
|
|
|
- actionRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- actionRow.setContentHuggingPriority(.required, for: .vertical)
|
|
|
|
|
- actionRow.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
|
|
|
- addSubview(actionRow)
|
|
|
|
|
addSubview(printButton)
|
|
addSubview(printButton)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
@@ -618,19 +490,13 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
|
|
|
previewContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AppTheme.contentPanelInset),
|
|
previewContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AppTheme.contentPanelInset),
|
|
|
previewContainer.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AppTheme.contentPanelInset),
|
|
previewContainer.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AppTheme.contentPanelInset),
|
|
|
previewContainer.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 28),
|
|
previewContainer.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 28),
|
|
|
- previewContainer.bottomAnchor.constraint(equalTo: actionRow.topAnchor, constant: -20),
|
|
|
|
|
|
|
+ previewContainer.bottomAnchor.constraint(equalTo: printButton.topAnchor, constant: -24),
|
|
|
|
|
|
|
|
imageView.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 12),
|
|
imageView.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 12),
|
|
|
imageView.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -12),
|
|
imageView.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -12),
|
|
|
imageView.topAnchor.constraint(equalTo: previewContainer.topAnchor, constant: 12),
|
|
imageView.topAnchor.constraint(equalTo: previewContainer.topAnchor, constant: 12),
|
|
|
imageView.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor, constant: -12),
|
|
imageView.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor, constant: -12),
|
|
|
|
|
|
|
|
- actionRow.leadingAnchor.constraint(equalTo: leadingAnchor, constant: AppTheme.contentPanelInset),
|
|
|
|
|
- actionRow.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -AppTheme.contentPanelInset),
|
|
|
|
|
- actionRow.bottomAnchor.constraint(equalTo: printButton.topAnchor, constant: -16),
|
|
|
|
|
- addPageButton.heightAnchor.constraint(equalToConstant: 44),
|
|
|
|
|
- saveButton.heightAnchor.constraint(equalToConstant: 44),
|
|
|
|
|
-
|
|
|
|
|
printButton.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
printButton.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
printButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -28),
|
|
printButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -28),
|
|
|
printButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 220),
|
|
printButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 220),
|
|
@@ -657,17 +523,6 @@ final class ScanFileOverlayView: NSView, AppearanceRefreshable {
|
|
|
counterLabel.isHidden = false
|
|
counterLabel.isHidden = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private func addPage() {
|
|
|
|
|
- ScanFileService.addPage(from: window) { [weak self] image in
|
|
|
|
|
- guard let self, let image else { return }
|
|
|
|
|
- appendPages([image])
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func saveScan() {
|
|
|
|
|
- ScanExporter.save(pages: pages, from: window)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private func printScan() {
|
|
private func printScan() {
|
|
|
guard !pages.isEmpty else { return }
|
|
guard !pages.isEmpty else { return }
|
|
|
for (index, page) in pages.enumerated() {
|
|
for (index, page) in pages.enumerated() {
|
|
@@ -724,72 +579,6 @@ private final class ScanFileToolbarButton: NSControl, AppearanceRefreshable {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-private final class ScanFileSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
|
|
- var onClick: (() -> Void)?
|
|
|
|
|
-
|
|
|
|
|
- private let titleLabel = NSTextField(labelWithString: "")
|
|
|
|
|
- private let iconView = NSImageView()
|
|
|
|
|
- private var hoverTracker: HoverTracker?
|
|
|
|
|
- private var isHovered = false
|
|
|
|
|
-
|
|
|
|
|
- init(title: String, symbolName: String) {
|
|
|
|
|
- super.init(frame: .zero)
|
|
|
|
|
- wantsLayer = true
|
|
|
|
|
- layer?.cornerRadius = 14
|
|
|
|
|
-
|
|
|
|
|
- titleLabel.stringValue = title
|
|
|
|
|
- titleLabel.font = AppTheme.semiboldFont(size: 14)
|
|
|
|
|
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
-
|
|
|
|
|
- if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
|
|
|
|
|
- let config = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
|
|
|
|
|
- iconView.image = image.withSymbolConfiguration(config)
|
|
|
|
|
- }
|
|
|
|
|
- iconView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
-
|
|
|
|
|
- addSubview(titleLabel)
|
|
|
|
|
- addSubview(iconView)
|
|
|
|
|
- NSLayoutConstraint.activate([
|
|
|
|
|
- heightAnchor.constraint(equalToConstant: 44),
|
|
|
|
|
- iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
|
|
|
|
- iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
|
|
- titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 8),
|
|
|
|
|
- titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
|
|
- titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16),
|
|
|
|
|
- ])
|
|
|
|
|
-
|
|
|
|
|
- hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
|
|
- self?.setHovered(hovering)
|
|
|
|
|
- }
|
|
|
|
|
- refreshAppearance()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @available(*, unavailable)
|
|
|
|
|
- required init?(coder: NSCoder) { nil }
|
|
|
|
|
-
|
|
|
|
|
- func refreshAppearance() {
|
|
|
|
|
- layer?.backgroundColor = (isHovered ? AppTheme.elevatedBackground.highlight(withLevel: 0.05) ?? AppTheme.elevatedBackground : AppTheme.elevatedBackground).cgColor
|
|
|
|
|
- layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
|
|
- layer?.borderWidth = 1.5
|
|
|
|
|
- titleLabel.textColor = AppTheme.textPrimary
|
|
|
|
|
- iconView.contentTintColor = AppTheme.textPrimary
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func setHovered(_ hovering: Bool) {
|
|
|
|
|
- isHovered = hovering
|
|
|
|
|
- refreshAppearance()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override func mouseUp(with event: NSEvent) {
|
|
|
|
|
- guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
|
|
- onClick?()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override func resetCursorRects() {
|
|
|
|
|
- addCursorRect(bounds, cursor: .pointingHand)
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
private final class ScanFilePrintButton: NSControl, AppearanceRefreshable {
|
|
private final class ScanFilePrintButton: NSControl, AppearanceRefreshable {
|
|
|
var onClick: (() -> Void)?
|
|
var onClick: (() -> Void)?
|
|
|
|
|
|