Draft: new installer system

the beginning of a long arduous cleanup of two years of debt
This commit is contained in:
pizzaboxer 2024-08-10 13:00:39 +01:00
parent d1343d35dc
commit 776dbc4097
No known key found for this signature in database
GPG Key ID: 59D4A1DBAD0F2BA8
75 changed files with 2334 additions and 1477 deletions

View File

@ -1,10 +1,10 @@
using System.Reflection;
using System.Web;
using System.Windows;
using System.Windows.Threading;
using Windows.Win32;
using Windows.Win32.Foundation;
using Microsoft.Win32;
using Bloxstrap.Resources;
namespace Bloxstrap
{
@ -15,29 +15,27 @@ namespace Bloxstrap
{
public const string ProjectName = "Bloxstrap";
public const string ProjectRepository = "pizzaboxer/bloxstrap";
public const string RobloxPlayerAppName = "RobloxPlayerBeta";
public const string RobloxStudioAppName = "RobloxStudioBeta";
// used only for communicating between app and menu - use Directories.Base for anything else
public static string BaseDirectory = null!;
public static string? CustomFontLocation;
public static bool ShouldSaveConfigs { get; set; } = false;
public static bool IsSetupComplete { get; set; } = true;
public static bool IsFirstRun { get; set; } = true;
// simple shorthand for extremely frequently used and long string - this goes under HKCU
public const string UninstallKey = $@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{ProjectName}";
public static LaunchSettings LaunchSettings { get; private set; } = null!;
public static BuildMetadataAttribute BuildMetadata = Assembly.GetExecutingAssembly().GetCustomAttribute<BuildMetadataAttribute>()!;
public static string Version = Assembly.GetExecutingAssembly().GetName().Version!.ToString()[..^2];
public static NotifyIconWrapper? NotifyIcon { get; private set; }
public static NotifyIconWrapper? NotifyIcon { get; set; }
public static readonly Logger Logger = new();
public static readonly JsonManager<Settings> Settings = new();
public static readonly JsonManager<State> State = new();
public static readonly FastFlagManager FastFlags = new();
public static readonly HttpClient HttpClient = new(
@ -52,18 +50,10 @@ namespace Bloxstrap
public static void Terminate(ErrorCode exitCode = ErrorCode.ERROR_SUCCESS)
{
if (IsFirstRun)
{
if (exitCode == ErrorCode.ERROR_CANCELLED)
exitCode = ErrorCode.ERROR_INSTALL_USEREXIT;
}
int exitCodeNum = (int)exitCode;
Logger.WriteLine("App::Terminate", $"Terminating with exit code {exitCodeNum} ({exitCode})");
Settings.Save();
State.Save();
NotifyIcon?.Dispose();
Environment.Exit(exitCodeNum);
@ -98,16 +88,7 @@ namespace Bloxstrap
#endif
}
private void StartupFinished()
{
const string LOG_IDENT = "App::StartupFinished";
Logger.WriteLine(LOG_IDENT, "Successfully reached end of main thread. Terminating...");
Terminate();
}
protected override async void OnStartup(StartupEventArgs e)
protected override void OnStartup(StartupEventArgs e)
{
const string LOG_IDENT = "App::OnStartup";
@ -128,19 +109,62 @@ namespace Bloxstrap
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
HttpClient.Timeout = TimeSpan.FromSeconds(30);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
LaunchSettings = new LaunchSettings(e.Args);
using (var checker = new InstallChecker())
// installation check begins here
using var uninstallKey = Registry.CurrentUser.OpenSubKey(UninstallKey);
string? installLocation = null;
if (uninstallKey?.GetValue("InstallLocation") is string value && Directory.Exists(value))
installLocation = value;
// silently change install location if we detect a portable run
// this should also handle renaming of the user profile folder
if (installLocation is null && Directory.GetParent(Paths.Process)?.FullName is string processDir)
{
checker.Check();
var files = Directory.GetFiles(processDir).Select(x => Path.GetFileName(x)).ToArray();
var installer = new Installer
{
InstallLocation = processDir,
IsImplicitInstall = true
};
// check if settings.json and state.json are the only files in the folder, and if we can write to it
if (files.Length <= 3
&& files.Contains("Settings.json")
&& files.Contains("State.json")
&& installer.CheckInstallLocation())
{
Logger.WriteLine(LOG_IDENT, $"Changing install location to '{processDir}'");
installer.DoInstall();
installLocation = processDir;
}
}
Paths.Initialize(BaseDirectory);
// we shouldn't save settings on the first run until the first installation is finished,
// just in case the user decides to cancel the install
if (!IsFirstRun)
if (installLocation is null)
{
Logger.Initialize(true);
LaunchHandler.LaunchInstaller();
}
else
{
Paths.Initialize(installLocation);
// ensure executable is in the install directory
if (Paths.Process != Paths.Application && !File.Exists(Paths.Application))
File.Copy(Paths.Process, Paths.Application);
Logger.Initialize(LaunchSettings.IsUninstall);
if (!Logger.Initialized && !Logger.NoWriteMode)
{
Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating.");
Terminate();
}
Settings.Load();
State.Load();
FastFlags.Load();
@ -152,145 +176,14 @@ namespace Bloxstrap
}
Locale.Set(Settings.Prop.Locale);
if (!LaunchSettings.IsUninstall)
Installer.HandleUpgrade();
LaunchHandler.ProcessLaunchArgs();
}
LaunchSettings.ParseRoblox();
HttpClient.Timeout = TimeSpan.FromSeconds(30);
HttpClient.DefaultRequestHeaders.Add("User-Agent", ProjectRepository);
// TEMPORARY FILL-IN FOR NEW FUNCTIONALITY
// REMOVE WHEN LARGER REFACTORING IS DONE
var connectionResult = await RobloxDeployment.InitializeConnectivity();
if (connectionResult is not null)
{
Logger.WriteException(LOG_IDENT, connectionResult);
Frontend.ShowConnectivityDialog(
Bloxstrap.Resources.Strings.Dialog_Connectivity_UnableToConnect,
Bloxstrap.Resources.Strings.Bootstrapper_Connectivity_Preventing,
connectionResult
);
return;
}
if (LaunchSettings.IsUninstall && IsFirstRun)
{
Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Bootstrapper_FirstRunUninstall, MessageBoxImage.Error);
Terminate(ErrorCode.ERROR_INVALID_FUNCTION);
return;
}
// we shouldn't save settings on the first run until the first installation is finished,
// just in case the user decides to cancel the install
if (!IsFirstRun)
{
Logger.Initialize(LaunchSettings.IsUninstall);
if (!Logger.Initialized && !Logger.NoWriteMode)
{
Logger.WriteLine(LOG_IDENT, "Possible duplicate launch detected, terminating.");
Terminate();
}
}
if (!LaunchSettings.IsUninstall && !LaunchSettings.IsMenuLaunch)
NotifyIcon = new();
#if !DEBUG
if (!LaunchSettings.IsUninstall && !IsFirstRun)
InstallChecker.CheckUpgrade();
#endif
if (LaunchSettings.IsMenuLaunch)
{
Process? menuProcess = Utilities.GetProcessesSafe().Where(x => x.MainWindowTitle == $"{ProjectName} Menu").FirstOrDefault();
if (menuProcess is not null)
{
var handle = menuProcess.MainWindowHandle;
Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window with handle {handle}");
PInvoke.SetForegroundWindow((HWND)handle);
}
else
{
bool showAlreadyRunningWarning = Process.GetProcessesByName(ProjectName).Length > 1 && !LaunchSettings.IsQuiet;
Frontend.ShowMenu(showAlreadyRunningWarning);
}
StartupFinished();
return;
}
if (!IsFirstRun)
ShouldSaveConfigs = true;
if (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
// which often hangs around for a few seconds after the window closes
// it would be better to have this rely on the activity tracker when we implement IPC in the planned refactoring
var result = Frontend.ShowMessageBox(Bloxstrap.Resources.Strings.Bootstrapper_ConfirmLaunch, MessageBoxImage.Warning, MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
{
StartupFinished();
return;
}
}
// start bootstrapper and show the bootstrapper modal if we're not running silently
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper");
Bootstrapper bootstrapper = new(LaunchSettings.RobloxLaunchArgs, LaunchSettings.RobloxLaunchMode);
IBootstrapperDialog? dialog = null;
if (!LaunchSettings.IsQuiet)
{
Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog");
dialog = Settings.Prop.BootstrapperStyle.GetNew();
bootstrapper.Dialog = dialog;
dialog.Bootstrapper = bootstrapper;
}
Task bootstrapperTask = Task.Run(async () => await bootstrapper.Run()).ContinueWith(t =>
{
Logger.WriteLine(LOG_IDENT, "Bootstrapper task has finished");
// notifyicon is blocking main thread, must be disposed here
NotifyIcon?.Dispose();
if (t.IsFaulted)
Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is null)
return;
Logger.WriteException(LOG_IDENT, t.Exception);
Exception exception = t.Exception;
#if !DEBUG
if (t.Exception.GetType().ToString() == "System.AggregateException")
exception = t.Exception.InnerException!;
#endif
FinalizeExceptionHandling(exception, false);
});
// 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 (!LaunchSettings.IsNoLaunch && Settings.Prop.EnableActivityTracking)
NotifyIcon?.InitializeContextMenu();
Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish");
bootstrapperTask.Wait();
StartupFinished();
Terminate();
}
}
}

View File

@ -7,8 +7,8 @@
<UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>2.7.0</Version>
<FileVersion>2.7.0</FileVersion>
<Version>2.8.0</Version>
<FileVersion>2.8.0</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
@ -20,7 +20,6 @@
<Resource Include="Resources\Fonts\Rubik-VariableFont_wght.ttf" />
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoDark.jpg" />
<Resource Include="Resources\BootstrapperStyles\ByfronDialog\ByfronLogoLight.jpg" />
<Resource Include="Resources\Menu\StartMenu.png" />
<Resource Include="Resources\MessageBox\Error.png" />
<Resource Include="Resources\MessageBox\Information.png" />
<Resource Include="Resources\MessageBox\Question.png" />

View File

