diff --git a/Clop.xcodeproj/project.pbxproj b/Clop.xcodeproj/project.pbxproj index 21bec68..9e4e562 100644 --- a/Clop.xcodeproj/project.pbxproj +++ b/Clop.xcodeproj/project.pbxproj @@ -239,7 +239,6 @@ C73802012BD1B804001BEE6C /* bin.tar.lrz */ = {isa = PBXFileReference; lastKnownFileType = file; path = bin.tar.lrz; sourceTree = ""; }; C73802042BD1B81A001BEE6C /* lrzip */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = lrzip; sourceTree = SOURCE_ROOT; }; C73802062BDCD76D001BEE6C /* CompareView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompareView.swift; sourceTree = ""; }; - C744D07D2B444F6B003D77DE /* Lowtech */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Lowtech; path = ../Lowtech; sourceTree = ""; }; C75FD1EB2AF4324C000B426B /* Clop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Clop.app; sourceTree = BUILT_PRODUCTS_DIR; }; C75FD1FD2AF434FE000B426B /* FinderOptimiser-setapp.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "FinderOptimiser-setapp.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; C75FD20C2AF4351D000B426B /* ClopCLI-setapp */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "ClopCLI-setapp"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -275,6 +274,7 @@ C7AB662B2883015A0041BEC8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; C7AB662D2883015A0041BEC8 /* Clop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Clop.entitlements; sourceTree = ""; }; C7AB663A288460F30041BEC8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + C7AD8E8A2C2463CE008E4182 /* Lowtech */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Lowtech; path = ../Lowtech; sourceTree = ""; }; C7C066702AF620A8004237F8 /* ClopCLI-setapp.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "ClopCLI-setapp.entitlements"; sourceTree = ""; }; C7C066712AF620A8004237F8 /* FinderOptimiser-setapp.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "FinderOptimiser-setapp.entitlements"; sourceTree = ""; }; C7C066722AF620A8004237F8 /* Setapp.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Setapp.entitlements; sourceTree = ""; }; @@ -379,7 +379,7 @@ C7AB6618288301590041BEC8 = { isa = PBXGroup; children = ( - C744D07D2B444F6B003D77DE /* Lowtech */, + C7AD8E8A2C2463CE008E4182 /* Lowtech */, C70B5F302AC029DA00345739 /* Shared.swift */, C7C0666F2AF62055004237F8 /* Setapp */, C7AB6623288301590041BEC8 /* Clop */, diff --git a/Clop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Clop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9ac0c91..15c542d 100644 --- a/Clop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Clop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "a62862c99f5bcb28fd78617fab1a5fe29607c06c", - "version" : "8.28.0" + "revision" : "08862789e1cbba7a9561bed69832a9306f339cd3", + "version" : "8.29.1" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "41847a58cdef7506b257591fcca6f9495df591d4", - "version" : "2.6.2" + "revision" : "b456fd404954a9e13f55aa0c88cd5a40b8399638", + "version" : "2.6.3" } }, { diff --git a/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist b/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist index f8a57fa..6a1f5aa 100644 --- a/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -17,26 +17,26 @@ ClopCLI-setapp.xcscheme_^#shared#^_ orderHint - 5 + 2 ClopCLI.xcscheme_^#shared#^_ orderHint - 2 + 5 Example (Playground) 1.xcscheme isShown orderHint - 13 + 10 Example (Playground) 2.xcscheme isShown orderHint - 14 + 11 Example (Playground) 3.xcscheme @@ -85,7 +85,7 @@ isShown orderHint - 9 + 6 FinderOptimiser copy.xcscheme_^#shared#^_ @@ -112,14 +112,14 @@ isShown orderHint - 11 + 8 SwiftDate (Playground) 2.xcscheme isShown orderHint - 12 + 9 SwiftDate (Playground) 3.xcscheme @@ -168,7 +168,7 @@ isShown orderHint - 10 + 7 SuppressBuildableAutocreation diff --git a/Clop/ClopApp.swift b/Clop/ClopApp.swift index ce810ce..3a8f425 100644 --- a/Clop/ClopApp.swift +++ b/Clop/ClopApp.swift @@ -677,8 +677,10 @@ class AppDelegate: AppDelegateParent { shouldHandle: shouldHandleVideo(event:), cancel: cancelVideoOptimisation(path:) ) { event in - let video = Video(path: FilePath(event.path)) Task.init { + await FileOptimisationWatcher.waitForModificationDateToSettle(event.path) + + let video = Video(path: FilePath(event.path)) try? await optimiseVideo(video, debounceMS: debounceMS, source: Defaults[.videoDirs].filter { event.path.starts(with: $0) }.max(by: \.count)) } } @@ -690,8 +692,10 @@ class AppDelegate: AppDelegateParent { shouldHandle: shouldHandleImage(event:), cancel: cancelImageOptimisation(path:) ) { event in - guard let img = Image(path: FilePath(event.path), retinaDownscaled: false) else { return } Task.init { + await FileOptimisationWatcher.waitForModificationDateToSettle(event.path) + + guard let img = Image(path: FilePath(event.path), retinaDownscaled: false) else { return } try? await optimiseImage(img, debounceMS: debounceMS, source: Defaults[.imageDirs].filter { event.path.starts(with: $0) }.max(by: \.count)) } } @@ -703,8 +707,10 @@ class AppDelegate: AppDelegateParent { shouldHandle: shouldHandlePDF(event:), cancel: cancelPDFOptimisation(path:) ) { event in - guard let path = event.path.existingFilePath else { return } Task.init { + await FileOptimisationWatcher.waitForModificationDateToSettle(event.path) + + guard let path = event.path.existingFilePath else { return } try? await optimisePDF(PDF(path), debounceMS: debounceMS, source: Defaults[.pdfDirs].filter { event.path.starts(with: $0) }.max(by: \.count)) } } @@ -1012,6 +1018,38 @@ class FileOptimisationWatcher { watching = true } + static func waitForModificationDateToSettle(_ path: String) async { + guard let attrs = try? fm.attributesOfItem(atPath: path), let date = attrs[.modificationDate] as? Date else { + log.warning("Failed to get modification date of \(path)") + return + } + + log.debug("Waiting for modification date of \(path) to settle") + log.debug("Initial modification date: \(date)") + var lastDate = date + while true { + do { + try await Task.sleep(nanoseconds: 300_000_000) // 300ms + } catch { + log.error("Failed to sleep: \(error)") + return + } + + guard let attrs = try? fm.attributesOfItem(atPath: path), let date = attrs[.modificationDate] as? Date else { + log.warning("Failed to get modification date of \(path)") + return + } + + guard date != lastDate else { + log.debug("Modification date of \(path) settled at \(date)") + return + } + + log.debug("Modification date of \(path) is still changing: \(lastDate) -> \(date)") + lastDate = date + } + } + func hasSpuriousEvent(_ event: EonilFSEventsEvent) -> Bool { guard withinSafeMeasureTime, !justAddedFiles.isEmpty else { return false @@ -1108,11 +1146,7 @@ class FileOptimisationWatcher { @MainActor func proLimitsReached(url: URL? = nil) {} #endif -#if DEBUG - let floatingResultsWindow = OSDWindow(swiftuiView: FloatingResultContainer().any, level: .floating, canScreenshot: true, allowsMouse: true) -#else - let floatingResultsWindow = OSDWindow(swiftuiView: FloatingResultContainer().any, level: .floating, canScreenshot: false, allowsMouse: true) -#endif +let floatingResultsWindow = OSDWindow(swiftuiView: FloatingResultContainer().any, level: .floating, canScreenshot: Defaults[.allowClopToAppearInScreenshots], allowsMouse: true) var clipboardWatcher: Timer? var pbChangeCount = NSPasteboard.general.changeCount let THUMB_SIZE = CGSize(width: 300, height: 220) diff --git a/Clop/CompareView.swift b/Clop/CompareView.swift index 71fabab..893cf3b 100644 --- a/Clop/CompareView.swift +++ b/Clop/CompareView.swift @@ -4,6 +4,7 @@ import Lowtech import PDFKit import SwiftUI +@MainActor struct LoopingVideoPlayer: View { init(videoURL: URL) { let asset = AVAsset(url: videoURL) @@ -23,121 +24,156 @@ struct LoopingVideoPlayer: View { private var playerLooper: AVPlayerLooper } -struct PDFKitView: NSViewRepresentable { - init(url: URL, scale: CGFloat = 1.0) { - self.url = url - self.scale = scale +class PDFDelegate: NSObject, PDFViewDelegate { + init(otherPDFView: PDFView) { + self.otherPDFView = otherPDFView } - open class Coordinator: NSObject { - var scaleFactor: CGFloat = 1.0 + let otherPDFView: PDFView + + func pdfViewPerformGo(toPage sender: PDFView) { + guard let page = sender.currentPage, page != otherPDFView.currentPage else { return } + otherPDFView.go(to: page) } +} + +struct PDFKitView: NSViewRepresentable { + static var pdfViewCache = [URL: PDFView]() let url: URL - var scale: CGFloat = 1.0 - func makeCoordinator() -> Coordinator { - Coordinator() + static func clearCache(for urls: [URL]) { + for url in urls { + pdfViewCache.removeValue(forKey: url) + } } func makeNSView(context: Context) -> PDFView { + if let pdfView = Self.pdfViewCache[url] { + return pdfView + } + let pdfView = PDFView() - pdfView.setFrameSize(NSSize(width: 300, height: 300)) - pdfView.displayMode = .singlePageContinuous + pdfView.setFrameSize(NSSize(width: COMPARISON_VIEW_SIZE, height: COMPARISON_VIEW_SIZE)) + pdfView.displayMode = .singlePage pdfView.document = PDFDocument(url: url) - if let page = pdfView.document?.page(at: 0) { - let pageBounds = page.bounds(for: pdfView.displayBox) - pdfView.scaleFactor = 270 / pageBounds.width - context.coordinator.scaleFactor = pdfView.scaleFactor - } + pdfView.autoScales = true + + Self.pdfViewCache[url] = pdfView return pdfView } - func updateNSView(_ pdfView: PDFView, context: Context) { - pdfView.scaleFactor = scale * context.coordinator.scaleFactor - } + func updateNSView(_ pdfView: PDFView, context: Context) {} - private var scaleFactor = 1.0 } +let COMPARISON_VIEW_SIZE: CGFloat = 500 + /// A view that allows the user to preview a comparison of the optimised and original image/video/PDF. struct CompareView: View { @ObservedObject var optimiser: Optimiser @ObservedObject var km = KM - var body: some View { - VStack { - GeometryReader { proxy in - HStack { - if let url = optimiser.url, let originalURL = optimiser.originalURL { - preview(url: originalURL, title: "Original", bytes: optimiser.oldBytes, size: optimiser.oldSize) { - switch optimiser.type { - case .video: - LoopingVideoPlayer(videoURL: originalURL) - .scaledToFit() - case .image: - SwiftUI.Image(nsImage: NSImage(contentsOf: originalURL) ?? .lowtech) - .resizable() - .scaledToFit() - case .pdf: - PDFKitView(url: originalURL) - .scaledToFit() - default: - EmptyView() - } + @State var pdfPage = 1.0 + + var previewStack: some View { + GeometryReader { proxy in + HStack { + if let url = optimiser.url, let originalURL = optimiser.comparisonOriginalURL { + preview(url: originalURL, title: "Original", bytes: optimiser.oldBytes, size: optimiser.oldSize) { + switch optimiser.type { + case .video: + LoopingVideoPlayer(videoURL: originalURL) + case .image: + SwiftUI.Image(nsImage: NSImage(contentsOf: originalURL) ?? .lowtech) + .resizable() + .scaledToFit() + case .pdf: + PDFKitView(url: originalURL) + .allowsHitTesting(false) + default: + EmptyView() } - preview(url: url, title: "Optimised", bytes: optimiser.newBytes, size: optimiser.newSize) { - switch optimiser.type { - case .video: - LoopingVideoPlayer(videoURL: url) - .scaledToFit() - case .image: - SwiftUI.Image(nsImage: NSImage(contentsOf: url) ?? .lowtech) - .resizable() - .scaledToFit() - case .pdf: - PDFKitView(url: url) - .scaledToFit() - default: - EmptyView() - } + } + preview(url: url, title: "Optimised", bytes: optimiser.newBytes ?! optimiser.oldBytes, size: optimiser.newSize ?? optimiser.oldSize) { + switch optimiser.type { + case .video: + LoopingVideoPlayer(videoURL: url) + case .image: + SwiftUI.Image(nsImage: NSImage(contentsOf: url) ?? .lowtech) + .resizable() + .scaledToFit() + case .pdf: + PDFKitView(url: url) + .allowsHitTesting(false) + default: + EmptyView() } } } - .if(zoomed) { - $0.onContinuousHover { hoverPhase in - guard case let .active(location) = hoverPhase else { return } - let frame = proxy.frame(in: .local) - let x = (location.x - frame.minX) / frame.width - let y = (location.y - frame.minY) / frame.height - zoomOffset = UnitPoint(x: x, y: y) + } + .hfill() + .padding(.vertical) + .onContinuousHover { hoverPhase in + guard zoomed else { return } + + guard case let .active(location) = hoverPhase else { return } + + let frame = proxy.frame(in: .local) + let x = (location.x - frame.minX) / frame.width + let y = (location.y - frame.minY) / frame.height + + zoomOffset = UnitPoint(x: x, y: y) + } + } + } + + var body: some View { + VStack { + previewStack + + if optimiser.type == .pdf, let url = optimiser.url ?? optimiser.startingURL ?? optimiser.originalURL, let pdf = PDFKitView.pdfViewCache[url]?.document { + VStack { + Text("Page \(pdfPage.i)/\(pdf.pageCount)") + Slider(value: $pdfPage, in: 1 ... pdf.pageCount.d, step: 1.0) + .frame(width: 400) + } + .font(.round(11)) + .padding(.top, 10) + .onChange(of: pdfPage) { page in + if let url = optimiser.startingURL ?? optimiser.originalURL, let pdfView = PDFKitView.pdfViewCache[url], let page = pdfView.document?.page(at: page.i) { + pdfView.go(to: page) + } + if let url = optimiser.url, let otherPDFView = PDFKitView.pdfViewCache[url], let page = otherPDFView.document?.page(at: page.i) { + otherPDFView.go(to: page) } } } + Text("Hold the **⌘ Command** key to zoom in") .round(10).foregroundColor(.tertiaryLabel) .padding(.top, 4) + Text("Add **⌥ Option** to zoom in further") + .round(10).foregroundColor(.tertiaryLabel) } - .onChange(of: km.lcmd) { lcmd in - guard (lcmd || km.rcmd) != zoomed else { return } - - withAnimation(.fastSpring) { - zoomed = lcmd || km.rcmd - zoom = zoomed ? 3.0 : 1.0 - } - } - .onChange(of: km.rcmd) { rcmd in - guard (rcmd || km.lcmd) != zoomed else { return } + .onChange(of: km.rcmd) { _ in flagsChanged(Set(km.flags)) } + .onChange(of: km.lcmd) { _ in flagsChanged(Set(km.flags)) } + .onChange(of: km.ralt) { _ in flagsChanged(Set(km.flags)) } + .onChange(of: km.lalt) { _ in flagsChanged(Set(km.flags)) } + } - withAnimation(.fastSpring) { - zoomed = rcmd || km.lcmd - zoom = zoomed ? 3.0 : 1.0 + func flagsChanged(_ flags: Set) { + guard NSApp.isActive else { return } + withAnimation(.fastSpring) { + zoomed = flags.hasElements(from: [.lcmd, .rcmd, .cmd]) + zoom = zoomed ? (flags.hasElements(from: [.lalt, .ralt, .alt]) ? 8.0 : 3.0) : 1.0 + if !zoomed { + zoomOffset = .center } } } - func preview(url: URL, title: String, bytes: Int? = nil, size: CGSize? = nil, @ViewBuilder content: () -> some View) -> some View { VStack { VStack { @@ -148,10 +184,11 @@ struct CompareView: View { .truncationMode(.middle) content() .scaleEffect(zoom, anchor: zoomOffset) - .frame(width: 300, height: 300) + .frame(width: COMPARISON_VIEW_SIZE, height: COMPARISON_VIEW_SIZE) + .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .clipped() - }.frame(width: 300) + }.frame(width: COMPARISON_VIEW_SIZE) VStack(alignment: .leading) { if let bytes { @@ -163,7 +200,7 @@ struct CompareView: View { .hfill(.leading) } } - .frame(width: 300) + .frame(width: COMPARISON_VIEW_SIZE) .foregroundColor(.secondary) .padding(.top, 4) } @@ -186,8 +223,9 @@ struct ComparePreview: View { noThumb.originalURL = "\(HOME)/Documents/pages-before.pdf".fileURL noThumb.finish(oldBytes: 12_250_190, newBytes: 5_211_932) - let videoOpt = Optimiser(id: "Movies/meeting-recording-video.mov", type: .video(.quickTimeMovie), running: true, progress: nil) - videoOpt.url = "\(HOME)/Movies/meeting-recording-video.mov".fileURL + let videoOpt = Optimiser(id: "Movies/sonoma-from-above.mov", type: .video(.quickTimeMovie), running: true, progress: nil) + videoOpt.url = "\(HOME)/Movies/sonoma-from-above-opt.mp4".fileURL + videoOpt.originalURL = "\(HOME)/Movies/sonoma-from-above.mov".fileURL videoOpt.thumbnail = NSImage(resource: .sonomaVideo) videoOpt.changePlaybackSpeedFactor = 2.0 videoOpt.finish(oldBytes: 7_750_190, newBytes: 2_211_932, oldSize: thumbSize) @@ -211,13 +249,17 @@ struct ComparePreview: View { }() var body: some View { - CompareView(optimiser: ComparePreview.om.optimisers.first(where: { $0.id == "pages.pdf" })!) - // Optimiser.IDs.clipboardImage })!) + CompareView( + optimiser: ComparePreview.om.optimisers + // .first(where: { $0.id == "Movies/sonoma-from-above.mov" })! ) +// .first(where: { $0.id == "pages.pdf" })!) + .first(where: { $0.id == Optimiser.IDs.clipboardImage })! + ) } } #Preview { ComparePreview() - .frame(width: 600, height: 400) + .frame(width: 700, height: 500) .padding() } diff --git a/Clop/Images.swift b/Clop/Images.swift index 6e0f075..8486d34 100644 --- a/Clop/Images.swift +++ b/Clop/Images.swift @@ -369,7 +369,7 @@ class Image: CustomStringConvertible { proc.waitUntilExit() shortcutOutFile.waitForFile(for: 2) - guard shortcutOutFile.exists else { + guard shortcutOutFile.exists, (shortcutOutFile.fileSize() ?? 1) > 0 else { return nil } var outImg: Image? diff --git a/Clop/Optimisable.swift b/Clop/Optimisable.swift index 7d097bb..0f26943 100644 --- a/Clop/Optimisable.swift +++ b/Clop/Optimisable.swift @@ -53,12 +53,12 @@ class Optimisable { } func runThroughShortcut(shortcut: Shortcut? = nil, optimiser: Optimiser, allowLarger: Bool, aggressiveOptimisation: Bool, source: String?) throws -> Self? { - let shortcutOutFile = FilePath.videos.appending("\(Date.now.timeIntervalSinceReferenceDate.i)-shortcut-output-for-\(path.stem!)") + let shortcutOutFile = (self is PDF ? FilePath.pdfs : FilePath.videos).appending("\(Date.now.timeIntervalSinceReferenceDate.i)-shortcut-output-for-\(path.stem!)") let proc: Process? = if let shortcut { optimiser.runShortcut(shortcut, outFile: shortcutOutFile, url: path.url) } else { - optimiser.runAutomation(outFile: shortcutOutFile, source: source, url: path.url, type: .video(UTType.from(filePath: path) ?? .mpeg4Movie)) + optimiser.runAutomation(outFile: shortcutOutFile, source: source, url: path.url, type: (self is PDF ? .pdf : (.video(UTType.from(filePath: path) ?? .mpeg4Movie)))) } guard let proc else { return nil } @@ -73,7 +73,7 @@ class Optimisable { Self(shortcutOutFile, id: id) } - guard let outfile, outfile.hash != hash else { + guard let outfile, outfile.hash != hash, outfile.fileSize > 0 else { return nil } diff --git a/Clop/OptimisationUtils.swift b/Clop/OptimisationUtils.swift index 422df5a..e425ca2 100644 --- a/Clop/OptimisationUtils.swift +++ b/Clop/OptimisationUtils.swift @@ -433,6 +433,12 @@ final class QuickLooker: QLPreviewPanelDataSource { @Published var originalURL: URL? @Published var startingURL: URL? @Published var convertedFromURL: URL? + var comparisonOriginalURL: URL? { + if let startingURL, startingURL != url, fm.fileExists(atPath: startingURL.path) { return startingURL } + if let originalURL, originalURL != url, fm.fileExists(atPath: originalURL.path) { return originalURL } + if let convertedFromURL, convertedFromURL != url, fm.fileExists(atPath: convertedFromURL.path) { return convertedFromURL } + return nil + } @Published var downscaleFactor = 1.0 @Published var changePlaybackSpeedFactor = 1.0 @@ -460,7 +466,9 @@ final class QuickLooker: QLPreviewPanelDataSource { lazy var video: Video? = fetchVideo() lazy var pdf: PDF? = fetchPDF() - var comparisonWindow: NSWindow? + var comparisonWindowController: NSWindowController? + + var isComparing = false @Published var editing = false { didSet { @@ -618,26 +626,39 @@ final class QuickLooker: QLPreviewPanelDataSource { func compare() { let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 600, height: 400), - styleMask: [.fullSizeContentView], + contentRect: NSRect(x: 0, y: 0, width: COMPARISON_VIEW_SIZE * 2 + 100, height: COMPARISON_VIEW_SIZE + 200), + styleMask: [.fullSizeContentView, .titled, .closable], backing: .buffered, defer: false ) + window.title = "Comparison" + window.isReleasedWhenClosed = true + window.titlebarAppearsTransparent = true window.center() window.setFrameAutosaveName("Compare Window") -// window.contentView = NSHostingView(rootView: CompareView(optimiser: self)) + + window.contentView = NSHostingView( + rootView: CompareView(optimiser: self) + .frame(width: COMPARISON_VIEW_SIZE * 2 + 100, height: COMPARISON_VIEW_SIZE + 200) + .padding() + .background(.regularMaterial) + ) + window.titlebarAppearsTransparent = true + window.backgroundColor = .clear + window.makeKeyAndOrderFront(nil) + window.orderFrontRegardless() + window.center() + focus() NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose), name: NSWindow.willCloseNotification, object: window) - comparisonWindow = window + comparisonWindowController = NSWindowController(window: window) + isComparing = true } @objc func windowWillClose(_ notification: Notification) { -// guard let window = notification.object as? NSWindow else { return } - - mainActor { - self.comparisonWindow = nil - } + isComparing = false + PDFKitView.clearCache(for: [url, startingURL ?? originalURL].compactMap { $0 }) } func fetchVideo() -> Video? { diff --git a/Clop/PDF.swift b/Clop/PDF.swift index 3bb0136..1e61fc4 100644 --- a/Clop/PDF.swift +++ b/Clop/PDF.swift @@ -302,6 +302,7 @@ let GHOSTSCRIPT_ENV = ["GS_LIB": BIN_DIR.appending(path: "share/ghostscript/9.56 showFloatingThumbnails() let fileSize = pdf.fileSize + var previouslyCached = true pdfOptimisationQueue.addOperation { var optimisedPDF: PDF? @@ -329,6 +330,14 @@ let GHOSTSCRIPT_ENV = ["GS_LIB": BIN_DIR.appending(path: "share/ghostscript/9.56 throw ClopError.pdfSizeLarger(path) } + + // Save optimised PDF path to cache to avoid re-optimising it after it is saved to file + mainActor { + if OM.optimisedFilesByHash[pdf.hash] == nil { + previouslyCached = false + OM.optimisedFilesByHash[pdf.hash] = optimisedPDF!.path + } + } } catch let ClopProcError.processError(proc) { if proc.terminated { log.debug("Process terminated by us: \(proc.commandLine)") @@ -346,14 +355,28 @@ let GHOSTSCRIPT_ENV = ["GS_LIB": BIN_DIR.appending(path: "share/ghostscript/9.56 mainActor { optimiser.finish(error: "Optimisation failed") } } - guard let optimisedPDF else { return } + guard var optimisedPDF else { return } + + var shortcutChangedPDF = false + if let changedPDF = try? optimisedPDF.runThroughShortcut(optimiser: optimiser, allowLarger: allowLarger, aggressiveOptimisation: aggressiveOptimisation ?? false, source: source) { + optimisedPDF = changedPDF + mainActor { optimiser.url = changedPDF.path.url } + + shortcutChangedPDF = true + } + mainActor { result = optimisedPDF optimiser.url = optimisedPDF.path.url optimiser.finish(oldBytes: fileSize, newBytes: optimisedPDF.fileSize, oldSize: optimisedPDF.size, removeAfterMs: hideFilesAfter) + if copyToClipboard { optimiser.copyToClipboard() } + + if !shortcutChangedPDF, !previouslyCached { + OM.optimisedFilesByHash[pdf.hash] = optimisedPDF.path + } } } } diff --git a/Clop/RightClickMenu.swift b/Clop/RightClickMenu.swift index c256672..75c69d1 100644 --- a/Clop/RightClickMenu.swift +++ b/Clop/RightClickMenu.swift @@ -88,6 +88,10 @@ struct RightClickMenuView: View { } .keyboardShortcut(" ") + Button("Compare") { + optimiser.compare() + }.disabled(optimiser.url == nil || (optimiser.startingURL ?? optimiser.originalURL) == nil) + if !optimiser.running { if optimiser.canDownscale() || optimiser.canChangePlaybackSpeed() || diff --git a/Clop/Settings.swift b/Clop/Settings.swift index b71f769..bed625c 100644 --- a/Clop/Settings.swift +++ b/Clop/Settings.swift @@ -139,6 +139,7 @@ extension Defaults.Keys { static let pauseAutomaticOptimisations = Key("pauseAutomaticOptimisations", default: false) static let syncSettingsCloud = Key("syncSettingsCloud", default: true) + static let allowClopToAppearInScreenshots = Key("allowClopToAppearInScreenshots", default: false) } let DEFAULT_CROP_SIZES: [CropSize] = [ diff --git a/Clop/Video.swift b/Clop/Video.swift index 9d58507..98865cd 100644 --- a/Clop/Video.swift +++ b/Clop/Video.swift @@ -285,7 +285,7 @@ class Video: Optimisable { outputPath.waitForFile(for: 2) outputPath.copyExif(from: inputPath, stripMetadata: Defaults[.stripMetadata]) if Defaults[.preserveDates] { - outputPath.copyCreationModificationDates(from: path) + outputPath.copyCreationModificationDates(from: inputPath) } try? outputPath.setOptimisationStatusXattr("true") @@ -540,6 +540,7 @@ var processTerminated = Set() showFloatingThumbnails() let fileSize = video.fileSize + var previouslyCached = true videoOptimisationQueue.addOperation { var optimisedVideo: Video? @@ -567,6 +568,14 @@ var processTerminated = Set() throw ClopError.videoSizeLarger(path) } + + // Save optimised video path to cache to avoid re-optimising it after it is saved to file + mainActor { + if OM.optimisedFilesByHash[video.hash] == nil { + previouslyCached = false + OM.optimisedFilesByHash[video.hash] = optimisedVideo!.path + } + } } catch let ClopProcError.processError(proc) { if proc.terminated { log.debug("Process terminated by us: \(proc.commandLine)") @@ -584,14 +593,28 @@ var processTerminated = Set() mainActor { optimiser.finish(error: "Optimisation failed") } } - guard let optimisedVideo else { return } + guard var optimisedVideo else { return } + + var shortcutChangedVideo = false + if let changedVideo = try? optimisedVideo.runThroughShortcut(optimiser: optimiser, allowLarger: allowLarger, aggressiveOptimisation: aggressiveOptimisation ?? false, source: source) { + optimisedVideo = changedVideo + mainActor { optimiser.url = changedVideo.path.url } + + shortcutChangedVideo = true + } + mainActor { result = optimisedVideo optimiser.url = optimisedVideo.path.url optimiser.finish(oldBytes: fileSize, newBytes: optimisedVideo.fileSize, removeAfterMs: hideFilesAfter) + if copyToClipboard { optimiser.copyToClipboard() } + + if !shortcutChangedVideo, !previouslyCached { + OM.optimisedFilesByHash[video.hash] = optimisedVideo.path + } } } } diff --git a/ClopCLI/main.swift b/ClopCLI/main.swift index b1c3afe..d84b4cd 100644 --- a/ClopCLI/main.swift +++ b/ClopCLI/main.swift @@ -1109,7 +1109,7 @@ struct Clop: ParsableCommand { if let size = crop, size == .zero { throw ValidationError("Invalid size, must be greater than 0") } - if let factor = downscaleFactor, factor >= 0.01, factor <= 0.99 { + if let factor = downscaleFactor, factor < 0.01, factor > 0.99 { throw ValidationError("Invalid downscale factor, must be greater than 0 and less than 1") } diff --git a/ReleaseNotes/2.6.0.md b/ReleaseNotes/2.6.0.md index af08fbe..2e512ab 100644 --- a/ReleaseNotes/2.6.0.md +++ b/ReleaseNotes/2.6.0.md @@ -9,3 +9,11 @@ - Add `--adaptive-optimisation` and `--no-adaptive-optimisation` options to CLI commands that can act on images - Speed up PNG optimisation by using `pngquant`'s `--speed 3` option - Improve EXIF metadata handling when optimising images and videos +- Detect files that are in progress of being created/modified and wait for the operation to settle before optimising + +## Fixes + +- Fix `--downscale-factor` parsing on `clop optimise` CLI +- Fix automation not triggering shortcuts for videos and PDFs +- Fix: if a previously optimised file was replaced with a new file, an old backup was being used for optimisations instead of the new file +- Fix creation/modification date not being preserved for videos