Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ run = "swiftformat ."

[tasks.build]
description = "Build the app"
run = "xcodebuild -scheme CaptureThis -configuration Debug -derivedDataPath .build -destination 'platform=macOS' build"
run = ["mise run generate", "xcodebuild -scheme CaptureThis -configuration Debug -derivedDataPath .build -destination 'platform=macOS' build"]

[tasks.test]
description = "Run unit tests"
Expand Down
43 changes: 43 additions & 0 deletions Sources/App/AppState+GIFExport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import AppKit
import CaptureThisCore
import Foundation

extension AppState {
func exportGIF(for recording: Recording) {
Task { @MainActor in
await exportGIF(recording)
}
}

func exportGIFFromNotification(fileURL: URL) {
let recording = recentRecordings.first { $0.url == fileURL } ?? Recording(
id: UUID(),
url: fileURL,
createdAt: Date(),
duration: nil,
captureType: .display
)

Task { @MainActor in
await exportGIF(recording)
}
}

private func exportGIF(_ recording: Recording) async {
gifExportState = .exporting(recording.id)
errorMessage = nil

do {
let outputURL = try await withRecordingsDirectoryAccess {
let gifExportService = GIFExportService(policy: GIFExportPolicy(quality: settings.gifExportQuality))
return try await gifExportService.export(recording: recording)
}
gifExportState = .idle
notificationService.sendGIFExportCompleteNotification(url: outputURL)
revealRecordingURL(outputURL)
} catch {
gifExportState = .idle
errorMessage = error.localizedDescription
}
}
}
21 changes: 21 additions & 0 deletions Sources/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class AppState: ObservableObject {
@Published var presenterOverlayEnabled = false
@Published var recordingStartDate: Date?
@Published var permissionSetupState = PermissionSetupState()
@Published var gifExportState: GIFExportState = .idle

let engine: RecordingEngine
let cameraService = CameraService()
Expand Down Expand Up @@ -143,4 +144,24 @@ final class AppState: ObservableObject {
}
}
}

func withRecordingsDirectoryAccess<T>(_ operation: () async throws -> T) async throws -> T {
_ = try await fileAccessService.ensureRecordingsDirectoryAccess()
defer {
fileAccessService.stopAccessingIfNeeded()
}
return try await operation()
}
}

enum GIFExportState: Equatable {
case idle
case exporting(UUID)

func isExporting(_ recording: Recording) -> Bool {
if case let .exporting(id) = self {
return id == recording.id
}
return false
}
}
4 changes: 4 additions & 0 deletions Sources/Core/Extensions/RecordingSettings+Updating.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public extension RecordingSettings {
systemAudioEnabled: Bool? = nil,
outputFormat: RecordingFileFormat? = nil,
recordingQuality: RecordingQuality? = nil,
gifExportQuality: GIFExportQuality? = nil,
debugModeEnabled: Bool? = nil
) -> RecordingSettings {
var copy = self
Expand All @@ -29,6 +30,9 @@ public extension RecordingSettings {
if let recordingQuality {
copy.recordingQuality = recordingQuality
}
if let gifExportQuality {
copy.gifExportQuality = gifExportQuality
}
if let debugModeEnabled {
copy.isDebugModeEnabled = debugModeEnabled
}
Expand Down
41 changes: 41 additions & 0 deletions Sources/Core/Models/RecordingSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,51 @@ public enum RecordingQuality: String, Codable, CaseIterable, Identifiable, Senda
}
}

public enum GIFExportQuality: String, Codable, CaseIterable, Identifiable, Sendable {
case compact
case balanced
case high

public var id: String {
rawValue
}

public var displayName: String {
switch self {
case .compact:
String(localized: "Compact", bundle: .captureThisCore)
case .balanced:
String(localized: "Balanced", bundle: .captureThisCore)
case .high:
String(localized: "High", bundle: .captureThisCore)
}
}

public var targetWidth: Double {
switch self {
case .compact: 1080
case .balanced: 1440
case .high: 2160
}
}

public var framesPerSecond: Double {
switch self {
case .compact: 15
case .balanced: 24
case .high: 24
}
}
}

public struct RecordingSettings: Codable, Sendable {
public var countdownSeconds: Int
public var isCameraEnabled: Bool
public var isMicrophoneEnabled: Bool
public var isSystemAudioEnabled: Bool
public var outputFormat: RecordingFileFormat
public var recordingQuality: RecordingQuality
public var gifExportQuality: GIFExportQuality
public var isDebugModeEnabled: Bool

public init(
Expand All @@ -52,6 +90,7 @@ public struct RecordingSettings: Codable, Sendable {
isSystemAudioEnabled: Bool = false,
outputFormat: RecordingFileFormat = .mp4,
recordingQuality: RecordingQuality = .standard,
gifExportQuality: GIFExportQuality = .balanced,
isDebugModeEnabled: Bool = false
) {
self.countdownSeconds = countdownSeconds
Expand All @@ -60,6 +99,7 @@ public struct RecordingSettings: Codable, Sendable {
self.isSystemAudioEnabled = isSystemAudioEnabled
self.outputFormat = outputFormat
self.recordingQuality = recordingQuality
self.gifExportQuality = gifExportQuality
self.isDebugModeEnabled = isDebugModeEnabled
}

Expand All @@ -76,6 +116,7 @@ public struct RecordingSettings: Codable, Sendable {
isSystemAudioEnabled = try value(.isSystemAudioEnabled, defaults.isSystemAudioEnabled)
outputFormat = try value(.outputFormat, defaults.outputFormat)
recordingQuality = try value(.recordingQuality, defaults.recordingQuality)
gifExportQuality = try value(.gifExportQuality, defaults.gifExportQuality)
isDebugModeEnabled = try value(.isDebugModeEnabled, defaults.isDebugModeEnabled)
}
}
4 changes: 3 additions & 1 deletion Sources/Core/Models/RecordingState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public enum RecordingState: Equatable, Sendable {
case error(String)

public var isRecording: Bool {
if case .recording = self { return true }
if case .recording = self {
return true
}
return false
}

Expand Down
16 changes: 12 additions & 4 deletions Sources/Core/RecordingEngine+Flow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,16 @@ extension RecordingEngine {

func runCountdown(total: Int, filter: SCContentFilter) async {
for remaining in stride(from: total, through: 1, by: -1) {
if Task.isCancelled { return }
if Task.isCancelled {
return
}
setState(.countdown(remaining))
try? await Task.sleep(nanoseconds: 1_000_000_000)
}

if Task.isCancelled { return }
if Task.isCancelled {
return
}
await beginRecording(with: filter)
}

Expand Down Expand Up @@ -207,12 +211,16 @@ extension RecordingEngine {

if settings.isCameraEnabled {
let granted = await permissionService.requestCameraAccess()
if !granted { throw AppError.permissionDenied }
if !granted {
throw AppError.permissionDenied
}
}

if settings.isMicrophoneEnabled {
let granted = await permissionService.requestMicrophoneAccess()
if !granted { throw AppError.permissionDenied }
if !granted {
throw AppError.permissionDenied
}
}
}

Expand Down
Loading
Loading