Merge branch 'main' into feature/taskbar-progressbar

This commit is contained in:
bluepilledgreat 2024-09-22 12:40:31 +01:00
commit 89fef194a0
121 changed files with 3880 additions and 2456 deletions

View File

@ -8,6 +8,7 @@ body:
### **Preliminary instructions**
- Before opening an issue, please [check the Wiki first](https://github.com/pizzaboxer/bloxstrap/wiki/) to see if your problem has been addressed there.
- If it isn't, please confirm which pages that you read that were relevant to your issue.
- Your issue ***will*** be closed without warning if there's a Wiki page addressing your problem.
- If your problem is with Roblox itself (i.e. it crashes or doesn't launch), [check to see if it happens without Bloxstrap](https://github.com/pizzaboxer/bloxstrap/wiki/Roblox-crashes-or-does-not-launch).
- Please only open an issue if your problem happens only with Bloxstrap, and state clearly that this is the case, as anything else is out of my control.
- If you are getting a Bloxstrap Exception error, please attach a copy of the provided log file. There is a button on the dialog that locates it for you.
@ -32,3 +33,10 @@ body:
description: Provide a comprehensive description of the problem you're facing. Don't forget to attach any additional resources you may have, such as log files and screenshots.
validations:
required: true
- type: textarea
id: log
attributes:
label: Bloxstrap Log
description: If you're getting a Bloxstrap Exception error, upload your log file here. Otherwise, just leave it empty.
value: "N/A"
#render: text

View File

@ -15,7 +15,11 @@ namespace Bloxstrap
public partial class App : Application
{
public const string ProjectName = "Bloxstrap";
public const string ProjectOwner = "pizzaboxer";
public const string ProjectRepository = "pizzaboxer/bloxstrap";
public const string ProjectDownloadLink = "https://bloxstraplabs.com";
public const string ProjectHelpLink = "https://github.com/pizzaboxer/bloxstrap/wiki";
public const string ProjectSupportLink = "https://github.com/pizzaboxer/bloxstrap/issues/new";
public const string RobloxPlayerAppName = "RobloxPlayerBeta";
public const string RobloxStudioAppName = "RobloxStudioBeta";
@ -29,9 +33,11 @@ namespace Bloxstrap
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
public static readonly MD5 MD5Provider = MD5.Create();
public static bool IsActionBuild => !String.IsNullOrEmpty(BuildMetadata.CommitRef);
public static NotifyIconWrapper? NotifyIcon { get; set; }
public static bool IsProductionBuild => IsActionBuild && BuildMetadata.CommitRef.StartsWith("tag", StringComparison.Ordinal);
public static readonly MD5 MD5Provider = MD5.Create();
public static readonly Logger Logger = new();
@ -49,9 +55,7 @@ namespace Bloxstrap
)
);
#if RELEASE
private static bool _showingExceptionDialog = false;
#endif
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{
@ -59,8 +63,6 @@ namespace Bloxstrap
Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");
NotifyIcon?.Dispose();
Environment.Exit(exitCodeNum);
}
@ -73,24 +75,51 @@ namespace Bloxstrap
FinalizeExceptionHandling(e.Exception);
}
public static void FinalizeExceptionHandling(Exception exception, bool log = true)
public static void FinalizeExceptionHandling(AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
Logger.WriteException("App::FinalizeExceptionHandling", innerEx);
FinalizeExceptionHandling(ex.GetBaseException(), false);
}
public static void FinalizeExceptionHandling(Exception ex, bool log = true)
{
if (log)
Logger.WriteException("App::FinalizeExceptionHandling", exception);
Logger.WriteException("App::FinalizeExceptionHandling", ex);
#if DEBUG
throw exception;
#else
if (_showingExceptionDialog)
return;
_showingExceptionDialog = true;
if (!LaunchSettings.QuietFlag.Active)
Frontend.ShowExceptionDialog(exception);
Frontend.ShowExceptionDialog(ex);
Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
#endif
}
public static async Task<GithubRelease?> GetLatestRelease()
{
const string LOG_IDENT = "App::GetLatestRelease";
try
{
var releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest");
if (releaseInfo is null || releaseInfo.Assets is null)
{
Logger.WriteLine(LOG_IDENT, "Encountered invalid data");
return null;
}
return releaseInfo;
}
catch (Exception ex)
{
Logger.WriteException(LOG_IDENT, ex);
}
return null;
}
protected override void OnStartup(StartupEventArgs e)
@ -103,10 +132,10 @@ namespace Bloxstrap
Logger.WriteLine(LOG_IDENT, $"Starting {ProjectName} v{Version}");
if (String.IsNullOrEmpty(BuildMetadata.CommitHash))
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
else
if (IsActionBuild)
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from commit {BuildMetadata.CommitHash} ({BuildMetadata.CommitRef})");
else
Logger.WriteLine(LOG_IDENT, $"Compiled {BuildMetadata.Timestamp.ToFriendlyString()} from {BuildMetadata.Machine}");
Logger.WriteLine(LOG_IDENT, $"Loaded from {Paths.Process}");
@ -162,6 +191,26 @@ namespace Bloxstrap
}
}
if (fixInstallLocation && installLocation is not null)
{
var installer = new Installer
{
InstallLocation = installLocation,
IsImplicitInstall = true
};
if (installer.CheckInstallLocation())
{
Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'");
installer.DoInstall();
}
else
{
// force reinstall
installLocation = null;
}
}
if (installLocation is null)
{
Logger.Initialize(true);
@ -169,21 +218,6 @@ namespace Bloxstrap
}
else
{
if (fixInstallLocation)
{
var installer = new Installer
{
InstallLocation = installLocation,
IsImplicitInstall = true
};
if (installer.CheckInstallLocation())
{
Logger.WriteLine(LOG_IDENT, $"Changing install location to '{installLocation}'");
installer.DoInstall();
}
}
Paths.Initialize(installLocation);
// ensure executable is in the install directory
@ -202,10 +236,6 @@ namespace Bloxstrap
State.Load();
FastFlags.Load();
// we can only parse them now as settings need
// to be loaded first to know what our channel is
// LaunchSettings.ParseRoblox();
if (!Locale.SupportedLocales.ContainsKey(Settings.Prop.Locale))
{
Settings.Prop.Locale = "nil";
@ -214,13 +244,13 @@ namespace Bloxstrap
Locale.Set(Settings.Prop.Locale);
if (!LaunchSettings.UninstallFlag.Active)
if (!LaunchSettings.BypassUpdateCheck)
Installer.HandleUpgrade();
LaunchHandler.ProcessLaunchArgs();
}
Terminate();
// you must *explicitly* call terminate when everything is done, it won't be called implicitly
}
}
}

View File

@ -39,8 +39,17 @@ namespace Bloxstrap.AppData
{ "extracontent-places.zip", @"ExtraContent\places\" },
};
public virtual string ExecutableName { get; } = null!;
public virtual string Directory { get; } = null!;
public string LockFilePath => Path.Combine(Directory, "Bloxstrap.lock");
public string ExecutablePath => Path.Combine(Directory, ExecutableName);
public virtual IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
public CommonAppData()
{
if (PackageDirectoryMap is null)

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.AppData
namespace Bloxstrap.AppData
{
internal interface IAppData
{
@ -18,6 +12,14 @@ namespace Bloxstrap.AppData
string StartEvent { get; }
string Directory { get; }
string LockFilePath { get; }
string ExecutablePath { get; }
AppState State { get; }
IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; }
}
}

View File

@ -8,15 +8,19 @@ namespace Bloxstrap.AppData
{
public class RobloxPlayerData : CommonAppData, IAppData
{
public string ProductName { get; } = "Roblox";
public string ProductName => "Roblox";
public string BinaryType { get; } = "WindowsPlayer";
public string BinaryType => "WindowsPlayer";
public string RegistryName { get; } = "RobloxPlayer";
public string RegistryName => "RobloxPlayer";
public string ExecutableName { get; } = "RobloxPlayerBeta.exe";
public override string ExecutableName => "RobloxPlayerBeta.exe";
public string StartEvent { get; } = "www.roblox.com/robloxStartedEvent";
public string StartEvent => "www.roblox.com/robloxStartedEvent";
public override string Directory => Path.Combine(Paths.Roblox, "Player");
public AppState State => App.State.Prop.Player;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{

View File

@ -1,22 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.AppData
namespace Bloxstrap.AppData
{
public class RobloxStudioData : CommonAppData, IAppData
{
public string ProductName { get; } = "Roblox Studio";
public string ProductName => "Roblox Studio";
public string BinaryType { get; } = "WindowsStudio64";
public string BinaryType => "WindowsStudio64";
public string RegistryName { get; } = "RobloxStudio";
public string RegistryName => "RobloxStudio";
public string ExecutableName { get; } = "RobloxStudioBeta.exe";
public override string ExecutableName => "RobloxStudioBeta.exe";
public string StartEvent { get; } = "www.roblox.com/robloxStudioStartedEvent";
public string StartEvent => "www.roblox.com/robloxStudioStartedEvent";
public override string Directory => Path.Combine(Paths.Roblox, "Studio");
public AppState State => App.State.Prop.Studio;
public override IReadOnlyDictionary<string, string> PackageDirectoryMap { get; set; } = new Dictionary<string, string>()
{

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,7 @@
{
[EnumName(FromTranslation = "Common.Automatic")]
Default,
// Vulkan,
D3D11,
D3D10,
// OpenGL
}
}

View File

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Exceptions
{
internal class HttpResponseException : Exception
{
public HttpResponseMessage ResponseMessage { get; }
public HttpResponseException(HttpResponseMessage responseMessage)
: base($"Could not connect to {responseMessage.RequestMessage!.RequestUri} because it returned HTTP {(int)responseMessage.StatusCode} ({responseMessage.ReasonPhrase})")
{
ResponseMessage = responseMessage;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Exceptions
{
internal class InvalidHTTPResponseException : Exception
{
public InvalidHTTPResponseException(string message) : base(message) { }
}
}

View File

@ -8,11 +8,28 @@ namespace Bloxstrap.Extensions
{
public static Icon GetSized(this Icon icon, int width, int height) => new(icon, new Size(width, height));
public static ImageSource GetImageSource(this Icon icon)
public static ImageSource GetImageSource(this Icon icon, bool handleException = true)
{
using MemoryStream stream = new();
icon.Save(stream);
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
if (handleException)
{
try
{
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
catch (Exception ex)
{
App.Logger.WriteException("IconEx::GetImageSource", ex);
Frontend.ShowMessageBox(String.Format(Strings.Dialog_IconLoadFailed, ex.Message));
return BootstrapperIcon.IconBloxstrap.GetIcon().GetImageSource(false);
}
}
else
{
return BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
}
}
}

View File

@ -4,9 +4,9 @@
{
public static string ToTranslatedString(this ServerType value) => value switch
{
ServerType.Public => Resources.Strings.Enums_ServerType_Public,
ServerType.Private => Resources.Strings.Enums_ServerType_Private,
ServerType.Reserved => Resources.Strings.Enums_ServerType_Reserved,
ServerType.Public => Strings.Enums_ServerType_Public,
ServerType.Private => Strings.Enums_ServerType_Private,
ServerType.Reserved => Strings.Enums_ServerType_Reserved,
_ => "?"
};
}

View File

@ -9,15 +9,10 @@ namespace Bloxstrap.Extensions
if (dialogTheme != Theme.Default)
return dialogTheme;
RegistryKey? key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
using var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
if (key is not null)
{
var value = key.GetValue("AppsUseLightTheme");
if (value is not null && (int)value == 0)
return Theme.Dark;
}
if (key?.GetValue("AppsUseLightTheme") is int value && value == 0)
return Theme.Dark;
return Theme.Light;
}

View File

@ -4,8 +4,14 @@ namespace Bloxstrap
{
public class FastFlagManager : JsonManager<Dictionary<string, object>>
{
public override string ClassName => nameof(FastFlagManager);
public override string LOG_IDENT_CLASS => ClassName;
public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json");
public bool Changed => !OriginalProp.SequenceEqual(Prop);
public static IReadOnlyDictionary<string, string> PresetFlags = new Dictionary<string, string>
{
{ "Network.Log", "FLogNetwork" },
@ -28,9 +34,6 @@ namespace Bloxstrap
{ "Rendering.Mode.D3D11", "FFlagDebugGraphicsPreferD3D11" },
{ "Rendering.Mode.D3D10", "FFlagDebugGraphicsPreferD3D11FL10" },
{ "Rendering.Mode.Vulkan", "FFlagDebugGraphicsPreferVulkan" },
{ "Rendering.Mode.Vulkan.Fix", "FFlagRenderVulkanFixMinimizeWindow" },
{ "Rendering.Mode.OpenGL", "FFlagDebugGraphicsPreferOpenGL" },
{ "Rendering.Lighting.Voxel", "DFFlagDebugRenderForceTechnologyVoxel" },
{ "Rendering.Lighting.ShadowMap", "FFlagDebugForceFutureIsBrightPhase2" },
@ -46,10 +49,9 @@ namespace Bloxstrap
{ "UI.FlagState", "FStringDebugShowFlagState" },
#endif
{ "UI.Menu.GraphicsSlider", "FFlagFixGraphicsQuality" },
{ "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" },
{ "UI.Menu.Style.DisableV2", "FFlagDisableNewIGMinDUA" },
{ "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" },
{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" },
{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" },
{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" },
@ -59,14 +61,11 @@ namespace Bloxstrap
{ "UI.Menu.Style.ABTest.3", "FFlagEnableInGameMenuChromeABTest3" }
};
// only one missing here is Metal because lol
public static IReadOnlyDictionary<RenderingMode, string> RenderingModes => new Dictionary<RenderingMode, string>
{
{ RenderingMode.Default, "None" },
// { RenderingMode.Vulkan, "Vulkan" },
{ RenderingMode.D3D11, "D3D11" },
{ RenderingMode.D3D10, "D3D10" },
// { RenderingMode.OpenGL, "OpenGL" }
};
public static IReadOnlyDictionary<LightingMode, string> LightingModes => new Dictionary<LightingMode, string>
@ -102,7 +101,7 @@ namespace Bloxstrap
InGameMenuVersion.Default,
new Dictionary<string, string?>
{
{ "DisableV2", null },
{ "V2Rollout", null },
{ "EnableV4", null },
{ "EnableV4Chrome", null },
{ "ABTest", null }
@ -113,7 +112,7 @@ namespace Bloxstrap
InGameMenuVersion.V1,
new Dictionary<string, string?>
{
{ "DisableV2", "True" },
{ "V2Rollout", "0" },
{ "EnableV4", "False" },
{ "EnableV4Chrome", "False" },
{ "ABTest", "False" }
@ -124,7 +123,7 @@ namespace Bloxstrap
InGameMenuVersion.V2,
new Dictionary<string, string?>
{
{ "DisableV2", "False" },
{ "V2Rollout", "100" },
{ "EnableV4", "False" },
{ "EnableV4Chrome", "False" },
{ "ABTest", "False" }
@ -135,7 +134,7 @@ namespace Bloxstrap
InGameMenuVersion.V4,
new Dictionary<string, string?>
{
{ "DisableV2", "True" },
{ "V2Rollout", "0" },
{ "EnableV4", "True" },
{ "EnableV4Chrome", "False" },
{ "ABTest", "False" }
@ -146,7 +145,7 @@ namespace Bloxstrap
InGameMenuVersion.V4Chrome,
new Dictionary<string, string?>
{
{ "DisableV2", "True" },
{ "V2Rollout", "0" },
{ "EnableV4", "True" },
{ "EnableV4Chrome", "True" },
{ "ABTest", "False" }
@ -228,14 +227,6 @@ namespace Bloxstrap
return mapping.First().Key;
}
public void CheckManualFullscreenPreset()
{
if (GetPreset("Rendering.Mode.Vulkan") == "True" || GetPreset("Rendering.Mode.OpenGL") == "True")
SetPreset("Rendering.ManualFullscreen", null);
else
SetPreset("Rendering.ManualFullscreen", "False");
}
public override void Save()
{
// convert all flag values to strings before saving
@ -244,21 +235,21 @@ namespace Bloxstrap
Prop[pair.Key] = pair.Value.ToString()!;
base.Save();
// clone the dictionary
OriginalProp = new(Prop);
}
public override void Load()
public override void Load(bool alertFailure = true)
{
base.Load();
base.Load(alertFailure);
CheckManualFullscreenPreset();
// clone the dictionary
OriginalProp = new(Prop);
// TODO - remove when activity tracking has been revamped
if (GetPreset("Network.Log") != "7")
SetPreset("Network.Log", "7");
string? val = GetPreset("UI.Menu.Style.EnableV4.1");
if (GetPreset("UI.Menu.Style.EnableV4.2") != val)
SetPreset("UI.Menu.Style.EnableV4.2", val);
}
}
}

7
Bloxstrap/GlobalCache.cs Normal file
View File

@ -0,0 +1,7 @@
namespace Bloxstrap
{
public static class GlobalCache
{
public static readonly Dictionary<string, string?> ServerLocation = new();
}
}

View File

@ -3,7 +3,6 @@ global using System.Collections.Generic;
global using System.Diagnostics;
global using System.Globalization;
global using System.IO;
global using System.IO.Compression;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Serialization;
@ -18,10 +17,16 @@ global using Bloxstrap.Enums;
global using Bloxstrap.Exceptions;
global using Bloxstrap.Extensions;
global using Bloxstrap.Models;
global using Bloxstrap.Models.APIs.Config;
global using Bloxstrap.Models.APIs.GitHub;
global using Bloxstrap.Models.APIs.Roblox;
global using Bloxstrap.Models.Attributes;
global using Bloxstrap.Models.BloxstrapRPC;
global using Bloxstrap.Models.RobloxApi;
global using Bloxstrap.Models.Entities;
global using Bloxstrap.Models.Manifest;
global using Bloxstrap.Models.Persistable;
global using Bloxstrap.Models.SettingTasks;
global using Bloxstrap.Models.SettingTasks.Base;
global using Bloxstrap.Resources;
global using Bloxstrap.UI;
global using Bloxstrap.Utility;

View File

@ -1,9 +1,4 @@
using System.DirectoryServices;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Windows;
using System.Windows.Media.Animation;
using Bloxstrap.Resources;
using System.Windows;
using Microsoft.Win32;
namespace Bloxstrap
@ -16,6 +11,8 @@ namespace Bloxstrap
public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap");
public bool ExistingDataPresent => File.Exists(Path.Combine(InstallLocation, "Settings.json"));
public bool CreateDesktopShortcuts = true;
public bool CreateStartMenuShortcuts = true;
@ -26,6 +23,10 @@ namespace Bloxstrap
public void DoInstall()
{
const string LOG_IDENT = "Installer::DoInstall";
App.Logger.WriteLine(LOG_IDENT, "Beginning installation");
// should've been created earlier from the write test anyway
Directory.CreateDirectory(InstallLocation);
@ -34,7 +35,19 @@ namespace Bloxstrap
if (!IsImplicitInstall)
{
Filesystem.AssertReadOnly(Paths.Application);
File.Copy(Paths.Process, Paths.Application, true);
try
{
File.Copy(Paths.Process, Paths.Application, true);
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Could not overwrite executable");
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(Strings.Installer_Install_CannotOverwrite, MessageBoxImage.Error);
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
}
}
// TODO: registry access checks, i'll need to look back on issues to see what the error looks like
@ -50,21 +63,19 @@ namespace Bloxstrap
uninstallKey.SetValue("InstallLocation", Paths.Base);
uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", "pizzaboxer");
uninstallKey.SetValue("Publisher", App.ProjectOwner);
uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -settings");
uninstallKey.SetValue("QuietUninstallString", $"\"{Paths.Application}\" -uninstall -quiet");
uninstallKey.SetValue("UninstallString", $"\"{Paths.Application}\" -uninstall");
uninstallKey.SetValue("URLInfoAbout", $"https://github.com/{App.ProjectRepository}");
uninstallKey.SetValue("URLUpdateInfo", $"https://github.com/{App.ProjectRepository}/releases/latest");
uninstallKey.SetValue("HelpLink", App.ProjectHelpLink);
uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink);
uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink);
}
// only register player, for the scenario where the user installs bloxstrap, closes it,
// and then launches from the website expecting it to work
// studio can be implicitly registered when it's first launched manually
ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\"");
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\"");
// TODO: implicit installation needs to reregister studio
WindowsRegistry.RegisterPlayer();
if (CreateDesktopShortcuts)
Shortcut.Create(Paths.Application, "", DesktopShortcut);
@ -73,9 +84,14 @@ namespace Bloxstrap
Shortcut.Create(Paths.Application, "", StartMenuShortcut);
// existing configuration persisting from an earlier install
App.Settings.Load();
App.State.Load();
App.FastFlags.Load();
App.Settings.Load(false);
App.State.Load(false);
App.FastFlags.Load(false);
if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid))
WindowsRegistry.RegisterStudio();
App.Logger.WriteLine(LOG_IDENT, "Installation finished");
}
private bool ValidateLocation()
@ -88,6 +104,10 @@ namespace Bloxstrap
if (InstallLocation.StartsWith("\\\\"))
return false;
if (InstallLocation.StartsWith(Path.GetTempPath(), StringComparison.InvariantCultureIgnoreCase)
|| InstallLocation.Contains("\\Temp\\", StringComparison.InvariantCultureIgnoreCase))
return false;
// prevent from installing to a onedrive folder
if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase))
return false;
@ -158,11 +178,12 @@ namespace Bloxstrap
const string LOG_IDENT = "Installer::DoUninstall";
var processes = new List<Process>();
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
#if STUDIO_FEATURES
processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName));
#endif
if (!String.IsNullOrEmpty(App.State.Prop.Player.VersionGuid))
processes.AddRange(Process.GetProcessesByName(App.RobloxPlayerAppName));
if (!String.IsNullOrEmpty(App.State.Prop.Studio.VersionGuid))
processes.AddRange(Process.GetProcessesByName(App.RobloxStudioAppName));
// prompt to shutdown roblox if its currently running
if (processes.Any())
@ -175,7 +196,10 @@ namespace Bloxstrap
);
if (result != MessageBoxResult.OK)
{
App.Terminate(ErrorCode.ERROR_CANCELLED);
return;
}
try
{
@ -203,44 +227,38 @@ namespace Bloxstrap
{
playerStillInstalled = false;
ProtocolHandler.Unregister("roblox");
ProtocolHandler.Unregister("roblox-player");
WindowsRegistry.Unregister("roblox");
WindowsRegistry.Unregister("roblox-player");
}
else
{
// revert launch uri handler to stock bootstrapper
string playerPath = Path.Combine((string)playerFolder, "RobloxPlayerBeta.exe");
ProtocolHandler.Register("roblox", "Roblox", playerPath);
ProtocolHandler.Register("roblox-player", "Roblox", playerPath);
WindowsRegistry.RegisterPlayer(playerPath, "%1");
}
using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
if (studioBootstrapperKey is null)
using var studioKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
var studioFolder = studioKey?.GetValue("InstallLocation");
if (studioKey is null || studioFolder is not string)
{
studioStillInstalled = false;
#if STUDIO_FEATURES
ProtocolHandler.Unregister("roblox-studio");
ProtocolHandler.Unregister("roblox-studio-auth");
WindowsRegistry.Unregister("roblox-studio");
WindowsRegistry.Unregister("roblox-studio-auth");
ProtocolHandler.Unregister("Roblox.Place");
ProtocolHandler.Unregister(".rbxl");
ProtocolHandler.Unregister(".rbxlx");
#endif
WindowsRegistry.Unregister("Roblox.Place");
WindowsRegistry.Unregister(".rbxl");
WindowsRegistry.Unregister(".rbxlx");
}
#if STUDIO_FEATURES
else
{
string studioLocation = (string?)studioBootstrapperKey.GetValue("InstallLocation") + "RobloxStudioBeta.exe"; // points to studio exe instead of bootstrapper
ProtocolHandler.Register("roblox-studio", "Roblox", studioLocation);
ProtocolHandler.Register("roblox-studio-auth", "Roblox", studioLocation);
string studioPath = Path.Combine((string)studioFolder, "RobloxStudioBeta.exe");
string studioLauncherPath = Path.Combine((string)studioFolder, "RobloxStudioLauncherBeta.exe");
ProtocolHandler.RegisterRobloxPlace(studioLocation);
WindowsRegistry.RegisterStudioProtocol(studioPath, "%1");
WindowsRegistry.RegisterStudioFileClass(studioPath, "-ide \"%1\"");
}
#endif
var cleanupSequence = new List<Action>
{
@ -257,8 +275,10 @@ namespace Bloxstrap
() => File.Delete(StartMenuShortcut),
() => Directory.Delete(Paths.Versions, true),
() => Directory.Delete(Paths.Downloads, true),
() => Directory.Delete(Paths.Roblox, true),
() => File.Delete(App.State.FileLocation)
};
if (!keepData)
@ -268,8 +288,7 @@ namespace Bloxstrap
() => Directory.Delete(Paths.Modifications, true),
() => Directory.Delete(Paths.Logs, true),
() => File.Delete(App.Settings.FileLocation),
() => File.Delete(App.State.FileLocation), // TODO: maybe this should always be deleted? not sure yet
() => File.Delete(App.Settings.FileLocation)
});
}
@ -331,8 +350,9 @@ namespace Bloxstrap
return;
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol
// TODO: 2.8.0 will download them to <Temp>/Bloxstrap/Updates
bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp"));
bool isAutoUpgrade = App.LaunchSettings.UpgradeFlag.Active
|| Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates"))
|| Paths.Process.StartsWith(Paths.Temp);
var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion;
var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion;
@ -340,8 +360,20 @@ namespace Bloxstrap
if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application))
return;
if (currentVer is not null && existingVer is not null && Utilities.CompareVersions(currentVer, existingVer) == VersionComparison.LessThan)
{
var result = Frontend.ShowMessageBox(
Strings.InstallChecker_VersionLessThanInstalled,
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
if (result != MessageBoxResult.Yes)
return;
}
// silently upgrade version if the command line flag is set or if we're launching from an auto update
if (!App.LaunchSettings.UpgradeFlag.Active && !isAutoUpgrade)
if (!isAutoUpgrade)
{
var result = Frontend.ShowMessageBox(
Strings.InstallChecker_VersionDifferentThanInstalled,
@ -353,47 +385,94 @@ namespace Bloxstrap
return;
}
App.Logger.WriteLine(LOG_IDENT, "Doing upgrade");
Filesystem.AssertReadOnly(Paths.Application);
// TODO: make this use a mutex somehow
// yes, this is EXTREMELY hacky, but the updater process that launched the
// new version may still be open and so we have to wait for it to close
int attempts = 0;
while (attempts < 10)
using (var ipl = new InterProcessLock("AutoUpdater", TimeSpan.FromSeconds(5)))
{
attempts++;
if (!ipl.IsAcquired)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not obtain singleton mutex)");
return;
}
}
// prior to 2.8.0, auto-updating was handled with this... bruteforce method
// now it's handled with the system mutex you see above, but we need to keep this logic for <2.8.0 versions
for (int i = 1; i <= 10; i++)
{
try
{
File.Delete(Paths.Application);
File.Copy(Paths.Process, Paths.Application, true);
break;
}
catch (Exception)
catch (Exception ex)
{
if (attempts == 1)
if (i == 1)
{
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
}
else if (i == 10)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 10 tries/5 seconds)");
App.Logger.WriteException(LOG_IDENT, ex);
return;
}
Thread.Sleep(500);
}
}
if (attempts == 10)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to update! (Could not get write permissions after 5 seconds)");
return;
}
File.Copy(Paths.Process, Paths.Application);
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
{
uninstallKey.SetValue("DisplayVersion", App.Version);
uninstallKey.SetValue("Publisher", App.ProjectOwner);
uninstallKey.SetValue("HelpLink", App.ProjectHelpLink);
uninstallKey.SetValue("URLInfoAbout", App.ProjectSupportLink);
uninstallKey.SetValue("URLUpdateInfo", App.ProjectDownloadLink);
}
// update migrations
if (existingVer is not null)
{
if (Utilities.CompareVersions(existingVer, "2.2.0") == VersionComparison.LessThan)
{
string path = Path.Combine(Paths.Integrations, "rbxfpsunlocker");
try
{
if (Directory.Exists(path))
Directory.Delete(path, true);
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
}
}
if (Utilities.CompareVersions(existingVer, "2.3.0") == VersionComparison.LessThan)
{
string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll");
string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini");
if (File.Exists(injectorLocation))
{
Frontend.ShowMessageBox(
Strings.Bootstrapper_HyperionUpdateInfo,
MessageBoxImage.Warning
);
File.Delete(injectorLocation);
}
if (File.Exists(configLocation))
File.Delete(configLocation);
}
if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan)
{
App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
@ -408,6 +487,13 @@ namespace Bloxstrap
App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false);
}
if (Utilities.CompareVersions(existingVer, "2.5.3") == VersionComparison.LessThan)
{
string? val = App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.1");
if (App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.2") != val)
App.FastFlags.SetPreset("UI.Menu.Style.EnableV4.2", val);
}
if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan)
{
if (App.Settings.Prop.UseDisableAppPatch)
@ -429,9 +515,7 @@ namespace Bloxstrap
_ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x);
if (x == 0)
{
App.FastFlags.SetPreset("Rendering.Framerate", null);
}
}
if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan)
@ -440,7 +524,7 @@ namespace Bloxstrap
string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap");
if (File.Exists(oldDesktopPath))
File.Move(oldDesktopPath, DesktopShortcut);
File.Move(oldDesktopPath, DesktopShortcut, true);
if (Directory.Exists(oldStartPath))
{
@ -458,14 +542,32 @@ namespace Bloxstrap
Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false);
ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\"");
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\"");
WindowsRegistry.RegisterPlayer();
string? oldV2Val = App.FastFlags.GetValue("FFlagDisableNewIGMinDUA");
if (oldV2Val is not null)
{
if (oldV2Val == "True")
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "0");
else
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "100");
App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null);
}
App.FastFlags.SetValue("FFlagFixGraphicsQuality", null);
Directory.Delete(Path.Combine(Paths.Base, "Versions"));
}
App.Settings.Save();
App.FastFlags.Save();
}
if (currentVer is null)
return;
if (isAutoUpgrade)
{
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}");

