Initial commit: SpdUp Windows Gaming Booster v1.0.0

- .NET 8 WPF app with full MVVM architecture
- One-click boost: CPU, RAM, GPU, network, services, timer resolution
- Real-time system monitor (LibreHardwareMonitor)
- Auto game detection (30+ games)
- RAM optimizer with kernel-level cleanup
- Network optimizer (TCP registry tweaks)
- Service manager, process manager, shader cache cleaner
- Desktop widget overlay
- Debug log viewer
- Custom dark gaming theme with trans pride colors
- 84 unit tests (xUnit)
- Inno Setup installer script
- Landing page website
This commit is contained in:
Phil-icyou
2026-03-08 17:34:45 +01:00
commit 19ba425e79
68 changed files with 6176 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# Compiled output
bin/
obj/
publish/
# IDE
.vs/
*.user
*.suo
# Temp files
*_wpftmp.csproj
*.rsuser
# NuGet
*.nupkg
packages/
# Build
*.dll
*.exe
*.pdb
# Test results
TestResults/
# Inno Setup output
Installer/Output/
+13
View File
@@ -0,0 +1,13 @@
<Application x:Class="SpdUp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SpdUp"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/DarkTheme.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+49
View File
@@ -0,0 +1,49 @@
using System.Windows;
using System.Windows.Threading;
using SpdUp.Core;
namespace SpdUp;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Global exception handlers
DispatcherUnhandledException += OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
// Enable required privileges early
NativeMethods.EnablePrivilege(NativeMethods.SE_DEBUG_NAME);
NativeMethods.EnablePrivilege(NativeMethods.SE_PROF_SINGLE_PROCESS_NAME);
NativeMethods.EnablePrivilege(NativeMethods.SE_INC_BASE_PRIORITY_NAME);
Logger.Info("SpdUp application starting.");
}
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
Logger.Error("Unhandled UI exception", e.Exception);
MessageBox.Show($"An unexpected error occurred:\n{e.Exception.Message}",
"SpdUp Error", MessageBoxButton.OK, MessageBoxImage.Error);
e.Handled = true;
}
private static void OnDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
if (e.ExceptionObject is Exception ex)
Logger.Error("Unhandled domain exception", ex);
}
private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
Logger.Error("Unobserved task exception", e.Exception);
e.SetObserved();
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+86
View File
@@ -0,0 +1,86 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace SpdUp.Converters;
/// <summary>Bool → Visibility converter.</summary>
public sealed class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is true ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> value is Visibility.Visible;
}
/// <summary>Inverted Bool → Visibility converter.</summary>
public sealed class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is true ? Visibility.Collapsed : Visibility.Visible;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> value is Visibility.Collapsed;
}
/// <summary>Float percentage (0100) to a progress bar width or colour.</summary>
public sealed class PercentToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var pct = System.Convert.ToSingle(value);
if (pct > 90) return new SolidColorBrush(Color.FromRgb(255, 69, 58)); // red
if (pct > 70) return new SolidColorBrush(Color.FromRgb(255, 159, 10)); // orange
return new SolidColorBrush(Color.FromRgb(48, 209, 88)); // green
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
/// <summary>Null → Visibility (collapsed when null).</summary>
public sealed class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is null ? Visibility.Collapsed : Visibility.Visible;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
/// <summary>Format float to one decimal.</summary>
public sealed class FloatFormatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var fmt = parameter as string ?? "F1";
return System.Convert.ToSingle(value).ToString(fmt);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
/// <summary>Bool to boost button text.</summary>
public sealed class BoostButtonTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is true ? "⏹ STOP BOOST" : "🚀 BOOST GAME";
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
/// <summary>Bool to brush (boost active colour).</summary>
public sealed class BoostButtonColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is true
? new SolidColorBrush(Color.FromRgb(255, 69, 58))
: new SolidColorBrush(Color.FromRgb(48, 209, 88));
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
+71
View File
@@ -0,0 +1,71 @@
using System.IO;
namespace SpdUp.Core;
/// <summary>
/// Application-wide logging service with live event for UI consumption.
/// </summary>
public static class Logger
{
private static readonly object _lock = new();
private static readonly string _logDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SpdUp", "Logs");
/// <summary>Fired on every log write subscribers receive the formatted line.</summary>
public static event Action<string>? LogWritten;
static Logger()
{
Directory.CreateDirectory(_logDir);
}
private static string LogFile => Path.Combine(_logDir, $"SpdUp_{DateTime.Now:yyyy-MM-dd}.log");
public static void Info(string message) => Write("INFO", message);
public static void Warn(string message) => Write("WARN", message);
public static void Error(string message, Exception? ex = null)
{
var msg = ex is null ? message : $"{message} | {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}";
Write("ERROR", msg);
}
private static void Write(string level, string message)
{
var line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}";
lock (_lock)
{
try
{
File.AppendAllText(LogFile, line + Environment.NewLine);
}
catch
{
// Swallow logging must never crash the app.
}
}
// Notify UI subscribers
try { LogWritten?.Invoke(line); } catch { }
#if DEBUG
System.Diagnostics.Debug.WriteLine(line);
#endif
}
/// <summary>
/// Removes log files older than the specified number of days.
/// </summary>
public static void PurgeOldLogs(int keepDays = 14)
{
try
{
var cutoff = DateTime.Now.AddDays(-keepDays);
foreach (var file in Directory.GetFiles(_logDir, "SpdUp_*.log"))
{
if (File.GetCreationTime(file) < cutoff)
File.Delete(file);
}
}
catch { /* best-effort */ }
}
}
+153
View File
@@ -0,0 +1,153 @@
using System.Runtime.InteropServices;
namespace SpdUp.Core;
/// <summary>
/// P/Invoke declarations used across the application.
/// </summary>
public static class NativeMethods
{
// ── Memory management ──────────────────────────────────────────────
[DllImport("psapi.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EmptyWorkingSet(IntPtr hProcess);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetProcessWorkingSetSizeEx(
IntPtr hProcess, IntPtr dwMinimumWorkingSetSize, IntPtr dwMaximumWorkingSetSize, uint flags);
// ── Timer resolution ───────────────────────────────────────────────
[DllImport("ntdll.dll", SetLastError = true)]
public static extern int NtSetTimerResolution(uint DesiredResolution, bool SetResolution, out uint CurrentResolution);
[DllImport("ntdll.dll", SetLastError = true)]
public static extern int NtQueryTimerResolution(out uint MinimumResolution, out uint MaximumResolution, out uint CurrentResolution);
// ── System memory info ─────────────────────────────────────────────
[StructLayout(LayoutKind.Sequential)]
public struct MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
// ── System cache / standby list purge ──────────────────────────────
public enum SYSTEM_MEMORY_LIST_COMMAND
{
MemoryCaptureAccessedBits = 0,
MemoryCaptureAndResetAccessedBits = 1,
MemoryEmptyWorkingSets = 2,
MemoryFlushModifiedList = 3,
MemoryPurgeStandbyList = 4,
MemoryPurgeLowPriorityStandbyList = 5,
MemoryCommandMax = 6
}
[DllImport("ntdll.dll")]
public static extern int NtSetSystemInformation(
int SystemInformationClass, ref int SystemInformation, int SystemInformationLength);
// SystemMemoryListInformation = 0x0050
public const int SystemMemoryListInformation = 0x0050;
// ── Privilege ──────────────────────────────────────────────────────
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool LookupPrivilegeValue(string? lpSystemName, string lpName, out LUID lpLuid);
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool AdjustTokenPrivileges(
IntPtr TokenHandle, bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState, uint BufferLength,
IntPtr PreviousState, IntPtr ReturnLength);
[StructLayout(LayoutKind.Sequential)]
public struct LUID
{
public uint LowPart;
public int HighPart;
}
[StructLayout(LayoutKind.Sequential)]
public struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public uint Attributes;
}
[StructLayout(LayoutKind.Sequential)]
public struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;
public LUID_AND_ATTRIBUTES Privileges;
}
public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
public const uint TOKEN_QUERY = 0x0008;
public const uint SE_PRIVILEGE_ENABLED = 0x00000002;
public const string SE_DEBUG_NAME = "SeDebugPrivilege";
public const string SE_PROF_SINGLE_PROCESS_NAME = "SeProfileSingleProcessPrivilege";
public const string SE_INC_BASE_PRIORITY_NAME = "SeIncreaseBasePriorityPrivilege";
public const uint PROCESS_ALL_ACCESS = 0x001F0FFF;
public const uint PROCESS_SET_INFORMATION = 0x0200;
public const uint PROCESS_QUERY_INFORMATION = 0x0400;
/// <summary>
/// Enable a privilege on the current process token.
/// </summary>
public static bool EnablePrivilege(string privilege)
{
try
{
if (!OpenProcessToken(System.Diagnostics.Process.GetCurrentProcess().Handle,
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out var tokenHandle))
return false;
try
{
if (!LookupPrivilegeValue(null, privilege, out var luid))
return false;
var tp = new TOKEN_PRIVILEGES
{
PrivilegeCount = 1,
Privileges = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = SE_PRIVILEGE_ENABLED }
};
return AdjustTokenPrivileges(tokenHandle, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
}
finally
{
CloseHandle(tokenHandle);
}
}
catch
{
return false;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace SpdUp.Core;
/// <summary>
/// Base class for ViewModels implementing INotifyPropertyChanged.
/// </summary>
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
+56
View File
@@ -0,0 +1,56 @@
using System.Windows.Input;
namespace SpdUp.Core;
/// <summary>
/// A basic ICommand implementation that delegates to Action/Func.
/// </summary>
public sealed class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public RelayCommand(Action execute, Func<bool>? canExecute = null)
: this(_ => execute(), canExecute is null ? null : _ => canExecute()) { }
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object? parameter) => _execute(parameter);
public void RaiseCanExecuteChanged() => CommandManager.InvalidateRequerySuggested();
}
/// <summary>
/// Typed relay command.
/// </summary>
public sealed class RelayCommand<T> : ICommand
{
private readonly Action<T?> _execute;
private readonly Func<T?, bool>? _canExecute;
public RelayCommand(Action<T?> execute, Func<T?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => _canExecute?.Invoke((T?)parameter) ?? true;
public void Execute(object? parameter) => _execute((T?)parameter);
}
+60
View File
@@ -0,0 +1,60 @@
; ══════════════════════════════════════════════════════════════
; SpdUp Gaming Booster · Inno Setup Installer Script
; Compile with Inno Setup 6+ (https://jrsoftware.org/isinfo.php)
; ══════════════════════════════════════════════════════════════
#define MyAppName "SpdUp"
#define MyAppVersion "1.0.0"
#define MyAppPublisher "Mandy"
#define MyAppExeName "SpdUp.exe"
#define PublishDir "..\publish\SpdUp"
[Setup]
AppId={{E7A3F2C1-8D4B-4F6A-9C2E-1B5D7F3A8E4C}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
OutputDir=..\publish
OutputBaseFilename=SpdUp_Setup_{#MyAppVersion}
Compression=lzma2/ultra64
SolidCompression=yes
LZMANumBlockThreads=4
SetupIconFile=
UninstallDisplayIcon={app}\{#MyAppExeName}
PrivilegesRequired=admin
WizardStyle=modern
WizardSizePercent=110
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
MinVersion=10.0.17763
DisableWelcomePage=no
AllowNoIcons=yes
ShowLanguageDialog=auto
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "startmenu"; Description: "Create Start Menu shortcut"; GroupDescription: "{cm:AdditionalIcons}"
[Files]
; Include all published files recursively
Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startmenu
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}"; Tasks: startmenu
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent runascurrentuser
[UninstallDelete]
; Clean up log files and settings on uninstall
Type: filesandordirs; Name: "{app}\logs"
Type: files; Name: "{app}\settings.json"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Mandy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+254
View File
@@ -0,0 +1,254 @@
<Window x:Class="SpdUp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:SpdUp.Views"
xmlns:vm="clr-namespace:SpdUp.ViewModels"
xmlns:conv="clr-namespace:SpdUp.Converters"
Title="SpdUp Gaming Booster"
Height="750" Width="1200" MinHeight="600" MinWidth="900"
WindowStartupLocation="CenterScreen"
WindowStyle="None" AllowsTransparency="False"
ResizeMode="CanResizeWithGrip"
Background="{StaticResource BgDarkBrush}"
Closing="Window_Closing">
<Window.Resources>
<conv:BoolToVisibilityConverter x:Key="BoolVis" />
<conv:BoostButtonTextConverter x:Key="BoostText" />
<conv:BoostButtonColorConverter x:Key="BoostColor" />
<!-- Page → View mapping -->
<DataTemplate DataType="{x:Type vm:DashboardViewModel}">
<views:DashboardView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:BoostViewModel}">
<views:BoostView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MonitorViewModel}">
<views:MonitorView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:GameLibraryViewModel}">
<views:GameLibraryView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ProcessManagerViewModel}">
<views:ProcessManagerView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ServiceManagerViewModel}">
<views:ServiceManagerView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SettingsViewModel}">
<views:SettingsView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:DebugLogViewModel}">
<views:DebugLogView />
</DataTemplate>
<!-- Window chrome button style -->
<Style x:Key="ChromeButton" TargetType="Button">
<Setter Property="Width" Value="46" />
<Setter Property="Height" Value="32" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bd" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="#33FFFFFF" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="bd" Property="Background" Value="#55FFFFFF" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ChromeCloseButton" TargetType="Button" BasedOn="{StaticResource ChromeButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bd" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="#E81123" />
<Setter Property="Foreground" Value="White" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="bd" Property="Background" Value="#C50F1F" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<!-- Custom chrome so we keep snap/resize but remove default frame -->
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="36" ResizeBorderThickness="6"
GlassFrameThickness="0" CornerRadius="0" />
</WindowChrome.WindowChrome>
<Border BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="36" /> <!-- Custom Title Bar -->
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ═══ CUSTOM TITLE BAR ═══════════════════════════════════ -->
<Border Grid.Row="0" Background="{StaticResource BgPanelBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="0,0,0,1">
<DockPanel>
<!-- Left: app icon + title (draggable area) -->
<TextBlock DockPanel.Dock="Left" VerticalAlignment="Center" Margin="12,0,0,0"
FontSize="12" Foreground="{StaticResource TextSecondaryBrush}">
<Run Text="⚡" Foreground="{StaticResource AccentGreenBrush}" />
<Run Text=" SpdUp" FontWeight="SemiBold" Foreground="{StaticResource TextPrimaryBrush}" />
<Run Text=" Gaming Booster" />
</TextBlock>
<!-- Right: window buttons -->
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="—" Style="{StaticResource ChromeButton}"
Click="Minimize_Click" />
<Button x:Name="MaxRestoreBtn" Content="☐" Style="{StaticResource ChromeButton}"
Click="MaximizeRestore_Click" />
<Button Content="✕" Style="{StaticResource ChromeCloseButton}"
Click="Close_Click" />
</StackPanel>
</DockPanel>
</Border>
<!-- ═══ MAIN LAYOUT ════════════════════════════════════════ -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="220" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- ═══ LEFT SIDEBAR ═══════════════════════════════════ -->
<Border Grid.Column="0" Background="{StaticResource BgPanelBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="0,0,1,0">
<DockPanel>
<!-- App Title -->
<StackPanel DockPanel.Dock="Top" Margin="16,16,16,8">
<TextBlock Text="⚡ SpdUp" FontSize="24" FontWeight="Bold"
Foreground="{StaticResource AccentGreenBrush}" />
<TextBlock Text="Gaming Booster" FontSize="11"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,0" />
</StackPanel>
<!-- Boost Status Indicator -->
<Border DockPanel.Dock="Top" Margin="12,8,12,12" Padding="12,8" CornerRadius="8"
Background="{Binding IsBoosted, Converter={StaticResource BoostColor}}"
Visibility="{Binding IsBoosted, Converter={StaticResource BoolVis}}">
<TextBlock Text="🚀 BOOST ACTIVE" FontWeight="Bold" FontSize="12"
Foreground="#000000" HorizontalAlignment="Center" />
</Border>
<!-- Trans Flag + Made with Love -->
<StackPanel DockPanel.Dock="Bottom" Margin="12,8,12,16">
<Grid Height="20" Margin="0,0,0,6">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0" Fill="{StaticResource TransBlueBrush}" RadiusX="3" RadiusY="3"/>
<Rectangle Grid.Row="1" Fill="{StaticResource TransPinkBrush}"/>
<Rectangle Grid.Row="2" Fill="{StaticResource TransWhiteBrush}"/>
<Rectangle Grid.Row="3" Fill="{StaticResource TransPinkBrush}"/>
<Rectangle Grid.Row="4" Fill="{StaticResource TransBlueBrush}" RadiusX="3" RadiusY="3"/>
</Grid>
<TextBlock Text="Made with ❤️ by Mandy" FontSize="10"
Foreground="{StaticResource TextSecondaryBrush}"
HorizontalAlignment="Center" />
</StackPanel>
<!-- Navigation -->
<ScrollViewer VerticalScrollBarVisibility="Auto" Margin="0,8,0,0">
<StackPanel>
<TextBlock Text="MAIN" FontSize="10" FontWeight="Bold" Margin="20,12,0,6"
Foreground="{StaticResource TextSecondaryBrush}" />
<Button Content="📊 Dashboard" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Dashboard" />
<Button Content="🚀 Game Boost" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Boost" />
<Button Content="📈 System Monitor" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Monitor" />
<TextBlock Text="MANAGE" FontSize="10" FontWeight="Bold" Margin="20,16,0,6"
Foreground="{StaticResource TextSecondaryBrush}" />
<Button Content="🎮 Game Library" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Games" />
<Button Content="📋 Processes" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Processes" />
<Button Content="⚙ Services" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Services" />
<TextBlock Text="OTHER" FontSize="10" FontWeight="Bold" Margin="20,16,0,6"
Foreground="{StaticResource TextSecondaryBrush}" />
<Button Content="🖥 Widget" Style="{StaticResource NavButton}"
Command="{Binding ToggleWidgetCommand}" />
<Button Content="🐛 Debug Log" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Debug" />
<Button Content="⚙ Settings" Style="{StaticResource NavButton}"
Command="{Binding NavigateCommand}" CommandParameter="Settings" />
</StackPanel>
</ScrollViewer>
</DockPanel>
</Border>
<!-- ═══ MAIN CONTENT ═══════════════════════════════════ -->
<DockPanel Grid.Column="1">
<!-- Top Bar -->
<Border DockPanel.Dock="Top" Padding="24,12" Background="{StaticResource BgDarkBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="0,0,0,1">
<DockPanel>
<TextBlock Text="{Binding CurrentPageTitle}" FontSize="20" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock Text="{Binding StatusText}" FontSize="11" VerticalAlignment="Center"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,16,0" />
<TextBlock VerticalAlignment="Center" Foreground="{StaticResource TextSecondaryBrush}"
FontSize="11">
<Run Text="CPU " /><Run Text="{Binding CpuUsage, StringFormat={}{0:F0}%}" Foreground="{StaticResource AccentCyanBrush}" />
<Run Text=" RAM " /><Run Text="{Binding RamUsagePercent, StringFormat={}{0:F0}%}" Foreground="{StaticResource AccentGreenBrush}" />
<Run Text=" GPU " /><Run Text="{Binding GpuUsage, StringFormat={}{0:F0}%}" Foreground="{StaticResource AccentPurpleBrush}" />
<Run Text=" Ping " /><Run Text="{Binding PingMs, StringFormat={}{0:F0}ms}" Foreground="{StaticResource AccentOrangeBrush}" />
</TextBlock>
</StackPanel>
</DockPanel>
</Border>
<!-- Status Bar -->
<Border DockPanel.Dock="Bottom" Padding="16,6" Background="{StaticResource BgPanelBrush}"
BorderBrush="{StaticResource BorderDimBrush}" BorderThickness="0,1,0,0">
<TextBlock Text="{Binding StatusText}" FontSize="11"
Foreground="{StaticResource TextSecondaryBrush}" />
</Border>
<!-- Page Host -->
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
Padding="24,20">
<ContentControl Content="{Binding CurrentPage}" />
</ScrollViewer>
</DockPanel>
</Grid>
</Grid>
</Border>
</Window>
+81
View File
@@ -0,0 +1,81 @@
using System.ComponentModel;
using System.Windows;
using SpdUp.ViewModels;
using SpdUp.Views;
namespace SpdUp;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly MainViewModel _vm;
private WidgetWindow? _widgetWindow;
public MainWindow()
{
InitializeComponent();
_vm = new MainViewModel();
DataContext = _vm;
_vm.PropertyChanged += OnVmPropertyChanged;
StateChanged += MainWindow_StateChanged;
if (_vm.IsWidgetVisible)
ShowWidget();
}
// ── Custom title-bar handlers ─────────────────────────────
private void Minimize_Click(object sender, RoutedEventArgs e)
=> WindowState = WindowState.Minimized;
private void MaximizeRestore_Click(object sender, RoutedEventArgs e)
=> WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
private void Close_Click(object sender, RoutedEventArgs e)
=> Close();
private void MainWindow_StateChanged(object? sender, System.EventArgs e)
{
// Update the maximize/restore glyph
if (MaxRestoreBtn is not null)
MaxRestoreBtn.Content = WindowState == WindowState.Maximized ? "❐" : "☐";
}
private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsWidgetVisible))
{
if (_vm.IsWidgetVisible)
ShowWidget();
else
HideWidget();
}
}
private void ShowWidget()
{
if (_widgetWindow is null || !_widgetWindow.IsLoaded)
{
_widgetWindow = new WidgetWindow(_vm);
_widgetWindow.Closed += (_, _) => _vm.IsWidgetVisible = false;
_widgetWindow.Show();
}
}
private void HideWidget()
{
_widgetWindow?.Close();
_widgetWindow = null;
}
private void Window_Closing(object sender, CancelEventArgs e)
{
HideWidget();
_vm.SaveSettings();
_vm.Dispose();
}
}
+77
View File
@@ -0,0 +1,77 @@
using System.IO;
using System.Text.Json;
namespace SpdUp.Models;
/// <summary>
/// Persisted application settings.
/// </summary>
public sealed class AppSettings
{
private static readonly string _filePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SpdUp", "settings.json");
// ── General ────────────────────────────────────────────────────────
public bool MinimizeToTray { get; set; } = true;
public bool StartWithWindows { get; set; } = false;
public bool AutoDetectGames { get; set; } = true;
public bool AutoBoostOnGameLaunch { get; set; } = true;
// ── Widget ─────────────────────────────────────────────────────────
public bool ShowWidget { get; set; } = false;
public bool WidgetCompactMode { get; set; } = true;
public double WidgetLeft { get; set; } = 100;
public double WidgetTop { get; set; } = 100;
public double WidgetOpacity { get; set; } = 0.85;
public int WidgetUpdateIntervalMs { get; set; } = 1000;
public bool WidgetClickThrough { get; set; } = false;
// ── Monitoring ─────────────────────────────────────────────────────
public int MonitoringIntervalMs { get; set; } = 1000;
// ── Network ────────────────────────────────────────────────────────
public string PingTarget { get; set; } = "8.8.8.8";
public string PreferredDns { get; set; } = "1.1.1.1";
// ── Game Library ───────────────────────────────────────────────────
public List<GameEntry> Games { get; set; } = new();
// ── Boost services to stop ─────────────────────────────────────────
public List<string> ServicesToStop { get; set; } = new()
{
"SysMain", "WSearch", "DiagTrack", "TabletInputService", "MapsBroker",
"PcaSvc", "wisvc"
};
// ── Persistence ────────────────────────────────────────────────────
private static readonly JsonSerializerOptions _jsonOpts = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true
};
public static AppSettings Load()
{
try
{
if (File.Exists(_filePath))
{
var json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<AppSettings>(json, _jsonOpts) ?? new AppSettings();
}
}
catch { /* return default */ }
return new AppSettings();
}
public void Save()
{
try
{
var dir = Path.GetDirectoryName(_filePath)!;
Directory.CreateDirectory(dir);
File.WriteAllText(_filePath, JsonSerializer.Serialize(this, _jsonOpts));
}
catch { /* best-effort */ }
}
}
+33
View File
@@ -0,0 +1,33 @@
using System.IO;
namespace SpdUp.Models;
/// <summary>
/// Represents a detected or manually-added game.
/// </summary>
public sealed class GameEntry
{
public string Name { get; set; } = string.Empty;
public string ExecutablePath { get; set; } = string.Empty;
public string ProcessName { get; set; } = string.Empty;
public string LauncherSource { get; set; } = "Manual";
public string? IconPath { get; set; }
public bool AutoBoost { get; set; } = true;
public BoostProfile Profile { get; set; } = new();
/// <summary>Derive the process name from the executable path.</summary>
public static string ProcessNameFromPath(string exePath)
=> Path.GetFileNameWithoutExtension(exePath);
}
/// <summary>
/// Per-game boost profile overrides.
/// </summary>
public sealed class BoostProfile
{
public bool CleanRam { get; set; } = true;
public bool SetHighPriority { get; set; } = true;
public bool ReduceTimerResolution { get; set; } = true;
public bool StopServices { get; set; } = true;
public bool OptimizeNetwork { get; set; } = false;
}
+15
View File
@@ -0,0 +1,15 @@
namespace SpdUp.Models;
/// <summary>
/// Represents a lightweight process view for the Process Manager.
/// </summary>
public sealed class ProcessEntry
{
public int Pid { get; init; }
public string Name { get; init; } = string.Empty;
public string? WindowTitle { get; init; }
public float CpuPercent { get; set; }
public float MemoryMB { get; set; }
public string PriorityClass { get; set; } = "Normal";
public bool IsSystem { get; init; }
}
+13
View File
@@ -0,0 +1,13 @@
namespace SpdUp.Models;
/// <summary>
/// Represents a Windows service relevant to the boost process.
/// </summary>
public sealed class ServiceEntry
{
public string ServiceName { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
public string Status { get; set; } = "Unknown";
public bool WasRunningBeforeBoost { get; set; }
public string Description { get; init; } = string.Empty;
}
+18
View File
@@ -0,0 +1,18 @@
namespace SpdUp.Models;
/// <summary>
/// Snapshot of current system performance metrics.
/// </summary>
public sealed class SystemMetrics
{
public float CpuUsage { get; init; }
public float RamUsageMB { get; init; }
public float RamTotalMB { get; init; }
public float RamUsagePercent => RamTotalMB > 0 ? (RamUsageMB / RamTotalMB) * 100f : 0f;
public float GpuUsage { get; init; }
public float CpuTemperature { get; init; }
public float GpuTemperature { get; init; }
public float NetworkPingMs { get; init; }
public float Fps { get; init; }
public DateTime Timestamp { get; init; } = DateTime.Now;
}
+130
View File
@@ -0,0 +1,130 @@
# ⚡ SpdUp Gaming Booster for Windows
<p align="center">
<img src="https://img.shields.io/badge/.NET-8.0-512BD4?logo=dotnet" alt=".NET 8">
<img src="https://img.shields.io/badge/WPF-Desktop-blue?logo=windows" alt="WPF">
<img src="https://img.shields.io/badge/License-MIT-green" alt="MIT License">
<img src="https://img.shields.io/badge/Platform-Windows%2010%2F11-0078D6?logo=windows" alt="Windows">
<img src="https://img.shields.io/badge/Made%20with-%E2%9D%A4%EF%B8%8F%20by%20Mandy-ff69b4" alt="Made with love by Mandy">
</p>
<p align="center">
<b>A free, open-source Windows gaming optimizer that boosts your PC's performance with a single click.</b>
</p>
---
## 🚀 Features
| Feature | Description |
|---|---|
| **One-Click Boost** | CPU priority, RAM cleanup, timer resolution, GPU power profiles, network tuning, and service management — all in one click |
| **RAM Optimizer** | Deep working-set trimming + Windows Standby List purge via kernel-level calls |
| **Real-Time Monitor** | Live CPU, GPU, RAM, temperatures, fan speeds, and network latency via LibreHardwareMonitor |
| **Network Optimizer** | TCP registry tweaks (Nagle's algorithm, ACK frequency, auto-tuning) for low-latency gaming |
| **Service Manager** | Identifies and safely disables non-essential Windows services during gaming |
| **Game Detection** | Auto-detects 30+ popular games (Steam, Epic, Riot, Blizzard, EA) and applies boost automatically |
| **Shader Cache Cleaner** | Clears DirectX, NVIDIA, AMD, and Intel shader caches to fix stuttering |
| **Process Manager** | View and manage processes with CPU priority and affinity controls |
| **Desktop Widget** | Always-on-top floating overlay with live metrics and quick-boost button |
| **Debug Log** | Built-in live log viewer with filtering, copy, and file logging |
## 📸 Screenshots
<p align="center">
<i>Dark gaming UI with custom window chrome, sidebar navigation, and live system metrics</i>
</p>
> Add your screenshots to the `website/screenshots/` folder and reference them here.
## 🏗️ Tech Stack
- **.NET 8** — Latest runtime, self-contained deployment (no framework install needed)
- **WPF** — Windows Presentation Foundation with full MVVM architecture
- **LibreHardwareMonitor** — Accurate CPU/GPU temps, fan speeds, and clock readings
- **P/Invoke** — Kernel-level calls for RAM cleanup, timer resolution, and privilege elevation
- **Custom Dark Theme** — Styled scrollbars, buttons, data grids, and window chrome
- **Inno Setup** — Single-file installer with LZMA2 compression
## 📦 Installation
### Option 1: Installer (Recommended)
1. Download `SpdUp_Setup_1.0.0.exe` from [Releases](../../releases)
2. Run the installer
3. Launch SpdUp from the Start Menu or Desktop
### Option 2: Build from Source
```bash
git clone https://github.com/immissmandy/SpdUp.git
cd SpdUp
dotnet build SpdUp.csproj -c Release
```
### Build the Installer
```powershell
# Publish self-contained
.\build-installer.ps1
# Publish + compile Inno Setup installer
.\build-installer.ps1 -Installer
```
> Requires [Inno Setup 6](https://jrsoftware.org/isdl.php) for the `-Installer` flag.
## 🧪 Tests
```bash
dotnet test SpdUp.Tests
```
**84 unit tests** covering:
- Core infrastructure (ObservableObject, RelayCommand, Logger)
- All model classes and computed properties
- WPF value converters (7 converters, 13 tests with Theory data)
- Service layer (Network, GameDetection, Optimization, ServiceManagement)
- ViewModel logic (DebugLogViewModel with STA thread support)
## 📁 Project Structure
```
SpdUp/
├── Core/ # ObservableObject, RelayCommand, Logger, NativeMethods (P/Invoke)
├── Models/ # AppSettings, GameEntry, ProcessEntry, ServiceEntry, SystemMetrics
├── Services/ # Optimization, RAM, Network, GameDetection, Monitoring, ShaderCache
├── ViewModels/ # MVVM ViewModels for each page
├── Views/ # WPF XAML Views + code-behind
├── Themes/ # DarkTheme.xaml (full custom dark gaming theme)
├── Converters/ # WPF value converters
├── Installer/ # Inno Setup script (SpdUpSetup.iss)
├── SpdUp.Tests/ # xUnit test project (84 tests)
├── website/ # Landing page (index.html)
├── build-installer.ps1 # Build + package script
└── SpdUp.csproj # .NET 8 WPF project
```
## ⚙️ How It Works
1. **Install & Launch** — SpdUp runs with administrator privileges for deep system access
2. **Hit Boost** — Optimizes CPU, RAM, GPU, network, timer resolution, and disables unnecessary services
3. **Game On** — Play with maximum performance; click Restore when done to revert all changes
All changes are **fully reversible**. SpdUp saves previous states and restores them on deactivation.
## 💻 System Requirements
| Requirement | Minimum |
|---|---|
| OS | Windows 10/11 (64-bit, version 1809+) |
| RAM | 4 GB (8 GB recommended) |
| Disk Space | ~200 MB |
| Privileges | Administrator (for RAM cleanup, service control, timer resolution) |
## 🏳️‍⚧️ Made with ❤️ by Mandy
<p align="center">
<img src="https://img.shields.io/badge/%F0%9F%8F%B3%EF%B8%8F%E2%80%8D%E2%9A%A7%EF%B8%8F-Trans%20Pride-5bcefa?labelColor=f5a9b8" alt="Trans Pride">
</p>
## 📄 License
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
+157
View File
@@ -0,0 +1,157 @@
using System.Diagnostics;
using SpdUp.Core;
using SpdUp.Models;
namespace SpdUp.Services;
/// <summary>
/// Detects running games and known launchers.
/// </summary>
public sealed class GameDetectionService : IDisposable
{
private readonly Timer _pollTimer;
private readonly AppSettings _settings;
private readonly HashSet<string> _knownGameProcesses = new(StringComparer.OrdinalIgnoreCase);
private string? _detectedGameProcess;
public event Action<string>? GameStarted;
public event Action<string>? GameStopped;
/// <summary>
/// Well-known launcher processes that indicate a game may launch.
/// </summary>
private static readonly HashSet<string> LauncherProcesses = new(StringComparer.OrdinalIgnoreCase)
{
"steam", "steamwebhelper",
"EpicGamesLauncher",
"Battle.net", "Agent"
};
/// <summary>
/// Heuristic: processes that look like games (high-memory GPU-using apps not in system dirs).
/// </summary>
private static readonly HashSet<string> ExcludedProcessNames = new(StringComparer.OrdinalIgnoreCase)
{
"explorer", "svchost", "System", "Idle", "csrss", "smss", "lsass",
"services", "wininit", "winlogon", "dwm", "taskhostw", "RuntimeBroker",
"ShellExperienceHost", "SearchHost", "SearchIndexer", "fontdrvhost",
"conhost", "dllhost", "sihost", "ctfmon", "TextInputHost",
"SecurityHealthSystray", "SecurityHealthService",
"devenv", "Code", "chrome", "msedge", "firefox", "Discord",
"Spotify", "slack", "Teams"
};
public GameDetectionService(AppSettings settings)
{
_settings = settings;
// Build known game process name set from settings.
foreach (var g in _settings.Games)
{
if (!string.IsNullOrWhiteSpace(g.ProcessName))
_knownGameProcesses.Add(g.ProcessName);
}
_pollTimer = new Timer(PollForGames, null, Timeout.Infinite, Timeout.Infinite);
}
public string? CurrentGameProcess => _detectedGameProcess;
public void Start(int intervalMs = 3000)
{
_pollTimer.Change(0, intervalMs);
Logger.Info("Game detection started.");
}
public void Stop()
{
_pollTimer.Change(Timeout.Infinite, Timeout.Infinite);
Logger.Info("Game detection stopped.");
}
public void AddGameProcess(string processName)
{
_knownGameProcesses.Add(processName);
}
private void PollForGames(object? state)
{
try
{
var running = Process.GetProcesses();
string? foundGame = null;
// First: check known game processes
foreach (var proc in running)
{
try
{
if (_knownGameProcesses.Contains(proc.ProcessName))
{
foundGame = proc.ProcessName;
break;
}
}
catch { /* access denied */ }
finally { proc.Dispose(); }
}
// Second: heuristic detect GPU-heavy, non-system, non-launcher processes
if (foundGame is null)
{
foreach (var proc in Process.GetProcesses())
{
try
{
if (ExcludedProcessNames.Contains(proc.ProcessName)) continue;
if (LauncherProcesses.Contains(proc.ProcessName)) continue;
if (proc.Id <= 4) continue;
// Heuristic: >300 MB working set and has a main window => likely a game
if (proc.WorkingSet64 > 300_000_000 && proc.MainWindowHandle != IntPtr.Zero)
{
// Check it's not from system folders
try
{
var path = proc.MainModule?.FileName;
if (path is not null &&
!path.StartsWith(Environment.GetFolderPath(Environment.SpecialFolder.Windows), StringComparison.OrdinalIgnoreCase))
{
foundGame = proc.ProcessName;
break;
}
}
catch { /* access denied */ }
}
}
catch { }
finally { proc.Dispose(); }
}
}
// State machine: detect start / stop
if (foundGame is not null && _detectedGameProcess is null)
{
_detectedGameProcess = foundGame;
Logger.Info($"Game detected: {foundGame}");
GameStarted?.Invoke(foundGame);
}
else if (foundGame is null && _detectedGameProcess is not null)
{
var prev = _detectedGameProcess;
_detectedGameProcess = null;
Logger.Info($"Game stopped: {prev}");
GameStopped?.Invoke(prev);
}
}
catch (Exception ex)
{
Logger.Error("Game detection poll error", ex);
}
}
public void Dispose()
{
_pollTimer.Dispose();
}
}
+157
View File
@@ -0,0 +1,157 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using LibreHardwareMonitor.Hardware;
using SpdUp.Core;
using SpdUp.Models;
namespace SpdUp.Services;
/// <summary>
/// Live system monitoring using performance counters + LibreHardwareMonitor.
/// Designed for low overhead one shared timer drives all consumers.
/// </summary>
public sealed class MonitoringService : IDisposable
{
private readonly Timer _timer;
private readonly Computer _computer;
private readonly PerformanceCounter _cpuCounter;
private readonly NetworkService _networkService = new();
private readonly string _pingTarget;
private SystemMetrics _latest = new();
private bool _disposed;
public event Action<SystemMetrics>? MetricsUpdated;
public SystemMetrics Latest => _latest;
public MonitoringService(string pingTarget = "8.8.8.8")
{
_pingTarget = pingTarget;
// CPU % counter
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total", readOnly: true);
_cpuCounter.NextValue(); // prime the counter
// LibreHardwareMonitor for temps + GPU
_computer = new Computer
{
IsCpuEnabled = true,
IsGpuEnabled = true,
IsMemoryEnabled = true,
};
try
{
_computer.Open();
}
catch (Exception ex)
{
Logger.Warn($"LibreHardwareMonitor init warning: {ex.Message}");
}
_timer = new Timer(Tick, null, Timeout.Infinite, Timeout.Infinite);
}
public void Start(int intervalMs = 1000)
{
_timer.Change(0, intervalMs);
Logger.Info($"Monitoring started ({intervalMs} ms interval).");
}
public void Stop()
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
Logger.Info("Monitoring stopped.");
}
public void SetInterval(int intervalMs)
{
_timer.Change(0, intervalMs);
}
private async void Tick(object? state)
{
if (_disposed) return;
try
{
float cpuUsage = _cpuCounter.NextValue();
// Memory
var mem = new NativeMethods.MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<NativeMethods.MEMORYSTATUSEX>() };
NativeMethods.GlobalMemoryStatusEx(ref mem);
float ramTotal = mem.ullTotalPhys / 1048576f;
float ramUsed = ramTotal - (mem.ullAvailPhys / 1048576f);
// Hardware sensors
float cpuTemp = 0, gpuUsage = 0, gpuTemp = 0;
try
{
foreach (var hw in _computer.Hardware)
{
hw.Update();
foreach (var sensor in hw.Sensors)
{
if (hw.HardwareType == HardwareType.Cpu &&
sensor.SensorType == SensorType.Temperature &&
sensor.Name.Contains("Package", StringComparison.OrdinalIgnoreCase))
{
cpuTemp = sensor.Value ?? 0;
}
if ((hw.HardwareType == HardwareType.GpuNvidia || hw.HardwareType == HardwareType.GpuAmd || hw.HardwareType == HardwareType.GpuIntel))
{
if (sensor.SensorType == SensorType.Load && sensor.Name.Contains("Core", StringComparison.OrdinalIgnoreCase))
gpuUsage = sensor.Value ?? 0;
if (sensor.SensorType == SensorType.Temperature && sensor.Name.Contains("Core", StringComparison.OrdinalIgnoreCase))
gpuTemp = sensor.Value ?? 0;
}
}
foreach (var sub in hw.SubHardware)
{
sub.Update();
foreach (var sensor in sub.Sensors)
{
if (sensor.SensorType == SensorType.Temperature &&
sensor.Name.Contains("Package", StringComparison.OrdinalIgnoreCase))
cpuTemp = sensor.Value ?? cpuTemp;
}
}
}
}
catch { /* sensor read failure continue with zeros */ }
// Ping
long pingMs = await NetworkService.PingAsync(_pingTarget, 1500);
_latest = new SystemMetrics
{
CpuUsage = cpuUsage,
RamUsageMB = ramUsed,
RamTotalMB = ramTotal,
GpuUsage = gpuUsage,
CpuTemperature = cpuTemp,
GpuTemperature = gpuTemp,
NetworkPingMs = pingMs,
Fps = 0, // FPS hook not implemented without injection
};
MetricsUpdated?.Invoke(_latest);
}
catch (Exception ex)
{
Logger.Error("Monitoring tick error", ex);
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_timer.Dispose();
_cpuCounter.Dispose();
try { _computer.Close(); } catch { }
}
}
+107
View File
@@ -0,0 +1,107 @@
using System.Net.NetworkInformation;
using Microsoft.Win32;
using SpdUp.Core;
namespace SpdUp.Services;
/// <summary>
/// Network tweaks for gaming: DNS, TCP settings, latency checks.
/// All changes are reversible via Restore methods.
/// </summary>
public sealed class NetworkService
{
private readonly Dictionary<string, object?> _previousRegValues = new();
/// <summary>
/// Measure round-trip ping to a host.
/// </summary>
public static async Task<long> PingAsync(string host = "8.8.8.8", int timeoutMs = 2000)
{
try
{
using var ping = new Ping();
var reply = await ping.SendPingAsync(host, timeoutMs);
return reply.Status == IPStatus.Success ? reply.RoundtripTime : -1;
}
catch
{
return -1;
}
}
/// <summary>
/// Apply TCP gaming optimisations via the registry (Nagle, TCP ack frequency, etc.).
/// </summary>
public void ApplyTcpOptimizations()
{
try
{
const string tcpKey = @"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters";
using var key = Registry.LocalMachine.OpenSubKey(tcpKey, writable: true);
if (key is null) return;
SetRegistryValueWithBackup(key, tcpKey, "TcpAckFrequency", 1, RegistryValueKind.DWord);
SetRegistryValueWithBackup(key, tcpKey, "TCPNoDelay", 1, RegistryValueKind.DWord);
SetRegistryValueWithBackup(key, tcpKey, "TcpDelAckTicks", 0, RegistryValueKind.DWord);
// Also per-interface
const string ifacesKey = @"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces";
using var ifacesRoot = Registry.LocalMachine.OpenSubKey(ifacesKey);
if (ifacesRoot is not null)
{
foreach (var subKeyName in ifacesRoot.GetSubKeyNames())
{
using var ifaceKey = ifacesRoot.OpenSubKey(subKeyName, writable: true);
if (ifaceKey is null) continue;
var fullPath = $"{ifacesKey}\\{subKeyName}";
SetRegistryValueWithBackup(ifaceKey, fullPath, "TcpAckFrequency", 1, RegistryValueKind.DWord);
SetRegistryValueWithBackup(ifaceKey, fullPath, "TCPNoDelay", 1, RegistryValueKind.DWord);
}
}
Logger.Info("TCP gaming optimizations applied.");
}
catch (Exception ex)
{
Logger.Error("Failed to apply TCP optimizations", ex);
}
}
/// <summary>
/// Restore all TCP settings to their previous values.
/// </summary>
public void RestoreTcpSettings()
{
try
{
foreach (var kvp in _previousRegValues)
{
var parts = kvp.Key.Split('|');
if (parts.Length != 2) continue;
using var key = Registry.LocalMachine.OpenSubKey(parts[0], writable: true);
if (key is null) continue;
if (kvp.Value is null)
key.DeleteValue(parts[1], throwOnMissingValue: false);
else
key.SetValue(parts[1], kvp.Value);
}
_previousRegValues.Clear();
Logger.Info("TCP settings restored.");
}
catch (Exception ex)
{
Logger.Error("Failed to restore TCP settings", ex);
}
}
private void SetRegistryValueWithBackup(RegistryKey key, string keyPath, string valueName, object newValue, RegistryValueKind kind)
{
var backupKey = $"{keyPath}|{valueName}";
if (!_previousRegValues.ContainsKey(backupKey))
_previousRegValues[backupKey] = key.GetValue(valueName); // null if not present
key.SetValue(valueName, newValue, kind);
}
}
+208
View File
@@ -0,0 +1,208 @@
using System.Diagnostics;
using System.ServiceProcess;
using SpdUp.Core;
using SpdUp.Models;
namespace SpdUp.Services;
/// <summary>
/// Manages the full boost lifecycle: activate → monitor → restore.
/// </summary>
public sealed class OptimizationService
{
private readonly RamOptimizationService _ramService = new();
private readonly AppSettings _settings;
private readonly List<string> _stoppedServices = new();
private uint _previousTimerResolution;
private bool _timerResolutionChanged;
private string? _boostedProcessName;
private bool _isBoosted;
public bool IsBoosted => _isBoosted;
public string? BoostedProcessName => _boostedProcessName;
public event Action<string>? BoostActivated;
public event Action? BoostDeactivated;
public OptimizationService(AppSettings settings)
{
_settings = settings;
}
/// <summary>
/// Activate all boost optimizations. Thread-safe.
/// </summary>
public async Task<BoostResult> ActivateBoostAsync(string? gameProcessName = null)
{
if (_isBoosted) return new BoostResult(false, "Boost is already active.");
var messages = new List<string>();
try
{
// 1. Enable required privileges
NativeMethods.EnablePrivilege(NativeMethods.SE_DEBUG_NAME);
NativeMethods.EnablePrivilege(NativeMethods.SE_INC_BASE_PRIORITY_NAME);
NativeMethods.EnablePrivilege(NativeMethods.SE_PROF_SINGLE_PROCESS_NAME);
// 2. RAM Cleanup
await Task.Run(() =>
{
var result = _ramService.FullCleanup();
messages.Add($"RAM: trimmed {result.TrimmedCount} processes, standby purge {(result.StandbyPurged > 0 ? "OK" : "skipped")}.");
});
// 3. Stop non-critical services
await Task.Run(() =>
{
foreach (var svcName in _settings.ServicesToStop)
{
try
{
using var sc = new ServiceController(svcName);
if (sc.Status == ServiceControllerStatus.Running)
{
sc.Stop();
_stoppedServices.Add(svcName);
messages.Add($"Stopped service: {sc.DisplayName}");
}
}
catch (Exception ex)
{
Logger.Warn($"Could not stop {svcName}: {ex.Message}");
}
}
});
// 4. Set game to HIGH priority
if (!string.IsNullOrEmpty(gameProcessName))
{
_boostedProcessName = gameProcessName;
await Task.Run(() => SetProcessPriority(gameProcessName, ProcessPriorityClass.High));
messages.Add($"Set {gameProcessName} to HIGH priority.");
}
// 5. Reduce timer resolution (0.5 ms)
await Task.Run(() =>
{
NativeMethods.NtQueryTimerResolution(out _, out _, out _previousTimerResolution);
int status = NativeMethods.NtSetTimerResolution(5000, true, out _);
_timerResolutionChanged = status == 0;
if (_timerResolutionChanged)
messages.Add("Timer resolution set to 0.5 ms.");
});
_isBoosted = true;
BoostActivated?.Invoke(string.Join("\n", messages));
Logger.Info("Boost activated: " + string.Join(" | ", messages));
return new BoostResult(true, string.Join("\n", messages));
}
catch (Exception ex)
{
Logger.Error("Boost activation failed", ex);
return new BoostResult(false, $"Error: {ex.Message}");
}
}
/// <summary>
/// Restore all changes made during boost.
/// </summary>
public async Task DeactivateBoostAsync()
{
if (!_isBoosted) return;
try
{
// 1. Restore timer resolution
if (_timerResolutionChanged)
{
NativeMethods.NtSetTimerResolution(_previousTimerResolution, true, out _);
_timerResolutionChanged = false;
Logger.Info("Timer resolution restored.");
}
// 2. Restart stopped services
await Task.Run(() =>
{
foreach (var svcName in _stoppedServices)
{
try
{
using var sc = new ServiceController(svcName);
if (sc.Status == ServiceControllerStatus.Stopped)
{
sc.Start();
Logger.Info($"Restarted service: {sc.DisplayName}");
}
}
catch (Exception ex)
{
Logger.Warn($"Could not restart {svcName}: {ex.Message}");
}
}
_stoppedServices.Clear();
});
// 3. Restore process priority
if (_boostedProcessName is not null)
{
await Task.Run(() => SetProcessPriority(_boostedProcessName, ProcessPriorityClass.Normal));
_boostedProcessName = null;
}
_isBoosted = false;
BoostDeactivated?.Invoke();
Logger.Info("Boost deactivated all settings restored.");
}
catch (Exception ex)
{
Logger.Error("Boost deactivation error", ex);
}
}
/// <summary>
/// Set priority class for a process by name.
/// </summary>
public static void SetProcessPriority(string processName, ProcessPriorityClass priority)
{
foreach (var proc in Process.GetProcessesByName(processName))
{
try
{
proc.PriorityClass = priority;
Logger.Info($"Set {processName} (PID {proc.Id}) priority to {priority}.");
}
catch (Exception ex)
{
Logger.Warn($"Cannot set priority for {processName}: {ex.Message}");
}
finally
{
proc.Dispose();
}
}
}
/// <summary>
/// Kill a process by PID.
/// </summary>
public static bool KillProcess(int pid)
{
try
{
using var proc = Process.GetProcessById(pid);
proc.Kill(entireProcessTree: true);
Logger.Info($"Killed process {proc.ProcessName} (PID {pid}).");
return true;
}
catch (Exception ex)
{
Logger.Error($"Failed to kill PID {pid}", ex);
return false;
}
}
}
public record BoostResult(bool Success, string Message);
+111
View File
@@ -0,0 +1,111 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using SpdUp.Core;
namespace SpdUp.Services;
/// <summary>
/// Performs real memory optimizations: working-set trimming + standby-list purge.
/// Requires administrator privileges.
/// </summary>
public sealed class RamOptimizationService
{
/// <summary>
/// Trim working sets of all accessible processes pushes unused pages to the standby list.
/// </summary>
public RamCleanupResult TrimWorkingSets()
{
int trimmed = 0, failed = 0;
var ownPid = Environment.ProcessId;
foreach (var proc in Process.GetProcesses())
{
try
{
if (proc.Id == ownPid || proc.Id == 0 || proc.Id == 4) continue;
var hProcess = NativeMethods.OpenProcess(
NativeMethods.PROCESS_SET_INFORMATION | NativeMethods.PROCESS_QUERY_INFORMATION,
false, proc.Id);
if (hProcess == IntPtr.Zero) { failed++; continue; }
try
{
if (NativeMethods.EmptyWorkingSet(hProcess))
trimmed++;
else
failed++;
}
finally
{
NativeMethods.CloseHandle(hProcess);
}
}
catch
{
failed++;
}
finally
{
proc.Dispose();
}
}
Logger.Info($"Working-set trim complete: {trimmed} trimmed, {failed} skipped.");
return new RamCleanupResult(trimmed, failed, 0);
}
/// <summary>
/// Purge the standby memory list. Requires SeProfileSingleProcessPrivilege.
/// </summary>
public bool PurgeStandbyList()
{
try
{
NativeMethods.EnablePrivilege(NativeMethods.SE_PROF_SINGLE_PROCESS_NAME);
int command = (int)NativeMethods.SYSTEM_MEMORY_LIST_COMMAND.MemoryPurgeStandbyList;
int status = NativeMethods.NtSetSystemInformation(
NativeMethods.SystemMemoryListInformation,
ref command,
Marshal.SizeOf<int>());
if (status == 0)
{
Logger.Info("Standby list purged successfully.");
return true;
}
Logger.Warn($"NtSetSystemInformation returned NTSTATUS 0x{status:X8}");
return false;
}
catch (Exception ex)
{
Logger.Error("Failed to purge standby list", ex);
return false;
}
}
/// <summary>
/// Full RAM optimisation: trim working sets + purge standby.
/// </summary>
public RamCleanupResult FullCleanup()
{
var result = TrimWorkingSets();
var purged = PurgeStandbyList();
return result with { StandbyPurged = purged ? 1 : 0 };
}
/// <summary>
/// Returns current memory statistics.
/// </summary>
public static (ulong totalMB, ulong availMB, uint loadPercent) GetMemoryStatus()
{
var mem = new NativeMethods.MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<NativeMethods.MEMORYSTATUSEX>() };
NativeMethods.GlobalMemoryStatusEx(ref mem);
return (mem.ullTotalPhys / 1048576, mem.ullAvailPhys / 1048576, mem.dwMemoryLoad);
}
}
public record struct RamCleanupResult(int TrimmedCount, int FailedCount, int StandbyPurged);
+96
View File
@@ -0,0 +1,96 @@
using System.Diagnostics;
using System.ServiceProcess;
using SpdUp.Core;
using SpdUp.Models;
namespace SpdUp.Services;
/// <summary>
/// Service management: list, stop, start Windows services.
/// </summary>
public static class ServiceManagementService
{
/// <summary>
/// Non-critical services commonly stopped for gaming.
/// </summary>
public static readonly Dictionary<string, string> KnownNonCriticalServices = new()
{
["SysMain"] = "Superfetch / SysMain pre-loads apps into RAM",
["WSearch"] = "Windows Search indexing",
["DiagTrack"] = "Connected User Experiences and Telemetry",
["TabletInputService"] = "Touch Keyboard and Handwriting Panel",
["MapsBroker"] = "Downloaded Maps Manager",
["PcaSvc"] = "Program Compatibility Assistant",
["wisvc"] = "Windows Insider Service",
["WbioSrvc"] = "Windows Biometric Service",
["Fax"] = "Fax service",
["XblAuthManager"] = "Xbox Live Auth Manager",
["XblGameSave"] = "Xbox Live Game Save",
["XboxNetApiSvc"] = "Xbox Live Networking Service",
};
public static List<ServiceEntry> GetServiceList(IEnumerable<string> serviceNames)
{
var entries = new List<ServiceEntry>();
foreach (var name in serviceNames)
{
try
{
using var sc = new ServiceController(name);
entries.Add(new ServiceEntry
{
ServiceName = sc.ServiceName,
DisplayName = sc.DisplayName,
Status = sc.Status.ToString(),
Description = KnownNonCriticalServices.GetValueOrDefault(name, "")
});
}
catch
{
entries.Add(new ServiceEntry
{
ServiceName = name,
DisplayName = name,
Status = "NotFound"
});
}
}
return entries;
}
public static bool StopService(string serviceName)
{
try
{
using var sc = new ServiceController(serviceName);
if (sc.Status == ServiceControllerStatus.Running)
{
sc.Stop();
sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(15));
Logger.Info($"Stopped service: {serviceName}");
return true;
}
}
catch (Exception ex) { Logger.Error($"Failed to stop {serviceName}", ex); }
return false;
}
public static bool StartService(string serviceName)
{
try
{
using var sc = new ServiceController(serviceName);
if (sc.Status == ServiceControllerStatus.Stopped)
{
sc.Start();
sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(15));
Logger.Info($"Started service: {serviceName}");
return true;
}
}
catch (Exception ex) { Logger.Error($"Failed to start {serviceName}", ex); }
return false;
}
}
+110
View File
@@ -0,0 +1,110 @@
using System.IO;
using SpdUp.Core;
namespace SpdUp.Services;
/// <summary>
/// Cleans DirectX shader cache, NVIDIA shader cache, and temp files
/// to reduce micro-stuttering in games.
/// </summary>
public static class ShaderCacheCleanerService
{
public static ShaderCleanResult CleanAll()
{
long totalBytes = 0;
int totalFiles = 0;
totalBytes += CleanDirectory(GetDirectXShaderCachePath(), ref totalFiles);
totalBytes += CleanDirectory(GetNvidiaShaderCachePath(), ref totalFiles);
totalBytes += CleanDirectory(GetWindowsTempPath(), ref totalFiles);
totalBytes += CleanDirectory(GetLocalTempPath(), ref totalFiles);
Logger.Info($"Shader cache clean: {totalFiles} files, {totalBytes / 1048576.0:F1} MB freed.");
return new ShaderCleanResult(totalFiles, totalBytes);
}
public static ShaderCleanResult CleanDirectXCache()
{
long totalBytes = 0;
int totalFiles = 0;
totalBytes += CleanDirectory(GetDirectXShaderCachePath(), ref totalFiles);
return new ShaderCleanResult(totalFiles, totalBytes);
}
public static ShaderCleanResult CleanNvidiaCache()
{
long totalBytes = 0;
int totalFiles = 0;
totalBytes += CleanDirectory(GetNvidiaShaderCachePath(), ref totalFiles);
return new ShaderCleanResult(totalFiles, totalBytes);
}
public static ShaderCleanResult CleanTempFiles()
{
long totalBytes = 0;
int totalFiles = 0;
totalBytes += CleanDirectory(GetWindowsTempPath(), ref totalFiles);
totalBytes += CleanDirectory(GetLocalTempPath(), ref totalFiles);
return new ShaderCleanResult(totalFiles, totalBytes);
}
// ── Paths ──────────────────────────────────────────────────────────
private static string GetDirectXShaderCachePath()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"D3DSCache");
private static string GetNvidiaShaderCachePath()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"NVIDIA", "DXCache");
private static string GetWindowsTempPath()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Temp");
private static string GetLocalTempPath()
=> Path.GetTempPath();
// ── Helpers ────────────────────────────────────────────────────────
private static long CleanDirectory(string path, ref int fileCount)
{
if (!Directory.Exists(path)) return 0;
long bytes = 0;
try
{
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
try
{
var fi = new FileInfo(file);
bytes += fi.Length;
fi.Delete();
fileCount++;
}
catch { /* locked / in use skip */ }
}
// Remove empty dirs
foreach (var dir in Directory.EnumerateDirectories(path, "*", SearchOption.AllDirectories)
.OrderByDescending(d => d.Length))
{
try
{
if (!Directory.EnumerateFileSystemEntries(dir).Any())
Directory.Delete(dir);
}
catch { }
}
}
catch (Exception ex)
{
Logger.Warn($"Error cleaning {path}: {ex.Message}");
}
return bytes;
}
}
public record struct ShaderCleanResult(int FilesDeleted, long BytesFreed)
{
public double MBFreed => BytesFreed / 1048576.0;
}
+175
View File
@@ -0,0 +1,175 @@
using System.Globalization;
using System.Windows;
using System.Windows.Media;
using SpdUp.Converters;
namespace SpdUp.Tests;
/// <summary>
/// Tests for WPF value converters.
/// </summary>
public class ConverterTests
{
private static readonly CultureInfo Culture = CultureInfo.InvariantCulture;
// ── BoolToVisibilityConverter ─────────────────────────────
[Fact]
public void BoolToVisibility_True_ReturnsVisible()
{
var conv = new BoolToVisibilityConverter();
var result = conv.Convert(true, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Visible, result);
}
[Fact]
public void BoolToVisibility_False_ReturnsCollapsed()
{
var conv = new BoolToVisibilityConverter();
var result = conv.Convert(false, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Collapsed, result);
}
[Fact]
public void BoolToVisibility_ConvertBack_Visible_ReturnsTrue()
{
var conv = new BoolToVisibilityConverter();
var result = conv.ConvertBack(Visibility.Visible, typeof(bool), null!, Culture);
Assert.Equal(true, result);
}
[Fact]
public void BoolToVisibility_ConvertBack_Collapsed_ReturnsFalse()
{
var conv = new BoolToVisibilityConverter();
var result = conv.ConvertBack(Visibility.Collapsed, typeof(bool), null!, Culture);
Assert.Equal(false, result);
}
// ── InverseBoolToVisibilityConverter ──────────────────────
[Fact]
public void InverseBoolToVisibility_True_ReturnsCollapsed()
{
var conv = new InverseBoolToVisibilityConverter();
var result = conv.Convert(true, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Collapsed, result);
}
[Fact]
public void InverseBoolToVisibility_False_ReturnsVisible()
{
var conv = new InverseBoolToVisibilityConverter();
var result = conv.Convert(false, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Visible, result);
}
// ── PercentToColorConverter ──────────────────────────────
[Theory]
[InlineData(95f, 255, 69, 58)] // red for > 90
[InlineData(91f, 255, 69, 58)] // red at boundary
[InlineData(80f, 255, 159, 10)] // orange for > 70
[InlineData(71f, 255, 159, 10)] // orange at boundary
[InlineData(50f, 48, 209, 88)] // green for <= 70
[InlineData(0f, 48, 209, 88)] // green at zero
public void PercentToColor_ReturnsCorrectColor(float pct, byte r, byte g, byte b)
{
var conv = new PercentToColorConverter();
var result = conv.Convert(pct, typeof(Brush), null!, Culture) as SolidColorBrush;
Assert.NotNull(result);
Assert.Equal(r, result.Color.R);
Assert.Equal(g, result.Color.G);
Assert.Equal(b, result.Color.B);
}
[Fact]
public void PercentToColor_ConvertBack_Throws()
{
var conv = new PercentToColorConverter();
Assert.Throws<NotSupportedException>(() =>
conv.ConvertBack(null!, typeof(float), null!, Culture));
}
// ── NullToVisibilityConverter ────────────────────────────
[Fact]
public void NullToVisibility_Null_ReturnsCollapsed()
{
var conv = new NullToVisibilityConverter();
var result = conv.Convert(null!, typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Collapsed, result);
}
[Fact]
public void NullToVisibility_NotNull_ReturnsVisible()
{
var conv = new NullToVisibilityConverter();
var result = conv.Convert("something", typeof(Visibility), null!, Culture);
Assert.Equal(Visibility.Visible, result);
}
// ── FloatFormatConverter ─────────────────────────────────
[Fact]
public void FloatFormat_DefaultFormat_OneDecimal()
{
var conv = new FloatFormatConverter();
var result = (string)conv.Convert(42.567f, typeof(string), null!, CultureInfo.InvariantCulture);
// Converter uses current culture internally, so parse back to float
Assert.True(float.TryParse(result, System.Globalization.NumberStyles.Float, CultureInfo.CurrentCulture, out var parsed));
Assert.Equal(42.6f, parsed, 0.05f);
}
[Fact]
public void FloatFormat_CustomFormat()
{
var conv = new FloatFormatConverter();
var result = (string)conv.Convert(42.567f, typeof(string), "F2", CultureInfo.InvariantCulture);
Assert.True(float.TryParse(result, System.Globalization.NumberStyles.Float, CultureInfo.CurrentCulture, out var parsed));
Assert.Equal(42.57f, parsed, 0.005f);
}
// ── BoostButtonTextConverter ─────────────────────────────
[Fact]
public void BoostButtonText_True_ReturnsStopText()
{
var conv = new BoostButtonTextConverter();
var result = conv.Convert(true, typeof(string), null!, Culture);
Assert.Contains("STOP", result as string ?? "");
}
[Fact]
public void BoostButtonText_False_ReturnsBoostText()
{
var conv = new BoostButtonTextConverter();
var result = conv.Convert(false, typeof(string), null!, Culture);
Assert.Contains("BOOST", result as string ?? "");
}
// ── BoostButtonColorConverter ────────────────────────────
[Fact]
public void BoostButtonColor_True_ReturnsRed()
{
var conv = new BoostButtonColorConverter();
var result = conv.Convert(true, typeof(Brush), null!, Culture) as SolidColorBrush;
Assert.NotNull(result);
Assert.Equal(255, result.Color.R);
Assert.Equal(69, result.Color.G);
}
[Fact]
public void BoostButtonColor_False_ReturnsGreen()
{
var conv = new BoostButtonColorConverter();
var result = conv.Convert(false, typeof(Brush), null!, Culture) as SolidColorBrush;
Assert.NotNull(result);
Assert.Equal(48, result.Color.R);
Assert.Equal(209, result.Color.G);
}
}
+1
View File
@@ -0,0 +1 @@
global using Xunit;
+117
View File
@@ -0,0 +1,117 @@
using SpdUp.Core;
namespace SpdUp.Tests;
/// <summary>
/// Tests for the Logger class.
/// </summary>
public class LoggerTests
{
[Fact]
public void Info_FiresLogWrittenEvent()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Info("test info message");
Assert.NotNull(received);
Assert.Contains("[INFO]", received);
Assert.Contains("test info message", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void Warn_FiresLogWrittenEventWithWarnLevel()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Warn("test warning");
Assert.NotNull(received);
Assert.Contains("[WARN]", received);
Assert.Contains("test warning", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void Error_IncludesExceptionDetails()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Error("something failed", new InvalidOperationException("bad state"));
Assert.NotNull(received);
Assert.Contains("[ERROR]", received);
Assert.Contains("something failed", received);
Assert.Contains("InvalidOperationException", received);
Assert.Contains("bad state", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void Error_WithoutException_DoesNotContainPipe()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Error("simple error");
Assert.NotNull(received);
Assert.Contains("[ERROR]", received);
Assert.Contains("simple error", received);
Assert.DoesNotContain("|", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
[Fact]
public void LogWritten_IncludesTimestamp()
{
string? received = null;
Logger.LogWritten += handler;
try
{
Logger.Info("timestamp check");
Assert.NotNull(received);
// Format: [yyyy-MM-dd HH:mm:ss.fff]
Assert.Matches(@"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]", received);
}
finally
{
Logger.LogWritten -= handler;
}
void handler(string line) => received = line;
}
}
+224
View File
@@ -0,0 +1,224 @@
using SpdUp.Models;
namespace SpdUp.Tests;
/// <summary>
/// Tests for the model classes.
/// </summary>
public class ModelTests
{
// ── SystemMetrics ────────────────────────────────────────
[Fact]
public void SystemMetrics_RamUsagePercent_CalculatesCorrectly()
{
var m = new SystemMetrics { RamUsageMB = 8000, RamTotalMB = 16000 };
Assert.Equal(50f, m.RamUsagePercent);
}
[Fact]
public void SystemMetrics_RamUsagePercent_ZeroTotal_ReturnsZero()
{
var m = new SystemMetrics { RamUsageMB = 100, RamTotalMB = 0 };
Assert.Equal(0f, m.RamUsagePercent);
}
[Fact]
public void SystemMetrics_RamUsagePercent_FullRam()
{
var m = new SystemMetrics { RamUsageMB = 16000, RamTotalMB = 16000 };
Assert.Equal(100f, m.RamUsagePercent);
}
[Fact]
public void SystemMetrics_DefaultTimestamp_IsNow()
{
var before = DateTime.Now.AddSeconds(-1);
var m = new SystemMetrics();
var after = DateTime.Now.AddSeconds(1);
Assert.InRange(m.Timestamp, before, after);
}
// ── GameEntry ────────────────────────────────────────────
[Fact]
public void GameEntry_ProcessNameFromPath_ExtractsFileName()
{
var result = GameEntry.ProcessNameFromPath(@"C:\Games\Cyberpunk2077\bin\Cyberpunk2077.exe");
Assert.Equal("Cyberpunk2077", result);
}
[Fact]
public void GameEntry_ProcessNameFromPath_HandlesForwardSlashes()
{
var result = GameEntry.ProcessNameFromPath("C:/Games/Game.exe");
Assert.Equal("Game", result);
}
[Fact]
public void GameEntry_ProcessNameFromPath_JustFileName()
{
var result = GameEntry.ProcessNameFromPath("myapp.exe");
Assert.Equal("myapp", result);
}
[Fact]
public void GameEntry_DefaultValues()
{
var g = new GameEntry();
Assert.Equal(string.Empty, g.Name);
Assert.Equal(string.Empty, g.ExecutablePath);
Assert.Equal("Manual", g.LauncherSource);
Assert.True(g.AutoBoost);
Assert.Null(g.IconPath);
}
[Fact]
public void BoostProfile_DefaultValues()
{
var p = new BoostProfile();
Assert.True(p.CleanRam);
Assert.True(p.SetHighPriority);
Assert.True(p.ReduceTimerResolution);
Assert.True(p.StopServices);
Assert.False(p.OptimizeNetwork);
}
// ── ProcessEntry ─────────────────────────────────────────
[Fact]
public void ProcessEntry_Properties()
{
var p = new ProcessEntry
{
Pid = 1234,
Name = "chrome",
WindowTitle = "Google Chrome",
CpuPercent = 12.5f,
MemoryMB = 350f,
PriorityClass = "High",
IsSystem = false
};
Assert.Equal(1234, p.Pid);
Assert.Equal("chrome", p.Name);
Assert.Equal("Google Chrome", p.WindowTitle);
Assert.Equal(12.5f, p.CpuPercent);
Assert.Equal(350f, p.MemoryMB);
Assert.Equal("High", p.PriorityClass);
Assert.False(p.IsSystem);
}
[Fact]
public void ProcessEntry_DefaultPriority_IsNormal()
{
var p = new ProcessEntry { Pid = 1, Name = "test" };
Assert.Equal("Normal", p.PriorityClass);
}
// ── ServiceEntry ─────────────────────────────────────────
[Fact]
public void ServiceEntry_DefaultStatus_IsUnknown()
{
var s = new ServiceEntry();
Assert.Equal("Unknown", s.Status);
Assert.False(s.WasRunningBeforeBoost);
}
[Fact]
public void ServiceEntry_Properties()
{
var s = new ServiceEntry
{
ServiceName = "WSearch",
DisplayName = "Windows Search",
Status = "Running",
WasRunningBeforeBoost = true,
Description = "Full-text search"
};
Assert.Equal("WSearch", s.ServiceName);
Assert.Equal("Windows Search", s.DisplayName);
Assert.Equal("Running", s.Status);
Assert.True(s.WasRunningBeforeBoost);
}
// ── AppSettings ──────────────────────────────────────────
[Fact]
public void AppSettings_DefaultValues()
{
var s = new AppSettings();
Assert.True(s.MinimizeToTray);
Assert.False(s.StartWithWindows);
Assert.True(s.AutoDetectGames);
Assert.True(s.AutoBoostOnGameLaunch);
Assert.False(s.ShowWidget);
Assert.Equal("8.8.8.8", s.PingTarget);
Assert.Equal("1.1.1.1", s.PreferredDns);
Assert.Equal(1000, s.MonitoringIntervalMs);
Assert.Contains("SysMain", s.ServicesToStop);
Assert.Contains("WSearch", s.ServicesToStop);
}
[Fact]
public void AppSettings_WidgetDefaults()
{
var s = new AppSettings();
Assert.True(s.WidgetCompactMode);
Assert.Equal(100, s.WidgetLeft);
Assert.Equal(100, s.WidgetTop);
Assert.Equal(0.85, s.WidgetOpacity);
Assert.Equal(1000, s.WidgetUpdateIntervalMs);
Assert.False(s.WidgetClickThrough);
}
// ── ShaderCleanResult ────────────────────────────────────
[Fact]
public void ShaderCleanResult_MBFreed_Calculation()
{
var r = new SpdUp.Services.ShaderCleanResult(10, 10_485_760); // 10 MB
Assert.Equal(10.0, r.MBFreed, 1);
}
[Fact]
public void ShaderCleanResult_ZeroBytes()
{
var r = new SpdUp.Services.ShaderCleanResult(0, 0);
Assert.Equal(0.0, r.MBFreed);
Assert.Equal(0, r.FilesDeleted);
}
// ── RamCleanupResult ─────────────────────────────────────
[Fact]
public void RamCleanupResult_RecordWithSyntax()
{
var r = new SpdUp.Services.RamCleanupResult(50, 5, 0);
var r2 = r with { StandbyPurged = 1 };
Assert.Equal(50, r2.TrimmedCount);
Assert.Equal(5, r2.FailedCount);
Assert.Equal(1, r2.StandbyPurged);
}
// ── BoostResult ──────────────────────────────────────────
[Fact]
public void BoostResult_Properties()
{
var r = new SpdUp.Services.BoostResult(true, "Boost activated");
Assert.True(r.Success);
Assert.Equal("Boost activated", r.Message);
}
[Fact]
public void BoostResult_Failure()
{
var r = new SpdUp.Services.BoostResult(false, "Error occurred");
Assert.False(r.Success);
}
}
+93
View File
@@ -0,0 +1,93 @@
using System.ComponentModel;
using SpdUp.Core;
namespace SpdUp.Tests;
/// <summary>
/// Tests for ObservableObject base class.
/// </summary>
public class ObservableObjectTests
{
private sealed class TestObservable : ObservableObject
{
private string _name = string.Empty;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private int _count;
public int Count
{
get => _count;
set => SetProperty(ref _count, value);
}
}
[Fact]
public void SetProperty_NewValue_RaisesPropertyChanged()
{
var obj = new TestObservable();
string? changedProp = null;
obj.PropertyChanged += (_, e) => changedProp = e.PropertyName;
obj.Name = "hello";
Assert.Equal("Name", changedProp);
Assert.Equal("hello", obj.Name);
}
[Fact]
public void SetProperty_SameValue_DoesNotRaisePropertyChanged()
{
var obj = new TestObservable { Name = "hello" };
bool raised = false;
obj.PropertyChanged += (_, _) => raised = true;
obj.Name = "hello";
Assert.False(raised);
}
[Fact]
public void SetProperty_ReturnsTrue_WhenValueChanged()
{
var obj = new TestObservable();
string? changedProp = null;
obj.PropertyChanged += (_, e) => changedProp = e.PropertyName;
obj.Count = 42;
Assert.Equal("Count", changedProp);
Assert.Equal(42, obj.Count);
}
[Fact]
public void SetProperty_ReturnsFalse_WhenValueUnchanged()
{
var obj = new TestObservable { Count = 5 };
bool raised = false;
obj.PropertyChanged += (_, _) => raised = true;
obj.Count = 5;
Assert.False(raised);
}
[Fact]
public void MultipleProperties_OnlyNotifyChanged()
{
var obj = new TestObservable();
var notifications = new List<string>();
obj.PropertyChanged += (_, e) => notifications.Add(e.PropertyName!);
obj.Name = "a";
obj.Count = 1;
obj.Name = "a"; // same no notification
obj.Count = 2;
Assert.Equal(3, notifications.Count);
Assert.Equal(["Name", "Count", "Count"], notifications);
}
}
+99
View File
@@ -0,0 +1,99 @@
using SpdUp.Core;
namespace SpdUp.Tests;
/// <summary>
/// Tests for RelayCommand and RelayCommand&lt;T&gt;.
/// </summary>
public class RelayCommandTests
{
[Fact]
public void Execute_InvokesAction()
{
bool executed = false;
var cmd = new RelayCommand(() => executed = true);
cmd.Execute(null);
Assert.True(executed);
}
[Fact]
public void Execute_WithParameter_InvokesParameterizedAction()
{
object? received = null;
var cmd = new RelayCommand(p => received = p);
cmd.Execute("test");
Assert.Equal("test", received);
}
[Fact]
public void CanExecute_DefaultsToTrue()
{
var cmd = new RelayCommand(() => { });
Assert.True(cmd.CanExecute(null));
}
[Fact]
public void CanExecute_RespectsCanExecuteFunc()
{
var cmd = new RelayCommand(() => { }, () => false);
Assert.False(cmd.CanExecute(null));
}
[Fact]
public void CanExecute_ParameterizedPredicate()
{
var cmd = new RelayCommand(_ => { }, p => p is "go");
Assert.True(cmd.CanExecute("go"));
Assert.False(cmd.CanExecute("stop"));
Assert.False(cmd.CanExecute(null));
}
[Fact]
public void Constructor_ThrowsOnNullAction()
{
Assert.Throws<ArgumentNullException>(() => new RelayCommand((Action<object?>)null!));
}
// ── Generic RelayCommand<T> ──────────────────────────────
[Fact]
public void Generic_Execute_InvokesWithTypedParameter()
{
string? received = null;
var cmd = new RelayCommand<string>(s => received = s);
cmd.Execute("hello");
Assert.Equal("hello", received);
}
[Fact]
public void Generic_CanExecute_DefaultsToTrue()
{
var cmd = new RelayCommand<int>(_ => { });
Assert.True(cmd.CanExecute(42));
}
[Fact]
public void Generic_CanExecute_RespectsFunc()
{
var cmd = new RelayCommand<int>(_ => { }, n => n > 0);
Assert.True(cmd.CanExecute(5));
Assert.False(cmd.CanExecute(-1));
}
[Fact]
public void Generic_Constructor_ThrowsOnNullAction()
{
Assert.Throws<ArgumentNullException>(() => new RelayCommand<string>(null!));
}
}
+150
View File
@@ -0,0 +1,150 @@
using SpdUp.Models;
using SpdUp.Services;
namespace SpdUp.Tests;
/// <summary>
/// Tests for services that can be tested without admin/system access.
/// </summary>
public class ServiceTests
{
// ── NetworkService.PingAsync ──────────────────────────────
[Fact]
public async Task PingAsync_Localhost_ReturnsPositive()
{
var ms = await NetworkService.PingAsync("127.0.0.1", 2000);
Assert.True(ms >= 0, $"Ping to localhost should succeed, got {ms}");
}
[Fact]
public async Task PingAsync_InvalidHost_ReturnsNegative()
{
var ms = await NetworkService.PingAsync("0.0.0.0", 500);
// 0.0.0.0 typically fails or returns -1
// On some machines it may succeed — accept either
Assert.True(ms >= -1);
}
// ── GameDetectionService ─────────────────────────────────
[Fact]
public void GameDetectionService_InitialState_NoGameDetected()
{
var settings = new AppSettings();
using var gds = new GameDetectionService(settings);
Assert.Null(gds.CurrentGameProcess);
}
[Fact]
public void GameDetectionService_AddGameProcess()
{
var settings = new AppSettings();
using var gds = new GameDetectionService(settings);
gds.AddGameProcess("TestGame");
// AddGameProcess should not throw
}
[Fact]
public void GameDetectionService_StartAndStop_DoesNotThrow()
{
var settings = new AppSettings();
using var gds = new GameDetectionService(settings);
gds.Start(60000); // very long interval so it doesn't actually poll during test
gds.Stop();
}
[Fact]
public void GameDetectionService_Dispose_DoesNotThrow()
{
var settings = new AppSettings();
var gds = new GameDetectionService(settings);
gds.Dispose();
// Double dispose should not throw
gds.Dispose();
}
// ── OptimizationService ──────────────────────────────────
[Fact]
public void OptimizationService_InitialState()
{
var settings = new AppSettings();
var svc = new OptimizationService(settings);
Assert.False(svc.IsBoosted);
Assert.Null(svc.BoostedProcessName);
}
[Fact]
public void OptimizationService_Events_CanSubscribe()
{
var settings = new AppSettings();
var svc = new OptimizationService(settings);
bool activated = false;
bool deactivated = false;
svc.BoostActivated += _ => activated = true;
svc.BoostDeactivated += () => deactivated = true;
// Just verify event subscription doesn't throw
Assert.False(activated);
Assert.False(deactivated);
}
// ── ServiceManagementService ─────────────────────────────
[Fact]
public void KnownNonCriticalServices_ContainsExpectedEntries()
{
var known = ServiceManagementService.KnownNonCriticalServices;
Assert.True(known.ContainsKey("SysMain"));
Assert.True(known.ContainsKey("WSearch"));
Assert.True(known.ContainsKey("DiagTrack"));
Assert.True(known.ContainsKey("XblAuthManager"));
Assert.True(known.Count >= 10, "Should have at least 10 known services");
}
[Fact]
public void GetServiceList_NonExistentService_ReturnsNotFound()
{
var entries = ServiceManagementService.GetServiceList(new[] { "SpdUpFakeService12345" });
Assert.Single(entries);
Assert.Equal("NotFound", entries[0].Status);
Assert.Equal("SpdUpFakeService12345", entries[0].ServiceName);
}
[Fact]
public void GetServiceList_EmptyInput_ReturnsEmpty()
{
var entries = ServiceManagementService.GetServiceList(Array.Empty<string>());
Assert.Empty(entries);
}
// ── ShaderCleanResult ────────────────────────────────────
[Fact]
public void ShaderCleanResult_MBFreed_LargeValue()
{
var r = new ShaderCleanResult(100, 1_073_741_824); // 1 GB
Assert.Equal(1024.0, r.MBFreed, 0.1);
}
// ── RamOptimizationService ───────────────────────────────
[Fact]
public void RamOptimizationService_GetMemoryStatus_ReturnsValidValues()
{
var (totalMB, availMB, loadPercent) = RamOptimizationService.GetMemoryStatus();
Assert.True(totalMB > 0, "Total RAM should be > 0");
Assert.True(availMB > 0, "Available RAM should be > 0");
Assert.True(availMB <= totalMB, "Available should be <= total");
Assert.InRange(loadPercent, (uint)1, (uint)100);
}
}
+30
View File
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SpdUp.csproj" />
</ItemGroup>
</Project>
+157
View File
@@ -0,0 +1,157 @@
using System.Collections.Specialized;
using SpdUp.Core;
using SpdUp.ViewModels;
namespace SpdUp.Tests;
/// <summary>
/// Tests for DebugLogViewModel.
/// Uses a helper to run on STA thread for WPF Dispatcher compatibility.
/// </summary>
public class DebugLogViewModelTests
{
private static void RunOnSta(Action action)
{
Exception? caught = null;
var thread = new Thread(() =>
{
try { action(); }
catch (Exception ex) { caught = ex; }
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (caught is not null)
throw new Xunit.Sdk.XunitException($"STA test failed: {caught.Message}", caught);
}
[Fact]
public void Initial_LogLines_IsEmpty()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.Empty(vm.LogLines);
});
}
[Fact]
public void AutoScroll_DefaultTrue()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.True(vm.AutoScroll);
});
}
[Fact]
public void AutoScroll_Settable()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.AutoScroll = false;
Assert.False(vm.AutoScroll);
});
}
[Fact]
public void FilterText_DefaultEmpty()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.Equal(string.Empty, vm.FilterText);
});
}
[Fact]
public void FilterText_RaisesPropertyChanged()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
var changed = new List<string>();
vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!);
vm.FilterText = "error";
Assert.Contains("FilterText", changed);
Assert.Contains("FilteredLogLines", changed);
});
}
[Fact]
public void ClearCommand_ClearsLogLines()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("test line 1");
vm.LogLines.Add("test line 2");
vm.ClearCommand.Execute(null);
Assert.Empty(vm.LogLines);
});
}
[Fact]
public void FilteredLogLines_NoFilter_ReturnsAll()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("[INFO] started");
vm.LogLines.Add("[ERROR] failed");
vm.LogLines.Add("[WARN] low memory");
var filtered = vm.FilteredLogLines.ToList();
Assert.Equal(3, filtered.Count);
});
}
[Fact]
public void FilteredLogLines_WithFilter_ReturnsMatches()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("[INFO] started");
vm.LogLines.Add("[ERROR] something failed");
vm.LogLines.Add("[WARN] low memory");
vm.FilterText = "error";
var filtered = vm.FilteredLogLines.ToList();
Assert.Single(filtered);
Assert.Contains("ERROR", filtered[0]);
});
}
[Fact]
public void FilteredLogLines_CaseInsensitive()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
vm.LogLines.Add("[INFO] Test Message");
vm.FilterText = "test message";
Assert.Single(vm.FilteredLogLines);
});
}
[Fact]
public void Commands_AreNotNull()
{
RunOnSta(() =>
{
var vm = new DebugLogViewModel();
Assert.NotNull(vm.ClearCommand);
Assert.NotNull(vm.CopyAllCommand);
});
}
}
+25
View File
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);SpdUp.Tests\**</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.3" />
<PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\**" />
</ItemGroup>
</Project>
+51
View File
@@ -0,0 +1,51 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpdUp", "SpdUp.csproj", "{F773DED5-00C7-8FFD-6136-0562AFD6C350}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpdUp.Tests", "SpdUp.Tests\SpdUp.Tests.csproj", "{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x64.ActiveCfg = Debug|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x64.Build.0 = Debug|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x86.ActiveCfg = Debug|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x86.Build.0 = Debug|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|Any CPU.Build.0 = Release|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x64.ActiveCfg = Release|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x64.Build.0 = Release|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x86.ActiveCfg = Release|Any CPU
{F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x86.Build.0 = Release|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x64.ActiveCfg = Debug|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x64.Build.0 = Debug|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x86.ActiveCfg = Debug|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x86.Build.0 = Debug|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|Any CPU.Build.0 = Release|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x64.ActiveCfg = Release|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x64.Build.0 = Release|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x86.ActiveCfg = Release|Any CPU
{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {50BCE545-BC95-4338-A07B-8D3FA391AB36}
EndGlobalSection
EndGlobal
+254
View File
@@ -0,0 +1,254 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- ══════════════════════════════════════════════════════════════════
DARK GAMING THEME
══════════════════════════════════════════════════════════════════ -->
<!-- Colour palette -->
<Color x:Key="BgDark">#0D0D0F</Color>
<Color x:Key="BgPanel">#16161A</Color>
<Color x:Key="BgCard">#1E1E24</Color>
<Color x:Key="BgHover">#2A2A32</Color>
<Color x:Key="BorderDim">#2E2E38</Color>
<Color x:Key="TextPrimary">#E8E8EC</Color>
<Color x:Key="TextSecondary">#8B8B96</Color>
<Color x:Key="AccentGreen">#30D158</Color>
<Color x:Key="AccentBlue">#0A84FF</Color>
<Color x:Key="AccentRed">#FF453A</Color>
<Color x:Key="AccentOrange">#FF9F0A</Color>
<Color x:Key="AccentPurple">#BF5AF2</Color>
<Color x:Key="AccentCyan">#64D2FF</Color>
<!-- Trans flag colours -->
<Color x:Key="TransBlue">#55CDFC</Color>
<Color x:Key="TransPink">#F7A8B8</Color>
<Color x:Key="TransWhite">#FFFFFF</Color>
<!-- Brushes -->
<SolidColorBrush x:Key="BgDarkBrush" Color="{StaticResource BgDark}" />
<SolidColorBrush x:Key="BgPanelBrush" Color="{StaticResource BgPanel}" />
<SolidColorBrush x:Key="BgCardBrush" Color="{StaticResource BgCard}" />
<SolidColorBrush x:Key="BgHoverBrush" Color="{StaticResource BgHover}" />
<SolidColorBrush x:Key="BorderDimBrush" Color="{StaticResource BorderDim}" />
<SolidColorBrush x:Key="TextPrimaryBrush" Color="{StaticResource TextPrimary}" />
<SolidColorBrush x:Key="TextSecondaryBrush" Color="{StaticResource TextSecondary}" />
<SolidColorBrush x:Key="AccentGreenBrush" Color="{StaticResource AccentGreen}" />
<SolidColorBrush x:Key="AccentBlueBrush" Color="{StaticResource AccentBlue}" />
<SolidColorBrush x:Key="AccentRedBrush" Color="{StaticResource AccentRed}" />
<SolidColorBrush x:Key="AccentOrangeBrush" Color="{StaticResource AccentOrange}" />
<SolidColorBrush x:Key="AccentPurpleBrush" Color="{StaticResource AccentPurple}" />
<SolidColorBrush x:Key="AccentCyanBrush" Color="{StaticResource AccentCyan}" />
<SolidColorBrush x:Key="TransBlueBrush" Color="{StaticResource TransBlue}" />
<SolidColorBrush x:Key="TransPinkBrush" Color="{StaticResource TransPink}" />
<SolidColorBrush x:Key="TransWhiteBrush" Color="{StaticResource TransWhite}" />
<!-- ── Nav Button ──────────────────────────────────────────────── -->
<Style x:Key="NavButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="16,12" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bd" Background="{TemplateBinding Background}"
CornerRadius="8" Padding="{TemplateBinding Padding}" Margin="4,2">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="{StaticResource BgHoverBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Primary Action Button ───────────────────────────────────── -->
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentGreenBrush}" />
<Setter Property="Foreground" Value="#000000" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="24,14" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bd" Background="{TemplateBinding Background}"
CornerRadius="12" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Opacity" Value="0.85" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="bd" Property="Opacity" Value="0.7" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Secondary Button ────────────────────────────────────────── -->
<Style x:Key="SecondaryButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="16,8" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="bd" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="{StaticResource BgHoverBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Danger Button ───────────────────────────────────────────── -->
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource SecondaryButton}">
<Setter Property="BorderBrush" Value="{StaticResource AccentRedBrush}" />
<Setter Property="Foreground" Value="{StaticResource AccentRedBrush}" />
</Style>
<!-- ── Card Panel ──────────────────────────────────────────────── -->
<Style x:Key="Card" TargetType="Border">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="20" />
<Setter Property="Margin" Value="0,0,0,12" />
<Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<!-- ── Metric Value Text ───────────────────────────────────────── -->
<Style x:Key="MetricValue" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style x:Key="MetricLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="FontWeight" Value="Medium" />
<Setter Property="Margin" Value="0,2,0,0" />
</Style>
<!-- ── DataGrid Dark ───────────────────────────────────────────── -->
<Style TargetType="DataGrid">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}" />
<Setter Property="RowBackground" Value="{StaticResource BgCardBrush}" />
<Setter Property="AlternatingRowBackground" Value="{StaticResource BgPanelBrush}" />
<Setter Property="GridLinesVisibility" Value="None" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="SelectionMode" Value="Single" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="{StaticResource BgPanelBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="Padding" Value="8,6" />
<Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="11" />
</Style>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridCell">
<Border Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}">
<ContentPresenter VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource BgHoverBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGridRow">
<Setter Property="BorderThickness" Value="0" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource BgHoverBrush}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource BgHoverBrush}" />
</Trigger>
</Style.Triggers>
</Style>
<!-- ── ScrollBar Dark ──────────────────────────────────────────── -->
<Style TargetType="ScrollBar">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Width" Value="8" />
</Style>
<!-- ── TextBox Dark ────────────────────────────────────────────── -->
<Style TargetType="TextBox">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}" />
<Setter Property="CaretBrush" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="Padding" Value="8,6" />
<Setter Property="FontSize" Value="12" />
</Style>
<!-- ── CheckBox Dark ───────────────────────────────────────────── -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0,4" />
</Style>
<!-- ── Slider Dark ─────────────────────────────────────────────── -->
<Style TargetType="Slider">
<Setter Property="Foreground" Value="{StaticResource AccentBlueBrush}" />
<Setter Property="Minimum" Value="0" />
<Setter Property="Maximum" Value="100" />
</Style>
<!-- ── ToolTip Dark ────────────────────────────────────────────── -->
<Style TargetType="ToolTip">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderDimBrush}" />
</Style>
</ResourceDictionary>
+46
View File
@@ -0,0 +1,46 @@
using System.Windows.Input;
using SpdUp.Core;
using SpdUp.Services;
namespace SpdUp.ViewModels;
/// <summary>
/// Boost page ViewModel.
/// </summary>
public sealed class BoostViewModel : ObservableObject
{
private readonly MainViewModel _main;
private readonly OptimizationService _optimizationService;
private string _boostLog = string.Empty;
public string BoostLog
{
get => _boostLog;
set => SetProperty(ref _boostLog, value);
}
public MainViewModel Main => _main;
public ICommand BoostCommand { get; }
public ICommand CleanRamCommand { get; }
public ICommand CleanShadersCommand { get; }
public BoostViewModel(MainViewModel main, OptimizationService optimizationService)
{
_main = main;
_optimizationService = optimizationService;
BoostCommand = _main.BoostCommand;
CleanRamCommand = _main.CleanRamCommand;
CleanShadersCommand = _main.CleanShaderCacheCommand;
_optimizationService.BoostActivated += msg =>
{
BoostLog = $"[{DateTime.Now:HH:mm:ss}] BOOST ON\n{msg}\n\n{BoostLog}";
};
_optimizationService.BoostDeactivated += () =>
{
BoostLog = $"[{DateTime.Now:HH:mm:ss}] BOOST OFF Restored.\n\n{BoostLog}";
};
}
}
+16
View File
@@ -0,0 +1,16 @@
using SpdUp.Core;
namespace SpdUp.ViewModels;
/// <summary>
/// Dashboard page just binds to MainViewModel's live metrics.
/// </summary>
public sealed class DashboardViewModel : ObservableObject
{
public MainViewModel Main { get; }
public DashboardViewModel(MainViewModel main)
{
Main = main;
}
}
+74
View File
@@ -0,0 +1,74 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Windows.Threading;
using SpdUp.Core;
namespace SpdUp.ViewModels;
/// <summary>
/// Debug Log page live scrolling log of every action the application performs.
/// </summary>
public sealed class DebugLogViewModel : ObservableObject
{
private const int MaxLines = 2000;
private readonly Dispatcher _dispatcher;
public ObservableCollection<string> LogLines { get; } = new();
private string _filterText = string.Empty;
public string FilterText
{
get => _filterText;
set
{
if (SetProperty(ref _filterText, value))
OnPropertyChanged(nameof(FilteredLogLines));
}
}
public IEnumerable<string> FilteredLogLines =>
string.IsNullOrWhiteSpace(_filterText)
? LogLines
: LogLines.Where(l => l.Contains(_filterText, StringComparison.OrdinalIgnoreCase));
private bool _autoScroll = true;
public bool AutoScroll
{
get => _autoScroll;
set => SetProperty(ref _autoScroll, value);
}
public ICommand ClearCommand { get; }
public ICommand CopyAllCommand { get; }
public DebugLogViewModel()
{
_dispatcher = Dispatcher.CurrentDispatcher;
ClearCommand = new RelayCommand(() => { LogLines.Clear(); OnPropertyChanged(nameof(FilteredLogLines)); });
CopyAllCommand = new RelayCommand(() =>
{
try
{
var text = string.Join(Environment.NewLine, LogLines);
System.Windows.Clipboard.SetText(text);
}
catch { }
});
// Subscribe to all log events
Logger.LogWritten += OnLogWritten;
}
private void OnLogWritten(string line)
{
_dispatcher.BeginInvoke(() =>
{
LogLines.Add(line);
while (LogLines.Count > MaxLines)
LogLines.RemoveAt(0);
OnPropertyChanged(nameof(FilteredLogLines));
});
}
}
+72
View File
@@ -0,0 +1,72 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using Microsoft.Win32;
using SpdUp.Core;
using SpdUp.Models;
using SpdUp.Services;
namespace SpdUp.ViewModels;
/// <summary>
/// Game Library page manage detected and manually-added games.
/// </summary>
public sealed class GameLibraryViewModel : ObservableObject
{
private readonly AppSettings _settings;
private readonly GameDetectionService _detectionService;
public ObservableCollection<GameEntry> Games { get; }
private GameEntry? _selectedGame;
public GameEntry? SelectedGame
{
get => _selectedGame;
set => SetProperty(ref _selectedGame, value);
}
public ICommand AddGameCommand { get; }
public ICommand RemoveGameCommand { get; }
public GameLibraryViewModel(AppSettings settings, GameDetectionService detectionService)
{
_settings = settings;
_detectionService = detectionService;
Games = new ObservableCollection<GameEntry>(_settings.Games);
AddGameCommand = new RelayCommand(AddGame);
RemoveGameCommand = new RelayCommand(RemoveGame, () => SelectedGame is not null);
}
private void AddGame()
{
var dlg = new OpenFileDialog
{
Filter = "Executables (*.exe)|*.exe",
Title = "Select Game Executable"
};
if (dlg.ShowDialog() == true)
{
var entry = new GameEntry
{
Name = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName),
ExecutablePath = dlg.FileName,
ProcessName = GameEntry.ProcessNameFromPath(dlg.FileName),
LauncherSource = "Manual"
};
Games.Add(entry);
_settings.Games.Add(entry);
_detectionService.AddGameProcess(entry.ProcessName);
_settings.Save();
}
}
private void RemoveGame()
{
if (SelectedGame is null) return;
_settings.Games.Remove(SelectedGame);
Games.Remove(SelectedGame);
_settings.Save();
}
}
+264
View File
@@ -0,0 +1,264 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using SpdUp.Core;
using SpdUp.Models;
using SpdUp.Services;
namespace SpdUp.ViewModels;
/// <summary>
/// Root ViewModel drives the main window, navigation, boost, and monitoring.
/// </summary>
public sealed class MainViewModel : ObservableObject, IDisposable
{
// ── Services ───────────────────────────────────────────────────────
private readonly AppSettings _settings;
private readonly OptimizationService _optimizationService;
private readonly GameDetectionService _gameDetectionService;
private readonly MonitoringService _monitoringService;
private readonly NetworkService _networkService = new();
private readonly Dispatcher _dispatcher;
// ── Sub-ViewModels ─────────────────────────────────────────────────
public DashboardViewModel Dashboard { get; }
public BoostViewModel Boost { get; }
public MonitorViewModel Monitor { get; }
public GameLibraryViewModel GameLibrary { get; }
public ProcessManagerViewModel ProcessManager { get; }
public ServiceManagerViewModel ServiceManager { get; }
public SettingsViewModel Settings { get; }
public DebugLogViewModel DebugLog { get; }
// ── Navigation ─────────────────────────────────────────────────────
private object _currentPage;
public object CurrentPage
{
get => _currentPage;
set => SetProperty(ref _currentPage, value);
}
private string _currentPageTitle = "Dashboard";
public string CurrentPageTitle
{
get => _currentPageTitle;
set => SetProperty(ref _currentPageTitle, value);
}
// ── Status ─────────────────────────────────────────────────────────
private string _statusText = "Ready";
public string StatusText
{
get => _statusText;
set => SetProperty(ref _statusText, value);
}
private bool _isBoosted;
public bool IsBoosted
{
get => _isBoosted;
set => SetProperty(ref _isBoosted, value);
}
// ── Live Metrics (bound by views & widget) ─────────────────────────
private float _cpuUsage;
public float CpuUsage { get => _cpuUsage; set => SetProperty(ref _cpuUsage, value); }
private float _ramUsagePercent;
public float RamUsagePercent { get => _ramUsagePercent; set => SetProperty(ref _ramUsagePercent, value); }
private float _ramUsedMB;
public float RamUsedMB { get => _ramUsedMB; set => SetProperty(ref _ramUsedMB, value); }
private float _ramTotalMB;
public float RamTotalMB { get => _ramTotalMB; set => SetProperty(ref _ramTotalMB, value); }
private float _gpuUsage;
public float GpuUsage { get => _gpuUsage; set => SetProperty(ref _gpuUsage, value); }
private float _cpuTemp;
public float CpuTemp { get => _cpuTemp; set => SetProperty(ref _cpuTemp, value); }
private float _gpuTemp;
public float GpuTemp { get => _gpuTemp; set => SetProperty(ref _gpuTemp, value); }
private float _pingMs;
public float PingMs { get => _pingMs; set => SetProperty(ref _pingMs, value); }
// ── Commands ───────────────────────────────────────────────────────
public ICommand NavigateCommand { get; }
public ICommand BoostCommand { get; }
public ICommand ToggleWidgetCommand { get; }
public ICommand CleanRamCommand { get; }
public ICommand CleanShaderCacheCommand { get; }
// ── Widget ─────────────────────────────────────────────────────────
private bool _isWidgetVisible;
public bool IsWidgetVisible
{
get => _isWidgetVisible;
set
{
if (SetProperty(ref _isWidgetVisible, value))
_settings.ShowWidget = value;
}
}
public MainViewModel()
{
_dispatcher = Dispatcher.CurrentDispatcher;
_settings = AppSettings.Load();
// Initialise services
_optimizationService = new OptimizationService(_settings);
_gameDetectionService = new GameDetectionService(_settings);
_monitoringService = new MonitoringService(_settings.PingTarget);
// Sub-ViewModels
Dashboard = new DashboardViewModel(this);
Boost = new BoostViewModel(this, _optimizationService);
Monitor = new MonitorViewModel(this);
GameLibrary = new GameLibraryViewModel(_settings, _gameDetectionService);
ProcessManager = new ProcessManagerViewModel();
ServiceManager = new ServiceManagerViewModel(_settings);
Settings = new SettingsViewModel(_settings, this);
DebugLog = new DebugLogViewModel();
_currentPage = Dashboard;
// Commands
NavigateCommand = new RelayCommand<string>(Navigate);
BoostCommand = new RelayCommand(async () => await ToggleBoostAsync());
ToggleWidgetCommand = new RelayCommand(() => IsWidgetVisible = !IsWidgetVisible);
CleanRamCommand = new RelayCommand(async () => await CleanRamAsync());
CleanShaderCacheCommand = new RelayCommand(async () => await CleanShaderCacheAsync());
// Wire up monitoring
_monitoringService.MetricsUpdated += OnMetricsUpdated;
_monitoringService.Start(_settings.MonitoringIntervalMs);
// Wire up game detection
_gameDetectionService.GameStarted += OnGameStarted;
_gameDetectionService.GameStopped += OnGameStopped;
if (_settings.AutoDetectGames)
_gameDetectionService.Start();
// Widget state
_isWidgetVisible = _settings.ShowWidget;
Logger.PurgeOldLogs();
Logger.Info("Application started.");
}
// ── Navigation ─────────────────────────────────────────────────────
private void Navigate(string? pageName)
{
(CurrentPage, CurrentPageTitle) = pageName switch
{
"Dashboard" => ((object)Dashboard, "Dashboard"),
"Boost" => (Boost, "Game Boost"),
"Monitor" => (Monitor, "System Monitor"),
"Games" => (GameLibrary, "Game Library"),
"Processes" => (ProcessManager, "Process Manager"),
"Services" => (ServiceManager, "Service Manager"),
"Settings" => (Settings, "Settings"),
"Debug" => (DebugLog, "Debug Log"),
_ => (Dashboard, "Dashboard")
};
}
// ── Boost Toggle ───────────────────────────────────────────────────
private async Task ToggleBoostAsync()
{
if (_optimizationService.IsBoosted)
{
StatusText = "Deactivating boost...";
await _optimizationService.DeactivateBoostAsync();
IsBoosted = false;
StatusText = "Boost deactivated settings restored.";
}
else
{
StatusText = "Activating boost...";
var gameProcess = _gameDetectionService.CurrentGameProcess;
var result = await _optimizationService.ActivateBoostAsync(gameProcess);
IsBoosted = result.Success;
StatusText = result.Success ? "🚀 Boost Active" : result.Message;
}
}
// ── Quick Actions ──────────────────────────────────────────────────
private async Task CleanRamAsync()
{
StatusText = "Cleaning RAM...";
var result = await Task.Run(() => new RamOptimizationService().FullCleanup());
StatusText = $"RAM cleaned: {result.TrimmedCount} processes trimmed.";
}
private async Task CleanShaderCacheAsync()
{
StatusText = "Cleaning shader caches...";
var result = await Task.Run(() => ShaderCacheCleanerService.CleanAll());
StatusText = $"Shader cache: {result.FilesDeleted} files, {result.MBFreed:F1} MB freed.";
}
// ── Monitoring Callback ────────────────────────────────────────────
private void OnMetricsUpdated(SystemMetrics m)
{
_dispatcher.BeginInvoke(() =>
{
CpuUsage = m.CpuUsage;
RamUsagePercent = m.RamUsagePercent;
RamUsedMB = m.RamUsageMB;
RamTotalMB = m.RamTotalMB;
GpuUsage = m.GpuUsage;
CpuTemp = m.CpuTemperature;
GpuTemp = m.GpuTemperature;
PingMs = m.NetworkPingMs;
});
}
// ── Game Detection Callbacks ───────────────────────────────────────
#pragma warning disable CS4014 // Fire-and-forget is intentional for event handlers
private async void OnGameStarted(string processName)
{
_dispatcher.BeginInvoke(() => StatusText = $"Game detected: {processName}");
if (_settings.AutoBoostOnGameLaunch && !_optimizationService.IsBoosted)
{
var result = await _optimizationService.ActivateBoostAsync(processName);
_dispatcher.BeginInvoke(() =>
{
IsBoosted = result.Success;
StatusText = result.Success ? $"🚀 Auto-Boost for {processName}" : result.Message;
});
}
}
private async void OnGameStopped(string processName)
{
if (_optimizationService.IsBoosted)
{
await _optimizationService.DeactivateBoostAsync();
_dispatcher.BeginInvoke(() =>
{
IsBoosted = false;
StatusText = $"Game exited: {processName} boost deactivated.";
});
}
}
#pragma warning restore CS4014
public void SaveSettings() => _settings.Save();
public void Dispose()
{
_monitoringService.MetricsUpdated -= OnMetricsUpdated;
_monitoringService.Dispose();
_gameDetectionService.Dispose();
_settings.Save();
Logger.Info("Application closed.");
}
}
+16
View File
@@ -0,0 +1,16 @@
using SpdUp.Core;
namespace SpdUp.ViewModels;
/// <summary>
/// System Monitor page just binds to MainViewModel's live metrics.
/// </summary>
public sealed class MonitorViewModel : ObservableObject
{
public MainViewModel Main { get; }
public MonitorViewModel(MainViewModel main)
{
Main = main;
}
}
+100
View File
@@ -0,0 +1,100 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Input;
using System.Windows.Threading;
using SpdUp.Core;
using SpdUp.Models;
using SpdUp.Services;
namespace SpdUp.ViewModels;
/// <summary>
/// Process Manager ViewModel.
/// </summary>
public sealed class ProcessManagerViewModel : ObservableObject
{
private readonly DispatcherTimer _refreshTimer;
public ObservableCollection<ProcessEntry> Processes { get; } = new();
private ProcessEntry? _selectedProcess;
public ProcessEntry? SelectedProcess
{
get => _selectedProcess;
set => SetProperty(ref _selectedProcess, value);
}
public ICommand RefreshCommand { get; }
public ICommand KillProcessCommand { get; }
public ICommand SetHighPriorityCommand { get; }
public ICommand SetNormalPriorityCommand { get; }
public ProcessManagerViewModel()
{
RefreshCommand = new RelayCommand(RefreshProcesses);
KillProcessCommand = new RelayCommand(KillSelected, () => SelectedProcess is not null);
SetHighPriorityCommand = new RelayCommand(() => SetPriority(ProcessPriorityClass.High), () => SelectedProcess is not null);
SetNormalPriorityCommand = new RelayCommand(() => SetPriority(ProcessPriorityClass.Normal), () => SelectedProcess is not null);
_refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
_refreshTimer.Tick += (_, _) => RefreshProcesses();
RefreshProcesses();
_refreshTimer.Start();
}
private void RefreshProcesses()
{
var procs = Process.GetProcesses()
.Select(p =>
{
try
{
return new ProcessEntry
{
Pid = p.Id,
Name = p.ProcessName,
WindowTitle = string.IsNullOrWhiteSpace(p.MainWindowTitle) ? null : p.MainWindowTitle,
MemoryMB = p.WorkingSet64 / 1048576f,
PriorityClass = TryGetPriority(p),
IsSystem = p.Id <= 4
};
}
catch { return null; }
finally { p.Dispose(); }
})
.Where(e => e is not null)
.OrderByDescending(e => e!.MemoryMB)
.Take(200)
.ToList();
Processes.Clear();
foreach (var p in procs)
Processes.Add(p!);
}
private static string TryGetPriority(Process p)
{
try { return p.PriorityClass.ToString(); }
catch { return "N/A"; }
}
private void KillSelected()
{
if (SelectedProcess is null) return;
OptimizationService.KillProcess(SelectedProcess.Pid);
RefreshProcesses();
}
private void SetPriority(ProcessPriorityClass priority)
{
if (SelectedProcess is null) return;
try
{
using var proc = Process.GetProcessById(SelectedProcess.Pid);
proc.PriorityClass = priority;
}
catch { }
RefreshProcesses();
}
}
+67
View File
@@ -0,0 +1,67 @@
using System.Collections.ObjectModel;
using System.ServiceProcess;
using System.Windows.Input;
using SpdUp.Core;
using SpdUp.Models;
using SpdUp.Services;
namespace SpdUp.ViewModels;
/// <summary>
/// Service Manager ViewModel.
/// </summary>
public sealed class ServiceManagerViewModel : ObservableObject
{
private readonly AppSettings _settings;
public ObservableCollection<ServiceEntry> Services { get; } = new();
private ServiceEntry? _selectedService;
public ServiceEntry? SelectedService
{
get => _selectedService;
set => SetProperty(ref _selectedService, value);
}
public ICommand RefreshCommand { get; }
public ICommand StopServiceCommand { get; }
public ICommand StartServiceCommand { get; }
public ServiceManagerViewModel(AppSettings settings)
{
_settings = settings;
RefreshCommand = new RelayCommand(Refresh);
StopServiceCommand = new RelayCommand(StopSelected, () => SelectedService is not null);
StartServiceCommand = new RelayCommand(StartSelected, () => SelectedService is not null);
Refresh();
}
private void Refresh()
{
Services.Clear();
// Show the configured services plus all known non-critical ones
var names = new HashSet<string>(_settings.ServicesToStop, StringComparer.OrdinalIgnoreCase);
foreach (var k in ServiceManagementService.KnownNonCriticalServices.Keys)
names.Add(k);
foreach (var entry in ServiceManagementService.GetServiceList(names))
Services.Add(entry);
}
private void StopSelected()
{
if (SelectedService is null) return;
ServiceManagementService.StopService(SelectedService.ServiceName);
Refresh();
}
private void StartSelected()
{
if (SelectedService is null) return;
ServiceManagementService.StartService(SelectedService.ServiceName);
Refresh();
}
}
+91
View File
@@ -0,0 +1,91 @@
using System.Windows.Input;
using SpdUp.Core;
using SpdUp.Models;
namespace SpdUp.ViewModels;
/// <summary>
/// Settings page ViewModel.
/// </summary>
public sealed class SettingsViewModel : ObservableObject
{
private readonly AppSettings _settings;
private readonly MainViewModel _main;
public bool AutoDetectGames
{
get => _settings.AutoDetectGames;
set { _settings.AutoDetectGames = value; OnPropertyChanged(); Save(); }
}
public bool AutoBoostOnGameLaunch
{
get => _settings.AutoBoostOnGameLaunch;
set { _settings.AutoBoostOnGameLaunch = value; OnPropertyChanged(); Save(); }
}
public bool MinimizeToTray
{
get => _settings.MinimizeToTray;
set { _settings.MinimizeToTray = value; OnPropertyChanged(); Save(); }
}
public bool StartWithWindows
{
get => _settings.StartWithWindows;
set { _settings.StartWithWindows = value; OnPropertyChanged(); Save(); }
}
public bool WidgetCompactMode
{
get => _settings.WidgetCompactMode;
set { _settings.WidgetCompactMode = value; OnPropertyChanged(); Save(); }
}
public double WidgetOpacity
{
get => _settings.WidgetOpacity;
set { _settings.WidgetOpacity = value; OnPropertyChanged(); Save(); }
}
public int WidgetUpdateInterval
{
get => _settings.WidgetUpdateIntervalMs;
set { _settings.WidgetUpdateIntervalMs = value; OnPropertyChanged(); Save(); }
}
public bool WidgetClickThrough
{
get => _settings.WidgetClickThrough;
set { _settings.WidgetClickThrough = value; OnPropertyChanged(); Save(); }
}
public int MonitoringInterval
{
get => _settings.MonitoringIntervalMs;
set { _settings.MonitoringIntervalMs = value; OnPropertyChanged(); Save(); }
}
public string PingTarget
{
get => _settings.PingTarget;
set { _settings.PingTarget = value; OnPropertyChanged(); Save(); }
}
public string PreferredDns
{
get => _settings.PreferredDns;
set { _settings.PreferredDns = value; OnPropertyChanged(); Save(); }
}
public ICommand SaveCommand { get; }
public SettingsViewModel(AppSettings settings, MainViewModel main)
{
_settings = settings;
_main = main;
SaveCommand = new RelayCommand(Save);
}
private void Save() => _settings.Save();
}
+55
View File
@@ -0,0 +1,55 @@
<UserControl x:Class="SpdUp.Views.BoostView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:SpdUp.Converters">
<UserControl.Resources>
<conv:BoostButtonTextConverter x:Key="BoostText" />
<conv:BoostButtonColorConverter x:Key="BoostColor" />
<conv:BoolToVisibilityConverter x:Key="BoolVis" />
</UserControl.Resources>
<StackPanel>
<!-- BOOST Button Hero -->
<Border Style="{StaticResource Card}" Padding="40" HorizontalAlignment="Stretch">
<StackPanel HorizontalAlignment="Center">
<TextBlock Text="One-Click Game Boost" FontSize="18" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" HorizontalAlignment="Center"
Margin="0,0,0,8" />
<TextBlock Text="Frees RAM · Stops services · Sets priority · Reduces latency"
FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"
HorizontalAlignment="Center" Margin="0,0,0,20" />
<Button MinWidth="260" MinHeight="56" FontSize="18"
Style="{StaticResource PrimaryButton}"
Content="{Binding Main.IsBoosted, Converter={StaticResource BoostText}}"
Background="{Binding Main.IsBoosted, Converter={StaticResource BoostColor}}"
Command="{Binding BoostCommand}" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Main.StatusText}" FontSize="12" Margin="0,12,0,0"
Foreground="{StaticResource TextSecondaryBrush}" HorizontalAlignment="Center" />
</StackPanel>
</Border>
<!-- Quick Tools -->
<TextBlock Text="Quick Tools" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,8,0,12" />
<WrapPanel>
<Button Style="{StaticResource SecondaryButton}" Content="🧹 Clean RAM"
Command="{Binding CleanRamCommand}" Margin="0,0,8,8" MinWidth="140" />
<Button Style="{StaticResource SecondaryButton}" Content="🗑 Clean Shader Cache"
Command="{Binding CleanShadersCommand}" Margin="0,0,8,8" MinWidth="160" />
</WrapPanel>
<!-- Boost Log -->
<TextBlock Text="Boost Log" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,12,0,8" />
<Border Style="{StaticResource Card}" MaxHeight="300">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock Text="{Binding BoostLog}" FontSize="11" FontFamily="Consolas"
Foreground="{StaticResource TextSecondaryBrush}" TextWrapping="Wrap" />
</ScrollViewer>
</Border>
</StackPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class BoostView : UserControl
{
public BoostView() => InitializeComponent();
}
+147
View File
@@ -0,0 +1,147 @@
<UserControl x:Class="SpdUp.Views.DashboardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:SpdUp.Converters">
<UserControl.Resources>
<conv:PercentToColorConverter x:Key="PctColor" />
<conv:FloatFormatConverter x:Key="FmtFloat" />
<conv:BoostButtonTextConverter x:Key="BoostText" />
<conv:BoostButtonColorConverter x:Key="BoostColor" />
</UserControl.Resources>
<StackPanel>
<!-- Quick Actions -->
<Border Style="{StaticResource Card}">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="Quick Actions" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,12" />
<WrapPanel>
<Button Style="{StaticResource PrimaryButton}" Margin="0,0,10,0"
Content="{Binding Main.IsBoosted, Converter={StaticResource BoostText}}"
Background="{Binding Main.IsBoosted, Converter={StaticResource BoostColor}}"
Command="{Binding Main.BoostCommand}" MinWidth="180" />
<Button Style="{StaticResource SecondaryButton}" Content="🧹 Clean RAM"
Command="{Binding Main.CleanRamCommand}" Margin="0,0,10,0" />
<Button Style="{StaticResource SecondaryButton}" Content="🗑 Clean Shader Cache"
Command="{Binding Main.CleanShaderCacheCommand}" Margin="0,0,10,0" />
<Button Style="{StaticResource SecondaryButton}" Content="🖥 Toggle Widget"
Command="{Binding Main.ToggleWidgetCommand}" />
</WrapPanel>
</DockPanel>
</Border>
<!-- System Metrics -->
<TextBlock Text="System Overview" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,4,0,12" />
<UniformGrid Columns="3" Rows="2">
<!-- CPU -->
<Border Style="{StaticResource Card}" Margin="0,0,8,8">
<StackPanel>
<TextBlock Text="CPU Usage" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}"
Foreground="{Binding Main.CpuUsage, Converter={StaticResource PctColor}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F1}%">
<Binding Path="Main.CpuUsage" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<ProgressBar Height="6" Margin="0,8,0,0" Maximum="100"
Value="{Binding Main.CpuUsage, Mode=OneWay}"
Foreground="{Binding Main.CpuUsage, Converter={StaticResource PctColor}}"
Background="{StaticResource BgPanelBrush}" />
</StackPanel>
</Border>
<!-- RAM -->
<Border Style="{StaticResource Card}" Margin="4,0,4,8">
<StackPanel>
<TextBlock Text="RAM Usage" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}"
Foreground="{Binding Main.RamUsagePercent, Converter={StaticResource PctColor}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F1}%">
<Binding Path="Main.RamUsagePercent" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock FontSize="11" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,2,0,0">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F0} / {1:F0} MB">
<Binding Path="Main.RamUsedMB" />
<Binding Path="Main.RamTotalMB" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<ProgressBar Height="6" Margin="0,6,0,0" Maximum="100"
Value="{Binding Main.RamUsagePercent, Mode=OneWay}"
Foreground="{Binding Main.RamUsagePercent, Converter={StaticResource PctColor}}"
Background="{StaticResource BgPanelBrush}" />
</StackPanel>
</Border>
<!-- GPU -->
<Border Style="{StaticResource Card}" Margin="8,0,0,8">
<StackPanel>
<TextBlock Text="GPU Usage" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}"
Foreground="{StaticResource AccentPurpleBrush}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F1}%">
<Binding Path="Main.GpuUsage" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<ProgressBar Height="6" Margin="0,8,0,0" Maximum="100"
Value="{Binding Main.GpuUsage, Mode=OneWay}"
Foreground="{StaticResource AccentPurpleBrush}"
Background="{StaticResource BgPanelBrush}" />
</StackPanel>
</Border>
<!-- CPU Temp -->
<Border Style="{StaticResource Card}" Margin="0,0,8,0">
<StackPanel>
<TextBlock Text="CPU Temperature" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}" Foreground="{StaticResource AccentOrangeBrush}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F0}°C">
<Binding Path="Main.CpuTemp" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
<!-- GPU Temp -->
<Border Style="{StaticResource Card}" Margin="4,0,4,0">
<StackPanel>
<TextBlock Text="GPU Temperature" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}" Foreground="{StaticResource AccentOrangeBrush}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F0}°C">
<Binding Path="Main.GpuTemp" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
<!-- Ping -->
<Border Style="{StaticResource Card}" Margin="8,0,0,0">
<StackPanel>
<TextBlock Text="Network Ping" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}" Foreground="{StaticResource AccentCyanBrush}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F0} ms">
<Binding Path="Main.PingMs" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Border>
</UniformGrid>
</StackPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class DashboardView : UserControl
{
public DashboardView() => InitializeComponent();
}
+69
View File
@@ -0,0 +1,69 @@
<UserControl x:Class="SpdUp.Views.DebugLogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<!-- Toolbar -->
<Border DockPanel.Dock="Top" Style="{StaticResource Card}" Padding="12" Margin="0,0,0,8">
<DockPanel>
<TextBlock Text="🔍" VerticalAlignment="Center" FontSize="14" Margin="0,0,8,0" />
<TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}"
Width="250" VerticalAlignment="Center"
Background="{StaticResource BgPanelBrush}"
Foreground="{StaticResource TextPrimaryBrush}"
BorderBrush="{StaticResource BorderDimBrush}"
CaretBrush="{StaticResource TextPrimaryBrush}"
Padding="8,6" FontSize="12" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<CheckBox Content="Auto-Scroll" IsChecked="{Binding AutoScroll}"
Foreground="{StaticResource TextPrimaryBrush}"
VerticalAlignment="Center" Margin="0,0,12,0" />
<Button Content="📋 Copy All" Style="{StaticResource SecondaryButton}"
Command="{Binding CopyAllCommand}" Margin="0,0,8,0" />
<Button Content="🗑 Clear" Style="{StaticResource DangerButton}"
Command="{Binding ClearCommand}" />
</StackPanel>
</DockPanel>
</Border>
<!-- Log output -->
<Border Style="{StaticResource Card}" Padding="2">
<ListBox x:Name="LogList"
ItemsSource="{Binding FilteredLogLines}"
Background="#0A0A0C"
BorderThickness="0"
FontFamily="Cascadia Mono, Consolas, Courier New"
FontSize="11.5"
Foreground="{StaticResource TextSecondaryBrush}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.HorizontalScrollBarVisibility="Auto">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="10,3" />
<Setter Property="Margin" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextSecondaryBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Bd" Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1AFFFFFF" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Border>
</DockPanel>
</UserControl>
+38
View File
@@ -0,0 +1,38 @@
using System.Collections.Specialized;
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class DebugLogView : UserControl
{
public DebugLogView()
{
InitializeComponent();
// Auto-scroll to bottom when new items are added
if (LogList.ItemsSource is INotifyCollectionChanged ncc)
{
ncc.CollectionChanged += (_, _) =>
{
if (DataContext is ViewModels.DebugLogViewModel vm && vm.AutoScroll && LogList.Items.Count > 0)
{
LogList.ScrollIntoView(LogList.Items[^1]);
}
};
}
Loaded += (_, _) =>
{
if (LogList.ItemsSource is INotifyCollectionChanged ncc2)
{
ncc2.CollectionChanged += (_, _) =>
{
if (DataContext is ViewModels.DebugLogViewModel vm && vm.AutoScroll && LogList.Items.Count > 0)
{
LogList.ScrollIntoView(LogList.Items[^1]);
}
};
}
};
}
}
+26
View File
@@ -0,0 +1,26 @@
<UserControl x:Class="SpdUp.Views.GameLibraryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,12">
<Button Style="{StaticResource PrimaryButton}" Content=" Add Game"
Command="{Binding AddGameCommand}" Margin="0,0,8,0" />
<Button Style="{StaticResource DangerButton}" Content="🗑 Remove Selected"
Command="{Binding RemoveGameCommand}" />
</StackPanel>
<Border Style="{StaticResource Card}" Padding="0">
<DataGrid ItemsSource="{Binding Games}" SelectedItem="{Binding SelectedGame}"
AutoGenerateColumns="False" IsReadOnly="True" CanUserSortColumns="True"
MinHeight="300">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
<DataGridTextColumn Header="Process" Binding="{Binding ProcessName}" Width="140" />
<DataGridTextColumn Header="Source" Binding="{Binding LauncherSource}" Width="100" />
<DataGridTextColumn Header="Path" Binding="{Binding ExecutablePath}" Width="2*" />
</DataGrid.Columns>
</DataGrid>
</Border>
</DockPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class GameLibraryView : UserControl
{
public GameLibraryView() => InitializeComponent();
}
+114
View File
@@ -0,0 +1,114 @@
<UserControl x:Class="SpdUp.Views.MonitorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:SpdUp.Converters">
<UserControl.Resources>
<conv:PercentToColorConverter x:Key="PctColor" />
</UserControl.Resources>
<StackPanel>
<TextBlock Text="Live System Monitoring" FontSize="14" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,16" />
<!-- CPU -->
<Border Style="{StaticResource Card}" Margin="0,0,0,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="CPU" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" />
<ProgressBar Grid.Column="1" Height="16" Maximum="100"
Value="{Binding Main.CpuUsage, Mode=OneWay}" Margin="12,0"
Foreground="{Binding Main.CpuUsage, Converter={StaticResource PctColor}}"
Background="{StaticResource BgPanelBrush}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="16" FontWeight="Bold"
Foreground="{Binding Main.CpuUsage, Converter={StaticResource PctColor}}"
Text="{Binding Main.CpuUsage, StringFormat={}{0:F1}%}" />
</Grid>
</Border>
<!-- RAM -->
<Border Style="{StaticResource Card}" Margin="0,0,0,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="RAM" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}" />
<TextBlock FontSize="10" Foreground="{StaticResource TextSecondaryBrush}">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F0} / {1:F0} MB">
<Binding Path="Main.RamUsedMB" />
<Binding Path="Main.RamTotalMB" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<ProgressBar Grid.Column="1" Height="16" Maximum="100"
Value="{Binding Main.RamUsagePercent, Mode=OneWay}" Margin="12,0"
Foreground="{Binding Main.RamUsagePercent, Converter={StaticResource PctColor}}"
Background="{StaticResource BgPanelBrush}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="16" FontWeight="Bold"
Foreground="{Binding Main.RamUsagePercent, Converter={StaticResource PctColor}}"
Text="{Binding Main.RamUsagePercent, StringFormat={}{0:F1}%}" />
</Grid>
</Border>
<!-- GPU -->
<Border Style="{StaticResource Card}" Margin="0,0,0,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="GPU" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource TextPrimaryBrush}" VerticalAlignment="Center" />
<ProgressBar Grid.Column="1" Height="16" Maximum="100"
Value="{Binding Main.GpuUsage, Mode=OneWay}" Margin="12,0"
Foreground="{StaticResource AccentPurpleBrush}"
Background="{StaticResource BgPanelBrush}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="16" FontWeight="Bold" Foreground="{StaticResource AccentPurpleBrush}"
Text="{Binding Main.GpuUsage, StringFormat={}{0:F1}%}" />
</Grid>
</Border>
<!-- Temperatures Row -->
<UniformGrid Columns="3" Margin="0,4,0,0">
<Border Style="{StaticResource Card}" Margin="0,0,8,0">
<StackPanel>
<TextBlock Text="CPU Temperature" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}" FontSize="22"
Foreground="{StaticResource AccentOrangeBrush}"
Text="{Binding Main.CpuTemp, StringFormat={}{0:F0}°C}" />
</StackPanel>
</Border>
<Border Style="{StaticResource Card}" Margin="4,0,4,0">
<StackPanel>
<TextBlock Text="GPU Temperature" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}" FontSize="22"
Foreground="{StaticResource AccentOrangeBrush}"
Text="{Binding Main.GpuTemp, StringFormat={}{0:F0}°C}" />
</StackPanel>
</Border>
<Border Style="{StaticResource Card}" Margin="8,0,0,0">
<StackPanel>
<TextBlock Text="Network Ping" Style="{StaticResource MetricLabel}" />
<TextBlock Style="{StaticResource MetricValue}" FontSize="22"
Foreground="{StaticResource AccentCyanBrush}"
Text="{Binding Main.PingMs, StringFormat={}{0:F0} ms}" />
</StackPanel>
</Border>
</UniformGrid>
</StackPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class MonitorView : UserControl
{
public MonitorView() => InitializeComponent();
}
+31
View File
@@ -0,0 +1,31 @@
<UserControl x:Class="SpdUp.Views.ProcessManagerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,12">
<Button Style="{StaticResource SecondaryButton}" Content="🔄 Refresh"
Command="{Binding RefreshCommand}" Margin="0,0,8,0" />
<Button Style="{StaticResource DangerButton}" Content="❌ Kill Process"
Command="{Binding KillProcessCommand}" Margin="0,0,8,0" />
<Button Style="{StaticResource SecondaryButton}" Content="⬆ Set High Priority"
Command="{Binding SetHighPriorityCommand}" Margin="0,0,8,0" />
<Button Style="{StaticResource SecondaryButton}" Content="↔ Set Normal Priority"
Command="{Binding SetNormalPriorityCommand}" />
</StackPanel>
<Border Style="{StaticResource Card}" Padding="0">
<DataGrid ItemsSource="{Binding Processes}" SelectedItem="{Binding SelectedProcess}"
AutoGenerateColumns="False" IsReadOnly="True" CanUserSortColumns="True"
MinHeight="400">
<DataGrid.Columns>
<DataGridTextColumn Header="PID" Binding="{Binding Pid}" Width="70" />
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="180" />
<DataGridTextColumn Header="Window" Binding="{Binding WindowTitle}" Width="*" />
<DataGridTextColumn Header="Memory (MB)" Binding="{Binding MemoryMB, StringFormat=F1}" Width="100" />
<DataGridTextColumn Header="Priority" Binding="{Binding PriorityClass}" Width="100" />
</DataGrid.Columns>
</DataGrid>
</Border>
</DockPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class ProcessManagerView : UserControl
{
public ProcessManagerView() => InitializeComponent();
}
+28
View File
@@ -0,0 +1,28 @@
<UserControl x:Class="SpdUp.Views.ServiceManagerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,12">
<Button Style="{StaticResource SecondaryButton}" Content="🔄 Refresh"
Command="{Binding RefreshCommand}" Margin="0,0,8,0" />
<Button Style="{StaticResource DangerButton}" Content="⏹ Stop Service"
Command="{Binding StopServiceCommand}" Margin="0,0,8,0" />
<Button Style="{StaticResource PrimaryButton}" Content="▶ Start Service"
Command="{Binding StartServiceCommand}" />
</StackPanel>
<Border Style="{StaticResource Card}" Padding="0">
<DataGrid ItemsSource="{Binding Services}" SelectedItem="{Binding SelectedService}"
AutoGenerateColumns="False" IsReadOnly="True" CanUserSortColumns="True"
MinHeight="400">
<DataGrid.Columns>
<DataGridTextColumn Header="Service" Binding="{Binding ServiceName}" Width="140" />
<DataGridTextColumn Header="Display Name" Binding="{Binding DisplayName}" Width="200" />
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100" />
<DataGridTextColumn Header="Description" Binding="{Binding Description}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</Border>
</DockPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class ServiceManagerView : UserControl
{
public ServiceManagerView() => InitializeComponent();
}
+83
View File
@@ -0,0 +1,83 @@
<UserControl x:Class="SpdUp.Views.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel MaxWidth="600">
<!-- General -->
<TextBlock Text="General" FontSize="16" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,0,0,12" />
<Border Style="{StaticResource Card}">
<StackPanel>
<CheckBox Content="Auto-detect games" IsChecked="{Binding AutoDetectGames}" />
<CheckBox Content="Auto-boost when game starts" IsChecked="{Binding AutoBoostOnGameLaunch}" />
<CheckBox Content="Minimize to system tray" IsChecked="{Binding MinimizeToTray}" />
<CheckBox Content="Start with Windows" IsChecked="{Binding StartWithWindows}" />
</StackPanel>
</Border>
<!-- Monitoring -->
<TextBlock Text="Monitoring" FontSize="16" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,16,0,12" />
<Border Style="{StaticResource Card}">
<StackPanel>
<TextBlock Text="Monitoring interval (ms)" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,0,0,4" />
<TextBox Text="{Binding MonitoringInterval, UpdateSourceTrigger=PropertyChanged}" Width="120"
HorizontalAlignment="Left" />
<TextBlock Text="Ping target" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,12,0,4" />
<TextBox Text="{Binding PingTarget, UpdateSourceTrigger=PropertyChanged}" Width="200"
HorizontalAlignment="Left" />
<TextBlock Text="Preferred DNS" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,12,0,4" />
<TextBox Text="{Binding PreferredDns, UpdateSourceTrigger=PropertyChanged}" Width="200"
HorizontalAlignment="Left" />
</StackPanel>
</Border>
<!-- Widget -->
<TextBlock Text="Desktop Widget" FontSize="16" FontWeight="Bold"
Foreground="{StaticResource TextPrimaryBrush}" Margin="0,16,0,12" />
<Border Style="{StaticResource Card}">
<StackPanel>
<CheckBox Content="Compact mode" IsChecked="{Binding WidgetCompactMode}" />
<CheckBox Content="Click-through mode" IsChecked="{Binding WidgetClickThrough}" />
<TextBlock Text="Widget opacity" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,12,0,4" />
<Slider Minimum="0.2" Maximum="1.0" Value="{Binding WidgetOpacity}"
TickFrequency="0.05" IsSnapToTickEnabled="True" Width="200"
HorizontalAlignment="Left" />
<TextBlock Text="Widget update interval (ms)" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,12,0,4" />
<TextBox Text="{Binding WidgetUpdateInterval, UpdateSourceTrigger=PropertyChanged}" Width="120"
HorizontalAlignment="Left" />
</StackPanel>
</Border>
<!-- Trans flag & attribution -->
<StackPanel Margin="0,24,0,0" HorizontalAlignment="Center">
<Grid Height="24" Width="200" Margin="0,0,0,8">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0" Fill="{StaticResource TransBlueBrush}" RadiusX="4" RadiusY="4"/>
<Rectangle Grid.Row="1" Fill="{StaticResource TransPinkBrush}"/>
<Rectangle Grid.Row="2" Fill="{StaticResource TransWhiteBrush}"/>
<Rectangle Grid.Row="3" Fill="{StaticResource TransPinkBrush}"/>
<Rectangle Grid.Row="4" Fill="{StaticResource TransBlueBrush}" RadiusX="4" RadiusY="4"/>
</Grid>
<TextBlock Text="Made with ❤️ by Mandy" FontSize="12" HorizontalAlignment="Center"
Foreground="{StaticResource TextSecondaryBrush}" />
<TextBlock Text="SpdUp v1.0 Gaming Booster" FontSize="10" HorizontalAlignment="Center"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
</StackPanel>
</UserControl>
+8
View File
@@ -0,0 +1,8 @@
using System.Windows.Controls;
namespace SpdUp.Views;
public partial class SettingsView : UserControl
{
public SettingsView() => InitializeComponent();
}
+122
View File
@@ -0,0 +1,122 @@
<Window x:Class="SpdUp.Views.WidgetWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:SpdUp.Converters"
Title="SpdUp Widget"
Width="220" SizeToContent="Height"
WindowStyle="None" AllowsTransparency="True"
Background="Transparent" Topmost="True"
ShowInTaskbar="False" ResizeMode="NoResize"
MouseLeftButtonDown="Widget_MouseLeftButtonDown"
MouseRightButtonDown="Widget_MouseRightButtonDown">
<Window.Resources>
<conv:PercentToColorConverter x:Key="PctColor" />
<conv:BoolToVisibilityConverter x:Key="BoolVis" />
</Window.Resources>
<Border x:Name="RootBorder" CornerRadius="14" Padding="14"
BorderBrush="#33FFFFFF" BorderThickness="1">
<Border.Background>
<SolidColorBrush Color="#16161A" Opacity="0.92" />
</Border.Background>
<StackPanel>
<!-- Header -->
<DockPanel Margin="0,0,0,8">
<TextBlock Text="⚡ SpdUp" FontSize="12" FontWeight="Bold"
Foreground="#30D158" VerticalAlignment="Center" />
<Button DockPanel.Dock="Right" Content="✕" FontSize="10" Cursor="Hand"
Click="Close_Click" HorizontalAlignment="Right" Padding="4,2"
Background="Transparent" Foreground="#8B8B96" BorderThickness="0" />
<Button DockPanel.Dock="Right" Content="⊟" FontSize="10" Cursor="Hand"
Click="Toggle_Click" HorizontalAlignment="Right" Padding="4,2" Margin="0,0,4,0"
Background="Transparent" Foreground="#8B8B96" BorderThickness="0" />
</DockPanel>
<!-- Compact metrics -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- CPU -->
<StackPanel Grid.Row="0" Grid.Column="0" Margin="0,2">
<TextBlock Text="CPU" FontSize="9" Foreground="#8B8B96" />
<TextBlock FontSize="16" FontWeight="Bold"
Foreground="{Binding CpuUsage, Converter={StaticResource PctColor}}"
Text="{Binding CpuUsage, StringFormat={}{0:F0}%}" />
</StackPanel>
<!-- RAM -->
<StackPanel Grid.Row="0" Grid.Column="1" Margin="0,2">
<TextBlock Text="RAM" FontSize="9" Foreground="#8B8B96" />
<TextBlock FontSize="16" FontWeight="Bold"
Foreground="{Binding RamUsagePercent, Converter={StaticResource PctColor}}"
Text="{Binding RamUsagePercent, StringFormat={}{0:F0}%}" />
</StackPanel>
<!-- GPU -->
<StackPanel Grid.Row="1" Grid.Column="0" Margin="0,2">
<TextBlock Text="GPU" FontSize="9" Foreground="#8B8B96" />
<TextBlock FontSize="16" FontWeight="Bold" Foreground="#BF5AF2"
Text="{Binding GpuUsage, StringFormat={}{0:F0}%}" />
</StackPanel>
<!-- Ping -->
<StackPanel Grid.Row="1" Grid.Column="1" Margin="0,2">
<TextBlock Text="PING" FontSize="9" Foreground="#8B8B96" />
<TextBlock FontSize="16" FontWeight="Bold" Foreground="#64D2FF"
Text="{Binding PingMs, StringFormat={}{0:F0}ms}" />
</StackPanel>
<!-- Expanded: Temperatures -->
<StackPanel x:Name="ExpandedPanel" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,6,0,0" Visibility="Collapsed">
<Separator Background="#2E2E38" Margin="0,0,0,6" />
<DockPanel>
<TextBlock Text="CPU Temp" FontSize="9" Foreground="#8B8B96" VerticalAlignment="Center" />
<TextBlock DockPanel.Dock="Right" FontSize="13" FontWeight="Bold"
Foreground="#FF9F0A" HorizontalAlignment="Right"
Text="{Binding CpuTemp, StringFormat={}{0:F0}°C}" />
</DockPanel>
<DockPanel Margin="0,3,0,0">
<TextBlock Text="GPU Temp" FontSize="9" Foreground="#8B8B96" VerticalAlignment="Center" />
<TextBlock DockPanel.Dock="Right" FontSize="13" FontWeight="Bold"
Foreground="#FF9F0A" HorizontalAlignment="Right"
Text="{Binding GpuTemp, StringFormat={}{0:F0}°C}" />
</DockPanel>
<DockPanel Margin="0,3,0,0">
<TextBlock Text="RAM" FontSize="9" Foreground="#8B8B96" VerticalAlignment="Center" />
<TextBlock DockPanel.Dock="Right" FontSize="11" Foreground="#8B8B96"
HorizontalAlignment="Right">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:F0}/{1:F0} MB">
<Binding Path="RamUsedMB" />
<Binding Path="RamTotalMB" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DockPanel>
</StackPanel>
</Grid>
<!-- Boost indicator -->
<Border Margin="0,6,0,0" Padding="4,2" CornerRadius="4"
Visibility="{Binding IsBoosted, Converter={StaticResource BoolVis},
FallbackValue=Collapsed}">
<Border.Background>
<SolidColorBrush Color="#30D158" Opacity="0.2" />
</Border.Background>
<TextBlock Text="🚀 BOOST ACTIVE" FontSize="9" FontWeight="Bold"
Foreground="#30D158" HorizontalAlignment="Center" />
</Border>
</StackPanel>
</Border>
</Window>
+83
View File
@@ -0,0 +1,83 @@
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using SpdUp.ViewModels;
namespace SpdUp.Views;
/// <summary>
/// Floating desktop widget borderless, transparent, always-on-top.
/// </summary>
public partial class WidgetWindow : Window
{
private bool _expanded;
// ── Win32 click-through support ───────────────────────────────────
private const int GWL_EXSTYLE = -20;
private const int WS_EX_TRANSPARENT = 0x00000020;
private const int WS_EX_TOOLWINDOW = 0x00000080;
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hwnd, int index);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
public WidgetWindow(MainViewModel vm)
{
InitializeComponent();
DataContext = vm;
// Restore position from settings
Left = vm.Settings.WidgetOpacity > 0 ? 100 : 100; // default
Top = 100;
Loaded += (_, _) =>
{
// Make it a tool window (no taskbar entry)
var hwnd = new WindowInteropHelper(this).Handle;
var exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOOLWINDOW);
Opacity = vm.Settings.WidgetOpacity;
if (!vm.Settings.WidgetCompactMode)
ToggleExpand();
};
}
private void Widget_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
DragMove();
}
private void Widget_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
ToggleExpand();
}
private void Toggle_Click(object sender, RoutedEventArgs e) => ToggleExpand();
private void Close_Click(object sender, RoutedEventArgs e) => Close();
private void ToggleExpand()
{
_expanded = !_expanded;
ExpandedPanel.Visibility = _expanded ? Visibility.Visible : Visibility.Collapsed;
}
/// <summary>
/// Enable click-through: all mouse events pass through to windows below.
/// </summary>
public void SetClickThrough(bool enable)
{
var hwnd = new WindowInteropHelper(this).Handle;
var exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
if (enable)
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT);
else
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT);
}
}
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="SpdUp.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
</application>
</compatibility>
</assembly>
+86
View File
@@ -0,0 +1,86 @@
<#
.SYNOPSIS
Build & package SpdUp Gaming Booster
.DESCRIPTION
1. Publishes the app as self-contained (win-x64)
2. Optionally compiles the Inno Setup installer into a single .exe
.EXAMPLE
.\build-installer.ps1 # Publish only
.\build-installer.ps1 -Installer # Publish + compile installer
#>
param(
[switch]$Installer,
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
$root = $PSScriptRoot
$proj = Join-Path $root "SpdUp.csproj"
$pubDir = Join-Path $root "publish\SpdUp"
$issFile = Join-Path $root "Installer\SpdUpSetup.iss"
# ── Step 1: Clean previous publish ────────────────────────
Write-Host "`n=== Cleaning previous publish ===" -ForegroundColor Cyan
if (Test-Path $pubDir) {
Remove-Item $pubDir -Recurse -Force
}
# ── Step 2: Publish self-contained ────────────────────────
Write-Host "`n=== Publishing SpdUp (self-contained, win-x64, $Configuration) ===" -ForegroundColor Cyan
dotnet publish $proj `
-c $Configuration `
-r win-x64 `
--self-contained true `
-o $pubDir
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet publish failed with exit code $LASTEXITCODE"
exit 1
}
$files = Get-ChildItem $pubDir -Recurse -File
$totalMB = [math]::Round(($files | Measure-Object -Property Length -Sum).Sum / 1MB, 1)
Write-Host "`nPublished $($files.Count) files ($totalMB MB) to: $pubDir" -ForegroundColor Green
# ── Step 3 (optional): Compile Inno Setup installer ──────
if ($Installer) {
Write-Host "`n=== Compiling Inno Setup installer ===" -ForegroundColor Cyan
# Try to find ISCC.exe (Inno Setup Compiler)
$isccPaths = @(
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles}\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 5\ISCC.exe"
)
$iscc = $isccPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $iscc) {
Write-Host ""
Write-Warning "Inno Setup not found! Install it from: https://jrsoftware.org/isdl.php"
Write-Host "After installing, run this script again with -Installer" -ForegroundColor Yellow
Write-Host "`nAlternatively, open '$issFile' directly in Inno Setup and click Compile." -ForegroundColor Yellow
exit 1
}
Write-Host "Using: $iscc"
& $iscc $issFile
if ($LASTEXITCODE -ne 0) {
Write-Error "Inno Setup compilation failed with exit code $LASTEXITCODE"
exit 1
}
$setupExe = Get-ChildItem (Join-Path $root "publish") -Filter "SpdUp_Setup_*.exe" | Select-Object -First 1
if ($setupExe) {
$sizeMB = [math]::Round($setupExe.Length / 1MB, 1)
Write-Host "`nInstaller created: $($setupExe.FullName) ($sizeMB MB)" -ForegroundColor Green
}
}
else {
Write-Host "`n=== Skipping installer (use -Installer to compile setup.exe) ===" -ForegroundColor Yellow
Write-Host "To create the installer, either:"
Write-Host " 1. Run: .\build-installer.ps1 -Installer"
Write-Host " 2. Install Inno Setup 6, open '$issFile', click Compile"
}
Write-Host "`nDone!" -ForegroundColor Green
+960
View File
@@ -0,0 +1,960 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SpdUp Gaming Booster for Windows</title>
<meta name="description" content="SpdUp is a free, open-source Windows gaming booster. Optimize CPU, RAM, GPU, network and services with one click. Built with love by Mandy.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* ═══ RESET & BASE ════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #08080a;
color: #e0e0e0;
line-height: 1.6;
overflow-x: hidden;
}
a { color: #30d158; text-decoration: none; transition: color .2s; }
a:hover { color: #5bea7d; }
img { max-width: 100%; display: block; }
/* ═══ UTILITIES ═══════════════════════════════════════ */
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.section { padding: 100px 0; }
.text-center { text-align: center; }
.text-gradient {
background: linear-gradient(135deg, #30d158 0%, #0af5f0 50%, #a78bfa 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.badge {
display: inline-block;
padding: 6px 16px;
border-radius: 100px;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.badge-green { background: rgba(48,209,88,.12); color: #30d158; border: 1px solid rgba(48,209,88,.25); }
.badge-cyan { background: rgba(10,245,240,.10); color: #0af5f0; border: 1px solid rgba(10,245,240,.2); }
.badge-purple{ background: rgba(167,139,250,.10);color: #a78bfa; border: 1px solid rgba(167,139,250,.2); }
/* ═══ NAVIGATION ══════════════════════════════════════ */
.navbar {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
padding: 16px 0;
background: rgba(8,8,10,.8);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255,255,255,.06);
transition: padding .3s;
}
.navbar.scrolled { padding: 10px 0; }
.navbar .container { display: flex; align-items: center; justify-content: space-between; }
.nav-logo {
font-size: 1.4rem; font-weight: 800;
display: flex; align-items: center; gap: 8px;
}
.nav-logo .bolt { color: #30d158; font-size: 1.5rem; }
.nav-links { display: flex; gap: 32px; list-style: none; }
.nav-links a {
color: #9ca3af; font-size: .9rem; font-weight: 500;
transition: color .2s;
}
.nav-links a:hover { color: #fff; }
.nav-cta {
padding: 8px 20px; border-radius: 8px;
background: #30d158; color: #000 !important;
font-weight: 600; font-size: .9rem;
transition: transform .2s, box-shadow .2s;
}
.nav-cta:hover {
transform: translateY(-1px);
box-shadow: 0 0 24px rgba(48,209,88,.35);
color: #000 !important;
}
.mobile-toggle { display: none; background: none; border: none; color: #fff; font-size: 1.5rem; cursor: pointer; }
/* ═══ HERO ════════════════════════════════════════════ */
.hero {
position: relative;
padding: 180px 0 120px;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute; top: -50%; left: -50%;
width: 200%; height: 200%;
background: radial-gradient(ellipse at 50% 0%, rgba(48,209,88,.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 50%, rgba(10,245,240,.05) 0%, transparent 40%),
radial-gradient(ellipse at 20% 80%, rgba(167,139,250,.05) 0%, transparent 40%);
animation: heroGlow 8s ease-in-out infinite alternate;
}
@keyframes heroGlow {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(-2%, -1%) scale(1.05); }
}
.hero .container { position: relative; z-index: 1; }
.hero h1 {
font-size: clamp(3rem, 7vw, 5.5rem);
font-weight: 900;
line-height: 1.05;
letter-spacing: -0.03em;
margin-bottom: 24px;
}
.hero .subtitle {
font-size: clamp(1.1rem, 2vw, 1.35rem);
color: #9ca3af;
max-width: 600px;
margin: 0 auto 40px;
}
.hero-buttons { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
.btn {
display: inline-flex; align-items: center; gap: 8px;
padding: 14px 32px;
border-radius: 12px;
font-size: 1rem; font-weight: 600;
cursor: pointer; border: none;
transition: transform .2s, box-shadow .3s;
}
.btn:hover { transform: translateY(-2px); }
.btn-primary {
background: linear-gradient(135deg, #30d158, #28b84c);
color: #000;
box-shadow: 0 4px 24px rgba(48,209,88,.25);
}
.btn-primary:hover { box-shadow: 0 8px 40px rgba(48,209,88,.4); color: #000; }
.btn-outline {
background: transparent;
color: #e0e0e0;
border: 1.5px solid rgba(255,255,255,.15);
}
.btn-outline:hover { border-color: rgba(255,255,255,.35); color: #fff; }
.hero-stats {
display: flex; justify-content: center; gap: 48px;
margin-top: 64px; flex-wrap: wrap;
}
.hero-stat { text-align: center; }
.hero-stat .num {
font-size: 2.5rem; font-weight: 800;
font-family: 'JetBrains Mono', monospace;
}
.hero-stat .label { font-size: .85rem; color: #6b7280; margin-top: 4px; }
/* ═══ APP PREVIEW (CSS MOCKUP) ════════════════════════ */
.app-preview-section { padding: 40px 0 100px; }
.app-window {
max-width: 1000px; margin: 0 auto;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255,255,255,.08);
box-shadow: 0 40px 100px rgba(0,0,0,.6), 0 0 80px rgba(48,209,88,.06);
background: #0d0d0f;
}
.app-titlebar {
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; height: 36px;
background: #16161a;
border-bottom: 1px solid rgba(255,255,255,.06);
}
.app-titlebar-title { font-size: .75rem; color: #9ca3af; }
.app-titlebar-title span { color: #30d158; }
.app-titlebar-btns { display: flex; gap: 2px; }
.app-titlebar-btns div {
width: 36px; height: 26px;
display: flex; align-items: center; justify-content: center;
font-size: .7rem; color: #6b7280; border-radius: 4px;
cursor: default;
}
.app-titlebar-btns div:hover { background: rgba(255,255,255,.08); }
.app-titlebar-btns div:last-child:hover { background: #e81123; color: #fff; }
.app-body { display: flex; min-height: 450px; }
.app-sidebar {
width: 200px; flex-shrink: 0;
background: #16161a;
border-right: 1px solid rgba(255,255,255,.06);
padding: 16px 0;
}
.app-sidebar-brand { padding: 0 16px 12px; font-size: 1.1rem; font-weight: 700; color: #30d158; }
.app-sidebar-sub { font-size: .65rem; color: #6b7280; font-weight: 400; }
.app-sidebar-section { font-size: .6rem; font-weight: 700; color: #4b5563; padding: 16px 16px 6px; letter-spacing: .08em; text-transform: uppercase; }
.app-sidebar-item {
display: flex; align-items: center; gap: 10px;
padding: 8px 16px; font-size: .8rem; color: #9ca3af;
cursor: default; transition: background .15s;
}
.app-sidebar-item:hover { background: rgba(255,255,255,.04); }
.app-sidebar-item.active { background: rgba(48,209,88,.08); color: #30d158; }
.app-sidebar-item .icon { font-size: .9rem; width: 20px; text-align: center; }
.app-sidebar-flag {
display: flex; flex-direction: column; padding: 12px 16px;
margin-top: auto;
}
.trans-flag-bar { height: 3px; }
.trans-flag-bar:nth-child(1) { background: #5bcefa; border-radius: 2px 2px 0 0; }
.trans-flag-bar:nth-child(2) { background: #f5a9b8; }
.trans-flag-bar:nth-child(3) { background: #fff; }
.trans-flag-bar:nth-child(4) { background: #f5a9b8; }
.trans-flag-bar:nth-child(5) { background: #5bcefa; border-radius: 0 0 2px 2px; }
.app-sidebar-credit { font-size: .6rem; color: #4b5563; text-align: center; margin-top: 6px; }
.app-main { flex: 1; padding: 24px; overflow: hidden; }
.app-main-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 20px;
}
.app-main-title { font-size: 1.2rem; font-weight: 700; }
.app-metrics { display: flex; gap: 16px; font-size: .75rem; font-family: 'JetBrains Mono', monospace; }
.app-metrics span { color: #6b7280; }
.metric-val-cyan { color: #0af5f0 !important; }
.metric-val-green { color: #30d158 !important; }
.metric-val-purple { color: #a78bfa !important; }
.metric-val-orange { color: #ff9f0a !important; }
/* Dashboard cards inside mockup */
.dash-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
.dash-card {
background: #1e1e24; border-radius: 10px; padding: 16px;
border: 1px solid rgba(255,255,255,.05);
}
.dash-card .label { font-size: .65rem; color: #6b7280; text-transform: uppercase; letter-spacing: .06em; margin-bottom: 4px; }
.dash-card .value { font-size: 1.5rem; font-weight: 700; font-family: 'JetBrains Mono', monospace; }
.boost-card {
background: linear-gradient(135deg, rgba(48,209,88,.1), rgba(10,245,240,.05));
border-radius: 12px; padding: 20px;
border: 1px solid rgba(48,209,88,.15);
display: flex; align-items: center; gap: 20px;
}
.boost-btn-mock {
width: 64px; height: 64px; border-radius: 50%;
background: linear-gradient(135deg, #30d158, #28b84c);
display: flex; align-items: center; justify-content: center;
font-size: 1.6rem; color: #000; flex-shrink: 0;
box-shadow: 0 0 24px rgba(48,209,88,.3);
}
.boost-info .boost-title { font-size: 1rem; font-weight: 700; color: #30d158; }
.boost-info .boost-desc { font-size: .75rem; color: #9ca3af; margin-top: 4px; }
.boost-steps { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.boost-step { font-size: .6rem; padding: 3px 8px; border-radius: 4px; background: rgba(48,209,88,.1); color: #30d158; }
/* ═══ FEATURES ════════════════════════════════════════ */
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 24px;
margin-top: 56px;
}
.feature-card {
background: linear-gradient(135deg, rgba(22,22,26,.8), rgba(30,30,36,.6));
border: 1px solid rgba(255,255,255,.06);
border-radius: 16px;
padding: 32px;
transition: transform .3s, border-color .3s, box-shadow .3s;
position: relative;
overflow: hidden;
}
.feature-card::before {
content: '';
position: absolute; top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
opacity: 0;
transition: opacity .3s;
}
.feature-card:hover {
transform: translateY(-4px);
border-color: rgba(255,255,255,.12);
box-shadow: 0 20px 60px rgba(0,0,0,.3);
}
.feature-card:hover::before { opacity: 1; }
.feature-icon {
width: 48px; height: 48px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 1.4rem;
margin-bottom: 20px;
}
.feature-card h3 { font-size: 1.15rem; font-weight: 700; margin-bottom: 10px; color: #fff; }
.feature-card p { font-size: .9rem; color: #9ca3af; line-height: 1.7; }
/* ═══ SCREENSHOT SECTION ══════════════════════════════ */
.screenshots { background: #0c0c0e; }
.screenshot-tabs {
display: flex; justify-content: center; gap: 8px;
margin-bottom: 48px; flex-wrap: wrap;
}
.screenshot-tab {
padding: 10px 24px; border-radius: 100px;
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.08);
color: #9ca3af; font-size: .85rem; font-weight: 500;
cursor: pointer; transition: all .2s;
}
.screenshot-tab:hover { background: rgba(255,255,255,.08); color: #fff; }
.screenshot-tab.active {
background: rgba(48,209,88,.1);
border-color: rgba(48,209,88,.3);
color: #30d158;
}
.screenshot-display {
max-width: 900px; margin: 0 auto;
border-radius: 12px; overflow: hidden;
border: 1px solid rgba(255,255,255,.08);
box-shadow: 0 30px 80px rgba(0,0,0,.5);
background: #0d0d0f;
}
.screenshot-placeholder {
aspect-ratio: 16/9;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: linear-gradient(135deg, #0d0d0f, #16161a);
color: #4b5563;
font-size: .9rem;
position: relative; overflow: hidden;
}
.screenshot-placeholder .icon { font-size: 3rem; margin-bottom: 12px; opacity: .5; }
.screenshot-placeholder .hint { font-size: .75rem; margin-top: 8px; color: #374151; }
.screenshot-placeholder img {
position: absolute; inset: 0;
width: 100%; height: 100%; object-fit: cover;
}
/* ═══ HOW IT WORKS ════════════════════════════════════ */
.steps-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
margin-top: 56px;
}
.step-card {
text-align: center;
padding: 40px 24px;
border-radius: 16px;
background: rgba(22,22,26,.5);
border: 1px solid rgba(255,255,255,.05);
position: relative;
}
.step-num {
width: 56px; height: 56px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 1.3rem; font-weight: 800;
margin: 0 auto 20px;
font-family: 'JetBrains Mono', monospace;
}
.step-card h3 { font-size: 1.1rem; font-weight: 700; margin-bottom: 10px; color: #fff; }
.step-card p { font-size: .88rem; color: #9ca3af; }
/* ═══ TECH SPECS ══════════════════════════════════════ */
.specs-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 48px;
}
.spec-item {
display: flex; align-items: flex-start; gap: 14px;
padding: 20px;
border-radius: 12px;
background: rgba(22,22,26,.4);
border: 1px solid rgba(255,255,255,.04);
}
.spec-icon {
width: 40px; height: 40px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 1.1rem; flex-shrink: 0;
background: rgba(48,209,88,.08);
}
.spec-item h4 { font-size: .95rem; font-weight: 600; color: #fff; margin-bottom: 4px; }
.spec-item p { font-size: .82rem; color: #6b7280; }
/* ═══ DOWNLOAD / CTA ══════════════════════════════════ */
.download-section {
position: relative;
overflow: hidden;
}
.download-section::before {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse at center, rgba(48,209,88,.06) 0%, transparent 60%);
}
.download-box {
position: relative;
max-width: 700px; margin: 0 auto;
padding: 56px 48px;
border-radius: 24px;
background: linear-gradient(135deg, rgba(22,22,26,.9), rgba(30,30,36,.7));
border: 1px solid rgba(48,209,88,.15);
text-align: center;
box-shadow: 0 40px 100px rgba(0,0,0,.4);
}
.download-box h2 { font-size: 2.2rem; font-weight: 800; margin-bottom: 12px; }
.download-box p { color: #9ca3af; margin-bottom: 32px; font-size: 1.05rem; }
.download-info {
display: flex; justify-content: center; gap: 32px;
margin-top: 24px; font-size: .82rem; color: #6b7280;
}
.download-info span { display: flex; align-items: center; gap: 6px; }
/* ═══ FOOTER ══════════════════════════════════════════ */
footer {
padding: 48px 0 32px;
border-top: 1px solid rgba(255,255,255,.05);
}
.footer-content {
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 16px;
}
.footer-brand { display: flex; align-items: center; gap: 10px; }
.footer-brand .name { font-weight: 700; font-size: 1.1rem; }
.footer-brand .bolt { color: #30d158; }
.footer-links { display: flex; gap: 24px; }
.footer-links a { color: #6b7280; font-size: .85rem; }
.footer-links a:hover { color: #e0e0e0; }
.footer-bottom {
text-align: center; margin-top: 32px;
padding-top: 24px;
border-top: 1px solid rgba(255,255,255,.04);
}
.trans-flag-inline {
display: inline-flex; gap: 2px;
vertical-align: middle; margin: 0 6px;
height: 12px; border-radius: 3px; overflow: hidden;
}
.trans-flag-inline span { width: 10px; }
.trans-flag-inline .tf-blue { background: #5bcefa; }
.trans-flag-inline .tf-pink { background: #f5a9b8; }
.trans-flag-inline .tf-white { background: #fff; }
/* ═══ ANIMATIONS ══════════════════════════════════════ */
.reveal {
opacity: 0;
transform: translateY(32px);
transition: opacity .7s ease, transform .7s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* ═══ RESPONSIVE ══════════════════════════════════════ */
@media (max-width: 900px) {
.steps-grid { grid-template-columns: 1fr; max-width: 400px; margin-left: auto; margin-right: auto; }
.dash-cards { grid-template-columns: repeat(2, 1fr); }
.app-sidebar { width: 160px; }
}
@media (max-width: 768px) {
.nav-links { display: none; }
.mobile-toggle { display: block; }
.nav-links.open {
display: flex; flex-direction: column;
position: absolute; top: 100%; left: 0; right: 0;
background: rgba(8,8,10,.95);
backdrop-filter: blur(20px);
padding: 16px 24px;
border-bottom: 1px solid rgba(255,255,255,.06);
}
.hero { padding: 140px 0 80px; }
.hero-stats { gap: 24px; }
.features-grid { grid-template-columns: 1fr; }
.app-body { flex-direction: column; }
.app-sidebar { width: 100%; border-right: none; border-bottom: 1px solid rgba(255,255,255,.06); padding: 8px 0; }
.app-sidebar-section, .app-sidebar-flag, .app-sidebar-credit { display: none; }
.app-sidebar-item { display: inline-flex; padding: 6px 12px; font-size: .7rem; }
.download-box { padding: 40px 24px; }
}
</style>
</head>
<body>
<!-- ═══ NAVBAR ═════════════════════════════════════════════════ -->
<nav class="navbar" id="navbar">
<div class="container">
<a href="#" class="nav-logo"><span class="bolt"></span> SpdUp</a>
<ul class="nav-links" id="navLinks">
<li><a href="#features">Features</a></li>
<li><a href="#preview">Preview</a></li>
<li><a href="#how-it-works">How it Works</a></li>
<li><a href="#specs">Tech</a></li>
<li><a href="#download" class="nav-cta">Download</a></li>
</ul>
<button class="mobile-toggle" id="mobileToggle" aria-label="Toggle navigation"></button>
</div>
</nav>
<!-- ═══ HERO ══════════════════════════════════════════════════ -->
<section class="hero text-center">
<div class="container">
<span class="badge badge-green">Free &amp; Open Source</span>
<h1 style="margin-top:20px">
Boost Your<br><span class="text-gradient">Gaming Performance</span>
</h1>
<p class="subtitle">
SpdUp optimizes your Windows PC for gaming — CPU priority, RAM cleanup, GPU tweaks, network tuning, and service management. All in one click.
</p>
<div class="hero-buttons">
<a href="#download" class="btn btn-primary">⬇ Download for Windows</a>
<a href="#features" class="btn btn-outline">Learn More →</a>
</div>
<div class="hero-stats">
<div class="hero-stat">
<div class="num text-gradient">8+</div>
<div class="label">Optimization Steps</div>
</div>
<div class="hero-stat">
<div class="num" style="color:#0af5f0">0ms</div>
<div class="label">Timer Resolution</div>
</div>
<div class="hero-stat">
<div class="num" style="color:#a78bfa">1Click</div>
<div class="label">Boost Activation</div>
</div>
<div class="hero-stat">
<div class="num" style="color:#ff9f0a">100%</div>
<div class="label">Free Forever</div>
</div>
</div>
</div>
</section>
<!-- ═══ APP PREVIEW (CSS MOCKUP) ══════════════════════════════ -->
<section class="app-preview-section">
<div class="container reveal">
<div class="app-window">
<div class="app-titlebar">
<div class="app-titlebar-title"><span></span> SpdUp Gaming Booster</div>
<div class="app-titlebar-btns">
<div></div>
<div></div>
<div></div>
</div>
</div>
<div class="app-body">
<div class="app-sidebar">
<div class="app-sidebar-brand">⚡ SpdUp <span class="app-sidebar-sub">Gaming Booster</span></div>
<div class="app-sidebar-section">Main</div>
<div class="app-sidebar-item active"><span class="icon">📊</span> Dashboard</div>
<div class="app-sidebar-item"><span class="icon">🚀</span> Game Boost</div>
<div class="app-sidebar-item"><span class="icon">📈</span> System Monitor</div>
<div class="app-sidebar-section">Manage</div>
<div class="app-sidebar-item"><span class="icon">🎮</span> Game Library</div>
<div class="app-sidebar-item"><span class="icon">📋</span> Processes</div>
<div class="app-sidebar-item"><span class="icon"></span> Services</div>
<div class="app-sidebar-section">Other</div>
<div class="app-sidebar-item"><span class="icon">🖥</span> Widget</div>
<div class="app-sidebar-item"><span class="icon">🐛</span> Debug Log</div>
<div class="app-sidebar-item"><span class="icon"></span> Settings</div>
<div style="flex:1;"></div>
<div class="app-sidebar-flag">
<div class="trans-flag-bar"></div>
<div class="trans-flag-bar"></div>
<div class="trans-flag-bar"></div>
<div class="trans-flag-bar"></div>
<div class="trans-flag-bar"></div>
<div class="app-sidebar-credit">Made with ❤️ by Mandy</div>
</div>
</div>
<div class="app-main">
<div class="app-main-header">
<div class="app-main-title">Dashboard</div>
<div class="app-metrics">
<span>CPU <span class="metric-val-cyan">12%</span></span>
<span>RAM <span class="metric-val-green">54%</span></span>
<span>GPU <span class="metric-val-purple">3%</span></span>
<span>Ping <span class="metric-val-orange">14ms</span></span>
</div>
</div>
<div class="dash-cards">
<div class="dash-card">
<div class="label">CPU Usage</div>
<div class="value" style="color:#0af5f0">12<span style="font-size:.8rem;color:#6b7280">%</span></div>
</div>
<div class="dash-card">
<div class="label">RAM Usage</div>
<div class="value" style="color:#30d158">8.7<span style="font-size:.8rem;color:#6b7280"> GB</span></div>
</div>
<div class="dash-card">
<div class="label">GPU Temp</div>
<div class="value" style="color:#a78bfa">42<span style="font-size:.8rem;color:#6b7280">°C</span></div>
</div>
<div class="dash-card">
<div class="label">Network</div>
<div class="value" style="color:#ff9f0a">14<span style="font-size:.8rem;color:#6b7280">ms</span></div>
</div>
</div>
<div class="boost-card">
<div class="boost-btn-mock">🚀</div>
<div class="boost-info">
<div class="boost-title">Game Boost Ready</div>
<div class="boost-desc">Click to optimize CPU, RAM, GPU, network, and services for peak gaming performance.</div>
<div class="boost-steps">
<span class="boost-step">CPU Priority</span>
<span class="boost-step">RAM Cleanup</span>
<span class="boost-step">Timer Res</span>
<span class="boost-step">GPU Tweaks</span>
<span class="boost-step">Network</span>
<span class="boost-step">Services</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ═══ FEATURES ══════════════════════════════════════════════ -->
<section class="section" id="features">
<div class="container text-center">
<span class="badge badge-cyan">Features</span>
<h2 style="font-size:2.5rem; font-weight:800; margin-top:16px;">
Everything You Need to <span class="text-gradient">Win</span>
</h2>
<p style="color:#9ca3af; max-width:560px; margin:12px auto 0; font-size:1.05rem;">
SpdUp packs powerful optimization tools into a beautiful dark UI&nbsp;— designed for gamers who want every last frame.
</p>
<div class="features-grid">
<div class="feature-card reveal" style="--accent:#30d158">
<div class="feature-icon" style="background:rgba(48,209,88,.1); color:#30d158;">🚀</div>
<h3>One-Click Boost</h3>
<p>Activate Game Boost to set CPU priority, flush RAM, lower timer resolution, tweak GPU power profiles, optimize network settings, and stop unnecessary services — all with a single click.</p>
</div>
<div class="feature-card reveal" style="--accent:#0af5f0">
<div class="feature-icon" style="background:rgba(10,245,240,.1); color:#0af5f0;">🧹</div>
<h3>RAM Optimizer</h3>
<p>Deep RAM cleanup that goes beyond simple cache clearing: trims working sets of all processes and purges the Windows Standby List using kernel-level calls for maximum free memory.</p>
</div>
<div class="feature-card reveal" style="--accent:#a78bfa">
<div class="feature-icon" style="background:rgba(167,139,250,.1); color:#a78bfa;">📈</div>
<h3>Real-Time Monitor</h3>
<p>Live system metrics including CPU, GPU, RAM utilization, temperatures, fan speeds, and network latency — powered by LibreHardwareMonitor for accurate hardware readings.</p>
</div>
<div class="feature-card reveal" style="--accent:#ff9f0a">
<div class="feature-icon" style="background:rgba(255,159,10,.1); color:#ff9f0a;">🌐</div>
<h3>Network Optimizer</h3>
<p>Applies TCP registry tweaks (Nagle's algorithm, receive window auto-tuning) and monitors ping latency in real-time. Designed for low-latency competitive gaming.</p>
</div>
<div class="feature-card reveal" style="--accent:#ff453a">
<div class="feature-icon" style="background:rgba(255,69,58,.1); color:#ff453a;">⚙️</div>
<h3>Service Manager</h3>
<p>Smart service detection identifies non-essential Windows services that consume resources. Safely disable them during gaming and restore them afterward.</p>
</div>
<div class="feature-card reveal" style="--accent:#5bcefa">
<div class="feature-icon" style="background:rgba(91,206,250,.1); color:#5bcefa;">🎮</div>
<h3>Game Detection</h3>
<p>Automatically detects when you launch a game and applies boost optimizations. Detects 30+ popular games including Steam, Epic, Riot, Blizzard, and EA titles.</p>
</div>
<div class="feature-card reveal" style="--accent:#30d158">
<div class="feature-icon" style="background:rgba(48,209,88,.1); color:#30d158;">🗑️</div>
<h3>Shader Cache Cleaner</h3>
<p>Clears DirectX, NVIDIA, AMD, and Intel shader caches to free disk space and fix rendering issues. Scans all known cache locations automatically.</p>
</div>
<div class="feature-card reveal" style="--accent:#f5a9b8">
<div class="feature-icon" style="background:rgba(245,169,184,.1); color:#f5a9b8;">📋</div>
<h3>Process Manager</h3>
<p>View and manage running processes with the ability to set CPU priority and affinity. Kill resource-hungry background processes to reclaim performance.</p>
</div>
<div class="feature-card reveal" style="--accent:#a78bfa">
<div class="feature-icon" style="background:rgba(167,139,250,.1); color:#a78bfa;">🖥️</div>
<h3>Desktop Widget</h3>
<p>Always-on-top floating widget shows CPU, RAM, GPU, and ping at a glance while you game — with a one-click boost button right on your desktop.</p>
</div>
</div>
</div>
</section>
<!-- ═══ SCREENSHOT GALLERY ════════════════════════════════════ -->
<section class="section screenshots" id="preview">
<div class="container text-center">
<span class="badge badge-purple">Preview</span>
<h2 style="font-size:2.5rem; font-weight:800; margin-top:16px;">
See It In <span class="text-gradient">Action</span>
</h2>
<p style="color:#9ca3af; max-width:500px; margin:12px auto 40px; font-size:1.02rem;">
A dark, modern UI designed for gamers who appreciate clean aesthetics.
</p>
<div class="screenshot-tabs">
<button class="screenshot-tab active" data-tab="dashboard">Dashboard</button>
<button class="screenshot-tab" data-tab="boost">Game Boost</button>
<button class="screenshot-tab" data-tab="monitor">Monitor</button>
<button class="screenshot-tab" data-tab="processes">Processes</button>
<button class="screenshot-tab" data-tab="services">Services</button>
<button class="screenshot-tab" data-tab="debug">Debug Log</button>
</div>
<div class="screenshot-display reveal">
<div class="screenshot-placeholder" id="screenshotArea">
<!-- Replace src with your actual screenshots: screenshots/dashboard.png etc. -->
<div class="icon" id="ssIcon">📊</div>
<div id="ssTitle">Dashboard View</div>
<div class="hint" id="ssHint">Place your screenshot in website/screenshots/dashboard.png</div>
</div>
</div>
</div>
</section>
<!-- ═══ HOW IT WORKS ══════════════════════════════════════════ -->
<section class="section" id="how-it-works">
<div class="container text-center">
<span class="badge badge-green">How it Works</span>
<h2 style="font-size:2.5rem; font-weight:800; margin-top:16px;">
Three Steps to <span class="text-gradient">Peak Performance</span>
</h2>
<div class="steps-grid">
<div class="step-card reveal">
<div class="step-num" style="background:rgba(48,209,88,.1); color:#30d158;">1</div>
<h3>Install &amp; Launch</h3>
<p>Run the installer and launch SpdUp. It automatically requests administrator privileges for deep system optimizations.</p>
</div>
<div class="step-card reveal">
<div class="step-num" style="background:rgba(10,245,240,.1); color:#0af5f0;">2</div>
<h3>Hit Boost</h3>
<p>Click the Boost button — SpdUp instantly optimizes CPU, RAM, GPU, network, timer resolution, and disables unnecessary services.</p>
</div>
<div class="step-card reveal">
<div class="step-num" style="background:rgba(167,139,250,.1); color:#a78bfa;">3</div>
<h3>Game On</h3>
<p>Play with maximum performance. When you're done, click Restore to bring everything back to normal. That's it.</p>
</div>
</div>
</div>
</section>
<!-- ═══ TECH SPECS ════════════════════════════════════════════ -->
<section class="section" id="specs" style="background:#0c0c0e">
<div class="container text-center">
<span class="badge badge-cyan">Under the Hood</span>
<h2 style="font-size:2.5rem; font-weight:800; margin-top:16px;">
Built with Modern <span class="text-gradient">Technology</span>
</h2>
<div class="specs-grid" style="text-align:left;">
<div class="spec-item reveal">
<div class="spec-icon">🏗️</div>
<div>
<h4>.NET 8 + WPF</h4>
<p>Built on the latest .NET runtime. Self-contained — no framework install needed.</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon">🧠</div>
<div>
<h4>MVVM Architecture</h4>
<p>Clean separation of concerns with full data binding and reactive UI updates.</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon">🔧</div>
<div>
<h4>Kernel-Level Tuning</h4>
<p>P/Invoke calls to NtSetTimerResolution, EmptyWorkingSet, and standby list purge.</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon">🌡️</div>
<div>
<h4>Hardware Monitoring</h4>
<p>LibreHardwareMonitor integration for real CPU/GPU temps, fan speeds, and clocks.</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon">🛡️</div>
<div>
<h4>Admin Privileges</h4>
<p>Runs elevated with proper manifest for safe access to protected system APIs.</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon">🎨</div>
<div>
<h4>Custom Dark Theme</h4>
<p>Full custom chrome, styled scrollbars, buttons, and data grids. No Windows chrome.</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon">📦</div>
<div>
<h4>Single Installer</h4>
<p>Inno Setup packages everything into one .exe — install, run, done.</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon">📝</div>
<div>
<h4>Debug Logging</h4>
<p>Built-in debug log with live filtering, copy, and file logging for troubleshooting.</p>
</div>
</div>
</div>
</div>
</section>
<!-- ═══ SYSTEM REQUIREMENTS ═══════════════════════════════════ -->
<section class="section">
<div class="container text-center">
<h2 style="font-size:2rem; font-weight:800; margin-bottom:40px;">System Requirements</h2>
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:20px; max-width:800px; margin:0 auto; text-align:left;">
<div class="spec-item reveal">
<div class="spec-icon" style="background:rgba(10,245,240,.08);">💻</div>
<div>
<h4>Windows 10/11</h4>
<p>64-bit, version 1809 or later</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon" style="background:rgba(167,139,250,.08);">🧮</div>
<div>
<h4>4 GB RAM</h4>
<p>8 GB or more recommended</p>
</div>
</div>
<div class="spec-item reveal">
<div class="spec-icon" style="background:rgba(255,159,10,.08);">💾</div>
<div>
<h4>200 MB Disk Space</h4>
<p>For the installed application</p>
</div>
</div>
</div>
</div>
</section>
<!-- ═══ DOWNLOAD CTA ══════════════════════════════════════════ -->
<section class="section download-section" id="download">
<div class="container">
<div class="download-box reveal">
<h2>Ready to <span class="text-gradient">Boost</span>?</h2>
<p>Download SpdUp for free and unlock your PC's full gaming potential.</p>
<a href="SpdUp_Setup_1.0.0.exe" class="btn btn-primary" style="font-size:1.1rem; padding:16px 40px;">
⬇ Download SpdUp v1.0.0
</a>
<div class="download-info">
<span>📦 ~60 MB</span>
<span>🪟 Windows 10/11</span>
<span>🔓 No account needed</span>
<span>💚 Free forever</span>
</div>
</div>
</div>
</section>
<!-- ═══ FOOTER ════════════════════════════════════════════════ -->
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-brand">
<span class="bolt" style="color:#30d158; font-size:1.3rem;"></span>
<span class="name">SpdUp</span>
</div>
<div class="footer-links">
<a href="#features">Features</a>
<a href="#preview">Preview</a>
<a href="#download">Download</a>
</div>
</div>
<div class="footer-bottom">
<p style="color:#4b5563; font-size:.85rem;">
Made with ❤️ by Mandy
<span class="trans-flag-inline">
<span class="tf-blue"></span>
<span class="tf-pink"></span>
<span class="tf-white"></span>
<span class="tf-pink"></span>
<span class="tf-blue"></span>
</span>
</p>
<p style="color:#374151; font-size:.75rem; margin-top:8px;">© 2026 SpdUp. Free &amp; open source.</p>
</div>
</div>
</footer>
<script>
// ── Navbar scroll effect ─────────────────────────────────
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
navbar.classList.toggle('scrolled', window.scrollY > 40);
});
// ── Mobile menu toggle ───────────────────────────────────
const mobileToggle = document.getElementById('mobileToggle');
const navLinks = document.getElementById('navLinks');
mobileToggle.addEventListener('click', () => {
navLinks.classList.toggle('open');
});
// ── Scroll reveal ────────────────────────────────────────
const reveals = document.querySelectorAll('.reveal');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' });
reveals.forEach(el => observer.observe(el));
// ── Screenshot tab switcher ──────────────────────────────
const tabData = {
dashboard: { icon: '📊', title: 'Dashboard View', hint: 'screenshots/dashboard.png' },
boost: { icon: '🚀', title: 'Game Boost Panel', hint: 'screenshots/boost.png' },
monitor: { icon: '📈', title: 'System Monitor', hint: 'screenshots/monitor.png' },
processes: { icon: '📋', title: 'Process Manager', hint: 'screenshots/processes.png' },
services: { icon: '⚙️', title: 'Service Manager', hint: 'screenshots/services.png' },
debug: { icon: '🐛', title: 'Debug Log', hint: 'screenshots/debug.png' }
};
document.querySelectorAll('.screenshot-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.screenshot-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const key = tab.dataset.tab;
const data = tabData[key];
document.getElementById('ssIcon').textContent = data.icon;
document.getElementById('ssTitle').textContent = data.title;
document.getElementById('ssHint').textContent = 'Place your screenshot in website/' + data.hint;
// Try to load an actual screenshot image
const area = document.getElementById('screenshotArea');
const existingImg = area.querySelector('img');
if (existingImg) existingImg.remove();
const img = new Image();
img.src = data.hint;
img.alt = data.title;
img.onload = () => {
area.querySelector('.icon').style.display = 'none';
area.querySelector('#ssTitle').style.display = 'none';
area.querySelector('#ssHint').style.display = 'none';
area.appendChild(img);
};
img.onerror = () => {
area.querySelector('.icon').style.display = '';
area.querySelector('#ssTitle').style.display = '';
area.querySelector('#ssHint').style.display = '';
};
});
});
// Trigger first tab to try loading screenshot
document.querySelector('.screenshot-tab.active').click();
// ── Smooth scroll for anchor links ───────────────────────
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', e => {
e.preventDefault();
const target = document.querySelector(anchor.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
navLinks.classList.remove('open');
}
});
});
</script>
</body>
</html>