1+ using System . Diagnostics ;
12using System . Runtime . InteropServices ;
23using UniGetUI . Core . Data ;
34using UniGetUI . Core . Logging ;
910namespace UniGetUI . Avalonia . Infrastructure ;
1011
1112/// <summary>
12- /// macOS system notification delivery via NSUserNotificationCenter (ObjC runtime P/Invoke).
13- /// Mirrors the pattern of WindowsAppNotificationBridge: guards on OS check, silent fallback on failure.
13+ /// macOS system notification delivery via osascript (works on all macOS versions).
14+ /// NSUserNotificationCenter was removed in macOS 14; UNUserNotificationCenter requires
15+ /// ObjC blocks that are impractical via pure P/Invoke. osascript is always available.
16+ /// Callers are responsible for the OperatingSystem.IsMacOS() guard before invoking.
1417/// </summary>
1518internal static class MacOsNotificationBridge
1619{
17- private static bool ? _available ;
18- private static readonly object _lock = new ( ) ;
19-
20- private static bool IsAvailable ( )
21- {
22- if ( ! OperatingSystem . IsMacOS ( ) ) return false ;
23- lock ( _lock )
24- {
25- if ( _available . HasValue ) return _available . Value ;
26- try
27- {
28- _available = objc_getClass ( "NSUserNotificationCenter" ) != IntPtr . Zero ;
29- }
30- catch
31- {
32- _available = false ;
33- }
34- return _available . Value ;
35- }
36- }
37-
3820 // ── Operation notifications ────────────────────────────────────────────
3921
4022 public static bool ShowProgress ( AbstractOperation operation )
4123 {
42- if ( ! IsAvailable ( ) || Settings . AreProgressNotificationsDisabled ( ) ) return false ;
24+ if ( Settings . AreProgressNotificationsDisabled ( ) ) return false ;
4325 try
4426 {
4527 string title = operation . Metadata . Title . Length > 0
@@ -61,7 +43,7 @@ public static bool ShowProgress(AbstractOperation operation)
6143
6244 public static bool ShowSuccess ( AbstractOperation operation )
6345 {
64- if ( ! IsAvailable ( ) || Settings . AreSuccessNotificationsDisabled ( ) ) return false ;
46+ if ( Settings . AreSuccessNotificationsDisabled ( ) ) return false ;
6547 try
6648 {
6749 string title = operation . Metadata . SuccessTitle . Length > 0
@@ -83,7 +65,7 @@ public static bool ShowSuccess(AbstractOperation operation)
8365
8466 public static bool ShowError ( AbstractOperation operation )
8567 {
86- if ( ! IsAvailable ( ) || Settings . AreErrorNotificationsDisabled ( ) ) return false ;
68+ if ( Settings . AreErrorNotificationsDisabled ( ) ) return false ;
8769 try
8870 {
8971 string title = operation . Metadata . FailureTitle . Length > 0
@@ -107,7 +89,7 @@ public static bool ShowError(AbstractOperation operation)
10789
10890 public static void ShowUpdatesAvailableNotification ( IReadOnlyList < IPackage > upgradable )
10991 {
110- if ( ! IsAvailable ( ) || Settings . AreUpdatesNotificationsDisabled ( ) ) return ;
92+ if ( Settings . AreUpdatesNotificationsDisabled ( ) ) return ;
11193 try
11294 {
11395 string title , message ;
@@ -131,9 +113,34 @@ public static void ShowUpdatesAvailableNotification(IReadOnlyList<IPackage> upgr
131113 }
132114 }
133115
116+ public static void ShowUpgradingPackagesNotification ( IReadOnlyList < IPackage > upgradable )
117+ {
118+ if ( Settings . AreUpdatesNotificationsDisabled ( ) ) return ;
119+ try
120+ {
121+ string title , message ;
122+ if ( upgradable . Count == 1 )
123+ {
124+ title = CoreTools . Translate ( "An update was found!" ) ;
125+ message = CoreTools . Translate ( "{0} is being updated to version {1}" ,
126+ upgradable [ 0 ] . Name , upgradable [ 0 ] . NewVersionString ) ;
127+ }
128+ else
129+ {
130+ title = CoreTools . Translate ( "{0} packages are being updated" , upgradable . Count ) ;
131+ message = string . Join ( ", " , upgradable . Select ( p => p . Name ) ) ;
132+ }
133+ DeliverNotification ( title , message ) ;
134+ }
135+ catch ( Exception ex )
136+ {
137+ Logger . Warn ( "macOS upgrading-packages notification failed" ) ;
138+ Logger . Warn ( ex ) ;
139+ }
140+ }
141+
134142 public static void ShowSelfUpdateAvailableNotification ( string newVersion )
135143 {
136- if ( ! IsAvailable ( ) ) return ;
137144 try
138145 {
139146 DeliverNotification (
@@ -149,7 +156,7 @@ public static void ShowSelfUpdateAvailableNotification(string newVersion)
149156
150157 public static void ShowNewShortcutsNotification ( IReadOnlyList < string > shortcuts )
151158 {
152- if ( ! IsAvailable ( ) || Settings . AreNotificationsDisabled ( ) ) return ;
159+ if ( Settings . AreNotificationsDisabled ( ) ) return ;
153160 try
154161 {
155162 string title , message ;
@@ -180,38 +187,25 @@ public static void ShowNewShortcutsNotification(IReadOnlyList<string> shortcuts)
180187
181188 private static void DeliverNotification ( string title , string message )
182189 {
183- var centerClass = objc_getClass ( "NSUserNotificationCenter" ) ;
184- var center = MsgSend ( centerClass , Sel ( "defaultUserNotificationCenter" ) ) ;
185-
186- var notifClass = objc_getClass ( "NSUserNotification" ) ;
187- var notif = MsgSend ( MsgSend ( notifClass , Sel ( "alloc" ) ) , Sel ( "init" ) ) ;
188-
189- MsgSend ( notif , Sel ( "setTitle:" ) , ToNSString ( title ) ) ;
190- MsgSend ( notif , Sel ( "setInformativeText:" ) , ToNSString ( message ) ) ;
191- MsgSend ( center , Sel ( "deliverNotification:" ) , notif ) ;
192- MsgSend ( notif , Sel ( "autorelease" ) ) ;
193- }
194-
195- private static IntPtr ToNSString ( string s )
196- {
197- IntPtr ptr = Marshal . StringToCoTaskMemUTF8 ( s ) ;
198- try
199- {
200- return MsgSend ( objc_getClass ( "NSString" ) , Sel ( "stringWithUTF8String:" ) , ptr ) ;
201- }
202- finally
190+ // NSUserNotificationCenter was removed in macOS 14; osascript works on all versions.
191+ string script = "display notification " + AppleScriptString ( message )
192+ + " with title " + AppleScriptString ( title ) ;
193+ Process . Start ( new ProcessStartInfo
203194 {
204- Marshal . FreeCoTaskMem ( ptr ) ;
205- }
195+ FileName = "/usr/bin/osascript" ,
196+ ArgumentList = { "-e" , script } ,
197+ UseShellExecute = false ,
198+ CreateNoWindow = true ,
199+ } ) ;
206200 }
207201
208- private static IntPtr Sel ( string name ) => sel_registerName ( name ) ;
202+ private static string AppleScriptString ( string s ) =>
203+ "\" " + s . Replace ( "\\ " , "\\ \\ " ) . Replace ( "\" " , "\\ \" " ) + "\" " ;
209204
210205 // ── Dock icon ──────────────────────────────────────────────────────────
211206
212207 public static void SetDockIcon ( byte [ ] pngBytes )
213208 {
214- if ( ! OperatingSystem . IsMacOS ( ) ) return ;
215209 try
216210 {
217211 var handle = GCHandle . Alloc ( pngBytes , GCHandleType . Pinned ) ;
@@ -241,7 +235,9 @@ public static void SetDockIcon(byte[] pngBytes)
241235 }
242236 }
243237
244- // ── ObjC runtime P/Invoke ──────────────────────────────────────────────
238+ private static IntPtr Sel ( string name ) => sel_registerName ( name ) ;
239+
240+ // ── ObjC runtime P/Invoke (used by SetDockIcon) ────────────────────────
245241
246242 [ DllImport ( "/usr/lib/libobjc.A.dylib" ) ]
247243 private static extern IntPtr objc_getClass ( string name ) ;
0 commit comments