@ -25,11 +25,11 @@ namespace Bloxstrap
private bool FreshInstall => String.IsNullOrEmpty(_versionGuid);
private string _playerFileName => _launchMode == LaunchMode.Player ? "RobloxPlayerBeta.exe" : "RobloxStudioBeta.exe";
// TODO: change name
private string _playerLocation => Path.Combine(_versionFolder, _playerFileName);
private string _launchCommandLine;
private LaunchMode _launchMode;
private bool _installWebView2;
private string _versionGuid
{
@ -81,10 +81,11 @@ namespace Bloxstrap
#endregion
#region Core
public Bootstrapper(string launchCommandLine, LaunchMode launchMode)
public Bootstrapper(string launchCommandLine, LaunchMode launchMode, bool installWebView2)
{
_launchCommandLine = launchCommandLine;
_launchMode = launchMode;
_installWebView2 = installWebView2;
_packageDirectories = _launchMode == LaunchMode.Player ? PackageMap.Player : PackageMap.Studio;
}
@ -96,7 +97,7 @@ namespace Bloxstrap
string productName = "Roblox";
if (_launchMode != LaunchMode.Player)
productName += " Studio";
productName = "Roblox Studio";
message = message.Replace("{product}", productName);
@ -124,47 +125,41 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Running bootstrapper");
if (App.LaunchSettings.IsUninstall)
{
Uninstall();
return;
}
// connectivity check
App.Logger.WriteLine(LOG_IDENT, "Performing connectivity check...");
SetStatus(Resources.Strings.Bootstrapper_Status_Connecting);
SetStatus(Strings.Bootstrapper_Status_Connecting);
try
{
await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel);
}
catch (Exception ex)
var connectionResult = await RobloxDeployment.InitializeConnectivity();
if (connectionResult is not null)
{
App.Logger.WriteLine(LOG_IDENT, "Connectivity check failed!");
App.Logger.WriteException(LOG_IDENT, ex);
App.Logger.WriteException(LOG_IDENT, connectionResult);
string message = Resources.Strings.Bootstrapper_Connectivity_Preventing;
string message = Strings.Bootstrapper_Connectivity_Preventing;
if (ex.GetType() == typeof(HttpResponseException))
message = Resources.Strings.Bootstrapper_Connectivity_RobloxDown;
else if (ex.GetType() == typeof(TaskCanceledException))
message = Resources.Strings.Bootstrapper_Connectivity_TimedOut;
else if (ex.GetType() == typeof(AggregateException))
ex = ex.InnerException!;
if (connectionResult.GetType() == typeof(HttpResponseException))
message = Strings.Bootstrapper_Connectivity_RobloxDown;
else if (connectionResult.GetType() == typeof(TaskCanceledException))
message = Strings.Bootstrapper_Connectivity_TimedOut;
else if (connectionResult.GetType() == typeof(AggregateException))
connectionResult = connectionResult.InnerException!;
Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, ex);
Frontend.ShowConnectivityDialog(Strings.Dialog_Connectivity_UnableToConnect, message, connectionResult);
App.Terminate(ErrorCode.ERROR_CANCELLED);
}
finally
{
App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished");
return;
}
App.Logger.WriteLine(LOG_IDENT, "Connectivity check finished");
await RobloxDeployment.GetInfo(RobloxDeployment.DefaultChannel);
#if !DEBUG
if (!App.IsFirstRun && App.Settings.Prop.CheckForUpdates)
if (App.Settings.Prop.CheckForUpdates)
await CheckForUpdates();
#endif
@ -177,7 +172,7 @@ namespace Bloxstrap
{
Mutex.OpenExisting("Bloxstrap_SingletonMutex").Close();
App.Logger.WriteLine(LOG_IDENT, "Bloxstrap_SingletonMutex mutex exists, waiting...");
SetStatus(Resources.Strings.Bootstrapper_Status_WaitingOtherInstances);
SetStatus(Strings.Bootstrapper_Status_WaitingOtherInstances);
mutexExists = true;
}
catch (Exception)
@ -199,37 +194,30 @@ namespace Bloxstrap
await CheckLatestVersion();
// install/update roblox if we're running for the first time, needs updating, or the player location doesn't exist
if (App.IsFirstRun || _latestVersionGuid != _versionGuid || !File.Exists(_playerLocation))
if (_latestVersionGuid != _versionGuid || !File.Exists(_playerLocation))
await InstallLatestVersion();
if (App.IsFirstRun)
App.ShouldSaveConfigs = true;
MigrateIntegrations();
await InstallWebView2();
if (_installWebView2)
await InstallWebView2();
App.FastFlags.Save();
await ApplyModifications();
if (App.IsFirstRun || FreshInstall)
{
Register();
// TODO: move this to install/upgrade flow
if (FreshInstall)
RegisterProgramSize();
}
CheckInstall();
// at this point we've finished updating our configs
App.Settings.Save();
App.State.Save();
App.ShouldSaveConfigs = false;
await mutex.ReleaseAsync();
if (App.IsFirstRun && App.LaunchSettings.IsNoLaunch)
Dialog?.ShowSuccess(Resources.Strings.Bootstrapper_SuccessfullyInstalled);
else if (!App.LaunchSettings.IsNoLaunch && !_cancelFired)
if (!App.LaunchSettings.IsNoLaunch && !_cancelFired)
await StartRoblox();
}
@ -272,21 +260,7 @@ namespace Bloxstrap
{
const string LOG_IDENT = "Bootstrapper::StartRoblox";
SetStatus(Resources.Strings.Bootstrapper_Status_Starting);
if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll")))
{
Frontend.ShowMessageBox(
Resources.Strings.Bootstrapper_WMFNotFound,
MessageBoxImage.Error
);
if (!App.LaunchSettings.IsQuiet)
Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a");
Dialog?.CloseBootstrapper();
return;
}
SetStatus(Strings.Bootstrapper_Status_Starting);
if (_launchMode != LaunchMode.StudioAuth)
{
@ -450,9 +424,7 @@ namespace Bloxstrap
try
{
// clean up install
if (App.IsFirstRun)
Directory.Delete(Paths.Base, true);
else if (Directory.Exists(_versionFolder))
if (Directory.Exists(_versionFolder))
Directory.Delete(_versionFolder, true);
}
catch (Exception ex)
@ -468,38 +440,6 @@ namespace Bloxstrap
#endregion
#region App Install
public static void Register()
{
const string LOG_IDENT = "Bootstrapper::Register";
using (RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{App.ProjectName}"))
{
applicationKey.SetValue("InstallLocation", Paths.Base);
}
// set uninstall key
using (RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}"))
{
uninstallKey.SetValue("DisplayIcon", $"{Paths.Application},0");
uninstallKey.SetValue("DisplayName", App.ProjectName);
uninstallKey.SetValue("DisplayVersion", App.Version);
if (uninstallKey.GetValue("InstallDate") is null)
uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
uninstallKey.SetValue("InstallLocation", Paths.Base);
uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", "pizzaboxer");
uninstallKey.SetValue("ModifyPath", $"\"{Paths.Application}\" -menu");
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");
}
App.Logger.WriteLine(LOG_IDENT, "Registered application");
}
public void RegisterProgramSize()
{
const string LOG_IDENT = "Bootstrapper::RegisterProgramSize";
@ -539,57 +479,6 @@ namespace Bloxstrap
ProtocolHandler.RegisterExtension(".rbxl");
ProtocolHandler.RegisterExtension(".rbxlx");
#endif
if (Environment.ProcessPath is not null && Environment.ProcessPath != Paths.Application)
{
// in case the user is reinstalling
if (File.Exists(Paths.Application) && App.IsFirstRun)
{
Filesystem.AssertReadOnly(Paths.Application);
File.Delete(Paths.Application);
}
// check to make sure bootstrapper is in the install folder
if (!File.Exists(Paths.Application))
File.Copy(Environment.ProcessPath, Paths.Application);
}
// this SHOULD go under Register(),
// but then people who have Bloxstrap v1.0.0 installed won't have this without a reinstall
// maybe in a later version?
if (!Directory.Exists(Paths.StartMenu))
{
Directory.CreateDirectory(Paths.StartMenu);
}
else
{
// v2.0.0 - rebadge configuration menu as just "Bloxstrap Menu"
string oldMenuShortcut = Path.Combine(Paths.StartMenu, $"Configure {App.ProjectName}.lnk");
if (File.Exists(oldMenuShortcut))
File.Delete(oldMenuShortcut);
}
Utility.Shortcut.Create(Paths.Application, "", Path.Combine(Paths.StartMenu, "Play Roblox.lnk"));
Utility.Shortcut.Create(Paths.Application, "-menu", Path.Combine(Paths.StartMenu, $"{App.ProjectName} Menu.lnk"));
#if STUDIO_FEATURES
Utility.Shortcut.Create(Paths.Application, "-ide", Path.Combine(Paths.StartMenu, $"Roblox Studio ({App.ProjectName}).lnk"));
#endif
if (App.Settings.Prop.CreateDesktopIcon)
{
try
{
Utility.Shortcut.Create(Paths.Application, "", Path.Combine(Paths.Desktop, "Play Roblox.lnk"));
// one-time toggle, set it back to false
App.Settings.Prop.CreateDesktopIcon = false;
}
catch (Exception)
{
// suppress, we likely just don't have write perms for the desktop folder
}
}
}
private async Task CheckForUpdates()
@ -606,6 +495,7 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, $"Checking for updates...");
GithubRelease? releaseInfo;
try
{
releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest");
@ -622,16 +512,16 @@ namespace Bloxstrap
return;
}
int versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName);
var versionComparison = Utilities.CompareVersions(App.Version, releaseInfo.TagName);
// check if we aren't using a deployed build, so we can update to one if a new version comes out
if (versionComparison == 0 && App.BuildMetadata.CommitRef.StartsWith("tag") || versionComparison == 1)
if (versionComparison == VersionComparison.Equal && App.BuildMetadata.CommitRef.StartsWith("tag") || versionComparison == VersionComparison.GreaterThan)
{
App.Logger.WriteLine(LOG_IDENT, $"No updates found");
return;
}
SetStatus(Resources.Strings.Bootstrapper_Status_UpgradingBloxstrap);
SetStatus(Strings.Bootstrapper_Status_UpgradingBloxstrap);
try
{
@ -660,7 +550,6 @@ namespace Bloxstrap
startInfo.ArgumentList.Add(arg);
App.Settings.Save();
App.ShouldSaveConfigs = false;
Process.Start(startInfo);
@ -672,177 +561,11 @@ namespace Bloxstrap
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox(
string.Format(Resources.Strings.Bootstrapper_AutoUpdateFailed, releaseInfo.TagName),
string.Format(Strings.Bootstrapper_AutoUpdateFailed, releaseInfo.TagName),
MessageBoxImage.Information
);
}
}
private void Uninstall()
{
const string LOG_IDENT = "Bootstrapper::Uninstall";
// prompt to shutdown roblox if its currently running
if (Process.GetProcessesByName(App.RobloxPlayerAppName).Any() || Process.GetProcessesByName(App.RobloxStudioAppName).Any())
{
App.Logger.WriteLine(LOG_IDENT, $"Prompting to shut down all open Roblox instances");
MessageBoxResult result = Frontend.ShowMessageBox(
Resources.Strings.Bootstrapper_Uninstall_RobloxRunning,
MessageBoxImage.Information,
MessageBoxButton.OKCancel
);
if (result != MessageBoxResult.OK)
App.Terminate(ErrorCode.ERROR_CANCELLED);
try
{
foreach (Process process in Process.GetProcessesByName(App.RobloxPlayerAppName))
{
process.Kill();
process.Close();
}
#if STUDIO_FEATURES
foreach (Process process in Process.GetProcessesByName(App.RobloxStudioAppName))
{
process.Kill();
process.Close();
}
#endif
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}");
}
App.Logger.WriteLine(LOG_IDENT, $"All Roblox processes closed");
}
SetStatus(Resources.Strings.Bootstrapper_Status_Uninstalling);
App.ShouldSaveConfigs = false;
bool robloxPlayerStillInstalled = true;
bool robloxStudioStillInstalled = true;
// check if stock bootstrapper is still installed
using RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player");
if (bootstrapperKey is null)
{
robloxPlayerStillInstalled = false;
ProtocolHandler.Unregister("roblox");
ProtocolHandler.Unregister("roblox-player");
}
else
{
// revert launch uri handler to stock bootstrapper
string bootstrapperLocation = (string?)bootstrapperKey.GetValue("InstallLocation") + "RobloxPlayerLauncher.exe";
ProtocolHandler.Register("roblox", "Roblox", bootstrapperLocation);
ProtocolHandler.Register("roblox-player", "Roblox", bootstrapperLocation);
}
#if STUDIO_FEATURES
using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
if (studioBootstrapperKey is null)
{
robloxStudioStillInstalled = false;
ProtocolHandler.Unregister("roblox-studio");
ProtocolHandler.Unregister("roblox-studio-auth");
ProtocolHandler.Unregister("Roblox.Place");
ProtocolHandler.Unregister(".rbxl");
ProtocolHandler.Unregister(".rbxlx");
}
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);
ProtocolHandler.RegisterRobloxPlace(studioLocation);
}
#endif
// if the folder we're installed to does not end with "Bloxstrap", we're installed to a user-selected folder
// in which case, chances are they chose to install to somewhere they didn't really mean to (prior to the added warning in 2.4.0)
// if so, we're walking on eggshells and have to ensure we only clean up what we need to clean up
bool cautiousUninstall = !Paths.Base.ToLower().EndsWith(App.ProjectName.ToLower());
var cleanupSequence = new List<Action>
{
() => Registry.CurrentUser.DeleteSubKey($@"Software\{App.ProjectName}"),
() => Directory.Delete(Paths.StartMenu, true),
() => File.Delete(Path.Combine(Paths.Desktop, "Play Roblox.lnk")),
() => Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{App.ProjectName}")
};
if (cautiousUninstall)
{
cleanupSequence.Add(() => Directory.Delete(Paths.Downloads, true));
cleanupSequence.Add(() => Directory.Delete(Paths.Modifications, true));
cleanupSequence.Add(() => Directory.Delete(Paths.Versions, true));
cleanupSequence.Add(() => Directory.Delete(Paths.Logs, true));
cleanupSequence.Add(() => File.Delete(App.Settings.FileLocation));
cleanupSequence.Add(() => File.Delete(App.State.FileLocation));
}
else
{
cleanupSequence.Add(() => Directory.Delete(Paths.Base, true));
}
string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox");
if (!robloxPlayerStillInstalled && !robloxStudioStillInstalled && Directory.Exists(robloxFolder))
cleanupSequence.Add(() => Directory.Delete(robloxFolder, true));
foreach (var process in cleanupSequence)
{
try
{
process();
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})");
App.Logger.WriteException(LOG_IDENT, ex);
}
}
Action? callback = null;
if (Directory.Exists(Paths.Base))
{
callback = delegate
{
// this is definitely one of the workaround hacks of all time
// could antiviruses falsely detect this as malicious behaviour though?
// "hmm whats this program doing running a cmd command chain quietly in the background that auto deletes an entire folder"
string deleteCommand;
if (cautiousUninstall)
deleteCommand = $"del /Q \"{Paths.Application}\"";
else
deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\"";
Process.Start(new ProcessStartInfo()
{
FileName = "cmd.exe",
Arguments = $"/c timeout 5 && {deleteCommand}",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
};
}
Dialog?.ShowSuccess(Resources.Strings.Bootstrapper_SuccessfullyUninstalled, callback);
}
#endregion
#region Roblox Install
@ -852,7 +575,7 @@ namespace Bloxstrap
_isInstalling = true;
SetStatus(FreshInstall ? Resources.Strings.Bootstrapper_Status_Installing : Resources.Strings.Bootstrapper_Status_Upgrading);
SetStatus(FreshInstall ? Strings.Bootstrapper_Status_Installing : Strings.Bootstrapper_Status_Upgrading);
Directory.CreateDirectory(Paths.Base);
Directory.CreateDirectory(Paths.Downloads);
@ -866,7 +589,7 @@ namespace Bloxstrap
if (Filesystem.GetFreeDiskSpace(Paths.Base) < totalSizeRequired)
{
Frontend.ShowMessageBox(
Resources.Strings.Bootstrapper_NotEnoughSpace,
Strings.Bootstrapper_NotEnoughSpace,
MessageBoxImage.Error
);
@ -911,7 +634,7 @@ namespace Bloxstrap
if (Dialog is not null)
{
Dialog.ProgressStyle = ProgressBarStyle.Marquee;
SetStatus(Resources.Strings.Bootstrapper_Status_Configuring);
SetStatus(Strings.Bootstrapper_Status_Configuring);
}
// wait for all packages to finish extracting, with an exception for the webview2 runtime installer
@ -996,7 +719,7 @@ namespace Bloxstrap
}
// don't register program size until the program is registered, which will be done after this
if (!App.IsFirstRun && !FreshInstall)
if (!FreshInstall)
RegisterProgramSize();
if (Dialog is not null)
@ -1004,20 +727,10 @@ namespace Bloxstrap
_isInstalling = false;
}
private async Task InstallWebView2()
{
const string LOG_IDENT = "Bootstrapper::InstallWebView2";
// check if the webview2 runtime needs to be installed
// webview2 can either be installed be per-user or globally, so we need to check in both hklm and hkcu
// https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#detect-if-a-suitable-webview2-runtime-is-already-installed
using RegistryKey? hklmKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}");
using RegistryKey? hkcuKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}");
if (hklmKey is not null || hkcuKey is not null)
return;
App.Logger.WriteLine(LOG_IDENT, "Installing runtime...");
@ -1036,7 +749,7 @@ namespace Bloxstrap
await ExtractPackage(package);
}
SetStatus(Resources.Strings.Bootstrapper_Status_InstallingWebView2);
SetStatus(Strings.Bootstrapper_Status_InstallingWebView2);
ProcessStartInfo startInfo = new()
{
@ -1065,7 +778,7 @@ namespace Bloxstrap
if (File.Exists(injectorLocation))
{
Frontend.ShowMessageBox(
Resources.Strings.Bootstrapper_HyperionUpdateInfo,
Strings.Bootstrapper_HyperionUpdateInfo,
MessageBoxImage.Warning
);
@ -1086,7 +799,7 @@ namespace Bloxstrap
return;
}
SetStatus(Resources.Strings.Bootstrapper_Status_ApplyingModifications);
SetStatus(Strings.Bootstrapper_Status_ApplyingModifications);
// set executable flags for fullscreen optimizations
App.Logger.WriteLine(LOG_IDENT, "Checking executable flags...");
@ -1212,12 +925,6 @@ namespace Bloxstrap
string modFontFamiliesFolder = Path.Combine(Paths.Modifications, "content\\fonts\\families");
if (App.IsFirstRun && App.CustomFontLocation is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!);
File.Copy(App.CustomFontLocation, Paths.CustomFont, true);
}
if (File.Exists(Paths.CustomFont))
{
App.Logger.WriteLine(LOG_IDENT, "Begin font check");

View File

@ -9,9 +9,11 @@
{
ERROR_SUCCESS = 0,
ERROR_INVALID_FUNCTION = 1,
ERROR_FILE_NOT_FOUND = 2,
ERROR_CANCELLED = 1223,
ERROR_INSTALL_USEREXIT = 1602,
ERROR_INSTALL_FAILURE = 1603,
ERROR_CANCELLED = 1223,
CO_E_APPNOTFOUND = -2147221003
}

View File

@ -0,0 +1,9 @@
namespace Bloxstrap.Enums
{
public enum NextAction
{
Terminate,
LaunchSettings,
LaunchRoblox
}
}

View File

@ -0,0 +1,9 @@
namespace Bloxstrap.Enums
{
enum VersionComparison
{
LessThan = -1,
Equal = 0,
GreaterThan = 1
}
}

View File

@ -1,279 +0,0 @@
using System.Windows;
using Microsoft.Win32;
namespace Bloxstrap
{
internal class InstallChecker : IDisposable
{
private RegistryKey? _registryKey;
private string? _installLocation;
internal InstallChecker()
{
_registryKey = Registry.CurrentUser.OpenSubKey($"Software\\{App.ProjectName}", true);
if (_registryKey is not null)
_installLocation = (string?)_registryKey.GetValue("InstallLocation");
}
internal void Check()
{
const string LOG_IDENT = "InstallChecker::Check";
if (_registryKey is null || _installLocation is null)
{
if (!File.Exists("Settings.json") || !File.Exists("State.json"))
{
FirstTimeRun();
return;
}
App.Logger.WriteLine(LOG_IDENT, "Installation registry key is likely malformed");
_installLocation = Path.GetDirectoryName(Paths.Process)!;
var result = Frontend.ShowMessageBox(
string.Format(Resources.Strings.InstallChecker_NotInstalledProperly, _installLocation),
MessageBoxImage.Warning,
MessageBoxButton.YesNo
);
if (result != MessageBoxResult.Yes)
{
FirstTimeRun();
return;
}
App.Logger.WriteLine(LOG_IDENT, $"Setting install location as '{_installLocation}'");
if (_registryKey is null)
_registryKey = Registry.CurrentUser.CreateSubKey($"Software\\{App.ProjectName}");
_registryKey.SetValue("InstallLocation", _installLocation);
}
// check if drive that bloxstrap was installed to was removed from system, or had its drive letter changed
if (!Directory.Exists(_installLocation))
{
App.Logger.WriteLine(LOG_IDENT, "Could not find install location. Checking if drive has changed...");
bool driveExists = false;
string driveName = _installLocation[..3];
string? newDriveName = null;
foreach (var drive in DriveInfo.GetDrives())
{
if (drive.Name == driveName)
driveExists = true;
else if (Directory.Exists(_installLocation.Replace(driveName, drive.Name)))
newDriveName = drive.Name;
}
if (newDriveName is not null)
{
App.Logger.WriteLine(LOG_IDENT, $"Drive has changed from {driveName} to {newDriveName}");
Frontend.ShowMessageBox(
string.Format(Resources.Strings.InstallChecker_DriveLetterChangeDetected, driveName, newDriveName),
MessageBoxImage.Warning,
MessageBoxButton.OK
);
_installLocation = _installLocation.Replace(driveName, newDriveName);
_registryKey.SetValue("InstallLocation", _installLocation);
}
else if (!driveExists)
{
App.Logger.WriteLine(LOG_IDENT, $"Drive {driveName} does not exist anymore, and has likely been removed");
var result = Frontend.ShowMessageBox(
string.Format(Resources.Strings.InstallChecker_InstallDriveMissing, driveName),
MessageBoxImage.Warning,
MessageBoxButton.OKCancel
);
if (result != MessageBoxResult.OK)
App.Terminate();
FirstTimeRun();
return;
}
else
{
App.Logger.WriteLine(LOG_IDENT, "Drive has not changed, folder was likely moved or deleted");
}
}
App.BaseDirectory = _installLocation;
App.IsFirstRun = false;
}
public void Dispose()
{
_registryKey?.Dispose();
GC.SuppressFinalize(this);
}
private static void FirstTimeRun()
{
const string LOG_IDENT = "InstallChecker::FirstTimeRun";
App.Logger.WriteLine(LOG_IDENT, "Running first-time install");
App.BaseDirectory = Path.Combine(Paths.LocalAppData, App.ProjectName);
App.Logger.Initialize(true);
if (App.LaunchSettings.IsQuiet)
return;
App.IsSetupComplete = false;
App.FastFlags.Load();
Frontend.ShowLanguageSelection();
Frontend.ShowMenu();
// exit if we don't click the install button on installation
if (App.IsSetupComplete)
return;
App.Logger.WriteLine(LOG_IDENT, "Installation cancelled!");
App.Terminate(ErrorCode.ERROR_CANCELLED);
}
internal static void CheckUpgrade()
{
const string LOG_IDENT = "InstallChecker::CheckUpgrade";
if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application)
return;
// 2.0.0 downloads updates to <BaseFolder>/Updates so lol
bool isAutoUpgrade = Paths.Process.StartsWith(Path.Combine(Paths.Base, "Updates")) || Paths.Process.StartsWith(Path.Combine(Paths.LocalAppData, "Temp"));
FileVersionInfo existingVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Application);
FileVersionInfo currentVersionInfo = FileVersionInfo.GetVersionInfo(Paths.Process);
if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application))
return;
MessageBoxResult result;
// silently upgrade version if the command line flag is set or if we're launching from an auto update
if (App.LaunchSettings.IsUpgrade || isAutoUpgrade)
{
result = MessageBoxResult.Yes;
}
else
{
result = Frontend.ShowMessageBox(
Resources.Strings.InstallChecker_VersionDifferentThanInstalled,
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
}
if (result != MessageBoxResult.Yes)
return;
Filesystem.AssertReadOnly(Paths.Application);
// 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)
{
attempts++;
try
{
File.Delete(Paths.Application);
break;
}
catch (Exception)
{
if (attempts == 1)
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
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);
Bootstrapper.Register();
// update migrations
if (App.BuildMetadata.CommitRef.StartsWith("tag") && currentVersionInfo.ProductVersion is not null)
{
if (existingVersionInfo.ProductVersion == "2.4.0")
{
App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
App.FastFlags.SetValue("DFFlagVariableDPIScale2", null);
App.FastFlags.Save();
}
else if (existingVersionInfo.ProductVersion == "2.5.0")
{
App.FastFlags.SetValue("FIntDebugForceMSAASamples", null);
if (App.FastFlags.GetPreset("UI.Menu.Style.DisableV2") is not null)
App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false);
App.FastFlags.Save();
}
else if (existingVersionInfo.ProductVersion == "2.5.4")
{
if (App.Settings.Prop.UseDisableAppPatch)
{
try
{
File.Delete(Path.Combine(Paths.Modifications, "ExtraContent\\places\\Mobile.rbxl"));
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
}
App.Settings.Prop.EnableActivityTracking = true;
}
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ClassicFluentDialog)
App.Settings.Prop.BootstrapperStyle = BootstrapperStyle.FluentDialog;
_ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x);
if (x == 0)
{
App.FastFlags.SetPreset("Rendering.Framerate", null);
App.FastFlags.Save();
}
App.Settings.Save();
}
}
if (isAutoUpgrade)
{
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVersionInfo.ProductVersion}");
}
else if (!App.LaunchSettings.IsQuiet)
{
Frontend.ShowMessageBox(
string.Format(Resources.Strings.InstallChecker_Updated, currentVersionInfo.ProductVersion),
MessageBoxImage.Information,
MessageBoxButton.OK
);
Frontend.ShowMenu();
App.Terminate();
}
}
}
}

