diff --git a/Clop.xcodeproj/project.pbxproj b/Clop.xcodeproj/project.pbxproj index 9e4e562..50521d4 100644 --- a/Clop.xcodeproj/project.pbxproj +++ b/Clop.xcodeproj/project.pbxproj @@ -592,7 +592,7 @@ C7AB661D288301590041BEC8 /* Sources */, C7AB661E288301590041BEC8 /* Frameworks */, C7AB661F288301590041BEC8 /* Resources */, - C71A88752A94AFAA00ABD6EE /* ShellScript */, + C71A88752A94AFAA00ABD6EE /* Create MASReceipt */, C70B5F2A2AC014BF00345739 /* Embed Foundation Extensions */, C7956A4B2AC317C100C0EDF2 /* Copy Files (1 item) */, ); @@ -718,7 +718,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - C71A88752A94AFAA00ABD6EE /* ShellScript */ = { + C71A88752A94AFAA00ABD6EE /* Create MASReceipt */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -727,6 +727,7 @@ ); inputPaths = ( ); + name = "Create MASReceipt"; outputFileListPaths = ( ); outputPaths = ( @@ -1347,6 +1348,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Clop/bin", + "$(PROJECT_DIR)/Clop", ); LLVM_LTO = YES_THIN; MACOSX_DEPLOYMENT_TARGET = 13.0; @@ -1387,6 +1389,7 @@ LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Clop/bin", + "$(PROJECT_DIR)/Clop", ); LLVM_LTO = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; diff --git a/Clop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Clop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 15c542d..48967dd 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" : "08862789e1cbba7a9561bed69832a9306f339cd3", - "version" : "8.29.1" + "revision" : "5421f94cc859eb65f5ae3866165a053aa634431e", + "version" : "8.32.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "b456fd404954a9e13f55aa0c88cd5a40b8399638", - "version" : "2.6.3" + "revision" : "0ef1ee0220239b3776f433314515fd849025673f", + "version" : "2.6.4" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version" : "1.4.0" + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" } }, { diff --git a/Clop.xcodeproj/project.xcworkspace/xcuserdata/alin.xcuserdatad/xcdebugger/Expressions.xcexplist b/Clop.xcodeproj/project.xcworkspace/xcuserdata/alin.xcuserdatad/xcdebugger/Expressions.xcexplist index 80901f9..8bdbc81 100644 --- a/Clop.xcodeproj/project.xcworkspace/xcuserdata/alin.xcuserdatad/xcdebugger/Expressions.xcexplist +++ b/Clop.xcodeproj/project.xcworkspace/xcuserdata/alin.xcuserdatad/xcdebugger/Expressions.xcexplist @@ -3,18 +3,18 @@ version = "1.0"> + contextName = "LowtechWindow.screenCorner.didset:OSDWindow.swift"> + value = "screenCorner!"> + contextName = "optimiseItem(_:id:hideFloatingResult:downscaleTo:cropTo:changePlaybackSpeedBy:aggressiveOptimisation:optimisationCount:copyToClipboard:source:output:removeAudio:):OptimisationUtils.swift"> + value = "path.string"> diff --git a/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist b/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist index 6a1f5aa..850a243 100644 --- a/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Clop.xcodeproj/xcuserdata/alin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -22,7 +22,7 @@ ClopCLI.xcscheme_^#shared#^_ orderHint - 5 + 4 Example (Playground) 1.xcscheme @@ -95,7 +95,7 @@ FinderOptimiser-setapp.xcscheme_^#shared#^_ orderHint - 4 + 5 FinderOptimiser.xcscheme_^#shared#^_ diff --git a/Clop/ClopApp.swift b/Clop/ClopApp.swift index 7f107ae..ef92e42 100644 --- a/Clop/ClopApp.swift +++ b/Clop/ClopApp.swift @@ -237,6 +237,8 @@ class AppDelegate: AppDelegateParent { opt.restoreOriginal() case .r where !opt.running: opt.editingFilename = true + case .d where opt.url != nil && opt.comparisonOriginalURL != nil: + opt.compare() case .c: opt.copyToClipboard() opt.overlayMessage = "Copied" @@ -600,7 +602,7 @@ class AppDelegate: AppDelegateParent { @objc func windowWillClose(_ notification: Notification) { guard let window = notification.object as? NSWindow else { return } - if window.title == "Settings" { + if window.title == "Settings" || window.title == "Comparison" { mainActor { settingsViewManager.windowOpen = false NSApp.setActivationPolicy(.accessory) @@ -620,6 +622,11 @@ class AppDelegate: AppDelegateParent { @objc func windowDidBecomeMainNotification(_ notification: Notification) { guard let window = notification.object as? NSWindow else { return } + + if window.title == "Comparison" { + NSApp.setActivationPolicy(.regular) + } + if window.title == "Settings" { mainActor { print(FloatingPreview.om, CompactPreview.om) diff --git a/Clop/ClopUtils.swift b/Clop/ClopUtils.swift index 27876a5..56994f3 100644 --- a/Clop/ClopUtils.swift +++ b/Clop/ClopUtils.swift @@ -275,6 +275,9 @@ extension FilePath { } } + var clopBackupPath: FilePath? { + FilePath.clopBackups.appending(nameWithHash) + } static var clopBackups = FilePath.dir(workdir / "backups", permissions: 0o777) static var videos = FilePath.dir(workdir / "videos", permissions: 0o777) static var images = FilePath.dir(workdir / "images", permissions: 0o777) diff --git a/Clop/CompareView.swift b/Clop/CompareView.swift index 893cf3b..80e3229 100644 --- a/Clop/CompareView.swift +++ b/Clop/CompareView.swift @@ -4,24 +4,127 @@ import Lowtech import PDFKit import SwiftUI +struct AVPlayerControllerRepresented: NSViewRepresentable { + var player: AVPlayer + + func makeNSView(context: Context) -> AVPlayerView { + let view = AVPlayerView() + view.controlsStyle = .none + view.player = player + return view + } + + func updateNSView(_ nsView: AVPlayerView, context: Context) {} +} + @MainActor struct LoopingVideoPlayer: View { - init(videoURL: URL) { + init(videoURL: URL, otherVideoURL: URL? = nil, playing: Binding) { + self.videoURL = videoURL + self.otherVideoURL = otherVideoURL + _playing = playing + + if let player = Self.playerCache[videoURL], + let playerLooper = Self.playerLooperCache[videoURL], + let video = Self.videoCache[videoURL] + { + self.video = video + self.player = player + self.playerLooper = playerLooper + return + } + let asset = AVAsset(url: videoURL) - let item = AVPlayerItem(asset: asset) + video = AVPlayerItem(asset: asset) + + player = AVQueuePlayer(playerItem: video) + player.isMuted = true + player.allowsExternalPlayback = false + + playerLooper = AVPlayerLooper(player: player, templateItem: video) + Self.playerLooperCache[videoURL] = playerLooper + Self.playerCache[videoURL] = player + Self.videoCache[videoURL] = video - player = AVQueuePlayer(playerItem: item) - playerLooper = AVPlayerLooper(player: player, templateItem: item) + if otherVideoURL != nil { + setupPeriodicTimeObserver() + } } + static var playerCache = [URL: AVQueuePlayer]() + static var playerLooperCache = [URL: AVPlayerLooper]() + static var videoCache = [URL: AVPlayerItem]() + static var timeObserverTokens = [URL: Any]() + + var videoURL: URL + var otherVideoURL: URL? + + @Binding var playing: Bool + var body: some View { - VideoPlayer(player: player) + AVPlayerControllerRepresented(player: player) .onAppear { player.play() } .onDisappear { player.pause() } + .onChange(of: playing) { playing in + if playing { + player.play() + } else { + player.pause() + } + } + } + + var otherPlayer: AVQueuePlayer? { + get { + guard let otherVideoURL else { return nil } + return Self.playerCache[otherVideoURL] + } + set { + guard let otherVideoURL else { return } + guard let newValue else { + Self.playerCache.removeValue(forKey: otherVideoURL) + return + } + Self.playerCache[otherVideoURL] = newValue + } } + static func clearCache(for urls: [URL]) { + for url in urls { + let player = playerCache.removeValue(forKey: url) + playerLooperCache.removeValue(forKey: url) + videoCache.removeValue(forKey: url) + + if let token = timeObserverTokens[url] { + player?.removeTimeObserver(token) + timeObserverTokens.removeValue(forKey: url) + } + } + } + + private var video: AVPlayerItem private var player: AVQueuePlayer private var playerLooper: AVPlayerLooper + + private func setupPeriodicTimeObserver() { + let timeInterval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + + Self.timeObserverTokens[videoURL] = player.addPeriodicTimeObserver(forInterval: timeInterval, queue: .main) { time in + mainActor { syncPlayerTimes(with: time) } + } + } + + private func syncPlayerTimes(with time: CMTime) { + guard let otherPlayer, let otherCurrentItem = otherPlayer.currentItem else { return } + + let otherCurrentTime = otherCurrentItem.currentTime() + let timeDifference = abs(CMTimeGetSeconds(time) - CMTimeGetSeconds(otherCurrentTime)) + + if timeDifference > 0.5 { + otherPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) + } + } + } class PDFDelegate: NSObject, PDFViewDelegate { @@ -37,6 +140,46 @@ class PDFDelegate: NSObject, PDFViewDelegate { } } +struct PannableImage: View { + init(url: URL, fitOrFill: Binding = .constant(.fill)) { + self.url = url + _fitOrFill = fitOrFill + if let image = Self.imageCache[url] { + self.image = image + return + } + + let image = NSImage(contentsOf: url) + Self.imageCache[url] = image + } + + static var imageCache = [URL: NSImage]() + + var url: URL + var image: NSImage? + + @Binding var fitOrFill: ContentMode + + var body: some View { + if let image { + SwiftUI.Image(nsImage: image) + .resizable() + .interpolation(.none) + .aspectRatio(contentMode: fitOrFill) + } else { + ProgressView() + } + } + + static func clearCache(for urls: [URL]) { + for url in urls { + imageCache.removeValue(forKey: url) + } + } + + @State private var offset = CGSize.zero +} + struct PDFKitView: NSViewRepresentable { static var pdfViewCache = [URL: PDFView]() @@ -76,38 +219,46 @@ struct CompareView: View { @ObservedObject var optimiser: Optimiser @ObservedObject var km = KM - @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) { + preview( + url: originalURL, + title: "Original", + bytes: optimiser.oldBytes, + size: optimiser.oldSize, + aspectRatio: (optimiser.newSize == nil || optimiser.oldSize?.aspectRatio == optimiser.newSize?.aspectRatio) ? (optimiser.oldSize?.aspectRatio ?? 1) : 1 + ) { switch optimiser.type { case .video: - LoopingVideoPlayer(videoURL: originalURL) + LoopingVideoPlayer(videoURL: originalURL, otherVideoURL: url, playing: $videoPlaying) case .image: - SwiftUI.Image(nsImage: NSImage(contentsOf: originalURL) ?? .lowtech) - .resizable() - .scaledToFit() + PannableImage(url: originalURL, fitOrFill: $fitOrFill) case .pdf: PDFKitView(url: originalURL) .allowsHitTesting(false) + .aspectRatio(contentMode: fitOrFill) default: EmptyView() } } - preview(url: url, title: "Optimised", bytes: optimiser.newBytes ?! optimiser.oldBytes, size: optimiser.newSize ?? optimiser.oldSize) { + preview( + url: url, + title: "Optimised", + bytes: optimiser.newBytes ?! optimiser.oldBytes, + size: optimiser.newSize ?? optimiser.oldSize, + aspectRatio: (optimiser.newSize == nil || optimiser.oldSize?.aspectRatio == optimiser.newSize?.aspectRatio) ? (optimiser.oldSize?.aspectRatio ?? 1) : 1 + ) { switch optimiser.type { case .video: - LoopingVideoPlayer(videoURL: url) + LoopingVideoPlayer(videoURL: url, playing: $videoPlaying) case .image: - SwiftUI.Image(nsImage: NSImage(contentsOf: url) ?? .lowtech) - .resizable() - .scaledToFit() + PannableImage(url: url, fitOrFill: $fitOrFill) case .pdf: PDFKitView(url: url) .allowsHitTesting(false) + .aspectRatio(contentMode: fitOrFill) default: EmptyView() } @@ -134,22 +285,73 @@ struct CompareView: View { VStack { previewStack - if optimiser.type == .pdf, let url = optimiser.url ?? optimiser.startingURL ?? optimiser.originalURL, let pdf = PDFKitView.pdfViewCache[url]?.document { + if optimiser.type.isVideo { + Button { + videoPlaying.toggle() + } label: { + SwiftUI.Image(systemName: videoPlaying ? "pause.fill" : "play.fill") + .font(.system(size: 20)) + } + .buttonStyle(FlatButton()) + .keyboardShortcut(.space, modifiers: []) + .padding(.top, 10) + } + + if optimiser.type == .pdf, let url = optimiser.url ?? optimiser.comparisonOriginalURL, 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) + if pdf.pageCount > 1 { + 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) { + if let url = optimiser.comparisonOriginalURL, 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) } } + + if pdf.pageCount > 1 { + HStack { + Button { + pdfPage -= 1 + } label: { + SwiftUI.Image(systemName: "chevron.left") + .font(.system(size: 20)) + } + .buttonStyle(FlatButton()) + .disabled(pdfPage == 1) + .keyboardShortcut(.leftArrow, modifiers: []) + + Button { + pdfPage += 1 + } label: { + SwiftUI.Image(systemName: "chevron.right") + .font(.system(size: 20)) + } + .buttonStyle(FlatButton()) + .disabled(Int(pdfPage) == pdf.pageCount) + .keyboardShortcut(.rightArrow, modifiers: []) + } + } + } + + if optimiser.type.isImage { + Button { + withAnimation(.fastSpring) { + fitOrFill = fitOrFill == .fill ? .fit : .fill + } + } label: { + SwiftUI.Image(systemName: fitOrFill == .fit ? "arrow.up.left.and.arrow.down.right" : "arrow.down.right.and.arrow.up.left") + .font(.system(size: 20)) + } + .buttonStyle(FlatButton()) + .keyboardShortcut(.space, modifiers: []) } Text("Hold the **⌘ Command** key to zoom in") @@ -162,6 +364,7 @@ struct CompareView: View { .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)) } + .focusable(false) } func flagsChanged(_ flags: Set) { @@ -174,21 +377,35 @@ struct CompareView: View { } } } - func preview(url: URL, title: String, bytes: Int? = nil, size: CGSize? = nil, @ViewBuilder content: () -> some View) -> some View { + func preview(url: URL, title: String, bytes: Int? = nil, size: CGSize? = nil, aspectRatio: Double = 1, @ViewBuilder content: () -> some View) -> some View { VStack { VStack { Text(title).bold(12) - Text(url.shellString) - .mono(10).lineLimit(1) - .foregroundColor(.secondary) - .truncationMode(.middle) + Menu(url.shellString) { + Button("Show in Finder") { + NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: "") + } + Button("Copy Path") { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(url.path, forType: .string) + } + } + .menuStyle(.button) + .buttonStyle(FlatButton(color: .bg.warm.opacity(0.5), textColor: .secondary)) + .font(.mono(10)).lineLimit(1) + .foregroundColor(.secondary) + .truncationMode(.middle) + content() .scaleEffect(zoom, anchor: zoomOffset) - .frame(width: COMPARISON_VIEW_SIZE, height: COMPARISON_VIEW_SIZE) + .frame( + minWidth: COMPARISON_VIEW_SIZE / 2, idealWidth: COMPARISON_VIEW_SIZE, maxWidth: .infinity, + minHeight: COMPARISON_VIEW_SIZE / 2, idealHeight: COMPARISON_VIEW_SIZE, maxHeight: .infinity + ) .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .clipped() - }.frame(width: COMPARISON_VIEW_SIZE) + }.frame(minWidth: COMPARISON_VIEW_SIZE / 2, idealWidth: COMPARISON_VIEW_SIZE) VStack(alignment: .leading) { if let bytes { @@ -200,16 +417,26 @@ struct CompareView: View { .hfill(.leading) } } - .frame(width: COMPARISON_VIEW_SIZE) + .frame(minWidth: COMPARISON_VIEW_SIZE / 2, idealWidth: COMPARISON_VIEW_SIZE) .foregroundColor(.secondary) .padding(.top, 4) } } + @State private var pdfPage = 1.0 + + @State private var originalImage: NSImage? + @State private var optimisedImage: NSImage? + + @State private var originalVideo: PDFDocument? + @State private var optimisedPDF: PDFDocument? + + @State private var videoPlaying = true + + @State private var fitOrFill = ContentMode.fill @State private var zoomed = false @State private var zoom = 1.0 @State private var zoomOffset = UnitPoint.center - } @MainActor @@ -251,8 +478,10 @@ struct ComparePreview: View { var body: some View { CompareView( optimiser: ComparePreview.om.optimisers - // .first(where: { $0.id == "Movies/sonoma-from-above.mov" })! ) -// .first(where: { $0.id == "pages.pdf" })!) + // .first(where: { $0.id == "Movies/sonoma-from-above.mov" })! + // ) +// .first(where: { $0.id == "pages.pdf" })! +// ) .first(where: { $0.id == Optimiser.IDs.clipboardImage })! ) } @@ -260,6 +489,6 @@ struct ComparePreview: View { #Preview { ComparePreview() - .frame(width: 700, height: 500) + .frame(width: COMPARISON_VIEW_SIZE + 100, height: COMPARISON_VIEW_SIZE / 2 + 200) .padding() } diff --git a/Clop/Images.swift b/Clop/Images.swift index 8486d34..69dbedc 100644 --- a/Clop/Images.swift +++ b/Clop/Images.swift @@ -449,7 +449,7 @@ class Image: CustomStringConvertible { let aggressiveOptimisation = aggressiveOptimisation ?? Defaults[.useAggressiveOptimisationGIF] mainActor { optimiser.aggressive = aggressiveOptimisation } - let backup = path.backup(operation: .copy) + let backup = path.backup(path: path.clopBackupPath, operation: .copy) let proc = try tryProc( GIFSICLE.string, args: [ @@ -513,7 +513,7 @@ class Image: CustomStringConvertible { procs.append(pngProc) } - let backup = path.backup(operation: .copy) + let backup = path.backup(path: path.clopBackupPath, operation: .copy) let procMaps = try tryProcs(procs, tries: 2) { procMap in mainActor { optimiser.processes = procMap.values.map { $0 } } } @@ -639,7 +639,7 @@ class Image: CustomStringConvertible { mainActor { optimiser.processes = procMap.values.map { $0 } } } - let backup = path.backup(operation: .copy) + let backup = path.backup(path: path.clopBackupPath, operation: .copy) guard let proc = procMaps[pngProc] else { throw ClopError.noProcess(pngProc.cmdline) } @@ -1010,7 +1010,7 @@ extension FilePath { let behaviour = Defaults[.convertedImageBehaviour] if behaviour == .inPlace { - img.path.backup(force: true, operation: .move) + img.path.backup(path: img.path.clopBackupPath, force: true, operation: .move) } if behaviour != .temporary { try converted.path.setOptimisationStatusXattr("pending") @@ -1058,7 +1058,7 @@ extension FilePath { scalingFactor = 1.0 optimiser.stop(remove: false) optimiser.operation = (Defaults[.showImages] ? "Optimising" : "Optimising \(optimiser.filename)") + (aggressiveOptimisation ?? false ? " (aggressive)" : "") - optimiser.originalURL = img.path.backup(force: false, operation: .copy)?.url ?? img.path.url + optimiser.originalURL = img.path.backup(path: img.path.clopBackupPath, force: false, operation: .copy)?.url ?? img.path.url optimiser.url = img.path.url if id == Optimiser.IDs.clipboardImage { optimiser.startingURL = optimiser.url diff --git a/Clop/Optimisable.swift b/Clop/Optimisable.swift index fa091ac..1482df5 100644 --- a/Clop/Optimisable.swift +++ b/Clop/Optimisable.swift @@ -32,6 +32,10 @@ class Optimisable { OM.optimisers.first(where: { $0.id == id ?? path.string || $0.id == path.string || $0.id == path.url.absoluteString }) } + @MainActor static func getOptimiser(id: String? = nil, path: FilePath) -> Optimiser? { + OM.optimisers.first(where: { $0.id == id ?? path.string || $0.id == path.string || $0.id == path.url.absoluteString }) + } + func copyWithPath(_ path: FilePath) -> Self { Self(path, thumb: true, id: id) } @@ -42,8 +46,8 @@ class Optimisable { log.debug("Using cached thumbnail from \(thumbURL.path) for \(path.string)") url = thumbURL } - generateThumbnail(for: url, size: THUMB_SIZE) { [weak self] thumb in - guard let self, let optimiser else { + generateThumbnail(for: url, size: THUMB_SIZE) { [url, id, path] thumb in + guard let optimiser = Self.getOptimiser(id: id, path: path) else { log.debug("Thumbnail generation cancelled for \(url.path)") return } diff --git a/Clop/OptimisationUtils.swift b/Clop/OptimisationUtils.swift index 110e94e..581a2fa 100644 --- a/Clop/OptimisationUtils.swift +++ b/Clop/OptimisationUtils.swift @@ -36,7 +36,7 @@ enum ItemType: Equatable, Identifiable { var convertibleTypes: [UTType] { switch self { - case let .image: + case .image: [.jpeg, .webP, .avif, .heic, .png, .gif].compactMap { $0 } default: [] @@ -467,6 +467,7 @@ final class QuickLooker: QLPreviewPanelDataSource { 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 } + if let backupPath = path?.clopBackupPath?.url, backupPath != url, fm.fileExists(atPath: backupPath.path) { return backupPath } return nil } @@ -627,7 +628,7 @@ final class QuickLooker: QLPreviewPanelDataSource { func compare() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: COMPARISON_VIEW_SIZE * 2 + 100, height: COMPARISON_VIEW_SIZE + 200), - styleMask: [.fullSizeContentView, .titled, .closable], + styleMask: [.fullSizeContentView, .titled, .closable, .resizable], backing: .buffered, defer: false ) window.title = "Comparison" @@ -638,7 +639,10 @@ final class QuickLooker: QLPreviewPanelDataSource { window.contentView = NSHostingView( rootView: CompareView(optimiser: self) - .frame(width: COMPARISON_VIEW_SIZE * 2 + 100, height: COMPARISON_VIEW_SIZE + 200) + .frame( + minWidth: COMPARISON_VIEW_SIZE + 100, idealWidth: COMPARISON_VIEW_SIZE * 2 + 100, + minHeight: COMPARISON_VIEW_SIZE / 2 + 200, idealHeight: COMPARISON_VIEW_SIZE + 200 + ) .padding() .background(.regularMaterial) ) @@ -658,7 +662,11 @@ final class QuickLooker: QLPreviewPanelDataSource { @objc func windowWillClose(_ notification: Notification) { isComparing = false - PDFKitView.clearCache(for: [url, startingURL ?? originalURL].compactMap { $0 }) + let cachedURLs = [url, startingURL, originalURL, convertedFromURL].compactMap { $0 } + PDFKitView.clearCache(for: cachedURLs) + PannableImage.clearCache(for: cachedURLs) + LoopingVideoPlayer.clearCache(for: cachedURLs) + comparisonWindowController = nil } func fetchVideo() -> Video? { @@ -831,7 +839,7 @@ final class QuickLooker: QLPreviewPanelDataSource { guard let path = originalURL?.filePath ?? path else { return } - let originalPath = (path.backupPath?.exists ?? false) ? path.backupPath : nil + let originalPath = (path.clopBackupPath?.exists ?? false) ? path.clopBackupPath : nil if !path.exists, let originalPath { let _ = try? originalPath.copy(to: path) } @@ -876,7 +884,7 @@ final class QuickLooker: QLPreviewPanelDataSource { path = selfPath } - let originalPath = (path.backupPath?.exists ?? false) ? path.backupPath : nil + let originalPath = (path.clopBackupPath?.exists ?? false) ? path.clopBackupPath : nil if !path.exists, let originalPath { let _ = try? originalPath.copy(to: path) } @@ -910,7 +918,7 @@ final class QuickLooker: QLPreviewPanelDataSource { let _ = try? await downscaleVideo( video, - originalPath: (path.backupPath?.exists ?? false) ? path.backupPath : nil, + originalPath: (path.clopBackupPath?.exists ?? false) ? path.clopBackupPath : nil, id: self.id, toFactor: factor, hideFloatingResult: hideFloatingResult, aggressiveOptimisation: shouldUseAggressiveOptimisation ) @@ -950,8 +958,8 @@ final class QuickLooker: QLPreviewPanelDataSource { notice = nil if fromOriginal, !path.exists || path.hasOptimisationStatusXattr() { - if let backup = path.backupPath, backup.exists { - path.restore(force: true) + if let backup = path.clopBackupPath, backup.exists { + path.restore(backupPath: backup, force: true) } else if let startingPath = startingURL?.existingFilePath, let originalPath = originalURL?.existingFilePath, originalPath != startingPath { path = (try? originalPath.copy(to: startingPath, force: true)) ?? path } @@ -996,22 +1004,21 @@ final class QuickLooker: QLPreviewPanelDataSource { resetRemover() let restore: (FilePath) -> Void = { path in - try? path.backupPath?.setOptimisationStatusXattr("original") - path.restore() + guard let backup = path.clopBackupPath, backup.exists else { return } + try? backup.setOptimisationStatusXattr("original") + path.restore(backupPath: backup) } if let convertedFromURL, let convertedFromPath = convertedFromURL.filePath { self.url = convertedFromURL path = convertedFromPath - if path.backupPath?.exists ?? false { - restore(path) - } + restore(path) if let startingPath = startingURL?.existingFilePath, startingPath != path, startingPath.stem == path.stem, startingPath.dir == path.dir { try? startingPath.delete() } - } else if let startingURL, let startingPath = startingURL.filePath, startingPath.backupPath?.exists ?? false { + } else if let startingURL, let startingPath = startingURL.filePath, startingPath.clopBackupPath?.exists ?? false { path = startingPath self.url = startingURL diff --git a/Clop/PDF.swift b/Clop/PDF.swift index 1e61fc4..0f04956 100644 --- a/Clop/PDF.swift +++ b/Clop/PDF.swift @@ -223,7 +223,7 @@ class PDF: Optimisable { guard proc.terminationStatus == 0 else { throw ClopProcError.processError(proc) } - path.backup(operation: .copy) + path.backup(path: path.clopBackupPath, operation: .copy) tempFile.waitForFile(for: 2) try? tempFile.setOptimisationStatusXattr("true") @@ -322,7 +322,7 @@ let GHOSTSCRIPT_ENV = ["GS_LIB": BIN_DIR.appending(path: "share/ghostscript/9.56 optimisedPDF!.cropTo(aspectRatio: cropSize.longEdge ? cropSize.fractionalAspectRatio : cropSize.aspectRatio) } if !allowLarger, cropSize == nil, optimisedPDF!.fileSize >= fileSize { - pdf.path.restore(force: true) + pdf.path.restore(backupPath: pdf.path.clopBackupPath, force: true) mainActor { optimiser.oldBytes = fileSize optimiser.url = pdf.path.url diff --git a/Clop/RightClickMenu.swift b/Clop/RightClickMenu.swift index 75c69d1..b2a8666 100644 --- a/Clop/RightClickMenu.swift +++ b/Clop/RightClickMenu.swift @@ -88,9 +88,11 @@ struct RightClickMenuView: View { } .keyboardShortcut(" ") - Button("Compare") { + Button("Compare (diff)") { optimiser.compare() - }.disabled(optimiser.url == nil || (optimiser.startingURL ?? optimiser.originalURL) == nil) + } + .disabled(optimiser.url == nil || optimiser.comparisonOriginalURL == nil) + .keyboardShortcut("d") if !optimiser.running { if optimiser.canDownscale() || diff --git a/Clop/Settings.swift b/Clop/Settings.swift index bed625c..7bfd300 100644 --- a/Clop/Settings.swift +++ b/Clop/Settings.swift @@ -26,7 +26,7 @@ let VIDEO_PASTEBOARD_TYPES = VIDEO_FORMATS.compactMap { NSPasteboard.PasteboardT let IMAGE_PASTEBOARD_TYPES = IMAGE_FORMATS.compactMap { NSPasteboard.PasteboardType(rawValue: $0.identifier) } let IMAGE_VIDEO_PASTEBOARD_TYPES: Set = (IMAGE_PASTEBOARD_TYPES + VIDEO_PASTEBOARD_TYPES + [.fileContents]).set -let DEFAULT_HOVER_KEYS: [SauceKey] = [.minus, .delete, .space, .z, .c, .a, .s, .x, .r, .f, .o, .comma, .u] +let DEFAULT_HOVER_KEYS: [SauceKey] = [.minus, .delete, .space, .z, .c, .a, .s, .x, .r, .f, .o, .comma, .u, .d] let DEFAULT_GLOBAL_KEYS: [SauceKey] = [.minus, .equal, .delete, .space, .z, .p, .c, .a, .x, .r, .escape] enum CleanupInterval: TimeInterval, Codable, Defaults.Serializable { diff --git a/Clop/Video.swift b/Clop/Video.swift index 98865cd..8b64e37 100644 --- a/Clop/Video.swift +++ b/Clop/Video.swift @@ -218,7 +218,7 @@ class Video: Optimisable { try? path.setOptimisationStatusXattr("pending") let outputPath = forceMP4 ? FilePath.videos.appending("\(name.stem).mp4") : path - var inputPath = originalPath ?? ((path == outputPath || backup) ? (path.backup(operation: .copy) ?? path) : path) + var inputPath = originalPath ?? ((path == outputPath || backup) ? (path.backup(path: path.clopBackupPath, operation: .copy) ?? path) : path) var additionalArgs = [String]() var newFPS = fps @@ -307,7 +307,7 @@ class Video: Optimisable { if let convertedFrom { let behaviour = Defaults[.convertedVideoBehaviour] if behaviour == .inPlace { - convertedFrom.path.backup(force: true, operation: .move) + convertedFrom.path.backup(path: convertedFrom.path.clopBackupPath, force: true, operation: .move) } if behaviour != .temporary { let path = try newVideo.path.copy(to: convertedFrom.path.dir, force: true) @@ -560,7 +560,7 @@ var processTerminated = Set() removeAudio: removeAudio ) if optimisedVideo!.convertedFrom == nil, optimisedVideo!.fileSize >= fileSize, !allowLarger { - video.path.restore(force: true) + video.path.restore(backupPath: video.path.clopBackupPath, force: true) mainActor { optimiser.oldBytes = fileSize optimiser.url = video.path.url