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