From b6d40deffd07555a3aa6e25db4f9ec044abda21d Mon Sep 17 00:00:00 2001 From: AlastorFox MAC Date: Thu, 1 Oct 2026 22:18:11 +0300 Subject: [PATCH] Change icon and name --- Core/AppSettings.cs | 48 ++++++++++++++++ Core/AudioSessionLabeler.cs | 15 ++++- MainWindow.xaml.cs | 98 +++++++++++++++++++++++++++++--- UI/AppSettingsWindow.xaml | 34 +++++++++++ UI/AppSettingsWindow.xaml.cs | 107 +++++++++++++++++++++++++++++++++++ UI/TrayIconManager.cs | 31 ++++++---- 6 files changed, 313 insertions(+), 20 deletions(-) create mode 100644 Core/AppSettings.cs create mode 100644 UI/AppSettingsWindow.xaml create mode 100644 UI/AppSettingsWindow.xaml.cs diff --git a/Core/AppSettings.cs b/Core/AppSettings.cs new file mode 100644 index 0000000..9b198e0 --- /dev/null +++ b/Core/AppSettings.cs @@ -0,0 +1,48 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FoxBrowser.Core; + +public sealed class AppSettings +{ + public string AppDisplayName { get; set; } = "FoxBrowser"; + public string? AppIconPath { get; set; } +} + +public static class AppSettingsStore +{ + public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "settings.json"); + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never + }; + + public static AppSettings Load() + { + try + { + if (!File.Exists(SettingsPath)) return new AppSettings(); + var settings = JsonSerializer.Deserialize(File.ReadAllText(SettingsPath), JsonOptions); + if (settings is null) return new AppSettings(); + if (string.IsNullOrWhiteSpace(settings.AppDisplayName)) settings.AppDisplayName = "FoxBrowser"; + if (!string.IsNullOrWhiteSpace(settings.AppIconPath) && !File.Exists(settings.AppIconPath)) + settings.AppIconPath = null; + return settings; + } + catch + { + return new AppSettings(); + } + } + + public static void Save(AppSettings settings) + { + Directory.CreateDirectory(AppContext.BaseDirectory); + var tempPath = SettingsPath + ".tmp"; + File.WriteAllText(tempPath, JsonSerializer.Serialize(settings, JsonOptions)); + File.Move(tempPath, SettingsPath, overwrite: true); + } +} diff --git a/Core/AudioSessionLabeler.cs b/Core/AudioSessionLabeler.cs index 1d914aa..fc1cda3 100644 --- a/Core/AudioSessionLabeler.cs +++ b/Core/AudioSessionLabeler.cs @@ -13,23 +13,32 @@ public sealed class AudioSessionLabeler : IDisposable { private const uint SnapshotProcess = 0x00000002; private readonly uint _rootProcessId; - private readonly string _iconPath; + private string _displayName; + private string _iconPath; private readonly MMDeviceEnumerator _devices = new(); private readonly AudioSessionManager _sessions; private readonly System.Threading.Timer _refreshTimer; private readonly object _gate = new(); private bool _disposed; - public AudioSessionLabeler(uint rootProcessId, string iconPath) + public AudioSessionLabeler(uint rootProcessId, string iconPath, string displayName = "FoxBrowser") { _rootProcessId = rootProcessId; _iconPath = iconPath; + _displayName = string.IsNullOrWhiteSpace(displayName) ? "FoxBrowser" : displayName; var device = _devices.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); _sessions = device.AudioSessionManager; _sessions.OnSessionCreated += OnSessionCreated; _refreshTimer = new System.Threading.Timer(_ => RefreshAndLabel(), null, TimeSpan.Zero, TimeSpan.FromSeconds(2)); } + public void UpdateIdentity(string displayName, string iconPath) + { + _displayName = string.IsNullOrWhiteSpace(displayName) ? "FoxBrowser" : displayName; + _iconPath = iconPath; + RefreshAndLabel(); + } + private void OnSessionCreated(object sender, IAudioSessionControl nativeSession) { LabelSession(new AudioSessionControl(nativeSession), disposeAfter: true); @@ -64,7 +73,7 @@ public sealed class AudioSessionLabeler : IDisposable { var ownedPids = knownOwnedPids ?? GetFoxBrowserWebViewPids(); if (!ownedPids.Contains(session.GetProcessID)) return; - session.DisplayName = "FoxBrowser"; + session.DisplayName = _displayName; if (File.Exists(_iconPath)) session.IconPath = _iconPath; } catch { /* A session may close between PID lookup and property update. */ } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 02558ac..00800da 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Threading; @@ -21,19 +22,101 @@ public partial class MainWindow : Window private readonly DispatcherTimer _watchdogTimer = new() { Interval = TimeSpan.FromSeconds(30) }; private AudioSessionLabeler? _audioLabeler; private bool _updatingDarkModeMenu; + private AppSettings _appSettings; + + private const int WmNcRButtonDown = 0x00A4; + private const int WmNcRButtonUp = 0x00A5; + private const int HtSysMenu = 3; public MainWindow() { InitializeComponent(); + _appSettings = AppSettingsStore.Load(); + ApplyAppIdentity(_appSettings); + SourceInitialized += MainWindow_SourceInitialized; + Loaded += MainWindow_Loaded; + } + + private void MainWindow_SourceInitialized(object? sender, EventArgs e) + { + ApplyDarkTitleBar(true); + var source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle); + source?.AddHook(MainWindowWndProc); + } + + private IntPtr MainWindowWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + if ((msg == WmNcRButtonDown || msg == WmNcRButtonUp) && wParam.ToInt32() == HtSysMenu) + { + handled = true; + if (msg == WmNcRButtonUp) + Dispatcher.BeginInvoke(OpenAppIdentityMenu); + } + return IntPtr.Zero; + } + + private void OpenAppIdentityMenu() + { + var popup = new Popup + { + Placement = PlacementMode.MousePoint, + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade + }; + var item = new System.Windows.Controls.Button + { + Content = "Змінити назву й іконку…", + Style = (Style)FindResource("FoxMenuButton") + }; + item.Click += (_, _) => + { + popup.IsOpen = false; + OpenAppSettings(); + }; + popup.Child = new Border + { + Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(23, 27, 34)), + BorderBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(51, 64, 78)), + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(8), + Padding = new Thickness(8), + Child = item + }; + popup.IsOpen = true; + } + + private void OpenAppSettings() + { + var window = new AppSettingsWindow(_appSettings, ApplyAppIdentity) { Owner = this }; + window.ShowDialog(); + } + + private void ApplyAppIdentity(AppSettings settings) + { + _appSettings = settings; + var iconPath = ResolveAppIconPath(settings); + Title = string.IsNullOrWhiteSpace(settings.AppDisplayName) ? "FoxBrowser" : settings.AppDisplayName; try { - var icoPath = Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico"); - if (File.Exists(icoPath)) - Icon = new System.Windows.Media.Imaging.BitmapImage(new Uri(icoPath, UriKind.Absolute)); + var image = new System.Windows.Media.Imaging.BitmapImage(); + image.BeginInit(); + image.UriSource = new Uri(iconPath, UriKind.Absolute); + image.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad; + image.EndInit(); + image.Freeze(); + Icon = image; } catch { } - SourceInitialized += (_, _) => ApplyDarkTitleBar(true); - Loaded += MainWindow_Loaded; + _tray?.UpdateIdentity(Title, iconPath); + _audioLabeler?.UpdateIdentity(Title, iconPath); + } + + private static string ResolveAppIconPath(AppSettings settings) + { + if (!string.IsNullOrWhiteSpace(settings.AppIconPath) && File.Exists(settings.AppIconPath)) + return settings.AppIconPath; + return Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico"); } private async void MainWindow_Loaded(object sender, RoutedEventArgs e) @@ -41,6 +124,7 @@ public partial class MainWindow : Window try { _tray = new TrayIconManager(this, ExitApplication); + _tray.UpdateIdentity(Title, ResolveAppIconPath(_appSettings)); var environmentFactory = new WebView2EnvironmentFactory(); var recoveryMode = false; CoreWebView2Environment environment; @@ -65,10 +149,10 @@ public partial class MainWindow : Window _tabManager.ActiveTabStateChanged += (_, _) => Dispatcher.Invoke(RefreshAddressBar); DataContext = _tabManager; await _tabManager.InitializeAsync(); - var iconPath = Path.Combine(AppContext.BaseDirectory, "FoxBrowser.ico"); + var iconPath = ResolveAppIconPath(_appSettings); try { - _audioLabeler = new AudioSessionLabeler((uint)Process.GetCurrentProcess().Id, iconPath); + _audioLabeler = new AudioSessionLabeler((uint)Process.GetCurrentProcess().Id, iconPath, Title); } catch { diff --git a/UI/AppSettingsWindow.xaml b/UI/AppSettingsWindow.xaml new file mode 100644 index 0000000..b793534 --- /dev/null +++ b/UI/AppSettingsWindow.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + +