diff --git a/FluentFlyoutWPF/Classes/Utils/MediaPlayerData.cs b/FluentFlyoutWPF/Classes/Utils/MediaPlayerData.cs index 7ee8a080..2f3d25d8 100644 --- a/FluentFlyoutWPF/Classes/Utils/MediaPlayerData.cs +++ b/FluentFlyoutWPF/Classes/Utils/MediaPlayerData.cs @@ -66,11 +66,8 @@ public static (string, ImageSource?) GetAndCacheMediaPlayerData(string mediaPlay { try { - // pre-filter processes without a main window handle - if (p.MainWindowHandle == IntPtr.Zero) - { - return null; - } + // we no longer filter processes without main window handle, + // as background media processes may not have one. var mainModule = p.MainModule; if (mainModule == null) return null; diff --git a/FluentFlyoutWPF/Controls/TaskbarWidgetControl.xaml.cs b/FluentFlyoutWPF/Controls/TaskbarWidgetControl.xaml.cs index a7957de9..18cb6e7f 100644 --- a/FluentFlyoutWPF/Controls/TaskbarWidgetControl.xaml.cs +++ b/FluentFlyoutWPF/Controls/TaskbarWidgetControl.xaml.cs @@ -409,10 +409,7 @@ private async void Previous_Click(object sender, RoutedEventArgs e) { if (_mainWindow == null) return; - var mediaManager = _mainWindow.mediaManager; - if (mediaManager == null) return; - - var focusedSession = mediaManager.GetFocusedSession(); + var focusedSession = _mainWindow.GetActiveMediaSession(); if (focusedSession == null) return; await focusedSession.ControlSession.TrySkipPreviousAsync(); @@ -422,30 +419,17 @@ private async void PlayPause_Click(object sender, RoutedEventArgs e) { if (_mainWindow == null) return; - var mediaManager = _mainWindow.mediaManager; - if (mediaManager == null) return; - - var focusedSession = mediaManager.GetFocusedSession(); + var focusedSession = _mainWindow.GetActiveMediaSession(); if (focusedSession == null) return; - if (_isPaused) // paused - { - await focusedSession.ControlSession.TryPlayAsync(); - } - else // playing - { - await focusedSession.ControlSession.TryPauseAsync(); - } + await focusedSession.ControlSession.TryTogglePlayPauseAsync(); } private async void Next_Click(object sender, RoutedEventArgs e) { if (_mainWindow == null) return; - var mediaManager = _mainWindow.mediaManager; - if (mediaManager == null) return; - - var focusedSession = mediaManager.GetFocusedSession(); + var focusedSession = _mainWindow.GetActiveMediaSession(); if (focusedSession == null) return; await focusedSession.ControlSession.TrySkipNextAsync(); diff --git a/FluentFlyoutWPF/MainWindow.xaml.cs b/FluentFlyoutWPF/MainWindow.xaml.cs index 6bcb019c..66a712d3 100644 --- a/FluentFlyoutWPF/MainWindow.xaml.cs +++ b/FluentFlyoutWPF/MainWindow.xaml.cs @@ -173,7 +173,7 @@ public MainWindow() RegisterShellHookWindow(new WindowInteropHelper(this).Handle); _positionTimer = new Timer(SeekbarUpdateUi, null, Timeout.Infinite, Timeout.Infinite); - if (_seekBarEnabled && mediaManager.GetFocusedSession() is { } session) + if (_seekBarEnabled && GetActiveMediaSession() is { } session) { UpdateSeekbarCurrentDuration(session.ControlSession.GetTimelineProperties().Position); } @@ -232,6 +232,73 @@ private async Task CheckForUpdatesOnStartupAsync() } } + public bool IsSessionAllowed(MediaSession? session) + { + if (session == null) return false; + if (!SettingsManager.Current.AppFilteringEnabled) return true; + + string appId = session.Id ?? string.Empty; + string appName = MediaPlayerData.GetAndCacheMediaPlayerData(appId).Item1 ?? appId; + + if (SettingsManager.Current.AppFilteringMode == 0) // Blacklist mode + { + if (SettingsManager.Current.BlockedApps != null && SettingsManager.Current.BlockedApps.Any(b => + appName.Contains(b, StringComparison.OrdinalIgnoreCase) || + appId.Contains(b, StringComparison.OrdinalIgnoreCase))) + return false; + + return true; + } + else // Whitelist mode + { + if (SettingsManager.Current.AllowedApps != null && SettingsManager.Current.AllowedApps.Any(a => + appName.Contains(a, StringComparison.OrdinalIgnoreCase) || + appId.Contains(a, StringComparison.OrdinalIgnoreCase))) + return true; + + return false; + } + } + + public MediaSession? GetActiveMediaSession() + { + var validSessions = mediaManager.CurrentMediaSessions.Values.Where(IsSessionAllowed).ToList(); + + if (validSessions.Count == 0) return null; + + var focused = mediaManager.GetFocusedSession(); + if (focused != null && validSessions.Any(s => s.Id == focused.Id)) + return focused; + + var playing = validSessions.FirstOrDefault(s => s.ControlSession.GetPlaybackInfo()?.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing); + if (playing != null) + return playing; + + return validSessions.FirstOrDefault(); + } + + public void RefreshFilteredMedia() + { + UpdateTaskbar(); + + if (IsVisible) + { + var activeSession = GetActiveMediaSession(); + + // UpdateUI handles a null value internally so we haven't checked for null here. + UpdateUI(activeSession!); + + if (activeSession != null) + { + HandlePlayBackState(activeSession.ControlSession.GetPlaybackInfo()?.PlaybackStatus); + } + else + { + HandlePlayBackState(GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed); + } + } + } + private static GlobalSystemMediaTransportControlsSessionMediaProperties? TryGetMediaProperties(GlobalSystemMediaTransportControlsSession controlSession) { try @@ -482,17 +549,18 @@ public void CloseAnimation(MicaWindow window, MonitorInfo? selectedMonitor = nul public void UpdateTaskbar() { - if (!mediaManager.IsStarted || mediaManager.GetFocusedSession() == null) + var activeSession = GetActiveMediaSession(); + if (!mediaManager.IsStarted || activeSession == null) { taskbarWindow?.UpdateUi("-", "-", null, GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed); return; } - var focusedSession = mediaManager.GetFocusedSession(); - var songInfo = TryGetMediaProperties(focusedSession.ControlSession); + + var songInfo = TryGetMediaProperties(activeSession.ControlSession); if (songInfo == null) return; - var playbackInfo = focusedSession.ControlSession.GetPlaybackInfo(); + var playbackInfo = activeSession.ControlSession.GetPlaybackInfo(); var thumbnail = BitmapHelper.GetThumbnail(songInfo.Thumbnail); BitmapHelper.GetDominantColors(1); taskbarWindow?.UpdateUi(songInfo.Title, songInfo.Artist, thumbnail, playbackInfo.PlaybackStatus, playbackInfo.Controls); @@ -546,20 +614,22 @@ private void CurrentSession_OnPlaybackStateChanged(MediaSession mediaSession, Gl #endif pauseOtherMediaSessionsIfNeeded(mediaSession); - var focusedSession = mediaManager.GetFocusedSession(); + var focusedSession = GetActiveMediaSession(); if (focusedSession == null) { taskbarWindow?.UpdateUi("-", "-", null, GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed); return; } - var songInfo = TryGetMediaProperties(focusedSession.ControlSession); - if (songInfo == null) - return; + var tbSongInfo = TryGetMediaProperties(focusedSession.ControlSession); + if (tbSongInfo != null) + { + var tbThumbnail = BitmapHelper.GetThumbnail(tbSongInfo.Thumbnail); + BitmapHelper.GetDominantColors(1); + var tbPlayback = focusedSession.ControlSession.GetPlaybackInfo(); - var thumbnail = BitmapHelper.GetThumbnail(songInfo.Thumbnail); - BitmapHelper.GetDominantColors(1); - taskbarWindow?.UpdateUi(songInfo.Title, songInfo.Artist, thumbnail, playbackInfo?.PlaybackStatus, playbackInfo?.Controls); + taskbarWindow?.UpdateUi(tbSongInfo.Title, tbSongInfo.Artist, tbThumbnail, tbPlayback?.PlaybackStatus, tbPlayback?.Controls); + } if (IsVisible) { @@ -580,17 +650,18 @@ private void MediaManager_OnAnyMediaPropertyChanged(MediaSession mediaSession, G #if DEBUG Logger.Debug("Media property changed: " + mediaProperties.Title + " " + mediaSession.ControlSession.GetPlaybackInfo().PlaybackStatus); #endif - if (mediaManager.GetFocusedSession() == null) + var currentActiveSession = GetActiveMediaSession(); + if (currentActiveSession == null) { taskbarWindow?.UpdateUi("-", "-", null, GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed); return; } - var songInfo = TryGetMediaProperties(mediaSession.ControlSession); + var songInfo = TryGetMediaProperties(currentActiveSession.ControlSession); if (songInfo == null) return; - var playbackInfo = mediaSession.ControlSession.GetPlaybackInfo(); + var playbackInfo = currentActiveSession.ControlSession.GetPlaybackInfo(); string check = songInfo.Title + songInfo.Artist + playbackInfo.PlaybackStatus; int checkThumbnail = BitmapHelper.GetStableThumbnailHash(songInfo.Thumbnail); @@ -607,6 +678,7 @@ private void MediaManager_OnAnyMediaPropertyChanged(MediaSession mediaSession, G var thumbnail = BitmapHelper.GetThumbnail(songInfo.Thumbnail); BitmapHelper.GetDominantColors(1); + taskbarWindow?.UpdateUi(songInfo.Title, songInfo.Artist, thumbnail, playbackInfo.PlaybackStatus, playbackInfo.Controls); pauseOtherMediaSessionsIfNeeded(mediaSession); @@ -617,7 +689,7 @@ void createNewNextUpWindow() { Dispatcher.Invoke(() => { - if (nextUpWindow == null && playbackInfo.Controls.IsPauseEnabled) // double-check within the Dispatcher to prevent race conditions + if (nextUpWindow == null && playbackInfo.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) // double-check within the Dispatcher to prevent race conditions { nextUpWindow = new NextUpWindow(songInfo.Title, songInfo.Artist, thumbnail); currentTitle = songInfo.Title; @@ -653,9 +725,12 @@ void createNewNextUpWindow() if (IsVisible) { - var focusedSession = mediaManager.GetFocusedSession(); - HandlePlayBackState(focusedSession.ControlSession.GetPlaybackInfo().PlaybackStatus); - UpdateUI(focusedSession); + var focusedSession = GetActiveMediaSession(); + if (focusedSession != null) + { + HandlePlayBackState(focusedSession.ControlSession.GetPlaybackInfo()?.PlaybackStatus); + UpdateUI(focusedSession); + } } } @@ -663,7 +738,7 @@ private void MediaManager_OnAnyTimelinePropertyChanged(MediaSession mediaSession { _lastSelfUpdateTimestamp = DateTime.Now; - if (mediaManager.GetFocusedSession() is not { } session) return; + if (GetActiveMediaSession() is not { } session || session.Id != mediaSession.Id) return; if (_seekBarEnabled) { @@ -682,23 +757,7 @@ private void MediaManager_OnAnySessionClosed(MediaSession mediaSession) #if DEBUG Logger.Debug("Session closed: " + (mediaSession.Id).ToString()); #endif - var focusedSession = mediaManager.GetFocusedSession(); - - if (focusedSession == null) - { - taskbarWindow?.UpdateUi("-", "-", null, GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed); - } - else - { - var songInfo = TryGetMediaProperties(focusedSession.ControlSession); - if (songInfo == null) - return; - - var playbackInfo = focusedSession.ControlSession.GetPlaybackInfo(); - var thumbnail = BitmapHelper.GetThumbnail(songInfo.Thumbnail); - BitmapHelper.GetDominantColors(1); - taskbarWindow?.UpdateUi(songInfo.Title, songInfo.Artist, thumbnail, playbackInfo.PlaybackStatus, playbackInfo.Controls); - } + UpdateTaskbar(); } private static IntPtr SetHook(LowLevelKeyboardProc proc) // set the keyboard hook @@ -786,7 +845,8 @@ private bool TryShowMediaFlyoutDebounced() public async void ShowMediaFlyout(bool toggleMode = false, bool forceShow = false) { - if (mediaManager.GetFocusedSession() == null || + var activeSession = GetActiveMediaSession(); + if (activeSession == null || (!forceShow && !SettingsManager.Current.MediaFlyoutEnabled) || FullscreenDetector.IsFullscreenApplicationRunning()) return; @@ -807,9 +867,9 @@ public async void ShowMediaFlyout(bool toggleMode = false, bool forceShow = fals return; } - UpdateUI(mediaManager.GetFocusedSession()); + UpdateUI(activeSession); if (_seekBarEnabled) - HandlePlayBackState(mediaManager.GetFocusedSession().ControlSession.GetPlaybackInfo().PlaybackStatus); + HandlePlayBackState(activeSession.ControlSession.GetPlaybackInfo().PlaybackStatus); if (nextUpWindow != null) // close NextUpWindow if it's open { @@ -917,18 +977,26 @@ private void UpdateUI(MediaSession mediaSession) var mediaProperties = controlSession.GetPlaybackInfo(); if (mediaProperties != null) { - if (mediaProperties.Controls.IsPauseEnabled) + if (mediaProperties.PlaybackStatus == GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing) { - ControlPlayPause.IsEnabled = true; - ControlPlayPause.Opacity = 1; SymbolPlayPause.Symbol = Wpf.Ui.Controls.SymbolRegular.Pause16; } else { - ControlPlayPause.IsEnabled = true; - ControlPlayPause.Opacity = 1; SymbolPlayPause.Symbol = Wpf.Ui.Controls.SymbolRegular.Play16; } + + ControlPlayPause.IsEnabled = mediaProperties.Controls.IsPlayEnabled || mediaProperties.Controls.IsPauseEnabled; + + if (ControlPlayPause.IsEnabled) + { + ControlPlayPause.Opacity = 1; + } + else + { + ControlPlayPause.Opacity = 0.35; + } + ControlBack.IsEnabled = ControlForward.IsEnabled = mediaProperties.Controls.IsNextEnabled; ControlBack.Opacity = ControlForward.Opacity = mediaProperties.Controls.IsNextEnabled ? 1 : 0.35; @@ -1152,77 +1220,64 @@ private void UpdateUILayout() // update the layout based on the settings private async void Back_Click(object sender, RoutedEventArgs e) { - if (mediaManager.GetFocusedSession() == null) - return; + var activeSession = GetActiveMediaSession(); + if (activeSession == null) return; - await mediaManager.GetFocusedSession().ControlSession.TrySkipPreviousAsync(); + await activeSession.ControlSession.TrySkipPreviousAsync(); } - private void PlayPause_Click(object sender, RoutedEventArgs e) + private async void PlayPause_Click(object sender, RoutedEventArgs e) { - keybd_event(0xB3, 0, 0, IntPtr.Zero); - - if (mediaManager.GetFocusedSession() == null) - return; + var activeSession = GetActiveMediaSession(); + if (activeSession == null) return; - //var controlsInfo = mediaManager.GetFocusedSession().ControlSession.GetPlaybackInfo().Controls; - - //if (controlsInfo.IsPauseEnabled == true) - //{ - // await mediaManager.GetFocusedSession().ControlSession.TryPauseAsync(); - //} - //else if (controlsInfo.IsPlayEnabled == true) - // await mediaManager.GetFocusedSession().ControlSession.TryPlayAsync(); - if (mediaManager.GetFocusedSession().ControlSession.GetPlaybackInfo().Controls.IsPauseEnabled) - SymbolPlayPause.Dispatcher.Invoke(() => SymbolPlayPause.Symbol = Wpf.Ui.Controls.SymbolRegular.Pause16); - else - SymbolPlayPause.Dispatcher.Invoke(() => SymbolPlayPause.Symbol = Wpf.Ui.Controls.SymbolRegular.Play16); + await activeSession.ControlSession.TryTogglePlayPauseAsync(); } private async void Forward_Click(object sender, RoutedEventArgs e) { - if (mediaManager.GetFocusedSession() == null) - return; + var activeSession = GetActiveMediaSession(); + if (activeSession == null) return; - await mediaManager.GetFocusedSession().ControlSession.TrySkipNextAsync(); + await activeSession.ControlSession.TrySkipNextAsync(); } private async void Repeat_Click(object sender, RoutedEventArgs e) { - if (mediaManager.GetFocusedSession() == null) - return; + var activeSession = GetActiveMediaSession(); + if (activeSession == null) return; - if (mediaManager.GetFocusedSession().ControlSession.GetPlaybackInfo().AutoRepeatMode == global::Windows.Media.MediaPlaybackAutoRepeatMode.None) + if (activeSession.ControlSession.GetPlaybackInfo().AutoRepeatMode == global::Windows.Media.MediaPlaybackAutoRepeatMode.None) { SymbolRepeat.Dispatcher.Invoke(() => SymbolRepeat.Symbol = Wpf.Ui.Controls.SymbolRegular.ArrowRepeatAll24); - await mediaManager.GetFocusedSession().ControlSession.TryChangeAutoRepeatModeAsync(global::Windows.Media.MediaPlaybackAutoRepeatMode.List); + await activeSession.ControlSession.TryChangeAutoRepeatModeAsync(global::Windows.Media.MediaPlaybackAutoRepeatMode.List); } - else if (mediaManager.GetFocusedSession().ControlSession.GetPlaybackInfo().AutoRepeatMode == global::Windows.Media.MediaPlaybackAutoRepeatMode.List) + else if (activeSession.ControlSession.GetPlaybackInfo().AutoRepeatMode == global::Windows.Media.MediaPlaybackAutoRepeatMode.List) { SymbolRepeat.Dispatcher.Invoke(() => SymbolRepeat.Symbol = Wpf.Ui.Controls.SymbolRegular.ArrowRepeat124); - await mediaManager.GetFocusedSession().ControlSession.TryChangeAutoRepeatModeAsync(global::Windows.Media.MediaPlaybackAutoRepeatMode.Track); + await activeSession.ControlSession.TryChangeAutoRepeatModeAsync(global::Windows.Media.MediaPlaybackAutoRepeatMode.Track); } - else if (mediaManager.GetFocusedSession().ControlSession.GetPlaybackInfo().AutoRepeatMode == global::Windows.Media.MediaPlaybackAutoRepeatMode.Track) + else if (activeSession.ControlSession.GetPlaybackInfo().AutoRepeatMode == global::Windows.Media.MediaPlaybackAutoRepeatMode.Track) { SymbolRepeat.Dispatcher.Invoke(() => SymbolRepeat.Symbol = Wpf.Ui.Controls.SymbolRegular.ArrowRepeatAllOff24); - await mediaManager.GetFocusedSession().ControlSession.TryChangeAutoRepeatModeAsync(global::Windows.Media.MediaPlaybackAutoRepeatMode.None); + await activeSession.ControlSession.TryChangeAutoRepeatModeAsync(global::Windows.Media.MediaPlaybackAutoRepeatMode.None); } } private async void Shuffle_Click(object sender, RoutedEventArgs e) { - if (mediaManager.GetFocusedSession() == null) - return; + var activeSession = GetActiveMediaSession(); + if (activeSession == null) return; - if (mediaManager.GetFocusedSession().ControlSession.GetPlaybackInfo().IsShuffleActive == true) + if (activeSession.ControlSession.GetPlaybackInfo().IsShuffleActive == true) { SymbolShuffle.Dispatcher.Invoke(() => SymbolShuffle.Symbol = Wpf.Ui.Controls.SymbolRegular.ArrowShuffleOff24); - await mediaManager.GetFocusedSession().ControlSession.TryChangeShuffleActiveAsync(false); + await activeSession.ControlSession.TryChangeShuffleActiveAsync(false); } else { SymbolShuffle.Dispatcher.Invoke(() => SymbolShuffle.Symbol = Wpf.Ui.Controls.SymbolRegular.ArrowShuffle24); - await mediaManager.GetFocusedSession().ControlSession.TryChangeShuffleActiveAsync(true); + await activeSession.ControlSession.TryChangeShuffleActiveAsync(true); } } @@ -1247,7 +1302,7 @@ private void Seekbar_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEven private async void Seekbar_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { - if (mediaManager.GetFocusedSession() is { } session) + if (GetActiveMediaSession() is { } session) { var seekPosition = TimeSpan.FromSeconds(Seekbar.Value); if (seekPosition == TimeSpan.Zero) seekPosition = TimeSpan.FromSeconds(1); @@ -1271,7 +1326,7 @@ private void SeekbarUpdateUi(object? sender) if (DateTime.Now.Subtract(_lastSelfUpdateTimestamp).TotalSeconds < 1) return; if (!_seekBarEnabled || Visibility != Visibility.Visible || _isDragging) return; - if (mediaManager.GetFocusedSession() is not { } session) return; + if (GetActiveMediaSession() is not { } session) return; var timeline = session.ControlSession.GetTimelineProperties(); var pos = timeline.Position + (DateTime.Now - timeline.LastUpdatedTime.DateTime); diff --git a/FluentFlyoutWPF/Pages/AppFilteringPage.xaml b/FluentFlyoutWPF/Pages/AppFilteringPage.xaml new file mode 100644 index 00000000..dc50d309 --- /dev/null +++ b/FluentFlyoutWPF/Pages/AppFilteringPage.xaml @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FluentFlyoutWPF/Pages/AppFilteringPage.xaml.cs b/FluentFlyoutWPF/Pages/AppFilteringPage.xaml.cs new file mode 100644 index 00000000..14f0ca1f --- /dev/null +++ b/FluentFlyoutWPF/Pages/AppFilteringPage.xaml.cs @@ -0,0 +1,207 @@ +// Copyright © 2024-2026 The FluentFlyout Authors +// SPDX-License-Identifier: GPL-3.0-or-later + +using FluentFlyout.Classes.Settings; +using FluentFlyout.Classes.Utils; + +using System.Windows; +using System.Windows.Controls; + +namespace FluentFlyoutWPF.Pages; + +public partial class AppFilteringPage : Page +{ + public AppFilteringPage() + { + InitializeComponent(); + DataContext = SettingsManager.Current; + } + + /// + /// Saves the current settings and refreshes the media sessions in the main window. + /// This ensures that any changes made to the allowed or blocked apps are immediately reflected in the media sessions displayed. + /// + private static void SaveAndRefreshMedia() + { + SettingsManager.SaveSettings(); + var mainWindow = Application.Current.MainWindow as MainWindow; + + mainWindow?.RefreshFilteredMedia(); + } + + /// + /// Normalizes the specified application name by removing the ".exe" extension and matching it against known session names. + /// + /// If the application name matches or is contained within any current media session name, the + /// method returns the matched session name. This is helpful to compare against known sources and different aliases they may have. + /// The application name to normalize. This may include the ".exe" extension. + /// A normalized application name that matches a known media source, else the default or default with extension stripped. + private static string NormalizeAppName(string app) + { + if (app.EndsWith(".exe", System.StringComparison.OrdinalIgnoreCase)) + { + app = app[..^4]; + } + + var mainWindow = Application.Current.MainWindow as MainWindow; + if (mainWindow?.mediaManager == null) return app; + + var match = mainWindow.mediaManager.CurrentMediaSessions.Values + .Select(s => MediaPlayerData.GetAndCacheMediaPlayerData(s.Id).Item1) + .FirstOrDefault(name => name.Equals(app, System.StringComparison.OrdinalIgnoreCase) || + name.Contains(app, System.StringComparison.OrdinalIgnoreCase) || + app.Contains(name, System.StringComparison.OrdinalIgnoreCase)); + + return match ?? app; + } + + /// + /// Populates the specified ComboBox with a distinct, alphabetically ordered list of applications that have media. + /// + /// If there are no active media sessions, the ComboBox will be populated with an empty list. + /// The ComboBox to populate with the list of applications. (Should not be null). + private static void PopulateComboBox(ComboBox comboBox) + { + var mainWindow = Application.Current.MainWindow as MainWindow; + + if (mainWindow?.mediaManager == null) return; + + var apps = mainWindow.mediaManager.CurrentMediaSessions.Values + .Select(s => MediaPlayerData.GetAndCacheMediaPlayerData(s.Id).Item1) + .Distinct() + .OrderBy(a => a) + .ToList(); + + comboBox.ItemsSource = apps; + } + + /// + /// Handles the DropDownOpened event for the AllowComboBox control and populates its items when the dropdown is opened. + /// + /// The source of the event (typically AllowComboBox). + /// An EventArgs object that contains the event data. + private void AllowComboBox_DropDownOpened(object sender, System.EventArgs e) + { + PopulateComboBox(AllowComboBox); + } + + /// + /// Handles the DropDownOpened event for the BlockComboBox control and populates its items when the dropdown is opened. + /// + /// The source of the event (typically BlockComboBox). + /// An EventArgs object that contains the event data. + private void BlockComboBox_DropDownOpened(object sender, System.EventArgs e) + { + PopulateComboBox(BlockComboBox); + } + + /// + /// Handles the Click event of the AddAllow button to add the selected application to the allowed applications list. + /// + /// The source of the event (typically the AddAllow button). + /// The event data associated with the Click event. + private void AddAllow_Click(object sender, RoutedEventArgs e) + { + var app = AllowComboBox.SelectedItem?.ToString()?.Trim(); + + if (string.IsNullOrEmpty(app) || SettingsManager.Current.AllowedApps.Any(a => a.Equals(app, System.StringComparison.OrdinalIgnoreCase))) return; + + SettingsManager.Current.AllowedApps.Add(app); + AllowComboBox.SelectedIndex = -1; + + SaveAndRefreshMedia(); + } + + /// + /// Handles the Click event for adding a manually entered application to the allowed applications list. + /// + /// If the entered application name is empty or already exists in the allowed list, no action is + /// taken. + /// The source of the event (typically the Add button) + /// The event data associated with the Click event. + private void AddAllowManual_Click(object sender, RoutedEventArgs e) + { + var app = AllowTextBox.Text?.Trim(); + + if (string.IsNullOrEmpty(app)) return; + + app = NormalizeAppName(app); + + if (SettingsManager.Current.AllowedApps.Any(a => a.Equals(app, System.StringComparison.OrdinalIgnoreCase))) return; + + SettingsManager.Current.AllowedApps.Add(app); + AllowTextBox.Text = string.Empty; + + SaveAndRefreshMedia(); + } + + /// + /// Handles the click event to remove an application from the allowed list. + /// + /// This method updates the allowed applications list and refreshes the media settings after removal. + /// The source of the event (usually a Button with its Tag property set to the application identifier). + /// The event data associated with the click event. + private void RemoveAllow_Click(object sender, RoutedEventArgs e) + { + if (sender is not Button { Tag: string app }) return; + + SettingsManager.Current.AllowedApps.Remove(app); + SaveAndRefreshMedia(); + } + + /// + /// Handles the Click event for adding a selected application to the blocked applications list. + /// + /// If the selected application is not already blocked and is not null or empty, it is added to + /// the blocked applications list. The media is then refreshed. + /// The source of the event (typically the button that was clicked). + /// The event data associated with the click event. + private void AddBlock_Click(object sender, RoutedEventArgs e) + { + var app = BlockComboBox.SelectedItem?.ToString()?.Trim(); + + if (string.IsNullOrEmpty(app) || SettingsManager.Current.BlockedApps.Any(b => b.Equals(app, System.StringComparison.OrdinalIgnoreCase))) return; + + SettingsManager.Current.BlockedApps.Add(app); + BlockComboBox.SelectedIndex = -1; + + SaveAndRefreshMedia(); + } + + /// + /// Handles the Click event for manually adding an application to the blocked list. + /// + /// Adds the trimmed text from the input box to the blocked applications list if it is not empty + /// and not already present. After adding, the input box is cleared and the media list is refreshed. + /// The source of the event (typically the button that was clicked). + /// The event data associated with the click event. + private void AddBlockManual_Click(object sender, RoutedEventArgs e) + { + var app = BlockTextBox.Text?.Trim(); + + if (string.IsNullOrEmpty(app)) return; + + app = NormalizeAppName(app); + + if (SettingsManager.Current.BlockedApps.Any(b => b.Equals(app, System.StringComparison.OrdinalIgnoreCase))) return; + + SettingsManager.Current.BlockedApps.Add(app); + BlockTextBox.Text = string.Empty; + + SaveAndRefreshMedia(); + } + + /// + /// Handles the click event to remove an application from the blocked list. + /// + /// The source of the event, expected to be a Button with its Tag property set to the application. + /// The event data associated with the click event. + private void RemoveBlock_Click(object sender, RoutedEventArgs e) + { + if (sender is not Button { Tag: string app }) return; + + SettingsManager.Current.BlockedApps.Remove(app); + + SaveAndRefreshMedia(); + } +} \ No newline at end of file diff --git a/FluentFlyoutWPF/Pages/SystemPage.xaml b/FluentFlyoutWPF/Pages/SystemPage.xaml index bea875b0..20248eba 100644 --- a/FluentFlyoutWPF/Pages/SystemPage.xaml +++ b/FluentFlyoutWPF/Pages/SystemPage.xaml @@ -175,7 +175,17 @@ - + + + + + + + + + + + diff --git a/FluentFlyoutWPF/Pages/SystemPage.xaml.cs b/FluentFlyoutWPF/Pages/SystemPage.xaml.cs index 78f7557d..4c849c97 100644 --- a/FluentFlyoutWPF/Pages/SystemPage.xaml.cs +++ b/FluentFlyoutWPF/Pages/SystemPage.xaml.cs @@ -198,6 +198,11 @@ private async void ImportButton_Click(object sender, System.Windows.RoutedEventA } } + private void AppFiltering_Click(object sender, System.Windows.RoutedEventArgs e) + { + SettingsWindow.NavigateToPage(typeof(AppFilteringPage)); + } + private void Advanced_Click(object sender, System.Windows.RoutedEventArgs e) { SettingsWindow.NavigateToPage(typeof(AdvancedPage)); diff --git a/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml b/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml index 2a43079a..2e0db4b1 100644 --- a/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml +++ b/FluentFlyoutWPF/Resources/Localization/Dictionary-en-US.xaml @@ -174,6 +174,22 @@ Note: As an open-source project, these features are also available for free via Tray Icon Updates Configure + App Filtering + Control which applications are allowed or blocked from displaying their media. + Enable Filtering + Mode + Blacklist (Allow all except blocked apps) + Whitelist (Only allow selected apps) + Allowed apps + If any applications are listed here, only their media will be shown. + Excluded apps + Excludes an application from being displayed + Can't find your app? Type it below: + e.g. Spotify.exe + e.g. Chrome.exe + Select a currently playing app to add + Add + Remove About ❤️ Meet Our Contributors A big thank you to all the amazing people who have contributed to FluentFlyout's development and success! diff --git a/FluentFlyoutWPF/ViewModels/UserSettings.cs b/FluentFlyoutWPF/ViewModels/UserSettings.cs index d37adc0b..9235a74e 100644 --- a/FluentFlyoutWPF/ViewModels/UserSettings.cs +++ b/FluentFlyoutWPF/ViewModels/UserSettings.cs @@ -437,6 +437,30 @@ public string TaskbarWidgetManualPaddingText [ObservableProperty] public partial bool TaskbarVisualizerEnabled { get; set; } + /// + /// Returns whether app filtering is enabled or disabled. + /// + [ObservableProperty] + public partial bool AppFilteringEnabled { get; set; } + + /// + /// Returns the active filtering mode. 0 for Blacklist, 1 for Whitelist. + /// + [ObservableProperty] + public partial int AppFilteringMode { get; set; } + + /// + /// Returns a list of apps that are allowed to display media/update the taskbar. + /// + [ObservableProperty] + public partial ObservableCollection AllowedApps { get; set; } = new(); + + /// + /// Returns a list of apps that are NOT allowed to display media/update the taskbar. + /// + [ObservableProperty] + public partial ObservableCollection BlockedApps { get; set; } = new(); + /// /// Position of the visualizer, where 0 and 1 are to the left or right of the widget. /// @@ -631,6 +655,8 @@ public UserSettings() TaskbarWidgetControlsPosition = 1; TaskbarWidgetAnimated = true; TaskbarVisualizerEnabled = false; + AppFilteringEnabled = false; + AppFilteringMode = 0; TaskbarVisualizerPosition = 1; TaskbarVisualizerClickable = false; TaskbarVisualizerBarCount = 10; @@ -795,6 +821,22 @@ partial void OnUseAlbumArtAsAccentColorChanged(bool oldValue, bool newValue) BitmapHelper.GetDominantColors(1); } + partial void OnAppFilteringEnabledChanged(bool oldValue, bool newValue) + { + if (oldValue == newValue || _initializing) return; + + MainWindow mainWindow = (MainWindow)Application.Current.MainWindow; + mainWindow?.RefreshFilteredMedia(); + } + + partial void OnAppFilteringModeChanged(int oldValue, int newValue) + { + if (oldValue == newValue || _initializing) return; + + MainWindow mainWindow = (MainWindow)Application.Current.MainWindow; + mainWindow?.RefreshFilteredMedia(); + } + partial void OnVolumeMixerHighlightActiveAppsChanged(bool oldValue, bool newValue) { if (oldValue == newValue || _initializing) return;