diff --git a/QuickRecorder.xcodeproj/project.pbxproj b/QuickRecorder.xcodeproj/project.pbxproj index 019761b..c65d37c 100644 --- a/QuickRecorder.xcodeproj/project.pbxproj +++ b/QuickRecorder.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1809EDEE2CA1116A00D8BDD7 /* Scriptable.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 1809EDED2CA1116A00D8BDD7 /* Scriptable.sdef */; }; + 1809EDF02CA112D200D8BDD7 /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1809EDEF2CA112D200D8BDD7 /* AppleScript.swift */; }; 181724052BE3BEA600F5F539 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 181724042BE3BEA600F5F539 /* Sparkle */; }; 181724082BE3BF7300F5F539 /* Sparkle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181724072BE3BF7300F5F539 /* Sparkle.swift */; }; 184506CE2C69213400AFDA45 /* MatrixColorSelector in Frameworks */ = {isa = PBXBuildFile; productRef = 184506CD2C69213400AFDA45 /* MatrixColorSelector */; }; @@ -38,6 +40,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1809EDED2CA1116A00D8BDD7 /* Scriptable.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Scriptable.sdef; sourceTree = ""; }; + 1809EDEF2CA112D200D8BDD7 /* AppleScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScript.swift; sourceTree = ""; }; 181724072BE3BF7300F5F539 /* Sparkle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sparkle.swift; sourceTree = ""; }; 184CAEC62BDCCC2300D61D57 /* AVContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVContext.swift; sourceTree = ""; }; 184CAEC82BDDEC6800D61D57 /* AppBlockSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBlockSelector.swift; sourceTree = ""; }; @@ -119,6 +123,8 @@ 18FEBDA92BCF8200003F09BC /* RecordEngine.swift */, 18D3BE032BCEC847006CFFC0 /* SCContext.swift */, 184CAEC62BDCCC2300D61D57 /* AVContext.swift */, + 1809EDED2CA1116A00D8BDD7 /* Scriptable.sdef */, + 1809EDEF2CA112D200D8BDD7 /* AppleScript.swift */, 181724072BE3BF7300F5F539 /* Sparkle.swift */, 18D3BE022BCEB1D4006CFFC0 /* ViewModel */, 18D3BDFD2BCE5DF5006CFFC0 /* Localizable.strings */, @@ -233,6 +239,7 @@ files = ( 18D3BDF22BCE5DC2006CFFC0 /* Preview Assets.xcassets in Resources */, 18D3BDFB2BCE5DF5006CFFC0 /* Localizable.strings in Resources */, + 1809EDEE2CA1116A00D8BDD7 /* Scriptable.sdef in Resources */, 18D3BDEF2BCE5DC2006CFFC0 /* Assets.xcassets in Resources */, 1862BF8D2BD5494E003ED522 /* Credits.rtf in Resources */, 189BD5E62BDE34B80056D06C /* InfoPlist.strings in Resources */, @@ -265,6 +272,7 @@ 18D3BDEB2BCE5DC1006CFFC0 /* QuickRecorderApp.swift in Sources */, 181724082BE3BF7300F5F539 /* Sparkle.swift in Sources */, 18FEBDAC2BD01E82003F09BC /* WinSelector.swift in Sources */, + 1809EDF02CA112D200D8BDD7 /* AppleScript.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -432,7 +440,7 @@ CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\""; DEVELOPMENT_TEAM = L4T783637F; @@ -450,7 +458,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.3; - MARKETING_VERSION = 1.4.7; + MARKETING_VERSION = 1.4.8; PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -466,7 +474,7 @@ CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\""; DEVELOPMENT_TEAM = L4T783637F; @@ -484,7 +492,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.3; - MARKETING_VERSION = 1.4.7; + MARKETING_VERSION = 1.4.8; PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/QuickRecorder/AppleScript.swift b/QuickRecorder/AppleScript.swift new file mode 100644 index 0000000..5b95f40 --- /dev/null +++ b/QuickRecorder/AppleScript.swift @@ -0,0 +1,233 @@ +// +// AppleScript.swift +// QuickRecorder +// +// Created by apple on 2024/9/23. +// + +import Foundation +import AppKit +import ScreenCaptureKit + +class selectScreen: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + if SCContext.stream != nil { + AppDelegate.shared.createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() + return nil + } + SCContext.updateAvailableContent{ + DispatchQueue.main.async { + AppDelegate.shared.closeAllWindow() + if var index = self.evaluatedArguments!["index"] as? Int { + guard let screens = SCContext.availableContent?.displays else { return } + index -= 1 + AppDelegate.shared.closeAllWindow() + if index >= screens.count || index < 0 { + AppDelegate.shared.createAlert(title: "Error".local, message: "Invalid screen number!".local, button1: "OK".local).runModal() + return + } else { + let screen = screens[index] + AppDelegate.shared.createCountdownPanel(screen: screen) { + AppDelegate.shared.prepRecord(type: "display", screens: screen, windows: nil, applications: nil) + } + } + } else { + AppDelegate.shared.closeAllWindow() + AppDelegate.shared.createNewWindow(view: ScreenSelector(), title: "Screen Selector".local) + } + } + } + return nil + } +} + +class selectArea: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + if SCContext.stream != nil { + AppDelegate.shared.createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() + return nil + } + SCContext.updateAvailableContent{ + DispatchQueue.main.async { + AppDelegate.shared.closeAllWindow() + DispatchQueue.main.async { + AppDelegate.shared.showAreaSelector(size: NSSize(width: 600, height: 450)) + var currentDisplay = SCContext.getSCDisplayWithMouse() + mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .rightMouseDown, .leftMouseDown, .otherMouseDown]) { event in + let display = SCContext.getSCDisplayWithMouse() + if display != currentDisplay { + currentDisplay = display + AppDelegate.shared.closeAllWindow() + AppDelegate.shared.showAreaSelector(size: NSSize(width: 600, height: 450)) + } + } + } + } + } + return nil + } +} + +class selectApps: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + if SCContext.stream != nil { + AppDelegate.shared.createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() + return nil + } + SCContext.updateAvailableContent{ + DispatchQueue.main.async { + AppDelegate.shared.closeAllWindow() + if let name = self.evaluatedArguments!["name"] as? String { + guard let app = SCContext.availableContent?.applications.first(where: { $0.applicationName == name }) else { + AppDelegate.shared.createAlert(title: "Error".local, message: "No such application!".local, button1: "OK".local).runModal() + return + } + AppDelegate.shared.closeAllWindow() + guard let screens = SCContext.availableContent?.displays else { return } + guard let windows = SCContext.availableContent?.windows.filter({ + guard let title = $0.title else { return false } + return !title.contains("Item-0") + && title != "Window" + && $0.frame.width > 40 + && $0.frame.height > 40 + }) else { return } + var s = [SCDisplay]() + for screen in screens { + for w in windows { + if NSIntersectsRect(screen.frame, w.frame) { if !s.contains(screen) { s.append(screen) }} + } + } + if s.isEmpty { + AppDelegate.shared.createAlert(title: "Error".local, message: "This application has no windows!".local, button1: "OK".local).runModal() + return + } + if s.count != 1 { + AppDelegate.shared.createNewWindow(view: AppSelector(), title: "App Selector".local) + AppDelegate.shared.createAlert(title: "Error".local, message: "This app exists in multiple screens, please select it manually!".local, button1: "OK".local).runModal() + } else { + AppDelegate.shared.createCountdownPanel(screen: s.first!) { + AppDelegate.shared.prepRecord(type: "application", screens: s.first!, windows: nil, applications: [app]) + } + } + } else { + AppDelegate.shared.closeAllWindow() + AppDelegate.shared.createNewWindow(view: AppSelector(), title: "App Selector".local) + } + } + } + return nil + } +} + +class selectWindows: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + if SCContext.stream != nil { + AppDelegate.shared.createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() + return nil + } + SCContext.updateAvailableContent{ + DispatchQueue.main.async { + AppDelegate.shared.closeAllWindow() + if let title = self.evaluatedArguments!["title"] as? String { + var windows = [SCWindow]() + guard let w = SCContext.availableContent?.windows.filter({ $0.title == title }) else { return } + windows = w + if let app = self.evaluatedArguments!["app"] as? String { + guard let w = SCContext.availableContent?.windows.filter({ $0.title == title && $0.owningApplication?.applicationName == app }) else { return } + windows = w + } + AppDelegate.shared.closeAllWindow() + if windows.isEmpty { + AppDelegate.shared.createAlert(title: "Error".local, message: "No such window!".local, button1: "OK".local).runModal() + return + } + if windows.count > 1 { + AppDelegate.shared.createNewWindow(view: WinSelector(), title: "Window Selector".local) + AppDelegate.shared.createAlert(title: "Error".local, message: "Duplicate window exists, please select it manually!".local, button1: "OK".local).runModal() + return + } + let window = windows.first! + guard let screens = SCContext.availableContent?.displays else { return } + var s = [SCDisplay]() + for screen in screens { + if NSIntersectsRect(screen.frame, window.frame) { if !s.contains(screen) { s.append(screen) }} + } + if s.isEmpty { + AppDelegate.shared.createAlert(title: "Error".local, message: "Unable to find the screen this window belongs to!".local, button1: "OK".local).runModal() + return + } + if let display = SCContext.getSCDisplayWithMouse() { + if s.contains(display) { + AppDelegate.shared.createCountdownPanel(screen: display) { + AppDelegate.shared.prepRecord(type: "window" , screens: s.first!, windows: [window], applications: nil) + } + } else { + AppDelegate.shared.createCountdownPanel(screen: s.first!) { + AppDelegate.shared.prepRecord(type: "window" , screens: s.first!, windows: [window], applications: nil) + } + } + } + } else { + AppDelegate.shared.closeAllWindow() + AppDelegate.shared.createNewWindow(view: WinSelector(), title: "Window Selector".local) + } + } + } + return nil + } +} + +class recordAudio: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + if SCContext.stream != nil { + AppDelegate.shared.createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() + return nil + } + SCContext.updateAvailableContent{ + DispatchQueue.main.async { + let m = UserDefaults.standard.bool(forKey: "recordMic") + if let mic = self.evaluatedArguments!["mic"] as? Bool { + UserDefaults.standard.set(mic, forKey: "recordMic") + } + AppDelegate.shared.closeAllWindow() + AppDelegate.shared.prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil) + UserDefaults.standard.set(m, forKey: "recordMic") + } + } + return nil + } +} + +class setPreferences: NSScriptCommand { + override func performDefaultImplementation() -> Any? { + if SCContext.stream != nil { + AppDelegate.shared.createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() + return nil + } + if let hires = self.evaluatedArguments!["hires"] as? Bool { UserDefaults.standard.set(hires, forKey: "highRes") } + if let fps = self.evaluatedArguments!["fps"] as? Int { UserDefaults.standard.set(fps, forKey: "frameRate") } + if let cursor = self.evaluatedArguments!["cursor"] as? Bool { UserDefaults.standard.set(cursor, forKey: "showMouse") } + if let sound = self.evaluatedArguments!["sound"] as? Bool { UserDefaults.standard.set(sound, forKey: "recordWinSound") } + if let microphone = self.evaluatedArguments!["microphone"] as? Bool { UserDefaults.standard.set(microphone, forKey: "recordMic") } + if let quality = self.evaluatedArguments!["quality"] as? Int { + if [1,2,3].contains(quality) { + switch quality { + case 1: UserDefaults.standard.set(0.3, forKey: "videoQuality") + case 2: UserDefaults.standard.set(0.7, forKey: "videoQuality") + default: UserDefaults.standard.set(1.0, forKey: "videoQuality") + } + } + } + if let micname = self.evaluatedArguments!["micname"] as? String { + if SCContext.getMicrophone().map({$0.localizedName}).contains(micname) || micname == "default" { + UserDefaults.standard.set(micname, forKey: "micDevice") + } + } + if let hdr = self.evaluatedArguments!["hdr"] as? Bool { + if #available(macOS 15.0, *) { + UserDefaults.standard.set(hdr, forKey: "recordHDR") + } + } + return nil + } +} diff --git a/QuickRecorder/Info.plist b/QuickRecorder/Info.plist index c11942e..b0b65aa 100644 --- a/QuickRecorder/Info.plist +++ b/QuickRecorder/Info.plist @@ -40,6 +40,10 @@ + NSAppleScriptEnabled + + OSAScriptingDefinition + Scriptable SUFeedURL https://raw.githubusercontent.com/lihaoyun6/QuickRecorder/main/appcast.xml SUPublicEDKey diff --git a/QuickRecorder/Scriptable.sdef b/QuickRecorder/Scriptable.sdef new file mode 100644 index 0000000..48503b0 --- /dev/null +++ b/QuickRecorder/Scriptable.sdef @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QuickRecorder/ViewModel/AppSelector.swift b/QuickRecorder/ViewModel/AppSelector.swift index 37b643f..f4f5d9c 100644 --- a/QuickRecorder/ViewModel/AppSelector.swift +++ b/QuickRecorder/ViewModel/AppSelector.swift @@ -230,6 +230,9 @@ struct OptionsView: View { //Text("Low (0.5x)").tag(0) }.buttonStyle(.borderless) Picker("", selection: $frameRate) { + if ![240, 144, 120, 90, 60, 30, 24, 15 ,10].contains(frameRate) { + Text("\(frameRate) FPS").tag(frameRate) + } Text("240 FPS").tag(240) Text("144 FPS").tag(144) Text("120 FPS").tag(120) diff --git a/QuickRecorder/ViewModel/CameraOverlayer.swift b/QuickRecorder/ViewModel/CameraOverlayer.swift index 1002845..ac3d3e9 100644 --- a/QuickRecorder/ViewModel/CameraOverlayer.swift +++ b/QuickRecorder/ViewModel/CameraOverlayer.swift @@ -138,7 +138,8 @@ struct CameraPopoverView: View { @State private var hoverIndex = -1 @State private var hoverIndex2 = -1 @State private var disabled = false - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + //@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var appDelegate = AppDelegate.shared var body: some View { VStack( alignment: .leading, spacing: 0) { diff --git a/QuickRecorder/ViewModel/ContentView.swift b/QuickRecorder/ViewModel/ContentView.swift index 04e22af..56e7ea8 100644 --- a/QuickRecorder/ViewModel/ContentView.swift +++ b/QuickRecorder/ViewModel/ContentView.swift @@ -21,7 +21,8 @@ struct ContentView: View { @AppStorage("enableAEC") private var enableAEC: Bool = false @AppStorage("recordMic") private var recordMic: Bool = false @AppStorage("micDevice") private var micDevice: String = "default" - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + //@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var appDelegate = AppDelegate.shared var body: some View { ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) { @@ -37,8 +38,12 @@ struct ContentView: View { if #available(macOS 13, *) { ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) { Button(action: { - appDelegate.closeMainWindow() - AppDelegate.shared.prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil) + if let display = SCContext.getSCDisplayWithMouse() { + appDelegate.closeMainWindow() + appDelegate.createCountdownPanel(screen: display) { + AppDelegate.shared.prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil) + } + } }, label: { SelectorView(title: "System Audio".local, symbol: "waveform") .cornerRadius(8) diff --git a/QuickRecorder/ViewModel/SettingsView.swift b/QuickRecorder/ViewModel/SettingsView.swift index ada422c..9feb306 100644 --- a/QuickRecorder/ViewModel/SettingsView.swift +++ b/QuickRecorder/ViewModel/SettingsView.swift @@ -13,7 +13,8 @@ import MatrixColorSelector struct SettingsView: View { @Environment(\.presentationMode) var presentationMode - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + //@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var appDelegate = AppDelegate.shared @State private var userColor: Color = Color.black @State private var launchAtLogin = false @AppStorage("encoder") private var encoder: Encoder = .h264 diff --git a/QuickRecorder/ViewModel/StatusBar.swift b/QuickRecorder/ViewModel/StatusBar.swift index baa4df8..8971c61 100644 --- a/QuickRecorder/ViewModel/StatusBar.swift +++ b/QuickRecorder/ViewModel/StatusBar.swift @@ -22,9 +22,10 @@ struct StatusBarItem: View { @State private var isHovering = false @State private var recordingLength = "00:00" @State private var isPassed = SCContext.isPaused - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + //@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @AppStorage("miniStatusBar") private var miniStatusBar: Bool = false @AppStorage("highlightMouse") private var highlightMouse: Bool = false + var appDelegate = AppDelegate.shared var body: some View { HStack(spacing: 0) { diff --git a/QuickRecorder/ViewModel/iDeviceSelector.swift b/QuickRecorder/ViewModel/iDeviceSelector.swift index abc878a..897af16 100644 --- a/QuickRecorder/ViewModel/iDeviceSelector.swift +++ b/QuickRecorder/ViewModel/iDeviceSelector.swift @@ -27,7 +27,8 @@ struct iDevicePopoverView: View { @State private var hoverIndex = -1 @State private var mute = false @State private var preset = AVCaptureSession.Preset.high - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + //@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var appDelegate = AppDelegate.shared var body: some View { VStack( alignment: .center, spacing: 0) { diff --git a/QuickRecorder/zh-Hans.lproj/Localizable.strings b/QuickRecorder/zh-Hans.lproj/Localizable.strings index e316e41..b79bf21 100644 --- a/QuickRecorder/zh-Hans.lproj/Localizable.strings +++ b/QuickRecorder/zh-Hans.lproj/Localizable.strings @@ -165,6 +165,15 @@ "The output path is a file instead of a folder!"= "所选的保存位置是一个文件, 应该是文件夹!"; "Use Mini Controller" = "使用迷你控制器"; "Check for Updates" = "立即检测程序更新"; +"Error" = "错误"; +"Already recording!" = "正在录制中!"; +"Invalid screen number!" = "无效的屏幕编号!"; +"No such application!" = "未找到此应用程序!"; +"This application has no windows!" = "此应用程序没有可录制的窗口!"; +"This app exists in multiple screens, please select it manually!" = "此应用程序同时存在于多个屏幕中, 请手动选择!"; +"No such window!" = "未找到此窗口!"; +"Duplicate window exists, please select it manually!" = "存在多个同名窗口, 请手动选择!"; +"Unable to find the screen this window belongs to!" = "无法确认此窗口所属的屏幕!"; // Please don't translate the following warning strings, it will make it difficult for me to read the logs "failed to fetch file for size indicator: %@" = "获取文件大小失败: ";