View File

@ -2,24 +2,29 @@
{
public class ActivityWatcher : IDisposable
{
// i'm thinking the functionality for parsing roblox logs could be broadened for more features than just rich presence,
// like checking the ping and region of the current connected server. maybe that's something to add?
private const string GameJoiningEntry = "[FLog::Output] ! Joining game";
private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer";
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = ";
private const string GameJoinedEntry = "[FLog::Network] serverId:";
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport";
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal";
private const string GameMessageEntry = "[FLog::Output] [BloxstrapRPC]";
private const string GameJoiningEntry = "[FLog::Output] ! Joining game";
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)";
// these entries are technically volatile!
// they only get printed depending on their configured FLog level, which could change at any time
// while levels being changed is fairly rare, please limit the number of varying number of FLog types you have to use, if possible
private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer";
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = ";
private const string GameJoinedEntry = "[FLog::Network] serverId:";
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
private const string GameTeleportingEntry = "[FLog::SingleSurfaceApp] initiateTeleport";
private const string GameLeavingEntry = "[FLog::SingleSurfaceApp] leaveUGCGameInternal";
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
private const string GameJoiningPrivateServerPattern = @"""accessCode"":""([0-9a-f\-]{36})""";
private const string GameJoiningUniversePattern = @"universeid:([0-9]+)";
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)";
private int _gameClientPid;
private int _logEntriesRead = 0;
private bool _teleportMarker = false;
private bool _reservedTeleportMarker = false;
@ -27,34 +32,28 @@
public event EventHandler<string>? OnLogEntry;
public event EventHandler? OnGameJoin;
public event EventHandler? OnGameLeave;
public event EventHandler? OnLogOpen;
public event EventHandler? OnAppClose;
public event EventHandler<Message>? OnRPCMessage;
private readonly Dictionary<string, string> GeolocationCache = new();
private DateTime LastRPCRequest;
public string LogLocation = null!;
// these are values to use assuming the player isn't currently in a game
// hmm... do i move this to a model?
public bool ActivityInGame = false;
public long ActivityPlaceId = 0;
public string ActivityJobId = "";
public string ActivityMachineAddress = "";
public bool ActivityMachineUDMUX = false;
public bool ActivityIsTeleport = false;
public ServerType ActivityServerType = ServerType.Public;
public bool InGame = false;
public ActivityData Data { get; private set; } = new();
/// <summary>
/// Ordered by newest to oldest
/// </summary>
public List<ActivityData> History = new();
public bool IsDisposed = false;
public ActivityWatcher(int gameClientPid)
public async void Start()
{
_gameClientPid = gameClientPid;
}
public async void StartWatcher()
{
const string LOG_IDENT = "ActivityWatcher::StartWatcher";
const string LOG_IDENT = "ActivityWatcher::Start";
// okay, here's the process:
//
@ -84,7 +83,7 @@
{
logFileInfo = new DirectoryInfo(logDirectory)
.GetFiles()
.Where(x => x.CreationTime <= DateTime.Now)
.Where(x => x.Name.Contains("Player", StringComparison.OrdinalIgnoreCase) && x.CreationTime <= DateTime.Now)
.OrderByDescending(x => x.CreationTime)
.First();
@ -95,12 +94,14 @@
await Task.Delay(1000);
}
OnLogOpen?.Invoke(this, EventArgs.Empty);
LogLocation = logFileInfo.FullName;
FileStream logFileStream = logFileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
App.Logger.WriteLine(LOG_IDENT, $"Opened {LogLocation}");
AutoResetEvent logUpdatedEvent = new(false);
FileSystemWatcher logWatcher = new()
var logUpdatedEvent = new AutoResetEvent(false);
var logWatcher = new FileSystemWatcher()
{
Path = logDirectory,
Filter = Path.GetFileName(logFileInfo.FullName),
@ -108,7 +109,7 @@
};
logWatcher.Changed += (s, e) => logUpdatedEvent.Set();
using StreamReader sr = new(logFileStream);
using var sr = new StreamReader(logFileStream);
while (!IsDisposed)
{
@ -117,13 +118,13 @@
if (log is null)
logUpdatedEvent.WaitOne(250);
else
ExamineLogEntry(log);
ReadLogEntry(log);
}
}
private void ExamineLogEntry(string entry)
private void ReadLogEntry(string entry)
{
const string LOG_IDENT = "ActivityWatcher::ExamineLogEntry";
const string LOG_IDENT = "ActivityWatcher::ReadLogEntry";
OnLogEntry?.Invoke(this, entry);
@ -137,14 +138,38 @@
App.Logger.WriteLine(LOG_IDENT, $"Read {_logEntriesRead} log entries");
if (entry.Contains(GameLeavingEntry))
OnAppClose?.Invoke(this, new EventArgs());
if (!ActivityInGame && ActivityPlaceId == 0)
{
App.Logger.WriteLine(LOG_IDENT, "User is back into the desktop app");
OnAppClose?.Invoke(this, EventArgs.Empty);
if (Data.PlaceId != 0 && !InGame)
{
App.Logger.WriteLine(LOG_IDENT, "User appears to be leaving from a cancelled/errored join");
Data = new();
}
}
if (!InGame && Data.PlaceId == 0)
{
// We are not in a game, nor are in the process of joining one
if (entry.Contains(GameJoiningPrivateServerEntry))
{
// we only expect to be joining a private server if we're not already in a game
ActivityServerType = ServerType.Private;
Data.ServerType = ServerType.Private;
var match = Regex.Match(entry, GameJoiningPrivateServerPattern);
if (match.Groups.Count != 2)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join private server entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
Data.AccessCode = match.Groups[1].Value;
}
else if (entry.Contains(GameJoiningEntry))
{
@ -157,80 +182,111 @@
return;
}
ActivityInGame = false;
ActivityPlaceId = long.Parse(match.Groups[2].Value);
ActivityJobId = match.Groups[1].Value;
ActivityMachineAddress = match.Groups[3].Value;
InGame = false;
Data.PlaceId = long.Parse(match.Groups[2].Value);
Data.JobId = match.Groups[1].Value;
Data.MachineAddress = match.Groups[3].Value;
if (App.Settings.Prop.ShowServerDetails && Data.MachineAddressValid)
_ = Data.QueryServerLocation();
if (_teleportMarker)
{
ActivityIsTeleport = true;
Data.IsTeleport = true;
_teleportMarker = false;
}
if (_reservedTeleportMarker)
{
ActivityServerType = ServerType.Reserved;
Data.ServerType = ServerType.Reserved;
_reservedTeleportMarker = false;
}
App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Joining Game ({Data})");
}
}
else if (!ActivityInGame && ActivityPlaceId != 0)
else if (!InGame && Data.PlaceId != 0)
{
if (entry.Contains(GameJoiningUDMUXEntry))
{
Match match = Regex.Match(entry, GameJoiningUDMUXPattern);
// We are not confirmed to be in a game, but we are in the process of joining one
if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress)
if (entry.Contains(GameJoiningUniverseEntry))
{
var match = Regex.Match(entry, GameJoiningUniversePattern);
if (match.Groups.Count != 2)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join UDMUX entry");
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join universe entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
ActivityMachineAddress = match.Groups[1].Value;
ActivityMachineUDMUX = true;
Data.UniverseId = long.Parse(match.Groups[1].Value);
App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
if (History.Any())
{
var lastActivity = History.First();
if (Data.UniverseId == lastActivity.UniverseId && Data.IsTeleport)
Data.RootActivity = lastActivity.RootActivity ?? lastActivity;
}
}
else if (entry.Contains(GameJoiningUDMUXEntry))
{
var match = Regex.Match(entry, GameJoiningUDMUXPattern);
if (match.Groups.Count != 3 || match.Groups[2].Value != Data.MachineAddress)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join UDMUX entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
Data.MachineAddress = match.Groups[1].Value;
if (App.Settings.Prop.ShowServerDetails)
_ = Data.QueryServerLocation();
App.Logger.WriteLine(LOG_IDENT, $"Server is UDMUX protected ({Data})");
}
else if (entry.Contains(GameJoinedEntry))
{
Match match = Regex.Match(entry, GameJoinedEntryPattern);
if (match.Groups.Count != 2 || match.Groups[1].Value != ActivityMachineAddress)
if (match.Groups.Count != 2 || match.Groups[1].Value != Data.MachineAddress)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game joined entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({Data})");
InGame = true;
Data.TimeJoined = DateTime.Now;
ActivityInGame = true;
OnGameJoin?.Invoke(this, new EventArgs());
}
}
else if (ActivityInGame && ActivityPlaceId != 0)
else if (InGame && Data.PlaceId != 0)
{
// We are confirmed to be in a game
if (entry.Contains(GameDisconnectedEntry))
{
App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({Data})");
ActivityInGame = false;
ActivityPlaceId = 0;
ActivityJobId = "";
ActivityMachineAddress = "";
ActivityMachineUDMUX = false;
ActivityIsTeleport = false;
ActivityServerType = ServerType.Public;
Data.TimeLeft = DateTime.Now;
History.Insert(0, Data);
InGame = false;
Data = new();
OnGameLeave?.Invoke(this, new EventArgs());
}
else if (entry.Contains(GameTeleportingEntry))
{
App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
App.Logger.WriteLine(LOG_IDENT, $"Initiating teleport to server ({Data})");
_teleportMarker = true;
}
else if (_teleportMarker && entry.Contains(GameJoiningReservedServerEntry))
@ -282,6 +338,35 @@
return;
}
if (message.Command == "SetLaunchData")
{
string? data;
try
{
data = message.Data.Deserialize<string>();
}
catch (Exception)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
return;
}
if (data is null)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
return;
}
if (data.Length > 200)
{
App.Logger.WriteLine(LOG_IDENT, "Data cannot be longer than 200 characters");
return;
}
Data.RPCLaunchData = data;
}
OnRPCMessage?.Invoke(this, message);
LastRPCRequest = DateTime.Now;
@ -289,44 +374,6 @@
}
}
public async Task<string> GetServerLocation()
{
const string LOG_IDENT = "ActivityWatcher::GetServerLocation";
if (GeolocationCache.ContainsKey(ActivityMachineAddress))
return GeolocationCache[ActivityMachineAddress];
try
{
string location = "";
var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{ActivityMachineAddress}/json");
if (ipInfo is null)
return $"? ({Resources.Strings.ActivityTracker_LookupFailed})";
if (string.IsNullOrEmpty(ipInfo.Country))
location = "?";
else if (ipInfo.City == ipInfo.Region)
location = $"{ipInfo.Region}, {ipInfo.Country}";
else
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
if (!ActivityInGame)
return $"? ({Resources.Strings.ActivityTracker_LeftGame})";
GeolocationCache[ActivityMachineAddress] = location;
return location;
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {ActivityMachineAddress}");
App.Logger.WriteException(LOG_IDENT, ex);
return $"? ({Resources.Strings.ActivityTracker_LookupFailed})";
}
}
public void Dispose()
{
IsDisposed = true;

View File

@ -1,4 +1,6 @@
using DiscordRPC;
using System.Windows;
using DiscordRPC;
namespace Bloxstrap.Integrations
{
@ -6,18 +8,16 @@ namespace Bloxstrap.Integrations
{
private readonly DiscordRpcClient _rpcClient = new("1005469189907173486");
private readonly ActivityWatcher _activityWatcher;
private readonly Queue<Message> _messageQueue = new();
private DiscordRPC.RichPresence? _currentPresence;
private DiscordRPC.RichPresence? _currentPresenceCopy;
private Message? _stashedRPCMessage;
private DiscordRPC.RichPresence? _originalPresence;
private bool _visible = true;
private long _currentUniverseId;
private DateTime? _timeStartedUniverse;
public DiscordRichPresence(ActivityWatcher activityWatcher)
{
const string LOG_IDENT = "DiscordRichPresence::DiscordRichPresence";
const string LOG_IDENT = "DiscordRichPresence";
_activityWatcher = activityWatcher;
@ -47,119 +47,121 @@ namespace Bloxstrap.Integrations
_rpcClient.Initialize();
}
public void ProcessRPCMessage(Message message)
public void ProcessRPCMessage(Message message, bool implicitUpdate = true)
{
const string LOG_IDENT = "DiscordRichPresence::ProcessRPCMessage";
if (message.Command != "SetRichPresence")
if (message.Command != "SetRichPresence" && message.Command != "SetLaunchData")
return;
if (_currentPresence is null || _currentPresenceCopy is null)
if (_currentPresence is null || _originalPresence is null)
{
if (_activityWatcher.ActivityInGame)
{
App.Logger.WriteLine(LOG_IDENT, "Presence is not yet set, but is currently in game, stashing presence set request");
_stashedRPCMessage = message;
return;
}
App.Logger.WriteLine(LOG_IDENT, "Presence is not set, aborting");
App.Logger.WriteLine(LOG_IDENT, "Presence is not set, enqueuing message");
_messageQueue.Enqueue(message);
return;
}
Models.BloxstrapRPC.RichPresence? presenceData;
// a lot of repeated code here, could this somehow be cleaned up?
try
if (message.Command == "SetLaunchData")
{
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
_currentPresence.Buttons = GetButtons();
}
catch (Exception)
else if (message.Command == "SetRichPresence")
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
return;
}
Models.BloxstrapRPC.RichPresence? presenceData;
if (presenceData is null)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
return;
}
if (presenceData.Details is not null)
{
if (presenceData.Details.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
else if (presenceData.Details == "<reset>")
_currentPresence.Details = _currentPresenceCopy.Details;
else
_currentPresence.Details = presenceData.Details;
}
if (presenceData.State is not null)
{
if (presenceData.State.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
else if (presenceData.State == "<reset>")
_currentPresence.State = _currentPresenceCopy.State;
else
_currentPresence.State = presenceData.State;
}
if (presenceData.TimestampStart == 0)
_currentPresence.Timestamps.Start = null;
else if (presenceData.TimestampStart is not null)
_currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
if (presenceData.TimestampEnd == 0)
_currentPresence.Timestamps.End = null;
else if (presenceData.TimestampEnd is not null)
_currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
if (presenceData.SmallImage is not null)
{
if (presenceData.SmallImage.Clear)
try
{
_currentPresence.Assets.SmallImageKey = "";
presenceData = message.Data.Deserialize<Models.BloxstrapRPC.RichPresence>();
}
else if (presenceData.SmallImage.Reset)
catch (Exception)
{
_currentPresence.Assets.SmallImageText = _currentPresenceCopy.Assets.SmallImageText;
_currentPresence.Assets.SmallImageKey = _currentPresenceCopy.Assets.SmallImageKey;
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization threw an exception)");
return;
}
else
{
if (presenceData.SmallImage.AssetId is not null)
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
if (presenceData.SmallImage.HoverText is not null)
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
if (presenceData is null)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to parse message! (JSON deserialization returned null)");
return;
}
if (presenceData.Details is not null)
{
if (presenceData.Details.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"Details cannot be longer than 128 characters");
else if (presenceData.Details == "<reset>")
_currentPresence.Details = _originalPresence.Details;
else
_currentPresence.Details = presenceData.Details;
}
if (presenceData.State is not null)
{
if (presenceData.State.Length > 128)
App.Logger.WriteLine(LOG_IDENT, $"State cannot be longer than 128 characters");
else if (presenceData.State == "<reset>")
_currentPresence.State = _originalPresence.State;
else
_currentPresence.State = presenceData.State;
}
if (presenceData.TimestampStart == 0)
_currentPresence.Timestamps.Start = null;
else if (presenceData.TimestampStart is not null)
_currentPresence.Timestamps.StartUnixMilliseconds = presenceData.TimestampStart * 1000;
if (presenceData.TimestampEnd == 0)
_currentPresence.Timestamps.End = null;
else if (presenceData.TimestampEnd is not null)
_currentPresence.Timestamps.EndUnixMilliseconds = presenceData.TimestampEnd * 1000;
if (presenceData.SmallImage is not null)
{
if (presenceData.SmallImage.Clear)
{
_currentPresence.Assets.SmallImageKey = "";
}
else if (presenceData.SmallImage.Reset)
{
_currentPresence.Assets.SmallImageText = _originalPresence.Assets.SmallImageText;
_currentPresence.Assets.SmallImageKey = _originalPresence.Assets.SmallImageKey;
}
else
{
if (presenceData.SmallImage.AssetId is not null)
_currentPresence.Assets.SmallImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.SmallImage.AssetId}";
if (presenceData.SmallImage.HoverText is not null)
_currentPresence.Assets.SmallImageText = presenceData.SmallImage.HoverText;
}
}
if (presenceData.LargeImage is not null)
{
if (presenceData.LargeImage.Clear)
{
_currentPresence.Assets.LargeImageKey = "";
}
else if (presenceData.LargeImage.Reset)
{
_currentPresence.Assets.LargeImageText = _originalPresence.Assets.LargeImageText;
_currentPresence.Assets.LargeImageKey = _originalPresence.Assets.LargeImageKey;
}
else
{
if (presenceData.LargeImage.AssetId is not null)
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
if (presenceData.LargeImage.HoverText is not null)
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
}
}
}
if (presenceData.LargeImage is not null)
{
if (presenceData.LargeImage.Clear)
{
_currentPresence.Assets.LargeImageKey = "";
}
else if (presenceData.LargeImage.Reset)
{
_currentPresence.Assets.LargeImageText = _currentPresenceCopy.Assets.LargeImageText;
_currentPresence.Assets.LargeImageKey = _currentPresenceCopy.Assets.LargeImageKey;
}
else
{
if (presenceData.LargeImage.AssetId is not null)
_currentPresence.Assets.LargeImageKey = $"https://assetdelivery.roblox.com/v1/asset/?id={presenceData.LargeImage.AssetId}";
if (presenceData.LargeImage.HoverText is not null)
_currentPresence.Assets.LargeImageText = presenceData.LargeImage.HoverText;
}
}
UpdatePresence();
if (implicitUpdate)
UpdatePresence();
}
public void SetVisibility(bool visible)
@ -178,124 +180,131 @@ namespace Bloxstrap.Integrations
{
const string LOG_IDENT = "DiscordRichPresence::SetCurrentGame";
if (!_activityWatcher.ActivityInGame)
if (!_activityWatcher.InGame)
{
App.Logger.WriteLine(LOG_IDENT, "Not in game, clearing presence");
_currentPresence = _currentPresenceCopy = null;
_stashedRPCMessage = null;
_currentPresence = _originalPresence = null;
_messageQueue.Clear();
UpdatePresence();
return true;
}
string icon = "roblox";
long placeId = _activityWatcher.ActivityPlaceId;
var activity = _activityWatcher.Data;
long placeId = activity.PlaceId;
App.Logger.WriteLine(LOG_IDENT, $"Setting presence for Place ID {placeId}");
var universeIdResponse = await Http.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{placeId}/universe");
if (universeIdResponse is null)
{
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe ID!");
return false;
}
long universeId = universeIdResponse.UniverseId;
App.Logger.WriteLine(LOG_IDENT, $"Got Universe ID as {universeId}");
// preserve time spent playing if we're teleporting between places in the same universe
if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId)
_timeStartedUniverse = DateTime.UtcNow;
var timeStarted = activity.TimeJoined;
_currentUniverseId = universeId;
if (activity.RootActivity is not null)
timeStarted = activity.RootActivity.TimeJoined;
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={universeId}");
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
if (activity.UniverseDetails is null)
{
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe info!");
return false;
}
GameDetailResponse universeDetails = gameDetailResponse.Data.ToArray()[0];
App.Logger.WriteLine(LOG_IDENT, "Got Universe details");
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeId}&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false");
if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any())
{
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe thumbnail info!");
}
else
{
icon = universeThumbnailResponse.Data.ToArray()[0].ImageUrl;
App.Logger.WriteLine(LOG_IDENT, $"Got Universe thumbnail as {icon}");
}
List<Button> buttons = new();
if (!App.Settings.Prop.HideRPCButtons && _activityWatcher.ActivityServerType == ServerType.Public)
{
buttons.Add(new Button
try
{
Label = "Join server",
Url = $"roblox://experiences/start?placeId={placeId}&gameInstanceId={_activityWatcher.ActivityJobId}"
});
await UniverseDetails.FetchSingle(activity.UniverseId);
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox($"{Strings.ActivityWatcher_RichPresenceLoadFailed}\n\n{ex.Message}", MessageBoxImage.Warning);
return false;
}
activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId);
}
buttons.Add(new Button
{
Label = "See game page",
Url = $"https://www.roblox.com/games/{placeId}"
});
var universeDetails = activity.UniverseDetails!;
if (!_activityWatcher.ActivityInGame || placeId != _activityWatcher.ActivityPlaceId)
icon = universeDetails.Thumbnail.ImageUrl;
if (!_activityWatcher.InGame || placeId != activity.PlaceId)
{
App.Logger.WriteLine(LOG_IDENT, "Aborting presence set because game activity has changed");
return false;
}
string status = _activityWatcher.ActivityServerType switch
string status = _activityWatcher.Data.ServerType switch
{
ServerType.Private => "In a private server",
ServerType.Reserved => "In a reserved server",
_ => $"by {universeDetails.Creator.Name}" + (universeDetails.Creator.HasVerifiedBadge ? " ☑️" : ""),
_ => $"by {universeDetails.Data.Creator.Name}" + (universeDetails.Data.Creator.HasVerifiedBadge ? " ☑️" : ""),
};
if (universeDetails.Name.Length < 2)
universeDetails.Name = $"{universeDetails.Name}\x2800\x2800\x2800";
string universeName = universeDetails.Data.Name;
if (universeName.Length < 2)
universeName = $"{universeName}\x2800\x2800\x2800";
_currentPresence = new DiscordRPC.RichPresence
{
Details = $"Playing {universeDetails.Name}",
Details = universeName,
State = status,
Timestamps = new Timestamps { Start = _timeStartedUniverse },
Buttons = buttons.ToArray(),
Timestamps = new Timestamps { Start = timeStarted.ToUniversalTime() },
Buttons = GetButtons(),
Assets = new Assets
{
LargeImageKey = icon,
LargeImageText = universeDetails.Name,
LargeImageText = universeName,
SmallImageKey = "roblox",
SmallImageText = "Roblox"
}
};
// this is used for configuration from BloxstrapRPC
_currentPresenceCopy = _currentPresence.Clone();
_originalPresence = _currentPresence.Clone();
if (_stashedRPCMessage is not null)
if (_messageQueue.Any())
{
App.Logger.WriteLine(LOG_IDENT, "Found stashed RPC message, invoking presence set command now");
ProcessRPCMessage(_stashedRPCMessage);
_stashedRPCMessage = null;
}
else
{
UpdatePresence();
App.Logger.WriteLine(LOG_IDENT, "Processing queued messages");
ProcessRPCMessage(_messageQueue.Dequeue(), false);
}
UpdatePresence();
return true;
}
public Button[] GetButtons()
{
var buttons = new List<Button>();
var data = _activityWatcher.Data;
if (!App.Settings.Prop.HideRPCButtons)
{
bool show = false;
if (data.ServerType == ServerType.Public)
show = true;
else if (data.ServerType == ServerType.Reserved && !String.IsNullOrEmpty(data.RPCLaunchData))
show = true;
if (show)
{
buttons.Add(new Button
{
Label = "Join server",
Url = data.GetInviteDeeplink()
});
}
}
buttons.Add(new Button
{
Label = "See game page",
Url = $"https://www.roblox.com/games/{data.PlaceId}"
});
return buttons.ToArray();
}
public void UpdatePresence()
{
const string LOG_IDENT = "DiscordRichPresence::UpdatePresence";

View File

@ -4,13 +4,17 @@ namespace Bloxstrap
{
public class JsonManager<T> where T : class, new()
{
public T OriginalProp { get; set; } = new();
public T Prop { get; set; } = new();
public virtual string FileLocation => Path.Combine(Paths.Base, $"{typeof(T).Name}.json");
public virtual string ClassName => typeof(T).Name;
private string LOG_IDENT_CLASS => $"JsonManager<{typeof(T).Name}>";
public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
public virtual void Load()
public virtual string LOG_IDENT_CLASS => $"JsonManager<{ClassName}>";
public virtual void Load(bool alertFailure = true)
{
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Load";
@ -30,7 +34,22 @@ namespace Bloxstrap
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to load!");
App.Logger.WriteLine(LOG_IDENT, $"{ex.Message}");
App.Logger.WriteException(LOG_IDENT, ex);
if (alertFailure)
{
string message = "";
if (ClassName == nameof(Settings))
message = Strings.JsonManager_SettingsLoadFailed;
else if (ClassName == nameof(FastFlagManager))
message = Strings.JsonManager_FastFlagsLoadFailed;
if (!String.IsNullOrEmpty(message))
Frontend.ShowMessageBox($"{message}\n\n{ex.Message}", System.Windows.MessageBoxImage.Warning);
}
Save();
}
}
@ -41,7 +60,21 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}...");
Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!);
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
try
{
File.WriteAllText(FileLocation, JsonSerializer.Serialize(Prop, new JsonSerializerOptions { WriteIndented = true }));
}
catch (IOException ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to save");
App.Logger.WriteException(LOG_IDENT, ex);
string errorMessage = string.Format(Resources.Strings.Bootstrapper_JsonManagerSaveFailed, ClassName, ex.Message);
Frontend.ShowMessageBox(errorMessage, System.Windows.MessageBoxImage.Warning);
return;
}
App.Logger.WriteLine(LOG_IDENT, "Save complete!");
}

