@@ -1626,31 +1626,49 @@ def _show_system_notification(title: str, body: str, sound: bool) -> tuple[bool,
16261626
16271627def _show_macos_notification (title : str , body : str , sound : bool ) -> tuple [bool , str ]:
16281628 permission_result = _request_macos_notification_permission ()
1629- script = f'display notification { _apple_script_quote (body )} with title { _apple_script_quote (title )} '
1630- if sound :
1631- script += ' sound name "Glass"'
1632- process = subprocess .run (
1633- ["osascript" , "-e" , script ],
1634- stdout = subprocess .PIPE ,
1635- stderr = subprocess .PIPE ,
1636- text = True ,
1637- encoding = "utf-8" ,
1638- errors = "replace" ,
1639- check = False ,
1640- ** subprocess_window_kwargs (),
1641- )
1642- if process .returncode == 0 :
1643- return True , "macOS notification sent with osascript."
1644- framework_result = _show_macos_notification_with_framework (title , body , sound )
1645- if framework_result [0 ]:
1646- return framework_result
1647- message = process .stderr .strip () or framework_result [1 ] or permission_result [1 ] or "macOS notification failed."
1629+ _request_macos_dock_attention ()
1630+ errors = []
1631+ for notifier in (_show_macos_notification_with_nsusernotification , _show_macos_notification_with_usernotifications ):
1632+ result = notifier (title , body , sound )
1633+ if result [0 ]:
1634+ return result
1635+ errors .append (result [1 ])
1636+ message = "; " .join (error for error in errors if error ) or permission_result [1 ] or "macOS notification failed."
16481637 return False , message
16491638
16501639
1651- def _show_macos_notification_with_framework (title : str , body : str , sound : bool ) -> tuple [bool , str ]:
1640+ def _request_macos_dock_attention () -> tuple [bool , str ]:
1641+ try :
1642+ from AppKit import NSApplication , NSCriticalRequest # type: ignore[import-not-found]
1643+
1644+ app = NSApplication .sharedApplication ()
1645+ app .requestUserAttention_ (NSCriticalRequest )
1646+ return True , "Dock attention requested."
1647+ except Exception as exc :
1648+ return False , str (exc )
1649+
1650+
1651+ def _show_macos_notification_with_nsusernotification (title : str , body : str , sound : bool ) -> tuple [bool , str ]:
1652+ try :
1653+ from Foundation import ( # type: ignore[import-not-found]
1654+ NSUserNotification ,
1655+ NSUserNotificationCenter ,
1656+ NSUserNotificationDefaultSoundName ,
1657+ )
1658+ except Exception as exc :
1659+ return False , f"NSUserNotification unavailable: { exc } "
1660+
1661+ notification = NSUserNotification .alloc ().init ()
1662+ notification .setTitle_ (title )
1663+ notification .setInformativeText_ (body )
1664+ if sound :
1665+ notification .setSoundName_ (NSUserNotificationDefaultSoundName )
1666+ NSUserNotificationCenter .defaultUserNotificationCenter ().deliverNotification_ (notification )
1667+ return True , "macOS notification sent by VideoMergingTool."
1668+
1669+
1670+ def _show_macos_notification_with_usernotifications (title : str , body : str , sound : bool ) -> tuple [bool , str ]:
16521671 try :
1653- from Foundation import NSDate # type: ignore[import-not-found]
16541672 from UserNotifications import ( # type: ignore[import-not-found]
16551673 UNMutableNotificationContent ,
16561674 UNNotificationRequest ,
@@ -1667,8 +1685,6 @@ def _show_macos_notification_with_framework(title: str, body: str, sound: bool)
16671685 content .setBody_ (body )
16681686 if sound :
16691687 content .setSound_ (UNNotificationSound .defaultSound ())
1670- if NSDate is None :
1671- return False , "Foundation unavailable."
16721688 trigger = UNTimeIntervalNotificationTrigger .triggerWithTimeInterval_repeats_ (0.1 , False )
16731689 request = UNNotificationRequest .requestWithIdentifier_content_trigger_ (identifier , content , trigger )
16741690 center = UNUserNotificationCenter .currentNotificationCenter ()
@@ -1677,7 +1693,7 @@ def _show_macos_notification_with_framework(title: str, body: str, sound: bool)
16771693
16781694
16791695def _show_windows_notification (title : str , body : str , sound : bool ) -> tuple [bool , str ]:
1680- script = _windows_toast_script (title , body , sound )
1696+ script = _windows_notification_script (title , body , sound )
16811697 subprocess .Popen (
16821698 ["powershell" , "-NoProfile" , "-STA" , "-ExecutionPolicy" , "Bypass" , "-Command" , script ],
16831699 stdout = subprocess .DEVNULL ,
@@ -1687,67 +1703,35 @@ def _show_windows_notification(title: str, body: str, sound: bool) -> tuple[bool
16871703 return True , "Windows notification requested."
16881704
16891705
1690- def _windows_toast_script (title : str , body : str , sound : bool ) -> str :
1691- title_xml = _xml_escape (title )
1692- body_xml = _xml_escape (body )
1693- audio = '<audio src="ms-winsoundevent:Notification.Default"/>' if sound else '<audio silent="true"/>'
1694- toast_xml = (
1695- "<toast>"
1696- "<visual><binding template=\" ToastGeneric\" >"
1697- f"<text>{ title_xml } </text><text>{ body_xml } </text>"
1698- "</binding></visual>"
1699- f"{ audio } "
1700- "</toast>"
1701- )
1702- toast_ps = _powershell_single_quote (toast_xml )
1706+ def _windows_notification_script (title : str , body : str , sound : bool ) -> str :
17031707 title_ps = _powershell_single_quote (title )
17041708 body_ps = _powershell_single_quote (body )
1709+ sound_ps = "$true" if sound else "$false"
17051710 return f"""
1706- $ErrorActionPreference = 'SilentlyContinue '
1711+ $ErrorActionPreference = 'Stop '
17071712try {{
1708- [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
1709- [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null
1710- $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
1711- $xml.LoadXml({ toast_ps } )
1712- $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
1713- $notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('VideoMergingTool')
1714- $notifier.Show($toast)
1715- Start-Sleep -Milliseconds 500
1716- exit 0
1717- }} catch {{
17181713 Add-Type -AssemblyName System.Windows.Forms
17191714 Add-Type -AssemblyName System.Drawing
17201715 $notify = New-Object System.Windows.Forms.NotifyIcon
17211716 $notify.Icon = [System.Drawing.SystemIcons]::Information
17221717 $notify.BalloonTipTitle = { title_ps }
17231718 $notify.BalloonTipText = { body_ps }
17241719 $notify.Visible = $true
1720+ if ({ sound_ps } ) {{ [System.Media.SystemSounds]::Asterisk.Play() }}
17251721 $notify.ShowBalloonTip(5000)
1726- if ( { '$true' if sound else '$false' } ) {{ [System.Media.SystemSounds ]::Asterisk.Play() }}
1722+ [System.Windows.Forms.Application ]::DoEvents()
17271723 Start-Sleep -Seconds 6
17281724 $notify.Dispose()
1725+ }} catch {{
1726+ if ({ sound_ps } ) {{ [System.Media.SystemSounds]::Asterisk.Play() }}
17291727}}
17301728"""
17311729
17321730
1733- def _apple_script_quote (value : str ) -> str :
1734- return '"' + value .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' ) + '"'
1735-
1736-
17371731def _powershell_single_quote (value : str ) -> str :
17381732 return "'" + value .replace ("'" , "''" ) + "'"
17391733
17401734
1741- def _xml_escape (value : str ) -> str :
1742- return (
1743- value .replace ("&" , "&" )
1744- .replace ("<" , "<" )
1745- .replace (">" , ">" )
1746- .replace ('"' , """ )
1747- .replace ("'" , "'" )
1748- )
1749-
1750-
17511735def _build_merge_command (payload : dict [str , object ]) -> list [str ]:
17521736 if getattr (sys , "frozen" , False ):
17531737 cmd = [sys .executable , "merge" , str (payload ["input_dir" ])]
0 commit comments