Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
991004f
Fix: static zoom stays active at 1x instead of exiting (match Windows…
Jul 18, 2026
c6a5135
Fix: break timer background image drawn upside down in flipped view
Jul 18, 2026
ae33073
Fix: panorama region rectangle drawn in yellow to match Windows ZoomIt
Jul 18, 2026
c80d9c4
Fix: Escape cancels/exits panorama during scrolling capture (match Wi…
Jul 18, 2026
3bae648
Fix: block screen saver / display sleep while break timer is active
Jul 18, 2026
c8bf30d
Fix: reorder menu-bar menu to match Windows ZoomIt; add Draw item
Jul 18, 2026
32fbeca
Fix: changing clip transition (e.g. fade to white) now updates existi…
Jul 18, 2026
6de2bdc
Fix: webcam overlay can be click-dragged to reposition (match Windows)
Jul 18, 2026
2c7415e
Fix: trimming an existing video no longer deletes the original file
Jul 18, 2026
a029a45
Fix: Settings window floats on top so it can't get lost behind other …
Jul 18, 2026
d8fe3d6
Fix: split live zoom into its own Options tab so Zoom tab is static-o…
Jul 18, 2026
0d6fbe9
Fix: Draw tab matches Windows - remove default pen width, blank scree…
Jul 18, 2026
127f2c9
Fix: Type tab Sample now renders in the selected font
Jul 18, 2026
37e513d
Fix: pad menu-bar icon so it matches system icon size and alignment
Jul 18, 2026
3e4e821
Move Break Timer menu item below Panorama Capture
Jul 18, 2026
603361e
Use standard rounded-square icon in permissions dialog; align icon to…
Jul 19, 2026
d5a8e89
Default typing font to the system (Mac default) font at 20pt
Jul 19, 2026
2b03ed4
Align Break tab controls in a grid (timer, colors, position, opacity,…
Jul 19, 2026
3537cda
Default typing font is now regular (non-bold) weight
Jul 20, 2026
d63c320
Change panorama region rectangle back to blue (distinct from orange r…
Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Fix: trimming an existing video no longer deletes the original file
  • Loading branch information
Mark Russinovich Mark Russinovich
Mark Russinovich authored and Mark Russinovich committed Jul 18, 2026
commit 2c7415e7015f563bfa65c18bd799585710347dff
50 changes: 49 additions & 1 deletion Sources/ZoomItMacCore/Capture/RecordingController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1132,12 +1132,60 @@ final class RecordingController {
self.clipEditor = editor
editor.present(tempURL: url, suggestedName: suggestedFilename(), onSave: { [weak self] editedURL in
self?.clipEditor = nil
self?.savePanel(for: editedURL)
self?.saveTrimmedClip(editedURL: editedURL, originalURL: url)
}, onCancel: { [weak self] in
self?.clipEditor = nil
})
}

/// Action for saving a clip opened from an existing file (the Trim
/// workflow). If the editor exported a new temp file, that temp is moved
/// into place; if it returned the user's own original (no edits), the
/// original is copied so it is preserved — matching Windows ZoomIt, which
/// never deletes the source file.
enum TrimSaveAction: Equatable { case move, copy }

static func trimSaveAction(editedURL: URL, originalURL: URL) -> TrimSaveAction {
editedURL == originalURL ? .copy : .move
}

/// Saves a clip that was opened from an existing file, always preserving the
/// user's original source file.
private func saveTrimmedClip(editedURL: URL, originalURL: URL) {
let panel = NSSavePanel()
panel.nameFieldStringValue = suggestedFilename()
panel.allowedContentTypes = [.mpeg4Movie]
panel.canCreateDirectories = true
NSApp.activate(ignoringOtherApps: true)

let action = Self.trimSaveAction(editedURL: editedURL, originalURL: originalURL)
if panel.runModal() == .OK, let destination = panel.url {
if destination != originalURL {
try? FileManager.default.removeItem(at: destination)
}
do {
switch action {
case .move:
// Move the exported temp file into place; original untouched.
if destination != editedURL {
try FileManager.default.moveItem(at: editedURL, to: destination)
}
case .copy:
// No edits: copy the user's original, preserving the source.
if destination != editedURL {
try FileManager.default.copyItem(at: editedURL, to: destination)
}
}
} catch {
let alert = NSAlert(error: error)
alert.runModal()
}
} else if action == .move {
// Discard the temp export; never delete the user's original file.
try? FileManager.default.removeItem(at: editedURL)
}
}

private func selectRegion(on display: DisplayDescriptor, completion: @escaping (CGRect?) -> Void) {
Task { @MainActor in
do {
Expand Down
18 changes: 18 additions & 0 deletions Sources/ZoomItMacCore/SelfTest/SelfTestRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public enum SelfTestRunner {
try testStatusMenuOrderMatchesWindows()
try testClipTransitionUpdatesOnChange()
try testWebcamOverlayDragOrigin()
try testTrimSavePreservesOriginal()
try testStaticZoomStaysAtOneX()
try testPanoramaStitching()
try testPanoramaTopSeamUsesSingleFramePixels()
Expand Down Expand Up @@ -573,6 +574,23 @@ public enum SelfTestRunner {
try expect(moved == CGPoint(x: 150, y: 130), "Expected dragged origin to track the cursor, got \(moved)")
}

/// Trimming an existing video and saving under a new name must NOT delete
/// the user's original file (it did, because the source was moved). When no
/// edits were made the editor returns the original URL and we copy it;
/// otherwise it returns an exported temp file that we move.
private static func testTrimSavePreservesOriginal() throws {
let original = URL(fileURLWithPath: "/tmp/original.mp4")

// No edits: editor hands back the original URL -> copy (preserve source).
try expect(RecordingController.trimSaveAction(editedURL: original, originalURL: original) == .copy,
"Expected an unedited trim save to copy the original, preserving it")

// Edited: editor exported a temp file -> move it (original untouched).
let exported = URL(fileURLWithPath: "/tmp/ZoomIt-edit-1234.mp4")
try expect(RecordingController.trimSaveAction(editedURL: exported, originalURL: original) == .move,
"Expected an edited trim save to move the exported temp file")
}

private static func testBreakTimerLayout() throws {
try expect(BreakTimerLayout.timerText(for: 601) == "10:01", "Expected positive break timer text to format as minutes and seconds")
try expect(BreakTimerLayout.timerText(for: 0) == "0:00", "Expected zero break timer text")
Expand Down