View File

@ -1,11 +1,10 @@
using System.Windows;
using Bloxstrap.UI.Elements.Dialogs;
using Microsoft.Win32;
using Windows.Win32;
using Windows.Win32.Foundation;
using Bloxstrap.UI.Elements.Dialogs;
namespace Bloxstrap
{
public static class LaunchHandler
@ -19,6 +18,7 @@ namespace Bloxstrap
break;
case NextAction.LaunchRoblox:
App.LaunchSettings.RobloxLaunchMode = LaunchMode.Player;
LaunchRoblox();
break;
@ -42,6 +42,8 @@ namespace Bloxstrap
LaunchRoblox();
else if (!App.LaunchSettings.QuietFlag.Active)
LaunchMenu();
else
App.Terminate();
}
public static void LaunchInstaller()
@ -51,6 +53,7 @@ namespace Bloxstrap
if (!interlock.IsAcquired)
{
Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Installer, MessageBoxImage.Stop);
App.Terminate();
return;
}
@ -95,6 +98,7 @@ namespace Bloxstrap
if (!interlock.IsAcquired)
{
Frontend.ShowMessageBox(Strings.Dialog_AlreadyRunning_Uninstaller, MessageBoxImage.Stop);
App.Terminate();
return;
}
@ -115,11 +119,16 @@ namespace Bloxstrap
}
if (!confirmed)
{
App.Terminate();
return;
}
Installer.DoUninstall(keepData);
Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information);
App.Terminate();
}
public static void LaunchSettings()
@ -131,7 +140,9 @@ namespace Bloxstrap
if (interlock.IsAcquired)
{
bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1;
new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning).ShowDialog();
var window = new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning);
window.Show();
}
else
{
@ -156,7 +167,6 @@ namespace Bloxstrap
{
const string LOG_IDENT = "LaunchHandler::LaunchRoblox";
if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll")))
{
Frontend.ShowMessageBox(Strings.Bootstrapper_WMFNotFound, MessageBoxImage.Error);
@ -167,15 +177,6 @@ namespace Bloxstrap
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND);
}
bool installWebView2 = false;
{
using var hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}");
using var hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}");
if (hklmKey is null && hkcuKey is null)
installWebView2 = Frontend.ShowMessageBox(Strings.Bootstrapper_WebView2NotFound, MessageBoxImage.Warning, MessageBoxButton.YesNo, MessageBoxResult.Yes) == MessageBoxResult.Yes;
}
if (App.Settings.Prop.ConfirmLaunches && Mutex.TryOpenExisting("ROBLOX_singletonMutex", out var _))
{
// this currently doesn't work very well since it relies on checking the existence of the singleton mutex
@ -191,11 +192,9 @@ namespace Bloxstrap
}
}
App.NotifyIcon = new();
// start bootstrapper and show the bootstrapper modal if we're not running silently
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
var bootstrapper = new Bootstrapper(installWebView2);
var bootstrapper = new Bootstrapper();
IBootstrapperDialog? dialog = null;
if (!App.LaunchSettings.QuietFlag.Active)
@ -206,45 +205,53 @@ namespace Bloxstrap
dialog.Bootstrapper = bootstrapper;
}
Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t =>
Task.Run(bootstrapper.Run).ContinueWith(t =>
{
App.Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
// notifyicon is blocking main thread, must be disposed here
App.NotifyIcon?.Dispose();
if (t.IsFaulted)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is null)
return;
if (t.Exception is not null)
App.FinalizeExceptionHandling(t.Exception);
}
App.Logger.WriteException(LOG_IDENT, t.Exception);
Exception exception = t.Exception;
#if !DEBUG
if (t.Exception.GetType().ToString() == "System.AggregateException")
exception = t.Exception.InnerException!;
#endif
App.FinalizeExceptionHandling(exception, false);
App.Terminate();
});
// this ordering is very important as all wpf windows are shown as modal dialogs, mess it up and you'll end up blocking input to one of them
dialog?.ShowBootstrapper();
if (!App.LaunchSettings.NoLaunchFlag.Active && App.Settings.Prop.EnableActivityTracking)
App.NotifyIcon?.InitializeContextMenu();
App.Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish");
bootstrapperTask.Wait();
}
public static void LaunchWatcher()
{
const string LOG_IDENT = "LaunchHandler::LaunchWatcher";
// this whole topology is a bit confusing, bear with me:
// main thread: strictly UI only, handles showing of the notification area icon, context menu, server details dialog
// - server information task: queries server location, invoked if either the explorer notification is shown or the server details dialog is opened
// - discord rpc thread: handles rpc connection with discord
// - discord rich presence tasks: handles querying and displaying of game information, invoked on activity watcher events
// - watcher task: runs activity watcher + waiting for roblox to close, terminates when it has
var watcher = new Watcher();
Task.Run(watcher.Run).ContinueWith(t =>
{
App.Logger.WriteLine(LOG_IDENT, "Watcher task has finished");
watcher.Dispose();
if (t.IsFaulted)
{
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the watcher");
if (t.Exception is not null)
App.FinalizeExceptionHandling(t.Exception);
}
App.Terminate();
});
}
}
}

View File