470
Bloxstrap/Installer.cs Normal file
View File

@ -0,0 +1,470 @@
using System.DirectoryServices;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Windows;
using System.Windows.Media.Animation;
using Bloxstrap.Resources;
using Microsoft.Win32;
namespace Bloxstrap
{
internal class Installer
{
private static string DesktopShortcut => Path.Combine(Paths.Desktop, "Bloxstrap.lnk");
private static string StartMenuShortcut => Path.Combine(Paths.WindowsStartMenu, "Bloxstrap.lnk");
public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap");
public bool CreateDesktopShortcuts = true;
public bool CreateStartMenuShortcuts = true;
public bool IsImplicitInstall = false;
public string InstallLocationError { get; set; } = "";
public void DoInstall()
{
// should've been created earlier from the write test anyway
Directory.CreateDirectory(InstallLocation);
Paths.Initialize(InstallLocation);
if (!IsImplicitInstall)
{
Filesystem.AssertReadOnly(Paths.Application);
File.Copy(Paths.Process, Paths.Application, true);
}
// TODO: registry access checks, i'll need to look back on issues to see what the error looks like
using (var uninstallKey = Registry.CurrentUser.CreateSubKey(App.UninstallKey))
{
uninstallKey.SetValue("DisplayIcon", $"{Paths.Application},0");
uninstallKey.SetValue("DisplayName", App.ProjectName);
uninstallKey.SetValue("DisplayVersion", App.Version);
if (uninstallKey.GetValue("InstallDate") is null)
uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
uninstallKey.SetValue("InstallLocation", Paths.Base);
uninstallKey.SetValue("NoRepair", 1);
uninstallKey.SetValue("Publisher", "pizzaboxer");
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");
}
// 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);
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application);
// TODO: implicit installation needs to reregister studio
if (CreateDesktopShortcuts)
Shortcut.Create(Paths.Application, "", DesktopShortcut);
if (CreateStartMenuShortcuts)
Shortcut.Create(Paths.Application, "", StartMenuShortcut);
// existing configuration persisting from an earlier install
App.Settings.Load();
App.State.Load();
App.FastFlags.Load();
}
private bool ValidateLocation()
{
// prevent from installing to the root of a drive
if (InstallLocation.Length <= 3)
return false;
// unc path, just to be safe
if (InstallLocation.StartsWith("\\\\"))
return false;
// prevent from installing to a onedrive folder
if (InstallLocation.Contains("OneDrive", StringComparison.InvariantCultureIgnoreCase))
return false;
// prevent from installing to an essential user profile folder (e.g. Documents, Downloads, Contacts idk)
if (String.Compare(Directory.GetParent(InstallLocation)?.FullName, Paths.UserProfile, StringComparison.InvariantCultureIgnoreCase) == 0)
return false;
return true;
}
public bool CheckInstallLocation()
{
if (string.IsNullOrEmpty(InstallLocation))
{
InstallLocationError = Strings.Menu_InstallLocation_NotSet;
}
else if (!ValidateLocation())
{
InstallLocationError = Strings.Menu_InstallLocation_CantInstall;
}
else
{
if (!IsImplicitInstall
&& !InstallLocation.EndsWith(App.ProjectName, StringComparison.InvariantCultureIgnoreCase)
&& Directory.Exists(InstallLocation)
&& Directory.EnumerateFileSystemEntries(InstallLocation).Any())
{
string suggestedChange = Path.Combine(InstallLocation, App.ProjectName);
MessageBoxResult result = Frontend.ShowMessageBox(
String.Format(Strings.Menu_InstallLocation_NotEmpty, suggestedChange),
MessageBoxImage.Warning,
MessageBoxButton.YesNoCancel,
MessageBoxResult.Yes
);
if (result == MessageBoxResult.Yes)
InstallLocation = suggestedChange;
else if (result == MessageBoxResult.Cancel || result == MessageBoxResult.None)
return false;
}
try
{
// check if we can write to the directory (a bit hacky but eh)
string testFile = Path.Combine(InstallLocation, $"{App.ProjectName}WriteTest.txt");
Directory.CreateDirectory(InstallLocation);
File.WriteAllText(testFile, "");
File.Delete(testFile);
}
catch (UnauthorizedAccessException)
{
InstallLocationError = Strings.Menu_InstallLocation_NoWritePerms;
}
catch (Exception ex)
{
InstallLocationError = ex.Message;
}
}
return String.IsNullOrEmpty(InstallLocationError);
}
public static void DoUninstall(bool keepData)
{
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
// prompt to shutdown roblox if its currently running
if (processes.Any())
{
if (!App.LaunchSettings.IsQuiet)
{
MessageBoxResult result = Frontend.ShowMessageBox(
Strings.Bootstrapper_Uninstall_RobloxRunning,
MessageBoxImage.Information,
MessageBoxButton.OKCancel
);
if (result != MessageBoxResult.OK)
App.Terminate(ErrorCode.ERROR_CANCELLED);
}
try
{
foreach (var process in processes)
{
process.Kill();
process.Close();
}
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to close process! {ex}");
}
}
string robloxFolder = Path.Combine(Paths.LocalAppData, "Roblox");
bool playerStillInstalled = true;
bool studioStillInstalled = true;
// check if stock bootstrapper is still installed
using var playerKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player");
var playerFolder = playerKey?.GetValue("InstallLocation");
if (playerKey is null || playerFolder is not string)
{
playerStillInstalled = false;
ProtocolHandler.Unregister("roblox");
ProtocolHandler.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);
}
using RegistryKey? studioBootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-studio");
if (studioBootstrapperKey is null)
{
studioStillInstalled = false;
#if STUDIO_FEATURES
ProtocolHandler.Unregister("roblox-studio");
ProtocolHandler.Unregister("roblox-studio-auth");
ProtocolHandler.Unregister("Roblox.Place");
ProtocolHandler.Unregister(".rbxl");
ProtocolHandler.Unregister(".rbxlx");
#endif
}
#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);
ProtocolHandler.RegisterRobloxPlace(studioLocation);
}
#endif
var cleanupSequence = new List<Action>
{
() => File.Delete(DesktopShortcut),
() => File.Delete(StartMenuShortcut),
() => Directory.Delete(Paths.Versions, true),
() => Directory.Delete(Paths.Downloads, true),
};
if (!keepData)
{
cleanupSequence.AddRange(new List<Action>
{
() => 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
});
}
bool deleteFolder = false;
if (Directory.Exists(Paths.Base))
{
var folderFiles = Directory.GetFiles(Paths.Base);
deleteFolder = folderFiles.Length == 1 && folderFiles.First().EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase);
}
if (deleteFolder)
cleanupSequence.Add(() => Directory.Delete(Paths.Base, true));
if (!playerStillInstalled && !studioStillInstalled && Directory.Exists(robloxFolder))
cleanupSequence.Add(() => Directory.Delete(robloxFolder, true));
cleanupSequence.Add(() => Registry.CurrentUser.DeleteSubKey(App.UninstallKey));
foreach (var process in cleanupSequence)
{
try
{
process();
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Encountered exception when running cleanup sequence (#{cleanupSequence.IndexOf(process)})");
App.Logger.WriteException(LOG_IDENT, ex);
}
}
if (Directory.Exists(Paths.Base))
{
// this is definitely one of the workaround hacks of all time
string deleteCommand;
if (deleteFolder)
deleteCommand = $"del /Q \"{Paths.Base}\\*\" && rmdir \"{Paths.Base}\"";
else
deleteCommand = $"del /Q \"{Paths.Application}\"";
Process.Start(new ProcessStartInfo()
{
FileName = "cmd.exe",
Arguments = $"/c timeout 5 && {deleteCommand}",
UseShellExecute = true,
WindowStyle = ProcessWindowStyle.Hidden
});
}
}
public static void HandleUpgrade()
{
const string LOG_IDENT = "Installer::HandleUpgrade";
if (!File.Exists(Paths.Application) || Paths.Process == Paths.Application)
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"));
var existingVer = FileVersionInfo.GetVersionInfo(Paths.Application).ProductVersion;
var currentVer = FileVersionInfo.GetVersionInfo(Paths.Process).ProductVersion;
if (MD5Hash.FromFile(Paths.Process) == MD5Hash.FromFile(Paths.Application))
return;
// silently upgrade version if the command line flag is set or if we're launching from an auto update
if (!App.LaunchSettings.IsUpgrade && !isAutoUpgrade)
{
var result = Frontend.ShowMessageBox(
Strings.InstallChecker_VersionDifferentThanInstalled,
MessageBoxImage.Question,
MessageBoxButton.YesNo
);
if (result != MessageBoxResult.Yes)
return;
}
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)
{
attempts++;
try
{
File.Delete(Paths.Application);
break;
}
catch (Exception)
{
if (attempts == 1)
App.Logger.WriteLine(LOG_IDENT, "Waiting for write permissions to update version");
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);
}
// update migrations
if (existingVer is not null)
{
if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan)
{
App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
App.FastFlags.SetValue("DFFlagVariableDPIScale2", null);
}
if (Utilities.CompareVersions(existingVer, "2.5.1") == VersionComparison.LessThan)
{
App.FastFlags.SetValue("FIntDebugForceMSAASamples", null);
if (App.FastFlags.GetPreset("UI.Menu.Style.DisableV2") is not null)
App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false);
}
if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan)
{
if (App.Settings.Prop.UseDisableAppPatch)
{
try
{
File.Delete(Path.Combine(Paths.Modifications, "ExtraContent\\places\\Mobile.rbxl"));
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
}
App.Settings.Prop.EnableActivityTracking = true;
}
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ClassicFluentDialog)
App.Settings.Prop.BootstrapperStyle = BootstrapperStyle.FluentDialog;
_ = 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)
{
string oldDesktopPath = Path.Combine(Paths.Desktop, "Play Roblox.lnk");
string oldStartPath = Path.Combine(Paths.WindowsStartMenu, "Bloxstrap");
if (File.Exists(oldDesktopPath))
File.Move(oldDesktopPath, DesktopShortcut);
if (Directory.Exists(oldStartPath))
{
try
{
Directory.Delete(oldStartPath, true);
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
}
Shortcut.Create(Paths.Application, "", StartMenuShortcut);
}
Registry.CurrentUser.DeleteSubKeyTree("Software\\Bloxstrap", false);
}
App.Settings.Save();
App.FastFlags.Save();
}
if (isAutoUpgrade)
{
Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki/Release-notes-for-Bloxstrap-v{currentVer}");
}
else if (!App.LaunchSettings.IsQuiet)
{
Frontend.ShowMessageBox(
string.Format(Strings.InstallChecker_Updated, currentVer),
MessageBoxImage.Information,
MessageBoxButton.OK
);
}
}
}
}

