Skip to content

Commit

Permalink
added zh_Hans localization; added app-icon for windows list; improved…
Browse files Browse the repository at this point in the history
… recording indicator on menu bar; deprecate the AppKit entry in main.swift and replace it with native SwiftUI; create preferences window using SwiftUI instead of AppKit
  • Loading branch information
lihaoyun6 committed Apr 14, 2024
1 parent 67e4178 commit e1672ed
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 77 deletions.
35 changes: 26 additions & 9 deletions Azayaka/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ import AVFoundation
import AVFAudio
import Cocoa
import ScreenCaptureKit
import SwiftUI

@main
struct AzayakaApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene {
Settings {
Preferences()
}
}
}

class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOutput {
var vW: AVAssetWriter!
Expand All @@ -32,8 +44,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu

var statusItem: NSStatusItem!
var menu = NSMenu()
let info = NSMenuItem(title: "One moment, waiting on update", action: nil, keyEquivalent: "")
let noneAvailable = NSMenuItem(title: "None available", action: nil, keyEquivalent: "")
let info = NSMenuItem(title: "One moment, waiting on update".local, action: nil, keyEquivalent: "")
let noneAvailable = NSMenuItem(title: "None available".local, action: nil, keyEquivalent: "")
let preferences = NSWindow()
let ud = UserDefaults.standard

Expand Down Expand Up @@ -70,12 +82,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
if let error = error {
switch error {
case SCStreamError.userDeclined: self.requestPermissions()
default: print("[err] failed to fetch available content:", error.localizedDescription)
default: print("[err] failed to fetch available content:".local, error.localizedDescription)
}
return
}
self.availableContent = content
assert(self.availableContent?.displays.isEmpty != nil, "There needs to be at least one display connected")
assert(self.availableContent?.displays.isEmpty != nil, "There needs to be at least one display connected".local)
let frontOnly = UserDefaults.standard.bool(forKey: Preferences.frontAppKey)
DispatchQueue.main.async {
if buildMenu {
Expand All @@ -90,18 +102,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
func requestPermissions() {
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "Azayaka needs permissions!"
alert.informativeText = "Azayaka needs screen recording permissions, even if you only intend on recording audio."
alert.addButton(withTitle: "Open Settings")
alert.addButton(withTitle: "No thanks, quit")
alert.messageText = "Azayaka needs permissions!".local
alert.informativeText = "Azayaka needs screen recording permissions, even if you only intend on recording audio.".local
alert.addButton(withTitle: "Open Settings".local)
alert.addButton(withTitle: "No thanks, quit".local)
alert.alertStyle = .informational
if alert.runModal() == .alertFirstButtonReturn {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")!)
}
NSApp.terminate(self)
}
}

func applicationWillTerminate(_ aNotification: Notification) {
if stream != nil {
stopRecording()
Expand All @@ -112,3 +124,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
return true
}
}

extension String {
var local: String { return NSLocalizedString(self, comment: "") }
}

71 changes: 48 additions & 23 deletions Azayaka/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// Created by Martin Persson on 2022-12-26.
//

import SwiftUI
import ScreenCaptureKit

extension AppDelegate: NSMenuDelegate {
Expand All @@ -15,50 +15,52 @@ extension AppDelegate: NSMenuDelegate {
if streamType != nil { // recording?
var typeText = ""
if screen != nil {
typeText = "Display " + String((availableContent?.displays.firstIndex(where: { $0.displayID == screen?.displayID }))!+1)
typeText = "Display ".local + String((availableContent?.displays.firstIndex(where: { $0.displayID == screen?.displayID }))!+1)
} else if window != nil {
typeText = window?.owningApplication?.applicationName.uppercased() ?? "A window"
typeText = window?.owningApplication?.applicationName.uppercased() ?? "A window".local
} else {
typeText = "System Audio"
typeText = "System Audio".local
}
menu.addItem(header("Recording " + typeText, size: 12))
menu.addItem(header("Recording ".local + typeText, size: 12))

menu.addItem(NSMenuItem(title: "Stop Recording", action: #selector(stopRecording), keyEquivalent: ""))
menu.addItem(NSMenuItem(title: "Stop Recording".local, action: #selector(stopRecording), keyEquivalent: ""))
menu.addItem(NSMenuItem.separator())
menu.addItem(info)
} else {
menu.addItem(header("Audio-only"))
menu.addItem(header("Audio-only".local))

let audio = NSMenuItem(title: "System Audio", action: #selector(prepRecord), keyEquivalent: "")
let audio = NSMenuItem(title: "System Audio".local, action: #selector(prepRecord), keyEquivalent: "")
audio.identifier = NSUserInterfaceItemIdentifier(rawValue: "audio")
menu.addItem(audio)

menu.addItem(header("Displays"))
menu.addItem(NSMenuItem.separator())
menu.addItem(header("Displays".local))

for (i, display) in availableContent!.displays.enumerated() {
let displayItem = NSMenuItem(title: "Unknown Display", action: #selector(prepRecord), keyEquivalent: "")
let displayName = "Display \(i+1)" + (display.displayID == CGMainDisplayID() ? " (Main)" : "")
let displayItem = NSMenuItem(title: "Unknown Display".local, action: #selector(prepRecord), keyEquivalent: "")
let displayName = "Display ".local + "\(i+1)" + (display.displayID == CGMainDisplayID() ? " (Main)".local : "")
displayItem.attributedTitle = NSAttributedString(string: displayName)
displayItem.setAccessibilityLabel(displayName)
displayItem.title = display.displayID.description
displayItem.identifier = NSUserInterfaceItemIdentifier(rawValue: "display")
menu.addItem(displayItem)
}

menu.addItem(header("Windows"))
menu.addItem(NSMenuItem.separator())
menu.addItem(header("Windows".local))

noneAvailable.isHidden = true
menu.addItem(noneAvailable)
}

menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Preferences…", action: #selector(openPreferences), keyEquivalent: ","))
menu.addItem(NSMenuItem(title: "Quit Azayaka", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
menu.addItem(NSMenuItem(title: "Preferences…".local, action: #selector(openPreferences), keyEquivalent: ","))
menu.addItem(NSMenuItem(title: "Quit Azayaka".local, action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
statusItem.menu = menu
}

func updateMenu() {
if streamType != nil { // recording?
info.attributedTitle = NSAttributedString(string: "Duration: \(getRecordingLength())\nFile size: \(getRecordingSize())")
updateIcon()
info.attributedTitle = NSAttributedString(string: String(format: "Duration: %@\nFile size: %@".local, arguments: [getRecordingLength(), getRecordingSize()]))
} else {
for window in menu.items.filter({ $0.identifier?.rawValue == "window" }) {
let matchingWindow = availableContent!.windows.first(where: { window.title == $0.windowID.description })
Expand Down Expand Up @@ -106,22 +108,42 @@ extension AppDelegate: NSMenuDelegate {
newWindow(window: window)
}
}

func getAppIcon(forBundleIdentifier bundleIdentifier: String) -> NSImage? {
if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) {
let icon = NSWorkspace.shared.icon(forFile: appURL.path)
return icon
}
return nil
}

func newWindow(window: SCWindow) {
let win = NSMenuItem(title: "Unknown", action: #selector(prepRecord), keyEquivalent: "")
let win = NSMenuItem(title: "Unknown".local, action: #selector(prepRecord), keyEquivalent: "")
win.attributedTitle = getFancyWindowString(window: window)
win.title = String(window.windowID)
win.identifier = NSUserInterfaceItemIdentifier("window")
win.setAccessibilityLabel("App name: " + (window.owningApplication?.applicationName ?? "Unknown App") + ", window title: " + (window.title ?? "No title")) // VoiceOver will otherwise read the window ID (the item's non-attributed title)
win.setAccessibilityLabel("App name: ".local + (window.owningApplication?.applicationName ?? "Unknown App".local) + ", window title: ".local + (window.title ?? "No title".local)) // VoiceOver will otherwise read the window ID (the item's non-attributed title)
menu.insertItem(win, at: menu.numberOfItems - 3)

}

func getFancyWindowString(window: SCWindow) -> NSAttributedString {
let str = NSMutableAttributedString(string: (window.owningApplication?.applicationName ?? "Unknown App") + "\n")
str.append(NSAttributedString(string: window.title ?? "No title",
let appID = window.owningApplication?.bundleIdentifier ?? "Unknown App".local
let imageAttachment = NSTextAttachment()
imageAttachment.image = getAppIcon(forBundleIdentifier: appID)
imageAttachment.bounds = CGRectMake(0, -3, 16, 16)
let imageString = NSAttributedString(attachment: imageAttachment)

let str = NSMutableAttributedString(string: " " + (window.owningApplication?.applicationName ?? "Unknown App".local) + "\n")
str.append(NSAttributedString(string: window.title ?? "No title".local,
attributes: [.font: NSFont.systemFont(ofSize: 12, weight: .regular),
.foregroundColor: NSColor.secondaryLabelColor]))
return str

let output = NSMutableAttributedString(string: "")
output.append(imageString)
output.append(str)

return output
}

func header(_ title: String, size: CGFloat = 10) -> NSMenuItem {
Expand All @@ -144,7 +166,10 @@ extension AppDelegate: NSMenuDelegate {

func updateIcon() {
if let button = statusItem.button {
button.image = NSImage(systemSymbolName: self.streamType != nil ? "record.circle.fill" : "record.circle", accessibilityDescription: "Azayaka")
let iconView = NSHostingView(rootView: MenuBar(recordingStatus: self.streamType != nil, recordingLength: getRecordingLength()))
iconView.frame = NSRect(x: 0, y: 1, width: self.streamType != nil ? 72 : 32, height: 20)
button.subviews = [iconView]
button.frame = iconView.frame
}
}
}
37 changes: 37 additions & 0 deletions Azayaka/MenuBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// MenuBar.swift
// Azayaka
//
// Created by apple on 2024/4/14.
//

import SwiftUI
import Foundation

struct MenuBar: View {
@State var recordingStatus: Bool!
@State var recordingLength = "00:00"
var body: some View {
ZStack {
if recordingStatus {
Rectangle()
.cornerRadius(3)
.opacity(0.1)
}
HStack(spacing: 2.5) {
if recordingStatus {
Image(systemName: "record.circle")
.foregroundStyle(.red)
Text(recordingLength)
.offset(y: -0.5)
} else {
Image(systemName: "record.circle")
}
}
}
}
}

#Preview {
MenuBar()
}
53 changes: 33 additions & 20 deletions Azayaka/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ struct Preferences: View {

var body: some View {
VStack(alignment: .leading) {
GroupBox(label: Text("Video Output".uppercased()).fontWeight(.bold)) {
GroupBox(label: Text("Video Output".local.uppercased()).fontWeight(.bold)) {
Form() {
Picker("FPS", selection: $frameRate) {
Text("60").tag(60)
Expand Down Expand Up @@ -52,7 +52,7 @@ struct Preferences: View {
Text("Show mouse cursor")
}.toggleStyle(CheckboxToggleStyle()).padding(.bottom, 10)
}
GroupBox(label: Text("Audio Output".uppercased()).fontWeight(.bold)) {
GroupBox(label: Text("Audio Output".local.uppercased()).fontWeight(.bold)) {
Form() {
Picker("Format", selection: $audioFormat) {
Text("AAC").tag(AudioFormat.aac)
Expand Down Expand Up @@ -94,7 +94,7 @@ struct Preferences: View {
Spacer()
VStack(spacing: 2) {
Button("Select output directory", action: updateOutputDirectory)
Text("Currently set to \"\(URL(fileURLWithPath: saveDirectory!).lastPathComponent)\"").font(.footnote).foregroundColor(Color.gray)
Text(String(format: "Currently set to \"%@\"".local, URL(fileURLWithPath: saveDirectory!).lastPathComponent)).font(.footnote).foregroundColor(Color.gray)
}.frame(maxWidth: .infinity)
}.frame(width: 260).padding([.leading, .trailing, .top], 10)
HStack {
Expand All @@ -111,10 +111,10 @@ struct Preferences: View {
recordMic = false
DispatchQueue.main.async {
let alert = NSAlert()
alert.messageText = "Azayaka needs permissions!"
alert.informativeText = "Azayaka needs permission to record your microphone to do this."
alert.addButton(withTitle: "Open Settings")
alert.addButton(withTitle: "No thanks")
alert.messageText = "Azayaka needs permissions!".local
alert.informativeText = "Azayaka needs permission to record your microphone to do this.".local
alert.addButton(withTitle: "Open Settings".local)
alert.addButton(withTitle: "No thanks".local)
alert.alertStyle = .warning
if alert.runModal() == .alertFirstButtonReturn {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")!)
Expand All @@ -134,11 +134,11 @@ struct Preferences: View {
}

func getVersion() -> String {
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown"
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown".local
}

func getBuild() -> String {
return Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown"
return Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown".local
}

struct VisualEffectView: NSViewRepresentable {
Expand All @@ -147,21 +147,34 @@ struct Preferences: View {
}
}

struct Preferences_Previews: PreviewProvider {
static var previews: some View {
Preferences()
}
#Preview {
Preferences()
}

extension AppDelegate {
@objc func openPreferences() {
preferences.isReleasedWhenClosed = false // otherwise we crash when opening the window again, WTF?
preferences.title = "Azayaka"
//preferences.subtitle = "Preferences"
preferences.contentView = NSHostingView(rootView: Preferences()) // is this how you SwiftUI help I'm scared
preferences.styleMask = [.titled, .closable]
preferences.center()
NSApp.activate(ignoringOtherApps: true)
preferences.makeKeyAndOrderFront(nil)
if #available(macOS 14, *) {
NSApp.mainMenu?.items.first?.submenu?.item(at: 2)?.performAction()
}else if #available(macOS 13, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
for w in NSApplication.shared.windows {
if w.level.rawValue == 0 || w.level.rawValue == 3 {
w.level = .floating
w.styleMask.remove(.resizable)
}
}
}
}

extension NSMenuItem {
func performAction() {
guard let menu else {
return
}
menu.performActionForItem(at: menu.index(of: self))
}
}
10 changes: 5 additions & 5 deletions Azayaka/Processing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension AppDelegate {
switch fileEnding {
case VideoFormat.mov.rawValue: fileType = AVFileType.mov
case VideoFormat.mp4.rawValue: fileType = AVFileType.mp4
default: assertionFailure("loaded unknown video format")
default: assertionFailure("loaded unknown video format".local)
}

filePath = "\(getFilePath()).\(fileEnding)"
Expand Down Expand Up @@ -112,20 +112,20 @@ extension AppDelegate {
do {
try audioFile?.write(from: samples)
}
catch { assertionFailure("audio file writing issue") }
catch { assertionFailure("audio file writing issue".local) }
} else { // otherwise send the audio data to AVAssetWriter
if awInput.isReadyForMoreMediaData {
awInput.append(sampleBuffer)
}
}
@unknown default:
assertionFailure("unknown stream type")
assertionFailure("unknown stream type".local)
}
}

func stream(_ stream: SCStream, didStopWithError error: Error) { // stream error
print("closing stream with error:\n", error,
"\nthis might be due to the window closing or the user stopping from the sonoma ui")
print("closing stream with error:\n".local, error,
"\nthis might be due to the window closing or the user stopping from the sonoma ui".local)
DispatchQueue.main.async {
self.stream = nil
self.stopRecording()
Expand Down
Loading

0 comments on commit e1672ed

Please sign in to comment.