1010
1111import AppKit
1212import AVFoundation
13+ import Combine
1314import MarkdownEditor
1415
1516// MARK: - 应用程序委托
@@ -28,8 +29,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
2829 private var preventSleepToolbarItem : NSToolbarItem ?
2930 private var layoutModeToolbarItem : NSToolbarItem ?
3031 private var layoutModeSegmentedControl : NSSegmentedControl ?
32+ private var recordingToolbarItem : NSToolbarItem ?
33+ private var recordingOutputToolbarItem : NSToolbarItem ?
34+ private var recordingOutputButton : NSButton ?
35+ private var recordingOutputWidthConstraint : NSLayoutConstraint ?
3136 private var markdownToggleToolbarItem : NSToolbarItem ?
3237 private var isRefreshing : Bool = false
38+ private var cancellables = Set < AnyCancellable > ( )
3339
3440 // MARK: - 菜单项
3541
@@ -56,6 +62,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
5662
5763 // MARK: - 应用生命周期
5864
65+ @MainActor
5966 func applicationDidFinishLaunching( _ notification: Notification ) {
6067 AppLogger . app. info ( " 应用启动 " )
6168
@@ -144,6 +151,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
144151 updateLayoutModeToolbarState ( )
145152 }
146153
154+ @MainActor
147155 @objc private func handleLanguageChange( ) {
148156 // 重建主菜单
149157 setupMainMenu ( )
@@ -160,6 +168,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
160168 mainViewController? . updateLocalizedTexts ( )
161169 }
162170
171+ @MainActor
163172 private func rebuildWindowToolbar( for window: NSWindow ) {
164173 // 移除旧工具栏
165174 window. toolbar = nil
@@ -168,10 +177,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
168177 preventSleepToolbarItem = nil
169178 layoutModeToolbarItem = nil
170179 layoutModeSegmentedControl = nil
180+ recordingToolbarItem = nil
181+ recordingOutputToolbarItem = nil
182+ recordingOutputButton = nil
183+ recordingOutputWidthConstraint = nil
171184 markdownToggleToolbarItem = nil
172185
173186 // 创建新工具栏
174187 setupWindowToolbar ( for: window)
188+ updateRecordingUI ( )
175189 }
176190
177191 /// 请求摄像头权限
@@ -1153,6 +1167,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
11531167 }
11541168 // MARK: - 窗口设置
11551169
1170+ @MainActor
11561171 private func setupMainWindow( ) {
11571172 // 创建主视图控制器
11581173 mainViewController = MainViewController ( )
@@ -1175,6 +1190,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
11751190
11761191 // 设置窗口工具栏
11771192 setupWindowToolbar ( for: window)
1193+ setupRecordingObservation ( )
11781194
11791195 // 设置窗口代理
11801196 window. delegate = self
@@ -1199,6 +1215,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FormatMenuProvider {
11991215 window. toolbar = toolbar
12001216 windowToolbar = toolbar
12011217 }
1218+
1219+ @MainActor
1220+ private func setupRecordingObservation( ) {
1221+ AppState . shared. recordingService. stateChangedPublisher
1222+ . receive ( on: DispatchQueue . main)
1223+ . sink { [ weak self] in
1224+ self ? . updateRecordingUI ( )
1225+ }
1226+ . store ( in: & cancellables)
1227+ }
12021228}
12031229
12041230// MARK: - 窗口代理
@@ -1471,6 +1497,46 @@ extension AppDelegate {
14711497 ToastView . success ( L10n . toolbar. refreshComplete, in: mainWindow)
14721498 }
14731499
1500+ // MARK: - 录制操作
1501+
1502+ @IBAction func toggleRecording( _ sender: Any ? ) {
1503+ let recordingService = AppState . shared. recordingService
1504+
1505+ if recordingService. state. isRecording {
1506+ let directory = recordingService. stopRecording ( )
1507+ AppState . shared. iosDeviceSource? . stopAudioCaptureForCurrentSession ( )
1508+ updateRecordingUI ( )
1509+ if directory != nil {
1510+ ToastView . success ( L10n . recording. saved, in: mainWindow)
1511+ }
1512+ return
1513+ }
1514+
1515+ Task {
1516+ do {
1517+ try await recordingService. startRecording ( )
1518+ await MainActor . run {
1519+ updateRecordingUI ( )
1520+ ToastView . success ( L10n . recording. started, in: mainWindow)
1521+ }
1522+ } catch {
1523+ await MainActor . run {
1524+ updateRecordingUI ( )
1525+ ToastView . error ( error. localizedDescription, in: mainWindow)
1526+ }
1527+ }
1528+ }
1529+ }
1530+
1531+ @MainActor
1532+ @objc private func openRecordingOutputDirectory( _ sender: Any ? ) {
1533+ guard let directory = AppState . shared. recordingService. lastOutputDirectory else {
1534+ NSSound . beep ( )
1535+ return
1536+ }
1537+ NSWorkspace . shared. activateFileViewerSelecting ( [ directory] )
1538+ }
1539+
14741540 // MARK: - 面板交换操作
14751541
14761542 @IBAction func swapPanels( _ sender: Any ? ) {
@@ -1757,6 +1823,70 @@ extension AppDelegate {
17571823 item. image = image
17581824 }
17591825
1826+ @MainActor
1827+ private func updateRecordingUI( ) {
1828+ let service = AppState . shared. recordingService
1829+ let isRecording = service. state. isRecording
1830+ let shouldShowOutput = service. lastOutputDirectory != nil && !isRecording
1831+
1832+ if let item = recordingToolbarItem {
1833+ updateRecordingToolbarItemImage ( item)
1834+ }
1835+
1836+ updateRecordingOutputToolbarPresence ( shouldShowOutput)
1837+
1838+ if let outputButton = recordingOutputButton {
1839+ if let directory = service. lastOutputDirectory, shouldShowOutput {
1840+ outputButton. title = L10n . recording. savedLocation (
1841+ ( directory. path as NSString ) . abbreviatingWithTildeInPath
1842+ )
1843+ outputButton. toolTip = L10n . recording. openOutputDirectory
1844+ outputButton. isHidden = false
1845+ recordingOutputWidthConstraint? . constant = 320
1846+ } else {
1847+ outputButton. title = " "
1848+ outputButton. toolTip = nil
1849+ outputButton. isHidden = true
1850+ recordingOutputWidthConstraint? . constant = 0
1851+ }
1852+ }
1853+
1854+ mainViewController? . setRecordingIndicatorVisible ( isRecording, elapsedSeconds: service. elapsedSeconds)
1855+ }
1856+
1857+ @MainActor
1858+ private func updateRecordingOutputToolbarPresence( _ shouldShow: Bool ) {
1859+ guard let toolbar = windowToolbar else { return }
1860+
1861+ let items = toolbar. items
1862+ let outputIndex = items. firstIndex { $0. itemIdentifier == ToolbarItemIdentifier . recordingOutput }
1863+
1864+ if shouldShow {
1865+ guard outputIndex == nil else { return }
1866+
1867+ let recordingIndex = items. firstIndex { $0. itemIdentifier == ToolbarItemIdentifier . recording } ?? 0
1868+ toolbar. insertItem ( withItemIdentifier: ToolbarItemIdentifier . recordingOutput, at: recordingIndex)
1869+ } else if let outputIndex {
1870+ toolbar. removeItem ( at: outputIndex)
1871+ recordingOutputToolbarItem = nil
1872+ recordingOutputButton = nil
1873+ recordingOutputWidthConstraint = nil
1874+ }
1875+ }
1876+
1877+ @MainActor
1878+ private func updateRecordingToolbarItemImage( _ item: NSToolbarItem ) {
1879+ let isRecording = AppState . shared. recordingService. state. isRecording
1880+ let symbolName = isRecording ? " stop.circle.fill " : " record.circle "
1881+ item. label = isRecording ? L10n . recording. stop : L10n . recording. record
1882+ item. paletteLabel = L10n . recording. record
1883+ item. toolTip = isRecording ? L10n . recording. stop : L10n . recording. record
1884+ item. image = NSImage (
1885+ systemSymbolName: symbolName,
1886+ accessibilityDescription: item. toolTip
1887+ )
1888+ }
1889+
17601890 private func updateLayoutModeToolbarState( ) {
17611891 guard let segmentedControl = layoutModeSegmentedControl else { return }
17621892 let currentMode = UserPreferences . shared. layoutMode
@@ -1797,6 +1927,8 @@ extension AppDelegate: NSMenuDelegate {
17971927
17981928extension AppDelegate : NSToolbarDelegate {
17991929 private enum ToolbarItemIdentifier {
1930+ static let recording = NSToolbarItem . Identifier ( " recording " )
1931+ static let recordingOutput = NSToolbarItem . Identifier ( " recordingOutput " )
18001932 static let layoutMode = NSToolbarItem . Identifier ( " layoutMode " )
18011933 static let refresh = NSToolbarItem . Identifier ( " refresh " )
18021934 static let toggleBezel = NSToolbarItem . Identifier ( " toggleBezel " )
@@ -1808,6 +1940,7 @@ extension AppDelegate: NSToolbarDelegate {
18081940
18091941 func toolbarDefaultItemIdentifiers( _ toolbar: NSToolbar ) -> [ NSToolbarItem . Identifier ] {
18101942 [
1943+ ToolbarItemIdentifier . recording,
18111944 ToolbarItemIdentifier . markdownToggle,
18121945 . space,
18131946 ToolbarItemIdentifier . layoutMode,
@@ -1820,6 +1953,8 @@ extension AppDelegate: NSToolbarDelegate {
18201953
18211954 func toolbarAllowedItemIdentifiers( _ toolbar: NSToolbar ) -> [ NSToolbarItem . Identifier ] {
18221955 [
1956+ ToolbarItemIdentifier . recording,
1957+ ToolbarItemIdentifier . recordingOutput,
18231958 ToolbarItemIdentifier . refresh,
18241959 ToolbarItemIdentifier . markdownToggle,
18251960 ToolbarItemIdentifier . layoutMode,
@@ -1835,6 +1970,39 @@ extension AppDelegate: NSToolbarDelegate {
18351970 willBeInsertedIntoToolbar flag: Bool
18361971 ) -> NSToolbarItem ? {
18371972 switch itemIdentifier {
1973+ case ToolbarItemIdentifier . recording:
1974+ let item = NSToolbarItem ( itemIdentifier: itemIdentifier)
1975+ item. target = self
1976+ item. action = #selector( toggleRecording ( _: ) )
1977+ recordingToolbarItem = item
1978+ updateRecordingToolbarItemImage ( item)
1979+ return item
1980+
1981+ case ToolbarItemIdentifier . recordingOutput:
1982+ let item = NSToolbarItem ( itemIdentifier: itemIdentifier)
1983+ item. label = L10n . recording. saved
1984+ item. paletteLabel = L10n . recording. saved
1985+
1986+ let button = NSButton ( title: " " , target: self , action: #selector( openRecordingOutputDirectory ( _: ) ) )
1987+ button. bezelStyle = . rounded
1988+ button. isBordered = true
1989+ button. font = NSFont . systemFont ( ofSize: 12 )
1990+ button. lineBreakMode = . byTruncatingMiddle
1991+ button. setContentCompressionResistancePriority ( . defaultLow, for: . horizontal)
1992+ button. translatesAutoresizingMaskIntoConstraints = false
1993+ button. isHidden = true
1994+
1995+ let widthConstraint = button. widthAnchor. constraint ( equalToConstant: 0 )
1996+ let heightConstraint = button. heightAnchor. constraint ( equalToConstant: 28 )
1997+ widthConstraint. isActive = true
1998+ heightConstraint. isActive = true
1999+
2000+ item. view = button
2001+ recordingOutputToolbarItem = item
2002+ recordingOutputButton = button
2003+ recordingOutputWidthConstraint = widthConstraint
2004+ return item
2005+
18382006 case ToolbarItemIdentifier . layoutMode:
18392007 let item = NSToolbarItem ( itemIdentifier: itemIdentifier)
18402008 item. label = L10n . toolbar. layoutMode
0 commit comments