View File

@ -37,12 +37,6 @@ namespace Bloxstrap
{
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Save";
if (!App.ShouldSaveConfigs)
{
App.Logger.WriteLine(LOG_IDENT, "Save request ignored");
return;
}
App.Logger.WriteLine(LOG_IDENT, $"Saving to {FileLocation}...");
Directory.CreateDirectory(Path.GetDirectoryName(FileLocation)!);

230
Bloxstrap/LaunchHandler.cs Normal file
View File

@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Bloxstrap.UI.Elements.Dialogs;
using Bloxstrap.Resources;
using Microsoft.Win32;
using Windows.Win32;
using Windows.Win32.Foundation;
namespace Bloxstrap
{
public static class LaunchHandler
{
public static void ProcessNextAction(NextAction action, bool isUnfinishedInstall = false)
{
switch (action)
{
case NextAction.LaunchSettings:
LaunchSettings();
break;
case NextAction.LaunchRoblox:
LaunchRoblox();
break;
default:
App.Terminate(isUnfinishedInstall ? ErrorCode.ERROR_INSTALL_USEREXIT : ErrorCode.ERROR_SUCCESS);
break;
}
}
public static void ProcessLaunchArgs()
{
// this order is specific
if (App.LaunchSettings.IsUninstall)
LaunchUninstaller();
else if (App.LaunchSettings.IsMenuLaunch)
LaunchSettings();
else if (App.LaunchSettings.IsRobloxLaunch)
LaunchRoblox();
else if (!App.LaunchSettings.IsQuiet)
LaunchMenu();
}
public static void LaunchInstaller()
{
// TODO: detect duplicate launch, mutex maybe?
if (App.LaunchSettings.IsUninstall)
{
Frontend.ShowMessageBox(Strings.Bootstrapper_FirstRunUninstall, MessageBoxImage.Error);
App.Terminate(ErrorCode.ERROR_INVALID_FUNCTION);
return;
}
if (App.LaunchSettings.IsQuiet)
{
var installer = new Installer();
if (!installer.CheckInstallLocation())
App.Terminate(ErrorCode.ERROR_INSTALL_FAILURE);
installer.DoInstall();
ProcessLaunchArgs();
}
else
{
new LanguageSelectorDialog().ShowDialog();
var installer = new UI.Elements.Installer.MainWindow();
installer.ShowDialog();
ProcessNextAction(installer.CloseAction, !installer.Finished);
}
}
public static void LaunchUninstaller()
{
bool confirmed = false;
bool keepData = true;
if (App.LaunchSettings.IsQuiet)
{
confirmed = true;
}
else
{
var dialog = new UninstallerDialog();
dialog.ShowDialog();
confirmed = dialog.Confirmed;
keepData = dialog.KeepData;
}
if (!confirmed)
return;
Installer.DoUninstall(keepData);
Frontend.ShowMessageBox(Strings.Bootstrapper_SuccessfullyUninstalled, MessageBoxImage.Information);
}
public static void LaunchSettings()
{
const string LOG_IDENT = "LaunchHandler::LaunchSettings";
// TODO: move to mutex (especially because multi language whatever)
Process? menuProcess = Utilities.GetProcessesSafe().Where(x => x.MainWindowTitle == Strings.Menu_Title).FirstOrDefault();
if (menuProcess is not null)
{
var handle = menuProcess.MainWindowHandle;
App.Logger.WriteLine(LOG_IDENT, $"Found an already existing menu window with handle {handle}");
PInvoke.SetForegroundWindow((HWND)handle);
}
else
{
bool showAlreadyRunningWarning = Process.GetProcessesByName(App.ProjectName).Length > 1 && !App.LaunchSettings.IsQuiet;
new UI.Elements.Settings.MainWindow(showAlreadyRunningWarning).ShowDialog();
}
}
public static void LaunchMenu()
{
var dialog = new LaunchMenuDialog();
dialog.ShowDialog();
ProcessNextAction(dialog.CloseAction);
}
public static void LaunchRoblox()
{
const string LOG_IDENT = "LaunchHandler::LaunchRoblox";
bool installWebView2 = false;
if (!File.Exists(Path.Combine(Paths.System, "mfplat.dll")))
{
Frontend.ShowMessageBox(Strings.Bootstrapper_WMFNotFound, MessageBoxImage.Error);
if (!App.LaunchSettings.IsQuiet)
Utilities.ShellExecute("https://support.microsoft.com/en-us/topic/media-feature-pack-list-for-windows-n-editions-c1c6fffa-d052-8338-7a79-a4bb980a700a");
App.Terminate(ErrorCode.ERROR_FILE_NOT_FOUND);
}
{
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
// which often hangs around for a few seconds after the window closes
// it would be better to have this rely on the activity tracker when we implement IPC in the planned refactoring
var result = Frontend.ShowMessageBox(Strings.Bootstrapper_ConfirmLaunch, MessageBoxImage.Warning, MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
{
App.Terminate();
return;
}
}
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(App.LaunchSettings.RobloxLaunchArgs, App.LaunchSettings.RobloxLaunchMode, installWebView2);
IBootstrapperDialog? dialog = null;
if (!App.LaunchSettings.IsQuiet)
{
App.Logger.WriteLine(LOG_IDENT, "Initializing bootstrapper dialog");
dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
bootstrapper.Dialog = dialog;
dialog.Bootstrapper = bootstrapper;
}
Task bootstrapperTask = Task.Run(async () => await 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;
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);
});
// 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.IsNoLaunch && App.Settings.Prop.EnableActivityTracking)
App.NotifyIcon?.InitializeContextMenu();
App.Logger.WriteLine(LOG_IDENT, "Waiting for bootstrapper task to finish");
bootstrapperTask.Wait();
}
}
}

View File

@ -12,8 +12,11 @@ namespace Bloxstrap
{
public class LaunchSettings
{
[LaunchFlag(new[] { "-preferences", "-menu" })]
public bool IsMenuLaunch { get; private set; } = false;
[LaunchFlag(new[] { "-preferences", "-menu", "-settings" })]
public bool IsMenuLaunch { get; set; } = false;
[LaunchFlag("-player")]
public bool IsRobloxLaunch { get; set; } = false;
[LaunchFlag("-quiet")]
public bool IsQuiet { get; private set; } = false;

View File

@ -16,6 +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 timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'");
string filename = $"{App.ProjectName}_{timestamp}.log";

View File

@ -7,7 +7,7 @@
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);
public static string StartMenu => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", App.ProjectName);
public static string WindowsStartMenu => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs");
public static string System => Environment.GetFolderPath(Environment.SpecialFolder.System);
public static string Process => Environment.ProcessPath!;
@ -35,8 +35,6 @@
Modifications = Path.Combine(Base, "Modifications");
Application = Path.Combine(Base, $"{App.ProjectName}.exe");
}
}
}

View File

@ -73,9 +73,9 @@ namespace Bloxstrap
{
string handlerArgs = $"\"{handler}\" %1";
using RegistryKey uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}");
using RegistryKey uriIconKey = uriKey.CreateSubKey("DefaultIcon");
using RegistryKey uriCommandKey = uriKey.CreateSubKey(@"shell\open\command");
using var uriKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{key}");
using var uriIconKey = uriKey.CreateSubKey("DefaultIcon");
using var uriCommandKey = uriKey.CreateSubKey(@"shell\open\command");
if (uriKey.GetValue("") is null)
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

View File

