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:
+28
@@ -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/
|
||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
)]
|
||||||
@@ -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 (0–100) 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();
|
||||||
|
}
|
||||||
@@ -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 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using SpdUp.Core;
|
||||||
|
|
||||||
|
namespace SpdUp.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for RelayCommand and RelayCommand<T>.
|
||||||
|
/// </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!));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SpdUp.Views;
|
||||||
|
|
||||||
|
public partial class BoostView : UserControl
|
||||||
|
{
|
||||||
|
public BoostView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SpdUp.Views;
|
||||||
|
|
||||||
|
public partial class DashboardView : UserControl
|
||||||
|
{
|
||||||
|
public DashboardView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SpdUp.Views;
|
||||||
|
|
||||||
|
public partial class GameLibraryView : UserControl
|
||||||
|
{
|
||||||
|
public GameLibraryView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SpdUp.Views;
|
||||||
|
|
||||||
|
public partial class MonitorView : UserControl
|
||||||
|
{
|
||||||
|
public MonitorView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SpdUp.Views;
|
||||||
|
|
||||||
|
public partial class ProcessManagerView : UserControl
|
||||||
|
{
|
||||||
|
public ProcessManagerView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SpdUp.Views;
|
||||||
|
|
||||||
|
public partial class ServiceManagerView : UserControl
|
||||||
|
{
|
||||||
|
public ServiceManagerView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace SpdUp.Views;
|
||||||
|
|
||||||
|
public partial class SettingsView : UserControl
|
||||||
|
{
|
||||||
|
public SettingsView() => InitializeComponent();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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 & 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">1‑Click</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 — 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 & 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 & 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>
|
||||||
Reference in New Issue
Block a user