diff --git a/FluentAutoClicker/App.xaml b/FluentAutoClicker/App.xaml index 9cb920c..b1603bd 100644 --- a/FluentAutoClicker/App.xaml +++ b/FluentAutoClicker/App.xaml @@ -1,9 +1,9 @@ + + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -14,4 +14,4 @@ - + \ No newline at end of file diff --git a/FluentAutoClicker/App.xaml.cs b/FluentAutoClicker/App.xaml.cs index 8933114..fcb96d3 100644 --- a/FluentAutoClicker/App.xaml.cs +++ b/FluentAutoClicker/App.xaml.cs @@ -25,8 +25,11 @@ namespace FluentAutoClicker; /// /// Provides application-specific behavior to supplement the default Application class. /// -public partial class App : Application +public partial class App { + /// + /// The main window of the application. + /// public static readonly MainWindow MainWindow = new(); /// @@ -42,7 +45,7 @@ public App() /// Invoked when the application is launched. /// /// Details about the launch request and process. - protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + protected override void OnLaunched(LaunchActivatedEventArgs args) { MainWindow.Activate(); } diff --git a/FluentAutoClicker/Controls/IsEnabledTextBlock/IsEnabledTextBlock.cs b/FluentAutoClicker/Controls/IsEnabledTextBlock/IsEnabledTextBlock.cs new file mode 100644 index 0000000..a7a56cc --- /dev/null +++ b/FluentAutoClicker/Controls/IsEnabledTextBlock/IsEnabledTextBlock.cs @@ -0,0 +1,89 @@ +// Copyright (C) 2025 Ryan Luu +// +// This file is part of Fluent Auto Clicker. +// +// Fluent Auto Clicker is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Fluent Auto Clicker is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Fluent Auto Clicker. If not, see . + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.ComponentModel; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace FluentAutoClicker.Controls; + +/// +/// A custom control that displays text and changes its visual state based on its enabled state. +/// +[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")] +[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")] +public partial class IsEnabledTextBlock : Control +{ + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TextProperty = DependencyProperty.Register( + "Text", + typeof(string), + typeof(IsEnabledTextBlock), + null); + + /// + /// Initializes a new instance of the class. + /// + public IsEnabledTextBlock() + { + DefaultStyleKey = typeof(IsEnabledTextBlock); + } + + /// + /// Gets or sets the text content of the control. + /// + [Localizable(true)] + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Applies the control template and sets up the initial visual state. + /// + protected override void OnApplyTemplate() + { + IsEnabledChanged -= IsEnabledTextBlock_IsEnabledChanged; + SetEnabledState(); + IsEnabledChanged += IsEnabledTextBlock_IsEnabledChanged; + base.OnApplyTemplate(); + } + + /// + /// Handles the IsEnabledChanged event and updates the visual state. + /// + /// The source of the event. + /// The event data. + private void IsEnabledTextBlock_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + SetEnabledState(); + } + + /// + /// Sets the visual state of the control based on its enabled state. + /// + private void SetEnabledState() + { + _ = VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true); + } +} \ No newline at end of file diff --git a/FluentAutoClicker/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml b/FluentAutoClicker/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml new file mode 100644 index 0000000..ec41958 --- /dev/null +++ b/FluentAutoClicker/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml @@ -0,0 +1,35 @@ + + + + \ No newline at end of file diff --git a/FluentAutoClicker/FluentAutoClicker.csproj b/FluentAutoClicker/FluentAutoClicker.csproj index 710ab2e..643866b 100644 --- a/FluentAutoClicker/FluentAutoClicker.csproj +++ b/FluentAutoClicker/FluentAutoClicker.csproj @@ -8,6 +8,7 @@ app.manifest x86;x64;ARM64 win-x86;win-x64;win-arm64 + 8305 win-$(Platform).pubxml enable enable @@ -38,13 +39,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + - - False - - False diff --git a/FluentAutoClicker/Helpers/AutoClicker.cs b/FluentAutoClicker/Helpers/AutoClicker.cs index c296b4c..2bd0dfe 100644 --- a/FluentAutoClicker/Helpers/AutoClicker.cs +++ b/FluentAutoClicker/Helpers/AutoClicker.cs @@ -35,7 +35,7 @@ public static class AutoClicker /// The number of milliseconds to wait before clicks. /// The number of clicks before stopping the auto clicker thread. /// The mouse button used to click. - /// The amount of time in milliseconds to add randomly to the millisecond delay between clicks. + /// Milliseconds to add randomly to delay between clicks. public static void Start(int millisecondsDelay = 100, int clickAmount = 0, int mouseButtonType = 0, int clickDelayOffset = 0) { @@ -82,23 +82,26 @@ private static async void AutoClickerThread(int clickInterval, int repeatAmount, /// /// Clicks the mouse button. /// - /// The mouse button to click. - private static void ClickMouse(int button) + /// The mouse button to click. + private static void ClickMouse(int mouseButton) { - if (button == 0) // Left mouse button + switch (mouseButton) { - SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN); - SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP); - } - else if (button == 1) // Middle mouse button - { - SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_MIDDLEDOWN); - SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_MIDDLEUP); - } - else if (button == 2) // Right mouse button - { - SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTDOWN); - SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTUP); + // Left mouse button + case 0: + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTDOWN); + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_LEFTUP); + break; + // Middle mouse button + case 1: + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_MIDDLEDOWN); + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_MIDDLEUP); + break; + // Right mouse button + case 2: + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTDOWN); + SendMouseInput(MOUSE_EVENT_FLAGS.MOUSEEVENTF_RIGHTUP); + break; } } @@ -119,4 +122,4 @@ private static void SendMouseInput(MOUSE_EVENT_FLAGS dwFlags) _ = PInvoke.SendInput(inputs, Marshal.SizeOf()); } -} +} \ No newline at end of file diff --git a/FluentAutoClicker/MainPage.xaml b/FluentAutoClicker/MainPage.xaml index 4cb3a57..d5fa29b 100644 --- a/FluentAutoClicker/MainPage.xaml +++ b/FluentAutoClicker/MainPage.xaml @@ -1,13 +1,25 @@ + - + + 4 + 8 + 8 + 8 + + + @@ -20,9 +32,9 @@ - - - + + + @@ -82,6 +94,7 @@ x:Name="MouseButtonTypeComboBox" x:Uid="ComboBoxMouseButton" Grid.Row="1" + Grid.Column="0" SelectedIndex="0"> @@ -89,12 +102,12 @@ - - - + \ No newline at end of file diff --git a/FluentAutoClicker/MainPage.xaml.cs b/FluentAutoClicker/MainPage.xaml.cs index e2b290a..ef4e958 100644 --- a/FluentAutoClicker/MainPage.xaml.cs +++ b/FluentAutoClicker/MainPage.xaml.cs @@ -18,98 +18,151 @@ using FluentAutoClicker.Helpers; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Media; +using Microsoft.Windows.BadgeNotifications; using System.Globalization; -using System.Runtime.InteropServices; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Input.KeyboardAndMouse; -using Windows.Win32.UI.WindowsAndMessaging; +using WinRT.Interop; +using WinUIEx.Messaging; namespace FluentAutoClicker; /// /// The main page containing all controls displayed on the main window. /// -public sealed partial class MainPage : Page +public sealed partial class MainPage { + /// + /// The settings page instance. + /// + private static readonly SettingsPage settingsPage = new(); + + /// + /// Initializes a new instance of the class. + /// public MainPage() { InitializeComponent(); Loaded += MainPage_Loaded; + + // Set tooltip + ToolTipService.SetToolTip(ToggleButtonStart, "ToggleButtonStartTooltipStart".GetLocalized()); } - private WNDPROC? origHotKeyProc; - private WNDPROC? hotKeyProcD; + /// + /// Gets or sets a value indicating whether the hotkey is registered. + /// + private bool IsHotKeyRegistered { get; set; } - private LRESULT HotKeyProc(HWND hWnd, uint Msg, WPARAM wParam, LPARAM lParam) + /// + /// Handles the Loaded event of the MainPage control. + /// + /// The source of the event. + /// The event data. + private async void MainPage_Loaded(object sender, RoutedEventArgs e) { - uint WM_HOTKEY = 0x0312; // HotKey Window Message + // Set badge notification + SetNotificationBadge(BadgeNotificationGlyph.Paused); - if (Msg == WM_HOTKEY) + // Prevent registering multiple hotkeys + if (IsHotKeyRegistered) { - if (StartToggleButton.IsEnabled) - { - StartToggleButton.IsChecked = !StartToggleButton.IsChecked; - } + return; } - return PInvoke.CallWindowProc(origHotKeyProc, hWnd, Msg, wParam, lParam); - } + // Get window handle + MainWindow window = App.MainWindow; + HWND hWnd = new(WindowNative.GetWindowHandle(window)); - [LibraryImport("user32.dll", EntryPoint = "SetWindowLongW", SetLastError = true)] - private static partial int SetWindowLong_x86(nint hWnd, int nIndex, int dwNewLong); + // Set up window message monitor + WindowMessageMonitor monitor = new(window); + monitor.WindowMessageReceived += OnWindowMessageReceived; - [LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)] - private static partial nint SetWindowLongPtr_x64(nint hWnd, int nIndex, nint dwNewLong); + // Register hotkey + bool success = PInvoke.RegisterHotKey(hWnd, 0x0000, HOT_KEY_MODIFIERS.MOD_NOREPEAT, 0x75); // F6 + if (success) + { + IsHotKeyRegistered = true; + } + else + { + ContentDialog dialog = new() + { + // XamlRoot must be set in the case of a ContentDialog running in a Desktop app + XamlRoot = XamlRoot, + Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style, + Title = "Failed to register hotkey", + Content = "Please modify the hotkey setting.", + CloseButtonText = "OK", + DefaultButton = ContentDialogButton.Primary + }; - // CsWin32 doesn't allow specifying the calling convention between x86 and x64 - private static nint SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong) - { - nint hWndInt = hWnd.Value; - return IntPtr.Size == 4 - ? SetWindowLong_x86(hWndInt, nIndex, (int)dwNewLong) - : SetWindowLongPtr_x64(hWndInt, nIndex, dwNewLong); + _ = await dialog.ShowAsync(); + } } - private void MainPage_Loaded(object sender, RoutedEventArgs e) + /// + /// Handles the WindowMessageReceived event of the WindowMessageMonitor control. + /// + /// The source of the event. + /// The event data. + private void OnWindowMessageReceived(object? sender, WindowMessageEventArgs e) { - // Get window handle - MainWindow window = App.MainWindow; - HWND hWnd = new(WinRT.Interop.WindowNative.GetWindowHandle(window)); + if (e.Message.MessageId == 0x0312) // WM_HOTKEY event + { + if (ToggleButtonStart.IsEnabled) + { + ToggleButtonStart.IsChecked = !ToggleButtonStart.IsChecked; + } + } + } - // Register hotkey - int id = 0x0000; - _ = PInvoke.RegisterHotKey(hWnd, id, HOT_KEY_MODIFIERS.MOD_NOREPEAT, 0x75); // F6 - - // Add hotkey function pointer to window procedure - int GWL_WNDPROC = -4; - hotKeyProcD = HotKeyProc; - IntPtr hotKeyProcPtr = Marshal.GetFunctionPointerForDelegate(hotKeyProcD); - IntPtr wndPtr = SetWindowLongPtr(hWnd, GWL_WNDPROC, hotKeyProcPtr); - origHotKeyProc = Marshal.GetDelegateForFunctionPointer(wndPtr); + /// + /// Sets the notification badge. + /// + /// The badge notification glyph. + private static void SetNotificationBadge(BadgeNotificationGlyph glyph) + { + if (glyph == BadgeNotificationGlyph.Paused && settingsPage.NotificationBadgePaused) + { + BadgeNotificationManager.Current.SetBadgeAsGlyph(glyph); + } + else if (glyph == BadgeNotificationGlyph.Playing && settingsPage.NotificationBadgePlaying) + { + BadgeNotificationManager.Current.SetBadgeAsGlyph(glyph); + } + else + { + BadgeNotificationManager.Current.ClearBadge(); + } } + /// + /// Sets the enabled state of the controls. + /// + /// if set to true the controls are enabled; otherwise, they are disabled. private void SetControlsEnabled(bool isEnabled) { + ClickIntervalTextBlock.IsEnabled = isEnabled; NumberBoxHours.IsEnabled = isEnabled; NumberBoxMinutes.IsEnabled = isEnabled; NumberBoxSeconds.IsEnabled = isEnabled; NumberBoxMilliseconds.IsEnabled = isEnabled; MouseButtonTypeComboBox.IsEnabled = isEnabled; + HotkeyTextBlock.IsEnabled = isEnabled; ClickRepeatCheckBox.IsEnabled = isEnabled; ClickOffsetCheckBox.IsEnabled = isEnabled; - - ClickOffsetAmount.IsEnabled = ClickOffsetCheckBox.IsChecked == true && isEnabled; - ClickRepeatAmount.IsEnabled = ClickRepeatCheckBox.IsChecked == true && isEnabled; - - // TODO: Change this to use a custom control. See https://github.com/RyanLua/FluentAutoClicker/issues/42 - string brushKey = - isEnabled ? "SystemControlForegroundBaseHighBrush" : "SystemControlForegroundBaseMediumLowBrush"; - ClickIntervalTextBlock.Foreground = Application.Current.Resources[brushKey] as Brush; - HotkeyTextBlock.Foreground = Application.Current.Resources[brushKey] as Brush; + ClickOffsetAmount.IsEnabled = isEnabled && ClickOffsetCheckBox.IsChecked == true; + ClickRepeatAmount.IsEnabled = isEnabled && ClickRepeatCheckBox.IsChecked == true; } + /// + /// Gets the value of the specified NumberBox. + /// + /// The NumberBox control. + /// The default value. + /// The value of the NumberBox. private static int GetNumberBoxValue(NumberBox numberBox, int defaultValue) { if (!int.TryParse(numberBox.Value.ToString(CultureInfo.InvariantCulture), out int value)) @@ -121,6 +174,10 @@ private static int GetNumberBoxValue(NumberBox numberBox, int defaultValue) return value; } + /// + /// Gets the interval in milliseconds. + /// + /// The interval in milliseconds. private int GetIntervalMilliseconds() { int hours = GetNumberBoxValue(NumberBoxHours, 0); @@ -139,35 +196,53 @@ private int GetIntervalMilliseconds() return totalTimeInMilliseconds; } - private async void StartToggleButton_OnChecked(object sender, RoutedEventArgs e) + /// + /// Handles the Checked event of the ToggleButtonStart control. + /// + /// The source of the event. + /// The event data. + private async void ToggleButtonStart_OnChecked(object sender, RoutedEventArgs e) { - StartToggleButton.IsEnabled = false; + // Update controls + ToggleButtonStart.IsEnabled = false; SetControlsEnabled(false); + await Task.Delay(1000); + FontIconStart.Glyph = "\uEDB4"; + BadgeNotificationManager.Current.SetBadgeAsGlyph(BadgeNotificationGlyph.Playing); + SetNotificationBadge(BadgeNotificationGlyph.Playing); + ToolTipService.SetToolTip(ToggleButtonStart, "ToggleButtonStartTooltipStop".GetLocalized()); - // 3-second countdown - for (int i = 3; i > 0; i--) - { - StartToggleButton.Content = i.ToString(); - await Task.Delay(1000); - } - - StartToggleButton.IsEnabled = true; - StartToggleButton.Content = "Stop"; - + // Start auto clicker int clickInterval = GetIntervalMilliseconds(); - int repeatAmount = ClickRepeatCheckBox.IsEnabled == true ? Convert.ToInt32(ClickRepeatAmount.Value) : 0; + int repeatAmount = ClickRepeatCheckBox.IsChecked == true ? Convert.ToInt32(ClickRepeatAmount.Value) : 0; int mouseButton = MouseButtonTypeComboBox.SelectedIndex; int clickOffset = ClickOffsetCheckBox.IsChecked == true ? Convert.ToInt32(ClickOffsetAmount.Value) : 0; AutoClicker.Start(clickInterval, repeatAmount, mouseButton, clickOffset); + ToggleButtonStart.IsEnabled = true; } - private void StartToggleButton_OnUnchecked(object sender, RoutedEventArgs e) + /// + /// Handles the Unchecked event of the ToggleButtonStart control. + /// + /// The source of the event. + /// The event data. + private void ToggleButtonStart_OnUnchecked(object sender, RoutedEventArgs e) { - StartToggleButton.Content = "Start"; - AutoClicker.Stop(); + // Update controls SetControlsEnabled(true); + FontIconStart.Glyph = "\uEE4A"; + SetNotificationBadge(BadgeNotificationGlyph.Paused); + ToolTipService.SetToolTip(ToggleButtonStart, "ToggleButtonStartTooltipStart".GetLocalized()); + + // Stop auto clicker + AutoClicker.Stop(); } + /// + /// Handles the Click event of the CheckBox controls. + /// + /// The source of the event. + /// The event data. private void CheckBox_Click(object sender, RoutedEventArgs e) { if (sender.Equals(ClickRepeatCheckBox)) @@ -179,4 +254,14 @@ private void CheckBox_Click(object sender, RoutedEventArgs e) ClickOffsetAmount.IsEnabled = ClickOffsetCheckBox.IsChecked == true; } } -} + + /// + /// Handles the Click event of the SettingsButton control. + /// + /// The source of the event. + /// The event data. + private void SettingsButton_Click(object sender, RoutedEventArgs e) + { + _ = Frame.Navigate(typeof(SettingsPage)); + } +} \ No newline at end of file diff --git a/FluentAutoClicker/MainWindow.xaml b/FluentAutoClicker/MainWindow.xaml index f56690b..e5d6130 100644 --- a/FluentAutoClicker/MainWindow.xaml +++ b/FluentAutoClicker/MainWindow.xaml @@ -1,37 +1,38 @@ + - - - - - + - + - - + + - - + + - + \ No newline at end of file diff --git a/FluentAutoClicker/MainWindow.xaml.cs b/FluentAutoClicker/MainWindow.xaml.cs index f131397..1ef8d1e 100644 --- a/FluentAutoClicker/MainWindow.xaml.cs +++ b/FluentAutoClicker/MainWindow.xaml.cs @@ -16,6 +16,8 @@ // along with Fluent Auto Clicker. If not, see . using FluentAutoClicker.Helpers; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -23,10 +25,13 @@ namespace FluentAutoClicker; /// -/// An window that displays a page's contents. +/// An window that displays a page's contents. /// public sealed partial class MainWindow { + /// + /// Initializes a new instance of the class. + /// public MainWindow() { InitializeComponent(); @@ -37,6 +42,31 @@ public MainWindow() // Set up window title bar ExtendsContentIntoTitleBar = true; - AppTitleBar.Title = "AppDisplayName".GetLocalized(); + + // Set up frame + _ = NavFrame.Navigate(typeof(MainPage)); + } + + /// + /// Handles the BackRequested event of the AppTitleBar control. + /// + /// The source of the event. + /// The event data. + private void AppTitleBar_BackRequested(TitleBar sender, object args) + { + if (NavFrame.CanGoBack) + { + NavFrame.GoBack(); + } + } + + /// + /// Handles the Navigated event of the NavFrame control. + /// + /// The source of the event. + /// The event data. + private void NavFrame_Navigated(object sender, NavigationEventArgs e) + { + AppTitleBar.IsBackButtonVisible = e.SourcePageType != typeof(MainPage); } } \ No newline at end of file diff --git a/FluentAutoClicker/Package.appxmanifest b/FluentAutoClicker/Package.appxmanifest index b54ee96..ff9ffa5 100644 --- a/FluentAutoClicker/Package.appxmanifest +++ b/FluentAutoClicker/Package.appxmanifest @@ -9,14 +9,14 @@ ms-resource:AppDisplayName - Ryan Luu + ms-resource:AppPublisherDisplayName Assets\StoreLogo.png diff --git a/FluentAutoClicker/Program.cs b/FluentAutoClicker/Program.cs index b830b22..ab91ce8 100644 --- a/FluentAutoClicker/Program.cs +++ b/FluentAutoClicker/Program.cs @@ -20,23 +20,31 @@ using Microsoft.Windows.AppLifecycle; using System.Diagnostics; using System.Runtime.InteropServices; +using WinRT; namespace FluentAutoClicker; /// -/// Customized Program.cs file to implement single-instancing in a WinUI app with C#. Single-instanced apps only allow one instance of the app running at a time. +/// Customized Program.cs file to implement +/// +/// single-instancing +/// in a WinUI app with C#. +/// +/// Single-instanced apps only allow one instance of the app running at a time. /// public partial class Program { + private static IntPtr _redirectEventHandle = IntPtr.Zero; + [STAThread] private static int Main() { - WinRT.ComWrappersSupport.InitializeComWrappers(); + ComWrappersSupport.InitializeComWrappers(); bool isRedirect = DecideRedirection(); if (!isRedirect) { - Application.Start((p) => + Application.Start(p => { DispatcherQueueSynchronizationContext context = new( DispatcherQueue.GetForCurrentThread()); @@ -52,7 +60,6 @@ private static bool DecideRedirection() { bool isRedirect = false; AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs(); - ExtendedActivationKind kind = args.Kind; AppInstance keyInstance = AppInstance.FindOrRegisterForKey("FluentAutoClickerApp"); if (keyInstance.IsCurrent) @@ -80,18 +87,16 @@ private static partial IntPtr CreateEvent( [LibraryImport("ole32.dll")] private static partial uint CoWaitForMultipleObjects( uint dwFlags, uint dwMilliseconds, ulong nHandles, - [In, Out] IntPtr[] pHandles, out uint dwIndex); + [In][Out] IntPtr[] pHandles, out uint dwIndex); [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool SetForegroundWindow(IntPtr hWnd); - private static IntPtr _redirectEventHandle = IntPtr.Zero; - // Do the redirection on another thread, and use a non-blocking // wait method to wait for the redirection to complete. - public static void RedirectActivationTo(AppActivationArguments args, - AppInstance keyInstance) + private static void RedirectActivationTo(AppActivationArguments args, + AppInstance keyInstance) { _redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null); _ = Task.Run(() => @@ -100,11 +105,11 @@ public static void RedirectActivationTo(AppActivationArguments args, _ = SetEvent(_redirectEventHandle); }); - uint cwmoDefault = 0; - uint infinite = 0xFFFFFFFF; + const uint cwmoDefault = 0; + const uint infinite = 0xFFFFFFFF; _ = CoWaitForMultipleObjects( - cwmoDefault, infinite, 1, - [_redirectEventHandle], out uint handleIndex); + cwmoDefault, infinite, 1, + [_redirectEventHandle], out uint _); // Bring the window to the foreground Process process = Process.GetProcessById((int)keyInstance.ProcessId); diff --git a/FluentAutoClicker/SettingsPage.xaml b/FluentAutoClicker/SettingsPage.xaml new file mode 100644 index 0000000..5677288 --- /dev/null +++ b/FluentAutoClicker/SettingsPage.xaml @@ -0,0 +1,120 @@ + + + + + + 4 + -12,0,0,0 + 0,8,0,8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FluentAutoClicker/SettingsPage.xaml.cs b/FluentAutoClicker/SettingsPage.xaml.cs new file mode 100644 index 0000000..ee1f085 --- /dev/null +++ b/FluentAutoClicker/SettingsPage.xaml.cs @@ -0,0 +1,201 @@ +// Copyright (C) 2025 Ryan Luu +// +// This file is part of Fluent Auto Clicker. +// +// Fluent Auto Clicker is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Fluent Auto Clicker is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Fluent Auto Clicker. If not, see . + +using FluentAutoClicker.Helpers; +using Microsoft.UI.Composition.SystemBackdrops; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using System.Runtime.InteropServices; +using Windows.ApplicationModel; +using Windows.Storage; +using Windows.System; +using WinUIEx; +using SystemBackdrop = Microsoft.UI.Xaml.Media.SystemBackdrop; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace FluentAutoClicker; + +/// +/// Represents the settings page of the Fluent Auto Clicker application. +/// +public sealed partial class SettingsPage +{ + /// + /// Application settings container in the local app data store. + /// + private readonly ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings; + + /// + /// Initializes a new instance of the class. + /// + public SettingsPage() + { + InitializeComponent(); + + // Initialize saved settings + ThemeSelectedIndex = ThemeSelectedIndex; + BackdropSelectedIndex = BackdropSelectedIndex; + IsAlwaysOnTop = IsAlwaysOnTop; + + // Initialize app name and version + AppAboutSettingsExpander.Header = AppName; + AppVersionTextBlock.Text = AppVersion; + } + + /// + /// Gets or sets the if the playing notification badge is enabled. + /// + public bool NotificationBadgePlaying + { + get => (bool)(localSettings.Values[nameof(NotificationBadgePlaying)] ?? true); + set => localSettings.Values[nameof(NotificationBadgePlaying)] = value; + } + + /// + /// Gets or sets the if the paused notification badge is enabled. + /// + public bool NotificationBadgePaused + { + get => (bool)(localSettings.Values[nameof(NotificationBadgePaused)] ?? true); + set => localSettings.Values[nameof(NotificationBadgePaused)] = value; + } + + /// + /// Gets the localized application name. + /// + private static string AppName => "AppDisplayName".GetLocalized(); + + /// + /// Gets the application version. + /// + private static string AppVersion + { + get + { + PackageVersion version = Package.Current.Id.Version; + return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + } + } + + /// + /// Gets the main application window. + /// + private static WindowEx MainWindow => App.MainWindow; + + /// + /// Gets or sets the selected theme index. + /// + private int ThemeSelectedIndex + { + get => (int)(localSettings.Values[nameof(ThemeSelectedIndex)] ?? 0); + set + { + localSettings.Values[nameof(ThemeSelectedIndex)] = value; + ((FrameworkElement)MainWindow.Content).RequestedTheme = value switch + { + 1 => ElementTheme.Light, + 2 => ElementTheme.Dark, + _ => ElementTheme.Default + }; + } + } + + /// + /// Gets or sets the selected backdrop index. + /// + private int BackdropSelectedIndex + { + get => (int)(localSettings.Values[nameof(BackdropSelectedIndex)] ?? 0); + set + { + localSettings.Values[nameof(BackdropSelectedIndex)] = value; + + // HACK: Prevent changing the backdrop so it doesn't flash between pages + SystemBackdrop currentBackdrop = MainWindow.SystemBackdrop; + bool needsChange = value switch + { + 1 => currentBackdrop is not MicaBackdrop { Kind: MicaKind.BaseAlt }, + 2 => currentBackdrop is not DesktopAcrylicBackdrop, + _ => currentBackdrop is not MicaBackdrop || + (currentBackdrop is MicaBackdrop mica && mica.Kind != MicaKind.Base) + }; + + if (needsChange) + { + MainWindow.SystemBackdrop = value switch + { + 1 => new MicaBackdrop { Kind = MicaKind.BaseAlt }, + 2 => new DesktopAcrylicBackdrop(), + _ => new MicaBackdrop() + }; + } + } + } + + /// + /// Gets or sets a value indicating whether the application window is always on top. + /// + private bool IsAlwaysOnTop + { + get => (bool)(localSettings.Values[nameof(IsAlwaysOnTop)] ?? true); + set + { + localSettings.Values[nameof(IsAlwaysOnTop)] = value; + MainWindow.IsAlwaysOnTop = value; + } + } + + /// + /// Handles the click event of the feedback hyperlink button. + /// + /// The source of the event. + /// The event data. + private async void HyperlinkButtonFeedback_Click(object sender, RoutedEventArgs e) + { + string recipientEmail = "feedback@fluentautoclicker.com"; + string subject = $"{AppName} App Feedback"; + string messageBody = $""" + + + ---------- Add your feedback above ---------- + + .NET installation: {RuntimeInformation.FrameworkDescription} + App version: {AppVersion} + App architecture: {RuntimeInformation.ProcessArchitecture} + OS version: {RuntimeInformation.OSDescription} + OS architecture: {RuntimeInformation.OSArchitecture} + """; + + await ComposeEmailAsync(recipientEmail, subject, messageBody); + } + + /// + /// Composes and launches an email with the specified recipient, subject, and message body. + /// + /// The recipient email address. + /// The email subject. + /// The email message body. + private static async Task ComposeEmailAsync(string recipientEmail, string subject, string messageBody) + { + Uri uri = new( + $"mailto:{recipientEmail}?subject={Uri.EscapeDataString(subject)}&body={Uri.EscapeDataString(messageBody)}"); + + _ = await Launcher.LaunchUriAsync(uri); + } +} \ No newline at end of file diff --git a/FluentAutoClicker/Strings/en-us/Resources.resw b/FluentAutoClicker/Strings/en-us/Resources.resw index 468f297..727894f 100644 --- a/FluentAutoClicker/Strings/en-us/Resources.resw +++ b/FluentAutoClicker/Strings/en-us/Resources.resw @@ -165,11 +165,11 @@ Which button to start the auto clicker - - Start + + Settings - - Start the auto clicker + + Start Click repeat @@ -186,4 +186,112 @@ Description + + Settings + + + Appearance + + + App theme + + + Select which app theme to display + + + Use system setting + + + Light + + + Dark + + + Backdrop material + + + System backdrop material for window background + + + Mica + + + Mica Alt + + + Acrylic + + + Always on top + + + Always show the app's window on top of others + + + Behavior + + + Ryan Luu + + + Preview + + + Fluent Auto Clicker + + + Privacy policy + + + https://github.com/RyanLua/FluentAutoClicker/blob/main/PRIVACY_POLICY.md + + + Source code + + + Website + + + https://fluentautoclicker.com + + + https://github.com/RyanLua/FluentAutoClicker + + + Translate + + + https://translate.fluentautoclicker.com + + + Discord + + + https://discord.gg/32AMskrpFf + + + Send feedback + + + License + + + https://github.com/RyanLua/FluentAutoClicker/blob/main/COPYING.md + + + Notification badge + + + Show the status notification badge on the app's taskbar icon + + + Show badge while clicking + + + Show badge while paused + + + Pause + \ No newline at end of file diff --git a/FluentAutoClicker/Styles.xaml b/FluentAutoClicker/Styles.xaml index e53df05..c0b81db 100644 --- a/FluentAutoClicker/Styles.xaml +++ b/FluentAutoClicker/Styles.xaml @@ -1,9 +1,5 @@ - - - 4 - 8 - 8 + 16,8,16,16 \ No newline at end of file diff --git a/FluentAutoClicker/Themes/Generic.xaml b/FluentAutoClicker/Themes/Generic.xaml new file mode 100644 index 0000000..d7756fc --- /dev/null +++ b/FluentAutoClicker/Themes/Generic.xaml @@ -0,0 +1,6 @@ + + + + + +