@ -296,6 +296,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to You currently do not have the WebView2 runtime installed. Some Roblox features will not work properly without it, such as the desktop app. Would you like to download it now?.
/// </summary>
public static string Bootstrapper_WebView2NotFound {
get {
return ResourceManager.GetString("Bootstrapper.WebView2NotFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox requires the use of Windows Media Foundation components. You appear to be missing them, likely because you are using an N edition of Windows. Please install them first, and then launch Roblox..
/// </summary>
@ -440,6 +449,24 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Back.
/// </summary>
public static string Common_Navigation_Back {
get {
return ResourceManager.GetString("Common.Navigation.Back", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Next.
/// </summary>
public static string Common_Navigation_Next {
get {
return ResourceManager.GetString("Common.Navigation.Next", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to New.
/// </summary>
@ -657,6 +684,15 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap was unable to create shortcuts for the Desktop and Start menu. Try creating them later through Bloxstrap Settings..
/// </summary>
public static string Dialog_CannotCreateShortcuts {
get {
return ResourceManager.GetString("Dialog.CannotCreateShortcuts", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to More information:.
/// </summary>
@ -1139,6 +1175,223 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Will drop you into the desktop app once everything&apos;s done.
/// </summary>
public static string Installer_Completion_Launch_Description {
get {
return ResourceManager.GetString("Installer.Completion.Launch.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install and launch Roblox.
/// </summary>
public static string Installer_Completion_Launch_Title {
get {
return ResourceManager.GetString("Installer.Completion.Launch.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tweak with all the features it has to offer.
/// </summary>
public static string Installer_Completion_Settings_Description {
get {
return ResourceManager.GetString("Installer.Completion.Settings.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Configure Bloxstrap&apos;s settings.
/// </summary>
public static string Installer_Completion_Settings_Title {
get {
return ResourceManager.GetString("Installer.Completion.Settings.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap has successfully been installed.
///
///Roblox has not yet been installed, that will happen when you launch it with Bloxstrap for the first time. However, before you do that, you may want to configure Bloxstrap&apos;s settings first.
///
///Also, to keep Bloxstrap registered as the website launch handler, avoid using the &quot;Roblox Player&quot; shortcut to launch Roblox. If you don&apos;t see Bloxstrap show when launching from the website, simply launch Roblox with Bloxstrap once from the desktop to fix it.
///
///What would y [rest of string was truncated]&quot;;.
/// </summary>
public static string Installer_Completion_Text {
get {
return ResourceManager.GetString("Installer.Completion.Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Completion.
/// </summary>
public static string Installer_Completion_Title {
get {
return ResourceManager.GetString("Installer.Completion.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Existing data found. Your mods and settings will be restored..
/// </summary>
public static string Installer_Install_Location_DataFound {
get {
return ResourceManager.GetString("Installer.Install.Location.DataFound", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Roblox will also be installed to this path. Change this if you prefer to install all your games to a separate drive. Otherwise, it&apos;s recommended that you keep this as it is..
/// </summary>
public static string Installer_Install_Location_Text {
get {
return ResourceManager.GetString("Installer.Install.Location.Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose where to install to.
/// </summary>
public static string Installer_Install_Location_Title {
get {
return ResourceManager.GetString("Installer.Install.Location.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create Desktop shortcuts.
/// </summary>
public static string Installer_Install_Shortcuts_Desktop {
get {
return ResourceManager.GetString("Installer.Install.Shortcuts.Desktop", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create Start Menu shortcuts.
/// </summary>
public static string Installer_Install_Shortcuts_StartMenu {
get {
return ResourceManager.GetString("Installer.Install.Shortcuts.StartMenu", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shortcuts.
/// </summary>
public static string Installer_Install_Shortcuts_Title {
get {
return ResourceManager.GetString("Installer.Install.Shortcuts.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install.
/// </summary>
public static string Installer_Install_Title {
get {
return ResourceManager.GetString("Installer.Install.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap Installer.
/// </summary>
public static string Installer_Title {
get {
return ResourceManager.GetString("Installer.Title", resourceCulture);
}
}
/// <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.
///
///This installation process will be quick and simple, and you will be able to configure any of Bloxstrap&apos;s settings after installation..
/// </summary>
public static string Installer_Welcome_MainText {
get {
return ResourceManager.GetString("Installer.Welcome.MainText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please click &apos;Next&apos; to continue..
/// </summary>
public static string Installer_Welcome_NextToContinue {
get {
return ResourceManager.GetString("Installer.Welcome.NextToContinue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Welcome.
/// </summary>
public static string Installer_Welcome_Title {
get {
return ResourceManager.GetString("Installer.Welcome.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You are trying to install version {0} of Bloxstrap, but the latest version available is {1}. Would you like to download it?.
/// </summary>
public static string Installer_Welcome_UpdateNotice {
get {
return ResourceManager.GetString("Installer.Welcome.UpdateNotice", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Configure settings.
/// </summary>
public static string LaunchMenu_ConfigureSettings {
get {
return ResourceManager.GetString("LaunchMenu.ConfigureSettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Launch Roblox.
/// </summary>
public static string LaunchMenu_LaunchRoblox {
get {
return ResourceManager.GetString("LaunchMenu.LaunchRoblox", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to What do you want to do?.
/// </summary>
public static string LaunchMenu_Title {
get {
return ResourceManager.GetString("LaunchMenu.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to See the Wiki for help.
/// </summary>
public static string LaunchMenu_Wiki_Description {
get {
return ResourceManager.GetString("LaunchMenu.Wiki.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Having an issue?.
/// </summary>
public static string LaunchMenu_Wiki_Title {
get {
return ResourceManager.GetString("LaunchMenu.Wiki.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to No log file will be written for this launch because Bloxstrap is unable to write to the folder at &apos;{0}&apos;.
/// </summary>
@ -2103,87 +2356,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Install.
/// </summary>
public static string Menu_Install {
get {
return ResourceManager.GetString("Menu.Install", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Configure how Bloxstrap/Roblox is installed..
/// </summary>
public static string Menu_Installation_Description {
get {
return ResourceManager.GetString("Menu.Installation.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Choose where Bloxstrap should be installed to..
/// </summary>
public static string Menu_Installation_InstallLocation_Description {
get {
return ResourceManager.GetString("Menu.Installation.InstallLocation.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Install Location.
/// </summary>
public static string Menu_Installation_InstallLocation_Title {
get {
return ResourceManager.GetString("Menu.Installation.InstallLocation.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Where Bloxstrap is currently installed to..
/// </summary>
public static string Menu_Installation_OpenInstallFolder_Description {
get {
return ResourceManager.GetString("Menu.Installation.OpenInstallFolder.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Installation Folder.
/// </summary>
public static string Menu_Installation_OpenInstallFolder_Title {
get {
return ResourceManager.GetString("Menu.Installation.OpenInstallFolder.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Installation.
/// </summary>
public static string Menu_Installation_Title {
get {
return ResourceManager.GetString("Menu.Installation.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Here&apos;s a guide on how to uninstall Bloxstrap..
/// </summary>
public static string Menu_Installation_UninstallGuide_Description {
get {
return ResourceManager.GetString("Menu.Installation.UninstallGuide.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Looking to uninstall?.
/// </summary>
public static string Menu_Installation_UninstallGuide_Title {
get {
return ResourceManager.GetString("Menu.Installation.UninstallGuide.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap cannot be installed here. Please choose a different location, or resort to using the default location by clicking the reset button..
/// </summary>
@ -2523,15 +2695,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap must first be installed..
/// </summary>
public static string Menu_Mods_OpenModsFolder_MustBeInstalled {
get {
return ResourceManager.GetString("Menu.Mods.OpenModsFolder.MustBeInstalled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Mods Folder.
/// </summary>
@ -2649,42 +2812,6 @@ namespace Bloxstrap.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to There&apos;s just a few things you first should know about..
/// </summary>
public static string Menu_PreInstall_Description {
get {
return ResourceManager.GetString("Menu.PreInstall.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to After installation has finished, the Bloxstrap Menu will be registered as an application in the Start menu. If you ever need to access it again to re-adjust your settings, or access resources such as Fast Flag management, you can find it there..
/// </summary>
public static string Menu_PreInstall_Info_1 {
get {
return ResourceManager.GetString("Menu.PreInstall.Info.1", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to If you ever need help or guidance with anything, be sure to check the [Wiki]({0}). If you still need something, open an [issue]({1}) on GitHub, or join our [Discord server]({2})..
/// </summary>
public static string Menu_PreInstall_Info_2 {
get {
return ResourceManager.GetString("Menu.PreInstall.Info.2", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Before you install....
/// </summary>
public static string Menu_PreInstall_Title {
get {
return ResourceManager.GetString("Menu.PreInstall.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Save.
/// </summary>
@ -2713,12 +2840,63 @@ namespace Bloxstrap.Resources {
}
/// <summary>
/// Looks up a localized string similar to Bloxstrap Menu.
/// Looks up a localized string similar to Bloxstrap Settings.
/// </summary>
public static string Menu_Title {
get {
return ResourceManager.GetString("Menu.Title", 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>
public static string Uninstaller_KeepData_Description {
get {
return ResourceManager.GetString("Uninstaller.KeepData.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Keep my settings and mods.
/// </summary>
public static string Uninstaller_KeepData_Label {
get {
return ResourceManager.GetString("Uninstaller.KeepData.Label", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Uninstalling will remove Bloxstrap from your system, and automatically reconfigure the default Roblox launcher if it&apos;s still installed.
///
///If you&apos;re uninstalling or reinstalling because you are having issues with Roblox, read [this help page]({0}) first.
///
///The uninstall process may not be able to fully clean up itself, so you may need to manually clean up leftover files where Bloxstrap was installed.
///
///Bloxstrap was installed at &quot;{1}&quot;..
/// </summary>
public static string Uninstaller_Text {
get {
return ResourceManager.GetString("Uninstaller.Text", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Uninstall Bloxstrap.
/// </summary>
public static string Uninstaller_Title {
get {
return ResourceManager.GetString("Uninstaller.Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Uninstall.
/// </summary>
public static string Uninstaller_Uninstall {
get {
return ResourceManager.GetString("Uninstaller.Uninstall", resourceCulture);
}
}
}
}

View File

@ -707,33 +707,6 @@ Do NOT use this to import large "flag lists" made by other people that promise t
<data name="Menu.IconFiles" xml:space="preserve">
<value>Icon files</value>
</data>
<data name="Menu.Install" xml:space="preserve">
<value>Install</value>
</data>
<data name="Menu.Installation.Description" xml:space="preserve">
<value>Configure how Bloxstrap/Roblox is installed.</value>
</data>
<data name="Menu.Installation.InstallLocation.Description" xml:space="preserve">
<value>Choose where Bloxstrap should be installed to.</value>
</data>
<data name="Menu.Installation.InstallLocation.Title" xml:space="preserve">
<value>Install Location</value>
</data>
<data name="Menu.Installation.OpenInstallFolder.Description" xml:space="preserve">
<value>Where Bloxstrap is currently installed to.</value>
</data>
<data name="Menu.Installation.OpenInstallFolder.Title" xml:space="preserve">
<value>Open Installation Folder</value>
</data>
<data name="Menu.Installation.Title" xml:space="preserve">
<value>Installation</value>
</data>
<data name="Menu.Installation.UninstallGuide.Description" xml:space="preserve">
<value>Here's a guide on how to uninstall Bloxstrap.</value>
</data>
<data name="Menu.Installation.UninstallGuide.Title" xml:space="preserve">
<value>Looking to uninstall?</value>
</data>
<data name="Menu.InstallLocation.CantInstall" xml:space="preserve">
<value>Bloxstrap cannot be installed here. Please choose a different location, or resort to using the default location by clicking the reset button.</value>
</data>
@ -852,9 +825,6 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Menu.Mods.OpenModsFolder.Description" xml:space="preserve">
<value>Manage custom Roblox mods here.</value>
</data>
<data name="Menu.Mods.OpenModsFolder.MustBeInstalled" xml:space="preserve">
<value>Bloxstrap must first be installed.</value>
</data>
<data name="Menu.Mods.OpenModsFolder.Title" xml:space="preserve">
<value>Open Mods Folder</value>
</data>
@ -894,18 +864,6 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Menu.MoreInfo" xml:space="preserve">
<value>Click for more information on this option.</value>
</data>
<data name="Menu.PreInstall.Description" xml:space="preserve">
<value>There's just a few things you first should know about.</value>
</data>
<data name="Menu.PreInstall.Info.1" xml:space="preserve">
<value>After installation has finished, the Bloxstrap Menu will be registered as an application in the Start menu. If you ever need to access it again to re-adjust your settings, or access resources such as Fast Flag management, you can find it there.</value>
</data>
<data name="Menu.PreInstall.Info.2" xml:space="preserve">
<value>If you ever need help or guidance with anything, be sure to check the [Wiki]({0}). If you still need something, open an [issue]({1}) on GitHub, or join our [Discord server]({2}).</value>
</data>
<data name="Menu.PreInstall.Title" xml:space="preserve">
<value>Before you install...</value>
</data>
<data name="Menu.Save" xml:space="preserve">
<value>Save</value>
</data>
@ -916,7 +874,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<value>Settings saved!</value>
</data>
<data name="Menu.Title" xml:space="preserve">
<value>Bloxstrap Menu</value>
<value>Bloxstrap Settings</value>
</data>
<data name="Dialog.LanguageSelector.Header" xml:space="preserve">
<value>Choose preferred language</value>
@ -1027,4 +985,116 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="ContextMenu.RobloxNotRunning" xml:space="preserve">
<value>Roblox is still launching. A log file will only be available once Roblox launches.</value>
</data>
<data name="Installer.Title" xml:space="preserve">
<value>Bloxstrap Installer</value>
</data>
<data name="Installer.Welcome.Title" xml:space="preserve">
<value>Welcome</value>
</data>
<data name="Installer.Install.Title" xml:space="preserve">
<value>Install</value>
</data>
<data name="Installer.Completion.Title" xml:space="preserve">
<value>Completion</value>
</data>
<data name="Common.Navigation.Back" xml:space="preserve">
<value>Back</value>
</data>
<data name="Common.Navigation.Next" xml:space="preserve">
<value>Next</value>
</data>
<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.
This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation.</value>
</data>
<data name="Installer.Welcome.NextToContinue" xml:space="preserve">
<value>Please click 'Next' to continue.</value>
</data>
<data name="Installer.Welcome.UpdateNotice" xml:space="preserve">
<value>You are trying to install version {0} of Bloxstrap, but the latest version available is {1}. Would you like to download it?</value>
</data>
<data name="Installer.Install.Location.Title" xml:space="preserve">
<value>Choose where to install to</value>
</data>
<data name="Installer.Install.Location.Text" xml:space="preserve">
<value>Roblox will also be installed to this path. Change this if you prefer to install all your games to a separate drive. Otherwise, it's recommended that you keep this as it is.</value>
</data>
<data name="Installer.Install.Location.DataFound" xml:space="preserve">
<value>Existing data found. Your mods and settings will be restored.</value>
</data>
<data name="Installer.Install.Shortcuts.Title" xml:space="preserve">
<value>Shortcuts</value>
</data>
<data name="Installer.Install.Shortcuts.Desktop" xml:space="preserve">
<value>Create Desktop shortcuts</value>
</data>
<data name="Installer.Install.Shortcuts.StartMenu" xml:space="preserve">
<value>Create Start Menu shortcuts</value>
</data>
<data name="Installer.Completion.Text" xml:space="preserve">
<value>Bloxstrap has successfully been installed.
Roblox has not yet been installed, that will happen when you launch it with Bloxstrap for the first time. However, before you do that, you may want to configure Bloxstrap's settings first.
Also, to keep Bloxstrap registered as the website launch handler, avoid using the "Roblox Player" shortcut to launch Roblox. If you don't see Bloxstrap show when launching from the website, simply launch Roblox with Bloxstrap once from the desktop to fix it.
What would you like to do?</value>
</data>
<data name="Installer.Completion.Settings.Title" xml:space="preserve">
<value>Configure Bloxstrap's settings</value>
</data>
<data name="Installer.Completion.Settings.Description" xml:space="preserve">
<value>Tweak with all the features it has to offer</value>
</data>
<data name="Installer.Completion.Launch.Title" xml:space="preserve">
<value>Install and launch Roblox</value>
</data>
<data name="Installer.Completion.Launch.Description" xml:space="preserve">
<value>Will drop you into the desktop app once everything's done</value>
</data>
<data name="Uninstaller.Title" xml:space="preserve">
<value>Uninstall Bloxstrap</value>
</data>
<data name="Uninstaller.Text" xml:space="preserve">
<value>Uninstalling will remove Bloxstrap from your system, and automatically reconfigure the default Roblox launcher if it's still installed.
If you're uninstalling or reinstalling because you are having issues with Roblox, read [this help page]({0}) first.
The uninstall process may not be able to fully clean up itself, so you may need to manually clean up leftover files where Bloxstrap was installed.
Bloxstrap was installed at "{1}".</value>
</data>
<data name="Uninstaller.KeepData.Label" xml:space="preserve">
<value>Keep my settings and mods</value>
</data>
<data name="Uninstaller.KeepData.Description" xml:space="preserve">
<value>They'll be kept where Bloxstrap was installed, and will automatically be restored on a reinstall.</value>
</data>
<data name="Uninstaller.Uninstall" xml:space="preserve">
<value>Uninstall</value>
</data>
<data name="LaunchMenu.Title" xml:space="preserve">
<value>What do you want to do?</value>
</data>
<data name="LaunchMenu.LaunchRoblox" xml:space="preserve">
<value>Launch Roblox</value>
</data>
<data name="LaunchMenu.ConfigureSettings" xml:space="preserve">
<value>Configure settings</value>
</data>
<data name="LaunchMenu.Wiki.Title" xml:space="preserve">
<value>Having an issue?</value>
</data>
<data name="LaunchMenu.Wiki.Description" xml:space="preserve">
<value>See the Wiki for help</value>
</data>
<data name="Bootstrapper.WebView2NotFound" xml:space="preserve">
<value>You currently do not have the WebView2 runtime installed. Some Roblox features will not work properly without it, such as the desktop app. Would you like to download it now?</value>
</data>
<data name="Dialog.CannotCreateShortcuts" xml:space="preserve">
<value>Bloxstrap was unable to create shortcuts for the Desktop and Start menu. Try creating them later through Bloxstrap Settings.</value>
</data>
</root>

View File

@ -1,5 +1,6 @@
namespace Bloxstrap
{
// TODO: this is a mess and desperately needs refactoring
public static class RobloxDeployment
{
public const string DefaultChannel = "LIVE";
@ -52,7 +53,7 @@
// this function serves double duty as the setup mirror enumerator, and as our connectivity check
// since we're basically asking four different urls for the exact same thing, if all four fail, then it has to be a user-side problem
// this should be checked for in the installer, in the menu, and in the bootstrapper, as each of those have a dedicated spot they show in
// this should be checked for in the installer and in the bootstrapper
// returns null for success
@ -168,7 +169,7 @@
{
var defaultClientVersion = await GetInfo(DefaultChannel);
if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == -1)
if (Utilities.CompareVersions(clientVersion.Version, defaultClientVersion.Version) == VersionComparison.LessThan)
clientVersion.IsBehindDefaultChannel = true;
}

View File

@ -8,7 +8,7 @@
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
Title="Bloxstrap"
Title="{x:Static resources:Strings.Installer_Title}"
MinWidth="0"
MinHeight="0"
Width="380"
@ -24,10 +24,10 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" Title="Bloxstrap" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" />
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" Title="{x:Static resources:Strings.Installer_Title}" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<StackPanel Grid.Row="1" Margin="12">
<TextBlock Text="{x:Static resources:Strings.Dialog_LanguageSelector_Header}" FontSize="18" FontWeight="Medium" />
<TextBlock Text="{x:Static resources:Strings.Dialog_LanguageSelector_Header}" FontSize="20" FontWeight="SemiBold" />
<TextBlock Text="{x:Static resources:Strings.Dialog_LanguageSelector_Subtext}" TextWrapping="Wrap" Margin="0,0,0,12" />
<ComboBox ItemsSource="{Binding Languages, Mode=OneTime}" Text="{Binding SelectedLanguage, Mode=TwoWay}" />
</StackPanel>

View File

@ -0,0 +1,54 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.Dialogs.LaunchMenuDialog"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
Title="Bloxstrap"
MinWidth="0"
MinHeight="0"
Width="320"
SizeToContent="Height"
ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<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" />
<StackPanel Grid.Row="1" Margin="12">
<TextBlock FontSize="24" Text="{x:Static resources:Strings.LaunchMenu_Title}" HorizontalAlignment="Center" Margin="0,0,0,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="Settings28" Command="{Binding LaunchSettingsCommand, Mode=OneTime}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.LaunchMenu_ConfigureSettings}" />
</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>
<TextBlock Margin="0,16,0,0" FontSize="12" Text="{Binding Version, Mode=OneTime}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Bloxstrap.UI.ViewModels.Dialogs;
using Bloxstrap.UI.ViewModels.Installer;
using Wpf.Ui.Mvvm.Interfaces;
namespace Bloxstrap.UI.Elements.Dialogs
{
/// <summary>
/// Interaction logic for LaunchMenuDialog.xaml
/// </summary>
public partial class LaunchMenuDialog
{
public NextAction CloseAction = NextAction.Terminate;
public LaunchMenuDialog()
{
var viewModel = new LaunchMenuViewModel();
viewModel.CloseWindowRequest += (_, closeAction) =>
{
CloseAction = closeAction;
Close();
};
DataContext = viewModel;
InitializeComponent();
ApplyTheme();
}
}
}

View File

@ -0,0 +1,56 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.Dialogs.UninstallerDialog"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Dialogs"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Dialogs"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance dmodels:UninstallerViewModel, IsDesignTimeCreatable=True}"
Title="Bloxstrap"
MinWidth="0"
MinHeight="0"
Width="480"
SizeToContent="Height"
ResizeMode="NoResize"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</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" />
<StackPanel Grid.Row="1" Margin="12">
<TextBlock FontSize="20" FontWeight="SemiBold" Text="{x:Static resources:Strings.Uninstaller_Title}" />
<controls:MarkdownTextBlock FontSize="14" Margin="0,0,0,16" MarkdownText="{Binding Text, Mode=OneTime}" TextWrapping="Wrap" />
<CheckBox Content="{x:Static resources:Strings.Uninstaller_KeepData_Label}" IsChecked="{Binding KeepData, Mode=TwoWay}" />
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Uninstaller_KeepData_Description}" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding KeepData, Mode=OneWay}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<Border Grid.Row="2" Margin="0,10,0,0" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button MinWidth="100" Content="{x:Static resources:Strings.Uninstaller_Uninstall}" Command="{Binding ConfirmUninstallCommand}" />
<Button MinWidth="100" Margin="12,0,0,0" Content="{x:Static resources:Strings.Common_Cancel}" IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Bloxstrap.UI.ViewModels.Dialogs;
using Bloxstrap.UI.ViewModels.Installer;
using Wpf.Ui.Mvvm.Interfaces;
namespace Bloxstrap.UI.Elements.Dialogs
{
/// <summary>
/// Interaction logic for UninstallerDialog.xaml
/// </summary>
public partial class UninstallerDialog
{
public bool Confirmed { get; private set; } = false;
public bool KeepData { get; private set; } = true;
public UninstallerDialog()
{
var viewModel = new UninstallerViewModel();
viewModel.ConfirmUninstallRequest += (_, _) =>
{
Confirmed = true;
KeepData = viewModel.KeepData;
Close();
};
DataContext = viewModel;
InitializeComponent();
ApplyTheme();
}
}
}

View File

@ -0,0 +1,79 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.Installer.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.Installer.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Installer"
mc:Ignorable="d"
Title="{x:Static resources:Strings.Installer_Title}"
Height="540" Width="840"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Padding="8" x:Name="RootTitleBar" Grid.Row="0" ForceShutdown="False" MinimizeToTray="False" UseSnapLayout="True" Title="{x:Static resources:Strings.Installer_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:NavigationFluent x:Name="RootNavigation" Grid.Row="1" Grid.Column="0" Margin="0,0,12,0" Frame="{Binding ElementName=RootFrame}" SelectedPageIndex="0">
<ui:NavigationFluent.Items>
<ui:NavigationItem Content="{x:Static resources:Strings.Installer_Welcome_Title}" PageType="{x:Type pages:WelcomePage}" Icon="ArrowCircleRight48" />
<ui:NavigationItem Content="{x:Static resources:Strings.Installer_Install_Title}" PageType="{x:Type pages:InstallPage}" Icon="ArrowCircleRight48" />
<ui:NavigationItem Content="{x:Static resources:Strings.Installer_Completion_Title}" PageType="{x:Type pages:CompletionPage}" Icon="ArrowCircleRight48" />
</ui:NavigationFluent.Items>
</ui:NavigationFluent>
<Grid Grid.Row="0" Grid.RowSpan="2" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:Breadcrumb Grid.Row="0" Margin="0,0,0,5" Navigation="{Binding ElementName=RootNavigation}" />
<Frame x:Name="RootFrame" Grid.Row="1" />
</Grid>
</Grid>
<StatusBar x:Name="RootStatusBar" Grid.Row="2" Padding="14,10" Background="{ui:ThemeResource ApplicationBackgroundBrush}" BorderThickness="0,1,0,0">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="1" Padding="0,0,4,0">
<ui:Button Content="{x:Static resources:Strings.Common_Navigation_Back}" Width="96" Command="{Binding BackPageCommand, Mode=OneWay}" IsEnabled="{Binding BackButtonEnabled, Mode=OneWay}" />
</StatusBarItem>
<StatusBarItem Grid.Column="2" Padding="4,0,4,0">
<ui:Button Name="NextButton" Content="{Binding NextButtonText, Mode=OneWay}" Width="96" Command="{Binding NextPageCommand, Mode=OneWay}" IsEnabled="{Binding NextButtonEnabled, Mode=OneWay}" />
</StatusBarItem>
<StatusBarItem Grid.Column="3" Padding="12,0,0,0">
<ui:Button Content="{x:Static resources:Strings.Common_Close}" Width="96" Command="{Binding CloseWindowCommand, Mode=OneWay}" />
</StatusBarItem>
</StatusBar>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,137 @@
using System.Windows.Controls;
using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts;
using System.ComponentModel;
using System.Windows;
using Bloxstrap.UI.ViewModels.Installer;
using Bloxstrap.UI.Elements.Installer.Pages;
using Bloxstrap.UI.Elements.Base;
using System.Windows.Media.Animation;
using System.Reflection.Metadata.Ecma335;
namespace Bloxstrap.UI.Elements.Installer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
///
/// The logic behind this wizard-like interface is full of gross hacks, but there's no easy way to do this and I've tried to
/// make it as nice and MVVM-"""conformant""" as can possibly be ¯\_(ツ)_/¯
///
/// Page ViewModels can request changing of navigation button states through the following call flow:
/// - Page ViewModel holds event for requesting button state change
/// - Page CodeBehind subscribes to event on page creation
/// - Page ViewModel invokes event when ready
/// - Page CodeBehind receives it, gets MainWindow, and directly calls MainWindow.SetButtonEnabled()
/// - MainWindow.SetButtonEnabled() directly calls MainWindowViewModel.SetButtonEnabled() which does the thing a voila
///
/// Page ViewModels can also be notified of when the next page button has been pressed and stop progression if needed through a callback
/// - MainWindow has a single-set Func<bool> property named NextPageCallback which is reset on every page load
/// - This callback is called when the next page button is pressed
/// - Page CodeBehind gets MainWindow and sets the callback to its own local function on page load
/// - CodeBehind's local function then directly calls the ViewModel to do whatever it needs to do
///
/// TODO: theme selection
public partial class MainWindow : WpfUiWindow, INavigationWindow
{
internal readonly MainWindowViewModel _viewModel = new();
private Type _currentPage = typeof(WelcomePage);
private List<Type> _pages = new() { typeof(WelcomePage), typeof(InstallPage), typeof(CompletionPage) };
public Func<bool>? NextPageCallback;
public NextAction CloseAction = NextAction.Terminate;
public bool Finished => _currentPage == _pages.Last();
public MainWindow()
{
_viewModel.CloseWindowRequest += (_, _) => CloseWindow();
_viewModel.PageRequest += (_, type) =>
{
if (type == "next")
NextPage();
else if (type == "back")
BackPage();
};
DataContext = _viewModel;
InitializeComponent();
ApplyTheme();
App.Logger.WriteLine("MainWindow::MainWindow", "Initializing installer");
Closing += new CancelEventHandler(MainWindow_Closing);
}
void NextPage()
{
if (NextPageCallback is not null && !NextPageCallback())
return;
if (_currentPage == _pages.Last())
return;
var page = _pages[_pages.IndexOf(_currentPage) + 1];
Navigate(page);
SetButtonEnabled("next", page != _pages.Last());
SetButtonEnabled("back", true);
}
void BackPage()
{
if (_currentPage == _pages.First())
return;
var page = _pages[_pages.IndexOf(_currentPage) - 1];
Navigate(page);
SetButtonEnabled("next", true);
SetButtonEnabled("back", page != _pages.First());
}
void MainWindow_Closing(object? sender, CancelEventArgs e)
{
if (Finished)
return;
var result = Frontend.ShowMessageBox("Are you sure you want to cancel the installation?", MessageBoxImage.Warning, MessageBoxButton.YesNo);
if (result != MessageBoxResult.Yes)
e.Cancel = true;
}
public void SetNextButtonText(string text) => _viewModel.SetNextButtonText(text);
public void SetButtonEnabled(string type, bool state) => _viewModel.SetButtonEnabled(type, state);
#region INavigationWindow methods
public Frame GetFrame() => RootFrame;
public INavigation GetNavigation() => RootNavigation;
public bool Navigate(Type pageType)
{
_currentPage = pageType;
NextPageCallback = null;
return 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,32 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Installer.Pages.CompletionPage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Installer.Pages"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="CompletionPage"
Scrollable="True"
Loaded="UiPage_Loaded">
<StackPanel Margin="0,0,14,14">
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Installer_Completion_Text}" TextWrapping="Wrap" Margin="0,8,0,0" />
<ui:CardAction Margin="0,16,0,0" Icon="Settings28" Command="{Binding LaunchSettingsCommand, Mode=OneTime}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Installer_Completion_Settings_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Installer_Completion_Settings_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Margin="0,8,0,0" Icon="ArrowRight12" Command="{Binding LaunchRobloxCommand, Mode=OneTime}">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Installer_Completion_Launch_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Installer_Completion_Launch_Description}" Padding="0,0,16,0" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardAction>
</StackPanel>
</ui:UiPage>

View File

@ -0,0 +1,36 @@
using System.Windows;
using Bloxstrap.UI.ViewModels.Installer;
namespace Bloxstrap.UI.Elements.Installer.Pages
{
/// <summary>
/// Interaction logic for CompletionPage.xaml
/// </summary>
public partial class CompletionPage
{
private readonly CompletionViewModel _viewModel = new();
public CompletionPage()
{
_viewModel.CloseWindowRequest += (_, closeAction) =>
{
if (Window.GetWindow(this) is MainWindow window)
{
window.CloseAction = closeAction;
window.Close();
}
};
DataContext = _viewModel;
InitializeComponent();
}
private void UiPage_Loaded(object sender, RoutedEventArgs e)
{
if (Window.GetWindow(this) is MainWindow window)
{
window.SetNextButtonText("Next");
window.SetButtonEnabled("back", false);
}
}
}
}

View File

@ -0,0 +1,62 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Installer.Pages.InstallPage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Installer.Pages"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="InstallPage"
Scrollable="True"
Loaded="UiPage_Loaded">
<StackPanel Margin="0,0,14,14">
<TextBlock FontSize="20" FontWeight="SemiBold" Text="{x:Static resources:Strings.Installer_Install_Location_Title}" TextWrapping="Wrap" />
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Installer_Install_Location_Text}" TextWrapping="Wrap" />
<ui:Card Margin="0,8,0,0" Padding="12">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Margin="0,0,4,0" Text="{Binding InstallLocation, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<ui:Button Grid.Column="1" Margin="4,0,4,0" Height="35" Icon="Folder24" Content="{x:Static resources:Strings.Common_Browse}" Command="{Binding BrowseInstallLocationCommand}" />
<ui:Button Grid.Column="2" Margin="4,0,0,0" Height="35" Icon="ArrowCounterclockwise24" Content="{x:Static resources:Strings.Common_Reset}" Command="{Binding ResetInstallLocationCommand}" />
</Grid>
</ui:Card>
<TextBlock Margin="0,8,0,0" FontSize="14" Text="{x:Static resources:Strings.Installer_Install_Location_DataFound}" Visibility="{Binding DataFoundMessageVisibility, Mode=OneWay}" TextWrapping="Wrap" />
<TextBlock FontSize="14" Text="{Binding ErrorMessage, Mode=OneWay}" Foreground="{DynamicResource SystemFillColorCriticalBrush}" TextWrapping="Wrap" Margin="0,4,0,0">
<TextBlock.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=Self},Path=Text}" Value="">
<Setter Property="UIElement.Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock FontSize="20" FontWeight="SemiBold" Text="{x:Static resources:Strings.Installer_Install_Shortcuts_Title}" TextWrapping="Wrap" Margin="0,16,0,0" />
<controls:OptionControl
Header="{x:Static resources:Strings.Installer_Install_Shortcuts_Desktop}">
<ui:ToggleSwitch IsChecked="{Binding CreateDesktopShortcuts, Mode=TwoWay}" />
</controls:OptionControl>
<controls:OptionControl
Header="{x:Static resources:Strings.Installer_Install_Shortcuts_StartMenu}">
<ui:ToggleSwitch IsChecked="{Binding CreateStartMenuShortcuts, Mode=TwoWay}" />
</controls:OptionControl>
</StackPanel>
</ui:UiPage>

View File

@ -0,0 +1,39 @@
using System.Windows;
using System.Windows.Controls;
using Bloxstrap.UI.ViewModels.Installer;
namespace Bloxstrap.UI.Elements.Installer.Pages
{
/// <summary>
/// Interaction logic for WelcomePage.xaml
/// </summary>
public partial class InstallPage
{
private readonly InstallViewModel _viewModel = new();
public InstallPage()
{
DataContext = _viewModel;
_viewModel.SetCanContinueEvent += (_, state) =>
{
if (Window.GetWindow(this) is MainWindow window)
window.SetButtonEnabled("next", state);
};
InitializeComponent();
}
private void UiPage_Loaded(object sender, RoutedEventArgs e)
{
if (Window.GetWindow(this) is MainWindow window)
{
window.SetNextButtonText("Install");
window.NextPageCallback += NextPageCallback;
}
}
public bool NextPageCallback() => _viewModel.DoInstall();
}
}

View File

@ -0,0 +1,55 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Installer.Pages.WelcomePage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Installer.Pages"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.Installer"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:enums="clr-namespace:Bloxstrap.Enums"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance dmodels:WelcomeViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="450" d:DesignWidth="800"
Title="WelcomePage"
Scrollable="True"
Loaded="UiPage_Loaded">
<StackPanel Margin="0,0,14,14">
<controls:MarkdownTextBlock FontSize="14" MarkdownText="{Binding MainText, Mode=OneWay}" TextWrapping="Wrap" />
<Grid Margin="0,24,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding VersionNotice, Mode=OneWay}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="32" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/MessageBox/Warning.png" />
<TextBlock Grid.Column="1" FontSize="14" Margin="12,0,12,0" Text="{Binding VersionNotice, Mode=OneWay}" TextWrapping="Wrap" />
<ui:Button Grid.Column="2" Icon="ArrowDownload16" Content="Download" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/releases/latest" />
</Grid>
<TextBlock Margin="0,24,0,0" FontSize="14" Text="{x:Static resources:Strings.Installer_Welcome_NextToContinue}" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding CanContinue, Mode=OneWay}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</ui:UiPage>

View File

@ -0,0 +1,33 @@
using System.Windows;
using Bloxstrap.UI.ViewModels.Installer;
namespace Bloxstrap.UI.Elements.Installer.Pages
{
/// <summary>
/// Interaction logic for WelcomePage.xaml
/// </summary>
public partial class WelcomePage
{
private readonly WelcomeViewModel _viewModel = new();
public WelcomePage()
{
_viewModel.CanContinueEvent += (_, _) =>
{
if (Window.GetWindow(this) is MainWindow window)
window.SetButtonEnabled("next", true);
};
DataContext = _viewModel;
InitializeComponent();
}
private void UiPage_Loaded(object sender, RoutedEventArgs e)
{
if (Window.GetWindow(this) is MainWindow window)
window.SetNextButtonText("Next");
_viewModel.DoChecks();
}
}
}

View File

@ -1,80 +0,0 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.InstallationPage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Menu.Pages"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:viewmodels="clr-namespace:Bloxstrap.UI.ViewModels.Menu"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
d:DataContext="{d:DesignInstance Type=viewmodels:InstallationViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="InstallationPage">
<StackPanel Margin="0,0,14,14">
<TextBlock Text="{x:Static resources:Strings.Menu_Installation_Description}" FontSize="14" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:CardExpander Margin="0,16,0,0" IsExpanded="True">
<ui:CardExpander.Style>
<Style TargetType="ui:CardExpander" BasedOn="{StaticResource {x:Type ui:CardExpander}}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Source={x:Static models:GlobalViewModel.IsNotFirstRun}}" Value="False">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</ui:CardExpander.Style>
<ui:CardExpander.Header>
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Installation_InstallLocation_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Installation_InstallLocation_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
</StackPanel>
</ui:CardExpander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Margin="0,0,4,0" Text="{Binding InstallLocation, Mode=TwoWay}" />
<ui:Button Grid.Column="1" Margin="4,0,4,0" Height="35" Icon="Folder24" Content="{x:Static resources:Strings.Common_Browse}" Command="{Binding BrowseInstallLocationCommand}" />
<ui:Button Grid.Column="2" Margin="4,0,0,0" Height="35" Icon="ArrowCounterclockwise24" Content="{x:Static resources:Strings.Common_Reset}" Command="{Binding ResetInstallLocationCommand}" />
</Grid>
</ui:CardExpander>
<Grid Margin="0,8,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Source={x:Static models:GlobalViewModel.IsNotFirstRun}}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:CardAction Grid.Column="0" x:Name="OpenFolderCardAction" Margin="0,0,4,0" Icon="Folder24" Command="{Binding OpenFolderCommand}" >
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Installation_OpenInstallFolder_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Installation_OpenInstallFolder_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" />
</StackPanel>
</ui:CardAction>
<ui:CardAction Grid.Column="1" Margin="4,0,0,0" Icon="UninstallApp24" Command="models:GlobalViewModel.OpenWebpageCommand" CommandParameter="https://github.com/pizzaboxer/bloxstrap/wiki/Uninstalling-Bloxstrap">
<StackPanel>
<TextBlock FontSize="14" Text="{x:Static resources:Strings.Menu_Installation_UninstallGuide_Title}" />
<TextBlock Margin="0,2,0,0" FontSize="12" Text="{x:Static resources:Strings.Menu_Installation_UninstallGuide_Description}" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap" />
</StackPanel>
</ui:CardAction>
</Grid>
</StackPanel>
</ui:UiPage>

View File

@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Bloxstrap.UI.ViewModels.Menu;
namespace Bloxstrap.UI.Elements.Menu.Pages
{
/// <summary>
/// Interaction logic for InstallationPage.xaml
/// </summary>
public partial class InstallationPage
{
public InstallationPage()
{
DataContext = new InstallationViewModel();
InitializeComponent();
}
}
}

View File

@ -1,38 +0,0 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.PreInstallPage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Menu.Pages"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="PreInstallPage"
Scrollable="True">
<Grid Margin="0,0,14,14">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="420" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="0,0,0,16" Text="{x:Static resources:Strings.Menu_PreInstall_Description}" FontSize="14" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Border Grid.Row="1" Grid.Column="0" Margin="0,0,16,0" BorderThickness="1" BorderBrush="{DynamicResource TextFillColorPrimaryBrush}">
<Image RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/Resources/Menu/StartMenu.png" />
</Border>
<StackPanel Grid.Row="1" Grid.Column="1">
<TextBlock FontSize="14" TextWrapping="Wrap" Text="{x:Static resources:Strings.Menu_PreInstall_Info_1}" />
<controls:MarkdownTextBlock Margin="0,16,0,0" FontSize="14" TextWrapping="Wrap" MarkdownText="{Binding Info2Text, Mode=OneTime}" />
</StackPanel>
</Grid>
</ui:UiPage>

View File

@ -1,16 +0,0 @@
using Bloxstrap.UI.ViewModels.Menu;
namespace Bloxstrap.UI.Elements.Menu.Pages
{
/// <summary>
/// Interaction logic for PreInstallPage.xaml
/// </summary>
public partial class PreInstallPage
{
public PreInstallPage()
{
DataContext = new PreInstallViewModel();
InitializeComponent();
}
}
}

View File

@ -1,10 +1,10 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.Menu.MainWindow"
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.Settings.MainWindow"
x:Name="ConfigurationWindow"
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.Menu.Pages"
xmlns:pages="clr-namespace:Bloxstrap.UI.Elements.Settings.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
@ -48,17 +48,15 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:NavigationFluent x:Name="RootNavigation" Grid.Row="1" Grid.Column="0" Margin="0,0,12,0" Frame="{Binding ElementName=RootFrame}" SelectedPageIndex="0" Visibility="{Binding NavigationVisibility, Mode=OneWay}">
<ui:NavigationFluent x:Name="RootNavigation" Grid.Row="1" Grid.Column="0" Margin="0,0,12,0" Frame="{Binding ElementName=RootFrame}" SelectedPageIndex="0">
<ui:NavigationFluent.Items>
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_Integrations_Title}" PageType="{x:Type pages:IntegrationsPage}" Icon="Add28" Tag="integrations" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_Mods_Title}" PageType="{x:Type pages:ModsPage}" Icon="WrenchScrewdriver20" Tag="mods" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_FastFlags_Title}" PageType="{x:Type pages:FastFlagsPage}" Icon="Flag24" Tag="fastflags" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_Appearance_Title}" PageType="{x:Type pages:AppearancePage}" Icon="PaintBrush24" Tag="appearance" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_Behaviour_Title}" PageType="{x:Type pages:BehaviourPage}" Icon="Settings24" Tag="behaviour" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_Installation_Title}" PageType="{x:Type pages:InstallationPage}" Icon="HardDrive20" Tag="installation" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_FastFlagEditor_Title}" PageType="{x:Type pages:FastFlagEditorPage}" Tag="fastflageditor" Visibility="Collapsed" />
<ui:NavigationItem Content="{x:Static resources:Strings.Menu_PreInstall_Title}" PageType="{x:Type pages:PreInstallPage}" Tag="preinstall" Visibility="Collapsed" x:Name="PreInstallNavItem" />
<ui:NavigationItem Content="" PageType="{x:Type pages:FastFlagEditorWarningPage}" Tag="fastflageditorwarning" Visibility="Collapsed" x:Name="EditorWarningNavItem" />
</ui:NavigationFluent.Items>
<ui:NavigationFluent.Footer>
@ -92,10 +90,10 @@
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Grid.Column="1" Padding="0,0,4,0">
<ui:Button Content="{Binding ConfirmButtonText, Mode=OneTime}" Appearance="Primary" Command="{Binding ConfirmSettingsCommand, Mode=OneWay}" IsEnabled="{Binding ConfirmButtonEnabled, Mode=OneWay}" />
<ui:Button Content="{x:Static resources:Strings.Menu_Save}" Appearance="Primary" Command="{Binding SaveSettingsCommand, Mode=OneWay}" IsEnabled="{Binding ConfirmButtonEnabled, Mode=OneWay}" />
</StatusBarItem>
<StatusBarItem Grid.Column="2" Padding="4,0,0,0">
<ui:Button Content="{Binding CloseButtonText, Mode=OneTime}" Command="{Binding CloseWindowCommand, Mode=OneWay}" />
<ui:Button Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
</StatusBarItem>
</StatusBar>
</Grid>

View File

@ -1,9 +1,9 @@
using System.Windows.Controls;
using Wpf.Ui.Controls.Interfaces;
using Wpf.Ui.Mvvm.Contracts;
using Bloxstrap.UI.ViewModels.Menu;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Menu
namespace Bloxstrap.UI.Elements.Settings
{
/// <summary>
/// Interaction logic for MainWindow.xaml
@ -12,30 +12,30 @@ namespace Bloxstrap.UI.Elements.Menu
{
public MainWindow(bool showAlreadyRunningWarning)
{
var viewModel = new MainWindowViewModel();
viewModel.RequestSaveNoticeEvent += (_, _) => SettingsSavedSnackbar.Show();
DataContext = viewModel;
InitializeComponent();
ApplyTheme();
App.Logger.WriteLine("MainWindow::MainWindow", "Initializing menu");
DataContext = new MainWindowViewModel(this);
#if DEBUG // easier access
PreInstallNavItem.Visibility = System.Windows.Visibility.Visible;
EditorWarningNavItem.Visibility = System.Windows.Visibility.Visible;
#endif
if (showAlreadyRunningWarning)
_ = ShowAlreadyRunningSnackbar();
ShowAlreadyRunningSnackbar();
}
private async Task ShowAlreadyRunningSnackbar()
private async void ShowAlreadyRunningSnackbar()
{
await Task.Delay(500); // wait for everything to finish loading
AlreadyRunningSnackbar.Show();
}
public void OpenWiki(object? sender, EventArgs e) => Utilities.ShellExecute($"https://github.com/{App.ProjectRepository}/wiki");
#region INavigationWindow methods
public Frame GetFrame() => RootFrame;

View File

@ -1,4 +1,4 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.AboutPage"
<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"

View File

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

View File

@ -1,4 +1,4 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.AppearancePage"
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.AppearancePage"
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"
@ -28,16 +28,6 @@
<controls:OptionControl
Header="{x:Static resources:Strings.Menu_Appearance_Language_Title}"
Description="{x:Static resources:Strings.Menu_Appearance_Language_Description}">
<controls:OptionControl.Style>
<Style TargetType="controls:OptionControl">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Source={x:Static models:GlobalViewModel.IsNotFirstRun}, Mode=OneTime}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</controls:OptionControl.Style>
<ComboBox Width="200" Padding="10,5,10,5" ItemsSource="{Binding Languages, Mode=OneTime}" Text="{Binding SelectedLanguage, Mode=TwoWay}" />
</controls:OptionControl>

View File

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

View File

@ -1,12 +1,12 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.BehaviourPage"
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.BehaviourPage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Menu.Pages"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Settings.Pages"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Menu"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Settings"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
d:DataContext="{d:DesignInstance Type=models:BehaviourViewModel}"
mc:Ignorable="d"

View File

@ -13,9 +13,9 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Bloxstrap.UI.ViewModels.Menu;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Menu.Pages
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
/// Interaction logic for BehaviourPage.xaml

View File

@ -1,10 +1,10 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.FastFlagEditorPage"
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.FastFlagEditorPage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Menu.Pages"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.Settings.Pages"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"

View File

@ -9,7 +9,7 @@ using Bloxstrap.UI.Elements.Dialogs;
using Newtonsoft.Json.Linq;
using System.Xml.Linq;
namespace Bloxstrap.UI.Elements.Menu.Pages
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
/// Interaction logic for FastFlagEditorPage.xaml

View File

@ -1,11 +1,11 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.FastFlagEditorWarningPage"
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.FastFlagEditorWarningPage"
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:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Menu"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.Settings"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Scrollable="True"

View File

@ -1,7 +1,7 @@
using Bloxstrap.UI.ViewModels.Menu;
using Bloxstrap.UI.ViewModels.Settings;
using System.Windows;
namespace Bloxstrap.UI.Elements.Menu.Pages
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
/// Interaction logic for FastFlagEditorWarningPage.xaml

View File

@ -1,4 +1,4 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.FastFlagsPage"
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.FastFlagsPage"
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"

View File

@ -1,9 +1,9 @@
using System.Windows;
using System.Windows.Input;
using Bloxstrap.UI.ViewModels.Menu;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Menu.Pages
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
/// Interaction logic for FastFlagsPage.xaml

View File

@ -1,4 +1,4 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.IntegrationsPage"
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.IntegrationsPage"
x:Name="IntegrationsPageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

View File

@ -1,8 +1,8 @@
using System.Windows.Controls;
using Bloxstrap.UI.ViewModels.Menu;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Menu.Pages
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
/// Interaction logic for IntegrationsPage.xaml

View File

@ -1,4 +1,4 @@
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Menu.Pages.ModsPage"
<ui:UiPage x:Class="Bloxstrap.UI.Elements.Settings.Pages.ModsPage"
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"
@ -23,34 +23,10 @@
<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}" IsEnabled="{Binding Source={x:Static models:GlobalViewModel.IsNotFirstRun}, Mode=OneTime}">
<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">
<!--this is so fucking stupid the disabled state of the cardaction doesnt change the header text colour-->
<TextBlock.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=OpenModFolderCardAction, Path=IsEnabled, Mode=OneTime}" Value="False">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Margin="0,2,0,0" FontSize="12" Foreground="{DynamicResource TextFillColorTertiaryBrush}" TextWrapping="Wrap">
<TextBlock.Style>
<Style>
<Setter Property="TextBlock.Text" Value="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_Description}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=OpenModFolderCardAction, Path=IsEnabled, Mode=OneTime}" Value="False">
<Setter Property="TextBlock.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
<Setter Property="TextBlock.Text" Value="{x:Static resources:Strings.Menu_Mods_OpenModsFolder_MustBeInstalled}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<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">

View File

@ -1,8 +1,8 @@
using System.Windows;
using Bloxstrap.UI.ViewModels.Menu;
using Bloxstrap.UI.ViewModels.Settings;
namespace Bloxstrap.UI.Elements.Menu.Pages
namespace Bloxstrap.UI.Elements.Settings.Pages
{
/// <summary>
/// Interaction logic for ModsPage.xaml

View File

@ -2,16 +2,14 @@
using Bloxstrap.UI.Elements.Bootstrapper;
using Bloxstrap.UI.Elements.Dialogs;
using Bloxstrap.UI.Elements.Menu;
using Bloxstrap.UI.Elements.Settings;
using Bloxstrap.UI.Elements.Installer;
using System.Drawing;
namespace Bloxstrap.UI
{
static class Frontend
{
public static void ShowLanguageSelection() => new LanguageSelectorDialog().ShowDialog();
public static void ShowMenu(bool showAlreadyRunningWarning = false) => new MainWindow(showAlreadyRunningWarning).ShowDialog();
public static MessageBoxResult ShowMessageBox(string message, MessageBoxImage icon = MessageBoxImage.None, MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxResult defaultResult = MessageBoxResult.None)
{
App.Logger.WriteLine("Frontend::ShowMessageBox", message);
@ -19,18 +17,16 @@ namespace Bloxstrap.UI
if (App.LaunchSettings.IsQuiet)
return defaultResult;
if (!App.LaunchSettings.IsRobloxLaunch)
return ShowFluentMessageBox(message, icon, buttons);
switch (App.Settings.Prop.BootstrapperStyle)
{
case BootstrapperStyle.FluentDialog:
case BootstrapperStyle.ClassicFluentDialog:
case BootstrapperStyle.FluentAeroDialog:
case BootstrapperStyle.ByfronDialog:
return Application.Current.Dispatcher.Invoke(new Func<MessageBoxResult>(() =>
{
var messagebox = new FluentMessageBox(message, icon, buttons);
messagebox.ShowDialog();
return messagebox.Result;
}));
return ShowFluentMessageBox(message, icon, buttons);
default:
return MessageBox.Show(message, App.ProjectName, buttons, icon);
@ -68,5 +64,15 @@ namespace Bloxstrap.UI
_ => new FluentDialog(false)
};
}
private static MessageBoxResult ShowFluentMessageBox(string message, MessageBoxImage icon, MessageBoxButton buttons)
{
return Application.Current.Dispatcher.Invoke(new Func<MessageBoxResult>(() =>
{
var messagebox = new FluentMessageBox(message, icon, buttons);
messagebox.ShowDialog();
return messagebox.Result;
}));
}
}
}

View File

@ -0,0 +1,23 @@
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.ViewModels.Installer
{
// TODO: have it so it shows "Launch Roblox"/"Install and Launch Roblox" depending on state of /App/ folder
public class LaunchMenuViewModel
{
public string Version => string.Format(Strings.Menu_About_Version, App.Version);
public ICommand LaunchSettingsCommand => new RelayCommand(LaunchSettings);
public ICommand LaunchRobloxCommand => new RelayCommand(LaunchRoblox);
public event EventHandler<NextAction>? CloseWindowRequest;
private void LaunchSettings() => CloseWindowRequest?.Invoke(this, NextAction.LaunchSettings);
private void LaunchRoblox() => CloseWindowRequest?.Invoke(this, NextAction.LaunchRoblox);
}
}

View File

@ -0,0 +1,24 @@
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.ViewModels.Dialogs
{
public class UninstallerViewModel
{
public string Text => String.Format(
Strings.Uninstaller_Text,
"https://github.com/pizzaboxer/bloxstrap/wiki/Roblox-crashes-or-does-not-launch",
Paths.Base
);
public bool KeepData { get; set; } = true;
public ICommand ConfirmUninstallCommand => new RelayCommand(ConfirmUninstall);
public event EventHandler? ConfirmUninstallRequest;
private void ConfirmUninstall() => ConfirmUninstallRequest?.Invoke(this, new EventArgs());
}
}

View File

@ -7,8 +7,6 @@ namespace Bloxstrap.UI.ViewModels
{
public static ICommand OpenWebpageCommand => new RelayCommand<string>(OpenWebpage);
public static bool IsNotFirstRun => !App.IsFirstRun;
private static void OpenWebpage(string? location)
{
if (location is null)

View File

@ -0,0 +1,23 @@
using System.Windows;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Resources;
using Microsoft.Win32;
namespace Bloxstrap.UI.ViewModels.Installer
{
public class CompletionViewModel
{
public ICommand LaunchSettingsCommand => new RelayCommand(LaunchSettings);
public ICommand LaunchRobloxCommand => new RelayCommand(LaunchRoblox);
public event EventHandler<NextAction>? CloseWindowRequest;
private void LaunchSettings() => CloseWindowRequest?.Invoke(this, NextAction.LaunchSettings);
private void LaunchRoblox() => CloseWindowRequest?.Invoke(this, NextAction.LaunchRoblox);
}
}

View File

@ -0,0 +1,111 @@
using System.Windows;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Resources;
using Microsoft.Win32;
using Wpf.Ui.Mvvm.Interfaces;
using System.ComponentModel;
namespace Bloxstrap.UI.ViewModels.Installer
{
public class InstallViewModel : NotifyPropertyChangedViewModel
{
private readonly Bloxstrap.Installer installer = new();
private readonly string _originalInstallLocation;
public EventHandler<bool>? SetCanContinueEvent;
public string InstallLocation
{
get => installer.InstallLocation;
set
{
if (!String.IsNullOrEmpty(ErrorMessage))
{
SetCanContinueEvent?.Invoke(this, true);
installer.InstallLocationError = "";
OnPropertyChanged(nameof(ErrorMessage));
}
installer.InstallLocation = value;
CheckExistingData();
}
}
public Visibility DataFoundMessageVisibility { get; set; } = Visibility.Collapsed;
public string ErrorMessage => installer.InstallLocationError;
public bool CreateDesktopShortcuts
{
get => installer.CreateDesktopShortcuts;
set => installer.CreateDesktopShortcuts = value;
}
public bool CreateStartMenuShortcuts
{
get => installer.CreateStartMenuShortcuts;
set => installer.CreateStartMenuShortcuts = value;
}
public ICommand BrowseInstallLocationCommand => new RelayCommand(BrowseInstallLocation);
public ICommand ResetInstallLocationCommand => new RelayCommand(ResetInstallLocation);
public ICommand OpenFolderCommand => new RelayCommand(OpenFolder);
public InstallViewModel()
{
_originalInstallLocation = installer.InstallLocation;
CheckExistingData();
}
public bool DoInstall()
{
if (!installer.CheckInstallLocation())
{
SetCanContinueEvent?.Invoke(this, false);
OnPropertyChanged(nameof(ErrorMessage));
return false;
}
installer.DoInstall();
return true;
}
public void CheckExistingData()
{
if (File.Exists(Path.Combine(InstallLocation, "Settings.json")))
DataFoundMessageVisibility = Visibility.Visible;
else
DataFoundMessageVisibility = Visibility.Collapsed;
OnPropertyChanged(nameof(DataFoundMessageVisibility));
}
private void BrowseInstallLocation()
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
return;
InstallLocation = dialog.SelectedPath;
OnPropertyChanged(nameof(InstallLocation));
}
private void ResetInstallLocation()
{
InstallLocation = _originalInstallLocation;
OnPropertyChanged(nameof(InstallLocation));
}
private void OpenFolder() => Process.Start("explorer.exe", Paths.Base);
}
}

View File

@ -0,0 +1,52 @@
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.Resources;
namespace Bloxstrap.UI.ViewModels.Installer
{
public class MainWindowViewModel : NotifyPropertyChangedViewModel
{
public string NextButtonText { get; private set; } = Strings.Common_Navigation_Next;
public bool BackButtonEnabled { get; private set; } = false;
public bool NextButtonEnabled { get; private set; } = false;
public ICommand BackPageCommand => new RelayCommand(BackPage);
public ICommand NextPageCommand => new RelayCommand(NextPage);
public ICommand CloseWindowCommand => new RelayCommand(CloseWindow);
public event EventHandler<string>? PageRequest;
public event EventHandler? CloseWindowRequest;
public void SetButtonEnabled(string type, bool state)
{
if (type == "next")
{
NextButtonEnabled = state;
OnPropertyChanged(nameof(NextButtonEnabled));
}
else if (type == "back")
{
BackButtonEnabled = state;
OnPropertyChanged(nameof(BackButtonEnabled));
}
}
public void SetNextButtonText(string text)
{
NextButtonText = text;
OnPropertyChanged(nameof(NextButtonText));
}
private void BackPage() => PageRequest?.Invoke(this, "back");
private void NextPage() => PageRequest?.Invoke(this, "next");
private void CloseWindow() => CloseWindowRequest?.Invoke(this, new EventArgs());
}
}

View File

@ -0,0 +1,56 @@
namespace Bloxstrap.UI.ViewModels.Installer
{
// TODO: administrator check?
public class WelcomeViewModel : NotifyPropertyChangedViewModel
{
// formatting is done here instead of in xaml, it's just a bit easier
public string MainText => String.Format(
Resources.Strings.Installer_Welcome_MainText,
"[github.com/pizzaboxer/bloxstrap](https://github.com/pizzaboxer/bloxstrap)",
"[bloxstrap.pizzaboxer.xyz](https://bloxstrap.pizzaboxer.xyz)"
);
public string VersionNotice { get; private set; } = "";
public bool CanContinue { get; set; } = false;
public event EventHandler? CanContinueEvent;
// called by codebehind on page load
public async void DoChecks()
{
const string LOG_IDENT = "WelcomeViewModel::DoChecks";
// TODO: move into unified function that bootstrapper can use too
GithubRelease? releaseInfo = null;
try
{
releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{App.ProjectRepository}/releases/latest");
if (releaseInfo is null || releaseInfo.Assets is null)
{
App.Logger.WriteLine(LOG_IDENT, $"Encountered invalid data when fetching GitHub releases");
}
else
{
if (Utilities.CompareVersions(App.Version, releaseInfo.TagName) == VersionComparison.LessThan)
{
VersionNotice = String.Format(Resources.Strings.Installer_Welcome_UpdateNotice, App.Version, releaseInfo.TagName.Replace("v", ""));
OnPropertyChanged(nameof(VersionNotice));
}
}
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Error occurred when fetching GitHub releases");
App.Logger.WriteException(LOG_IDENT, ex);
}
CanContinue = true;
OnPropertyChanged(nameof(CanContinue));
CanContinueEvent?.Invoke(this, new EventArgs());
}
}
}

View File

@ -1,43 +0,0 @@
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.UI.ViewModels.Menu
{
public class InstallationViewModel : NotifyPropertyChangedViewModel
{
private string _originalInstallLocation = App.BaseDirectory;
public ICommand BrowseInstallLocationCommand => new RelayCommand(BrowseInstallLocation);
public ICommand ResetInstallLocationCommand => new RelayCommand(ResetInstallLocation);
public ICommand OpenFolderCommand => new RelayCommand(OpenFolder);
private void BrowseInstallLocation()
{
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
if (dialog.ShowDialog() != System.Windows.Forms.DialogResult.OK)
return;
InstallLocation = dialog.SelectedPath;
OnPropertyChanged(nameof(InstallLocation));
}
private void ResetInstallLocation()
{
InstallLocation = _originalInstallLocation;
OnPropertyChanged(nameof(InstallLocation));
}
private void OpenFolder()
{
Process.Start("explorer.exe", Paths.Base);
}
public string InstallLocation
{
get => App.BaseDirectory;
set => App.BaseDirectory = value;
}
}
}

View File

@ -1,139 +0,0 @@
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using CommunityToolkit.Mvvm.Input;
using Wpf.Ui.Mvvm.Contracts;
using Bloxstrap.UI.Elements.Menu;
using Bloxstrap.UI.Elements.Menu.Pages;
namespace Bloxstrap.UI.ViewModels.Menu
{
public class MainWindowViewModel : NotifyPropertyChangedViewModel
{
private readonly MainWindow _window;
public ICommand CloseWindowCommand => new RelayCommand(CloseWindow);
public ICommand ConfirmSettingsCommand => new RelayCommand(ConfirmSettings);
public Visibility NavigationVisibility { get; set; } = Visibility.Visible;
public string ConfirmButtonText => App.IsFirstRun ? Resources.Strings.Menu_Install : Resources.Strings.Menu_Save;
public string CloseButtonText => App.IsFirstRun ? Resources.Strings.Common_Cancel : Resources.Strings.Common_Close;
public bool ConfirmButtonEnabled { get; set; } = true;
public MainWindowViewModel(MainWindow window)
{
_window = window;
}
private void CloseWindow() => _window.Close();
private void ConfirmSettings()
{
if (!App.IsFirstRun)
{
App.ShouldSaveConfigs = true;
App.Settings.Save();
App.State.Save();
App.FastFlags.Save();
App.ShouldSaveConfigs = false;
_window.SettingsSavedSnackbar.Show();
return;
}
if (string.IsNullOrEmpty(App.BaseDirectory))
{
Frontend.ShowMessageBox(Resources.Strings.Menu_InstallLocation_NotSet, MessageBoxImage.Error);
return;
}
if (NavigationVisibility == Visibility.Visible)
{
try
{
// check if we can write to the directory (a bit hacky but eh)
string testFile = Path.Combine(App.BaseDirectory, $"{App.ProjectName}WriteTest.txt");
Directory.CreateDirectory(App.BaseDirectory);
File.WriteAllText(testFile, "hi");
File.Delete(testFile);
}
catch (UnauthorizedAccessException)
{
Frontend.ShowMessageBox(
Resources.Strings.Menu_InstallLocation_NoWritePerms,
MessageBoxImage.Error
);
return;
}
catch (Exception ex)
{
Frontend.ShowMessageBox(ex.Message, MessageBoxImage.Error);
return;
}
if (!App.BaseDirectory.EndsWith(App.ProjectName) && Directory.Exists(App.BaseDirectory) && Directory.EnumerateFileSystemEntries(App.BaseDirectory).Any())
{
string suggestedChange = Path.Combine(App.BaseDirectory, App.ProjectName);
MessageBoxResult result = Frontend.ShowMessageBox(
string.Format(Resources.Strings.Menu_InstallLocation_NotEmpty, suggestedChange),
MessageBoxImage.Warning,
MessageBoxButton.YesNoCancel,
MessageBoxResult.Yes
);
if (result == MessageBoxResult.Yes)
App.BaseDirectory = suggestedChange;
else if (result == MessageBoxResult.Cancel)
return;
}
if (
App.BaseDirectory.Length <= 3 || // prevent from installing to the root of a drive
App.BaseDirectory.StartsWith("\\\\") || // i actually haven't encountered anyone doing this and i dont even know if this is possible but this is just to be safe lmao
App.BaseDirectory.ToLowerInvariant().Contains("onedrive") || // prevent from installing to a onedrive folder
Directory.GetParent(App.BaseDirectory)!.ToString().ToLowerInvariant() == Paths.UserProfile.ToLowerInvariant() // prevent from installing to an essential user profile folder
)
{
Frontend.ShowMessageBox(
Resources.Strings.Menu_InstallLocation_CantInstall,
MessageBoxImage.Error,
MessageBoxButton.OK
);
return;
}
}
if (NavigationVisibility == Visibility.Visible)
{
((INavigationWindow)_window).Navigate(typeof(PreInstallPage));
NavigationVisibility = Visibility.Collapsed;
OnPropertyChanged(nameof(NavigationVisibility));
ConfirmButtonEnabled = false;
OnPropertyChanged(nameof(ConfirmButtonEnabled));
Task.Run(async delegate
{
await Task.Delay(3000);
ConfirmButtonEnabled = true;
OnPropertyChanged(nameof(ConfirmButtonEnabled));
});
}
else
{
App.IsSetupComplete = true;
CloseWindow();
}
}
}
}

View File

@ -1,14 +0,0 @@
namespace Bloxstrap.UI.ViewModels.Menu
{
class PreInstallViewModel
{
public string Info2Text
{
get => string.Format(
Resources.Strings.Menu_PreInstall_Info_2,
"https://www.github.com/pizzaboxer/bloxstrap/wiki",
"https://www.github.com/pizzaboxer/bloxstrap/issues",
"https://discord.gg/nKjV3mGq6R");
}
}
}

View File

@ -1,6 +1,6 @@
using System.Windows;
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Settings
{
public class AboutViewModel
{

View File

@ -7,9 +7,9 @@ using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using Bloxstrap.UI.Elements.Menu;
using Bloxstrap.UI.Elements.Settings;
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Settings
{
public class AppearanceViewModel : NotifyPropertyChangedViewModel
{

View File

@ -1,4 +1,4 @@
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Settings
{
public class BehaviourViewModel : NotifyPropertyChangedViewModel
{

View File

@ -5,9 +5,9 @@ using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
using Wpf.Ui.Mvvm.Contracts;
using Bloxstrap.UI.Elements.Menu.Pages;
using Bloxstrap.UI.Elements.Settings.Pages;
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Settings
{
internal class FastFlagEditorWarningViewModel : NotifyPropertyChangedViewModel
{

View File

@ -6,10 +6,10 @@ using Wpf.Ui.Mvvm.Contracts;
using CommunityToolkit.Mvvm.Input;
using Bloxstrap.UI.Elements.Menu.Pages;
using Bloxstrap.UI.Elements.Settings.Pages;
using Bloxstrap.Enums.FlagPresets;
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Settings
{
public class FastFlagsViewModel : NotifyPropertyChangedViewModel
{

View File

@ -7,7 +7,7 @@ using Microsoft.Win32;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Settings
{
public class IntegrationsViewModel : NotifyPropertyChangedViewModel
{

View File

@ -0,0 +1,22 @@
using System.Windows.Input;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.UI.ViewModels.Settings
{
public class MainWindowViewModel : NotifyPropertyChangedViewModel
{
public ICommand SaveSettingsCommand => new RelayCommand(SaveSettings);
public EventHandler? RequestSaveNoticeEvent;
private void SaveSettings()
{
App.Settings.Save();
App.State.Save();
App.FastFlags.Save();
RequestSaveNoticeEvent?.Invoke(this, new EventArgs());
}
}
}

View File

@ -5,13 +5,13 @@ using Microsoft.Win32;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.UI.ViewModels.Menu
namespace Bloxstrap.UI.ViewModels.Settings
{
public class ModsViewModel : NotifyPropertyChangedViewModel
{
private void OpenModsFolder() => Process.Start("explorer.exe", Paths.Modifications);
private bool _usingCustomFont => App.IsFirstRun && App.CustomFontLocation is not null || !App.IsFirstRun && File.Exists(Paths.CustomFont);
private bool _usingCustomFont => File.Exists(Paths.CustomFont);
private readonly Dictionary<string, byte[]> FontHeaders = new()
{
@ -24,15 +24,8 @@ namespace Bloxstrap.UI.ViewModels.Menu
{
if (_usingCustomFont)
{
if (App.IsFirstRun)
{
App.CustomFontLocation = null;
}
else
{
Filesystem.AssertReadOnly(Paths.CustomFont);
File.Delete(Paths.CustomFont);
}
Filesystem.AssertReadOnly(Paths.CustomFont);
File.Delete(Paths.CustomFont);
}
else
{
@ -51,17 +44,10 @@ namespace Bloxstrap.UI.ViewModels.Menu
Frontend.ShowMessageBox(Resources.Strings.Menu_Mods_Misc_CustomFont_Invalid, MessageBoxImage.Error);
return;
}
if (App.IsFirstRun)
{
App.CustomFontLocation = dialog.FileName;
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!);
File.Copy(dialog.FileName, Paths.CustomFont);
Filesystem.AssertReadOnly(Paths.CustomFont);
}
Directory.CreateDirectory(Path.GetDirectoryName(Paths.CustomFont)!);
File.Copy(dialog.FileName, Paths.CustomFont);
Filesystem.AssertReadOnly(Paths.CustomFont);
}
OnPropertyChanged(nameof(ChooseCustomFontVisibility));

View File

@ -49,12 +49,12 @@ namespace Bloxstrap
/// 0: version1 == version2 <br />
/// 1: version1 &gt; version2
/// </returns>
public static int CompareVersions(string versionStr1, string versionStr2)
public static VersionComparison CompareVersions(string versionStr1, string versionStr2)
{
var version1 = new Version(versionStr1.Replace("v", ""));
var version2 = new Version(versionStr2.Replace("v", ""));
return version1.CompareTo(version2);
return (VersionComparison)version1.CompareTo(version2);
}
public static string GetRobloxVersion(bool studio)

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap
namespace Bloxstrap.Utility
{
public class InterProcessLock : IDisposable
{

View File

@ -1,4 +1,5 @@
using System.Windows;
using Bloxstrap.Resources;
namespace Bloxstrap.Utility
{
@ -30,10 +31,7 @@ namespace Bloxstrap.Utility
_loadStatus = GenericTriState.Failed;
Frontend.ShowMessageBox(
$"{App.ProjectName} was unable to create shortcuts for the Desktop and Start menu. They will be created the next time Roblox is launched.",
MessageBoxImage.Information
);
Frontend.ShowMessageBox(Strings.Dialog_CannotCreateShortcuts, MessageBoxImage.Information);
}
}
}