Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1d0e759
Add source refresh on startup if OBS connects
WolfwithSword Aug 22, 2026
6c799d7
Merge branch 'main' into dev
WolfwithSword Aug 26, 2026
6d77788
fix coverage script
WolfwithSword Aug 26, 2026
7ba0f45
update gitignore
WolfwithSword Aug 26, 2026
efb3d0d
add forced timeout for twitch hypetrains, start codestyle enforcement
WolfwithSword Aug 26, 2026
912ef2f
stage some formatting changes
WolfwithSword Aug 26, 2026
6404e04
stage formatting changes
WolfwithSword Aug 26, 2026
f2d1ab1
format data proj
WolfwithSword Aug 26, 2026
ebe11f6
integration formatting changes
WolfwithSword Aug 26, 2026
01a5c92
formatting changes in webserver
WolfwithSword Aug 26, 2026
cd2cbb2
services formatting changes
WolfwithSword Aug 26, 2026
3f77316
reformat tests oh god
WolfwithSword Aug 26, 2026
3e4f152
formatting ui code
WolfwithSword Aug 26, 2026
6a691c9
Test fix for twitch username check
WolfwithSword Aug 29, 2026
80d8195
Optimize websockets in widgets overlays, add presets (#323)
WolfwithSword Aug 30, 2026
d64e8c2
OBS Canvas Sync and TwitchRaid fix (#326)
WolfwithSword Aug 30, 2026
c481348
fix magic rune presets for wheel and prompt
WolfwithSword Aug 31, 2026
4f990fe
initial vts integration experimental
WolfwithSword Sep 1, 2026
3743e15
handle vts model polling due to lib bug
WolfwithSword Sep 1, 2026
e9d8999
add experimental warning featureflag for vts
WolfwithSword Sep 1, 2026
ae17f28
vts unit tests and exclusions
WolfwithSword Sep 1, 2026
01c9e04
add widget arreow key nudging
WolfwithSword Sep 1, 2026
06d7fb1
Adjust enter on save for goals to add new empty row too
WolfwithSword Sep 1, 2026
7c1e23f
add vts plugin icon
WolfwithSword Sep 2, 2026
a1b2f59
feature flag lock vts
WolfwithSword Sep 5, 2026
2139f7e
update vts setup
WolfwithSword Sep 6, 2026
e6b8c76
new vts params
WolfwithSword Sep 6, 2026
e7f84f4
add process check by name
WolfwithSword Sep 6, 2026
8b58f7c
revert discover useage as I forgot about remote setups
WolfwithSword Sep 7, 2026
11e234e
add new api for leaderboard stats
WolfwithSword Sep 9, 2026
628ab3a
optimize leaderboard query
WolfwithSword Sep 9, 2026
6f63cdb
Change goaffpro methodology as featureflag
WolfwithSword Sep 12, 2026
e5774bc
leaderboard panel
WolfwithSword Sep 13, 2026
f712e5b
db safety and wheelspin queuing
WolfwithSword Sep 13, 2026
23d91ab
make pause/lock button state more apparent on home page
WolfwithSword Sep 13, 2026
4749b04
fix tests
WolfwithSword Sep 13, 2026
f692cd8
add better hints for devtunnel connected status
WolfwithSword Sep 17, 2026
412e616
adjust window opening behaviour
WolfwithSword Sep 19, 2026
dea1c39
allow main window to be resized
WolfwithSword Sep 19, 2026
5a91999
allow drag ordering goals
WolfwithSword Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/scripts/coverage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ try {
Remove-Item -Path "SubathonManager.Tests\TestResults\*" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -Path "coverage-local" -Recurse -Force -ErrorAction SilentlyContinue

dotnet test SubathonManager.Tests\SubathonManager.Tests.csproj `
dotnet test --project SubathonManager.Tests\SubathonManager.Tests.csproj `
--results-directory coverage-local `
--coverage `
--coverage-output-format cobertura `
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ jobs:
-p:FileVersion=${{ steps.avars.outputs.file_version }}
-p:AssemblyVersion=${{ steps.avars.outputs.file_version }}
-p:InformationalVersion=${{ steps.avars.outputs.version }}
-p:Copyright="© 2025–${{ env.CopyrightYear }} WolfwithSword and contributors"
-p:Copyright="© 2025-${{ env.CopyrightYear }} WolfwithSword and contributors"
-o build-avalonia

- name: Copy presets, resources folder and license into build output
Expand Down Expand Up @@ -611,7 +611,7 @@ jobs:
else
LINK="-"
fi
echo "| \`${RUNTIME}\` | $(icon "$STATUS") | ${STORAGE:-–} | ${LINK} |"
echo "| \`${RUNTIME}\` | $(icon "$STATUS") | ${STORAGE:--} | ${LINK} |"
done
if [ "$found" -eq 0 ]; then
echo "| - | no platform builds ran | - | - |"
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ presets/**
!.idea/inspectionProfiles/
!.idea/runConfigurations/

*cobertura.xml

resources/HONSE/*

## Ignore Visual Studio temporary files, build results, and
Expand Down Expand Up @@ -227,6 +229,7 @@ obj

TestResults/
coverage-report/
coverage-local/

# Stream Deck plugin packaging stuff
*.streamDeckPlugin
Expand Down
200 changes: 94 additions & 106 deletions SubathonManager.Core/AppServices.cs
Original file line number Diff line number Diff line change
@@ -1,128 +1,116 @@
using Updatum;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace SubathonManager.Core {

public static class AppServices
{
public static IServiceProvider Provider { get; set; } = default!;

public static string AppVersion
{
get
{
var ver = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? "dev";

var plusIndex = ver.IndexOf('+');
return plusIndex > 0 && ver.StartsWith('v') ? ver[..plusIndex] : ver;
}
}
using Updatum;

namespace SubathonManager.Core;

public static class AppServices {
private static readonly UpdatumManager AppUpdater = new("WolfwithSword", "SubathonManager") {
AssetExtensionFilter = "zip",
CurrentVersion = GetVersion(),
FetchOnlyLatestRelease = true,
AssetRegexPattern = GetAssetRegexPattern()
};

private static readonly UpdatumManager AppUpdater = new("WolfwithSword", "SubathonManager")
{
AssetExtensionFilter = "zip",
CurrentVersion = GetVersion(),
FetchOnlyLatestRelease = true,
AssetRegexPattern = GetAssetRegexPattern(),
};

private static string GetAssetRegexPattern()
{
string rid =
OperatingSystem.IsWindows() ? "win-x64" :
OperatingSystem.IsLinux() ? "linux-x64" :
OperatingSystem.IsMacOS()
? (RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "osx-arm64" : "osx-x64")
: string.Empty;

return string.IsNullOrEmpty(rid) ? string.Empty : $@"(?i).*{rid}.*\.zip$";
public static IServiceProvider Provider { get; set; } = default!;

public static string AppVersion {
get {
string ver = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? "dev";

int plusIndex = ver.IndexOf('+');
return plusIndex > 0 && ver.StartsWith('v') ? ver[..plusIndex] : ver;
}
}

private static Version GetVersion()
{
if (string.IsNullOrWhiteSpace(AppVersion))
throw new ArgumentException("Version string cannot be null or empty.", nameof(AppVersion));
private static string GetAssetRegexPattern() {
string rid =
OperatingSystem.IsWindows()
? "win-x64"
: OperatingSystem.IsLinux()
? "linux-x64"
: OperatingSystem.IsMacOS()
? RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "osx-arm64" : "osx-x64"
: string.Empty;

return string.IsNullOrEmpty(rid) ? string.Empty : $@"(?i).*{rid}.*\.zip$";
}

private static Version GetVersion() {
if (string.IsNullOrWhiteSpace(AppVersion))
throw new ArgumentException("Version string cannot be null or empty.", nameof(AppVersion));

string input = AppVersion.Trim();
if (input.StartsWith("v", StringComparison.OrdinalIgnoreCase))
input = input.Substring(1);
string input = AppVersion.Trim();
if (input.StartsWith("v", StringComparison.OrdinalIgnoreCase))
input = input.Substring(1);

var parts = input.Split('.', StringSplitOptions.RemoveEmptyEntries);
string[] parts = input.Split('.', StringSplitOptions.RemoveEmptyEntries);

int major = parts.Length > 0 && int.TryParse(parts[0], out var m) ? m : 0;
int minor = parts.Length > 1 && int.TryParse(parts[1], out var n) ? n : 0;
int build = parts.Length > 2 && int.TryParse(parts[2], out var b) ? b : 0;
int revision = parts.Length > 3 && int.TryParse(parts[3], out var r) ? r : 0;
int major = parts.Length > 0 && int.TryParse(parts[0], out int m) ? m : 0;
int minor = parts.Length > 1 && int.TryParse(parts[1], out int n) ? n : 0;
int build = parts.Length > 2 && int.TryParse(parts[2], out int b) ? b : 0;
int revision = parts.Length > 3 && int.TryParse(parts[3], out int r) ? r : 0;

return new Version(major, minor, build, revision);
}

return new Version(major, minor, build, revision);
public static async Task<(bool, string?, string?)> CheckForUpdate(ILogger? logger) {
try {
bool updateFound = await AppUpdater.CheckForUpdatesAsync();
if (AppUpdater.LatestReleaseTagVersionStr == AppVersion) updateFound = false;
if (updateFound)
logger?.LogInformation(
$"SubathonManager found an update ({AppUpdater.LatestReleaseTagVersionStr}) from current {AppVersion}." +
$" Changelog: {AppUpdater.GetChangelog()}");
return (updateFound, AppUpdater.LatestReleaseTagVersionStr,
$"{AppUpdater.RepositoryUrl}/releases/{AppUpdater.LatestReleaseTagVersionStr}");
}
catch (Exception ex) {
logger?.LogWarning(ex, "Failed to check for updates");
}

public static async Task<(bool, string?, string?)> CheckForUpdate(ILogger? logger)
{

try
{
bool updateFound = await AppUpdater.CheckForUpdatesAsync();
if (AppUpdater.LatestReleaseTagVersionStr == AppVersion) updateFound = false;
if (updateFound)
logger?.LogInformation($"SubathonManager found an update ({AppUpdater.LatestReleaseTagVersionStr}) from current {AppVersion}." +
$" Changelog: {AppUpdater.GetChangelog()}");
return (updateFound, AppUpdater.LatestReleaseTagVersionStr, $"{AppUpdater.RepositoryUrl}/releases/{AppUpdater.LatestReleaseTagVersionStr}");
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Failed to check for updates");
return (false, null, null);
}

public static async Task<UpdatumDownloadedAsset?> DownloadUpdate(ILogger? logger) {
try {
UpdatumDownloadedAsset? downloads = await AppUpdater.DownloadUpdateAsync();
if (downloads == null) {
logger?.LogError("SubathonManager found an update but failed to download.");
return null;
}
return (false, null, null);

return downloads;
}
catch (Exception ex) {
logger?.LogWarning(ex, "Failed to download update");
}

public static async Task<UpdatumDownloadedAsset?> DownloadUpdate(ILogger? logger)
{
try
{
var downloads = await AppUpdater.DownloadUpdateAsync();
if (downloads == null)
{
logger?.LogError($"SubathonManager found an update but failed to download.");
return null;
}
return null;
}

return downloads;
[ExcludeFromCodeCoverage]
public static async Task<bool> InstallUpdate(UpdatumDownloadedAsset? asset, ILogger? logger) {
if (asset == null)
return false;

}
catch (Exception ex)
{
logger?.LogWarning(ex, "Failed to download update");
}
return null;
try {
await AppUpdater.InstallUpdateAsync(asset);
return true;
}

[ExcludeFromCodeCoverage]
public static async Task<bool> InstallUpdate(UpdatumDownloadedAsset? asset, ILogger? logger)
{
if (asset == null)
return false;

try
{
await AppUpdater.InstallUpdateAsync(asset, true);
return true;
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Failed to install update");
}
return false;
catch (Exception ex) {
logger?.LogWarning(ex, "Failed to install update");
}

public static async Task<bool> DownloadAndInstall(ILogger? logger)
{
var asset = await DownloadUpdate(logger);
return await InstallUpdate(asset, logger);
}
return false;
}

public static async Task<bool> DownloadAndInstall(ILogger? logger) {
UpdatumDownloadedAsset? asset = await DownloadUpdate(logger);
return await InstallUpdate(asset, logger);
}
}
Loading
Loading