@ -28,7 +28,13 @@ namespace Bloxstrap
public LaunchFlag StudioFlag { get; } = new("studio");
public LaunchMode RobloxLaunchMode { get; private set; } = LaunchMode.None;
#if DEBUG
public bool BypassUpdateCheck => true;
#else
public bool BypassUpdateCheck => UninstallFlag.Active || WatcherFlag.Active;
#endif
public LaunchMode RobloxLaunchMode { get; set; } = LaunchMode.None;
public string RobloxLaunchArgs { get; private set; } = "";
@ -37,7 +43,7 @@ namespace Bloxstrap
/// </summary>
public string[] Args { get; private set; }
private Dictionary<string, LaunchFlag> _flagMap = new();
private readonly Dictionary<string, LaunchFlag> _flagMap = new();
public LaunchSettings(string[] args)
{
@ -68,7 +74,7 @@ namespace Bloxstrap
string identifier = arg[1..];
if (_flagMap[identifier] is not LaunchFlag flag)
if (!_flagMap.TryGetValue(identifier, out LaunchFlag? flag) || flag is null)
continue;
flag.Active = true;

View File

@ -7,7 +7,7 @@
private readonly SemaphoreSlim _semaphore = new(1, 1);
private FileStream? _filestream;
public readonly List<string> Backlog = new();
public readonly List<string> History = new();
public bool Initialized = false;
public bool NoWriteMode = false;
public string? FileLocation;
@ -16,8 +16,7 @@
{
const string LOG_IDENT = "Logger::Initialize";
// TODO: <Temp>/Bloxstrap/Logs/
string directory = useTempDir ? Path.Combine(Paths.LocalAppData, "Temp") : Path.Combine(Paths.Base, "Logs");
string directory = useTempDir ? Path.Combine(Paths.TempLogs) : Path.Combine(Paths.Base, "Logs");
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'");
string filename = $"{App.ProjectName}_{timestamp}.log";
string location = Path.Combine(directory, filename);
@ -55,7 +54,7 @@
WriteLine(LOG_IDENT, $"Failed to initialize because Bloxstrap cannot write to {directory}");
Frontend.ShowMessageBox(
String.Format(Resources.Strings.Logger_NoWriteMode, directory),
String.Format(Strings.Logger_NoWriteMode, directory),
System.Windows.MessageBoxImage.Warning,
System.Windows.MessageBoxButton.OK
);
@ -68,8 +67,8 @@
Initialized = true;
if (Backlog.Count > 0)
WriteToLog(string.Join("\r\n", Backlog));
if (History.Count > 0)
WriteToLog(string.Join("\r\n", History));
WriteLine(LOG_IDENT, "Finished initializing!");
@ -102,10 +101,12 @@
{
string timestamp = DateTime.UtcNow.ToString("s") + "Z";
string outcon = $"{timestamp} {message}";
string outlog = outcon.Replace(Paths.UserProfile, "%UserProfile%");
string outlog = outcon.Replace(Paths.UserProfile, "%UserProfile%", StringComparison.InvariantCultureIgnoreCase);
Debug.WriteLine(outcon);
WriteToLog(outlog);
History.Add(outlog);
}
public void WriteLine(string identifier, string message) => WriteLine($"[{identifier}] {message}");
@ -122,10 +123,7 @@
private async void WriteToLog(string message)
{
if (!Initialized)
{
Backlog.Add(message);
return;
}
try
{

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.Models.APIs.Config
{
public class Supporter
{
[JsonPropertyName("imageAsset")]
public string ImageAsset { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
public string Image => $"https://raw.githubusercontent.com/bloxstraplabs/config/main/assets/{ImageAsset}";
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models.APIs.Config
{
public class SupporterData
{
[JsonPropertyName("columns")]
public int Columns { get; set; }
[JsonPropertyName("supporters")]
public List<Supporter> Supporters { get; set; } = null!;
}
}

View File

@ -0,0 +1,8 @@
public class GithubReleaseAsset
{
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
}

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models
namespace Bloxstrap.Models.APIs.GitHub
{
public class GithubRelease
{
@ -17,13 +17,4 @@
[JsonPropertyName("assets")]
public List<GithubReleaseAsset>? Assets { get; set; }
}
public class GithubReleaseAsset
{
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
}
}

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models
namespace Bloxstrap.Models.APIs
{
public class IPInfoResponse
{

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.APIs.Roblox
{
/// <summary>
/// Roblox.Web.WebAPI.Models.ApiArrayResponse

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models
namespace Bloxstrap.Models.APIs.Roblox
{
public class ClientFlagSettings
{

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models
namespace Bloxstrap.Models.APIs.Roblox
{
public class ClientVersion
{

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.APIs.Roblox
{
/// <summary>
/// Roblox.Games.Api.Models.Response.GameCreator

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.APIs.Roblox
{
/// <summary>

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.APIs.Roblox
{
/// <summary>
/// Roblox.Web.Responses.Thumbnails.ThumbnailResponse

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.Models.RobloxApi
namespace Bloxstrap.Models.APIs.Roblox
{
// lmao its just one property
public class UniverseIdResponse

View File

@ -0,0 +1,149 @@
using System.Web;
using System.Windows;
using System.Windows.Input;
using Bloxstrap.Models.APIs;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.Models.Entities
{
public class ActivityData
{
private long _universeId = 0;
/// <summary>
/// If the current activity stems from an in-universe teleport, then this will be
/// set to the activity that corresponds to the initial game join
/// </summary>
public ActivityData? RootActivity;
public long UniverseId
{
get => _universeId;
set
{
_universeId = value;
UniverseDetails.LoadFromCache(value);
}
}
public long PlaceId { get; set; } = 0;
public string JobId { get; set; } = string.Empty;
/// <summary>
/// This will be empty unless the server joined is a private server
/// </summary>
public string AccessCode { get; set; } = string.Empty;
public string MachineAddress { get; set; } = string.Empty;
public bool MachineAddressValid => !string.IsNullOrEmpty(MachineAddress) && !MachineAddress.StartsWith("10.");
public bool IsTeleport { get; set; } = false;
public ServerType ServerType { get; set; } = ServerType.Public;
public DateTime TimeJoined { get; set; }
public DateTime? TimeLeft { get; set; }
// everything below here is optional strictly for bloxstraprpc, discord rich presence, or game history
/// <summary>
/// This is intended only for other people to use, i.e. context menu invite link, rich presence joining
/// </summary>
public string RPCLaunchData { get; set; } = string.Empty;
public UniverseDetails? UniverseDetails { get; set; }
public string GameHistoryDescription
{
get
{
string desc = string.Format("{0} • {1} - {2}", UniverseDetails?.Data.Creator.Name, TimeJoined.ToString("h:mm tt"), TimeLeft?.ToString("h:mm tt"));
if (ServerType != ServerType.Public)
desc += " • " + ServerType.ToTranslatedString();
return desc;
}
}
public ICommand RejoinServerCommand => new RelayCommand(RejoinServer);
private SemaphoreSlim serverQuerySemaphore = new(1, 1);
public string GetInviteDeeplink(bool launchData = true)
{
string deeplink = $"roblox://experiences/start?placeId={PlaceId}";
if (ServerType == ServerType.Private)
deeplink += "&accessCode=" + AccessCode;
else
deeplink += "&gameInstanceId=" + JobId;
if (launchData && !string.IsNullOrEmpty(RPCLaunchData))
deeplink += "&launchData=" + HttpUtility.UrlEncode(RPCLaunchData);
return deeplink;
}
public async Task<string?> QueryServerLocation()
{
const string LOG_IDENT = "ActivityData::QueryServerLocation";
if (!MachineAddressValid)
throw new InvalidOperationException($"Machine address is invalid ({MachineAddress})");
await serverQuerySemaphore.WaitAsync();
if (GlobalCache.ServerLocation.TryGetValue(MachineAddress, out string? location))
{
serverQuerySemaphore.Release();
return location;
}
try
{
var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{MachineAddress}/json");
if (string.IsNullOrEmpty(ipInfo.City))
throw new InvalidHTTPResponseException("Reported city was blank");
if (ipInfo.City == ipInfo.Region)
location = $"{ipInfo.Region}, {ipInfo.Country}";
else
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
GlobalCache.ServerLocation[MachineAddress] = location;
serverQuerySemaphore.Release();
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {MachineAddress}");
App.Logger.WriteException(LOG_IDENT, ex);
GlobalCache.ServerLocation[MachineAddress] = location;
serverQuerySemaphore.Release();
Frontend.ShowConnectivityDialog(
string.Format(Strings.Dialog_Connectivity_UnableToConnect, "ipinfo.io"),
Strings.ActivityWatcher_LocationQueryFailed,
MessageBoxImage.Warning,
ex
);
}
return location;
}
public override string ToString() => $"{PlaceId}/{JobId}";
private void RejoinServer()
{
string playerPath = Path.Combine(Paths.Roblox, "Player", "RobloxPlayerBeta.exe");
Process.Start(playerPath, GetInviteDeeplink(false));
}
}
}

View File

@ -1,7 +1,7 @@
using System.Security.Cryptography;
using System.Windows.Markup;
namespace Bloxstrap.Models
namespace Bloxstrap.Models.Entities
{
public class ModPresetFileData
{

View File

@ -0,0 +1,52 @@
using Bloxstrap.Models.APIs.Roblox;
namespace Bloxstrap.Models.Entities
{
public class UniverseDetails
{
private static List<UniverseDetails> _cache { get; set; } = new();
public GameDetailResponse Data { get; set; } = null!;
/// <summary>
/// Returns data for a 128x128 icon
/// </summary>
public ThumbnailResponse Thumbnail { get; set; } = null!;
public static UniverseDetails? LoadFromCache(long id)
{
var cacheQuery = _cache.Where(x => x.Data?.Id == id);
if (cacheQuery.Any())
return cacheQuery.First();
return null;
}
public static Task FetchSingle(long id) => FetchBulk(id.ToString());
public static async Task FetchBulk(string ids)
{
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={ids}");
if (!gameDetailResponse.Data.Any())
throw new InvalidHTTPResponseException("Roblox API for Game Details returned invalid data");
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={ids}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false");
if (!universeThumbnailResponse.Data.Any())
throw new InvalidHTTPResponseException("Roblox API for Game Thumbnails returned invalid data");
foreach (string strId in ids.Split(','))
{
long id = long.Parse(strId);
_cache.Add(new UniverseDetails
{
Data = gameDetailResponse.Data.Where(x => x.Id == id).First(),
Thumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == id).First(),
});
}
}
}
}

View File

@ -9,10 +9,15 @@ namespace Bloxstrap.Models.Manifest
public class Package
{
public string Name { get; set; } = "";
public string Signature { get; set; } = "";
public int PackedSize { get; set; }
public int Size { get; set; }
public string DownloadPath => Path.Combine(Paths.Downloads, Signature);
public override string ToString()
{
return $"[{Signature}] {Name}";

View File

@ -8,9 +8,9 @@ namespace Bloxstrap.Models.Manifest
{
public class PackageManifest : List<Package>
{
private PackageManifest(string data)
public PackageManifest(string data)
{
using StringReader reader = new StringReader(data);
using var reader = new StringReader(data);
string? version = reader.ReadLine();
if (version != "v0")
@ -46,13 +46,5 @@ namespace Bloxstrap.Models.Manifest
});
}
}
public static async Task<PackageManifest> Get(string versionGuid)
{
string pkgManifestUrl = RobloxDeployment.GetLocation($"/{versionGuid}-rbxPkgManifest.txt");
var pkgManifestData = await App.HttpClient.GetStringAsync(pkgManifestUrl);
return new PackageManifest(pkgManifestData);
}
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models.Persistable
{
public class AppState
{
public string VersionGuid { get; set; } = string.Empty;
public Dictionary<string, string> PackageHashes { get; set; } = new();
public int Size { get; set; }
}
}

View File

@ -1,6 +1,6 @@
using System.Collections.ObjectModel;
namespace Bloxstrap.Models
namespace Bloxstrap.Models.Persistable
{
public class Settings
{
@ -25,6 +25,5 @@ namespace Bloxstrap.Models
// mod preset configuration
public bool UseDisableAppPatch { get; set; } = false;
public bool DisableFullscreenOptimizations { get; set; } = false;
}
}

View File

@ -0,0 +1,17 @@
namespace Bloxstrap.Models.Persistable
{
public class State
{
public bool ShowFFlagEditorWarning { get; set; } = true;
public bool PromptWebView2Install { get; set; } = true;
public AppState Player { get; set; } = new();
public AppState Studio { get; set; } = new();
public WindowState SettingsWindow { get; set; } = new();
public List<string> ModManifest { get; set; } = new();
}
}

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.Models.Persistable
{
public class WindowState
{
public double Width { get; set; }
public double Height { get; set; }
public double Left { get; set; }
public double Top { get; set; }
}
}

View File

@ -29,12 +29,16 @@ namespace Bloxstrap.Models.SettingTasks.Base
set
{
App.PendingSettingTasks[Name] = this;
_newState = value;
if (Changed)
App.PendingSettingTasks[Name] = this;
else
App.PendingSettingTasks.Remove(Name);
}
}
public override bool Changed => NewState != OriginalState;
public override bool Changed => _newState != OriginalState;
public BoolBaseTask(string prefix, string name) : base(prefix, name) { }
}

View File

@ -23,12 +23,16 @@
set
{
App.PendingSettingTasks[Name] = this;
_newState = value;
if (Changed)
App.PendingSettingTasks[Name] = this;
else
App.PendingSettingTasks.Remove(Name);
}
}
public override bool Changed => !NewState.Equals(OriginalState);
public override bool Changed => !_newState.Equals(OriginalState);
public IEnumerable<T> Selections { get; private set; }
= Enum.GetValues(typeof(T)).Cast<T>().OrderBy(x =>

View File

@ -29,12 +29,16 @@ namespace Bloxstrap.Models.SettingTasks.Base
set
{
App.PendingSettingTasks[Name] = this;
_newState = value;
if (Changed)
App.PendingSettingTasks[Name] = this;
else
App.PendingSettingTasks.Remove(Name);
}
}
public override bool Changed => NewState != OriginalState;
public override bool Changed => _newState != OriginalState;
public StringBaseTask(string prefix, string name) : base(prefix, name) { }
}

View File

@ -52,9 +52,12 @@ namespace Bloxstrap.Models.SettingTasks
{
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(
String.Format(Strings.Menu_Mods_Presets_EmojiType_Error, ex.Message),
MessageBoxImage.Warning);
Frontend.ShowConnectivityDialog(
String.Format(Strings.Dialog_Connectivity_UnableToConnect, "GitHub"),
$"{Strings.Menu_Mods_Presets_EmojiType_Error}\n\n{Strings.Dialog_Connectivity_TryAgainLater}",
MessageBoxImage.Warning,
ex
);
}
}
else if (query is not null && query.Any())

View File

@ -1,4 +1,5 @@
using Bloxstrap.Models.SettingTasks.Base;
using Bloxstrap.Models.Entities;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks
{

View File

@ -1,4 +1,5 @@
using Bloxstrap.Models.SettingTasks.Base;
using Bloxstrap.Models.Entities;
using Bloxstrap.Models.SettingTasks.Base;
namespace Bloxstrap.Models.SettingTasks
{

View File

@ -1,17 +0,0 @@
namespace Bloxstrap.Models
{
public class State
{
public bool ShowFFlagEditorWarning { get; set; } = true;
[Obsolete("Use PlayerVersionGuid instead", true)]
public string VersionGuid { set { PlayerVersionGuid = value; } }
public string PlayerVersionGuid { get; set; } = "";
public string StudioVersionGuid { get; set; } = "";
public int PlayerSize { get; set; } = 0;
public int StudioSize { get; set; } = 0;
public List<string> ModManifest { get; set; } = new();
}
}

View File

@ -2,4 +2,4 @@
FlashWindow
GetWindowLong
SetWindowLong
EnumDisplaySettings
SHObjectProperties

View File

@ -4,6 +4,7 @@
{
// note that these are directories that aren't tethered to the basedirectory
// so these can safely be called before initialization
public static string Temp => Path.Combine(Path.GetTempPath(), App.ProjectName);
public static string UserProfile => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
public static string LocalAppData => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
public static string Desktop => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
@ -12,12 +13,15 @@
public static string Process => Environment.ProcessPath!;
public static string TempUpdates => Path.Combine(Temp, "Updates");
public static string TempLogs => Path.Combine(Temp, "Logs");
public static string Base { get; private set; } = "";
public static string Downloads { get; private set; } = "";
public static string Logs { get; private set; } = "";
public static string Integrations { get; private set; } = "";
public static string Versions { get; private set; } = "";
public static string Modifications { get; private set; } = "";
public static string Roblox { get; private set; } = "";
public static string Application { get; private set; } = "";
@ -31,8 +35,8 @@
Downloads = Path.Combine(Base, "Downloads");
Logs = Path.Combine(Base, "Logs");
Integrations = Path.Combine(Base, "Integrations");
Versions = Path.Combine(Base, "Versions");
Modifications = Path.Combine(Base, "Modifications");
Roblox = Path.Combine(Base, "Roblox");
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
}

View File

@ -26,6 +26,10 @@
"Bloxstrap (Studio Launch)": {
"commandName": "Project",
"commandLineArgs": "-studio"
},
"Bloxstrap (Watcher)": {
"commandName": "Project",
"commandLineArgs": "-watcher"
}
}
}

View File

@ -61,25 +61,70 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to left game.
/// Looks up a localized string similar to Licenses.
/// </summary>
public static string ActivityTracker_LeftGame {
public static string About_Licenses_Title {
get {
return ResourceManager.GetString("ActivityTracker.LeftGame", resourceCulture);
return ResourceManager.GetString("About.Licenses.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to lookup failed.
/// Looks up a localized string similar to These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!.
/// </summary>
public static string ActivityTracker_LookupFailed {
public static string About_Supporters_Description {
get {
return ResourceManager.GetString("ActivityTracker.LookupFailed", resourceCulture);
return ResourceManager.GetString("About.Supporters.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap was unable to auto-update to {0}. Please update it manually by downloading and running the latest release from the GitHub page..
/// Looks up a localized string similar to Supporters.
/// </summary>
public static string About_Supporters_Title {
get {
return ResourceManager.GetString("About.Supporters.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to About Bloxstrap.
/// </summary>
public static string About_Title {
get {
return ResourceManager.GetString("About.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Translators.
/// </summary>
public static string About_Translators_Title {
get {
return ResourceManager.GetString("About.Translators.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The server location could not be queried. You may be joining games too quickly..
/// </summary>
public static string ActivityWatcher_LocationQueryFailed {
get {
return ResourceManager.GetString("ActivityWatcher.LocationQueryFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your current game will not show on your Discord presence because an error occurred when loading the game information..
/// </summary>
public static string ActivityWatcher_RichPresenceLoadFailed {
get {
return ResourceManager.GetString("ActivityWatcher.RichPresenceLoadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website..
/// </summary>
public static string Bootstrapper_AutoUpdateFailed {
get {
@ -96,33 +141,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to It&apos;s possible that something is preventing Bloxstrap from connecting to the internet. Please check and try again..
/// </summary>
public static string Bootstrapper_Connectivity_Preventing {
get {
return ResourceManager.GetString("Bootstrapper.Connectivity.Preventing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox may be down right now. See status.roblox.com for more information. Please try again later..
/// </summary>
public static string Bootstrapper_Connectivity_RobloxDown {
get {
return ResourceManager.GetString("Bootstrapper.Connectivity.RobloxDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap timed out when trying to connect to three different Roblox deployment mirrors, indicating a poor internet connection. Please try again later..
/// </summary>
public static string Bootstrapper_Connectivity_TimedOut {
get {
return ResourceManager.GetString("Bootstrapper.Connectivity.TimedOut", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu..
/// </summary>
@ -152,6 +170,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Failed to save {0}: {1}.
/// </summary>
public static string Bootstrapper_JsonManagerSaveFailed {
get {
return ResourceManager.GetString("Bootstrapper.JsonManagerSaveFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap does not have enough disk space to download and install Roblox. Please free up some disk space and try again..
/// </summary>
@ -423,11 +450,11 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Locate log file.
/// Looks up a localized string similar to Loading, please wait....
/// </summary>
public static string Common_LocateLogFile {
public static string Common_Loading {
get {
return ResourceManager.GetString("Common.LocateLogFile", resourceCulture);
return ResourceManager.GetString("Common.Loading", resourceCulture);
}
}
@ -467,6 +494,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Could not load data because of a network error..
/// </summary>
public static string Common_NetworkError {
get {
return ResourceManager.GetString("Common.NetworkError", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New.
/// </summary>
@ -485,6 +521,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Not available.
/// </summary>
public static string Common_NotAvailable {
get {
return ResourceManager.GetString("Common.NotAvailable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to OK.
/// </summary>
@ -494,6 +539,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Open log file.
/// </summary>
public static string Common_OpenLogFile {
get {
return ResourceManager.GetString("Common.OpenLogFile", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Presets.
/// </summary>
@ -512,6 +566,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option..
/// </summary>
public static string Common_RobloxNotInstalled {
get {
return ResourceManager.GetString("Common.RobloxNotInstalled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shortcuts.
/// </summary>
@ -603,11 +666,29 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Open log file.
/// Looks up a localized string similar to Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them..
/// </summary>
public static string ContextMenu_OpenLogFile {
public static string ContextMenu_GameHistory_Description {
get {
return ResourceManager.GetString("ContextMenu.OpenLogFile", resourceCulture);
return ResourceManager.GetString("ContextMenu.GameHistory.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Rejoin.
/// </summary>
public static string ContextMenu_GameHistory_Rejoin {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Rejoin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game history.
/// </summary>
public static string ContextMenu_GameHistory_Title {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Title", resourceCulture);
}
}
@ -620,15 +701,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to See server details.
/// </summary>
public static string ContextMenu_SeeServerDetails {
get {
return ResourceManager.GetString("ContextMenu.SeeServerDetails", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy Instance ID.
/// </summary>
@ -647,15 +719,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Loading, please wait....
/// </summary>
public static string ContextMenu_ServerInformation_Loading {
get {
return ResourceManager.GetString("ContextMenu.ServerInformation.Loading", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Location.
/// </summary>
@ -765,6 +828,60 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Something is likely preventing Bloxstrap from connecting to the internet..
/// </summary>
public static string Dialog_Connectivity_Preventing {
get {
return ResourceManager.GetString("Dialog.Connectivity.Preventing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox may be down right now. See {0} for more information..
/// </summary>
public static string Dialog_Connectivity_RobloxDown {
get {
return ResourceManager.GetString("Dialog.Connectivity.RobloxDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Because Roblox needs to be installed or upgraded, Bloxstrap cannot continue..
/// </summary>
public static string Dialog_Connectivity_RobloxUpgradeNeeded {
get {
return ResourceManager.GetString("Dialog.Connectivity.RobloxUpgradeNeeded", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to For this launch, Roblox will not be checked for upgrades, and changes to mods will not be applied..
/// </summary>
public static string Dialog_Connectivity_RobloxUpgradeSkip {
get {
return ResourceManager.GetString("Dialog.Connectivity.RobloxUpgradeSkip", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} may be down right now..
/// </summary>
public static string Dialog_Connectivity_ServiceDown {
get {
return ResourceManager.GetString("Dialog.Connectivity.ServiceDown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The connection timed out, which could indicate a poor internet connection or a firewall block..
/// </summary>
public static string Dialog_Connectivity_TimedOut {
get {
return ResourceManager.GetString("Dialog.Connectivity.TimedOut", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connectivity error.
/// </summary>
@ -775,7 +892,16 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap is unable to connect to Roblox.
/// Looks up a localized string similar to Please try again later..
/// </summary>
public static string Dialog_Connectivity_TryAgainLater {
get {
return ResourceManager.GetString("Dialog.Connectivity.TryAgainLater", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap is unable to connect to {0}.
/// </summary>
public static string Dialog_Connectivity_UnableToConnect {
get {
@ -850,6 +976,17 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The chosen bootstrapper icon could not be loaded.
///
///{0}.
/// </summary>
public static string Dialog_IconLoadFailed {
get {
return ResourceManager.GetString("Dialog.IconLoadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose preferred language.
/// </summary>
@ -868,6 +1005,33 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Roblox has crashed..
/// </summary>
public static string Dialog_PlayerError_Crash {
get {
return ResourceManager.GetString("Dialog.PlayerError.Crash", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox failed to launch..
/// </summary>
public static string Dialog_PlayerError_FailedLaunch {
get {
return ResourceManager.GetString("Dialog.PlayerError.FailedLaunch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please read the following help information, which will open in your web browser when you close this dialog..
/// </summary>
public static string Dialog_PlayerError_HelpInformation {
get {
return ResourceManager.GetString("Dialog.PlayerError.HelpInformation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Early 2015.
/// </summary>
@ -1139,7 +1303,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Private.
/// Looks up a localized string similar to Private server.
/// </summary>
public static string Enums_ServerType_Private {
get {
@ -1148,7 +1312,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Public.
/// Looks up a localized string similar to Public server.
/// </summary>
public static string Enums_ServerType_Public {
get {
@ -1157,7 +1321,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Reserved.
/// Looks up a localized string similar to Reserved server.
/// </summary>
public static string Enums_ServerType_Reserved {
get {
@ -1249,6 +1413,17 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The version of Bloxstrap you&apos;ve launched is older than the version you currently have installed.
///Issues may occur and your settings may be altered. A reinstall is recommended.
///Are you sure you want to continue?.
/// </summary>
public static string InstallChecker_VersionLessThanInstalled {
get {
return ResourceManager.GetString("InstallChecker.VersionLessThanInstalled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Will drop you into the desktop app once everything&apos;s done.
/// </summary>
@ -1309,6 +1484,17 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap has been installed to this location before and is still present, however the installer cannot overwrite the old executable.
///
///Please manually delete Bloxstrap.exe from the install location or try restarting your system, and then retry installation afterwards..
/// </summary>
public static string Installer_Install_CannotOverwrite {
get {
return ResourceManager.GetString("Installer.Install.CannotOverwrite", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Existing data found. Your mods and settings will be restored..
/// </summary>
@ -1354,6 +1540,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Are you sure you want to cancel the installation?.
/// </summary>
public static string Installer_ShouldCancel {
get {
return ResourceManager.GetString("Installer.ShouldCancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap Installer.
/// </summary>
@ -1366,7 +1561,7 @@ namespace Bloxstrap.Resources {
/// <summary>
/// Looks up a localized string similar to Thank you for downloading Bloxstrap.
///
///You should have gotten it from either {0} or {1}. Those are the only official websites to get it from.
///You should have downloaded it from either {0} or {1}. Those are the only official websites to get it from. It is your responsibility to ensure you download from an official source.
///
///This installation process will be quick and simple, and you will be able to configure any of Bloxstrap&apos;s settings after installation..
/// </summary>
@ -1403,6 +1598,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Your Fast Flags could not be loaded. They have been reset to the default configuration..
/// </summary>
public static string JsonManager_FastFlagsLoadFailed {
get {
return ResourceManager.GetString("JsonManager.FastFlagsLoadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your Settings could not be loaded. They have been reset to the default configuration..
/// </summary>
public static string JsonManager_SettingsLoadFailed {
get {
return ResourceManager.GetString("JsonManager.SettingsLoadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Configure settings.
/// </summary>
@ -2151,24 +2364,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Allows you to configure 21 different quality levels instead of 10..
/// </summary>
public static string Menu_FastFlags_Presets_AltGraphicsSelector_Description {
get {
return ResourceManager.GetString("Menu.FastFlags.Presets.AltGraphicsSelector.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use advanced graphics quality selector.
/// </summary>
public static string Menu_FastFlags_Presets_AltGraphicsSelector_Title {
get {
return ResourceManager.GetString("Menu.FastFlags.Presets.AltGraphicsSelector.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Rendering and Graphics.
/// </summary>
@ -2472,15 +2667,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to e.g. C:\Windows\System32\cmd.exe.
/// </summary>
public static string Menu_Integrations_Custom_AppLocation_Placeholder {
get {
return ResourceManager.GetString("Menu.Integrations.Custom.AppLocation.Placeholder", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Auto close when Roblox closes.
/// </summary>
@ -2509,7 +2695,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to e.g. /k echo Roblox is running!.
/// Looks up a localized string similar to Roblox is running!.
/// </summary>
public static string Menu_Integrations_Custom_LaunchArgs_Placeholder {
get {
@ -2590,7 +2776,25 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running..
/// Looks up a localized string similar to When in-game, you&apos;ll be able to see where your server is located via [ipinfo.io]({0})..
/// </summary>
public static string Menu_Integrations_QueryServerLocation_Description {
get {
return ResourceManager.GetString("Menu.Integrations.QueryServerLocation.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Query server location.
/// </summary>
public static string Menu_Integrations_QueryServerLocation_Title {
get {
return ResourceManager.GetString("Menu.Integrations.QueryServerLocation.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running. [Find out more]({0})..
/// </summary>
public static string Menu_Integrations_RequiresActivityTracking {
get {
@ -2616,24 +2820,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to When you join a game, you&apos;ll be notified of where your server&apos;s located. Won&apos;t show in fullscreen..
/// </summary>
public static string Menu_Integrations_ShowServerDetails_Description {
get {
return ResourceManager.GetString("Menu.Integrations.ShowServerDetails.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to See server location when joining a game.
/// </summary>
public static string Menu_Integrations_ShowServerDetails_Title {
get {
return ResourceManager.GetString("Menu.Integrations.ShowServerDetails.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Integrations.
/// </summary>
@ -2661,6 +2847,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Configure application parameters such as DPI scaling behaviour and [fullscreen optimizations]({0})..
/// </summary>
public static string Menu_Mods_Misc_CompatibilitySettings_Description {
get {
return ResourceManager.GetString("Menu.Mods.Misc.CompatibilitySettings.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Manage compatibility settings.
/// </summary>
public static string Menu_Mods_Misc_CompatibilitySettings_Title {
get {
return ResourceManager.GetString("Menu.Mods.Misc.CompatibilitySettings.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose font....
/// </summary>
@ -2752,9 +2956,7 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to The emoji mod could not be applied because of a network error during download.
///
///{0}.
/// Looks up a localized string similar to The emoji mod can not be applied at this time..
/// </summary>
public static string Menu_Mods_Presets_EmojiType_Error {
get {
@ -2942,6 +3144,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to You have unsaved changes. Are you sure you want to close without saving?.
/// </summary>
public static string Menu_UnsavedChanges {
get {
return ResourceManager.GetString("Menu.UnsavedChanges", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to They&apos;ll be kept where Bloxstrap was installed, and will automatically be restored on a reinstall..
/// </summary>

View File

@ -117,26 +117,20 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ActivityTracker.LeftGame" xml:space="preserve">
<value>left game</value>
</data>
<data name="ActivityTracker.LookupFailed" xml:space="preserve">
<value>lookup failed</value>
</data>
<data name="Bootstrapper.AutoUpdateFailed" xml:space="preserve">
<value>Bloxstrap was unable to auto-update to {0}. Please update it manually by downloading and running the latest release from the GitHub page.</value>
<value>Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website.</value>
</data>
<data name="Bootstrapper.ConfirmLaunch" xml:space="preserve">
<value>Roblox is currently running, and launching another instance will close it. Are you sure you want to continue launching?</value>
</data>
<data name="Bootstrapper.Connectivity.Preventing" xml:space="preserve">
<value>It's possible that something is preventing Bloxstrap from connecting to the internet. Please check and try again.</value>
<data name="Dialog.Connectivity.Preventing" xml:space="preserve">
<value>Something is likely preventing Bloxstrap from connecting to the internet.</value>
</data>
<data name="Bootstrapper.Connectivity.RobloxDown" xml:space="preserve">
<value>Roblox may be down right now. See status.roblox.com for more information. Please try again later.</value>
<data name="Dialog.Connectivity.RobloxDown" xml:space="preserve">
<value>Roblox may be down right now. See {0} for more information.</value>
</data>
<data name="Bootstrapper.Connectivity.TimedOut" xml:space="preserve">
<value>Bloxstrap timed out when trying to connect to three different Roblox deployment mirrors, indicating a poor internet connection. Please try again later.</value>
<data name="Dialog.Connectivity.TimedOut" xml:space="preserve">
<value>The connection timed out, which could indicate a poor internet connection or a firewall block.</value>
</data>
<data name="Bootstrapper.EmojiPresetFetchFailed" xml:space="preserve">
<value>Could not apply the {0} emoji mod preset because of a network error. To try again, please reconfigure the option in the Bloxstrap Menu.</value>
@ -226,8 +220,8 @@ Your ReShade configuration files will still be saved, and you can locate them by
<data name="Common.ImportJson" xml:space="preserve">
<value>Import JSON</value>
</data>
<data name="Common.LocateLogFile" xml:space="preserve">
<value>Locate log file</value>
<data name="Common.OpenLogFile" xml:space="preserve">
<value>Open log file</value>
</data>
<data name="Common.Miscellaneous" xml:space="preserve">
<value>Miscellaneous</value>
@ -268,21 +262,12 @@ Your ReShade configuration files will still be saved, and you can locate them by
<data name="ContextMenu.CopyDeeplinkInvite" xml:space="preserve">
<value>Copy invite deeplink</value>
</data>
<data name="ContextMenu.OpenLogFile" xml:space="preserve">
<value>Open log file</value>
</data>
<data name="ContextMenu.SeeServerDetails" xml:space="preserve">
<value>See server details</value>
</data>
<data name="ContextMenu.ServerInformation.CopyInstanceId" xml:space="preserve">
<value>Copy Instance ID</value>
</data>
<data name="ContextMenu.ServerInformation.InstanceId" xml:space="preserve">
<value>Instance ID</value>
</data>
<data name="ContextMenu.ServerInformation.Loading" xml:space="preserve">
<value>Loading, please wait...</value>
</data>
<data name="ContextMenu.ServerInformation.Location" xml:space="preserve">
<value>Location</value>
</data>
@ -309,7 +294,7 @@ Click for more information</value>
<value>Connectivity error</value>
</data>
<data name="Dialog.Connectivity.UnableToConnect" xml:space="preserve">
<value>Bloxstrap is unable to connect to Roblox</value>
<value>Bloxstrap is unable to connect to {0}</value>
</data>
<data name="Dialog.Exception.CopyLogContents" xml:space="preserve">
<value>Copy log contents</value>
@ -404,13 +389,13 @@ If not, then please report this exception through a [GitHub issue]({1}) along wi
<value>Direct3D 11</value>
</data>
<data name="Enums.ServerType.Private" xml:space="preserve">
<value>Private</value>
<value>Private server</value>
</data>
<data name="Enums.ServerType.Public" xml:space="preserve">
<value>Public</value>
<value>Public server</value>
</data>
<data name="Enums.ServerType.Reserved" xml:space="preserve">
<value>Reserved</value>
<value>Reserved server</value>
</data>
<data name="Enums.Theme.Dark" xml:space="preserve">
<value>Dark</value>
@ -652,12 +637,6 @@ Do NOT use this to import large "flag lists" made by other people that promise t
<value>Learn more about Fast Flags, what these presets do, and how to use them.</value>
<comment>Title is Common.Help</comment>
</data>
<data name="Menu.FastFlags.Presets.AltGraphicsSelector.Description" xml:space="preserve">
<value>Allows you to configure 21 different quality levels instead of 10.</value>
</data>
<data name="Menu.FastFlags.Presets.AltGraphicsSelector.Title" xml:space="preserve">
<value>Use advanced graphics quality selector</value>
</data>
<data name="Menu.FastFlags.Presets.D3DExclusiveFullscreenInfo" xml:space="preserve">
<value>Direct3D [exclusive fullscreen]({0}) using Alt+Enter is enabled by default.</value>
</data>
@ -730,9 +709,6 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Menu.Integrations.Custom.AppLocation" xml:space="preserve">
<value>Application Location</value>
</data>
<data name="Menu.Integrations.Custom.AppLocation.Placeholder" xml:space="preserve">
<value>e.g. C:\Windows\System32\cmd.exe</value>
</data>
<data name="Menu.Integrations.Custom.AutoClose" xml:space="preserve">
<value>Auto close when Roblox closes</value>
</data>
@ -743,7 +719,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<value>Launch Arguments</value>
</data>
<data name="Menu.Integrations.Custom.LaunchArgs.Placeholder" xml:space="preserve">
<value>e.g. /k echo Roblox is running!</value>
<value>Roblox is running!</value>
</data>
<data name="Menu.Integrations.Custom.NewIntegration" xml:space="preserve">
<value>New Integration</value>
@ -770,7 +746,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<value>Enable activity tracking</value>
</data>
<data name="Menu.Integrations.RequiresActivityTracking" xml:space="preserve">
<value>This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running.</value>
<value>This feature requires activity tracking to be enabled and the Discord desktop app to be installed and running. [Find out more]({0}).</value>
</data>
<data name="Menu.Integrations.ShowGameActivity.Description" xml:space="preserve">
<value>The Roblox game you're playing will be shown on your Discord profile. [Not working?]({0})</value>
@ -778,11 +754,11 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Menu.Integrations.ShowGameActivity.Title" xml:space="preserve">
<value>Show game activity</value>
</data>
<data name="Menu.Integrations.ShowServerDetails.Description" xml:space="preserve">
<value>When you join a game, you'll be notified of where your server's located. Won't show in fullscreen.</value>
<data name="Menu.Integrations.QueryServerLocation.Description" xml:space="preserve">
<value>When in-game, you'll be able to see where your server is located via [ipinfo.io]({0}).</value>
</data>
<data name="Menu.Integrations.ShowServerDetails.Title" xml:space="preserve">
<value>See server location when joining a game</value>
<data name="Menu.Integrations.QueryServerLocation.Title" xml:space="preserve">
<value>Query server location</value>
</data>
<data name="Menu.Integrations.Title" xml:space="preserve">
<value>Integrations</value>
@ -999,7 +975,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Installer.Welcome.MainText" xml:space="preserve">
<value>Thank you for downloading Bloxstrap.
You should have gotten it from either {0} or {1}. Those are the only official websites to get it from.
You should have downloaded it from either {0} or {1}. Those are the only official websites to get it from. It is your responsibility to ensure you download from an official source.
This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation.</value>
</data>
@ -1120,9 +1096,7 @@ If not, then please report this exception to the maintainers of this fork. Do NO
<value>Connected to reserved server</value>
</data>
<data name="Menu.Mods.Presets.EmojiType.Error" xml:space="preserve">
<value>The emoji mod could not be applied because of a network error during download.
{0}</value>
<value>The emoji mod can not be applied at this time.</value>
</data>
<data name="Dialog.AlreadyRunning.Installer" xml:space="preserve">
<value>Please wait for installation to finish.</value>
@ -1130,4 +1104,103 @@ If not, then please report this exception to the maintainers of this fork. Do NO
<data name="Dialog.AlreadyRunning.Uninstaller" xml:space="preserve">
<value>Please wait for uninstallation to finish.</value>
</data>
<data name="About.Title" xml:space="preserve">
<value>About Bloxstrap</value>
</data>
<data name="About.Licenses.Title" xml:space="preserve">
<value>Licenses</value>
</data>
<data name="About.Translators.Title" xml:space="preserve">
<value>Translators</value>
</data>
<data name="Menu.UnsavedChanges" xml:space="preserve">
<value>You have unsaved changes. Are you sure you want to close without saving?</value>
</data>
<data name="InstallChecker.VersionLessThanInstalled" xml:space="preserve">
<value>The version of Bloxstrap you've launched is older than the version you currently have installed.
Issues may occur and your settings may be altered. A reinstall is recommended.
Are you sure you want to continue?</value>
</data>
<data name="Dialog.PlayerError.FailedLaunch" xml:space="preserve">
<value>Roblox failed to launch.</value>
</data>
<data name="Dialog.PlayerError.Crash" xml:space="preserve">
<value>Roblox has crashed.</value>
</data>
<data name="Dialog.PlayerError.HelpInformation" xml:space="preserve">
<value>Please read the following help information, which will open in your web browser when you close this dialog.</value>
</data>
<data name="Common.NetworkError" xml:space="preserve">
<value>Could not load data because of a network error.</value>
</data>
<data name="Common.Loading" xml:space="preserve">
<value>Loading, please wait...</value>
</data>
<data name="About.Supporters.Title" xml:space="preserve">
<value>Supporters</value>
</data>
<data name="About.Supporters.Description" xml:space="preserve">
<value>These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!</value>
</data>
<data name="JsonManager.SettingsLoadFailed" xml:space="preserve">
<value>Your Settings could not be loaded. They have been reset to the default configuration.</value>
</data>
<data name="JsonManager.FastFlagsLoadFailed" xml:space="preserve">
<value>Your Fast Flags could not be loaded. They have been reset to the default configuration.</value>
</data>
<data name="ContextMenu.GameHistory.Title" xml:space="preserve">
<value>Game history</value>
</data>
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
<value>Rejoin</value>
</data>
<data name="ActivityWatcher.RichPresenceLoadFailed" xml:space="preserve">
<value>Your current game will not show on your Discord presence because an error occurred when loading the game information.</value>
</data>
<data name="ContextMenu.GameHistory.Description" xml:space="preserve">
<value>Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them.</value>
</data>
<data name="ActivityWatcher.LocationQueryFailed" xml:space="preserve">
<value>The server location could not be queried. You may be joining games too quickly.</value>
</data>
<data name="Dialog.Connectivity.ServiceDown" xml:space="preserve">
<value>{0} may be down right now.</value>
</data>
<data name="Dialog.Connectivity.TryAgainLater" xml:space="preserve">
<value>Please try again later.</value>
</data>
<data name="Dialog.Connectivity.RobloxUpgradeSkip" xml:space="preserve">
<value>For this launch, Roblox will not be checked for upgrades, and changes to mods will not be applied.</value>
</data>
<data name="Dialog.Connectivity.RobloxUpgradeNeeded" xml:space="preserve">
<value>Because Roblox needs to be installed or upgraded, Bloxstrap cannot continue.</value>
</data>
<data name="Installer.Install.CannotOverwrite" xml:space="preserve">
<value>Bloxstrap has been installed to this location before and is still present, however the installer cannot overwrite the old executable.
Please manually delete Bloxstrap.exe from the install location or try restarting your system, and then retry installation afterwards.</value>
</data>
<data name="Common.NotAvailable" xml:space="preserve">
<value>Not available</value>
</data>
<data name="Menu.Mods.Misc.CompatibilitySettings.Title" xml:space="preserve">
<value>Manage compatibility settings</value>
</data>
<data name="Menu.Mods.Misc.CompatibilitySettings.Description" xml:space="preserve">
<value>Configure application parameters such as DPI scaling behaviour and [fullscreen optimizations]({0}).</value>
</data>
<data name="Common.RobloxNotInstalled" xml:space="preserve">
<value>Roblox has not yet been installed. Please launch Roblox using Bloxstrap at least once before trying to use this option.</value>
</data>
<data name="Installer.ShouldCancel" xml:space="preserve">
<value>Are you sure you want to cancel the installation?</value>
</data>
<data name="Bootstrapper.JsonManagerSaveFailed" xml:space="preserve">
<value>Failed to save {0}: {1}</value>
</data>
<data name="Dialog.IconLoadFailed" xml:space="preserve">
<value>The chosen bootstrapper icon could not be loaded.
{0}</value>
</data>
</root>

View File

@ -15,6 +15,7 @@
private static readonly Dictionary<string, int> BaseUrls = new()
{
{ "https://setup.rbxcdn.com", 0 },
{ "https://setup-aws.rbxcdn.com", 2 },
{ "https://setup-ak.rbxcdn.com", 2 },
{ "https://roblox-setup.cachefly.net", 2 },
{ "https://s3.amazonaws.com/setup.roblox.com", 4 }
@ -22,7 +23,7 @@
private static async Task<string?> TestConnection(string url, int priority, CancellationToken token)
{
string LOG_IDENT = $"RobloxDeployment::TestConnection.{url}";
string LOG_IDENT = $"RobloxDeployment::TestConnection<{url}>";
await Task.Delay(priority * 1000, token);
@ -32,14 +33,14 @@
{
var response = await App.HttpClient.GetAsync($"{url}/versionStudio", token);
if (!response.IsSuccessStatusCode)
throw new HttpResponseException(response);
response.EnsureSuccessStatusCode();
// versionStudio is the version hash for the last MFC studio to be deployed.
// the response body should always be "version-012732894899482c".
string content = await response.Content.ReadAsStringAsync(token);
if (content != VersionStudioHash)
throw new Exception($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")");
throw new InvalidHTTPResponseException($"versionStudio response does not match (expected \"{VersionStudioHash}\", got \"{content}\")");
}
catch (TaskCanceledException)
{
@ -66,11 +67,10 @@
// returns null for success
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
var tokenSource = new CancellationTokenSource();
var exceptions = new List<Exception>();
var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, token)).ToList();
var tasks = (from entry in BaseUrls select TestConnection(entry.Key, entry.Value, tokenSource.Token)).ToList();
App.Logger.WriteLine(LOG_IDENT, "Testing connectivity...");
@ -127,7 +127,11 @@
App.Logger.WriteLine(LOG_IDENT, $"Getting deploy info for channel {channel}");
if (String.IsNullOrEmpty(channel))
channel = DefaultChannel;
string cacheKey = $"{channel}-{binaryType}";
ClientVersion clientVersion;
if (ClientVersionCache.ContainsKey(cacheKey))
@ -137,57 +141,37 @@
}
else
{
bool isDefaultChannel = String.Compare(channel, DefaultChannel, StringComparison.OrdinalIgnoreCase) == 0;
string path = $"/v2/client-version/{binaryType}";
if (String.Compare(channel, DefaultChannel, StringComparison.InvariantCultureIgnoreCase) != 0)
if (!isDefaultChannel)
path = $"/v2/client-version/{binaryType}/channel/{channel}";
HttpResponseMessage deployInfoResponse;
try
{
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettingscdn.roblox.com" + path);
clientVersion = await Http.GetJson<ClientVersion>("https://clientsettingscdn.roblox.com" + path);
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to contact clientsettingscdn! Falling back to clientsettings...");
App.Logger.WriteException(LOG_IDENT, ex);
deployInfoResponse = await App.HttpClient.GetAsync("https://clientsettings.roblox.com" + path);
clientVersion = await Http.GetJson<ClientVersion>("https://clientsettings.roblox.com" + path);
}
string rawResponse = await deployInfoResponse.Content.ReadAsStringAsync();
if (!deployInfoResponse.IsSuccessStatusCode)
// check if channel is behind LIVE
if (!isDefaultChannel)
{
// 400 = Invalid binaryType.
// 404 = Could not find version details for binaryType.
// 500 = Error while fetching version information.
// either way, we throw
var defaultClientVersion = await GetInfo(DefaultChannel);
App.Logger.WriteLine(LOG_IDENT,
"Failed to fetch deploy info!\r\n" +
$"\tStatus code: {deployInfoResponse.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new HttpResponseException(deployInfoResponse);
if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan)
clientVersion.IsBehindDefaultChannel = true;
}
clientVersion = JsonSerializer.Deserialize<ClientVersion>(rawResponse)!;
ClientVersionCache[cacheKey] = clientVersion;
}
// check if channel is behind LIVE
if (channel != DefaultChannel)
{
var defaultClientVersion = await GetInfo(DefaultChannel);
if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan)
clientVersion.IsBehindDefaultChannel = true;
}
ClientVersionCache[cacheKey] = clientVersion;
return clientVersion;
}
}

View File

@ -52,16 +52,7 @@ namespace Bloxstrap
string rawResponse = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
App.Logger.WriteLine(logIndent,
"Failed to fetch client settings!\r\n" +
$"\tStatus code: {response.StatusCode}\r\n" +
$"\tResponse: {rawResponse}"
);
throw new HttpResponseException(response);
}
response.EnsureSuccessStatusCode();
var clientSettings = JsonSerializer.Deserialize<ClientFlagSettings>(rawResponse);

View File

@ -32,11 +32,11 @@ namespace Bloxstrap.UI.Converters
return attribute.StaticName;
if (attribute.FromTranslation is not null)
return Resources.Strings.ResourceManager.GetStringSafe(attribute.FromTranslation);
return Strings.ResourceManager.GetStringSafe(attribute.FromTranslation);
}
}
return Resources.Strings.ResourceManager.GetStringSafe(String.Format(
return Strings.ResourceManager.GetStringSafe(String.Format(
"{0}.{1}",
typeName.Substring(typeName.IndexOf('.', StringComparison.Ordinal) + 1),
stringVal

View File

@ -0,0 +1,48 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.About.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="clr-namespace:Bloxstrap.UI.Elements.About.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
Title="{x:Static resources:Strings.About_Title}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
MinWidth="740"
Width="740"
Height="440"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TitleBar Padding="8" x:Name="RootTitleBar" Grid.Row="0" ForceShutdown="False" MinimizeToTray="False" UseSnapLayout="True" Title="{x:Static resources:Strings.About_Title}" Icon="pack://application:,,,/Bloxstrap.ico" />
<Grid x:Name="RootGrid" Grid.Row="1" Margin="12,12,0,0" Visibility="Visible">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:NavigationStore x:Name="RootNavigation" Grid.Row="1" Grid.Column="0" Margin="0,0,12,0" Frame="{Binding ElementName=RootFrame}" SelectedPageIndex="0">
<ui:NavigationStore.Items>
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_About_Title}" PageType="{x:Type pages:AboutPage}" Icon="QuestionCircle48" Tag="about" Margin="0,0,0,12" />
<ui:NavigationItem Content="{x:Static resources:Strings.About_Translators_Title}" PageType="{x:Type pages:TranslatorsPage}" Icon="Translate24" Tag="translators" Margin="0,0,0,12" />
<ui:NavigationItem Content="{x:Static resources:Strings.About_Licenses_Title}" PageType="{x:Type pages:LicensesPage}" Icon="Code24" Tag="licenses" Margin="0,0,0,12" />
</ui:NavigationStore.Items>
</ui:NavigationStore>
<Frame x:Name="RootFrame" Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" />
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,35 @@
using System.Windows.Controls;
using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts;
namespace Bloxstrap.UI.Elements.About
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : INavigationWindow
{
public MainWindow()
{
InitializeComponent();
App.Logger.WriteLine("MainWindow::MainWindow", "Initializing menu");
}
#region INavigationWindow methods
public Frame GetFrame() => RootFrame;
public INavigation GetNavigation() => RootNavigation;
public bool Navigate(Type pageType) => RootNavigation.Navigate(pageType);
public void SetPageService(IPageService pageService) => RootNavigation.PageService = pageService;
public void ShowWindow() => Show();
public void CloseWindow() => Close();
#endregion INavigationWindow methods
}
}

View File

@ -0,0 +1,222 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.About.Pages.AboutPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:enums="clr-namespace:Bloxstrap.Enums"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.About"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<!--d:DataContext="{d:DesignInstance dmodels:AboutViewModel, IsDesignTimeCreatable=True}"-->
<StackPanel Margin="0,0,14,14">
<StackPanel.Resources>
<FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
</StackPanel.Resources>
<Grid Margin="0,0,0,24" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="72" Height="72" VerticalAlignment="Center" Source="pack://application:,,,/Bloxstrap.ico" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Bloxstrap" Margin="0,0,4,0" FontSize="24" FontWeight="Medium" />
<TextBlock Grid.Column="1" Text="{Binding Version, Mode=OneTime}" Margin="4,0,0,2" VerticalAlignment="Bottom" FontSize="16" FontWeight="Medium" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</Grid>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
<WrapPanel HorizontalAlignment="Center" Orientation="Horizontal">
<ui:Anchor Margin="0,0,8,8" Content="{x:Static resources:Strings.Menu_About_GithubRepository}" Icon="Code24" NavigateUri="https://github.com/pizzaboxer/bloxstrap" />
<ui:Anchor Margin="0,0,8,8" Content="{x:Static resources:Strings.Menu_About_HelpInformation}" Icon="BookQuestionMark24" NavigateUri="https://github.com/pizzaboxer/bloxstrap/wiki" />
<ui:Anchor Margin="0,0,8,8" Content="{x:Static resources:Strings.Menu_About_ReportIssue}" Icon="BookExclamationMark24" NavigateUri="https://github.com/pizzaboxer/bloxstrap/issues" />
<ui:Anchor Margin="0,0,0,8" Content="{x:Static resources:Strings.Menu_About_DiscordServer}" Icon="Chat48" NavigateUri="https://discord.gg/nKjV3mGq6R" />
</WrapPanel>
<StackPanel Visibility="{Binding BuildInformationVisibility, Mode=OneTime}">
<TextBlock Text="Build Information" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="hmmmm" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Column="0" Margin="0,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Timestamp" />
<TextBlock Grid.Row="0" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildTimestamp, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Machine" />
<TextBlock Grid.Row="1" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.Machine, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Hash" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="2" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}">
<Hyperlink Foreground="{DynamicResource TextFillColorTertiaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="{Binding BuildCommitHashUrl, Mode=OneTime}">
<TextBlock Text="{Binding BuildMetadata.CommitHash, Mode=OneTime}" />
</Hyperlink>
</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Ref" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.CommitRef, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
</Grid>
</StackPanel>
<TextBlock Text="{x:Static resources:Strings.About_Supporters_Title}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<controls:MarkdownTextBlock MarkdownText="{Binding Source={x:Static resources:Strings.About_Supporters_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://ko-fi.com/boxerpizza'}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Unknown}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:ProgressRing Grid.Column="0" IsIndeterminate="True" />
<TextBlock Grid.Column="1" Margin="16,0,0,0" Text="{x:Static resources:Strings.Common_Loading}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="0,8,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Failed}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Source="pack://application:,,,/Resources/MessageBox/Error.png" Width="60" Height="60" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{x:Static resources:Strings.Common_NetworkError}" />
<TextBlock Text="{Binding SupportersLoadError, Mode=OneWay}" />
</StackPanel>
</Grid>
<ListView ItemsSource="{Binding Supporters, Mode=OneWay}" Margin="0,8,0,0" ScrollViewer.CanContentScroll="False" IsEnabled="False">
<ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Successful}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</ListView.Style>
<ListView.ItemTemplate>
<DataTemplate>
<ui:Card Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Height="32" Width="32" VerticalAlignment="Center">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding Image, IsAsync=True}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Margin="8,0,2,0" VerticalAlignment="Center" Text="{Binding Name}" />
</Grid>
</ui:Card>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding SupporterColumns}" Margin="-4" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:Expander Grid.Column="0" Margin="0,0,4,0" HeaderIcon="Code24" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_Code}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[Matt](https://github.com/bluepilledgreat)" />
<controls:MarkdownTextBlock MarkdownText="[lolmanurfunny](https://github.com/lolmanurfunny)" />
<controls:MarkdownTextBlock MarkdownText="[1011025m](https://github.com/1011025m)" />
<controls:MarkdownTextBlock MarkdownText="[EasternBloxxer](https://github.com/EasternBloxxer)" />
<controls:MarkdownTextBlock MarkdownText="[sitiom](https://github.com/sitiom)" />
<controls:MarkdownTextBlock MarkdownText="[Extravi](https://github.com/Extravi)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[swatTurret](https://github.com/swatTurret)" />
<controls:MarkdownTextBlock MarkdownText="[fxeP1](https://github.com/fxeP1)" />
<controls:MarkdownTextBlock MarkdownText="[Redusofficial](https://github.com/Redusofficial)" />
<controls:MarkdownTextBlock MarkdownText="[srthMD](https://github.com/srthMD)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Column="1" Margin="4,0,4,0" HeaderIcon="AppsAddIn28" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_FeatureIdeas}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[he3als](https://github.com/he3als)" />
<controls:MarkdownTextBlock MarkdownText="[NikSavchenk0](https://github.com/NikSavchenk0)" />
<controls:MarkdownTextBlock MarkdownText="[carter0nline](https://github.com/carter0nline)" />
<controls:MarkdownTextBlock MarkdownText="[lolmanurfunny](https://github.com/lolmanurfunny)" />
<controls:MarkdownTextBlock MarkdownText="[MehKako](https://github.com/MehKako)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[knivesofeylis](https://github.com/knivesofeylis)" />
<controls:MarkdownTextBlock MarkdownText="[sha4owz](https://github.com/sha4owz)" />
<controls:MarkdownTextBlock MarkdownText="[DaMlgNoodle](https://github.com/DaMlgNoodle)" />
<controls:MarkdownTextBlock MarkdownText="[nakoyasha](https://github.com/nakoyasha)" />
<controls:MarkdownTextBlock MarkdownText="[exurd](https://github.com/exurd)" />
<controls:MarkdownTextBlock MarkdownText="[0xFE0F](https://github.com/0xFE0F)" />
<controls:MarkdownTextBlock MarkdownText="[Tezos](https://github.com/GoingCrazyDude)" />
<controls:MarkdownTextBlock MarkdownText="[CfwSky](https://www.roblox.com/users/129425241/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ruubloo](https://www.roblox.com/users/158082266/profile)" />
<controls:MarkdownTextBlock MarkdownText="[toyoda165](https://www.roblox.com/users/923416649/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ShadowCodeX](https://github.com/ShadowCodeX-debug)" />
<controls:MarkdownTextBlock MarkdownText="[cub-has-injected](https://github.com/cub-has-injected)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Column="2" Margin="4,0,0,0" HeaderIcon="Heart16" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_SpecialThanks}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[MaximumADHD](https://github.com/MaximumADHD)" />
<controls:MarkdownTextBlock MarkdownText="[Multako](https://www.roblox.com/users/2485612194/profile)" />
<controls:MarkdownTextBlock MarkdownText="[axstin](https://github.com/axstin)" />
<controls:MarkdownTextBlock MarkdownText="[taskmanager](https://github.com/Mantaraix)" />
<controls:MarkdownTextBlock MarkdownText="[apprehensions](https://github.com/apprehensions)" />
</StackPanel>
</controls:Expander>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -1,6 +1,6 @@
using Bloxstrap.UI.ViewModels.Settings;
using Bloxstrap.UI.ViewModels.About;
namespace Bloxstrap.UI.Elements.Settings.Pages
namespace Bloxstrap.UI.Elements.About.Pages
{
/// <summary>
/// Interaction logic for AboutPage.xaml

View File

@ -0,0 +1,72 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.About.Pages.LicensesPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
<TextBlock Text="{x:Static resources:Strings.Menu_About_Licenses}" FontWeight="Medium" FontSize="24" Margin="0,0,0,16" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardAction Grid.Row="0" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="Bloxstrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/lepoco/wpfui/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="WPF-UI" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/securifybv/ShellLink/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="14" Text="ShellLink" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/Lachee/discord-rpc-csharp/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="DiscordRPC" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="13" Text="Roblox Studio Mod Manager" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="13" Text="SharpZipLib" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<StackPanel>
<TextBlock FontSize="14" Text="Markdig" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_BSD2}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.UI.Elements.About.Pages
{
/// <summary>
/// Interaction logic for LicensesPage.xaml
/// </summary>
public partial class LicensesPage
{
public LicensesPage()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,484 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.About.Pages.TranslatorsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
<TextBlock Text="{x:Static resources:Strings.About_Translators_Title}" FontWeight="Medium" FontSize="24" Margin="0,0,0,16" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Bahasa Indonesia" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="e7leopard" />
<TextBlock Text="hfzrk" />
<TextBlock Text="soudblox" />
<TextBlock Text="Bokmål" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="darkevilmage" />
<TextBlock Text="endsouls" />
<TextBlock Text="letoek" />
<!--<TextBlock Text="Čeština" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Chusaak" />
<TextBlock Text="DanyCraftCZ" />
<TextBlock Text="Franklin_Surten" />
<TextBlock Text="Galaxy_Gangster6" />
<TextBlock Text="jasperholo" />
<TextBlock Text="letoek" />
<TextBlock Text="noobkid4545" />
<TextBlock Text="radim776" />-->
<!-- <TextBlock Text="Dansk" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Momslayer98" />
<TextBlock Text="SirBlue" /> -->
<TextBlock Text="Deutsch" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="agent_phoenix" />
<TextBlock Text="avoidr" />
<TextBlock Text="hcjohnsgd" />
<TextBlock Text="hxmbt" />
<TextBlock Text="kale123" />
<TextBlock Text="Marvin_Chu" />
<TextBlock Text="nzxt_xll" />
<TextBlock Text="Nlx095" />
<TextBlock Text="Ph1lwtf" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="TEAM_LILA" />
<TextBlock Text="xDevoidx" />
<TextBlock Text="Español" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="4Xisty" />
<TextBlock Text="Botkid" />
<TextBlock Text="colocky" />
<TextBlock Text="D0N-B0T" />
<TextBlock Text="Dasp" />
<TextBlock Text="devyyxn" />
<TextBlock Text="ItzzExcel" />
<TextBlock Text="Ilayhlinda" />
<TextBlock Text="Ilushiouss" />
<TextBlock Text="jayces." />
<TextBlock Text="kroesufos" />
<TextBlock Text="LaiyLiod" />
<TextBlock Text="lyalekin" />
<TextBlock Text="NezumiDS" />
<TextBlock Text="NimuruDP" />
<TextBlock Text="NescafeCL" />
<TextBlock Text="Sw7gger" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="Urzy" />
<TextBlock Text="Filipino" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alphjectiom" />
<TextBlock Text="FlaminDaPotato" />
<TextBlock Text="RobiTheRobloxxer" />
<TextBlock Text="shadow01148" />
<TextBlock Text="Français" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="At0zDx" />
<TextBlock Text="built4aiming" />
<TextBlock Text="hahaloserz360" />
<TextBlock Text="K0ga" />
<TextBlock Text="Marcssebaa" />
<TextBlock Text="MommySernox" />
<TextBlock Text="owentempest8" />
<TextBlock Text="Subsical" />
<TextBlock Text="thatsirwaffles" />
<TextBlock Text="tyundrai" />
<TextBlock Text="Waza80" />
<TextBlock Text="Hindi (Latin)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="kunaljainop" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="Sur_" />
<TextBlock Text="Tezos" />
<TextBlock Text="TheTakuo" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Hrvatski" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Dzigos" />
<TextBlock Text="Koyroii" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Italiano" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="crow_zxcr" />
<TextBlock Text="devyyxn" />
<TextBlock Text="domenicoiacono" />
<TextBlock Text="hulabulaseop" />
<TextBlock Text="lord_moth" />
<TextBlock Text="loridori" />
<TextBlock Text="Lupo01" />
<TextBlock Text="Mogunars" />
<TextBlock Text="pave08" />
<TextBlock Text="spectrumbruh" />
<TextBlock Text="Lietuvių" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="cr0000142" />
<TextBlock Text="Duexo" />
<TextBlock Text="jessethedev" />
<TextBlock Text="Vac31." />
<TextBlock Text="Magyar" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="DynoPlays" />
<TextBlock Text="Elotomka" />
<TextBlock Text="xM4rk1" />
<!--<TextBlock Text="Nederlands" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cosmix" />
<TextBlock Text="Miwwzy" />
<TextBlock Text="Quickvision1" />
<TextBlock Text="ydboss" />-->
<TextBlock Text="Polski" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Eztaru" />
<TextBlock Text="lunar" />
<TextBlock Text="markontm" />
<TextBlock Text="my5q" />
<TextBlock Text="nemzik2137" />
<TextBlock Text="plexar" />
<TextBlock Text="r.efil" />
<TextBlock Text="Português (Brasil)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="anormalevis" />
<TextBlock Text="ErisvaldoBalbino" />
<TextBlock Text="G3xneric" />
<TextBlock Text="hnter" />
<TextBlock Text="issei_" />
<TextBlock Text="iyto.lk" />
<TextBlock Text="jhermesn" />
<TextBlock Text="JorgeDaPelada" />
<TextBlock Text="LwgoDev" />
<TextBlock Text="nunk7" />
<TextBlock Text="peke7374" />
<TextBlock Text="SeeF" />
<TextBlock Text="Snowzin1" />
<TextBlock Text="storm930" />
<TextBlock Text="toofastforboo" />
<TextBlock Text="VMOICE" />
<TextBlock Text="Ye4" />
<TextBlock Text="ZaPeZaPe" />
<TextBlock Text="Română" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Externalkinetics" />
<TextBlock Text="MonochromeAlex" />
<TextBlock Text="PlayerValley" />
<TextBlock Text="Smuki" />
<TextBlock Text="theflopperguy" />
<TextBlock Text="Suomi" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="gelaxiz" />
<TextBlock Text="jes5" />
<TextBlock Text="retromaxwell" />
<TextBlock Text="SomePinglord" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Svenska" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Axellse" />
<TextBlock Text="CroppingFlea479" />
<TextBlock Text="FishySpelar" />
<TextBlock Text="PineappleSnackz" />
<TextBlock Text="simonixen" />
<TextBlock Text="thatgurkangurk" />
<TextBlock Text="Tiếng Việt" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alexking2068" />
<TextBlock Text="baterblx" />
<TextBlock Text="Elytronn" />
<TextBlock Text="fox810891" />
<TextBlock Text="ItsPoofy" />
<TextBlock Text="Limer1" />
<TextBlock Text="makayu203332" />
<TextBlock Text="MEx2/j7x6" />
<TextBlock Text="NguyenDat208" />
<TextBlock Text="quanmequankk" />
<TextBlock Text="SomeRandomGuy175" />
<TextBlock Text="SonThanhVN" />
<TextBlock Text="teaanguyenn" />
<TextBlock Text="Veiiorra" />
<TextBlock Text="Türkçe" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="canny19133" />
<TextBlock Text="cfors55" />
<TextBlock Text="drakfreddy" />
<TextBlock Text="enisify" />
<TextBlock Text="jayces." />
<TextBlock Text="nyatie" />
<TextBlock Text="PixelArmy" />
<TextBlock Text="plants8332" />
<TextBlock Text="r02" />
<TextBlock Text="siyamicik" />
<TextBlock Text="ydboss" />
<TextBlock Text="Українська" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="9zh" />
<TextBlock Text="andrey3569s" />
<TextBlock Text="DexterBloxxer" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="maksimvlad7" />
<TextBlock Text="Босански" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cortex_1" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Ren" />
<TextBlock Text="Български" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="GrafitNiki" />
<TextBlock Text="sidefrappe" />
<TextBlock Text="Русский" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="3tcy" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="andBroz" />
<TextBlock Text="alexneonwithglue" />
<TextBlock Text="AnonymousDudeLOL123" />
<TextBlock Text="aperna_of_the_ticks" />
<TextBlock Text="arsenijveselov77" />
<TextBlock Text="Art3mLapa" />
<TextBlock Text="cherkash" />
<TextBlock Text="cub-has-injected" />
<TextBlock Text="dallyuser" />
<TextBlock Text="Dr1mG" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="fxstyxx" />
<TextBlock Text="Gustodd4202" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="harababura" />
<TextBlock Text="ImperialRhyme" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="IStoleYourCheese" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="khat7" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="kostyan" />
<TextBlock Text="Maks" />
<TextBlock Text="niktoyou" />
<TextBlock Text="nurgament2" />
<TextBlock Text="poflexim" />
<TextBlock Text="Prob1rka" />
<TextBlock Text="Provo" />
<TextBlock Text="Quenevelly" />
<TextBlock Text="sally13249" />
<TextBlock Text="simmon8800" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Skylan031" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Spuffio" />
<TextBlock Text="StraiF" />
<TextBlock Text="StrayCatSimb" />
<TextBlock Text="Voxel" />
<TextBlock Text="XonaShera" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Ziio123" />
<TextBlock Text="zor9na90000" />
</StackPanel>
<StackPanel Grid.Column="3">
<TextBlock Text="עברית" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="1cur1" />
<TextBlock Text="ilan0098" />
<TextBlock Text="koerga" />
<TextBlock Text="Sezei" />
<TextBlock Text="العربية" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="busguesjshbahsj" />
<TextBlock Text="cq2i" />
<TextBlock Text="mmiky" />
<TextBlock Text="mostafagamingx1" />
<TextBlock Text="RoRed" />
<TextBlock Text="Sakupen" />
<TextBlock Text="streoic" />
<TextBlock Text="uvq18" />
<TextBlock Text="wyfast" />
<TextBlock Text="Zida" />
<TextBlock Text="বাংলা" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Arnian" />
<TextBlock Text="Hydrated_panda" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="red_hi" />
<TextBlock Text="ภาษาไทย" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="._bonus_." />
<TextBlock Text="marc15772" />
<TextBlock Text="arthurwagon" />
<TextBlock Text="Sem1z" />
<TextBlock Text="xAom" />
<TextBlock Text="한국어" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="ADVI50R" />
<TextBlock Text="asd123456fghqwerty" />
<TextBlock Text="bacon1295" />
<TextBlock Text="NightPlay" />
<TextBlock Text="中文 (简体)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="Aling00" />
<TextBlock Text="Clock" />
<TextBlock Text="ERSN_CERROR" />
<TextBlock Text="Kirxvil" />
<TextBlock Text="Typel" />
<TextBlock Text="yuhaodatt" />
<TextBlock Text="中文 (廣東話)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="henrychu1125" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="shhh_op" />
<TextBlock Text="中文 (繁體)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="DXuwu" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="日本語" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="MintJapan" />
<TextBlock Text="pimeja7" />
<TextBlock Text="yixhuaa" />
</StackPanel>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.UI.Elements.About.Pages
{
/// <summary>
/// Interaction logic for TranslatorsPage.xaml
/// </summary>
public partial class TranslatorsPage
{
public TranslatorsPage()
{
InitializeComponent();
}
}
}

View File

@ -133,7 +133,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper.Base
public void Dialog_FormClosing(object sender, FormClosingEventArgs e)
{
if (!_isClosing)
Bootstrapper?.CancelInstall();
Bootstrapper?.Cancel();
}
#endregion

View File

@ -126,7 +126,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
private void Window_Closing(object sender, CancelEventArgs e)
{
if (!_isClosing)
Bootstrapper?.CancelInstall();
Bootstrapper?.Cancel();
}
#region IBootstrapperDialog Methods

View File

@ -105,7 +105,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
private void UiWindow_Closing(object sender, CancelEventArgs e)
{
if (!_isClosing)
Bootstrapper?.CancelInstall();
Bootstrapper?.Cancel();
}
#region IBootstrapperDialog Methods

View File

@ -123,7 +123,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
private void UiWindow_Closing(object sender, CancelEventArgs e)
{
if (!_isClosing)
Bootstrapper?.CancelInstall();
Bootstrapper?.Cancel();
}
#region IBootstrapperDialog Methods

View File

@ -43,7 +43,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
{
InitializeComponent();
this.buttonCancel.Text = Resources.Strings.Common_Cancel;
this.buttonCancel.Text = Strings.Common_Cancel;
ScaleWindow();
SetupDialog();

View File

@ -43,7 +43,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
InitializeComponent();
this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().ToBitmap();
this.buttonCancel.Text = Resources.Strings.Common_Cancel;
this.buttonCancel.Text = Strings.Common_Cancel;
ScaleWindow();
SetupDialog();

View File

@ -52,8 +52,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
this.BackColor = Color.FromArgb(25, 27, 29);
}
this.labelMessage.Text = Resources.Strings.Bootstrapper_StylePreview_TextCancel;
this.buttonCancel.Text = Resources.Strings.Common_Cancel;
this.labelMessage.Text = Strings.Bootstrapper_StylePreview_TextCancel;
this.buttonCancel.Text = Strings.Common_Cancel;
this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().GetSized(128, 128).ToBitmap();
SetupDialog();

View File

@ -49,7 +49,7 @@
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem x:Name="ServerDetailsMenuItem" Visibility="Collapsed" Click="ServerDetailsMenuItem_Click">
<MenuItem x:Name="ServerDetailsMenuItem" Visibility="Collapsed" Click="ServerDetailsMenuItem_Click">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
@ -57,11 +57,23 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="Info28"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_SeeServerDetails}" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Title}" />
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem x:Name="CloseRobloxMenuItem" Visibility="Collapsed" Click="CloseRobloxMenuItem_Click">
<MenuItem x:Name="GameHistoryMenuItem" Click="JoinLastServerMenuItem_Click" Visibility="Collapsed">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="History24"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" />
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem Click="CloseRobloxMenuItem_Click">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
@ -73,7 +85,7 @@
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem x:Name="LogTracerMenuItem" Click="LogTracerMenuItem_Click">
<MenuItem x:Name="LogTracerMenuItem" Visibility="Collapsed" Click="LogTracerMenuItem_Click">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
@ -81,7 +93,7 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="DocumentCatchUp20"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_OpenLogFile}" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.Common_OpenLogFile}" />
</Grid>
</MenuItem.Header>
</MenuItem>

View File

@ -2,16 +2,11 @@
using System.Windows.Controls;
using System.Windows.Interop;
using Wpf.Ui.Appearance;
using Wpf.Ui.Mvvm.Contracts;
using Wpf.Ui.Mvvm.Services;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using Bloxstrap.Integrations;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.Elements.ContextMenu
{
@ -22,31 +17,32 @@ namespace Bloxstrap.UI.Elements.ContextMenu
{
// i wouldve gladly done this as mvvm but turns out that data binding just does not work with menuitems for some reason so idk this sucks
private readonly ActivityWatcher? _activityWatcher;
private readonly DiscordRichPresence? _richPresenceHandler;
private readonly Watcher _watcher;
private ActivityWatcher? _activityWatcher => _watcher.ActivityWatcher;
private ServerInformation? _serverInformationWindow;
private int? _processId;
public MenuContainer(ActivityWatcher? activityWatcher, DiscordRichPresence? richPresenceHandler, int? processId)
private ServerHistory? _gameHistoryWindow;
public MenuContainer(Watcher watcher)
{
InitializeComponent();
_activityWatcher = activityWatcher;
_richPresenceHandler = richPresenceHandler;
_processId = processId;
_watcher = watcher;
if (_activityWatcher is not null)
{
_activityWatcher.OnLogOpen += ActivityWatcher_OnLogOpen;
_activityWatcher.OnGameJoin += ActivityWatcher_OnGameJoin;
_activityWatcher.OnGameLeave += ActivityWatcher_OnGameLeave;
}
if (_richPresenceHandler is not null)
if (_watcher.RichPresence is not null)
RichPresenceMenuItem.Visibility = Visibility.Visible;
if (_processId is not null)
CloseRobloxMenuItem.Visibility = Visibility.Visible;
if (!App.Settings.Prop.UseDisableAppPatch)
GameHistoryMenuItem.Visibility = Visibility.Visible;
VersionTextBlock.Text = $"{App.ProjectName} v{App.Version}";
}
@ -55,27 +51,33 @@ namespace Bloxstrap.UI.Elements.ContextMenu
{
if (_serverInformationWindow is null)
{
_serverInformationWindow = new ServerInformation(_activityWatcher!);
_serverInformationWindow = new(_watcher);
_serverInformationWindow.Closed += (_, _) => _serverInformationWindow = null;
}
if (!_serverInformationWindow.IsVisible)
_serverInformationWindow.Show();
_serverInformationWindow.Activate();
_serverInformationWindow.ShowDialog();
else
_serverInformationWindow.Activate();
}
private void ActivityWatcher_OnGameJoin(object? sender, EventArgs e)
public void ActivityWatcher_OnLogOpen(object? sender, EventArgs e) =>
Dispatcher.Invoke(() => LogTracerMenuItem.Visibility = Visibility.Visible);
public void ActivityWatcher_OnGameJoin(object? sender, EventArgs e)
{
if (_activityWatcher is null)
return;
Dispatcher.Invoke(() => {
if (_activityWatcher?.ActivityServerType == ServerType.Public)
if (_activityWatcher.Data.ServerType == ServerType.Public)
InviteDeeplinkMenuItem.Visibility = Visibility.Visible;
ServerDetailsMenuItem.Visibility = Visibility.Visible;
});
}
private void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
{
Dispatcher.Invoke(() => {
InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed;
@ -100,9 +102,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu
private void Window_Closed(object sender, EventArgs e) => App.Logger.WriteLine("MenuContainer::Window_Closed", "Context menu container closed");
private void RichPresenceMenuItem_Click(object sender, RoutedEventArgs e) => _richPresenceHandler?.SetVisibility(((MenuItem)sender).IsChecked);
private void RichPresenceMenuItem_Click(object sender, RoutedEventArgs e) => _watcher.RichPresence?.SetVisibility(((MenuItem)sender).IsChecked);
private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetDataObject($"roblox://experiences/start?placeId={_activityWatcher?.ActivityPlaceId}&gameInstanceId={_activityWatcher?.ActivityJobId}");
private void InviteDeeplinkMenuItem_Click(object sender, RoutedEventArgs e) => Clipboard.SetDataObject(_activityWatcher?.Data.GetInviteDeeplink());
private void ServerDetailsMenuItem_Click(object sender, RoutedEventArgs e) => ShowServerInformationWindow();
@ -110,13 +112,8 @@ namespace Bloxstrap.UI.Elements.ContextMenu
{
string? location = _activityWatcher?.LogLocation;
if (location is null)
{
Frontend.ShowMessageBox(Strings.ContextMenu_RobloxNotRunning, MessageBoxImage.Information);
return;
}
Utilities.ShellExecute(location);
if (location is not null)
Utilities.ShellExecute(location);
}
private void CloseRobloxMenuItem_Click(object sender, RoutedEventArgs e)
@ -130,9 +127,24 @@ namespace Bloxstrap.UI.Elements.ContextMenu
if (result != MessageBoxResult.Yes)
return;
using Process process = Process.GetProcessById((int)_processId!);
process.Kill();
process.Close();
_watcher.KillRobloxProcess();
}
private void JoinLastServerMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_activityWatcher is null)
throw new ArgumentNullException(nameof(_activityWatcher));
if (_gameHistoryWindow is null)
{
_gameHistoryWindow = new(_activityWatcher);
_gameHistoryWindow.Closed += (_, _) => _gameHistoryWindow = null;
}
if (!_gameHistoryWindow.IsVisible)
_gameHistoryWindow.ShowDialog();
else
_gameHistoryWindow.Activate();
}
}
}

View File

@ -0,0 +1,107 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.ContextMenu.ServerHistory"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.ContextMenu"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:enums="clr-namespace:Bloxstrap.Enums"
d:DataContext="{d:DesignInstance Type=models:ServerHistoryViewModel}"
mc:Ignorable="d"
Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}"
MinWidth="420"
MinHeight="420"
Width="580"
Height="420"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" x:Name="RootTitleBar" Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<TextBlock Grid.Row="1" Margin="16,8,16,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Description}" TextWrapping="Wrap" />
<TextBlock Grid.Row="2" Margin="16,8,16,0" Text="{Binding Error, Mode=OneWay}" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Failed}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</TextBlock.Style>
</TextBlock>
<Border Grid.Row="2">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Unknown}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Border.Style>
<ui:ProgressRing Grid.Row="1" IsIndeterminate="True" />
</Border>
<ListView Grid.Row="2" ItemsSource="{Binding GameHistory, Mode=OneWay}" Margin="8">
<ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Successful}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</ListView.Style>
<ListView.ItemTemplate>
<DataTemplate>
<ui:Card Padding="12">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="84" Height="84" CornerRadius="4">
<Border.Background>
<ImageBrush ImageSource="{Binding UniverseDetails.Thumbnail.ImageUrl, IsAsync=True}" />
</Border.Background>
</Border>
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{Binding UniverseDetails.Data.Name}" FontSize="18" FontWeight="Medium" />
<TextBlock Text="{Binding GameHistoryDescription}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:Button Margin="0,8,0,0" Content="{x:Static resources:Strings.ContextMenu_GameHistory_Rejoin}" Command="{Binding RejoinServerCommand}"
Appearance="Success" Foreground="White" IconForeground="White" Icon="Play28" IconFilled="True" />
</StackPanel>
</Grid>
</ui:Card>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Border Grid.Row="3" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,21 @@
using Bloxstrap.Integrations;
using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
{
/// <summary>
/// Interaction logic for ServerInformation.xaml
/// </summary>
public partial class ServerHistory
{
public ServerHistory(ActivityWatcher watcher)
{
var viewModel = new ServerHistoryViewModel(watcher);
viewModel.RequestCloseEvent += (_, _) => Close();
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -46,14 +46,14 @@
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_InstanceId}" />
<TextBlock Grid.Row="1" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding InstanceId, Mode=OneWay}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Location}" />
<TextBlock Grid.Row="2" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ServerLocation, Mode=OneWay}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,0,16,12" VerticalAlignment="Center" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Location}" Visibility="{Binding ServerLocationVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="2" Grid.Column="1" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Text="{Binding ServerLocation, Mode=OneWay}" Visibility="{Binding ServerLocationVisibility, Mode=OneTime}" />
</Grid>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button MinWidth="100" Content="{x:Static resources:Strings.ContextMenu_ServerInformation_CopyInstanceId}" Command="{Binding CopyInstanceIdCommand, Mode=OneTime}" />
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" Command="{Binding CloseWindowCommand, Mode=OneTime}" />
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
</StackPanel>
</Border>
</Grid>

View File

@ -22,9 +22,9 @@ namespace Bloxstrap.UI.Elements.ContextMenu
/// </summary>
public partial class ServerInformation
{
public ServerInformation(ActivityWatcher activityWatcher)
public ServerInformation(Watcher watcher)
{
DataContext = new ServerInformationViewModel(this, activityWatcher);
DataContext = new ServerInformationViewModel(watcher);
InitializeComponent();
}
}

View File

@ -12,6 +12,7 @@
Width="480"
MinHeight="0"
SizeToContent="Height"
Title="{x:Static resources:Strings.Dialog_Connectivity_Title}"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
@ -29,9 +30,9 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Top" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Error.png" />
<Image x:Name="IconImage" Grid.Column="0" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Top" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Error.png" />
<StackPanel Grid.Column="1">
<TextBlock x:Name="TitleTextBlock" Text="? is unable to connect to ?" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="TitleTextBlock" Text="? is unable to connect to ?" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" TextWrapping="Wrap" />
<controls:MarkdownTextBlock x:Name="DescriptionTextBlock" MarkdownText="?" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="{x:Static resources:Strings.Dialog_Connectivity_MoreInfo}" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<RichTextBox x:Name="ErrorRichTextBox" Padding="8" Margin="0,8,0,0" Block.LineHeight="2" FontFamily="Courier New" IsReadOnly="True" />

View File

@ -1,5 +1,7 @@
using System.Media;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Windows.Win32;
using Windows.Win32.Foundation;
@ -14,10 +16,41 @@ namespace Bloxstrap.UI.Elements.Dialogs
/// </summary>
public partial class ConnectivityDialog
{
public ConnectivityDialog(string title, string description, Exception exception)
public ConnectivityDialog(string title, string description, MessageBoxImage image, Exception exception)
{
InitializeComponent();
string? iconFilename = null;
SystemSound? sound = null;
switch (image)
{
case MessageBoxImage.Error:
iconFilename = "Error";
sound = SystemSounds.Hand;
break;
case MessageBoxImage.Question:
iconFilename = "Question";
sound = SystemSounds.Question;
break;
case MessageBoxImage.Warning:
iconFilename = "Warning";
sound = SystemSounds.Exclamation;
break;
case MessageBoxImage.Information:
iconFilename = "Information";
sound = SystemSounds.Asterisk;
break;
}
if (iconFilename is null)
IconImage.Visibility = Visibility.Collapsed;
else
IconImage.Source = new BitmapImage(new Uri($"pack://application:,,,/Resources/MessageBox/{iconFilename}.png"));
TitleTextBlock.Text = title;
DescriptionTextBlock.MarkdownText = description;
@ -28,7 +61,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
Close();
};
SystemSounds.Hand.Play();
sound?.Play();
Loaded += delegate
{

View File

@ -33,7 +33,7 @@
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="32" Height="32" Margin="0,0,15,0" VerticalAlignment="Top" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Error.png" />
<StackPanel Grid.Column="1">
<TextBlock Text="{x:Static resources:Strings.Dialog_Exception_Info_1}" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="{x:Static resources:Strings.Dialog_Exception_Info_1}" FontSize="18" Foreground="{DynamicResource TextFillColorPrimaryBrush}" TextWrapping="Wrap" />
<RichTextBox x:Name="ErrorRichTextBox" Padding="8" Margin="0,16,0,0" Block.LineHeight="2" FontFamily="Courier New" IsReadOnly="True" />
<controls:MarkdownTextBlock x:Name="HelpMessageMDTextBlock" Margin="0,16,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
@ -41,7 +41,7 @@
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button x:Name="LocateLogFileButton" Content="{x:Static resources:Strings.Common_LocateLogFile}" />
<Button x:Name="LocateLogFileButton" Content="{x:Static resources:Strings.Common_OpenLogFile}" />
<Button x:Name="CloseButton" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" Margin="12,0,0,0" />
</StackPanel>
</Border>

View File

@ -1,13 +1,11 @@
using System.Media;
using System.Web;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using Windows.Win32;
using Windows.Win32.Foundation;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.Elements.Dialogs
{
// hmm... do i use MVVM for this?
@ -26,19 +24,29 @@ namespace Bloxstrap.UI.Elements.Dialogs
if (!App.Logger.Initialized)
LocateLogFileButton.Content = Strings.Dialog_Exception_CopyLogContents;
string helpMessage = String.Format(Strings.Dialog_Exception_Info_2, "https://github.com/pizzaboxer/bloxstrap/wiki", "https://github.com/pizzaboxer/bloxstrap/issues/new?template=bug_report.yaml");
string repoUrl = $"https://github.com/{App.ProjectRepository}";
string wikiUrl = $"{repoUrl}/wiki";
if (String.IsNullOrEmpty(App.BuildMetadata.CommitHash))
helpMessage = String.Format(Strings.Dialog_Exception_Info_2_Alt, "https://github.com/pizzaboxer/bloxstrap/wiki");
string issueUrl = String.Format(
"{0}/issues/new?template=bug_report.yaml&title={1}&log={2}",
repoUrl,
HttpUtility.UrlEncode($"[BUG] {exception.GetType()}: {exception.Message}"),
HttpUtility.UrlEncode(String.Join('\n', App.Logger.History))
);
string helpMessage = String.Format(Strings.Dialog_Exception_Info_2, wikiUrl, issueUrl);
if (!App.IsActionBuild && !App.BuildMetadata.Machine.Contains("pizzaboxer", StringComparison.Ordinal))
helpMessage = String.Format(Strings.Dialog_Exception_Info_2_Alt, wikiUrl);
HelpMessageMDTextBlock.MarkdownText = helpMessage;
LocateLogFileButton.Click += delegate
{
if (App.Logger.Initialized)
Process.Start("explorer.exe", $"/select,\"{App.Logger.FileLocation}\"");
if (App.Logger.Initialized && !String.IsNullOrEmpty(App.Logger.FileLocation))
Utilities.ShellExecute(App.Logger.FileLocation);
else
Clipboard.SetDataObject(String.Join("\r\n", App.Logger.Backlog));
Clipboard.SetDataObject(String.Join("\r\n", App.Logger.History));
};
CloseButton.Click += delegate

View File

@ -41,7 +41,7 @@ namespace Bloxstrap.UI.Elements.Dialogs
case MessageBoxImage.Warning:
iconFilename = "Warning";
sound = SystemSounds.Asterisk;
sound = SystemSounds.Exclamation;
break;
case MessageBoxImage.Information:
@ -121,13 +121,13 @@ namespace Bloxstrap.UI.Elements.Dialogs
switch (result)
{
case MessageBoxResult.OK:
return Bloxstrap.Resources.Strings.Common_OK;
return Strings.Common_OK;
case MessageBoxResult.Cancel:
return Bloxstrap.Resources.Strings.Common_Cancel;
return Strings.Common_Cancel;
case MessageBoxResult.Yes:
return Bloxstrap.Resources.Strings.Common_Yes;
return Strings.Common_Yes;
case MessageBoxResult.No:
return Bloxstrap.Resources.Strings.Common_No;
return Strings.Common_No;
default:
Debug.Assert(false);
return result.ToString();

View File

@ -12,7 +12,7 @@
Title="Bloxstrap"
MinWidth="0"
MinHeight="0"
Width="320"
Width="580"
SizeToContent="Height"
ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
@ -24,31 +24,61 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" Title="Bloxstrap" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" ShowMinimize="False" ShowMaximize="False" Title="Bloxstrap" Icon="pack://application:,,,/Bloxstrap.ico" CanMaximize="False" KeyboardNavigation.TabNavigation="None" />
<StackPanel Grid.Row="1" Margin="12">
<TextBlock FontSize="24" Text="{x:Static resources:Strings.LaunchMenu_Title}" HorizontalAlignment="Center" Margin="0,0,0,16" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardAction Icon="ArrowRight12" Command="{Binding LaunchRobloxCommand, Mode=OneTime}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_LaunchRoblox}" />
<Grid Grid.Column="0" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" HorizontalAlignment="Center" Margin="0,0,0,32">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="64" Height="64" Source="pack://application:,,,/Bloxstrap.ico" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<TextBlock Text="Bloxstrap" FontSize="24" />
<TextBlock Text="{Binding Version, Mode=OneTime}" VerticalAlignment="Bottom" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="1" HorizontalAlignment="Center">
<ui:Hyperlink Icon="QuestionCircle48" Content="About Bloxstrap" Margin="0,0,0,8" Command="{Binding LaunchAboutCommand, Mode=OneTime}" />
<ui:Hyperlink Icon="Heart48" Content="Support us on Ko-fi!" NavigateUri="https://ko-fi.com/boxerpizza" />
</StackPanel>
</ui:CardAction>
</Grid>
<ui:CardAction Margin="0,8,0,0" Icon="Settings28" Command="{Binding LaunchSettingsCommand, Mode=OneTime}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_ConfigureSettings}" />
</StackPanel>
</ui:CardAction>
<StackPanel Grid.Column="1" Margin="16">
<ui:CardAction Icon="ArrowRight12" Command="{Binding LaunchRobloxCommand, Mode=OneTime}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_LaunchRoblox}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Margin="0,8,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Margin="0,8,0,0" Icon="Settings28" Command="{Binding LaunchSettingsCommand, Mode=OneTime}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_ConfigureSettings}" />
</StackPanel>
</ui:CardAction>
<TextBlock Margin="0,16,0,0" FontSize="12" Text="{Binding Version, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
<Border Margin="16" />
<ui:CardAction Margin="0,8,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.LaunchMenu_Wiki_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</StackPanel>
</Grid>
</Grid>
</base:WpfUiWindow>

View File

@ -9,6 +9,7 @@ using Bloxstrap.UI.Elements.Installer.Pages;
using Bloxstrap.UI.Elements.Base;
using System.Windows.Media.Animation;
using System.Reflection.Metadata.Ecma335;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.Elements.Installer
{
@ -102,7 +103,7 @@ namespace Bloxstrap.UI.Elements.Installer
if (Finished)
return;
var result = Frontend.ShowMessageBox("Are you sure you want to cancel the installation?", MessageBoxImage.Warning, MessageBoxButton.YesNo);
var result = Frontend.ShowMessageBox(Strings.Installer_ShouldCancel, MessageBoxImage.Warning, MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
e.Cancel = true;

View File

@ -15,7 +15,8 @@
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowStartupLocation="CenterScreen">
WindowStartupLocation="CenterScreen"
Closing="WpfUiWindow_Closing">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -60,7 +61,7 @@
<ui:NavigationItem Content="" PageType="{x:Type pages:FastFlagEditorWarningPage}" Tag="fastflageditorwarning" Visibility="Collapsed" x:Name="EditorWarningNavItem" />
</ui:NavigationFluent.Items>
<ui:NavigationFluent.Footer>
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_About_Title}" PageType="{x:Type pages:AboutPage}" Icon="QuestionCircle48" Tag="about" Margin="0,0,0,12" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_About_Title}" Icon="QuestionCircle48" Margin="0,0,0,12" Command="{Binding OpenAboutCommand, Mode=OneTime}" />
</ui:NavigationFluent.Footer>
</ui:NavigationFluent>
@ -93,7 +94,7 @@
<ui:Button Content="{x:Static resources:Strings.Menu_Save}" Appearance="Primary" Command="{Binding SaveSettingsCommand, Mode=OneWay}" />
</StatusBarItem>
<StatusBarItem Grid.Column="2" Padding="4,0,0,0">
<ui:Button Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
<ui:Button Content="{x:Static resources:Strings.Common_Close}" Command="{Binding CloseWindowCommand, Mode=OneWay}" />
</StatusBarItem>
</StatusBar>
</Grid>

View File

@ -1,6 +1,10 @@
using System.Windows.Controls;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Settings
@ -10,10 +14,14 @@ namespace Bloxstrap.UI.Elements.Settings
/// </summary>
public partial class MainWindow : INavigationWindow
{
private Models.Persistable.WindowState _state => App.State.Prop.SettingsWindow;
public MainWindow(bool showAlreadyRunningWarning)
{
var viewModel = new MainWindowViewModel();
viewModel.RequestSaveNoticeEvent += (_, _) => SettingsSavedSnackbar.Show();
viewModel.RequestCloseWindowEvent += (_, _) => Close();
DataContext = viewModel;
@ -22,11 +30,35 @@ namespace Bloxstrap.UI.Elements.Settings
App.Logger.WriteLine("MainWindow::MainWindow", "Initializing menu");
#if DEBUG // easier access
EditorWarningNavItem.Visibility = System.Windows.Visibility.Visible;
EditorWarningNavItem.Visibility = Visibility.Visible;
#endif
if (showAlreadyRunningWarning)
ShowAlreadyRunningSnackbar();
LoadState();
}
public void LoadState()
{
if (_state.Left > SystemParameters.VirtualScreenWidth)
_state.Left = 0;
if (_state.Top > SystemParameters.VirtualScreenHeight)
_state.Top = 0;
if (_state.Width > 0)
this.Width = _state.Width;
if (_state.Height > 0)
this.Height = _state.Height;
if (_state.Left > 0 && _state.Top > 0)
{
this.WindowStartupLocation = WindowStartupLocation.Manual;
this.Left = _state.Left;
this.Top = _state.Top;
}
}
private async void ShowAlreadyRunningSnackbar()
@ -50,5 +82,27 @@ namespace Bloxstrap.UI.Elements.Settings
public void CloseWindow() => Close();
#endregion INavigationWindow methods
private void WpfUiWindow_Closing(object sender, CancelEventArgs e)
{
if (App.FastFlags.Changed || App.PendingSettingTasks.Any())
{
var result = Frontend.ShowMessageBox(Strings.Menu_UnsavedChanges, MessageBoxImage.Warning, MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
e.Cancel = true;
}
_state.Width = this.Width;
_state.Height = this.Height;
_state.Top = this.Top;
_state.Left = this.Left;
App.State.Save();
if (!e.Cancel)
App.Terminate();
}
}
}

View File

@ -1,665 +0,0 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.AboutPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage"
Scrollable="True">
<StackPanel Margin="0,0,14,14">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="60" Height="60" VerticalAlignment="Center" Source="pack://application:,,,/Bloxstrap.ico" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Bloxstrap" Margin="0,0,4,0" FontSize="24" FontWeight="Medium" />
<TextBlock Grid.Column="1" Text="{Binding Version, Mode=OneTime}" Margin="4,0,0,2" VerticalAlignment="Bottom" FontSize="16" FontWeight="Medium" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</Grid>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
<WrapPanel Orientation="Horizontal">
<ui:Anchor Margin="0,16,8,0" Content="{x:Static resources:Strings.Menu_About_GithubRepository}" Icon="Code24" NavigateUri="https://github.com/pizzaboxer/bloxstrap" />
<ui:Anchor Margin="0,16,8,0" Content="{x:Static resources:Strings.Menu_About_ReportIssue}" Icon="Chat48" NavigateUri="https://github.com/pizzaboxer/bloxstrap/issues" />
<ui:Anchor Margin="0,16,8,0" Content="{x:Static resources:Strings.Menu_About_HelpInformation}" Icon="BookQuestionMark24" NavigateUri="https://github.com/pizzaboxer/bloxstrap/wiki" />
<ui:Anchor Margin="0,16,0,0" Content="{x:Static resources:Strings.Menu_About_DiscordServer}" Icon="Chat48" NavigateUri="https://discord.gg/nKjV3mGq6R" />
</WrapPanel>
<StackPanel Visibility="{Binding BuildInformationVisibility, Mode=OneTime}">
<TextBlock Text="Build Information" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="hmmmm" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Column="0" Margin="0,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Timestamp" />
<TextBlock Grid.Row="0" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildTimestamp, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Machine" />
<TextBlock Grid.Row="1" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.Machine, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<TextBlock Grid.Row="2" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Hash" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="2" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}">
<Hyperlink Foreground="{DynamicResource TextFillColorTertiaryBrush}" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="{Binding BuildCommitHashUrl, Mode=OneTime}">
<TextBlock Text="{Binding BuildMetadata.CommitHash, Mode=OneTime}" />
</Hyperlink>
</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" Margin="0,4,16,4" FontSize="14" FontWeight="Medium" Text="Commit Ref" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
<TextBlock Grid.Row="3" Grid.Column="1" Margin="0,0,0,4" VerticalAlignment="Bottom" Text="{Binding BuildMetadata.CommitRef, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" Visibility="{Binding BuildCommitVisibility, Mode=OneTime}" />
</Grid>
</StackPanel>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:Expander Grid.Row="0" Grid.ColumnSpan="3" Margin="0,0,0,8" HeaderIcon="Translate24" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_Translations}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Bahasa Indonesia" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="e7leopard" />
<TextBlock Text="hfzrk" />
<TextBlock Text="soudblox" />
<TextBlock Text="Bokmål" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="darkevilmage" />
<TextBlock Text="endsouls" />
<TextBlock Text="letoek" />
<!--<TextBlock Text="Čeština" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Chusaak" />
<TextBlock Text="DanyCraftCZ" />
<TextBlock Text="Franklin_Surten" />
<TextBlock Text="Galaxy_Gangster6" />
<TextBlock Text="jasperholo" />
<TextBlock Text="letoek" />
<TextBlock Text="noobkid4545" />
<TextBlock Text="radim776" />-->
<!-- <TextBlock Text="Dansk" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Momslayer98" />
<TextBlock Text="SirBlue" /> -->
<TextBlock Text="Deutsch" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="agent_phoenix" />
<TextBlock Text="avoidr" />
<TextBlock Text="hcjohnsgd" />
<TextBlock Text="hxmbt" />
<TextBlock Text="kale123" />
<TextBlock Text="Marvin_Chu" />
<TextBlock Text="nzxt_xll" />
<TextBlock Text="Nlx095" />
<TextBlock Text="Ph1lwtf" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="TEAM_LILA" />
<TextBlock Text="xDevoidx" />
<TextBlock Text="Español" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="4Xisty" />
<TextBlock Text="Botkid" />
<TextBlock Text="colocky" />
<TextBlock Text="D0N-B0T" />
<TextBlock Text="Dasp" />
<TextBlock Text="devyyxn" />
<TextBlock Text="ItzzExcel" />
<TextBlock Text="Ilayhlinda" />
<TextBlock Text="Ilushiouss" />
<TextBlock Text="jayces." />
<TextBlock Text="kroesufos" />
<TextBlock Text="LaiyLiod" />
<TextBlock Text="lyalekin" />
<TextBlock Text="NezumiDS" />
<TextBlock Text="NimuruDP" />
<TextBlock Text="NescafeCL" />
<TextBlock Text="Sw7gger" />
<TextBlock Text="sxckqerz" />
<TextBlock Text="Urzy" />
<TextBlock Text="Filipino" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alphjectiom" />
<TextBlock Text="FlaminDaPotato" />
<TextBlock Text="RobiTheRobloxxer" />
<TextBlock Text="shadow01148" />
<TextBlock Text="Français" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="At0zDx" />
<TextBlock Text="built4aiming" />
<TextBlock Text="hahaloserz360" />
<TextBlock Text="K0ga" />
<TextBlock Text="Marcssebaa" />
<TextBlock Text="MommySernox" />
<TextBlock Text="owentempest8" />
<TextBlock Text="Subsical" />
<TextBlock Text="thatsirwaffles" />
<TextBlock Text="tyundrai" />
<TextBlock Text="Waza80" />
<TextBlock Text="Hindi (Latin)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="kunaljainop" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="Sur_" />
<TextBlock Text="Tezos" />
<TextBlock Text="TheTakuo" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Hrvatski" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Dzigos" />
<TextBlock Text="Koyroii" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Italiano" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="crow_zxcr" />
<TextBlock Text="devyyxn" />
<TextBlock Text="domenicoiacono" />
<TextBlock Text="hulabulaseop" />
<TextBlock Text="lord_moth" />
<TextBlock Text="loridori" />
<TextBlock Text="Lupo01" />
<TextBlock Text="Mogunars" />
<TextBlock Text="pave08" />
<TextBlock Text="spectrumbruh" />
<TextBlock Text="Lietuvių" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="cr0000142" />
<TextBlock Text="Duexo" />
<TextBlock Text="jessethedev" />
<TextBlock Text="Vac31." />
<TextBlock Text="Magyar" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="DynoPlays" />
<TextBlock Text="Elotomka" />
<TextBlock Text="xM4rk1" />
<!--<TextBlock Text="Nederlands" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cosmix" />
<TextBlock Text="Miwwzy" />
<TextBlock Text="Quickvision1" />
<TextBlock Text="ydboss" />-->
<TextBlock Text="Polski" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Eztaru" />
<TextBlock Text="lunar" />
<TextBlock Text="markontm" />
<TextBlock Text="my5q" />
<TextBlock Text="nemzik2137" />
<TextBlock Text="plexar" />
<TextBlock Text="r.efil" />
<TextBlock Text="Português (Brasil)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="anormalevis" />
<TextBlock Text="ErisvaldoBalbino" />
<TextBlock Text="G3xneric" />
<TextBlock Text="hnter" />
<TextBlock Text="issei_" />
<TextBlock Text="iyto.lk" />
<TextBlock Text="jhermesn" />
<TextBlock Text="JorgeDaPelada" />
<TextBlock Text="LwgoDev" />
<TextBlock Text="nunk7" />
<TextBlock Text="peke7374" />
<TextBlock Text="SeeF" />
<TextBlock Text="Snowzin1" />
<TextBlock Text="storm930" />
<TextBlock Text="toofastforboo" />
<TextBlock Text="VMOICE" />
<TextBlock Text="Ye4" />
<TextBlock Text="ZaPeZaPe" />
<TextBlock Text="Română" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Externalkinetics" />
<TextBlock Text="MonochromeAlex" />
<TextBlock Text="PlayerValley" />
<TextBlock Text="Smuki" />
<TextBlock Text="theflopperguy" />
<TextBlock Text="Suomi" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="gelaxiz" />
<TextBlock Text="jes5" />
<TextBlock Text="retromaxwell" />
<TextBlock Text="SomePinglord" />
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Svenska" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Axellse" />
<TextBlock Text="CroppingFlea479" />
<TextBlock Text="FishySpelar" />
<TextBlock Text="PineappleSnackz" />
<TextBlock Text="simonixen" />
<TextBlock Text="thatgurkangurk" />
<TextBlock Text="Tiếng Việt" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="alexking2068" />
<TextBlock Text="baterblx" />
<TextBlock Text="Elytronn" />
<TextBlock Text="fox810891" />
<TextBlock Text="ItsPoofy" />
<TextBlock Text="Limer1" />
<TextBlock Text="makayu203332" />
<TextBlock Text="MEx2/j7x6" />
<TextBlock Text="NguyenDat208" />
<TextBlock Text="quanmequankk" />
<TextBlock Text="SomeRandomGuy175" />
<TextBlock Text="SonThanhVN" />
<TextBlock Text="teaanguyenn" />
<TextBlock Text="Veiiorra" />
<TextBlock Text="Türkçe" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="canny19133" />
<TextBlock Text="cfors55" />
<TextBlock Text="drakfreddy" />
<TextBlock Text="enisify" />
<TextBlock Text="jayces." />
<TextBlock Text="nyatie" />
<TextBlock Text="PixelArmy" />
<TextBlock Text="plants8332" />
<TextBlock Text="r02" />
<TextBlock Text="siyamicik" />
<TextBlock Text="ydboss" />
<TextBlock Text="Українська" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="9zh" />
<TextBlock Text="andrey3569s" />
<TextBlock Text="DexterBloxxer" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="maksimvlad7" />
<TextBlock Text="Босански" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Cortex_1" />
<TextBlock Text="Nemznja" />
<TextBlock Text="Ren" />
<TextBlock Text="Български" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="GrafitNiki" />
<TextBlock Text="sidefrappe" />
<TextBlock Text="Русский" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="3tcy" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="andBroz" />
<TextBlock Text="alexneonwithglue" />
<TextBlock Text="AnonymousDudeLOL123" />
<TextBlock Text="aperna_of_the_ticks" />
<TextBlock Text="arsenijveselov77" />
<TextBlock Text="Art3mLapa" />
<TextBlock Text="cherkash" />
<TextBlock Text="cub-has-injected" />
<TextBlock Text="dallyuser" />
<TextBlock Text="Dr1mG" />
<TextBlock Text="Externalkinetics" />
<TextBlock Text="fxstyxx" />
<TextBlock Text="Gustodd4202" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="harababura" />
<TextBlock Text="ImperialRhyme" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="IStoleYourCheese" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="khat7" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="kostyan" />
<TextBlock Text="Maks" />
<TextBlock Text="niktoyou" />
<TextBlock Text="nurgament2" />
<TextBlock Text="poflexim" />
<TextBlock Text="Prob1rka" />
<TextBlock Text="Provo" />
<TextBlock Text="Quenevelly" />
<TextBlock Text="sally13249" />
<TextBlock Text="simmon8800" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Skylan031" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Spuffio" />
<TextBlock Text="StraiF" />
<TextBlock Text="StrayCatSimb" />
<TextBlock Text="Voxel" />
<TextBlock Text="XonaShera" Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
<TextBlock Text="Ziio123" />
<TextBlock Text="zor9na90000" />
</StackPanel>
<StackPanel Grid.Column="3">
<TextBlock Text="עברית" FontSize="16" FontWeight="Medium">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="1cur1" />
<TextBlock Text="ilan0098" />
<TextBlock Text="koerga" />
<TextBlock Text="Sezei" />
<TextBlock Text="العربية" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="busguesjshbahsj" />
<TextBlock Text="cq2i" />
<TextBlock Text="mmiky" />
<TextBlock Text="mostafagamingx1" />
<TextBlock Text="RoRed" />
<TextBlock Text="Sakupen" />
<TextBlock Text="streoic" />
<TextBlock Text="uvq18" />
<TextBlock Text="wyfast" />
<TextBlock Text="Zida" />
<TextBlock Text="বাংলা" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="Arnian" />
<TextBlock Text="Hydrated_panda" />
<TextBlock Text="marathedonroblox" />
<TextBlock Text="red_hi" />
<TextBlock Text="ภาษาไทย" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="._bonus_." />
<TextBlock Text="marc15772" />
<TextBlock Text="arthurwagon" />
<TextBlock Text="Sem1z" />
<TextBlock Text="xAom" />
<TextBlock Text="한국어" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="ADVI50R" />
<TextBlock Text="asd123456fghqwerty" />
<TextBlock Text="bacon1295" />
<TextBlock Text="NightPlay" />
<TextBlock Text="中文 (简体)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="Aling00" />
<TextBlock Text="Clock" />
<TextBlock Text="ERSN_CERROR" />
<TextBlock Text="Kirxvil" />
<TextBlock Text="Typel" />
<TextBlock Text="yuhaodatt" />
<TextBlock Text="中文 (廣東話)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="henrychu1125" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="shhh_op" />
<TextBlock Text="中文 (繁體)" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="14TQD" />
<TextBlock Text="DXuwu" />
<TextBlock Text="kitzure" />
<TextBlock Text="Kimina898" />
<TextBlock Text="日本語" FontSize="16" FontWeight="Medium" Margin="0,16,0,0">
<TextBlock.Foreground>
<SolidColorBrush Color="{DynamicResource SystemAccentColorSecondary}" />
</TextBlock.Foreground>
</TextBlock>
<TextBlock Text="MintJapan" />
<TextBlock Text="pimeja7" />
<TextBlock Text="yixhuaa" />
</StackPanel>
</Grid>
</controls:Expander>
<controls:Expander Grid.Row="1" Grid.Column="0" Margin="0,0,4,0" HeaderIcon="Code24" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_Code}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[Matt](https://github.com/bluepilledgreat)" />
<controls:MarkdownTextBlock MarkdownText="[1011025m](https://github.com/1011025m)" />
<controls:MarkdownTextBlock MarkdownText="[EasternBloxxer](https://github.com/EasternBloxxer)" />
<controls:MarkdownTextBlock MarkdownText="[sitiom](https://github.com/sitiom)" />
<controls:MarkdownTextBlock MarkdownText="[Extravi](https://github.com/Extravi)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[swatTurret](https://github.com/swatTurret)" />
<controls:MarkdownTextBlock MarkdownText="[fxeP1](https://github.com/fxeP1)" />
<controls:MarkdownTextBlock MarkdownText="[Redusofficial](https://github.com/Redusofficial)" />
<controls:MarkdownTextBlock MarkdownText="[srthMD](https://github.com/srthMD)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Row="1" Grid.Column="1" Margin="4,0,4,0" HeaderIcon="AppsAddIn28" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_FeatureIdeas}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[he3als](https://github.com/he3als)" />
<controls:MarkdownTextBlock MarkdownText="[NikSavchenk0](https://github.com/NikSavchenk0)" />
<controls:MarkdownTextBlock MarkdownText="[carter0nline](https://github.com/carter0nline)" />
<controls:MarkdownTextBlock MarkdownText="[lolmanurfunny](https://github.com/lolmanurfunny)" />
<controls:MarkdownTextBlock MarkdownText="[MehKako](https://github.com/MehKako)" />
<controls:MarkdownTextBlock MarkdownText="[EpixScripts](https://github.com/EpixScripts)" />
<controls:MarkdownTextBlock MarkdownText="[knivesofeylis](https://github.com/knivesofeylis)" />
<controls:MarkdownTextBlock MarkdownText="[sha4owz](https://github.com/sha4owz)" />
<controls:MarkdownTextBlock MarkdownText="[DaMlgNoodle](https://github.com/DaMlgNoodle)" />
<controls:MarkdownTextBlock MarkdownText="[nakoyasha](https://github.com/nakoyasha)" />
<controls:MarkdownTextBlock MarkdownText="[exurd](https://github.com/exurd)" />
<controls:MarkdownTextBlock MarkdownText="[0xFE0F](https://github.com/0xFE0F)" />
<controls:MarkdownTextBlock MarkdownText="[Tezos](https://github.com/GoingCrazyDude)" />
<controls:MarkdownTextBlock MarkdownText="[CfwSky](https://www.roblox.com/users/129425241/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ruubloo](https://www.roblox.com/users/158082266/profile)" />
<controls:MarkdownTextBlock MarkdownText="[toyoda165](https://www.roblox.com/users/923416649/profile)" />
<controls:MarkdownTextBlock MarkdownText="[ShadowCodeX](https://github.com/ShadowCodeX-debug)" />
<controls:MarkdownTextBlock MarkdownText="[cub-has-injected](https://github.com/cub-has-injected)" />
</StackPanel>
</controls:Expander>
<controls:Expander Grid.Row="1" Grid.Column="2" Margin="4,0,0,0" HeaderIcon="Heart16" HeaderText="{x:Static resources:Strings.Menu_About_Contributors_SpecialThanks}" IsExpanded="True">
<StackPanel>
<controls:MarkdownTextBlock MarkdownText="[MaximumADHD](https://github.com/MaximumADHD)" />
<controls:MarkdownTextBlock MarkdownText="[Multako](https://www.roblox.com/users/2485612194/profile)" />
<controls:MarkdownTextBlock MarkdownText="[axstin](https://github.com/axstin)" />
<controls:MarkdownTextBlock MarkdownText="[taskmanager](https://github.com/Mantaraix)" />
<controls:MarkdownTextBlock MarkdownText="[apprehensions](https://github.com/apprehensions)" />
</StackPanel>
</controls:Expander>
</Grid>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Licenses}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardAction Grid.Row="0" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="Bloxstrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="1" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/lepoco/wpfui/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="WPF-UI" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/securifybv/ShellLink/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="14" Text="ShellLink" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/Lachee/discord-rpc-csharp/blob/master/LICENSE">
<StackPanel>
<TextBlock FontSize="14" Text="DiscordRPC" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/MaximumADHD/Roblox-Studio-Mod-Manager/blob/main/LICENSE">
<StackPanel>
<TextBlock FontSize="13" Text="Roblox Studio Mod Manager" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="0" Margin="0,8,8,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/icsharpcode/SharpZipLib/blob/master/LICENSE.txt">
<StackPanel>
<TextBlock FontSize="13" Text="SharpZipLib" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_MIT}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/xoofx/markdig/blob/master/license.txt">
<StackPanel>
<TextBlock FontSize="14" Text="Markdig" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_About_Licenses_BSD2}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -137,7 +137,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
}
else
{
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information);
Frontend.ShowMessageBox(Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information);
bool refresh = false;
@ -175,7 +175,14 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
json = '{' + json;
if (!json.EndsWith('}'))
json += '}';
{
int lastIndex = json.LastIndexOf('}');
if (lastIndex == -1)
json += '}';
else
json = json.Substring(0, lastIndex+1);
}
try
{
@ -193,7 +200,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
catch (Exception ex)
{
Frontend.ShowMessageBox(
String.Format(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidJSON, ex.Message),
String.Format(Strings.Menu_FastFlagEditor_InvalidJSON, ex.Message),
MessageBoxImage.Error
);
@ -205,7 +212,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
if (list.Count > 16)
{
var result = Frontend.ShowMessageBox(
Bloxstrap.Resources.Strings.Menu_FastFlagEditor_LargeConfig,
Strings.Menu_FastFlagEditor_LargeConfig,
MessageBoxImage.Warning,
MessageBoxButton.YesNo
);
@ -222,7 +229,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
int count = conflictingFlags.Count();
string message = String.Format(
Bloxstrap.Resources.Strings.Menu_FastFlagEditor_ConflictingImport,
Strings.Menu_FastFlagEditor_ConflictingImport,
count,
String.Join(", ", conflictingFlags.Take(25))
);
@ -263,16 +270,16 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
string errorMessage = "";
if (!_validPrefixes.Any(name.StartsWith))
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidPrefix;
errorMessage = Strings.Menu_FastFlagEditor_InvalidPrefix;
else if (!name.All(x => char.IsLetterOrDigit(x) || x == '_'))
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidCharacter;
errorMessage = Strings.Menu_FastFlagEditor_InvalidCharacter;
if (name.EndsWith("_PlaceFilter") || name.EndsWith("_DataCenterFilter"))
errorMessage = !ValidateFilter(name, value) ? Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidPlaceFilter : "";
errorMessage = !ValidateFilter(name, value) ? Strings.Menu_FastFlagEditor_InvalidPlaceFilter : "";
else if ((name.StartsWith("FInt") || name.StartsWith("DFInt")) && !Int32.TryParse(value, out _))
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidNumberValue;
errorMessage = Strings.Menu_FastFlagEditor_InvalidNumberValue;
else if ((name.StartsWith("FFlag") || name.StartsWith("DFFlag")) && lowerValue != "true" && lowerValue != "false")
errorMessage = Bloxstrap.Resources.Strings.Menu_FastFlagEditor_InvalidBoolValue;
errorMessage = Strings.Menu_FastFlagEditor_InvalidBoolValue;
if (!String.IsNullOrEmpty(errorMessage))
{
@ -300,12 +307,10 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
int index = e.Row.GetIndex();
FastFlag entry = _fastFlagList[index];
if (e.Row.DataContext is not FastFlag entry)
return;
var textbox = e.EditingElement as TextBox;
if (textbox is null)
if (e.EditingElement is not TextBox textbox)
return;
switch (e.Column.Header)
@ -319,7 +324,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
if (App.FastFlags.GetValue(newName) is not null)
{
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information);
Frontend.ShowMessageBox(Strings.Menu_FastFlagEditor_AlreadyExists, MessageBoxImage.Information);
e.Cancel = true;
textbox.Text = oldName;
return;
@ -387,7 +392,7 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
{
string json = JsonSerializer.Serialize(App.FastFlags.Prop, new JsonSerializerOptions { WriteIndented = true });
Clipboard.SetDataObject(json);
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Menu_FastFlagEditor_JsonCopiedToClipboard, MessageBoxImage.Information);
Frontend.ShowMessageBox(Strings.Menu_FastFlagEditor_JsonCopiedToClipboard, MessageBoxImage.Information);
}
private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e)

View File

@ -86,7 +86,7 @@
Header="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Title}"
Description="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Description}"
HelpLink="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#framerate-limit">
<ui:TextBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" Text="{Binding FramerateLimit, Mode=TwoWay}" PreviewTextInput="ValidateInt32" />
<ui:TextBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" Text="{Binding FramerateLimit, Mode=TwoWay}" PreviewTextInput="ValidateUInt32" />
</controls:OptionControl>
<controls:OptionControl
@ -165,12 +165,6 @@
</ComboBox>
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_FastFlags_Presets_AltGraphicsSelector_Title}"
Description="{x:Static resources:Strings.Menu_FastFlags_Presets_AltGraphicsSelector_Description}">
<ui:ToggleSwitch IsChecked="{Binding AlternateGraphicsSelectorEnabled, Mode=TwoWay}" />
</controls:OptionControl>
<ui:CardAction Margin="0,24,0,0" Icon="EditSettings24" Command="{Binding OpenFastFlagEditorCommand}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_FastFlagEditor_Title}" />

View File

@ -31,6 +31,8 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
DataContext = new FastFlagsViewModel(this);
}
private void ValidateInt32(object sender, TextCompositionEventArgs e) => e.Handled = !Int32.TryParse(e.Text, out int _);
private void ValidateInt32(object sender, TextCompositionEventArgs e) => e.Handled = e.Text != "-" && !Int32.TryParse(e.Text, out int _);
private void ValidateUInt32(object sender, TextCompositionEventArgs e) => e.Handled = !UInt32.TryParse(e.Text, out uint _);
}
}

View File

@ -26,8 +26,9 @@
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Integrations_ShowServerDetails_Title}"
Description="{x:Static resources:Strings.Menu_Integrations_ShowServerDetails_Description}"
Header="{x:Static resources:Strings.Menu_Integrations_QueryServerLocation_Title}"
Description="{Binding Source={x:Static resources:Strings.Menu_Integrations_QueryServerLocation_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://ipinfo.io'}"
HelpLink="https://github.com/pizzaboxer/bloxstrap/wiki/What-is-activity-tracking%3F#server-location-querying"
IsEnabled="{Binding InnerContent.IsChecked, ElementName=ActivityTrackingOption, Mode=OneWay}">
<ui:ToggleSwitch IsChecked="{Binding ShowServerDetailsEnabled, Mode=TwoWay}" />
</controls:OptionControl>
@ -40,7 +41,7 @@
</controls:OptionControl>
<TextBlock Text="{x:Static resources:Strings.Common_DiscordRichPresence}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_Integrations_RequiresActivityTracking}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<controls:MarkdownTextBlock MarkdownText="{Binding Source={x:Static resources:Strings.Menu_Integrations_RequiresActivityTracking}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://github.com/pizzaboxer/bloxstrap/wiki/What-is-activity-tracking%3F#discord-rich-presence'}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Integrations_ShowGameActivity_Title}"
@ -88,18 +89,18 @@
</Style>
</StackPanel.Style>
<TextBlock Text="{x:Static resources:Strings.Common_Name}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomIntegration.Name}" />
<ui:TextBox Margin="0,4,0,0" Text="{Binding SelectedCustomIntegration.Name, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_AppLocation}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Grid Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBox Grid.Column="0" Margin="0,0,0,0" PlaceholderText="{x:Static resources:Strings.Menu_Integrations_Custom_AppLocation_Placeholder}" Text="{Binding SelectedCustomIntegration.Location}" />
<ui:TextBox Grid.Column="0" Margin="0,0,0,0" PlaceholderText="C:\Windows\System32\cmd.exe" Text="{Binding SelectedCustomIntegration.Location}" />
<ui:Button Grid.Column="1" Margin="8,0,0,0" Height="34" Icon="Folder24" Content="{x:Static resources:Strings.Common_Browse}" Command="{Binding BrowseIntegrationLocationCommand}" />
</Grid>
<TextBlock Margin="0,8,0,0" Text="{x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:TextBox Margin="0,4,0,0" PlaceholderText="{x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs_Placeholder}" Text="{Binding SelectedCustomIntegration.LaunchArgs}" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" />
<ui:TextBox Margin="0,4,0,0" PlaceholderText="{Binding Source='/k echo {0}', Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static resources:Strings.Menu_Integrations_Custom_LaunchArgs_Placeholder}}" Text="{Binding SelectedCustomIntegration.LaunchArgs}" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" />
<CheckBox Margin="0,8,0,0" Content="{x:Static resources:Strings.Menu_Integrations_Custom_AutoClose}" IsChecked="{Binding SelectedCustomIntegration.AutoClose}" />
</StackPanel>
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Text="{x:Static resources:Strings.Menu_Integrations_Custom_NoneSelected}" TextWrapping="Wrap" VerticalAlignment="Center" HorizontalAlignment="Center">

View File

@ -25,18 +25,27 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardAction Grid.Row="0" Grid.Column="0" x:Name="OpenModFolderCardAction" Margin="0,0,4,0" Icon="Folder24" Command="{Binding OpenModsFolderCommand}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_Title}" TextWrapping="Wrap" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="0" Grid.Column="1" Margin="4,0,0,0" Icon="BookQuestionMark24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/Adding-custom-mods">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Common_Help}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Mods_Help_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Row="1" Grid.ColumnSpan="2" Margin="0,8,0,0" Icon="WindowWrench24" Command="{Binding OpenCompatSettingsCommand}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Mods_Misc_CompatibilitySettings_Title}" TextWrapping="Wrap" />
<controls:MarkdownTextBlock Margin="0,2,0,0" FontSize="12" MarkdownText="{Binding Source={x:Static resources:Strings.Menu_Mods_Misc_CompatibilitySettings_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://devblogs.microsoft.com/directx/demystifying-full-screen-optimizations/'}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" />
</StackPanel>
</ui:CardAction>
</Grid>
<TextBlock Text="{x:Static resources:Strings.Common_Presets}" FontSize="20" FontWeight="Medium" Margin="0,16,0,0" />
@ -93,13 +102,5 @@
<ui:Button Icon="Delete16" Content="{x:Static resources:Strings.Menu_Mods_Misc_CustomFont_Remove}" Appearance="Danger" Command="{Binding ManageCustomFontCommand}" Visibility="{Binding DeleteCustomFontVisibility, Mode=OneWay}" />
</StackPanel>
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Mods_Misc_DisableFullscreenOptimisations_Title}"
Description="{x:Static resources:Strings.Menu_Mods_Misc_DisableFullscreenOptimisations_Description}"
HelpLink="https://devblogs.microsoft.com/directx/demystifying-full-screen-optimizations/"
x:Name="FullscreenOptimizationsToggle">
<ui:ToggleSwitch IsChecked="{Binding DisableFullscreenOptimizations, Mode=TwoWay}" />
</controls:OptionControl>
</StackPanel>
</ui:UiPage>

View File

@ -1,6 +1,4 @@
using System.Windows;
using Bloxstrap.UI.ViewModels.Settings;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Settings.Pages
{
@ -13,10 +11,6 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
{
DataContext = new ModsViewModel();
InitializeComponent();
// fullscreen optimizations were only added in windows 10 build 17093
if (Environment.OSVersion.Version.Build < 17093)
this.FullscreenOptimizationsToggle.Visibility = Visibility.Collapsed;
}
}
}

View File

@ -17,35 +17,42 @@ namespace Bloxstrap.UI
if (App.LaunchSettings.QuietFlag.Active)
return defaultResult;
if (App.LaunchSettings.RobloxLaunchMode != LaunchMode.None)
return ShowFluentMessageBox(message, icon, buttons);
return ShowFluentMessageBox(message, icon, buttons);
}
switch (App.Settings.Prop.BootstrapperStyle)
{
case BootstrapperStyle.FluentDialog:
case BootstrapperStyle.ClassicFluentDialog:
case BootstrapperStyle.FluentAeroDialog:
case BootstrapperStyle.ByfronDialog:
return ShowFluentMessageBox(message, icon, buttons);
public static void ShowPlayerErrorDialog(bool crash = false)
{
if (App.LaunchSettings.QuietFlag.Active)
return;
default:
return MessageBox.Show(message, App.ProjectName, buttons, icon);
}
string topLine = Strings.Dialog_PlayerError_FailedLaunch;
if (crash)
topLine = Strings.Dialog_PlayerError_Crash;
ShowMessageBox($"{topLine}\n\n{Strings.Dialog_PlayerError_HelpInformation}", MessageBoxImage.Error);
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Roblox-crashes-or-does-not-launch");
}
public static void ShowExceptionDialog(Exception exception)
{
if (App.LaunchSettings.QuietFlag.Active)
return;
Application.Current.Dispatcher.Invoke(() =>
{
new ExceptionDialog(exception).ShowDialog();
});
}
public static void ShowConnectivityDialog(string title, string description, Exception exception)
public static void ShowConnectivityDialog(string title, string description, MessageBoxImage image, Exception exception)
{
if (App.LaunchSettings.QuietFlag.Active)
return;
Application.Current.Dispatcher.Invoke(() =>
{
new ConnectivityDialog(title, description, exception).ShowDialog();
new ConnectivityDialog(title, description, image, exception).ShowDialog();
});
}

View File

@ -1,4 +1,5 @@
using Bloxstrap.Integrations;
using Bloxstrap.UI.Elements.About;
using Bloxstrap.UI.Elements.ContextMenu;
namespace Bloxstrap.UI
@ -10,19 +11,22 @@ namespace Bloxstrap.UI
private bool _disposing = false;
private readonly System.Windows.Forms.NotifyIcon _notifyIcon;
private MenuContainer? _menuContainer;
private ActivityWatcher? _activityWatcher;
private DiscordRichPresence? _richPresenceHandler;
private int? _processId;
private readonly MenuContainer _menuContainer;
private readonly Watcher _watcher;
private ActivityWatcher? _activityWatcher => _watcher.ActivityWatcher;
EventHandler? _alertClickHandler;
public NotifyIconWrapper()
public NotifyIconWrapper(Watcher watcher)
{
App.Logger.WriteLine("NotifyIconWrapper::NotifyIconWrapper", "Initializing notification area icon");
_notifyIcon = new()
_watcher = watcher;
_notifyIcon = new(new System.ComponentModel.Container())
{
Icon = Properties.Resources.IconBloxstrap,
Text = App.ProjectName,
@ -30,52 +34,18 @@ namespace Bloxstrap.UI
};
_notifyIcon.MouseClick += MouseClickEventHandler;
if (_activityWatcher is not null && App.Settings.Prop.ShowServerDetails)
_activityWatcher.OnGameJoin += OnGameJoin;
_menuContainer = new(_watcher);
_menuContainer.Show();
}
#region Handler registers
public void SetRichPresenceHandler(DiscordRichPresence richPresenceHandler)
{
if (_richPresenceHandler is not null)
return;
_richPresenceHandler = richPresenceHandler;
}
public void SetActivityWatcher(ActivityWatcher activityWatcher)
{
if (_activityWatcher is not null)
return;
_activityWatcher = activityWatcher;
if (App.Settings.Prop.ShowServerDetails)
_activityWatcher.OnGameJoin += (_, _) => Task.Run(OnGameJoin);
}
public void SetProcessId(int processId)
{
if (_processId is not null)
return;
_processId = processId;
}
#endregion
#region Context menu
public void InitializeContextMenu()
{
if (_menuContainer is not null || _disposing)
return;
App.Logger.WriteLine("NotifyIconWrapper::InitializeContextMenu", "Initializing context menu");
_menuContainer = new(_activityWatcher, _richPresenceHandler, _processId);
_menuContainer.ShowDialog();
}
public void MouseClickEventHandler(object? sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button != System.Windows.Forms.MouseButtons.Right || _menuContainer is null)
if (e.Button != System.Windows.Forms.MouseButtons.Right)
return;
_menuContainer.Activate();
@ -84,10 +54,17 @@ namespace Bloxstrap.UI
#endregion
#region Activity handlers
public async void OnGameJoin()
public async void OnGameJoin(object? sender, EventArgs e)
{
string serverLocation = await _activityWatcher!.GetServerLocation();
string title = _activityWatcher.ActivityServerType switch
if (_activityWatcher is null)
return;
string? serverLocation = await _activityWatcher.Data.QueryServerLocation();
if (string.IsNullOrEmpty(serverLocation))
return;
string title = _activityWatcher.Data.ServerType switch
{
ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public,
ServerType.Private => Strings.ContextMenu_ServerInformation_Notification_Title_Private,
@ -99,11 +76,12 @@ namespace Bloxstrap.UI
title,
String.Format(Strings.ContextMenu_ServerInformation_Notification_Text, serverLocation),
10,
(_, _) => _menuContainer?.ShowServerInformationWindow()
(_, _) => _menuContainer.ShowServerInformationWindow()
);
}
#endregion
// we may need to create our own handler for this, because this sorta sucks
public void ShowAlert(string caption, string message, int duration, EventHandler? clickHandler)
{
string id = Guid.NewGuid().ToString()[..8];
@ -151,9 +129,8 @@ namespace Bloxstrap.UI
App.Logger.WriteLine("NotifyIconWrapper::Dispose", "Disposing NotifyIcon");
_menuContainer?.Dispatcher.Invoke(_menuContainer.Close);
_notifyIcon?.Dispose();
_menuContainer.Dispatcher.Invoke(_menuContainer.Close);
_notifyIcon.Dispose();
GC.SuppressFinalize(this);
}

View File

@ -0,0 +1,63 @@
using System.Windows;
namespace Bloxstrap.UI.ViewModels.About
{
public class AboutViewModel : NotifyPropertyChangedViewModel
{
private SupporterData? _supporterData;
public string Version => string.Format(Strings.Menu_About_Version, App.Version);
public BuildMetadataAttribute BuildMetadata => App.BuildMetadata;
public string BuildTimestamp => BuildMetadata.Timestamp.ToFriendlyString();
public string BuildCommitHashUrl => $"https://github.com/{App.ProjectRepository}/commit/{BuildMetadata.CommitHash}";
public Visibility BuildInformationVisibility => App.IsProductionBuild ? Visibility.Collapsed : Visibility.Visible;
public Visibility BuildCommitVisibility => App.IsActionBuild ? Visibility.Visible : Visibility.Collapsed;
public List<Supporter> Supporters => _supporterData?.Supporters ?? Enumerable.Empty<Supporter>().ToList();
public int SupporterColumns => _supporterData?.Columns ?? 0;
public GenericTriState SupportersLoadedState { get; set; } = GenericTriState.Unknown;
public string SupportersLoadError { get; set; } = "";
public AboutViewModel()
{
// this will cause momentary freezes only when ran under the debugger
LoadSupporterData();
}
public async void LoadSupporterData()
{
const string LOG_IDENT = "AboutViewModel::LoadSupporterData";
try
{
_supporterData = await Http.GetJson<SupporterData>("https://raw.githubusercontent.com/bloxstraplabs/config/main/supporters.json");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Could not load supporter data");
App.Logger.WriteException(LOG_IDENT, ex);
SupportersLoadedState = GenericTriState.Failed;
SupportersLoadError = ex.Message;
OnPropertyChanged(nameof(SupportersLoadError));
}
if (_supporterData is not null)
{
SupportersLoadedState = GenericTriState.Successful;
OnPropertyChanged(nameof(Supporters));
OnPropertyChanged(nameof(SupporterColumns));
}
OnPropertyChanged(nameof(SupportersLoadedState));
}
}
}

View File

@ -39,7 +39,7 @@ namespace Bloxstrap.UI.ViewModels.Bootstrapper
private void CancelInstall()
{
_dialog.Bootstrapper?.CancelInstall();
_dialog.Bootstrapper?.Cancel();
_dialog.CloseBootstrapper();
}
}

Some files were not shown because too many files have changed in this diff Show More